Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

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>