@heyramzi/ui 3.1.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/lib/sliding-pill.js +2 -2
- package/dist/record/measure-clamp.js +23 -0
- package/dist/shadcn/button-group/index.js +6 -0
- package/dist/shadcn/checkbox/checkbox-box.js +3 -0
- package/dist/shadcn/drawer/index.js +14 -0
- package/dist/shadcn/input-group/index.js +9 -0
- package/dist/shadcn/item/index.js +13 -0
- package/dist/shadcn/native-select/index.js +6 -0
- package/dist/shadcn/pagination/index.js +12 -0
- package/package.json +7 -2
- package/scripts/sync-shadcn.mjs +0 -6
- package/src/ai/generation-progress/GenerationProgress.svelte +14 -12
- package/src/canvas/pen/PenOverlay.svelte +1 -30
- package/src/canvas/screen/PartView.svelte +13 -8
- package/src/canvas/screen/ScreenBody.svelte +18 -11
- package/src/canvas/ui/BarTooltip.svelte +46 -0
- package/src/canvas/ui/BarTooltip.svelte.d.ts +17 -0
- package/src/canvas/ui/CanvasToolbar.svelte +170 -156
- package/src/canvas/ui/StylePanel.svelte +179 -102
- package/src/canvas/ui/canvas-ui.css +12 -76
- package/src/charts/dashboard/data-table.svelte +80 -80
- package/src/charts/dashboard/hint.svelte +34 -0
- package/src/charts/dashboard/hint.svelte.d.ts +10 -0
- package/src/charts/dashboard/rating-chip.svelte +4 -3
- package/src/charts/legend.css +0 -8
- package/src/charts/legend.svelte +26 -30
- package/src/components/copy/copy-button.svelte +10 -2
- package/src/components/copy/copy-button.test.ts +7 -0
- package/src/components/data-table/table-skeleton.svelte +15 -7
- package/src/components/data-table/table-skeleton.svelte.d.ts +4 -0
- package/src/components/docs/on-this-page.svelte +2 -2
- package/src/components/hold-to-confirm/hold-to-confirm.svelte +2 -1
- package/src/components/list-page/filter-item.svelte +20 -7
- package/src/components/list-page/filter-popover.svelte +57 -17
- package/src/components/list-page/filter-popover.svelte.d.ts +1 -1
- package/src/components/list-page/grouped-list.svelte +41 -23
- 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/components/picker/picker.svelte +8 -3
- package/src/components/picker/picker.svelte.d.ts +2 -1
- package/src/components/segmented/segmented-filter.svelte +19 -10
- package/src/components/segmented/segmented-track.svelte +11 -3
- package/src/components/table-view/table-view.svelte +22 -6
- package/src/components/tags-input/tags-input.svelte +94 -77
- package/src/components/toolbar/toolbar.svelte +2 -1
- package/src/components/toolbar/toolbar.test.ts +5 -4
- package/src/components/work-parts/group-head.svelte +28 -9
- package/src/components/work-parts/group-head.svelte.d.ts +5 -2
- package/src/editor/editor-toolbar.svelte +223 -193
- package/src/editor/editor.svelte +56 -38
- package/src/editor/image-toolbar.svelte +99 -37
- package/src/editor/image-toolbar.svelte.d.ts +4 -5
- package/src/editor/url-dialog.svelte +96 -0
- package/src/editor/url-dialog.svelte.d.ts +15 -0
- package/src/effects/theme-toggle/ThemeToggle.css +8 -8
- package/src/effects/theme-toggle/ThemeToggle.svelte +20 -6
- package/src/inbox/thread-inbox.svelte +9 -1
- package/src/lib/live-region.svelte +12 -4
- package/src/lib/live-region.svelte.d.ts +5 -1
- package/src/lib/sliding-pill.ts +3 -2
- package/src/record/info-list.svelte +33 -59
- package/src/record/measure-clamp.ts +31 -0
- package/src/record/record-shell.svelte +38 -39
- package/src/record/record-timeline.svelte +10 -5
- package/src/record/steps-rail.svelte +50 -36
- package/src/record/where-we-are.svelte +5 -19
- package/src/shadcn/button-group/button-group-separator.svelte +23 -0
- package/src/shadcn/button-group/button-group-separator.svelte.d.ts +13 -0
- package/src/shadcn/button-group/button-group-text.svelte +28 -0
- package/src/shadcn/button-group/button-group-text.svelte.d.ts +11 -0
- package/src/shadcn/button-group/button-group.svelte +46 -0
- package/src/shadcn/button-group/button-group.svelte.d.ts +16 -0
- package/src/shadcn/button-group/index.ts +15 -0
- package/src/shadcn/checkbox/checkbox-box.ts +4 -0
- package/src/shadcn/checkbox/checkbox.svelte +2 -4
- 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
- package/src/shadcn/input-group/index.ts +22 -0
- package/src/shadcn/input-group/input-group-addon.svelte +51 -0
- package/src/shadcn/input-group/input-group-addon.svelte.d.ts +18 -0
- package/src/shadcn/input-group/input-group-button.svelte +49 -0
- package/src/shadcn/input-group/input-group-button.svelte.d.ts +18 -0
- package/src/shadcn/input-group/input-group-input.svelte +20 -0
- package/src/shadcn/input-group/input-group-input.svelte.d.ts +11 -0
- package/src/shadcn/input-group/input-group-text.svelte +19 -0
- package/src/shadcn/input-group/input-group-text.svelte.d.ts +5 -0
- package/src/shadcn/input-group/input-group-textarea.svelte +20 -0
- package/src/shadcn/input-group/input-group-textarea.svelte.d.ts +3 -0
- package/src/shadcn/input-group/input-group.svelte +24 -0
- package/src/shadcn/input-group/input-group.svelte.d.ts +5 -0
- package/src/shadcn/item/index.ts +34 -0
- package/src/shadcn/item/item-actions.svelte +20 -0
- package/src/shadcn/item/item-actions.svelte.d.ts +5 -0
- package/src/shadcn/item/item-content.svelte +23 -0
- package/src/shadcn/item/item-content.svelte.d.ts +5 -0
- package/src/shadcn/item/item-description.svelte +23 -0
- package/src/shadcn/item/item-description.svelte.d.ts +5 -0
- package/src/shadcn/item/item-footer.svelte +20 -0
- package/src/shadcn/item/item-footer.svelte.d.ts +5 -0
- package/src/shadcn/item/item-group.svelte +21 -0
- package/src/shadcn/item/item-group.svelte.d.ts +5 -0
- package/src/shadcn/item/item-header.svelte +20 -0
- package/src/shadcn/item/item-header.svelte.d.ts +5 -0
- package/src/shadcn/item/item-media.svelte +42 -0
- package/src/shadcn/item/item-media.svelte.d.ts +17 -0
- package/src/shadcn/item/item-separator.svelte +19 -0
- package/src/shadcn/item/item-separator.svelte.d.ts +13 -0
- package/src/shadcn/item/item-title.svelte +20 -0
- package/src/shadcn/item/item-title.svelte.d.ts +5 -0
- package/src/shadcn/item/item.svelte +61 -0
- package/src/shadcn/item/item.svelte.d.ts +28 -0
- package/src/shadcn/native-select/index.ts +13 -0
- package/src/shadcn/native-select/native-select-opt-group.svelte +20 -0
- package/src/shadcn/native-select/native-select-opt-group.svelte.d.ts +5 -0
- package/src/shadcn/native-select/native-select-option.svelte +20 -0
- package/src/shadcn/native-select/native-select-option.svelte.d.ts +5 -0
- package/src/shadcn/native-select/native-select.svelte +45 -0
- package/src/shadcn/native-select/native-select.svelte.d.ts +8 -0
- package/src/shadcn/pagination/index.ts +31 -0
- package/src/shadcn/pagination/pagination-content.svelte +20 -0
- package/src/shadcn/pagination/pagination-content.svelte.d.ts +5 -0
- package/src/shadcn/pagination/pagination-ellipsis.svelte +22 -0
- package/src/shadcn/pagination/pagination-ellipsis.svelte.d.ts +5 -0
- package/src/shadcn/pagination/pagination-item.svelte +14 -0
- package/src/shadcn/pagination/pagination-item.svelte.d.ts +5 -0
- package/src/shadcn/pagination/pagination-link.svelte +41 -0
- package/src/shadcn/pagination/pagination-link.svelte.d.ts +9 -0
- package/src/shadcn/pagination/pagination-next-button.svelte +34 -0
- package/src/shadcn/pagination/pagination-next-button.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination-next.svelte +26 -0
- package/src/shadcn/pagination/pagination-next.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination-prev-button.svelte +34 -0
- package/src/shadcn/pagination/pagination-prev-button.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination-previous.svelte +30 -0
- package/src/shadcn/pagination/pagination-previous.svelte.d.ts +4 -0
- package/src/shadcn/pagination/pagination.svelte +27 -0
- package/src/shadcn/pagination/pagination.svelte.d.ts +4 -0
- package/src/slides/Controls.svelte +39 -36
- package/src/slides/Deck.svelte +11 -8
- package/src/thread/thread.svelte +15 -8
- package/src/work/detail/task-detail.svelte +1 -1
- package/src/work/gantt/gantt-task-row.svelte +3 -6
- package/src/work/inbox/inbox-card.svelte +7 -5
- package/src/work/inbox/inbox-trace.svelte +14 -10
- package/src/work/kanban-board.svelte +40 -25
- package/src/work/kanban-hidden-rail.svelte +67 -65
- package/src/work/task-list.svelte +145 -124
- package/src/work/task-list.svelte.test.ts +5 -2
- package/src/work/time/time-tracker.svelte +35 -10
|
@@ -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
|
}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
options?: PickerOption<V>[];
|
|
19
19
|
/** Titled runs, when the menu has a shape worth naming. */
|
|
20
20
|
groups?: PickerGroup<V>[];
|
|
21
|
-
|
|
21
|
+
/** An array checks each row in it, and a pick leaves the menu open for the next one. */
|
|
22
|
+
value: V | V[] | null;
|
|
22
23
|
onSelect: (value: V) => void;
|
|
23
24
|
/** The control that opens the menu. Rendered inside `Popover.Trigger`. */
|
|
24
25
|
trigger: Snippet;
|
|
@@ -74,10 +75,14 @@
|
|
|
74
75
|
const WIDTH = { sm: "w-44", default: "w-52", lg: "w-60" } as const;
|
|
75
76
|
|
|
76
77
|
function pick(next: V) {
|
|
77
|
-
open = false;
|
|
78
|
+
if (!Array.isArray(value)) open = false;
|
|
78
79
|
onSelect(next);
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
function isChecked(option: PickerOption<V>): boolean {
|
|
83
|
+
return Array.isArray(value) ? value.includes(option.value) : option.value === value;
|
|
84
|
+
}
|
|
85
|
+
|
|
81
86
|
/**
|
|
82
87
|
* WHY a custom filter: the default one scores `value`, which is often an id.
|
|
83
88
|
* Typing "an" fuzzy-matched every uuid, so only the visible words are searched.
|
|
@@ -152,7 +157,7 @@
|
|
|
152
157
|
{#each resolved as group, i (group.heading ?? i)}
|
|
153
158
|
<Command.Group heading={group.heading}>
|
|
154
159
|
{#each group.options as option (option.value)}
|
|
155
|
-
{@const active = option
|
|
160
|
+
{@const active = isChecked(option)}
|
|
156
161
|
<Command.Item
|
|
157
162
|
value={option.value}
|
|
158
163
|
keywords={keywordsFor(option)}
|
|
@@ -6,7 +6,8 @@ declare function $$render<V extends string>(): {
|
|
|
6
6
|
options?: PickerOption<V>[];
|
|
7
7
|
/** Titled runs, when the menu has a shape worth naming. */
|
|
8
8
|
groups?: PickerGroup<V>[];
|
|
9
|
-
|
|
9
|
+
/** An array checks each row in it, and a pick leaves the menu open for the next one. */
|
|
10
|
+
value: V | V[] | null;
|
|
10
11
|
onSelect: (value: V) => void;
|
|
11
12
|
/** The control that opens the menu. Rendered inside `Popover.Trigger`. */
|
|
12
13
|
trigger: Snippet;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// =========================
|
|
3
3
|
// IMPORTS
|
|
4
4
|
// =========================
|
|
5
|
+
import { ToggleGroup } from "bits-ui";
|
|
5
6
|
import { cn } from "../../lib/utils.js";
|
|
6
7
|
import Segment from "./segment.svelte";
|
|
7
8
|
import type { TabDef } from "./animated-tabs.svelte";
|
|
@@ -26,8 +27,9 @@
|
|
|
26
27
|
// WHY: picks which rows are listed, with no panel of its own. That's a
|
|
27
28
|
// segmented control, not a tab set.
|
|
28
29
|
|
|
29
|
-
// WHY not `AnimatedTabs`: a strip with no tabpanel is a
|
|
30
|
-
//
|
|
30
|
+
// WHY not `AnimatedTabs`: a strip with no tabpanel is a bits-ui single toggle group,
|
|
31
|
+
// one Tab stop whose segments report checked. "Tab 1 of 4" sends a screen reader
|
|
32
|
+
// looking for a panel.
|
|
31
33
|
|
|
32
34
|
// WHY `TabDef`: an option and a tab were the same five fields written twice.
|
|
33
35
|
options: readonly TabDef<T>[];
|
|
@@ -107,6 +109,12 @@
|
|
|
107
109
|
value = next;
|
|
108
110
|
onValueChange?.(next);
|
|
109
111
|
}
|
|
112
|
+
|
|
113
|
+
// bits-ui hands back a plain string, and "" when the picked segment is pressed again.
|
|
114
|
+
function pickByValue(next: string) {
|
|
115
|
+
const option = merged.find((o) => o.value === next);
|
|
116
|
+
if (option) pick(option.value);
|
|
117
|
+
}
|
|
110
118
|
</script>
|
|
111
119
|
|
|
112
120
|
<!-- =========================== -->
|
|
@@ -132,20 +140,21 @@
|
|
|
132
140
|
{/snippet}
|
|
133
141
|
</Picker>
|
|
134
142
|
{:else}
|
|
135
|
-
|
|
143
|
+
<!-- WHY a function binding: a filter always has one option on, so the setter drops the ""
|
|
144
|
+
and the getter keeps showing `value`. -->
|
|
145
|
+
<ToggleGroup.Root
|
|
146
|
+
type="single"
|
|
147
|
+
bind:value={() => value, pickByValue}
|
|
136
148
|
class={cn("no-scrollbar flex max-w-full min-w-0 overflow-x-auto", className)}
|
|
137
|
-
role="group"
|
|
138
149
|
aria-label={ariaLabel}
|
|
139
150
|
>
|
|
140
151
|
<SegmentedTrack fill={block}>
|
|
141
152
|
{#each split.visible as option (option.value)}
|
|
142
153
|
{@const active = value === option.value}
|
|
143
154
|
{@const markOnly = iconOnly && Boolean(option.image || option.icon)}
|
|
144
|
-
<
|
|
145
|
-
|
|
155
|
+
<ToggleGroup.Item
|
|
156
|
+
value={option.value}
|
|
146
157
|
class={segmentedTriggerClass({ fill: block, active })}
|
|
147
|
-
onclick={() => pick(option.value)}
|
|
148
|
-
aria-pressed={active}
|
|
149
158
|
aria-label={markOnly ? option.label : undefined}
|
|
150
159
|
title={markOnly ? option.label : undefined}
|
|
151
160
|
>
|
|
@@ -173,7 +182,7 @@
|
|
|
173
182
|
{active}
|
|
174
183
|
/>
|
|
175
184
|
{/if}
|
|
176
|
-
</
|
|
185
|
+
</ToggleGroup.Item>
|
|
177
186
|
{/each}
|
|
178
187
|
{#snippet after()}
|
|
179
188
|
{#if split.hidden.length > 0}
|
|
@@ -181,5 +190,5 @@
|
|
|
181
190
|
{/if}
|
|
182
191
|
{/snippet}
|
|
183
192
|
</SegmentedTrack>
|
|
184
|
-
</
|
|
193
|
+
</ToggleGroup.Root>
|
|
185
194
|
{/if}
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
[data-state="active"],
|
|
84
84
|
[aria-current="page"],
|
|
85
85
|
[aria-pressed="true"],
|
|
86
|
+
[data-state="on"],
|
|
86
87
|
[data-state="open"]
|
|
87
88
|
)
|
|
88
89
|
) {
|
|
@@ -116,7 +117,9 @@
|
|
|
116
117
|
/* Before the attachment measures, the active segment draws the pill itself,
|
|
117
118
|
so a server-rendered strip is right before any JS arrives. */
|
|
118
119
|
.track:not([data-pill="on"])
|
|
119
|
-
:global(
|
|
120
|
+
:global(
|
|
121
|
+
:is([data-state="active"], [aria-current="page"], [aria-pressed="true"], [data-state="on"])
|
|
122
|
+
) {
|
|
120
123
|
background-color: var(--color-card);
|
|
121
124
|
box-shadow: var(--elev-btn), var(--elev-hairline);
|
|
122
125
|
}
|
|
@@ -125,7 +128,9 @@
|
|
|
125
128
|
:global(.dark) .pill,
|
|
126
129
|
:global(.dark)
|
|
127
130
|
.track:not([data-pill="on"])
|
|
128
|
-
:global(
|
|
131
|
+
:global(
|
|
132
|
+
:is([data-state="active"], [aria-current="page"], [aria-pressed="true"], [data-state="on"])
|
|
133
|
+
) {
|
|
129
134
|
background-color: color-mix(in oklch, var(--color-foreground) 14%, var(--color-card));
|
|
130
135
|
}
|
|
131
136
|
|
|
@@ -138,6 +143,7 @@
|
|
|
138
143
|
[data-state="active"],
|
|
139
144
|
[aria-current="page"],
|
|
140
145
|
[aria-pressed="true"],
|
|
146
|
+
[data-state="on"],
|
|
141
147
|
[data-state="open"]
|
|
142
148
|
)
|
|
143
149
|
) {
|
|
@@ -155,7 +161,9 @@
|
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
.track[data-variant="underline"]:not([data-pill="on"])
|
|
158
|
-
:global(
|
|
164
|
+
:global(
|
|
165
|
+
:is([data-state="active"], [aria-current="page"], [aria-pressed="true"], [data-state="on"])
|
|
166
|
+
) {
|
|
159
167
|
background-color: transparent;
|
|
160
168
|
box-shadow: inset 0 -2px 0 var(--segmented-rule, var(--color-foreground));
|
|
161
169
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import * as ContextMenu from "../../shadcn/context-menu/index.js";
|
|
18
18
|
import { Checkbox } from "../../shadcn/checkbox/index.js";
|
|
19
19
|
import { SortableHead, TableSkeleton } from "../data-table/index.js";
|
|
20
|
+
import GroupHead from "../work-parts/group-head.svelte";
|
|
20
21
|
import { toggleSort, type SortState } from "../data-table/data-table.js";
|
|
21
22
|
import { MENU_KEYS, openMenuFromKey } from "../data-table/menu-key.js";
|
|
22
23
|
|
|
@@ -119,6 +120,9 @@
|
|
|
119
120
|
skeletonRows = 5,
|
|
120
121
|
}: Props = $props();
|
|
121
122
|
|
|
123
|
+
// Names each group's body, so a fold button can say which rows it folds.
|
|
124
|
+
const uid = $props.id();
|
|
125
|
+
|
|
122
126
|
// =========================
|
|
123
127
|
// DERIVED
|
|
124
128
|
// =========================
|
|
@@ -229,6 +233,15 @@
|
|
|
229
233
|
<!-- =========================== -->
|
|
230
234
|
<!-- MARKUP -->
|
|
231
235
|
<!-- =========================== -->
|
|
236
|
+
{#snippet groupTitle(tableGroup: TableGroup<Row>, className?: string)}
|
|
237
|
+
<!-- The th sets the type: GroupHead takes the name as markup, so no size of its own lands on it. -->
|
|
238
|
+
<GroupHead class={className}>
|
|
239
|
+
{#snippet title()}
|
|
240
|
+
{#if groupHeader}{@render groupHeader(tableGroup)}{:else}{tableGroup.key}{/if}
|
|
241
|
+
{/snippet}
|
|
242
|
+
</GroupHead>
|
|
243
|
+
{/snippet}
|
|
244
|
+
|
|
232
245
|
{#snippet body(row: Row)}
|
|
233
246
|
{#each columns as column (column.key)}
|
|
234
247
|
<Table.Cell
|
|
@@ -403,10 +416,11 @@
|
|
|
403
416
|
<TableSkeleton rows={skeletonRows} columns={skeletonColumns} />
|
|
404
417
|
{:else}
|
|
405
418
|
{#if group}
|
|
406
|
-
{#each entries as entry (entry.id)}
|
|
419
|
+
{#each entries as entry, index (entry.id)}
|
|
407
420
|
{@const folded = entry.key !== null && collapsible && collapsed.has(entry.key)}
|
|
408
421
|
{@const tableGroup = { key: entry.key ?? "", rows: entry.rows }}
|
|
409
|
-
|
|
422
|
+
{@const bodyId = `${uid}-group-${index}`}
|
|
423
|
+
<Table.Body id={bodyId}>
|
|
410
424
|
{#if entry.key !== null}
|
|
411
425
|
<Table.Row
|
|
412
426
|
class="bg-foreground/4 hover:translate-y-0 hover:bg-foreground/4 hover:shadow-none"
|
|
@@ -416,6 +430,7 @@
|
|
|
416
430
|
<button
|
|
417
431
|
type="button"
|
|
418
432
|
aria-expanded={!folded}
|
|
433
|
+
aria-controls={bodyId}
|
|
419
434
|
class="t-press flex h-8 w-full items-center gap-2 px-2 text-start outline-none focus-visible:shadow-(--ring-focus-inset)"
|
|
420
435
|
onclick={() => toggleGroup(tableGroup.key)}
|
|
421
436
|
>
|
|
@@ -426,12 +441,13 @@
|
|
|
426
441
|
)}
|
|
427
442
|
aria-hidden="true"
|
|
428
443
|
/>
|
|
429
|
-
{
|
|
444
|
+
{@render groupTitle(tableGroup)}
|
|
430
445
|
</button>
|
|
431
446
|
{:else}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
447
|
+
<!-- No gap: unlike the toggle, a fixed head never spaced its parts. -->
|
|
448
|
+
<div class="flex h-8 items-center px-2">
|
|
449
|
+
{@render groupTitle(tableGroup, "gap-0")}
|
|
450
|
+
</div>
|
|
435
451
|
{/if}
|
|
436
452
|
</Table.Head>
|
|
437
453
|
</Table.Row>
|