@spaethtech/svelte-ui 0.21.2-dev.136.05477c6 → 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
- class="{cellClasses(col)} flex min-w-0 items-center {alignClass(col)} {truncate
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
- <Button
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 grid.sort?.col === sortKeyOf(col)}
365
- {#if grid.sort.dir === "asc"}<IconSortAsc
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 class="w-[1em] h-[1em] opacity-40" />
392
+ <IconSortNone
393
+ class="w-[1em] h-[1em] shrink-0 opacity-40 transition-opacity group-hover/sort:opacity-100"
394
+ />
370
395
  {/if}
371
- </Button>
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 ConfirmDialog from "./ConfirmDialog.svelte";
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
- <ConfirmDialog bind:isOpen={helpOpen} title={i18n.messages.querySyntax} showConfirm={false} showCancel={false}>
233
- <div class="max-h-[70vh] space-y-4 overflow-y-auto text-sm [color:var(--ui-color-text)]">
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
- </div>
316
- </ConfirmDialog>
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
- if (!placed || !aside) return { cls: `${farH} ${near} ${opp}`, seam, nudge };
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}{@render arrowRow(id, rows.length, -1)}{/if}
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}{@render arrowRow(id, rows.length, 1)}{/if}
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
- if (snap > 0 && c !== v) {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.21.2-dev.136.05477c6",
3
+ "version": "0.21.2-dev.139.462a2ec",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"