@spaethtech/svelte-ui 0.21.1 → 0.21.2-dev.139.462a2ec
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.
|
@@ -347,28 +347,53 @@
|
|
|
347
347
|
|
|
348
348
|
<div class="grid grow grid-cols-12 items-center {cellGap}">
|
|
349
349
|
{#each columns as col (col.id)}
|
|
350
|
+
{@const key = sortKeyOf(col)}
|
|
351
|
+
{@const activeDir = col.sortable && grid.sort && grid.sort.col === key ? grid.sort.dir : null}
|
|
352
|
+
<!-- The WHOLE header cell is the sort control — no nested Button (so the header text keeps the
|
|
353
|
+
row's semibold/uppercase and there's no button hover-fill). Sortable cells get a pointer +
|
|
354
|
+
keyboard (role="columnheader" + aria-sort), and hover/focus tints the text + sort icon to
|
|
355
|
+
--ui-accent (the variant colour; the neutral default IS the text colour, so a neutral table
|
|
356
|
+
stays monochrome). The inactive sort glyph also brightens on hover as a universal cue. The
|
|
357
|
+
inner inline-flex keeps text + icon on one line (the cell goes `block` at breakpoints via
|
|
358
|
+
cellClasses, which would otherwise wrap the icon). -->
|
|
350
359
|
<div
|
|
351
360
|
role="columnheader"
|
|
352
|
-
|
|
361
|
+
aria-sort={col.sortable
|
|
362
|
+
? activeDir
|
|
363
|
+
? activeDir === "asc"
|
|
364
|
+
? "ascending"
|
|
365
|
+
: "descending"
|
|
366
|
+
: "none"
|
|
367
|
+
: undefined}
|
|
368
|
+
tabindex={col.sortable ? 0 : undefined}
|
|
369
|
+
onclick={col.sortable ? () => grid.toggleSort(key) : undefined}
|
|
370
|
+
onkeydown={col.sortable
|
|
371
|
+
? (e) => {
|
|
372
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
373
|
+
e.preventDefault();
|
|
374
|
+
grid.toggleSort(key);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
: undefined}
|
|
378
|
+
class="group/sort {cellClasses(col)} flex min-w-0 items-center {alignClass(col)} {truncate
|
|
353
379
|
? 'truncate'
|
|
380
|
+
: ''} {col.sortable
|
|
381
|
+
? 'cursor-pointer select-none transition-colors hover:[color:var(--ui-accent)] focus-visible:[color:var(--ui-accent)] outline-none'
|
|
354
382
|
: ''}"
|
|
355
383
|
>
|
|
356
384
|
{#if col.sortable}
|
|
357
|
-
<
|
|
358
|
-
variant="ghost"
|
|
359
|
-
{size}
|
|
360
|
-
class="!px-0 [font-size:inherit]"
|
|
361
|
-
onclick={() => grid.toggleSort(sortKeyOf(col))}
|
|
362
|
-
>
|
|
385
|
+
<span class="inline-flex items-center gap-1">
|
|
363
386
|
<span>{col.header ?? col.id}</span>
|
|
364
|
-
{#if
|
|
365
|
-
{#if
|
|
366
|
-
class="w-[1em] h-[1em]"
|
|
367
|
-
/>{:else}<IconSortDesc class="w-[1em] h-[1em]" />{/if}
|
|
387
|
+
{#if activeDir}
|
|
388
|
+
{#if activeDir === "asc"}<IconSortAsc
|
|
389
|
+
class="w-[1em] h-[1em] shrink-0"
|
|
390
|
+
/>{:else}<IconSortDesc class="w-[1em] h-[1em] shrink-0" />{/if}
|
|
368
391
|
{:else}
|
|
369
|
-
<IconSortNone
|
|
392
|
+
<IconSortNone
|
|
393
|
+
class="w-[1em] h-[1em] shrink-0 opacity-40 transition-opacity group-hover/sort:opacity-100"
|
|
394
|
+
/>
|
|
370
395
|
{/if}
|
|
371
|
-
</
|
|
396
|
+
</span>
|
|
372
397
|
{:else}
|
|
373
398
|
<span>{col.header ?? col.id}</span>
|
|
374
399
|
{/if}
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
import Input from "./Input.svelte";
|
|
18
18
|
import Button from "./Button.svelte";
|
|
19
19
|
import Popup from "./Popup.svelte";
|
|
20
|
-
import
|
|
20
|
+
import Dialog from "./Dialog.svelte";
|
|
21
|
+
import Card from "./Card.svelte";
|
|
22
|
+
import CardHeader from "./CardHeader.svelte";
|
|
21
23
|
import type { DataSet } from "../data/dataset.js";
|
|
22
24
|
import type { Size } from "../types/sizes.js";
|
|
23
25
|
import type { Responsive } from "../types/responsive.js";
|
|
@@ -229,8 +231,15 @@
|
|
|
229
231
|
</ul>
|
|
230
232
|
</Popup>
|
|
231
233
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
+
<!-- Help modal: a fixed 80vw × 80vh box (consistent large panel) with a sticky title and a
|
|
235
|
+
body that scrolls inside it — Dialog directly (not ConfirmDialog) so the Card can fill the
|
|
236
|
+
fixed height as a flex column. -->
|
|
237
|
+
<Dialog bind:isOpen={helpOpen} width="80vw" height="80vh" aria-label={i18n.messages.querySyntax}>
|
|
238
|
+
<Card size="md" class="flex h-full w-full flex-col shadow-xl">
|
|
239
|
+
<CardHeader size="md">
|
|
240
|
+
<h3 class="text-lg font-medium [color:var(--ui-color-text)]">{i18n.messages.querySyntax}</h3>
|
|
241
|
+
</CardHeader>
|
|
242
|
+
<div class="min-h-0 flex-1 space-y-4 overflow-y-auto text-sm [color:var(--ui-color-text)]">
|
|
234
243
|
<p>
|
|
235
244
|
A filter is one or more conditions, <code class="font-mono">$column operator value</code>.
|
|
236
245
|
Whitespace is insignificant; combine with <code>&&</code>, <code>||</code>, <code>!</code>,
|
|
@@ -312,5 +321,6 @@
|
|
|
312
321
|
Tip: type <kbd class="rounded border px-1 {bc}">$</kbd> for columns; enum columns suggest
|
|
313
322
|
values. Applies on <kbd class="rounded border px-1 {bc}">{i18n.messages.enter}</kbd>.
|
|
314
323
|
</p>
|
|
315
|
-
|
|
316
|
-
</
|
|
324
|
+
</div>
|
|
325
|
+
</Card>
|
|
326
|
+
</Dialog>
|
|
@@ -834,6 +834,11 @@
|
|
|
834
834
|
// always border both vertical edges + nudge.
|
|
835
835
|
function popoutBorder(id: ItemId, group: "top" | "bottom", top: boolean, rowsShown: number) {
|
|
836
836
|
const farH = dockRight ? "border-l" : "border-r";
|
|
837
|
+
// The vertical edge facing the strip/parent (opposite the far edge). A TOP-level popout draws it via
|
|
838
|
+
// `seam` (in the dock-border colour, overlapping the strip's own border); a NESTED cascade floats
|
|
839
|
+
// mid-content against its parent popout, so it must draw this edge itself as a normal border —
|
|
840
|
+
// without it the second-level popout (e.g. Reports > Exports > …) is missing its parent-facing side.
|
|
841
|
+
const nearV = dockRight ? "border-r" : "border-l";
|
|
837
842
|
const near = group === "bottom" ? "border-b" : "border-t";
|
|
838
843
|
const opp = group === "bottom" ? "border-t" : "border-b";
|
|
839
844
|
const nudge = group === "bottom" ? 1 : -1;
|
|
@@ -848,7 +853,10 @@
|
|
|
848
853
|
: "";
|
|
849
854
|
const placed = top ? placedById[id] : undefined;
|
|
850
855
|
const aside = top ? asideRef?.getBoundingClientRect() : undefined;
|
|
851
|
-
|
|
856
|
+
// No placement to flush-test against: a top-level popout gets its strip-facing edge from `seam`;
|
|
857
|
+
// a nested cascade (never placed-tracked) must draw `nearV` so BOTH vertical edges are bordered.
|
|
858
|
+
if (!placed || !aside)
|
|
859
|
+
return { cls: `${farH}${top ? "" : ` ${nearV}`} ${near} ${opp}`, seam, nudge };
|
|
852
860
|
const h = rowsShown * rowPx;
|
|
853
861
|
const topFlush = placed.y <= aside.top + 1;
|
|
854
862
|
const bottomFlush = placed.y + h >= aside.bottom - 1;
|
|
@@ -1088,7 +1096,7 @@
|
|
|
1088
1096
|
open={isOnPath(id)}
|
|
1089
1097
|
side={popoutSide}
|
|
1090
1098
|
align={geom.align}
|
|
1091
|
-
offset={0}
|
|
1099
|
+
offset={top ? 0 : -1}
|
|
1092
1100
|
margin={top ? 0 : 8}
|
|
1093
1101
|
snap={rowPx}
|
|
1094
1102
|
lightDismiss={false}
|
|
@@ -1172,7 +1180,11 @@
|
|
|
1172
1180
|
{@render item.popout()}
|
|
1173
1181
|
</div>
|
|
1174
1182
|
{:else}
|
|
1175
|
-
{#if win.up}
|
|
1183
|
+
{#if win.up}
|
|
1184
|
+
{@render arrowRow(id, rows.length, -1)}
|
|
1185
|
+
<!-- Separate the ▲ scroll-step row from the items it scrolls, matching the inter-item divider. -->
|
|
1186
|
+
<div class="border-t [border-color:var(--ui-border-color)]" role="separator"></div>
|
|
1187
|
+
{/if}
|
|
1176
1188
|
<ul class="flex flex-col">
|
|
1177
1189
|
{#each rows.slice(win.start, win.end) as r, i (r.index)}
|
|
1178
1190
|
<!-- A divider renders only BETWEEN real rows: suppress it at the window's top edge (i===0),
|
|
@@ -1185,7 +1197,11 @@
|
|
|
1185
1197
|
</li>
|
|
1186
1198
|
{/each}
|
|
1187
1199
|
</ul>
|
|
1188
|
-
{#if win.down}
|
|
1200
|
+
{#if win.down}
|
|
1201
|
+
<!-- Separate the items from the ▼ scroll-step row below them. -->
|
|
1202
|
+
<div class="border-t [border-color:var(--ui-border-color)]" role="separator"></div>
|
|
1203
|
+
{@render arrowRow(id, rows.length, 1)}
|
|
1204
|
+
{/if}
|
|
1189
1205
|
{/if}
|
|
1190
1206
|
</div>
|
|
1191
1207
|
{/snippet}
|
|
@@ -79,7 +79,12 @@ export function computePlacement(anchor, size, opts = {}) {
|
|
|
79
79
|
// perpendicular to the chosen side) can `snap` its shift to whole steps; the main axis never snaps.
|
|
80
80
|
const clampSnap = (v, min, max) => {
|
|
81
81
|
const c = Math.min(Math.max(v, min), max);
|
|
82
|
-
|
|
82
|
+
// Only a REAL (>= 1px) clamp snaps to whole steps. A sub-pixel clamp is a rounding artifact — the
|
|
83
|
+
// boundary comes from the integer `clientWidth/clientHeight`, while the anchor rect is fractional on
|
|
84
|
+
// a fractional-DPR display or under browser zoom, so a box flush with the boundary edge can overflow
|
|
85
|
+
// by a fraction of a px. Rounding that UP to a whole step shoves the box a full row/column off (the
|
|
86
|
+
// bottom-group SideBarMenu popout landing one row too high). Below 1px, just clamp to the edge.
|
|
87
|
+
if (snap > 0 && Math.abs(c - v) >= 1) {
|
|
83
88
|
// Shoved by (c - v); round the magnitude UP to a whole step, keep direction, re-clamp.
|
|
84
89
|
const shoved = v + Math.sign(c - v) * Math.ceil(Math.abs(c - v) / snap) * snap;
|
|
85
90
|
return Math.min(Math.max(shoved, min), max);
|