@gradio/tabs 0.2.12 → 0.3.0-beta.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.
- package/CHANGELOG.md +30 -0
- package/dist/Index.svelte +25 -0
- package/dist/Index.svelte.d.ts +28 -0
- package/dist/shared/OverflowIcon.svelte +11 -0
- package/dist/shared/OverflowIcon.svelte.d.ts +23 -0
- package/dist/shared/Tabs.svelte +311 -0
- package/dist/shared/Tabs.svelte.d.ts +26 -0
- package/package.json +16 -4
- package/shared/OverflowIcon.svelte +11 -0
- package/shared/Tabs.svelte +242 -69
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @gradio/tabs
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.1
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- [#9199](https://github.com/gradio-app/gradio/pull/9199) [`3175c7a`](https://github.com/gradio-app/gradio/commit/3175c7aebc6ad2466d31d6949580f5a3cb4cd698) - Redesign `gr.Tabs()`. Thanks @hannahblair!
|
|
8
|
+
|
|
9
|
+
### Dependency updates
|
|
10
|
+
|
|
11
|
+
- @gradio/utils@0.7.0-beta.1
|
|
12
|
+
|
|
13
|
+
## 0.2.14-beta.0
|
|
14
|
+
|
|
15
|
+
### Fixes
|
|
16
|
+
|
|
17
|
+
- [#9163](https://github.com/gradio-app/gradio/pull/9163) [`2b6cbf2`](https://github.com/gradio-app/gradio/commit/2b6cbf25908e42cf027324e54ef2cc0baad11a91) - fix exports and generate types. Thanks @pngwn!
|
|
18
|
+
|
|
19
|
+
### Dependency updates
|
|
20
|
+
|
|
21
|
+
- @gradio/utils@0.7.0-beta.0
|
|
22
|
+
|
|
23
|
+
## 0.2.13
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
- [#9118](https://github.com/gradio-app/gradio/pull/9118) [`e1c404d`](https://github.com/gradio-app/gradio/commit/e1c404da1143fb52b659d03e028bdba1badf443d) - setup npm-previews of all packages. Thanks @pngwn!
|
|
28
|
+
|
|
29
|
+
### Dependency updates
|
|
30
|
+
|
|
31
|
+
- @gradio/utils@0.6.0
|
|
32
|
+
|
|
3
33
|
## 0.2.12
|
|
4
34
|
|
|
5
35
|
### Dependency updates
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script context="module">export { TABS } from "./shared/Tabs.svelte";
|
|
2
|
+
</script>
|
|
3
|
+
|
|
4
|
+
<script>import { createEventDispatcher } from "svelte";
|
|
5
|
+
import Tabs from "./shared/Tabs.svelte";
|
|
6
|
+
const dispatch = createEventDispatcher();
|
|
7
|
+
export let visible = true;
|
|
8
|
+
export let elem_id = "";
|
|
9
|
+
export let elem_classes = [];
|
|
10
|
+
export let selected;
|
|
11
|
+
export let gradio;
|
|
12
|
+
$:
|
|
13
|
+
dispatch("prop_change", { selected });
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<Tabs
|
|
17
|
+
{visible}
|
|
18
|
+
{elem_id}
|
|
19
|
+
{elem_classes}
|
|
20
|
+
bind:selected
|
|
21
|
+
on:change={() => gradio.dispatch("change")}
|
|
22
|
+
on:select={(e) => gradio.dispatch("select", e.detail)}
|
|
23
|
+
>
|
|
24
|
+
<slot />
|
|
25
|
+
</Tabs>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
|
2
|
+
export { TABS } from "./shared/Tabs.svelte";
|
|
3
|
+
import type { Gradio, SelectData } from "@gradio/utils";
|
|
4
|
+
declare const __propDef: {
|
|
5
|
+
props: {
|
|
6
|
+
visible?: boolean | undefined;
|
|
7
|
+
elem_id?: string | undefined;
|
|
8
|
+
elem_classes?: string[] | undefined;
|
|
9
|
+
selected: number | string;
|
|
10
|
+
gradio: Gradio<{
|
|
11
|
+
change: never;
|
|
12
|
+
select: SelectData;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
events: {
|
|
16
|
+
prop_change: CustomEvent<any>;
|
|
17
|
+
} & {
|
|
18
|
+
[evt: string]: CustomEvent<any>;
|
|
19
|
+
};
|
|
20
|
+
slots: {
|
|
21
|
+
default: {};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export type IndexProps = typeof __propDef.props;
|
|
25
|
+
export type IndexEvents = typeof __propDef.events;
|
|
26
|
+
export type IndexSlots = typeof __propDef.slots;
|
|
27
|
+
export default class Index extends SvelteComponent<IndexProps, IndexEvents, IndexSlots> {
|
|
28
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
width="16"
|
|
3
|
+
height="16"
|
|
4
|
+
viewBox="0 0 16 16"
|
|
5
|
+
fill="none"
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
>
|
|
8
|
+
<circle cx="2.5" cy="8" r="1.5" fill="currentColor" />
|
|
9
|
+
<circle cx="8" cy="8" r="1.5" fill="currentColor" />
|
|
10
|
+
<circle cx="13.5" cy="8" r="1.5" fill="currentColor" />
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @typedef {typeof __propDef.props} OverflowIconProps */
|
|
2
|
+
/** @typedef {typeof __propDef.events} OverflowIconEvents */
|
|
3
|
+
/** @typedef {typeof __propDef.slots} OverflowIconSlots */
|
|
4
|
+
export default class OverflowIcon extends SvelteComponent<{
|
|
5
|
+
[x: string]: never;
|
|
6
|
+
}, {
|
|
7
|
+
[evt: string]: CustomEvent<any>;
|
|
8
|
+
}, {}> {
|
|
9
|
+
}
|
|
10
|
+
export type OverflowIconProps = typeof __propDef.props;
|
|
11
|
+
export type OverflowIconEvents = typeof __propDef.events;
|
|
12
|
+
export type OverflowIconSlots = typeof __propDef.slots;
|
|
13
|
+
import { SvelteComponent } from "svelte";
|
|
14
|
+
declare const __propDef: {
|
|
15
|
+
props: {
|
|
16
|
+
[x: string]: never;
|
|
17
|
+
};
|
|
18
|
+
events: {
|
|
19
|
+
[evt: string]: CustomEvent<any>;
|
|
20
|
+
};
|
|
21
|
+
slots: {};
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
<script context="module">
|
|
2
|
+
export const TABS = {};
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script>import {
|
|
6
|
+
setContext,
|
|
7
|
+
createEventDispatcher,
|
|
8
|
+
onMount,
|
|
9
|
+
onDestroy
|
|
10
|
+
} from "svelte";
|
|
11
|
+
import OverflowIcon from "./OverflowIcon.svelte";
|
|
12
|
+
import { writable } from "svelte/store";
|
|
13
|
+
export let visible = true;
|
|
14
|
+
export let elem_id = "id";
|
|
15
|
+
export let elem_classes = [];
|
|
16
|
+
export let selected;
|
|
17
|
+
let tabs = [];
|
|
18
|
+
let overflow_menu_open = false;
|
|
19
|
+
let overflow_menu;
|
|
20
|
+
$:
|
|
21
|
+
has_tabs = tabs.length > 0;
|
|
22
|
+
let tab_nav_el;
|
|
23
|
+
let overflow_nav;
|
|
24
|
+
const selected_tab = writable(false);
|
|
25
|
+
const selected_tab_index = writable(0);
|
|
26
|
+
const dispatch = createEventDispatcher();
|
|
27
|
+
let is_overflowing = false;
|
|
28
|
+
let overflow_has_selected_tab = false;
|
|
29
|
+
setContext(TABS, {
|
|
30
|
+
register_tab: (tab) => {
|
|
31
|
+
let index = tabs.findIndex((t) => t.id === tab.id);
|
|
32
|
+
if (index !== -1) {
|
|
33
|
+
tabs[index] = { ...tabs[index], ...tab };
|
|
34
|
+
} else {
|
|
35
|
+
tabs = [...tabs, tab];
|
|
36
|
+
index = tabs.length - 1;
|
|
37
|
+
}
|
|
38
|
+
if ($selected_tab === false && tab.visible && tab.interactive) {
|
|
39
|
+
$selected_tab = tab.id;
|
|
40
|
+
}
|
|
41
|
+
return index;
|
|
42
|
+
},
|
|
43
|
+
unregister_tab: (tab) => {
|
|
44
|
+
const index = tabs.findIndex((t) => t.id === tab.id);
|
|
45
|
+
if (index !== -1) {
|
|
46
|
+
tabs = tabs.filter((t) => t.id !== tab.id);
|
|
47
|
+
if ($selected_tab === tab.id) {
|
|
48
|
+
$selected_tab = tabs[0]?.id || false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
selected_tab,
|
|
53
|
+
selected_tab_index
|
|
54
|
+
});
|
|
55
|
+
function change_tab(id) {
|
|
56
|
+
const tab_to_activate = tabs.find((t) => t.id === id);
|
|
57
|
+
if (tab_to_activate && tab_to_activate.interactive && tab_to_activate.visible) {
|
|
58
|
+
selected = id;
|
|
59
|
+
$selected_tab = id;
|
|
60
|
+
$selected_tab_index = tabs.findIndex((t) => t.id === id);
|
|
61
|
+
dispatch("change");
|
|
62
|
+
overflow_menu_open = false;
|
|
63
|
+
} else {
|
|
64
|
+
console.warn("Attempted to select a non-interactive or hidden tab.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
$:
|
|
68
|
+
tabs, selected !== null && change_tab(selected);
|
|
69
|
+
onMount(() => {
|
|
70
|
+
handle_menu_overflow();
|
|
71
|
+
window.addEventListener("resize", handle_menu_overflow);
|
|
72
|
+
window.addEventListener("click", handle_outside_click);
|
|
73
|
+
});
|
|
74
|
+
onDestroy(() => {
|
|
75
|
+
window.removeEventListener("resize", handle_menu_overflow);
|
|
76
|
+
window.removeEventListener("click", handle_outside_click);
|
|
77
|
+
});
|
|
78
|
+
function handle_outside_click(event) {
|
|
79
|
+
if (overflow_menu_open && overflow_menu && !overflow_menu.contains(event.target)) {
|
|
80
|
+
overflow_menu_open = false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function handle_menu_overflow() {
|
|
84
|
+
if (!tab_nav_el) {
|
|
85
|
+
console.error("Menu elements not found");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let all_items = [];
|
|
89
|
+
[tab_nav_el, overflow_nav].forEach((menu) => {
|
|
90
|
+
Array.from(menu.querySelectorAll("button")).forEach(
|
|
91
|
+
(item) => all_items.push(item)
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
all_items.forEach((item) => tab_nav_el.appendChild(item));
|
|
95
|
+
const nav_items = [];
|
|
96
|
+
const overflow_items = [];
|
|
97
|
+
Array.from(tab_nav_el.querySelectorAll("button")).forEach((item) => {
|
|
98
|
+
const tab_rect = item.getBoundingClientRect();
|
|
99
|
+
const tab_menu_rect = tab_nav_el.getBoundingClientRect();
|
|
100
|
+
is_overflowing = tab_rect.right > tab_menu_rect.right || tab_rect.left < tab_menu_rect.left;
|
|
101
|
+
if (is_overflowing) {
|
|
102
|
+
overflow_items.push(item);
|
|
103
|
+
} else {
|
|
104
|
+
nav_items.push(item);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
nav_items.forEach((item) => tab_nav_el.appendChild(item));
|
|
108
|
+
overflow_items.forEach((item) => overflow_nav.appendChild(item));
|
|
109
|
+
overflow_has_selected_tab = tabs.some(
|
|
110
|
+
(t) => t.id === $selected_tab && overflow_nav.contains(document.querySelector(`[data-tab-id="${t.id}"]`))
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
{#if has_tabs}
|
|
116
|
+
<div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
|
|
117
|
+
<div class="tab-wrapper">
|
|
118
|
+
<div class="tab-container" bind:this={tab_nav_el} role="tablist">
|
|
119
|
+
{#each tabs as t, i (t.id)}
|
|
120
|
+
<button
|
|
121
|
+
role="tab"
|
|
122
|
+
class:selected={t.id === $selected_tab}
|
|
123
|
+
aria-selected={t.id === $selected_tab}
|
|
124
|
+
aria-controls={t.elem_id}
|
|
125
|
+
disabled={!t.interactive}
|
|
126
|
+
aria-disabled={!t.interactive}
|
|
127
|
+
id={t.elem_id ? t.elem_id + "-button" : null}
|
|
128
|
+
data-tab-id={t.id}
|
|
129
|
+
on:click={() => {
|
|
130
|
+
if (t.id !== $selected_tab) {
|
|
131
|
+
change_tab(t.id);
|
|
132
|
+
dispatch("select", { value: t.name, index: i });
|
|
133
|
+
}
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{t.name}
|
|
137
|
+
</button>
|
|
138
|
+
{/each}
|
|
139
|
+
</div>
|
|
140
|
+
<span
|
|
141
|
+
class="overflow-menu"
|
|
142
|
+
class:hide={!is_overflowing}
|
|
143
|
+
bind:this={overflow_menu}
|
|
144
|
+
>
|
|
145
|
+
<button
|
|
146
|
+
on:click|stopPropagation={() =>
|
|
147
|
+
(overflow_menu_open = !overflow_menu_open)}
|
|
148
|
+
class:overflow-item-selected={overflow_has_selected_tab}
|
|
149
|
+
>
|
|
150
|
+
<OverflowIcon />
|
|
151
|
+
</button>
|
|
152
|
+
<div
|
|
153
|
+
class="overflow-dropdown"
|
|
154
|
+
bind:this={overflow_nav}
|
|
155
|
+
class:hide={!overflow_menu_open}
|
|
156
|
+
/>
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
{/if}
|
|
161
|
+
|
|
162
|
+
<div>
|
|
163
|
+
<slot />
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<style>
|
|
167
|
+
.tabs {
|
|
168
|
+
position: relative;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.hide {
|
|
172
|
+
display: none;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.tab-wrapper {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
justify-content: space-between;
|
|
179
|
+
position: relative;
|
|
180
|
+
height: var(--size-8);
|
|
181
|
+
padding-bottom: var(--size-2);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.tab-container {
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
width: 100%;
|
|
188
|
+
position: relative;
|
|
189
|
+
overflow: hidden;
|
|
190
|
+
height: var(--size-8);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.tab-container::after {
|
|
194
|
+
content: "";
|
|
195
|
+
position: absolute;
|
|
196
|
+
bottom: 0;
|
|
197
|
+
left: 0;
|
|
198
|
+
right: 0;
|
|
199
|
+
height: 1px;
|
|
200
|
+
background-color: var(--border-color-primary);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.overflow-menu {
|
|
204
|
+
flex-shrink: 0;
|
|
205
|
+
margin-left: var(--size-2);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
button {
|
|
209
|
+
margin-bottom: 0;
|
|
210
|
+
border: none;
|
|
211
|
+
border-radius: 0;
|
|
212
|
+
padding: 0 var(--size-4);
|
|
213
|
+
color: var(--body-text-color-subdued);
|
|
214
|
+
font-weight: var(--section-header-text-weight);
|
|
215
|
+
font-size: var(--section-header-text-size);
|
|
216
|
+
transition: all 0.2s ease-out;
|
|
217
|
+
background-color: transparent;
|
|
218
|
+
height: 100%;
|
|
219
|
+
display: flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
white-space: nowrap;
|
|
222
|
+
position: relative;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
button:disabled {
|
|
226
|
+
opacity: 0.5;
|
|
227
|
+
cursor: not-allowed;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
button:hover:not(:disabled):not(.selected) {
|
|
231
|
+
background-color: var(--background-fill-secondary);
|
|
232
|
+
color: var(--body-text-color);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.selected {
|
|
236
|
+
background-color: transparent;
|
|
237
|
+
color: var(--color-accent);
|
|
238
|
+
position: relative;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.selected::after {
|
|
242
|
+
content: "";
|
|
243
|
+
position: absolute;
|
|
244
|
+
bottom: 0;
|
|
245
|
+
left: 0;
|
|
246
|
+
width: 100%;
|
|
247
|
+
height: 2px;
|
|
248
|
+
background-color: var(--color-accent);
|
|
249
|
+
animation: fade-grow 0.2s ease-out forwards;
|
|
250
|
+
transform-origin: center;
|
|
251
|
+
z-index: 1;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@keyframes fade-grow {
|
|
255
|
+
from {
|
|
256
|
+
opacity: 0;
|
|
257
|
+
transform: scaleX(0.8);
|
|
258
|
+
}
|
|
259
|
+
to {
|
|
260
|
+
opacity: 1;
|
|
261
|
+
transform: scaleX(1);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.overflow-dropdown {
|
|
266
|
+
position: absolute;
|
|
267
|
+
top: calc(100% + var(--size-2));
|
|
268
|
+
right: 0;
|
|
269
|
+
background-color: var(--background-fill-primary);
|
|
270
|
+
border: 1px solid var(--border-color-primary);
|
|
271
|
+
border-radius: var(--radius-sm);
|
|
272
|
+
z-index: var(--layer-5);
|
|
273
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
274
|
+
padding: var(--size-2);
|
|
275
|
+
min-width: 150px;
|
|
276
|
+
width: max-content;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.overflow-dropdown button {
|
|
280
|
+
display: block;
|
|
281
|
+
width: 100%;
|
|
282
|
+
text-align: left;
|
|
283
|
+
padding: var(--size-2);
|
|
284
|
+
white-space: nowrap;
|
|
285
|
+
overflow: hidden;
|
|
286
|
+
text-overflow: ellipsis;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.overflow-menu > button {
|
|
290
|
+
padding: var(--size-1) var(--size-2);
|
|
291
|
+
min-width: auto;
|
|
292
|
+
border: 1px solid var(--border-color-primary);
|
|
293
|
+
border-radius: var(--radius-sm);
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
justify-content: center;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.overflow-menu > button:hover {
|
|
300
|
+
background-color: var(--background-fill-secondary);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.overflow-menu :global(svg) {
|
|
304
|
+
width: 16px;
|
|
305
|
+
height: 16px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.overflow-item-selected :global(svg) {
|
|
309
|
+
color: var(--color-accent);
|
|
310
|
+
}
|
|
311
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SvelteComponent } from "svelte";
|
|
2
|
+
export declare const TABS: {};
|
|
3
|
+
import type { SelectData } from "@gradio/utils";
|
|
4
|
+
declare const __propDef: {
|
|
5
|
+
props: {
|
|
6
|
+
visible?: boolean | undefined;
|
|
7
|
+
elem_id?: string | undefined;
|
|
8
|
+
elem_classes?: string[] | undefined;
|
|
9
|
+
selected: number | string | object;
|
|
10
|
+
};
|
|
11
|
+
events: {
|
|
12
|
+
change: CustomEvent<undefined>;
|
|
13
|
+
select: CustomEvent<SelectData>;
|
|
14
|
+
} & {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
};
|
|
17
|
+
slots: {
|
|
18
|
+
default: {};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export type TabsProps = typeof __propDef.props;
|
|
22
|
+
export type TabsEvents = typeof __propDef.events;
|
|
23
|
+
export type TabsSlots = typeof __propDef.slots;
|
|
24
|
+
export default class Tabs extends SvelteComponent<TabsProps, TabsEvents, TabsSlots> {
|
|
25
|
+
}
|
|
26
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradio/tabs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-beta.1",
|
|
4
4
|
"description": "Gradio UI packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "",
|
|
@@ -9,13 +9,25 @@
|
|
|
9
9
|
"main_changeset": true,
|
|
10
10
|
"main": "Index.svelte",
|
|
11
11
|
"exports": {
|
|
12
|
-
".":
|
|
12
|
+
".": {
|
|
13
|
+
"gradio": "./Index.svelte",
|
|
14
|
+
"svelte": "./dist/Index.svelte",
|
|
15
|
+
"types": "./dist/Index.svelte.d.ts"
|
|
16
|
+
},
|
|
13
17
|
"./package.json": "./package.json"
|
|
14
18
|
},
|
|
15
19
|
"dependencies": {
|
|
16
|
-
"@gradio/utils": "^0.
|
|
20
|
+
"@gradio/utils": "^0.7.0-beta.1"
|
|
17
21
|
},
|
|
18
22
|
"devDependencies": {
|
|
19
|
-
"@gradio/preview": "^0.
|
|
23
|
+
"@gradio/preview": "^0.11.1-beta.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"svelte": "^4.0.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/gradio-app/gradio.git",
|
|
31
|
+
"directory": "js/tabs"
|
|
20
32
|
}
|
|
21
33
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
width="16"
|
|
3
|
+
height="16"
|
|
4
|
+
viewBox="0 0 16 16"
|
|
5
|
+
fill="none"
|
|
6
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
7
|
+
>
|
|
8
|
+
<circle cx="2.5" cy="8" r="1.5" fill="currentColor" />
|
|
9
|
+
<circle cx="8" cy="8" r="1.5" fill="currentColor" />
|
|
10
|
+
<circle cx="13.5" cy="8" r="1.5" fill="currentColor" />
|
|
11
|
+
</svg>
|
package/shared/Tabs.svelte
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
setContext,
|
|
8
|
+
createEventDispatcher,
|
|
9
|
+
onMount,
|
|
10
|
+
onDestroy
|
|
11
|
+
} from "svelte";
|
|
12
|
+
import OverflowIcon from "./OverflowIcon.svelte";
|
|
7
13
|
import { writable } from "svelte/store";
|
|
8
14
|
import type { SelectData } from "@gradio/utils";
|
|
9
15
|
|
|
@@ -21,6 +27,13 @@
|
|
|
21
27
|
export let selected: number | string | object;
|
|
22
28
|
|
|
23
29
|
let tabs: Tab[] = [];
|
|
30
|
+
let overflow_menu_open = false;
|
|
31
|
+
let overflow_menu: HTMLElement;
|
|
32
|
+
|
|
33
|
+
$: has_tabs = tabs.length > 0;
|
|
34
|
+
|
|
35
|
+
let tab_nav_el: HTMLElement;
|
|
36
|
+
let overflow_nav: HTMLElement;
|
|
24
37
|
|
|
25
38
|
const selected_tab = writable<false | object | number | string>(false);
|
|
26
39
|
const selected_tab_index = writable<number>(0);
|
|
@@ -29,41 +42,31 @@
|
|
|
29
42
|
select: SelectData;
|
|
30
43
|
}>();
|
|
31
44
|
|
|
45
|
+
let is_overflowing = false;
|
|
46
|
+
let overflow_has_selected_tab = false;
|
|
47
|
+
|
|
32
48
|
setContext(TABS, {
|
|
33
49
|
register_tab: (tab: Tab) => {
|
|
34
|
-
let index
|
|
35
|
-
|
|
36
|
-
if (existingTab) {
|
|
37
|
-
// update existing tab with newer values
|
|
38
|
-
index = tabs.findIndex((t) => t.id === tab.id);
|
|
50
|
+
let index = tabs.findIndex((t) => t.id === tab.id);
|
|
51
|
+
if (index !== -1) {
|
|
39
52
|
tabs[index] = { ...tabs[index], ...tab };
|
|
40
53
|
} else {
|
|
41
|
-
tabs
|
|
42
|
-
name: tab.name,
|
|
43
|
-
id: tab.id,
|
|
44
|
-
elem_id: tab.elem_id,
|
|
45
|
-
visible: tab.visible,
|
|
46
|
-
interactive: tab.interactive
|
|
47
|
-
});
|
|
54
|
+
tabs = [...tabs, tab];
|
|
48
55
|
index = tabs.length - 1;
|
|
49
56
|
}
|
|
50
|
-
selected_tab.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
let nextTab = tabs.find((t) => t.visible && t.interactive);
|
|
56
|
-
return nextTab ? nextTab.id : current;
|
|
57
|
-
});
|
|
58
|
-
tabs = tabs;
|
|
57
|
+
if ($selected_tab === false && tab.visible && tab.interactive) {
|
|
58
|
+
$selected_tab = tab.id;
|
|
59
|
+
}
|
|
59
60
|
return index;
|
|
60
61
|
},
|
|
61
62
|
unregister_tab: (tab: Tab) => {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const index = tabs.findIndex((t) => t.id === tab.id);
|
|
64
|
+
if (index !== -1) {
|
|
65
|
+
tabs = tabs.filter((t) => t.id !== tab.id);
|
|
66
|
+
if ($selected_tab === tab.id) {
|
|
67
|
+
$selected_tab = tabs[0]?.id || false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
67
70
|
},
|
|
68
71
|
selected_tab,
|
|
69
72
|
selected_tab_index
|
|
@@ -80,47 +83,127 @@
|
|
|
80
83
|
$selected_tab = id;
|
|
81
84
|
$selected_tab_index = tabs.findIndex((t) => t.id === id);
|
|
82
85
|
dispatch("change");
|
|
86
|
+
overflow_menu_open = false;
|
|
83
87
|
} else {
|
|
84
88
|
console.warn("Attempted to select a non-interactive or hidden tab.");
|
|
85
89
|
}
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
$: tabs, selected !== null && change_tab(selected);
|
|
93
|
+
|
|
94
|
+
onMount(() => {
|
|
95
|
+
handle_menu_overflow();
|
|
96
|
+
window.addEventListener("resize", handle_menu_overflow);
|
|
97
|
+
window.addEventListener("click", handle_outside_click);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
onDestroy(() => {
|
|
101
|
+
window.removeEventListener("resize", handle_menu_overflow);
|
|
102
|
+
window.removeEventListener("click", handle_outside_click);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
function handle_outside_click(event: MouseEvent): void {
|
|
106
|
+
if (
|
|
107
|
+
overflow_menu_open &&
|
|
108
|
+
overflow_menu &&
|
|
109
|
+
!overflow_menu.contains(event.target as Node)
|
|
110
|
+
) {
|
|
111
|
+
overflow_menu_open = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function handle_menu_overflow(): void {
|
|
116
|
+
if (!tab_nav_el) {
|
|
117
|
+
console.error("Menu elements not found");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let all_items: HTMLElement[] = [];
|
|
122
|
+
|
|
123
|
+
[tab_nav_el, overflow_nav].forEach((menu) => {
|
|
124
|
+
Array.from(menu.querySelectorAll("button")).forEach((item) =>
|
|
125
|
+
all_items.push(item as HTMLElement)
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
all_items.forEach((item) => tab_nav_el.appendChild(item));
|
|
130
|
+
|
|
131
|
+
const nav_items: HTMLElement[] = [];
|
|
132
|
+
const overflow_items: HTMLElement[] = [];
|
|
133
|
+
|
|
134
|
+
Array.from(tab_nav_el.querySelectorAll("button")).forEach((item) => {
|
|
135
|
+
const tab_rect = item.getBoundingClientRect();
|
|
136
|
+
const tab_menu_rect = tab_nav_el.getBoundingClientRect();
|
|
137
|
+
is_overflowing =
|
|
138
|
+
tab_rect.right > tab_menu_rect.right ||
|
|
139
|
+
tab_rect.left < tab_menu_rect.left;
|
|
140
|
+
|
|
141
|
+
if (is_overflowing) {
|
|
142
|
+
overflow_items.push(item as HTMLElement);
|
|
143
|
+
} else {
|
|
144
|
+
nav_items.push(item as HTMLElement);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
nav_items.forEach((item) => tab_nav_el.appendChild(item));
|
|
149
|
+
overflow_items.forEach((item) => overflow_nav.appendChild(item));
|
|
150
|
+
|
|
151
|
+
overflow_has_selected_tab = tabs.some(
|
|
152
|
+
(t) =>
|
|
153
|
+
t.id === $selected_tab &&
|
|
154
|
+
overflow_nav.contains(document.querySelector(`[data-tab-id="${t.id}"]`))
|
|
155
|
+
);
|
|
156
|
+
}
|
|
89
157
|
</script>
|
|
90
158
|
|
|
91
|
-
|
|
92
|
-
<div class="
|
|
93
|
-
|
|
94
|
-
{
|
|
95
|
-
{#
|
|
96
|
-
<button
|
|
97
|
-
role="tab"
|
|
98
|
-
class="selected"
|
|
99
|
-
aria-selected={true}
|
|
100
|
-
aria-controls={t.elem_id}
|
|
101
|
-
id={t.elem_id ? t.elem_id + "-button" : null}
|
|
102
|
-
>
|
|
103
|
-
{t.name}
|
|
104
|
-
</button>
|
|
105
|
-
{:else}
|
|
159
|
+
{#if has_tabs}
|
|
160
|
+
<div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
|
|
161
|
+
<div class="tab-wrapper">
|
|
162
|
+
<div class="tab-container" bind:this={tab_nav_el} role="tablist">
|
|
163
|
+
{#each tabs as t, i (t.id)}
|
|
106
164
|
<button
|
|
107
165
|
role="tab"
|
|
108
|
-
|
|
166
|
+
class:selected={t.id === $selected_tab}
|
|
167
|
+
aria-selected={t.id === $selected_tab}
|
|
109
168
|
aria-controls={t.elem_id}
|
|
110
169
|
disabled={!t.interactive}
|
|
111
170
|
aria-disabled={!t.interactive}
|
|
112
171
|
id={t.elem_id ? t.elem_id + "-button" : null}
|
|
172
|
+
data-tab-id={t.id}
|
|
113
173
|
on:click={() => {
|
|
114
|
-
|
|
115
|
-
|
|
174
|
+
if (t.id !== $selected_tab) {
|
|
175
|
+
change_tab(t.id);
|
|
176
|
+
dispatch("select", { value: t.name, index: i });
|
|
177
|
+
}
|
|
116
178
|
}}
|
|
117
179
|
>
|
|
118
180
|
{t.name}
|
|
119
181
|
</button>
|
|
120
|
-
{/
|
|
121
|
-
|
|
122
|
-
|
|
182
|
+
{/each}
|
|
183
|
+
</div>
|
|
184
|
+
<span
|
|
185
|
+
class="overflow-menu"
|
|
186
|
+
class:hide={!is_overflowing}
|
|
187
|
+
bind:this={overflow_menu}
|
|
188
|
+
>
|
|
189
|
+
<button
|
|
190
|
+
on:click|stopPropagation={() =>
|
|
191
|
+
(overflow_menu_open = !overflow_menu_open)}
|
|
192
|
+
class:overflow-item-selected={overflow_has_selected_tab}
|
|
193
|
+
>
|
|
194
|
+
<OverflowIcon />
|
|
195
|
+
</button>
|
|
196
|
+
<div
|
|
197
|
+
class="overflow-dropdown"
|
|
198
|
+
bind:this={overflow_nav}
|
|
199
|
+
class:hide={!overflow_menu_open}
|
|
200
|
+
/>
|
|
201
|
+
</span>
|
|
202
|
+
</div>
|
|
123
203
|
</div>
|
|
204
|
+
{/if}
|
|
205
|
+
|
|
206
|
+
<div>
|
|
124
207
|
<slot />
|
|
125
208
|
</div>
|
|
126
209
|
|
|
@@ -133,50 +216,140 @@
|
|
|
133
216
|
display: none;
|
|
134
217
|
}
|
|
135
218
|
|
|
136
|
-
.tab-
|
|
219
|
+
.tab-wrapper {
|
|
137
220
|
display: flex;
|
|
221
|
+
align-items: center;
|
|
222
|
+
justify-content: space-between;
|
|
138
223
|
position: relative;
|
|
139
|
-
|
|
140
|
-
|
|
224
|
+
height: var(--size-8);
|
|
225
|
+
padding-bottom: var(--size-2);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.tab-container {
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
width: 100%;
|
|
232
|
+
position: relative;
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
height: var(--size-8);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.tab-container::after {
|
|
238
|
+
content: "";
|
|
239
|
+
position: absolute;
|
|
240
|
+
bottom: 0;
|
|
241
|
+
left: 0;
|
|
242
|
+
right: 0;
|
|
243
|
+
height: 1px;
|
|
244
|
+
background-color: var(--border-color-primary);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.overflow-menu {
|
|
248
|
+
flex-shrink: 0;
|
|
249
|
+
margin-left: var(--size-2);
|
|
141
250
|
}
|
|
142
251
|
|
|
143
252
|
button {
|
|
144
|
-
margin-bottom:
|
|
145
|
-
border:
|
|
146
|
-
border-
|
|
147
|
-
|
|
148
|
-
border-top-right-radius: var(--container-radius);
|
|
149
|
-
border-top-left-radius: var(--container-radius);
|
|
150
|
-
padding: var(--size-1) var(--size-4);
|
|
253
|
+
margin-bottom: 0;
|
|
254
|
+
border: none;
|
|
255
|
+
border-radius: 0;
|
|
256
|
+
padding: 0 var(--size-4);
|
|
151
257
|
color: var(--body-text-color-subdued);
|
|
152
258
|
font-weight: var(--section-header-text-weight);
|
|
153
259
|
font-size: var(--section-header-text-size);
|
|
260
|
+
transition: all 0.2s ease-out;
|
|
261
|
+
background-color: transparent;
|
|
262
|
+
height: 100%;
|
|
263
|
+
display: flex;
|
|
264
|
+
align-items: center;
|
|
265
|
+
white-space: nowrap;
|
|
266
|
+
position: relative;
|
|
154
267
|
}
|
|
155
268
|
|
|
156
269
|
button:disabled {
|
|
157
|
-
color: var(--body-text-color-subdued);
|
|
158
270
|
opacity: 0.5;
|
|
159
271
|
cursor: not-allowed;
|
|
160
272
|
}
|
|
161
273
|
|
|
162
|
-
button:hover {
|
|
274
|
+
button:hover:not(:disabled):not(.selected) {
|
|
275
|
+
background-color: var(--background-fill-secondary);
|
|
163
276
|
color: var(--body-text-color);
|
|
164
277
|
}
|
|
278
|
+
|
|
165
279
|
.selected {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
280
|
+
background-color: transparent;
|
|
281
|
+
color: var(--color-accent);
|
|
282
|
+
position: relative;
|
|
169
283
|
}
|
|
170
284
|
|
|
171
|
-
.
|
|
172
|
-
|
|
285
|
+
.selected::after {
|
|
286
|
+
content: "";
|
|
173
287
|
position: absolute;
|
|
174
|
-
bottom:
|
|
288
|
+
bottom: 0;
|
|
175
289
|
left: 0;
|
|
176
|
-
z-index: 999;
|
|
177
|
-
background: var(--background-fill-primary);
|
|
178
290
|
width: 100%;
|
|
179
291
|
height: 2px;
|
|
180
|
-
|
|
292
|
+
background-color: var(--color-accent);
|
|
293
|
+
animation: fade-grow 0.2s ease-out forwards;
|
|
294
|
+
transform-origin: center;
|
|
295
|
+
z-index: 1;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
@keyframes fade-grow {
|
|
299
|
+
from {
|
|
300
|
+
opacity: 0;
|
|
301
|
+
transform: scaleX(0.8);
|
|
302
|
+
}
|
|
303
|
+
to {
|
|
304
|
+
opacity: 1;
|
|
305
|
+
transform: scaleX(1);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.overflow-dropdown {
|
|
310
|
+
position: absolute;
|
|
311
|
+
top: calc(100% + var(--size-2));
|
|
312
|
+
right: 0;
|
|
313
|
+
background-color: var(--background-fill-primary);
|
|
314
|
+
border: 1px solid var(--border-color-primary);
|
|
315
|
+
border-radius: var(--radius-sm);
|
|
316
|
+
z-index: var(--layer-5);
|
|
317
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
318
|
+
padding: var(--size-2);
|
|
319
|
+
min-width: 150px;
|
|
320
|
+
width: max-content;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.overflow-dropdown button {
|
|
324
|
+
display: block;
|
|
325
|
+
width: 100%;
|
|
326
|
+
text-align: left;
|
|
327
|
+
padding: var(--size-2);
|
|
328
|
+
white-space: nowrap;
|
|
329
|
+
overflow: hidden;
|
|
330
|
+
text-overflow: ellipsis;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.overflow-menu > button {
|
|
334
|
+
padding: var(--size-1) var(--size-2);
|
|
335
|
+
min-width: auto;
|
|
336
|
+
border: 1px solid var(--border-color-primary);
|
|
337
|
+
border-radius: var(--radius-sm);
|
|
338
|
+
display: flex;
|
|
339
|
+
align-items: center;
|
|
340
|
+
justify-content: center;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.overflow-menu > button:hover {
|
|
344
|
+
background-color: var(--background-fill-secondary);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.overflow-menu :global(svg) {
|
|
348
|
+
width: 16px;
|
|
349
|
+
height: 16px;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.overflow-item-selected :global(svg) {
|
|
353
|
+
color: var(--color-accent);
|
|
181
354
|
}
|
|
182
355
|
</style>
|