adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
486 lines
10 KiB
Svelte
486 lines
10 KiB
Svelte
<script context="module" lang="ts">
|
|
import { TABS, type Tab, type TabSelectData } from "./Tabs.svelte";
|
|
|
|
function is_visible_tab(tab: Tab | null | undefined): tab is Tab {
|
|
return !!tab && tab.visible !== false && tab.visible !== "hidden";
|
|
}
|
|
|
|
function find_tab_index(tabs: (Tab | null)[], id: string | number): number {
|
|
const index = tabs.findIndex((t) => t?.id === id);
|
|
return index === -1 ? 0 : index;
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { setContext, tick } from "svelte";
|
|
import { writable } from "svelte/store";
|
|
|
|
let {
|
|
visible = true,
|
|
elem_id = "",
|
|
elem_classes = [],
|
|
selected = $bindable(),
|
|
initial_tabs,
|
|
onchange,
|
|
onselect
|
|
}: {
|
|
visible?: boolean | "hidden";
|
|
elem_id?: string;
|
|
elem_classes?: string[];
|
|
selected: number | string;
|
|
initial_tabs: Tab[];
|
|
onchange?: () => void;
|
|
onselect?: (data: TabSelectData) => void;
|
|
} = $props();
|
|
|
|
let tabs = $state<(Tab | null)[]>([...initial_tabs]);
|
|
let stepper_container: HTMLDivElement;
|
|
let show_labels_for_all = $state(true);
|
|
let measurement_container: HTMLDivElement;
|
|
let step_buttons: HTMLButtonElement[] = $state([]);
|
|
let step_labels: HTMLSpanElement[] = $state([]);
|
|
let label_height = $state(0);
|
|
let compact = $state(false);
|
|
let recompute_overflow = $state(true);
|
|
|
|
let has_tabs = $derived(tabs.length > 0);
|
|
|
|
const selected_tab = writable<false | number | string>(
|
|
selected ?? tabs[0]?.id ?? false
|
|
);
|
|
const selected_tab_index = writable<number>(find_tab_index(tabs, selected));
|
|
|
|
async function check_overflow(): Promise<void> {
|
|
if (!stepper_container || !measurement_container || !recompute_overflow)
|
|
return;
|
|
recompute_overflow = false;
|
|
await tick();
|
|
|
|
show_labels_for_all = true;
|
|
await tick();
|
|
|
|
const SEP_WIDTH = 50;
|
|
const button_width =
|
|
step_buttons[0].getBoundingClientRect().width * step_buttons.length +
|
|
SEP_WIDTH * (step_buttons.length - 1);
|
|
|
|
const containerWidth = stepper_container.getBoundingClientRect().width;
|
|
const does_it_fit = button_width < containerWidth;
|
|
|
|
if (!does_it_fit) {
|
|
show_labels_for_all = false;
|
|
compact = true;
|
|
return;
|
|
}
|
|
|
|
let max_height = 0;
|
|
let is_overlapping = false;
|
|
let last_right = 0;
|
|
|
|
for (const label of step_labels) {
|
|
const { height, width, left, right } = label.getBoundingClientRect();
|
|
if (height > max_height) {
|
|
max_height = height;
|
|
}
|
|
if (last_right && left - 10 < last_right && !is_overlapping) {
|
|
is_overlapping = true;
|
|
}
|
|
last_right = right;
|
|
}
|
|
label_height = max_height;
|
|
|
|
if (is_overlapping) {
|
|
show_labels_for_all = false;
|
|
}
|
|
}
|
|
|
|
let last_width = 0;
|
|
|
|
$effect(() => {
|
|
check_overflow();
|
|
|
|
const observer = new ResizeObserver((entries) => {
|
|
if (entries[0].contentRect.width === last_width) return;
|
|
last_width = entries[0].contentRect.width;
|
|
compact = false;
|
|
recompute_overflow = true;
|
|
check_overflow();
|
|
});
|
|
|
|
if (stepper_container) {
|
|
observer.observe(stepper_container);
|
|
}
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
});
|
|
|
|
setContext(TABS, {
|
|
register_tab: (tab: Tab, order: number) => {
|
|
tabs[order] = tab;
|
|
|
|
if ($selected_tab === false && is_visible_tab(tab) && tab.interactive) {
|
|
$selected_tab = tab.id;
|
|
$selected_tab_index = order;
|
|
}
|
|
return order;
|
|
},
|
|
unregister_tab: (tab: Tab, order: number) => {
|
|
if ($selected_tab === tab.id) {
|
|
$selected_tab = tabs[0]?.id ?? false;
|
|
}
|
|
tabs[order] = null;
|
|
},
|
|
selected_tab,
|
|
selected_tab_index
|
|
});
|
|
|
|
function change_tab(id: string | number | undefined, index: number): void {
|
|
const tab_to_activate = tabs.find((t) => t?.id === id);
|
|
if (
|
|
id !== undefined &&
|
|
tab_to_activate &&
|
|
tab_to_activate.interactive &&
|
|
is_visible_tab(tab_to_activate) &&
|
|
$selected_tab !== tab_to_activate.id
|
|
) {
|
|
selected = id;
|
|
$selected_tab = id;
|
|
$selected_tab_index = tabs.findIndex((t) => t?.id === id);
|
|
onchange?.();
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
tabs;
|
|
if (selected !== null) {
|
|
change_tab(
|
|
selected,
|
|
tabs.findIndex((t) => t?.id === selected)
|
|
);
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
tabs;
|
|
check_overflow();
|
|
});
|
|
|
|
$effect(() => {
|
|
$selected_tab_index;
|
|
check_overflow();
|
|
});
|
|
|
|
let tab_scale = $derived(
|
|
tabs[$selected_tab_index >= 0 ? $selected_tab_index : 0]?.scale
|
|
);
|
|
</script>
|
|
|
|
<svelte:window onresize={check_overflow} />
|
|
|
|
<div
|
|
class="stepper {elem_classes.join(' ')}"
|
|
class:hide={visible === false}
|
|
class:hidden={visible === "hidden"}
|
|
id={elem_id}
|
|
style:flex-grow={tab_scale}
|
|
class:compact
|
|
>
|
|
{#if has_tabs}
|
|
{#if compact}
|
|
<p class="step-title">
|
|
<strong>Step {($selected_tab_index || 0) + 1}/{tabs.length}:</strong>
|
|
{tabs[$selected_tab_index]?.label || "Walkthrough"}
|
|
</p>
|
|
{/if}
|
|
<div
|
|
class="stepper-wrapper"
|
|
bind:this={stepper_container}
|
|
style:--label-height={label_height + "px"}
|
|
>
|
|
<div
|
|
class="stepper-container"
|
|
bind:this={measurement_container}
|
|
role="tablist"
|
|
>
|
|
{#each tabs as t, i}
|
|
{#if is_visible_tab(t)}
|
|
<div class="step-item">
|
|
<button
|
|
bind:this={step_buttons[i]}
|
|
role="tab"
|
|
class="step-button"
|
|
class:active={t.id === $selected_tab}
|
|
class:completed={t.id < $selected_tab}
|
|
class:pending={t.id > $selected_tab}
|
|
aria-selected={t.id === $selected_tab}
|
|
aria-controls={t.elem_id}
|
|
disabled={!t.interactive || i > $selected_tab_index}
|
|
aria-disabled={!t.interactive || i > $selected_tab_index}
|
|
id={t.elem_id ? t.elem_id + "-button" : null}
|
|
data-tab-id={t.id}
|
|
onclick={() => {
|
|
if (i <= $selected_tab_index && t.id !== $selected_tab) {
|
|
change_tab(t.id, i);
|
|
onselect?.({
|
|
value: t.label,
|
|
index: i,
|
|
id: t.id,
|
|
component_id: t.component_id
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
<span class="step-number">
|
|
{#if t.id < $selected_tab}
|
|
<svg
|
|
width="12"
|
|
height="10"
|
|
viewBox="0 0 12 10"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
d="M1 5L4.5 8.5L11 1.5"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
</svg>
|
|
{:else}
|
|
{i + 1}
|
|
{/if}
|
|
</span>
|
|
{#if !compact}
|
|
<span
|
|
bind:this={step_labels[i]}
|
|
class="step-label"
|
|
class:visible={show_labels_for_all ||
|
|
i === $selected_tab_index}
|
|
>
|
|
{t?.label !== undefined ? t?.label : "Step " + (i + 1)}
|
|
</span>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
{#if i < tabs.length - 1 && !compact}
|
|
<div
|
|
class="step-connector"
|
|
class:completed={i < $selected_tab_index}
|
|
></div>
|
|
{/if}
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
<slot />
|
|
</div>
|
|
|
|
<style>
|
|
.stepper {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--layout-gap);
|
|
}
|
|
|
|
.compact.stepper {
|
|
gap: 0;
|
|
}
|
|
|
|
.hide {
|
|
display: none;
|
|
}
|
|
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
.stepper-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
position: relative;
|
|
padding-top: var(--size-4);
|
|
padding-bottom: calc(var(--label-height) + var(--size-4));
|
|
}
|
|
|
|
.compact .stepper-wrapper {
|
|
padding-top: var(--size-2);
|
|
padding-bottom: var(--size-6);
|
|
}
|
|
|
|
.stepper-container {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
width: 100%;
|
|
position: relative;
|
|
padding: var(--size-2);
|
|
gap: var(--size-1);
|
|
}
|
|
|
|
.compact .stepper-container {
|
|
justify-content: center;
|
|
gap: 2px;
|
|
padding: 0;
|
|
}
|
|
|
|
.step-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 1 1 0;
|
|
position: relative;
|
|
}
|
|
|
|
.compact .step-item {
|
|
/* flex: 0 0 auto; */
|
|
width: 100%;
|
|
}
|
|
|
|
.step-button {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--size-1);
|
|
|
|
border: none;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
border-radius: var(--radius-md);
|
|
transition: background-color 0.2s ease;
|
|
font-size: var(--text-sm);
|
|
color: var(--body-text-color-subdued);
|
|
white-space: nowrap;
|
|
z-index: 1;
|
|
position: relative;
|
|
}
|
|
|
|
.compact .step-button {
|
|
padding: 0;
|
|
width: 100%;
|
|
border: none;
|
|
}
|
|
|
|
.compact .step-number {
|
|
height: 10px;
|
|
width: 100%;
|
|
border-radius: 0;
|
|
border: none;
|
|
}
|
|
|
|
.step-button:hover:not(:disabled) {
|
|
background-color: var(--background-fill-secondary);
|
|
}
|
|
|
|
.step-button:disabled {
|
|
cursor: not-allowed;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.step-button.active {
|
|
color: var(--body-text-color);
|
|
}
|
|
|
|
.step-button.completed {
|
|
color: var(--body-text-color);
|
|
}
|
|
|
|
.step-button.pending {
|
|
color: var(--body-text-color-subdued);
|
|
}
|
|
|
|
.step-number {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
font-size: var(--text-sm);
|
|
font-weight: var(--weight-semibold);
|
|
transition: background-color 0.2s ease;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.active .step-number {
|
|
background-color: var(--color-accent);
|
|
color: white;
|
|
box-shadow: 0 0 0 4px rgba(var(--color-accent-rgb), 0.1);
|
|
}
|
|
|
|
.completed .step-number {
|
|
background-color: var(--color-accent);
|
|
color: white;
|
|
}
|
|
|
|
.compact .completed .step-number {
|
|
color: transparent;
|
|
}
|
|
|
|
.compact .pending .step-number {
|
|
color: transparent;
|
|
background-color: var(--body-text-color-subdued);
|
|
}
|
|
|
|
.compact .active .step-number {
|
|
color: transparent;
|
|
}
|
|
|
|
.pending .step-number {
|
|
background-color: var(--button-secondary-background-fill);
|
|
color: var(--button-secondary-text-color);
|
|
}
|
|
|
|
.compact .step-item:last-child .step-number {
|
|
border-top-right-radius: var(--radius-xs);
|
|
border-bottom-right-radius: var(--radius-xs);
|
|
}
|
|
.compact .step-item:first-child .step-number {
|
|
border-top-left-radius: var(--radius-xs);
|
|
border-bottom-left-radius: var(--radius-xs);
|
|
}
|
|
|
|
.step-label {
|
|
font-size: var(--text-md);
|
|
line-height: 1.2;
|
|
text-align: center;
|
|
max-width: 120px;
|
|
position: absolute;
|
|
bottom: -20px;
|
|
display: none;
|
|
}
|
|
|
|
.step-label.visible {
|
|
display: block;
|
|
}
|
|
|
|
.step-connector {
|
|
width: 100%;
|
|
height: 2px;
|
|
background-color: var(--border-color-primary);
|
|
transition: background-color 0.3s ease;
|
|
z-index: 0;
|
|
transform: translate(0, 15px);
|
|
}
|
|
|
|
.step-connector.completed {
|
|
background-color: var(--color-accent);
|
|
}
|
|
|
|
:global(.dark) .pending .step-number {
|
|
background-color: var(--neutral-800);
|
|
color: var(--neutral-400);
|
|
border-color: var(--neutral-600);
|
|
}
|
|
|
|
:global(.dark) .step-connector {
|
|
background-color: var(--neutral-600);
|
|
}
|
|
</style>
|