@celestia-island/hikari 0.55.319 → 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.319",
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",
@@ -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
+ });
@@ -471,32 +471,38 @@
471
471
  Table content is not prose. While the copy layer is enabled
472
472
  (`copy-cells`, the default), body cells kill drag mass-selection
473
473
  (user-select: none) and the cells whose background click copies
474
- advertise the click-to-copy affordance with the `copy` cursor — text
475
- clicks run the shared clipboard-with-toast path (HkTable.tsx). Both
476
- halves scope under `data-copy-cells`, so a consumer that opts out
477
- 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.
478
478
 
479
- The `copy` cursor rides `[data-copy-cell]`, NOT every cell
479
+ The `pointer` cursor rides `[data-copy-cell]`, NOT every cell
480
480
  (2026-11-07 user direction): the marker is the same predicate as the
481
481
  `title` hint and as a DEFAULT text cell's background click — a
482
482
  default-rendered cell, in a `copyable` column, whose value still
483
483
  renders text. Slot cells (chips/badges/layout wrappers) never take
484
484
  the click on their td SURFACE — only their inner text leaves copy —
485
485
  and `copyable: false` columns copy nothing at all, so both keep the
486
- ordinary arrow; a `copy` cursor there (which Windows paints as the
487
- legacy arrow+plus glyph) promised an affordance the surface click
488
- does not deliver. Two deliberate under-promises follow: an inner
489
- leaf that copies still shows the arrow, and a slot that renders BARE
490
- text (no wrapper) makes the td's own surface copy — yet stays
491
- unmarked because the slot owns its hints. Under-promising is the
492
- 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. */
493
499
  .hk-table[data-copy-cells] {
494
500
  .hk-table-cell {
495
501
  @include user-select(none);
496
502
  }
497
503
 
498
504
  .hk-table-cell[data-copy-cell] {
499
- cursor: copy;
505
+ cursor: pointer;
500
506
  }
501
507
 
502
508
  /* Defense in depth: the row-actions track is a control and never
@@ -504,7 +510,7 @@
504
510
  `.hk-table-cell` on purpose: that keeps its specificity equal to the
505
511
  marker rule above (`(0,4,0)`) and, declared after it, it still wins
506
512
  if a future widening ever hands the marker to the ⋯ trigger. A bare
507
- `.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. */
508
514
  .hk-table-cell.hk-table-manage-cell {
509
515
  cursor: default;
510
516
  }
@@ -1076,7 +1076,7 @@ export default defineComponent({
1076
1076
  const isSlotCell = Boolean(slots[`cell-${col.key}`]);
1077
1077
  const value = row[col.key];
1078
1078
  // ONE predicate behind the hover hint AND the
1079
- // `copy` cursor (`data-copy-cell`): a DEFAULT text
1079
+ // copy-affordance cursor (`pointer`, `data-copy-cell`): a DEFAULT text
1080
1080
  // cell advertises the copy affordance only when its
1081
1081
  // background click really copies — the td is a text
1082
1082
  // leaf that still RENDERS text (see `renderedText`).
@@ -1085,7 +1085,7 @@ export default defineComponent({
1085
1085
  // copy) and `copyable: false` columns copy nothing,
1086
1086
  // so both keep the plain arrow — a deliberate
1087
1087
  // under-promise, never an over-promise (2026-11-07
1088
- // user direction: `cursor: copy` on every cell
1088
+ // user direction: a copy affordance on every cell
1089
1089
  // promised more than the handler does; badges carry
1090
1090
  // their own pointer/tooltip). A slot that renders
1091
1091
  // BARE text is the one case where the td surface
@@ -139,7 +139,7 @@ describe("selection policy sheet", () => {
139
139
  // 2026-10-18 contract: copy-cells (default) denies drag selection on
140
140
  // every body cell; the opt-out now needs an explicit mirror — absence
141
141
  // of a deny no longer means selectable once the root denies. The
142
- // `copy` CURSOR is narrower than the selection deny since 2026-11-07
142
+ // click CURSOR is narrower than the selection deny since 2026-11-07
143
143
  // (marker-scoped, asserted by the next test): the deny still covers
144
144
  // the whole cell body, only the pointer narrowed.
145
145
  const css = compile("../components/HkTable.scss");
@@ -149,45 +149,62 @@ describe("selection policy sheet", () => {
149
149
  );
150
150
  });
151
151
 
152
- it("hands the copy cursor only to the marked cells, never to every cell", () => {
153
- // 2026-11-07 user direction: `.hk-table-cell { cursor: copy }` on the
154
- // whole body promised a copy on slot/control/empty cells whose
155
- // background click copies nothing. Asserted on the COMPILED sheet so
152
+ it("hands the clickable-text pointer only to the marked cells, never to every cell", () => {
153
+ // 2026-11-07 user direction: the copy affordance used to ride every
154
+ // `.hk-table-cell` (a copy cursor on slot/control/empty cells whose
155
+ // background click copies nothing). It now rides `[data-copy-cell]`
156
+ // only, and the cursor is the generic `pointer` — the OS paints CSS
157
+ // `copy` with the legacy arrow+plus bitmap, so the copy semantics
158
+ // live in the `title` hint instead. Asserted on the COMPILED sheet so
156
159
  // neither a commented-out rule nor a selector list that re-widens the
157
- // rule (`.hk-table-cell, .hk-table-cell[data-copy-cell]`) can pass.
160
+ // rule can pass.
158
161
  const css = compile("../components/HkTable.scss");
159
- const copyRules = [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)].filter((match) =>
160
- /cursor:\s*copy/.test(match[2]!),
162
+ // 1) The legacy `copy` glyph must not come back anywhere — not as the
163
+ // bare keyword, not as a `url(...)` fallback list, not in any case.
164
+ // Only the KEYWORD counts: `url(copy-icon.cur)` / `var(--copy-cursor)`
165
+ // merely spell "copy" without selecting the legacy glyph.
166
+ const legacyCopy = [...css.matchAll(/cursor\s*:([^;}]*)/gi)].filter((match) => {
167
+ const value = match[1]!.replace(/url\([^)]*\)|var\([^)]*\)/gi, " ");
168
+ return /\bcopy\b/i.test(value);
169
+ });
170
+ expect(legacyCopy).toHaveLength(0);
171
+ // 2) The marked cell IS the clickable-text surface.
172
+ expect(css).toMatch(
173
+ /\.hk-table\[data-copy-cells\] \.hk-table-cell\[data-copy-cell(?:="")?\]\{[^}]*?cursor:\s*pointer/,
174
+ );
175
+ // 3) No rule may hand an UNMARKED table cell the copy layer's pointer
176
+ // (that is exactly the re-widening this contract removed).
177
+ expect(css).not.toMatch(
178
+ /\.hk-table\[data-copy-cells\] \.hk-table-cell\{[^}]*?cursor:\s*pointer/,
161
179
  );
162
- expect(copyRules.length).toBeGreaterThan(0);
163
- for (const rule of copyRules) {
180
+ // 4) The marker must sit on the CELL compound, not on an ancestor:
181
+ // `.hk-table[data-copy-cell] .hk-table-cell` keeps the token in the
182
+ // selector yet can never match a real cell.
183
+ for (const rule of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
184
+ if (!/cursor\s*:[^;}]*\bpointer\b/i.test(rule[2]!)) continue;
164
185
  for (const selector of rule[1]!.split(",")) {
165
- // Tolerate the empty-value spelling Vue emits
166
- // (`[data-copy-cell=""]`) but neither a prefix token nor a
167
- // selector carrying a different value.
168
- expect(selector).toMatch(/\[data-copy-cell(?:="")?\]/);
169
- // The marker must sit on the CELL compound: moving it to an
170
- // ancestor (`.hk-table[data-copy-cell] .hk-table-cell`) keeps the
171
- // token in the selector yet can never match a real cell.
172
186
  const lastCompound = selector.trim().split(/\s+/).pop() ?? "";
173
- expect(lastCompound).toMatch(/\.hk-table-cell/);
187
+ // Only the CELL compound itself is constrained: a legitimate
188
+ // descendant rule (`.hk-table-cell a { cursor: pointer }`) is a
189
+ // control inside the cell and keeps its own cursor.
190
+ if (!lastCompound.startsWith(".hk-table-cell")) continue;
174
191
  expect(lastCompound).toMatch(/\[data-copy-cell(?:="")?\]/);
175
192
  }
176
193
  }
177
- // The marked rule must also sit under the table-level opt-out gate:
178
- // moving it into a dead block (`.hk-table:not([data-copy-cells])`)
179
- // would keep a marker-bearing selector while the cursor never applies.
180
- expect(css).toMatch(
181
- /\.hk-table\[data-copy-cells\] \.hk-table-cell\[data-copy-cell(?:="")?\]\{[^}]*?cursor:\s*copy/,
182
- );
183
- // The control-track override repeats `.hk-table-cell` so its
184
- // specificity TIES the marker rule above and, declared after it, wins
185
- // the tie — the defense-in-depth promise needs that ordering.
194
+ // 5) The control-track override repeats `.hk-table-cell` so its
195
+ // specificity TIES the marker rule above and, declared after it,
196
+ // wins the tie — the defense-in-depth promise needs that ordering.
186
197
  expect(css).toMatch(
187
198
  /\.hk-table\[data-copy-cells\] \.hk-table-cell\.hk-table-manage-cell\{[^}]*?cursor:\s*default/,
188
199
  );
189
- expect(css.indexOf(".hk-table-cell.hk-table-manage-cell")).toBeGreaterThan(
190
- css.indexOf("[data-copy-cell]"),
200
+ // Anchor on the rule that DECLARES the cursor (not merely on the first
201
+ // selector that mentions the marker — a decoy rule carrying the selector
202
+ // without a cursor would neutralize the order pin).
203
+ const markedRule = css.match(
204
+ /\.hk-table\[data-copy-cells\] \.hk-table-cell\[data-copy-cell(?:="")?\]\{[^}]*?cursor:\s*pointer/,
191
205
  );
192
- });
206
+ expect(markedRule).not.toBeNull();
207
+ expect(css.indexOf(".hk-table-cell.hk-table-manage-cell")).toBeGreaterThan(
208
+ css.indexOf(markedRule![0]),
209
+ ); });
193
210
  });