@gradio/tabs 0.2.14 → 0.3.0-beta.2

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