@gradio/tabs 0.4.4 → 0.5.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @gradio/tabs
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Features
6
+
7
+ - [#11783](https://github.com/gradio-app/gradio/pull/11783) [`f407daf`](https://github.com/gradio-app/gradio/commit/f407daf8046f37e042ab8b86730ff0ab8d174bcf) - Add Walkthrough and Step compoents to facilitate multi-step workflows. Thanks @pngwn!
8
+
9
+ ## 0.4.5
10
+
11
+ ### Fixes
12
+
13
+ - [#11344](https://github.com/gradio-app/gradio/pull/11344) [`b961441`](https://github.com/gradio-app/gradio/commit/b961441780d70c82504141bd4deae7b3290c2227) - Fixes default tab label. Thanks @dawoodkhan82!
14
+
3
15
  ## 0.4.4
4
16
 
5
17
  ### Fixes
package/Index.svelte CHANGED
@@ -6,6 +6,7 @@
6
6
  import type { Gradio, SelectData } from "@gradio/utils";
7
7
  import { createEventDispatcher } from "svelte";
8
8
  import Tabs, { type Tab } from "./shared/Tabs.svelte";
9
+ import Walkthrough from "./shared/Walkthrough.svelte";
9
10
 
10
11
  const dispatch = createEventDispatcher();
11
12
 
@@ -14,6 +15,7 @@
14
15
  export let elem_classes: string[] = [];
15
16
  export let selected: number | string;
16
17
  export let initial_tabs: Tab[] = [];
18
+ export let name: "tabs" | "walkthrough" = "tabs";
17
19
  export let gradio:
18
20
  | Gradio<{
19
21
  change: never;
@@ -24,14 +26,28 @@
24
26
  $: dispatch("prop_change", { selected });
25
27
  </script>
26
28
 
27
- <Tabs
28
- {visible}
29
- {elem_id}
30
- {elem_classes}
31
- bind:selected
32
- on:change={() => gradio?.dispatch("change")}
33
- on:select={(e) => gradio?.dispatch("select", e.detail)}
34
- {initial_tabs}
35
- >
36
- <slot />
37
- </Tabs>
29
+ {#if name === "walkthrough"}
30
+ <Walkthrough
31
+ {visible}
32
+ {elem_id}
33
+ {elem_classes}
34
+ bind:selected
35
+ on:change={() => gradio?.dispatch("change")}
36
+ on:select={(e) => gradio?.dispatch("select", e.detail)}
37
+ {initial_tabs}
38
+ >
39
+ <slot />
40
+ </Walkthrough>
41
+ {:else}
42
+ <Tabs
43
+ {visible}
44
+ {elem_id}
45
+ {elem_classes}
46
+ bind:selected
47
+ on:change={() => gradio?.dispatch("change")}
48
+ on:select={(e) => gradio?.dispatch("select", e.detail)}
49
+ {initial_tabs}
50
+ >
51
+ <slot />
52
+ </Tabs>
53
+ {/if}
@@ -0,0 +1,116 @@
1
+ <script>
2
+ import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
3
+ import Tabs from "./Index.svelte";
4
+ import TabItem from "../tabitem/Index.svelte";
5
+ </script>
6
+
7
+ <Meta title="Components/Walkthrough" component={Tabs} />
8
+
9
+ <Template let:args>
10
+ <Tabs {...args} name="walkthrough">
11
+ <TabItem
12
+ order={0}
13
+ id="tab-1"
14
+ label="Image Tab"
15
+ gradio={undefined}
16
+ visible
17
+ interactive
18
+ elem_classes={["editor-tabitem"]}
19
+ scale={0}
20
+ >
21
+ <img
22
+ style="width: 200px;"
23
+ alt="Cheetah"
24
+ src="https://gradio-builds.s3.amazonaws.com/demo-files/ghepardo-primo-piano.jpg"
25
+ />
26
+ </TabItem>
27
+ <TabItem
28
+ order={1}
29
+ id="tab-2"
30
+ label="Hidden Tab"
31
+ gradio={undefined}
32
+ visible={false}
33
+ interactive
34
+ elem_classes={["editor-tabitem"]}
35
+ scale={0}
36
+ >
37
+ Secret Tab
38
+ </TabItem>
39
+ <TabItem
40
+ order={2}
41
+ id="tab-3"
42
+ label="Visible Tab"
43
+ gradio={undefined}
44
+ visible
45
+ interactive
46
+ elem_classes={["editor-tabitem"]}
47
+ scale={0}
48
+ >
49
+ Visible Tab
50
+ </TabItem>
51
+ <TabItem
52
+ order={3}
53
+ id="tab-4"
54
+ label="Visible Tab"
55
+ gradio={undefined}
56
+ visible
57
+ interactive
58
+ elem_classes={["editor-tabitem"]}
59
+ scale={0}
60
+ >
61
+ Visible Tab
62
+ </TabItem>
63
+ </Tabs>
64
+ </Template>
65
+
66
+ <Story name="Tabs Walkthrough" args={{}} />
67
+
68
+ <Story name="TabsLastInvisible" args={{}}>
69
+ <Tabs selected="tab-1" gradio={undefined} name="walkthrough">
70
+ <TabItem
71
+ order={0}
72
+ id="tab-1"
73
+ label="This is visible tab 1"
74
+ gradio={undefined}
75
+ visible
76
+ interactive
77
+ scale={0}
78
+ ></TabItem>
79
+ <TabItem
80
+ order={1}
81
+ id="tab-2"
82
+ label="This is visible tab 2"
83
+ gradio={undefined}
84
+ visible
85
+ interactive
86
+ scale={0}
87
+ ></TabItem>
88
+ <TabItem
89
+ order={2}
90
+ id="tab-3"
91
+ label="This is visible tab 3"
92
+ gradio={undefined}
93
+ visible
94
+ interactive
95
+ scale={0}
96
+ ></TabItem>
97
+ <TabItem
98
+ order={3}
99
+ id="tab-4"
100
+ label="This is invisible tab 4"
101
+ gradio={undefined}
102
+ visible={false}
103
+ interactive
104
+ scale={0}
105
+ ></TabItem>
106
+ <TabItem
107
+ order={4}
108
+ id="tab-5"
109
+ label="This is invisible tab 5"
110
+ gradio={undefined}
111
+ visible={false}
112
+ interactive
113
+ scale={0}
114
+ ></TabItem>
115
+ </Tabs>
116
+ </Story>
package/dist/Index.svelte CHANGED
@@ -3,25 +3,40 @@
3
3
 
4
4
  <script>import { createEventDispatcher } from "svelte";
5
5
  import Tabs, {} from "./shared/Tabs.svelte";
6
+ import Walkthrough from "./shared/Walkthrough.svelte";
6
7
  const dispatch = createEventDispatcher();
7
8
  export let visible = true;
8
9
  export let elem_id = "";
9
10
  export let elem_classes = [];
10
11
  export let selected;
11
12
  export let initial_tabs = [];
13
+ export let name = "tabs";
12
14
  export let gradio;
13
- $:
14
- dispatch("prop_change", { selected });
15
+ $: dispatch("prop_change", { selected });
15
16
  </script>
16
17
 
17
- <Tabs
18
- {visible}
19
- {elem_id}
20
- {elem_classes}
21
- bind:selected
22
- on:change={() => gradio?.dispatch("change")}
23
- on:select={(e) => gradio?.dispatch("select", e.detail)}
24
- {initial_tabs}
25
- >
26
- <slot />
27
- </Tabs>
18
+ {#if name === "walkthrough"}
19
+ <Walkthrough
20
+ {visible}
21
+ {elem_id}
22
+ {elem_classes}
23
+ bind:selected
24
+ on:change={() => gradio?.dispatch("change")}
25
+ on:select={(e) => gradio?.dispatch("select", e.detail)}
26
+ {initial_tabs}
27
+ >
28
+ <slot />
29
+ </Walkthrough>
30
+ {:else}
31
+ <Tabs
32
+ {visible}
33
+ {elem_id}
34
+ {elem_classes}
35
+ bind:selected
36
+ on:change={() => gradio?.dispatch("change")}
37
+ on:select={(e) => gradio?.dispatch("select", e.detail)}
38
+ {initial_tabs}
39
+ >
40
+ <slot />
41
+ </Tabs>
42
+ {/if}
@@ -4,11 +4,12 @@ import type { Gradio, SelectData } from "@gradio/utils";
4
4
  import { type Tab } from "./shared/Tabs.svelte";
5
5
  declare const __propDef: {
6
6
  props: {
7
- visible?: boolean | undefined;
8
- elem_id?: string | undefined;
9
- elem_classes?: string[] | undefined;
7
+ visible?: boolean;
8
+ elem_id?: string;
9
+ elem_classes?: string[];
10
10
  selected: number | string;
11
- initial_tabs?: Tab[] | undefined;
11
+ initial_tabs?: Tab[];
12
+ name?: "tabs" | "walkthrough";
12
13
  gradio: Gradio<{
13
14
  change: never;
14
15
  select: SelectData;
@@ -22,6 +23,8 @@ declare const __propDef: {
22
23
  slots: {
23
24
  default: {};
24
25
  };
26
+ exports?: {} | undefined;
27
+ bindings?: string | undefined;
25
28
  };
26
29
  export type IndexProps = typeof __propDef.props;
27
30
  export type IndexEvents = typeof __propDef.events;
@@ -19,5 +19,7 @@ declare const __propDef: {
19
19
  [evt: string]: CustomEvent<any>;
20
20
  };
21
21
  slots: {};
22
+ exports?: undefined;
23
+ bindings?: undefined;
22
24
  };
23
25
  export {};
@@ -14,8 +14,7 @@ let visible_tabs = [...initial_tabs];
14
14
  let overflow_tabs = [];
15
15
  let overflow_menu_open = false;
16
16
  let overflow_menu;
17
- $:
18
- has_tabs = tabs.length > 0;
17
+ $: has_tabs = tabs.length > 0;
19
18
  let tab_nav_el;
20
19
  const selected_tab = writable(
21
20
  selected || tabs[0]?.id || false
@@ -28,6 +27,7 @@ let is_overflowing = false;
28
27
  let overflow_has_selected_tab = false;
29
28
  let tab_els = {};
30
29
  onMount(() => {
30
+ if (!tab_nav_el) return;
31
31
  const observer = new IntersectionObserver((entries) => {
32
32
  handle_menu_overflow();
33
33
  });
@@ -61,18 +61,15 @@ function change_tab(id) {
61
61
  overflow_menu_open = false;
62
62
  }
63
63
  }
64
- $:
65
- tabs, selected !== null && change_tab(selected);
66
- $:
67
- tabs, tab_nav_el, tab_els, handle_menu_overflow();
64
+ $: tabs, selected !== null && change_tab(selected);
65
+ $: tabs, tab_nav_el, tab_els, handle_menu_overflow();
68
66
  function handle_outside_click(event) {
69
67
  if (overflow_menu_open && overflow_menu && !overflow_menu.contains(event.target)) {
70
68
  overflow_menu_open = false;
71
69
  }
72
70
  }
73
71
  async function handle_menu_overflow() {
74
- if (!tab_nav_el)
75
- return;
72
+ if (!tab_nav_el) return;
76
73
  await tick();
77
74
  const tab_nav_size = tab_nav_el.getBoundingClientRect();
78
75
  let max_width = tab_nav_size.width;
@@ -81,11 +78,9 @@ async function handle_menu_overflow() {
81
78
  const offset = tab_nav_size.left;
82
79
  for (let i = tabs.length - 1; i >= 0; i--) {
83
80
  const tab = tabs[i];
84
- if (!tab)
85
- continue;
81
+ if (!tab) continue;
86
82
  const tab_rect = tab_sizes[tab.id];
87
- if (!tab_rect)
88
- continue;
83
+ if (!tab_rect) continue;
89
84
  if (tab_rect.right - offset < max_width) {
90
85
  last_visible_index = i;
91
86
  break;
@@ -96,24 +91,20 @@ async function handle_menu_overflow() {
96
91
  overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
97
92
  is_overflowing = overflow_tabs.length > 0;
98
93
  }
99
- $:
100
- overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
94
+ $: overflow_has_selected_tab = handle_overflow_has_selected_tab($selected_tab);
101
95
  function handle_overflow_has_selected_tab(selected_tab2) {
102
- if (selected_tab2 === false)
103
- return false;
96
+ if (selected_tab2 === false) return false;
104
97
  return overflow_tabs.some((t) => t?.id === selected_tab2);
105
98
  }
106
99
  function get_tab_sizes(tabs2, tab_els2) {
107
100
  const tab_sizes = {};
108
101
  tabs2.forEach((tab) => {
109
- if (!tab)
110
- return;
102
+ if (!tab) return;
111
103
  tab_sizes[tab.id] = tab_els2[tab.id]?.getBoundingClientRect();
112
104
  });
113
105
  return tab_sizes;
114
106
  }
115
- $:
116
- tab_scale = tabs[$selected_tab_index >= 0 ? $selected_tab_index : 0]?.scale;
107
+ $: tab_scale = tabs[$selected_tab_index >= 0 ? $selected_tab_index : 0]?.scale;
117
108
  </script>
118
109
 
119
110
  <svelte:window
@@ -157,7 +148,7 @@ $:
157
148
  }
158
149
  }}
159
150
  >
160
- {t.label}
151
+ {t?.label !== undefined ? t?.label : "Tab " + (i + 1)}
161
152
  </button>
162
153
  {/if}
163
154
  {/each}
@@ -11,9 +11,9 @@ export interface Tab {
11
11
  import type { SelectData } from "@gradio/utils";
12
12
  declare const __propDef: {
13
13
  props: {
14
- visible?: boolean | undefined;
15
- elem_id?: string | undefined;
16
- elem_classes?: string[] | undefined;
14
+ visible?: boolean;
15
+ elem_id?: string;
16
+ elem_classes?: string[];
17
17
  selected: number | string;
18
18
  initial_tabs: Tab[];
19
19
  };
@@ -26,6 +26,8 @@ declare const __propDef: {
26
26
  slots: {
27
27
  default: {};
28
28
  };
29
+ exports?: {} | undefined;
30
+ bindings?: string | undefined;
29
31
  };
30
32
  export type TabsProps = typeof __propDef.props;
31
33
  export type TabsEvents = typeof __propDef.events;
@@ -0,0 +1,410 @@
1
+ <script context="module">import { TABS } from "./Tabs.svelte";
2
+ </script>
3
+
4
+ <script>import { setContext, createEventDispatcher, tick, onMount } from "svelte";
5
+ import { writable } from "svelte/store";
6
+ export let visible = true;
7
+ export let elem_id = "";
8
+ export let elem_classes = [];
9
+ export let selected;
10
+ export let initial_tabs;
11
+ let tabs = [...initial_tabs];
12
+ let stepper_container;
13
+ let show_labels_for_all = true;
14
+ let measurement_container;
15
+ let step_buttons = [];
16
+ let step_labels = [];
17
+ let label_height = 0;
18
+ let compact = false;
19
+ let recompute_overflow = true;
20
+ $: has_tabs = tabs.length > 0;
21
+ const selected_tab = writable(
22
+ selected || tabs[0]?.id || false
23
+ );
24
+ const selected_tab_index = writable(
25
+ tabs.findIndex((t) => t?.id === selected) || 0
26
+ );
27
+ const dispatch = createEventDispatcher();
28
+ async function check_overflow() {
29
+ if (!stepper_container || !measurement_container || !recompute_overflow)
30
+ return;
31
+ recompute_overflow = false;
32
+ await tick();
33
+ show_labels_for_all = true;
34
+ await tick();
35
+ const SEP_WIDTH = 50;
36
+ const button_width = step_buttons[0].getBoundingClientRect().width * step_buttons.length + SEP_WIDTH * (step_buttons.length - 1);
37
+ const containerWidth = stepper_container.getBoundingClientRect().width;
38
+ const does_it_fit = button_width < containerWidth;
39
+ if (!does_it_fit) {
40
+ show_labels_for_all = false;
41
+ compact = true;
42
+ return;
43
+ }
44
+ let max_height = 0;
45
+ let is_overlapping = false;
46
+ let last_right = 0;
47
+ for (const label of step_labels) {
48
+ const { height, width, left, right } = label.getBoundingClientRect();
49
+ if (height > max_height) {
50
+ max_height = height;
51
+ }
52
+ if (last_right && left - 10 < last_right && !is_overlapping) {
53
+ is_overlapping = true;
54
+ }
55
+ last_right = right;
56
+ }
57
+ label_height = max_height;
58
+ if (is_overlapping) {
59
+ show_labels_for_all = false;
60
+ }
61
+ }
62
+ let last_width = 0;
63
+ onMount(() => {
64
+ check_overflow();
65
+ const observer = new ResizeObserver((entries) => {
66
+ if (entries[0].contentRect.width === last_width) return;
67
+ last_width = entries[0].contentRect.width;
68
+ compact = false;
69
+ recompute_overflow = true;
70
+ check_overflow();
71
+ });
72
+ if (stepper_container) {
73
+ observer.observe(stepper_container);
74
+ }
75
+ return () => {
76
+ observer.disconnect();
77
+ };
78
+ });
79
+ setContext(TABS, {
80
+ register_tab: (tab, order) => {
81
+ tabs[order] = tab;
82
+ if ($selected_tab === false && tab.visible && tab.interactive) {
83
+ $selected_tab = tab.id;
84
+ $selected_tab_index = order;
85
+ }
86
+ return order;
87
+ },
88
+ unregister_tab: (tab, order) => {
89
+ if ($selected_tab === tab.id) {
90
+ $selected_tab = tabs[0]?.id || false;
91
+ }
92
+ tabs[order] = null;
93
+ },
94
+ selected_tab,
95
+ selected_tab_index
96
+ });
97
+ function change_tab(id, index) {
98
+ const tab_to_activate = tabs.find((t) => t?.id === id);
99
+ if (id !== void 0 && tab_to_activate && tab_to_activate.interactive && tab_to_activate.visible && $selected_tab !== tab_to_activate.id) {
100
+ selected = id;
101
+ $selected_tab = id;
102
+ $selected_tab_index = tabs.findIndex((t) => t?.id === id);
103
+ dispatch("change");
104
+ }
105
+ }
106
+ $: tabs, selected !== null && change_tab(
107
+ selected,
108
+ tabs.findIndex((t) => t?.id === selected)
109
+ );
110
+ $: tabs, check_overflow();
111
+ $: $selected_tab_index, check_overflow();
112
+ $: tab_scale = tabs[$selected_tab_index >= 0 ? $selected_tab_index : 0]?.scale;
113
+ </script>
114
+
115
+ <svelte:window on:resize={check_overflow} />
116
+
117
+ <div
118
+ class="stepper {elem_classes.join(' ')}"
119
+ class:hide={!visible}
120
+ id={elem_id}
121
+ style:flex-grow={tab_scale}
122
+ class:compact
123
+ >
124
+ {#if has_tabs}
125
+ {#if compact}
126
+ <p class="step-title">
127
+ <strong>Step {($selected_tab_index || 0) + 1}/{tabs.length}:</strong>
128
+ {tabs[$selected_tab_index]?.label || "Walkthrough"}
129
+ </p>
130
+ {/if}
131
+ <div
132
+ class="stepper-wrapper"
133
+ bind:this={stepper_container}
134
+ style:--label-height={label_height + "px"}
135
+ >
136
+ <div
137
+ class="stepper-container"
138
+ bind:this={measurement_container}
139
+ role="tablist"
140
+ >
141
+ {#each tabs as t, i}
142
+ {#if t?.visible}
143
+ <div class="step-item">
144
+ <button
145
+ bind:this={step_buttons[i]}
146
+ role="tab"
147
+ class="step-button"
148
+ class:active={t.id === $selected_tab}
149
+ class:completed={t.id < $selected_tab}
150
+ class:pending={t.id > $selected_tab}
151
+ aria-selected={t.id === $selected_tab}
152
+ aria-controls={t.elem_id}
153
+ disabled={!t.interactive || i > $selected_tab_index}
154
+ aria-disabled={!t.interactive || i > $selected_tab_index}
155
+ id={t.elem_id ? t.elem_id + "-button" : null}
156
+ data-tab-id={t.id}
157
+ on:click={() => {
158
+ if (i <= $selected_tab_index && t.id !== $selected_tab) {
159
+ change_tab(t.id, i);
160
+ dispatch("select", { value: t.label, index: i });
161
+ }
162
+ }}
163
+ >
164
+ <span class="step-number">
165
+ {#if t.id < $selected_tab}
166
+ <svg
167
+ width="12"
168
+ height="10"
169
+ viewBox="0 0 12 10"
170
+ fill="none"
171
+ xmlns="http://www.w3.org/2000/svg"
172
+ >
173
+ <path
174
+ d="M1 5L4.5 8.5L11 1.5"
175
+ stroke="currentColor"
176
+ stroke-width="2"
177
+ stroke-linecap="round"
178
+ stroke-linejoin="round"
179
+ />
180
+ </svg>
181
+ {:else}
182
+ {i + 1}
183
+ {/if}
184
+ </span>
185
+ {#if !compact}
186
+ <span
187
+ bind:this={step_labels[i]}
188
+ class="step-label"
189
+ class:visible={show_labels_for_all ||
190
+ i === $selected_tab_index}
191
+ >
192
+ {t?.label !== undefined ? t?.label : "Step " + (i + 1)}
193
+ </span>
194
+ {/if}
195
+ </button>
196
+ </div>
197
+ {#if i < tabs.length - 1 && !compact}
198
+ <div
199
+ class="step-connector"
200
+ class:completed={i < $selected_tab_index}
201
+ ></div>
202
+ {/if}
203
+ {/if}
204
+ {/each}
205
+ </div>
206
+ </div>
207
+ {/if}
208
+ <slot />
209
+ </div>
210
+
211
+ <style>
212
+ .stepper {
213
+ position: relative;
214
+ display: flex;
215
+ flex-direction: column;
216
+ gap: var(--layout-gap);
217
+ }
218
+
219
+ .compact.stepper {
220
+ gap: 0;
221
+ }
222
+
223
+ .hide {
224
+ display: none;
225
+ }
226
+
227
+ .stepper-wrapper {
228
+ display: flex;
229
+ align-items: center;
230
+ position: relative;
231
+ padding-top: var(--size-4);
232
+ padding-bottom: calc(var(--label-height) + var(--size-4));
233
+ }
234
+
235
+ .compact .stepper-wrapper {
236
+ padding-top: var(--size-2);
237
+ padding-bottom: var(--size-6);
238
+ }
239
+
240
+ .stepper-container {
241
+ display: flex;
242
+ justify-content: space-between;
243
+ align-items: flex-start;
244
+ width: 100%;
245
+ position: relative;
246
+ padding: var(--size-2);
247
+ gap: var(--size-1);
248
+ }
249
+
250
+ .compact .stepper-container {
251
+ justify-content: center;
252
+ gap: 2px;
253
+ padding: 0;
254
+ }
255
+
256
+ .step-item {
257
+ display: flex;
258
+ align-items: center;
259
+ justify-content: center;
260
+ flex: 1 1 0;
261
+ position: relative;
262
+ }
263
+
264
+ .compact .step-item {
265
+ /* flex: 0 0 auto; */
266
+ width: 100%;
267
+ }
268
+
269
+ .step-button {
270
+ position: relative;
271
+ display: flex;
272
+ flex-direction: column;
273
+ align-items: center;
274
+ justify-content: center;
275
+ gap: var(--size-1);
276
+
277
+ border: none;
278
+ background: transparent;
279
+ cursor: pointer;
280
+ border-radius: var(--radius-md);
281
+ transition: background-color 0.2s ease;
282
+ font-size: var(--text-sm);
283
+ color: var(--body-text-color-subdued);
284
+ white-space: nowrap;
285
+ z-index: 1;
286
+ position: relative;
287
+ }
288
+
289
+ .compact .step-button {
290
+ padding: 0;
291
+ width: 100%;
292
+ border: none;
293
+ }
294
+
295
+ .compact .step-number {
296
+ height: 10px;
297
+ width: 100%;
298
+ border-radius: 0;
299
+ border: none;
300
+ }
301
+
302
+ .step-button:hover:not(:disabled) {
303
+ background-color: var(--background-fill-secondary);
304
+ }
305
+
306
+ .step-button:disabled {
307
+ cursor: not-allowed;
308
+ opacity: 0.5;
309
+ }
310
+
311
+ .step-button.active {
312
+ color: var(--body-text-color);
313
+ }
314
+
315
+ .step-button.completed {
316
+ color: var(--body-text-color);
317
+ }
318
+
319
+ .step-button.pending {
320
+ color: var(--body-text-color-subdued);
321
+ }
322
+
323
+ .step-number {
324
+ display: flex;
325
+ align-items: center;
326
+ justify-content: center;
327
+ width: 32px;
328
+ height: 32px;
329
+ border-radius: 50%;
330
+ font-size: var(--text-sm);
331
+ font-weight: var(--weight-semibold);
332
+ transition: background-color 0.2s ease;
333
+ flex-shrink: 0;
334
+ }
335
+
336
+ .active .step-number {
337
+ background-color: var(--color-accent);
338
+ color: white;
339
+ box-shadow: 0 0 0 4px rgba(var(--color-accent-rgb), 0.1);
340
+ }
341
+
342
+ .completed .step-number {
343
+ background-color: var(--color-accent);
344
+ color: white;
345
+ }
346
+
347
+ .compact .completed .step-number {
348
+ color: transparent;
349
+ }
350
+
351
+ .compact .pending .step-number {
352
+ color: transparent;
353
+ background-color: var(--body-text-color-subdued);
354
+ }
355
+
356
+ .compact .active .step-number {
357
+ color: transparent;
358
+ }
359
+
360
+ .pending .step-number {
361
+ background-color: var(--button-secondary-background-fill);
362
+ color: var(--button-secondary-text-color);
363
+ }
364
+
365
+ .compact .step-item:last-child .step-number {
366
+ border-top-right-radius: var(--radius-xs);
367
+ border-bottom-right-radius: var(--radius-xs);
368
+ }
369
+ .compact .step-item:first-child .step-number {
370
+ border-top-left-radius: var(--radius-xs);
371
+ border-bottom-left-radius: var(--radius-xs);
372
+ }
373
+
374
+ .step-label {
375
+ font-size: var(--text-md);
376
+ line-height: 1.2;
377
+ text-align: center;
378
+ max-width: 120px;
379
+ position: absolute;
380
+ bottom: -20px;
381
+ display: none;
382
+ }
383
+
384
+ .step-label.visible {
385
+ display: block;
386
+ }
387
+
388
+ .step-connector {
389
+ width: 100%;
390
+ height: 2px;
391
+ background-color: var(--border-color-primary);
392
+ transition: background-color 0.3s ease;
393
+ z-index: 0;
394
+ transform: translate(0, 15px);
395
+ }
396
+
397
+ .step-connector.completed {
398
+ background-color: var(--color-accent);
399
+ }
400
+
401
+ :global(.dark) .pending .step-number {
402
+ background-color: var(--neutral-800);
403
+ color: var(--neutral-400);
404
+ border-color: var(--neutral-600);
405
+ }
406
+
407
+ :global(.dark) .step-connector {
408
+ background-color: var(--neutral-600);
409
+ }
410
+ </style>
@@ -0,0 +1,29 @@
1
+ import { SvelteComponent } from "svelte";
2
+ import { type Tab } from "./Tabs.svelte";
3
+ import type { SelectData } from "@gradio/utils";
4
+ declare const __propDef: {
5
+ props: {
6
+ visible?: boolean;
7
+ elem_id?: string;
8
+ elem_classes?: string[];
9
+ selected: number | string;
10
+ initial_tabs: Tab[];
11
+ };
12
+ events: {
13
+ change: CustomEvent<undefined>;
14
+ select: CustomEvent<SelectData>;
15
+ } & {
16
+ [evt: string]: CustomEvent<any>;
17
+ };
18
+ slots: {
19
+ default: {};
20
+ };
21
+ exports?: {} | undefined;
22
+ bindings?: string | undefined;
23
+ };
24
+ export type WalkthroughProps = typeof __propDef.props;
25
+ export type WalkthroughEvents = typeof __propDef.events;
26
+ export type WalkthroughSlots = typeof __propDef.slots;
27
+ export default class Walkthrough extends SvelteComponent<WalkthroughProps, WalkthroughEvents, WalkthroughSlots> {
28
+ }
29
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/tabs",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -20,7 +20,7 @@
20
20
  "@gradio/utils": "^0.10.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@gradio/preview": "^0.13.0"
23
+ "@gradio/preview": "^0.14.0"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "svelte": "^4.0.0"
@@ -49,6 +49,7 @@
49
49
  let tab_els: Record<string | number, HTMLElement> = {};
50
50
 
51
51
  onMount(() => {
52
+ if (!tab_nav_el) return;
52
53
  const observer = new IntersectionObserver((entries) => {
53
54
  handle_menu_overflow();
54
55
  });
@@ -201,7 +202,7 @@
201
202
  }
202
203
  }}
203
204
  >
204
- {t.label}
205
+ {t?.label !== undefined ? t?.label : "Tab " + (i + 1)}
205
206
  </button>
206
207
  {/if}
207
208
  {/each}
@@ -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 = 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>