@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 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.2.12",
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
- ".": "./Index.svelte",
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.5.2"
20
+ "@gradio/utils": "^0.7.0-beta.1"
17
21
  },
18
22
  "devDependencies": {
19
- "@gradio/preview": "^0.10.2"
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>
@@ -3,7 +3,13 @@
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
- import { setContext, createEventDispatcher } from "svelte";
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: number;
35
- let existingTab = tabs.find((t) => t.id === tab.id);
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.push({
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.update((current) => {
51
- if (current === false && tab.visible && tab.interactive) {
52
- return tab.id;
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 i = tabs.findIndex((t) => t.id === tab.id);
63
- tabs.splice(i, 1);
64
- selected_tab.update((current) =>
65
- current === tab.id ? tabs[i]?.id || tabs[tabs.length - 1]?.id : current
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
- <div class="tabs {elem_classes.join(' ')}" class:hide={!visible} id={elem_id}>
92
- <div class="tab-nav scroll-hide" role="tablist">
93
- {#each tabs as t, i (t.id)}
94
- {#if t.visible}
95
- {#if t.id === $selected_tab}
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
- aria-selected={false}
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
- change_tab(t.id);
115
- dispatch("select", { value: t.name, index: i });
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
- {/if}
121
- {/if}
122
- {/each}
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-nav {
219
+ .tab-wrapper {
137
220
  display: flex;
221
+ align-items: center;
222
+ justify-content: space-between;
138
223
  position: relative;
139
- flex-wrap: wrap;
140
- border-bottom: 1px solid var(--border-color-primary);
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: -1px;
145
- border: 1px solid transparent;
146
- border-color: transparent;
147
- border-bottom: none;
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
- border-color: var(--border-color-primary);
167
- background: var(--background-fill-primary);
168
- color: var(--body-text-color);
280
+ background-color: transparent;
281
+ color: var(--color-accent);
282
+ position: relative;
169
283
  }
170
284
 
171
- .bar {
172
- display: block;
285
+ .selected::after {
286
+ content: "";
173
287
  position: absolute;
174
- bottom: -2px;
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
- content: "";
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>