@gradio/tabs 0.4.5 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,449 @@
1
+ <script context="module" lang="ts">
2
+ import { TABS, type Tab } from "./Tabs.svelte";
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ import { setContext, createEventDispatcher, tick, onMount } from "svelte";
7
+ import { writable } from "svelte/store";
8
+ import type { SelectData } from "@gradio/utils";
9
+
10
+ export let visible: boolean | "hidden" = true;
11
+ export let elem_id = "";
12
+ export let elem_classes: string[] = [];
13
+ export let selected: number | string;
14
+ export let initial_tabs: Tab[];
15
+
16
+ let tabs: (Tab | null)[] = [...initial_tabs];
17
+ let stepper_container: HTMLDivElement;
18
+ let show_labels_for_all = true;
19
+ let measurement_container: HTMLDivElement;
20
+ let step_buttons: HTMLButtonElement[] = [];
21
+ let step_labels: HTMLSpanElement[] = [];
22
+ let label_height = 0;
23
+ let compact = false;
24
+ let recompute_overflow = true;
25
+ $: has_tabs = tabs.length > 0;
26
+
27
+ const selected_tab = writable<false | number | string>(
28
+ selected || tabs[0]?.id || false
29
+ );
30
+ const selected_tab_index = writable<number>(
31
+ tabs.findIndex((t) => t?.id === selected) || 0
32
+ );
33
+ const dispatch = createEventDispatcher<{
34
+ change: undefined;
35
+ select: SelectData;
36
+ }>();
37
+
38
+ async function check_overflow(): Promise<void> {
39
+ if (!stepper_container || !measurement_container || !recompute_overflow)
40
+ return;
41
+ recompute_overflow = false;
42
+ await tick();
43
+
44
+ // First, show all labels to measure
45
+ show_labels_for_all = true;
46
+ await tick();
47
+
48
+ const SEP_WIDTH = 50;
49
+ const button_width =
50
+ step_buttons[0].getBoundingClientRect().width * step_buttons.length +
51
+ SEP_WIDTH * (step_buttons.length - 1);
52
+
53
+ const containerWidth = stepper_container.getBoundingClientRect().width;
54
+ const does_it_fit = button_width < containerWidth;
55
+
56
+ if (!does_it_fit) {
57
+ show_labels_for_all = false;
58
+ compact = true;
59
+ return;
60
+ }
61
+
62
+ let max_height = 0;
63
+ let is_overlapping = false;
64
+ let last_right = 0;
65
+
66
+ for (const label of step_labels) {
67
+ const { height, width, left, right } = label.getBoundingClientRect();
68
+ if (height > max_height) {
69
+ max_height = height;
70
+ }
71
+ if (last_right && left - 10 < last_right && !is_overlapping) {
72
+ is_overlapping = true;
73
+ }
74
+ last_right = right;
75
+ }
76
+ label_height = max_height;
77
+
78
+ if (is_overlapping) {
79
+ show_labels_for_all = false;
80
+ }
81
+ }
82
+
83
+ let last_width = 0;
84
+
85
+ onMount(() => {
86
+ check_overflow();
87
+
88
+ const observer = new ResizeObserver((entries) => {
89
+ if (entries[0].contentRect.width === last_width) return;
90
+ last_width = entries[0].contentRect.width;
91
+ compact = false;
92
+ recompute_overflow = true;
93
+ check_overflow();
94
+ });
95
+
96
+ if (stepper_container) {
97
+ observer.observe(stepper_container);
98
+ }
99
+
100
+ return () => {
101
+ observer.disconnect();
102
+ };
103
+ });
104
+
105
+ setContext(TABS, {
106
+ register_tab: (tab: Tab, order: number) => {
107
+ tabs[order] = tab;
108
+
109
+ if ($selected_tab === false && tab.visible && tab.interactive) {
110
+ $selected_tab = tab.id;
111
+ $selected_tab_index = order;
112
+ }
113
+ return order;
114
+ },
115
+ unregister_tab: (tab: Tab, order: number) => {
116
+ if ($selected_tab === tab.id) {
117
+ $selected_tab = tabs[0]?.id || false;
118
+ }
119
+ tabs[order] = null;
120
+ },
121
+ selected_tab,
122
+ selected_tab_index
123
+ });
124
+
125
+ function change_tab(id: string | number | undefined, index: number): void {
126
+ const tab_to_activate = tabs.find((t) => t?.id === id);
127
+ if (
128
+ id !== undefined &&
129
+ tab_to_activate &&
130
+ tab_to_activate.interactive &&
131
+ tab_to_activate.visible &&
132
+ $selected_tab !== tab_to_activate.id
133
+ ) {
134
+ selected = id;
135
+ $selected_tab = id;
136
+ $selected_tab_index = tabs.findIndex((t) => t?.id === id);
137
+ dispatch("change");
138
+ }
139
+ }
140
+
141
+ $: tabs,
142
+ selected !== null &&
143
+ change_tab(
144
+ selected,
145
+ tabs.findIndex((t) => t?.id === selected)
146
+ );
147
+ $: tabs, check_overflow();
148
+ $: $selected_tab_index, check_overflow();
149
+
150
+ $: tab_scale =
151
+ tabs[$selected_tab_index >= 0 ? $selected_tab_index : 0]?.scale;
152
+ </script>
153
+
154
+ <svelte:window on:resize={check_overflow} />
155
+
156
+ <div
157
+ class="stepper {elem_classes.join(' ')}"
158
+ class:hide={!visible}
159
+ id={elem_id}
160
+ style:flex-grow={tab_scale}
161
+ class:compact
162
+ >
163
+ {#if has_tabs}
164
+ {#if compact}
165
+ <p class="step-title">
166
+ <strong>Step {($selected_tab_index || 0) + 1}/{tabs.length}:</strong>
167
+ {tabs[$selected_tab_index]?.label || "Walkthrough"}
168
+ </p>
169
+ {/if}
170
+ <div
171
+ class="stepper-wrapper"
172
+ bind:this={stepper_container}
173
+ style:--label-height={label_height + "px"}
174
+ >
175
+ <div
176
+ class="stepper-container"
177
+ bind:this={measurement_container}
178
+ role="tablist"
179
+ >
180
+ {#each tabs as t, i}
181
+ {#if t?.visible}
182
+ <div class="step-item">
183
+ <button
184
+ bind:this={step_buttons[i]}
185
+ role="tab"
186
+ class="step-button"
187
+ class:active={t.id === $selected_tab}
188
+ class:completed={t.id < $selected_tab}
189
+ class:pending={t.id > $selected_tab}
190
+ aria-selected={t.id === $selected_tab}
191
+ aria-controls={t.elem_id}
192
+ disabled={!t.interactive || i > $selected_tab_index}
193
+ aria-disabled={!t.interactive || i > $selected_tab_index}
194
+ id={t.elem_id ? t.elem_id + "-button" : null}
195
+ data-tab-id={t.id}
196
+ on:click={() => {
197
+ if (i <= $selected_tab_index && t.id !== $selected_tab) {
198
+ change_tab(t.id, i);
199
+ dispatch("select", { value: t.label, index: i });
200
+ }
201
+ }}
202
+ >
203
+ <span class="step-number">
204
+ {#if t.id < $selected_tab}
205
+ <svg
206
+ width="12"
207
+ height="10"
208
+ viewBox="0 0 12 10"
209
+ fill="none"
210
+ xmlns="http://www.w3.org/2000/svg"
211
+ >
212
+ <path
213
+ d="M1 5L4.5 8.5L11 1.5"
214
+ stroke="currentColor"
215
+ stroke-width="2"
216
+ stroke-linecap="round"
217
+ stroke-linejoin="round"
218
+ />
219
+ </svg>
220
+ {:else}
221
+ {i + 1}
222
+ {/if}
223
+ </span>
224
+ {#if !compact}
225
+ <span
226
+ bind:this={step_labels[i]}
227
+ class="step-label"
228
+ class:visible={show_labels_for_all ||
229
+ i === $selected_tab_index}
230
+ >
231
+ {t?.label !== undefined ? t?.label : "Step " + (i + 1)}
232
+ </span>
233
+ {/if}
234
+ </button>
235
+ </div>
236
+ {#if i < tabs.length - 1 && !compact}
237
+ <div
238
+ class="step-connector"
239
+ class:completed={i < $selected_tab_index}
240
+ ></div>
241
+ {/if}
242
+ {/if}
243
+ {/each}
244
+ </div>
245
+ </div>
246
+ {/if}
247
+ <slot />
248
+ </div>
249
+
250
+ <style>
251
+ .stepper {
252
+ position: relative;
253
+ display: flex;
254
+ flex-direction: column;
255
+ gap: var(--layout-gap);
256
+ }
257
+
258
+ .compact.stepper {
259
+ gap: 0;
260
+ }
261
+
262
+ .hide {
263
+ display: none;
264
+ }
265
+
266
+ .stepper-wrapper {
267
+ display: flex;
268
+ align-items: center;
269
+ position: relative;
270
+ padding-top: var(--size-4);
271
+ padding-bottom: calc(var(--label-height) + var(--size-4));
272
+ }
273
+
274
+ .compact .stepper-wrapper {
275
+ padding-top: var(--size-2);
276
+ padding-bottom: var(--size-6);
277
+ }
278
+
279
+ .stepper-container {
280
+ display: flex;
281
+ justify-content: space-between;
282
+ align-items: flex-start;
283
+ width: 100%;
284
+ position: relative;
285
+ padding: var(--size-2);
286
+ gap: var(--size-1);
287
+ }
288
+
289
+ .compact .stepper-container {
290
+ justify-content: center;
291
+ gap: 2px;
292
+ padding: 0;
293
+ }
294
+
295
+ .step-item {
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: center;
299
+ flex: 1 1 0;
300
+ position: relative;
301
+ }
302
+
303
+ .compact .step-item {
304
+ /* flex: 0 0 auto; */
305
+ width: 100%;
306
+ }
307
+
308
+ .step-button {
309
+ position: relative;
310
+ display: flex;
311
+ flex-direction: column;
312
+ align-items: center;
313
+ justify-content: center;
314
+ gap: var(--size-1);
315
+
316
+ border: none;
317
+ background: transparent;
318
+ cursor: pointer;
319
+ border-radius: var(--radius-md);
320
+ transition: background-color 0.2s ease;
321
+ font-size: var(--text-sm);
322
+ color: var(--body-text-color-subdued);
323
+ white-space: nowrap;
324
+ z-index: 1;
325
+ position: relative;
326
+ }
327
+
328
+ .compact .step-button {
329
+ padding: 0;
330
+ width: 100%;
331
+ border: none;
332
+ }
333
+
334
+ .compact .step-number {
335
+ height: 10px;
336
+ width: 100%;
337
+ border-radius: 0;
338
+ border: none;
339
+ }
340
+
341
+ .step-button:hover:not(:disabled) {
342
+ background-color: var(--background-fill-secondary);
343
+ }
344
+
345
+ .step-button:disabled {
346
+ cursor: not-allowed;
347
+ opacity: 0.5;
348
+ }
349
+
350
+ .step-button.active {
351
+ color: var(--body-text-color);
352
+ }
353
+
354
+ .step-button.completed {
355
+ color: var(--body-text-color);
356
+ }
357
+
358
+ .step-button.pending {
359
+ color: var(--body-text-color-subdued);
360
+ }
361
+
362
+ .step-number {
363
+ display: flex;
364
+ align-items: center;
365
+ justify-content: center;
366
+ width: 32px;
367
+ height: 32px;
368
+ border-radius: 50%;
369
+ font-size: var(--text-sm);
370
+ font-weight: var(--weight-semibold);
371
+ transition: background-color 0.2s ease;
372
+ flex-shrink: 0;
373
+ }
374
+
375
+ .active .step-number {
376
+ background-color: var(--color-accent);
377
+ color: white;
378
+ box-shadow: 0 0 0 4px rgba(var(--color-accent-rgb), 0.1);
379
+ }
380
+
381
+ .completed .step-number {
382
+ background-color: var(--color-accent);
383
+ color: white;
384
+ }
385
+
386
+ .compact .completed .step-number {
387
+ color: transparent;
388
+ }
389
+
390
+ .compact .pending .step-number {
391
+ color: transparent;
392
+ background-color: var(--body-text-color-subdued);
393
+ }
394
+
395
+ .compact .active .step-number {
396
+ color: transparent;
397
+ }
398
+
399
+ .pending .step-number {
400
+ background-color: var(--button-secondary-background-fill);
401
+ color: var(--button-secondary-text-color);
402
+ }
403
+
404
+ .compact .step-item:last-child .step-number {
405
+ border-top-right-radius: var(--radius-xs);
406
+ border-bottom-right-radius: var(--radius-xs);
407
+ }
408
+ .compact .step-item:first-child .step-number {
409
+ border-top-left-radius: var(--radius-xs);
410
+ border-bottom-left-radius: var(--radius-xs);
411
+ }
412
+
413
+ .step-label {
414
+ font-size: var(--text-md);
415
+ line-height: 1.2;
416
+ text-align: center;
417
+ max-width: 120px;
418
+ position: absolute;
419
+ bottom: -20px;
420
+ display: none;
421
+ }
422
+
423
+ .step-label.visible {
424
+ display: block;
425
+ }
426
+
427
+ .step-connector {
428
+ width: 100%;
429
+ height: 2px;
430
+ background-color: var(--border-color-primary);
431
+ transition: background-color 0.3s ease;
432
+ z-index: 0;
433
+ transform: translate(0, 15px);
434
+ }
435
+
436
+ .step-connector.completed {
437
+ background-color: var(--color-accent);
438
+ }
439
+
440
+ :global(.dark) .pending .step-number {
441
+ background-color: var(--neutral-800);
442
+ color: var(--neutral-400);
443
+ border-color: var(--neutral-600);
444
+ }
445
+
446
+ :global(.dark) .step-connector {
447
+ background-color: var(--neutral-600);
448
+ }
449
+ </style>