@heyramzi/ui 3.2.0 → 3.3.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/dist/components/list-page/toolbar-density.js +20 -1
- package/dist/shadcn/drawer/index.js +14 -0
- package/package.json +7 -2
- package/scripts/sync-shadcn.mjs +0 -1
- package/src/components/list-page/filter-popover.svelte +17 -2
- package/src/components/list-page/list-page-shell.svelte +155 -69
- package/src/components/list-page/list-page-shell.svelte.d.ts +6 -1
- package/src/components/list-page/toolbar-density.ts +24 -1
- package/src/shadcn/drawer/drawer-close.svelte +7 -0
- package/src/shadcn/drawer/drawer-close.svelte.d.ts +4 -0
- package/src/shadcn/drawer/drawer-content.svelte +36 -0
- package/src/shadcn/drawer/drawer-content.svelte.d.ts +10 -0
- package/src/shadcn/drawer/drawer-description.svelte +17 -0
- package/src/shadcn/drawer/drawer-description.svelte.d.ts +4 -0
- package/src/shadcn/drawer/drawer-footer.svelte +20 -0
- package/src/shadcn/drawer/drawer-footer.svelte.d.ts +5 -0
- package/src/shadcn/drawer/drawer-header.svelte +23 -0
- package/src/shadcn/drawer/drawer-header.svelte.d.ts +5 -0
- package/src/shadcn/drawer/drawer-nested.svelte +12 -0
- package/src/shadcn/drawer/drawer-nested.svelte.d.ts +3 -0
- package/src/shadcn/drawer/drawer-overlay.svelte +20 -0
- package/src/shadcn/drawer/drawer-overlay.svelte.d.ts +4 -0
- package/src/shadcn/drawer/drawer-portal.svelte +7 -0
- package/src/shadcn/drawer/drawer-portal.svelte.d.ts +3 -0
- package/src/shadcn/drawer/drawer-title.svelte +17 -0
- package/src/shadcn/drawer/drawer-title.svelte.d.ts +4 -0
- package/src/shadcn/drawer/drawer-trigger.svelte +7 -0
- package/src/shadcn/drawer/drawer-trigger.svelte.d.ts +4 -0
- package/src/shadcn/drawer/drawer.svelte +12 -0
- package/src/shadcn/drawer/drawer.svelte.d.ts +3 -0
- package/src/shadcn/drawer/index.ts +38 -0
|
@@ -15,12 +15,31 @@ export function getToolbarDensity() {
|
|
|
15
15
|
*/
|
|
16
16
|
// WHY `max-content` on a probe, not the row's own width: the row is sized by
|
|
17
17
|
// its container, so reading it back always says "it fits" (see collapse.ts).
|
|
18
|
+
// WHY `flex: none` too: a row that is itself a flex item takes its width from
|
|
19
|
+
// flex-basis, so `max-content` alone read back the container (10 Nov 2026).
|
|
20
|
+
// WHY transitions are stopped: under reduced motion `tokens/base.css` gives every
|
|
21
|
+
// element an 80ms transition on `all`, so the probe's own width change started a
|
|
22
|
+
// transition and the read got the old width. The row never folded at 390px and
|
|
23
|
+
// axe flagged the squeezed search field (gallery, 10 Nov 2026). The last fold's
|
|
24
|
+
// gap and shrink changes are still running then too, so they're finished first.
|
|
18
25
|
export function naturalRowWidth(row) {
|
|
19
|
-
const {
|
|
26
|
+
for (const animation of row.getAnimations({ subtree: true })) {
|
|
27
|
+
if (animation instanceof CSSTransition) animation.finish();
|
|
28
|
+
}
|
|
29
|
+
const { width, flexWrap, flex } = row.style;
|
|
30
|
+
const transition = row.style.getPropertyValue("transition");
|
|
31
|
+
const priority = row.style.getPropertyPriority("transition");
|
|
32
|
+
row.style.setProperty("transition", "none", "important");
|
|
20
33
|
row.style.width = "max-content";
|
|
21
34
|
row.style.flexWrap = "nowrap";
|
|
35
|
+
row.style.flex = "none";
|
|
22
36
|
const needed = row.getBoundingClientRect().width;
|
|
23
37
|
row.style.width = width;
|
|
24
38
|
row.style.flexWrap = flexWrap;
|
|
39
|
+
row.style.flex = flex;
|
|
40
|
+
// The restored size has to settle before transitions come back, or the
|
|
41
|
+
// row would animate back from its probe width.
|
|
42
|
+
void row.offsetWidth;
|
|
43
|
+
row.style.setProperty("transition", transition, priority);
|
|
25
44
|
return needed;
|
|
26
45
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Close from "../../../src/shadcn/drawer/drawer-close.svelte";
|
|
2
|
+
import Content from "../../../src/shadcn/drawer/drawer-content.svelte";
|
|
3
|
+
import Description from "../../../src/shadcn/drawer/drawer-description.svelte";
|
|
4
|
+
import Footer from "../../../src/shadcn/drawer/drawer-footer.svelte";
|
|
5
|
+
import Header from "../../../src/shadcn/drawer/drawer-header.svelte";
|
|
6
|
+
import NestedRoot from "../../../src/shadcn/drawer/drawer-nested.svelte";
|
|
7
|
+
import Overlay from "../../../src/shadcn/drawer/drawer-overlay.svelte";
|
|
8
|
+
import Portal from "../../../src/shadcn/drawer/drawer-portal.svelte";
|
|
9
|
+
import Title from "../../../src/shadcn/drawer/drawer-title.svelte";
|
|
10
|
+
import Trigger from "../../../src/shadcn/drawer/drawer-trigger.svelte";
|
|
11
|
+
import Root from "../../../src/shadcn/drawer/drawer.svelte";
|
|
12
|
+
export { Root, NestedRoot, Content, Description, Overlay, Footer, Header, Title, Trigger, Portal, Close,
|
|
13
|
+
//
|
|
14
|
+
Root as Drawer, NestedRoot as DrawerNestedRoot, Content as DrawerContent, Description as DrawerDescription, Overlay as DrawerOverlay, Footer as DrawerFooter, Header as DrawerHeader, Title as DrawerTitle, Trigger as DrawerTrigger, Portal as DrawerPortal, Close as DrawerClose };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyramzi/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Svelte 5 UI for apps: primitives, effects and animations, plus work (tasks and docs), charts, slides, an editor, onboarding and a canvas as subpaths.",
|
|
5
5
|
"homepage": "https://ui.heyramzi.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -353,6 +353,7 @@
|
|
|
353
353
|
"svelte-sonner": "^1.2.3",
|
|
354
354
|
"svelte2tsx": "^0.7.63",
|
|
355
355
|
"typescript": "~7.0.2",
|
|
356
|
+
"vaul-svelte": "1.0.0-next.7",
|
|
356
357
|
"vite-plus": "1.5.0"
|
|
357
358
|
},
|
|
358
359
|
"peerDependencies": {
|
|
@@ -361,7 +362,8 @@
|
|
|
361
362
|
"bits-ui": "^2.19.5",
|
|
362
363
|
"mode-watcher": "^1.1.0",
|
|
363
364
|
"svelte": "^5.57.2",
|
|
364
|
-
"svelte-sonner": "^1.2.1"
|
|
365
|
+
"svelte-sonner": "^1.2.1",
|
|
366
|
+
"vaul-svelte": "^1.0.0-next.7"
|
|
365
367
|
},
|
|
366
368
|
"peerDependenciesMeta": {
|
|
367
369
|
"mode-watcher": {
|
|
@@ -375,6 +377,9 @@
|
|
|
375
377
|
},
|
|
376
378
|
"@xyflow/svelte": {
|
|
377
379
|
"optional": true
|
|
380
|
+
},
|
|
381
|
+
"vaul-svelte": {
|
|
382
|
+
"optional": true
|
|
378
383
|
}
|
|
379
384
|
},
|
|
380
385
|
"scripts": {
|
package/scripts/sync-shadcn.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { cn } from "../../lib/utils.js";
|
|
10
10
|
import { Button } from "../../shadcn/button/index.js";
|
|
11
11
|
import { toolbarTriggerClass } from "../toolbar/toolbar-trigger.js";
|
|
12
|
+
import { getToolbarDensity } from "./toolbar-density.js";
|
|
12
13
|
import CountChip from "../chip/count-chip.svelte";
|
|
13
14
|
import type { PhosphorIcon } from "../../lib/icon-types.js";
|
|
14
15
|
|
|
@@ -59,6 +60,10 @@
|
|
|
59
60
|
// =========================
|
|
60
61
|
let hasCount = $derived(count > 0);
|
|
61
62
|
let isActive = $derived(active ?? hasCount);
|
|
63
|
+
// In a list-page toolbar that ran out of room even with its tabs folded,
|
|
64
|
+
// the trigger keeps its icon and count and drops the word. See `ListPageShell`.
|
|
65
|
+
const density = getToolbarDensity();
|
|
66
|
+
let iconOnly = $derived(density?.tight === true);
|
|
62
67
|
|
|
63
68
|
// =========================
|
|
64
69
|
// STATE
|
|
@@ -80,9 +85,19 @@
|
|
|
80
85
|
<!-- MARKUP -->
|
|
81
86
|
<!-- =========================== -->
|
|
82
87
|
<DropdownMenu.Root bind:open>
|
|
83
|
-
<DropdownMenu.Trigger
|
|
88
|
+
<DropdownMenu.Trigger
|
|
89
|
+
class={toolbarTriggerClass({
|
|
90
|
+
active: isActive,
|
|
91
|
+
className: cn(
|
|
92
|
+
iconOnly && !hasCount && !trailing && "aspect-square justify-center px-0",
|
|
93
|
+
triggerClass,
|
|
94
|
+
),
|
|
95
|
+
})}
|
|
96
|
+
title={iconOnly ? label : undefined}
|
|
97
|
+
>
|
|
84
98
|
<IconComponent aria-hidden="true" class="size-3.5" />
|
|
85
|
-
|
|
99
|
+
<!-- Hidden, not dropped: the label stays the trigger's accessible name. -->
|
|
100
|
+
<span class={cn(iconOnly && "sr-only")}>{label}</span>
|
|
86
101
|
{#if trailing}
|
|
87
102
|
{@render trailing()}
|
|
88
103
|
{/if}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// =========================
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
|
-
import type
|
|
5
|
+
import { flushSync, tick, untrack, type Snippet } from "svelte";
|
|
6
|
+
import { MagnifyingGlass, X } from "phosphor-svelte";
|
|
7
|
+
import { toolbarTriggerClass } from "../toolbar/toolbar-trigger.js";
|
|
6
8
|
import EmptyState from "./empty-state.svelte";
|
|
7
9
|
import { cn } from "../../lib/utils.js";
|
|
8
10
|
import { pageGutterClass } from "../../lib/layout.js";
|
|
@@ -40,6 +42,11 @@
|
|
|
40
42
|
content?: Snippet;
|
|
41
43
|
empty?: Snippet;
|
|
42
44
|
extra?: Snippet;
|
|
45
|
+
/** The folded search icon's name, on a toolbar too narrow for the field.
|
|
46
|
+
* Translate at the call site. */
|
|
47
|
+
searchLabel?: string;
|
|
48
|
+
/** The close button's name once the folded search is open. */
|
|
49
|
+
closeSearchLabel?: string;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
// =========================
|
|
@@ -61,6 +68,8 @@
|
|
|
61
68
|
content,
|
|
62
69
|
empty,
|
|
63
70
|
extra,
|
|
71
|
+
searchLabel = "Search",
|
|
72
|
+
closeSearchLabel = "Close search",
|
|
64
73
|
}: Props = $props();
|
|
65
74
|
|
|
66
75
|
// =========================
|
|
@@ -69,16 +78,33 @@
|
|
|
69
78
|
// Null off an app shell, where the toolbar just has no section strip to draw.
|
|
70
79
|
const section = getSectionHost();
|
|
71
80
|
let row = $state<HTMLElement>();
|
|
72
|
-
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
81
|
+
// How far the row has folded to stay on one line. 0 is full size; 1 folds
|
|
82
|
+
// the tabs into a dropdown; 2 also drops the filter and display labels;
|
|
83
|
+
// 3 also turns search into an icon that opens the field across the row.
|
|
84
|
+
type Level = 0 | 1 | 2 | 3;
|
|
85
|
+
const DEEPEST: Level = 3;
|
|
86
|
+
let level = $state<Level>(0);
|
|
87
|
+
let searchOpen = $state(false);
|
|
88
|
+
let searchLayer = $state<HTMLElement>();
|
|
76
89
|
setToolbarDensity({
|
|
77
90
|
get compact() {
|
|
78
|
-
return
|
|
91
|
+
return level >= 1;
|
|
92
|
+
},
|
|
93
|
+
get tight() {
|
|
94
|
+
return level >= 2;
|
|
79
95
|
},
|
|
80
96
|
});
|
|
81
97
|
|
|
98
|
+
// =========================
|
|
99
|
+
// HANDLERS
|
|
100
|
+
// =========================
|
|
101
|
+
// Focus lands in the field, since opening it is asking to type.
|
|
102
|
+
async function openSearch() {
|
|
103
|
+
searchOpen = true;
|
|
104
|
+
await tick();
|
|
105
|
+
searchLayer?.querySelector<HTMLElement>("input, [contenteditable]")?.focus();
|
|
106
|
+
}
|
|
107
|
+
|
|
82
108
|
// =========================
|
|
83
109
|
// DERIVED
|
|
84
110
|
// =========================
|
|
@@ -101,22 +127,39 @@
|
|
|
101
127
|
if (!hasToolbar) return;
|
|
102
128
|
return section?.claim();
|
|
103
129
|
});
|
|
130
|
+
// Widened past the last fold, the field is back in the row, so the open
|
|
131
|
+
// layer has nothing left to cover.
|
|
132
|
+
$effect(() => {
|
|
133
|
+
if (level < DEEPEST) searchOpen = false;
|
|
134
|
+
});
|
|
104
135
|
// One row, never two (Ramzi, 9 Nov 2026): /tasks at 1500px put its status
|
|
105
136
|
// tabs and actions on a second line under search and filters.
|
|
106
|
-
//
|
|
107
|
-
//
|
|
137
|
+
// Then 620px and 420px still wrapped, the filters on a line of their own, so
|
|
138
|
+
// a second fold drops their labels (10 Nov 2026).
|
|
139
|
+
// WHY each level's width is remembered: a folded row only holds the smaller
|
|
140
|
+
// controls, and a probe of it can't say whether the larger ones would fit.
|
|
141
|
+
// The probe refreshes the current level's entry, so new counts or chips
|
|
142
|
+
// cost at most one extra step before the row settles.
|
|
108
143
|
$effect(() => {
|
|
109
144
|
if (!row) return;
|
|
110
145
|
const el = row;
|
|
111
|
-
|
|
146
|
+
const needs = [Infinity, Infinity, Infinity, Infinity];
|
|
112
147
|
let frame = 0;
|
|
148
|
+
// WHY settled in one pass with flushSync, not a frame per level: a phone
|
|
149
|
+
// load walked 0 to 3 over four frames, and axe read the squeezed search
|
|
150
|
+
// field in between (gallery, 390px).
|
|
113
151
|
const measure = () => {
|
|
114
152
|
frame = 0;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
153
|
+
for (let step = 0; step <= DEEPEST * 2; step++) {
|
|
154
|
+
const room = el.clientWidth + 0.5;
|
|
155
|
+
const at = untrack(() => level);
|
|
156
|
+
needs[at] = naturalRowWidth(el);
|
|
157
|
+
let next = at;
|
|
158
|
+
if (needs[at] > room && at < DEEPEST) next = (at + 1) as Level;
|
|
159
|
+
else if (at > 0 && needs[at - 1] <= room) next = (at - 1) as Level;
|
|
160
|
+
if (next === at) return;
|
|
161
|
+
flushSync(() => (level = next));
|
|
162
|
+
}
|
|
120
163
|
};
|
|
121
164
|
// A frame's delay folds a burst of resizes into one probe and keeps the
|
|
122
165
|
// observer from looping on its own state change.
|
|
@@ -160,68 +203,111 @@
|
|
|
160
203
|
{#if hasToolbar}
|
|
161
204
|
<!-- One row on every list page: search and filters left, then tabs,
|
|
162
205
|
view toggle and primary action right. -->
|
|
163
|
-
<!--
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
and clipped a 733px toolbar into a 538px column. -->
|
|
206
|
+
<!-- The row never wraps. Short of room it folds the tabs, then drops the
|
|
207
|
+
filter and display labels; past that, search and the filters strip
|
|
208
|
+
shrink and the strip scrolls. The actions never shrink. -->
|
|
167
209
|
<!-- Sticky, because the section tabs live here. `-mx-4 px-4` covers the
|
|
168
210
|
gutter, `-mt-4 py-4` pulls the top padding inside the sticky box. -->
|
|
211
|
+
<!-- `data-fold` lets a caller's own control fold too: a primary button
|
|
212
|
+
writes `group-data-[fold=3]/toolbar:sr-only` on its label. -->
|
|
169
213
|
<div
|
|
170
|
-
|
|
214
|
+
data-fold={level}
|
|
215
|
+
class="group/toolbar @container/toolbar sticky top-0 z-20 -mx-4 -mt-4 bg-card/95 px-4 py-4 backdrop-blur supports-[backdrop-filter]:bg-card/70"
|
|
171
216
|
>
|
|
172
|
-
<!--
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
compact && "@3xl/toolbar:flex-nowrap",
|
|
179
|
-
toolbarClass,
|
|
180
|
-
)}
|
|
181
|
-
>
|
|
182
|
-
<!-- The section tabs lead: which page comes before which slice of it.
|
|
183
|
-
One row replaces two rows that held four controls. -->
|
|
217
|
+
<!-- WHY the section strip sits outside the measured row: it's the nav
|
|
218
|
+
between pages, and under 768px it takes a line of its own above the
|
|
219
|
+
toolbar. Counted in the probe, it folded controls that had room. -->
|
|
220
|
+
<!-- A container query, not a breakpoint: an 834px iPad read as desktop
|
|
221
|
+
and clipped a 733px toolbar into a 538px column. -->
|
|
222
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
184
223
|
{#if section?.strip}
|
|
185
|
-
<SectionRuns
|
|
186
|
-
strip={section.strip}
|
|
187
|
-
class="order-first w-full max-w-full @3xl/toolbar:w-auto"
|
|
188
|
-
/>
|
|
189
|
-
{/if}
|
|
190
|
-
{#if search}
|
|
191
|
-
<div class="order-1 flex min-w-0 flex-1 items-center @3xl/toolbar:flex-none">
|
|
192
|
-
{@render search()}
|
|
193
|
-
</div>
|
|
224
|
+
<SectionRuns strip={section.strip} class="w-full max-w-full @3xl/toolbar:w-auto" />
|
|
194
225
|
{/if}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
226
|
+
<div
|
|
227
|
+
bind:this={row}
|
|
228
|
+
class={cn(
|
|
229
|
+
"relative flex min-w-0 flex-1 flex-nowrap items-center",
|
|
230
|
+
// Icon triggers sit closer: at 390px the last fold was 4px over.
|
|
231
|
+
level >= 2 ? "gap-1.5" : "gap-2",
|
|
232
|
+
toolbarClass,
|
|
233
|
+
)}
|
|
234
|
+
>
|
|
235
|
+
{#if search && level < DEEPEST}
|
|
236
|
+
<div class="flex min-w-0 shrink items-center">
|
|
237
|
+
{@render search()}
|
|
238
|
+
</div>
|
|
239
|
+
{:else if search && !searchOpen}
|
|
240
|
+
<!-- WHY a fold and not a squeeze: at 390px the field shrank under
|
|
241
|
+
axe's 24px target size and showed two letters of its placeholder. -->
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
class={toolbarTriggerClass({
|
|
245
|
+
className: "aspect-square shrink-0 justify-center px-0",
|
|
246
|
+
})}
|
|
247
|
+
aria-label={searchLabel}
|
|
248
|
+
title={searchLabel}
|
|
249
|
+
onclick={openSearch}
|
|
250
|
+
>
|
|
251
|
+
<MagnifyingGlass aria-hidden="true" class="size-3.5" />
|
|
252
|
+
</button>
|
|
253
|
+
{:else if search}
|
|
254
|
+
<!-- Open, the field covers the whole row; Escape or the close
|
|
255
|
+
button hands the row back, with the query kept. -->
|
|
256
|
+
<div
|
|
257
|
+
bind:this={searchLayer}
|
|
258
|
+
role="search"
|
|
259
|
+
class="absolute inset-0 z-10 flex items-center gap-2 bg-card"
|
|
260
|
+
onkeydown={(event) => {
|
|
261
|
+
if (event.key === "Escape") searchOpen = false;
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<div class="flex min-w-0 flex-1 items-center [&>*]:w-full [&>*]:max-w-none">
|
|
265
|
+
{@render search()}
|
|
211
266
|
</div>
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
class={toolbarTriggerClass({
|
|
270
|
+
className: "aspect-square shrink-0 justify-center px-0",
|
|
271
|
+
})}
|
|
272
|
+
aria-label={closeSearchLabel}
|
|
273
|
+
onclick={() => (searchOpen = false)}
|
|
274
|
+
>
|
|
275
|
+
<X aria-hidden="true" class="size-3.5" />
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
{/if}
|
|
279
|
+
{#if filters}
|
|
280
|
+
<!-- Tight, the strip stops shrinking: squeezed under its icon
|
|
281
|
+
trigger, the trigger slid beneath the tabs (420px). Its
|
|
282
|
+
chips scroll inside a capped width instead. -->
|
|
283
|
+
<div
|
|
284
|
+
class={cn(
|
|
285
|
+
"no-scrollbar flex min-w-0 items-center gap-2 overflow-x-auto",
|
|
286
|
+
level >= 2 ? "max-w-40 shrink-0" : "shrink",
|
|
287
|
+
)}
|
|
288
|
+
>
|
|
289
|
+
{@render filters()}
|
|
290
|
+
</div>
|
|
291
|
+
{/if}
|
|
292
|
+
{#if tabs || rightActions}
|
|
293
|
+
<div
|
|
294
|
+
class={cn(
|
|
295
|
+
"flex shrink-0 items-center",
|
|
296
|
+
level >= 2 ? "gap-1.5" : "gap-2",
|
|
297
|
+
hasLeading ? "ms-auto" : "flex-1",
|
|
298
|
+
)}
|
|
299
|
+
>
|
|
300
|
+
{#if tabs}
|
|
301
|
+
<div class={cn("flex min-w-0 items-center", !hasLeading && "me-auto")}>
|
|
302
|
+
{@render tabs()}
|
|
303
|
+
</div>
|
|
304
|
+
{/if}
|
|
305
|
+
{#if rightActions}
|
|
306
|
+
{@render rightActions()}
|
|
307
|
+
{/if}
|
|
308
|
+
</div>
|
|
309
|
+
{/if}
|
|
310
|
+
</div>
|
|
225
311
|
</div>
|
|
226
312
|
</div>
|
|
227
313
|
{/if}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Snippet } from "svelte";
|
|
2
2
|
import type { PhosphorIcon, IconWeight } from "../../lib/icon-types.js";
|
|
3
3
|
type EmptyConfig = {
|
|
4
4
|
icon?: PhosphorIcon;
|
|
@@ -25,6 +25,11 @@ interface Props {
|
|
|
25
25
|
content?: Snippet;
|
|
26
26
|
empty?: Snippet;
|
|
27
27
|
extra?: Snippet;
|
|
28
|
+
/** The folded search icon's name, on a toolbar too narrow for the field.
|
|
29
|
+
* Translate at the call site. */
|
|
30
|
+
searchLabel?: string;
|
|
31
|
+
/** The close button's name once the folded search is open. */
|
|
32
|
+
closeSearchLabel?: string;
|
|
28
33
|
}
|
|
29
34
|
declare const $$Component: import("svelte").Component<Props, {}, "">;
|
|
30
35
|
type $$Component = ReturnType<typeof $$Component>;
|
|
@@ -5,7 +5,11 @@ import { getContext, setContext } from "svelte";
|
|
|
5
5
|
* its row and sets this; a control inside it reads it and draws a smaller self.
|
|
6
6
|
*/
|
|
7
7
|
export interface ToolbarDensity {
|
|
8
|
+
/** The tabs fold into one dropdown. */
|
|
8
9
|
readonly compact: boolean;
|
|
10
|
+
/** Folded and still short: a filter or display trigger drops its label and
|
|
11
|
+
* keeps the icon, so the row never wraps (TSK-2121, 420px). Implies compact. */
|
|
12
|
+
readonly tight: boolean;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
// WHY context: the shell owns the measurement and 26 pages render their tabs
|
|
@@ -27,12 +31,31 @@ export function getToolbarDensity(): ToolbarDensity | undefined {
|
|
|
27
31
|
*/
|
|
28
32
|
// WHY `max-content` on a probe, not the row's own width: the row is sized by
|
|
29
33
|
// its container, so reading it back always says "it fits" (see collapse.ts).
|
|
34
|
+
// WHY `flex: none` too: a row that is itself a flex item takes its width from
|
|
35
|
+
// flex-basis, so `max-content` alone read back the container (10 Nov 2026).
|
|
36
|
+
// WHY transitions are stopped: under reduced motion `tokens/base.css` gives every
|
|
37
|
+
// element an 80ms transition on `all`, so the probe's own width change started a
|
|
38
|
+
// transition and the read got the old width. The row never folded at 390px and
|
|
39
|
+
// axe flagged the squeezed search field (gallery, 10 Nov 2026). The last fold's
|
|
40
|
+
// gap and shrink changes are still running then too, so they're finished first.
|
|
30
41
|
export function naturalRowWidth(row: HTMLElement): number {
|
|
31
|
-
const {
|
|
42
|
+
for (const animation of row.getAnimations({ subtree: true })) {
|
|
43
|
+
if (animation instanceof CSSTransition) animation.finish();
|
|
44
|
+
}
|
|
45
|
+
const { width, flexWrap, flex } = row.style;
|
|
46
|
+
const transition = row.style.getPropertyValue("transition");
|
|
47
|
+
const priority = row.style.getPropertyPriority("transition");
|
|
48
|
+
row.style.setProperty("transition", "none", "important");
|
|
32
49
|
row.style.width = "max-content";
|
|
33
50
|
row.style.flexWrap = "nowrap";
|
|
51
|
+
row.style.flex = "none";
|
|
34
52
|
const needed = row.getBoundingClientRect().width;
|
|
35
53
|
row.style.width = width;
|
|
36
54
|
row.style.flexWrap = flexWrap;
|
|
55
|
+
row.style.flex = flex;
|
|
56
|
+
// The restored size has to settle before transitions come back, or the
|
|
57
|
+
// row would animate back from its probe width.
|
|
58
|
+
void row.offsetWidth;
|
|
59
|
+
row.style.setProperty("transition", transition, priority);
|
|
37
60
|
return needed;
|
|
38
61
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import type { WithoutChildrenOrChild } from "../../lib/utils.js";
|
|
5
|
+
import DrawerOverlay from "./drawer-overlay.svelte";
|
|
6
|
+
import DrawerPortal from "./drawer-portal.svelte";
|
|
7
|
+
import type { ComponentProps } from "svelte";
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
ref = $bindable(null),
|
|
11
|
+
class: className,
|
|
12
|
+
portalProps,
|
|
13
|
+
children,
|
|
14
|
+
...restProps
|
|
15
|
+
}: DrawerPrimitive.ContentProps & {
|
|
16
|
+
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DrawerPortal>>;
|
|
17
|
+
} = $props();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<DrawerPortal {...portalProps}>
|
|
21
|
+
<DrawerOverlay />
|
|
22
|
+
<DrawerPrimitive.Content
|
|
23
|
+
bind:ref
|
|
24
|
+
data-slot="drawer-content"
|
|
25
|
+
class={cn(
|
|
26
|
+
"bg-popover text-popover-foreground flex h-auto flex-col text-sm data-[vaul-drawer-direction=bottom]:rounded-t-xl data-[vaul-drawer-direction=bottom]:border-t data-[vaul-drawer-direction=left]:rounded-r-xl data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=right]:rounded-l-xl data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=top]:rounded-b-xl data-[vaul-drawer-direction=top]:border-b group/drawer-content fixed z-50 data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:start-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:end-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=left]:sm:max-w-sm data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
{...restProps}
|
|
30
|
+
>
|
|
31
|
+
<div
|
|
32
|
+
class="bg-muted mx-auto mt-4 hidden h-1.5 w-[100px] shrink-0 rounded-full mx-auto hidden shrink-0 group-data-[vaul-drawer-direction=bottom]/drawer-content:block"
|
|
33
|
+
></div>
|
|
34
|
+
{@render children?.()}
|
|
35
|
+
</DrawerPrimitive.Content>
|
|
36
|
+
</DrawerPortal>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
|
2
|
+
import type { WithoutChildrenOrChild } from "../../lib/utils.js";
|
|
3
|
+
import DrawerPortal from "./drawer-portal.svelte";
|
|
4
|
+
import type { ComponentProps } from "svelte";
|
|
5
|
+
type $$ComponentProps = DrawerPrimitive.ContentProps & {
|
|
6
|
+
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof DrawerPortal>>;
|
|
7
|
+
};
|
|
8
|
+
declare const $$Component: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
|
9
|
+
type $$Component = ReturnType<typeof $$Component>;
|
|
10
|
+
export default $$Component;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
...restProps
|
|
9
|
+
}: DrawerPrimitive.DescriptionProps = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<DrawerPrimitive.Description
|
|
13
|
+
bind:ref
|
|
14
|
+
data-slot="drawer-description"
|
|
15
|
+
class={cn("text-muted-foreground text-sm", className)}
|
|
16
|
+
{...restProps}
|
|
17
|
+
/>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, type WithElementRef } from "../../lib/utils.js";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
children,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
bind:this={ref}
|
|
15
|
+
data-slot="drawer-footer"
|
|
16
|
+
class={cn("gap-2 p-4 mt-auto flex flex-col", className)}
|
|
17
|
+
{...restProps}
|
|
18
|
+
>
|
|
19
|
+
{@render children?.()}
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type WithElementRef } from "../../lib/utils.js";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
declare const $$Component: import("svelte").Component<WithElementRef<HTMLAttributes<HTMLDivElement>>, {}, "ref">;
|
|
4
|
+
type $$Component = ReturnType<typeof $$Component>;
|
|
5
|
+
export default $$Component;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, type WithElementRef } from "../../lib/utils.js";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
children,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
bind:this={ref}
|
|
15
|
+
data-slot="drawer-header"
|
|
16
|
+
class={cn(
|
|
17
|
+
"gap-0.5 p-4 md:gap-1.5 md:text-left flex flex-col group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...restProps}
|
|
21
|
+
>
|
|
22
|
+
{@render children?.()}
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type WithElementRef } from "../../lib/utils.js";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
declare const $$Component: import("svelte").Component<WithElementRef<HTMLAttributes<HTMLDivElement>>, {}, "ref">;
|
|
4
|
+
type $$Component = ReturnType<typeof $$Component>;
|
|
5
|
+
export default $$Component;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
shouldScaleBackground = true,
|
|
6
|
+
open = $bindable(false),
|
|
7
|
+
activeSnapPoint = $bindable(null),
|
|
8
|
+
...restProps
|
|
9
|
+
}: DrawerPrimitive.RootProps = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<DrawerPrimitive.NestedRoot {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
...restProps
|
|
9
|
+
}: DrawerPrimitive.OverlayProps = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<DrawerPrimitive.Overlay
|
|
13
|
+
bind:ref
|
|
14
|
+
data-slot="drawer-overlay"
|
|
15
|
+
class={cn(
|
|
16
|
+
"bg-black/10 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...restProps}
|
|
20
|
+
/>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
...restProps
|
|
9
|
+
}: DrawerPrimitive.TitleProps = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<DrawerPrimitive.Title
|
|
13
|
+
bind:ref
|
|
14
|
+
data-slot="drawer-title"
|
|
15
|
+
class={cn("cn-font-heading text-foreground font-medium", className)}
|
|
16
|
+
{...restProps}
|
|
17
|
+
/>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Drawer as DrawerPrimitive } from "vaul-svelte";
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
shouldScaleBackground = true,
|
|
6
|
+
open = $bindable(false),
|
|
7
|
+
activeSnapPoint = $bindable(null),
|
|
8
|
+
...restProps
|
|
9
|
+
}: DrawerPrimitive.RootProps = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<DrawerPrimitive.Root {shouldScaleBackground} bind:open bind:activeSnapPoint {...restProps} />
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Close from "./drawer-close.svelte";
|
|
2
|
+
import Content from "./drawer-content.svelte";
|
|
3
|
+
import Description from "./drawer-description.svelte";
|
|
4
|
+
import Footer from "./drawer-footer.svelte";
|
|
5
|
+
import Header from "./drawer-header.svelte";
|
|
6
|
+
import NestedRoot from "./drawer-nested.svelte";
|
|
7
|
+
import Overlay from "./drawer-overlay.svelte";
|
|
8
|
+
import Portal from "./drawer-portal.svelte";
|
|
9
|
+
import Title from "./drawer-title.svelte";
|
|
10
|
+
import Trigger from "./drawer-trigger.svelte";
|
|
11
|
+
import Root from "./drawer.svelte";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
Root,
|
|
15
|
+
NestedRoot,
|
|
16
|
+
Content,
|
|
17
|
+
Description,
|
|
18
|
+
Overlay,
|
|
19
|
+
Footer,
|
|
20
|
+
Header,
|
|
21
|
+
Title,
|
|
22
|
+
Trigger,
|
|
23
|
+
Portal,
|
|
24
|
+
Close,
|
|
25
|
+
|
|
26
|
+
//
|
|
27
|
+
Root as Drawer,
|
|
28
|
+
NestedRoot as DrawerNestedRoot,
|
|
29
|
+
Content as DrawerContent,
|
|
30
|
+
Description as DrawerDescription,
|
|
31
|
+
Overlay as DrawerOverlay,
|
|
32
|
+
Footer as DrawerFooter,
|
|
33
|
+
Header as DrawerHeader,
|
|
34
|
+
Title as DrawerTitle,
|
|
35
|
+
Trigger as DrawerTrigger,
|
|
36
|
+
Portal as DrawerPortal,
|
|
37
|
+
Close as DrawerClose,
|
|
38
|
+
};
|