@celestia-island/hikari 0.55.318 → 0.55.320

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.318",
3
+ "version": "0.55.320",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -453,7 +453,13 @@ export default defineComponent({
453
453
  }
454
454
  const band = vp.getBoundingClientRect();
455
455
  const active = pickActiveProgressRoot(band);
456
- const rows = active ? active.root.querySelectorAll(props.scrollProgressSelector) : [];
456
+ // A list may own the row grammar (HkTable's section bands are body
457
+ // `<tr>`s that are not data rows); its `getRows` wins over the
458
+ // container selector. No `getRows` (or an undefined return) keeps the
459
+ // existing selector path byte-for-byte.
460
+ const rows = active
461
+ ? active.handle.getRows?.() ?? active.root.querySelectorAll(props.scrollProgressSelector)
462
+ : [];
457
463
  if (!active || rows.length === 0 || vp.scrollHeight <= vp.clientHeight + 1) {
458
464
  setProgressHidden();
459
465
  return;
@@ -11,6 +11,7 @@ import { createApp, defineComponent, h, inject, nextTick, onMounted, onUnmounted
11
11
 
12
12
  import HkScrollContainer from "./HkScrollContainer";
13
13
  import { HkAdminTablePage } from "./HkAdminTablePage";
14
+ import HTable from "./HkTable";
14
15
  import { HK_SCROLL_PROGRESS_REGISTRAR, type HkScrollProgressRegistrar } from "./HkScrollProgress";
15
16
 
16
17
  const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
@@ -219,6 +220,24 @@ describe("HkScrollProgress", () => {
219
220
  expect(pill.textContent).toBe("11 / 340");
220
221
  });
221
222
 
223
+ it("counts only a sectioned HkTable's data rows, not its bands", async () => {
224
+ // The band is a `<tr>` in the section body but not a row. The table's
225
+ // handle `getRows` wins over the container's `tbody tr` selector, so
226
+ // the pill reports 2 sections x 2 rows = 4, not 6.
227
+ const sections = [
228
+ { key: "panel-a", label: "Panel A", rows: [{ name: "a0" }, { name: "a1" }] },
229
+ { key: "panel-b", label: "Panel B", rows: [{ name: "b0" }, { name: "b1" }] },
230
+ ];
231
+ const scroller = mountScroller(() =>
232
+ h(HTable, { columns: [{ key: "name", title: "Name" }], sections }),
233
+ );
234
+ stubRootRect(scroller.container, ".hk-table-host", -100, 1900);
235
+ stubRows(scroller.container, 6, 0);
236
+ scroller.refresh();
237
+ await nextTick();
238
+ expect(scroller.pill()!.textContent).toBe("1 / 4");
239
+ });
240
+
222
241
  it("falls back to the DOM row count when the total is absent", async () => {
223
242
  const List = makeList({ rows: ROWS });
224
243
  const scroller = mountScroller(() => h(List));
@@ -24,6 +24,12 @@ import "./HkScrollProgress.scss";
24
24
  export interface HkScrollProgressHandle {
25
25
  getRoot(): HTMLElement | null;
26
26
  getTotal(): number | undefined;
27
+ /** The list's own row elements in display order — wins over the
28
+ * container's `scrollProgressSelector` when present. This is for a
29
+ * list whose DOM rows are not all data rows (HkTable's section bands
30
+ * are `<tr>`s in the body, but they are not rows). Return `undefined`
31
+ * to fall back to the container's selector. */
32
+ getRows?(): readonly Element[] | undefined;
27
33
  }
28
34
 
29
35
  /** Provided by every HkScrollContainer instance whose `scrollProgress`
@@ -1,9 +1,11 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { createApp, h } from "vue";
3
3
  import { readFileSync } from "node:fs";
4
- import { dirname, join } from "node:path";
4
+ import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
+ import * as sass from "sass";
8
+
7
9
  import HTable from "./HkTable";
8
10
  import { useToast } from "../runtime/useToast";
9
11
 
@@ -23,10 +25,11 @@ import { useToast } from "../runtime/useToast";
23
25
  * row-action affordance)
24
26
  * - `Column.copyable: false` and the table-wide `copy-cells=false` opt
25
27
  * out wholesale
26
- * - the `copy` cursor and the hover hint ride `[data-copy-cell]` — the
27
- * default text cells, in a copyable column, whose value still renders
28
- * text — never every cell, so the pointer describes what a background
29
- * click really does. Slot cells' inner leaves still copy, so their
28
+ * - the click cursor (`pointer`; the copy SEMANTICS live in the `title`
29
+ * hint) and the hover hint ride `[data-copy-cell]` — the default text
30
+ * cells, in a copyable column, whose value still renders text — never
31
+ * every cell, so the cursor describes what a background click really
32
+ * does. Slot cells' inner leaves still copy, so their
30
33
  * plain arrow is a deliberate under-promise (2026-11-07 user
31
34
  * direction)
32
35
  * - `manageable` renders the trailing ⋯ column (HkRowActions) and
@@ -77,6 +80,34 @@ const COLS = [
77
80
  { key: "endpoint", title: "Endpoint" },
78
81
  ];
79
82
 
83
+ // The compiled sheet, injected into the document so `getComputedStyle` sees
84
+ // the REAL cascade. Selector-text assertions cannot catch a rule that reaches
85
+ // an unmarked cell through inheritance, an element selector (`td`) or a media
86
+ // block — the cascade can.
87
+ //
88
+ // `style: "expanded"` is load-bearing: the compressed writer emits
89
+ // `@media(min-width:600px)` (no space after `@media`) and happy-dom then
90
+ // DROPS the whole block (cssRules stays empty), which would make a
91
+ // media-wrapped widening rule invisible to this suite.
92
+ const HERE = dirname(fileURLToPath(import.meta.url));
93
+ const SRC = resolve(HERE, "..");
94
+ const SHEET_STYLE = "expanded" as const;
95
+ const SHEET_CSS = sass.compile(resolve(HERE, "HkTable.scss"), {
96
+ loadPaths: [SRC, join(SRC, "components"), join(SRC, "styles"), join(SRC, "styles", "theme")],
97
+ style: SHEET_STYLE,
98
+ silenceDeprecations: ["import", "global-builtin", "legacy-js-api"],
99
+ }).css;
100
+ // The parser probe below is compiled with the SAME style on purpose: if a
101
+ // future edit flips SHEET_STYLE back to "compressed", the probe loses its
102
+ // media block too and this suite goes red. That keeps the "fixture must stay
103
+ // `expanded`" premise under its own tripwire instead of silently disarming
104
+ // the cascade assertion.
105
+ const PROBE_CSS = sass.compileString("@media (min-width: 600px) { .hk-probe { color: red } }", {
106
+ style: SHEET_STYLE,
107
+ }).css;
108
+ // happy-dom reports an unset cursor as "" (a browser reports "auto").
109
+ const isAuto = (value: string): boolean => value === "" || value === "auto";
110
+
80
111
  async function settle() {
81
112
  await Promise.resolve();
82
113
  await Promise.resolve();
@@ -416,3 +447,69 @@ describe("HkTable interaction stylesheet", () => {
416
447
  expect(block.indexOf("cursor: default")).toBeGreaterThan(-1);
417
448
  });
418
449
  });
450
+
451
+ describe("HkTable click-cursor cascade", () => {
452
+ let sheet: HTMLStyleElement;
453
+ beforeEach(() => {
454
+ sheet = document.createElement("style");
455
+ sheet.textContent = SHEET_CSS;
456
+ document.head.appendChild(sheet);
457
+ });
458
+ afterEach(() => {
459
+ sheet.remove();
460
+ });
461
+
462
+ it("parses the injected sheet (a blind sheet would make this suite vacuous)", () => {
463
+ expect(sheet.sheet?.cssRules?.length ?? 0).toBeGreaterThan(0);
464
+ // Probe the PARSER, not this sheet: the next test can only catch a
465
+ // media-wrapped widening rule if a media block survives parsing. The
466
+ // probe CSS is synthetic (so unrelated sheet edits, e.g. dropping the
467
+ // reduced-motion block, can never turn this suite red) but compiled with
468
+ // the SAME style as the fixture (so a fixture-style regression does).
469
+ const probe = document.createElement("style");
470
+ probe.textContent = PROBE_CSS;
471
+ document.head.appendChild(probe);
472
+ try {
473
+ const rules = [...(probe.sheet?.cssRules ?? [])];
474
+ expect(rules.some((rule) => typeof (rule as { media?: unknown }).media !== "undefined")).toBe(
475
+ true,
476
+ );
477
+ } finally {
478
+ probe.remove();
479
+ }
480
+ });
481
+
482
+ it("reaches the marked cell only — computed cascade, not selector text", () => {
483
+ const c = mount(
484
+ h(
485
+ HTable,
486
+ {
487
+ columns: [...COLS, { key: "secret", title: "Secret", copyable: false }],
488
+ rows: [{ name: "alpha", endpoint: "https://x.example", secret: "nope-token" }],
489
+ draggable: true,
490
+ selectable: true,
491
+ manageable: true,
492
+ },
493
+ { "cell-endpoint": () => "pure-text-slot" },
494
+ ),
495
+ );
496
+ // drag grip, selection, name, endpoint (slot), secret (opted out), row actions
497
+ const tds = Array.from(c.querySelectorAll<HTMLElement>("tbody td"));
498
+ expect(tds).toHaveLength(6);
499
+ const cursor = (index: number): string => getComputedStyle(tds[index]!).cursor;
500
+ expect(cursor(2)).toBe("pointer");
501
+ for (const index of [0, 1, 3, 4]) {
502
+ expect(isAuto(cursor(index))).toBe(true);
503
+ }
504
+ expect(cursor(5)).toBe("default");
505
+ });
506
+
507
+ it("an opted-out table hands every cell the ordinary cursor back", () => {
508
+ const c = mount(
509
+ h(HTable, { columns: COLS, rows: [{ name: "alpha", endpoint: "x" }], copyCells: false }),
510
+ );
511
+ for (const td of Array.from(c.querySelectorAll<HTMLElement>("tbody td"))) {
512
+ expect(isAuto(getComputedStyle(td).cursor)).toBe(true);
513
+ }
514
+ });
515
+ });
@@ -95,6 +95,26 @@
95
95
  background: var(--hi-color-bg-subtle, rgba(255, 255, 255, 0.03));
96
96
  }
97
97
 
98
+ /* A section's band is the first child of its body, so the base zebra rule
99
+ would shade each section's FIRST data row (child 2). Restart the phase
100
+ after the band: even data children are unshaded, odd ones carry the
101
+ stripe — the same first-row-unshaded phase an unsectioned body has. */
102
+ .hk-table-striped .hk-table-section-body .hk-table-row:nth-child(even) {
103
+ background: none;
104
+ }
105
+
106
+ .hk-table-striped .hk-table-section-body .hk-table-row:nth-child(odd) {
107
+ background: var(--hi-color-bg-subtle, rgba(255, 255, 255, 0.03));
108
+ }
109
+
110
+ /* The sectioned zebra pair above is (0,4,0) — it would out-specify the
111
+ shared `.hk-table-hover .hk-table-row:hover` rule (0,3,0) and kill hover
112
+ feedback on EVERY row of a striped sectioned table. Keep the pointer
113
+ feedback by re-stating it at a spec that beats the pair. */
114
+ .hk-table-striped.hk-table-hover .hk-table-section-body .hk-table-row:hover {
115
+ background: var(--hi-color-bg-subtle, rgba(255, 255, 255, 0.04));
116
+ }
117
+
98
118
  .hk-table-bordered {
99
119
  border: 1px solid var(--hi-color-border, rgba(100,100,100,0.1));
100
120
  border-radius: var(--radius-md, 8px);
@@ -153,6 +173,51 @@
153
173
  color: var(--hi-color-text-secondary, #666666);
154
174
  }
155
175
 
176
+ /* ── Section bands (grouped tables) ───────────────────────────────────
177
+ One `<tbody>` per section, each led by a band row. The band is a
178
+ rowgroup header, not a data row: it never carries `.hk-table-row`, so
179
+ the copy layer (which marks and cursor-styles cells via
180
+ `[data-copy-cell]`), the hover paint, the striped rule, the drag strip
181
+ and the selection/⋯ tracks all ignore it by construction. The band cell
182
+ carries no `data-copy-cell`, so it needs no copy-cursor override either.
183
+
184
+ The cell itself is TYPE-NEUTRAL for consumer markup: the `section` slot
185
+ carries arbitrary content (badges, panel names, status chips), so only
186
+ the track is styled here (padding / hairline / background / start
187
+ alignment). `font-weight: inherit` neutralizes the UA `th { font-weight:
188
+ bold }`; nothing here sets `white-space` (long panel names may wrap) or
189
+ `cursor` (selectable band text keeps its I-beam). Sizing is delivered by
190
+ the consumer's own classes; the plain `label` fallback gets its own span
191
+ below. The hk-table-sm/lg ladder steps ONLY the band's padding —
192
+ typography deliberately does not scale with the table size, because the
193
+ consumer's slot content owns its type. */
194
+ .hk-table-section-cell {
195
+ padding: var(--space-8, 8px) var(--space-12, 12px);
196
+ border-bottom: 1px solid var(--hi-color-border, rgba(100,100,100,0.08));
197
+ text-align: start;
198
+ background: var(--hi-color-bg-subtle, rgba(255, 255, 255, 0.03));
199
+ font-weight: inherit;
200
+ }
201
+
202
+ /* The default `label` branch only (the `section` slot bypasses this span
203
+ entirely): the previous text-sm / font-medium band look. */
204
+ .hk-table-section-label {
205
+ font-size: var(--text-sm, 0.875rem);
206
+ font-weight: 500;
207
+ }
208
+
209
+ .hk-table-sm .hk-table-section-cell {
210
+ padding: var(--space-4, 4px) var(--space-8, 8px);
211
+ }
212
+
213
+ .hk-table-lg .hk-table-section-cell {
214
+ padding: var(--space-16, 16px) var(--space-24, 24px);
215
+ }
216
+
217
+ .hk-table-bordered .hk-table-section-cell {
218
+ border: 1px solid var(--hi-color-border, rgba(100,100,100,0.06));
219
+ }
220
+
156
221
  /* ── Drag-to-reorder (draggable) ─────────────────────────────────────── */
157
222
 
158
223
  .hk-table-drag-handle {
@@ -406,32 +471,38 @@
406
471
  Table content is not prose. While the copy layer is enabled
407
472
  (`copy-cells`, the default), body cells kill drag mass-selection
408
473
  (user-select: none) and the cells whose background click copies
409
- advertise the click-to-copy affordance with the `copy` cursor — text
410
- clicks run the shared clipboard-with-toast path (HkTable.tsx). Both
411
- halves scope under `data-copy-cells`, so a consumer that opts out
412
- gets plain selectable text back wholesale.
474
+ advertise the click-to-copy affordance with the `pointer` cursor —
475
+ text clicks run the shared clipboard-with-toast path (HkTable.tsx).
476
+ Both halves scope under `data-copy-cells`, so a consumer that opts
477
+ out gets plain selectable text back wholesale.
413
478
 
414
- The `copy` cursor rides `[data-copy-cell]`, NOT every cell
479
+ The `pointer` cursor rides `[data-copy-cell]`, NOT every cell
415
480
  (2026-11-07 user direction): the marker is the same predicate as the
416
481
  `title` hint and as a DEFAULT text cell's background click — a
417
482
  default-rendered cell, in a `copyable` column, whose value still
418
483
  renders text. Slot cells (chips/badges/layout wrappers) never take
419
484
  the click on their td SURFACE — only their inner text leaves copy —
420
485
  and `copyable: false` columns copy nothing at all, so both keep the
421
- ordinary arrow; a `copy` cursor there (which Windows paints as the
422
- legacy arrow+plus glyph) promised an affordance the surface click
423
- does not deliver. Two deliberate under-promises follow: an inner
424
- leaf that copies still shows the arrow, and a slot that renders BARE
425
- text (no wrapper) makes the td's own surface copy — yet stays
426
- unmarked because the slot owns its hints. Under-promising is the
427
- point; over-promising is the bug this rule removes. */
486
+ ordinary arrow; a copy affordance there promised something the
487
+ surface click does not deliver. Two deliberate under-promises
488
+ follow: an inner leaf that copies still shows the arrow, and a slot
489
+ that renders BARE text (no wrapper) makes the td's own surface copy
490
+ — yet stays unmarked because the slot owns its hints. Under-
491
+ promising is the point; over-promising is the bug this rule removes.
492
+
493
+ Why `pointer`, not `copy` (2026-11-07 user direction, second turn):
494
+ the OS paints CSS `copy` with the legacy arrow+plus bitmap (no
495
+ themed cursor exists for it), which reads as a 90s artifact next to
496
+ the modern arrow. The COPY semantics live in the `title` hint
497
+ ("Click to copy"); the cursor only has to say "this text reacts to a
498
+ click", which is exactly what `pointer` says. */
428
499
  .hk-table[data-copy-cells] {
429
500
  .hk-table-cell {
430
501
  @include user-select(none);
431
502
  }
432
503
 
433
504
  .hk-table-cell[data-copy-cell] {
434
- cursor: copy;
505
+ cursor: pointer;
435
506
  }
436
507
 
437
508
  /* Defense in depth: the row-actions track is a control and never
@@ -439,7 +510,7 @@
439
510
  `.hk-table-cell` on purpose: that keeps its specificity equal to the
440
511
  marker rule above (`(0,4,0)`) and, declared after it, it still wins
441
512
  if a future widening ever hands the marker to the ⋯ trigger. A bare
442
- `.hk-table-manage-cell` (`(0,3,0)`) would now lose that tie. */
513
+ `.hk-table-manage-cell` (`(0,3,0)`) would lose that tie outright. */
443
514
  .hk-table-cell.hk-table-manage-cell {
444
515
  cursor: default;
445
516
  }