@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,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
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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 `
|
|
475
|
-
clicks run the shared clipboard-with-toast path (HkTable.tsx).
|
|
476
|
-
halves scope under `data-copy-cells`, so a consumer that opts
|
|
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 `
|
|
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
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
//
|
|
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
|
|
153
|
-
// 2026-11-07 user direction:
|
|
154
|
-
//
|
|
155
|
-
// background click copies nothing.
|
|
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
|
|
160
|
+
// rule can pass.
|
|
158
161
|
const css = compile("../components/HkTable.scss");
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
|
178
|
-
//
|
|
179
|
-
//
|
|
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
|
-
|
|
190
|
-
|
|
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
|
});
|