@celestia-island/hikari 0.55.223 → 0.55.224
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 +1 -1
- package/src/components/HkAlert.scss +5 -0
- package/src/components/HkBlockingToast.scss +7 -0
- package/src/components/HkConfirmDialog.scss +4 -0
- package/src/components/HkDiffViewer.scss +4 -0
- package/src/components/HkJsonTree.scss +5 -0
- package/src/components/HkLogWindow.scss +6 -0
- package/src/components/HkMarkdownRenderer.scss +5 -0
- package/src/components/HkMessageBox.scss +5 -0
- package/src/components/HkTable.scss +11 -0
- package/src/components/HkToast.scss +6 -0
- package/src/res/boot-chrome.js +5 -4
- package/src/res/bootChrome.test.ts +5 -4
- package/src/styles/index.scss +4 -0
- package/src/styles/selection.scss +59 -0
- package/src/styles/selectionSheet.test.ts +134 -0
package/package.json
CHANGED
|
@@ -93,6 +93,11 @@
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
.hk-alert-text {
|
|
96
|
+
/* Alert body = the same copy-this-error-detail content class as the
|
|
97
|
+
* toast message and message-box text (L2 selection policy re-enables,
|
|
98
|
+
* 2026-10-29); the title above stays chrome. */
|
|
99
|
+
-webkit-user-select: text;
|
|
100
|
+
user-select: text;
|
|
96
101
|
margin: 0;
|
|
97
102
|
line-height: 1.5;
|
|
98
103
|
color: color-mix(in srgb, var(--hi-color-text-secondary, #666666) 85%, transparent);
|
|
@@ -58,10 +58,17 @@
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
.hk-blocking-toast-title {
|
|
61
|
+
/* Content, not chrome — copy-worthy detail under the L2 selection
|
|
62
|
+
* policy (2026-10-29): the card shell denies, these two text blocks
|
|
63
|
+
* select again. */
|
|
64
|
+
-webkit-user-select: text;
|
|
65
|
+
user-select: text;
|
|
61
66
|
font-weight: 700;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
.hk-blocking-toast-message {
|
|
70
|
+
-webkit-user-select: text;
|
|
71
|
+
user-select: text;
|
|
65
72
|
margin: 0;
|
|
66
73
|
display: -webkit-box;
|
|
67
74
|
-webkit-line-clamp: 5;
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
.hk-confirm-dialog-message {
|
|
18
|
+
/* Dialog message = copy-worthy content, same class as the message-box
|
|
19
|
+
* text (L2 selection policy re-enables, 2026-10-29). */
|
|
20
|
+
-webkit-user-select: text;
|
|
21
|
+
user-select: text;
|
|
18
22
|
margin: 0;
|
|
19
23
|
color: color-mix(in srgb, var(--hi-color-text-secondary) 85%, transparent);
|
|
20
24
|
font-size: var(--text-sm, 0.8125rem);
|
|
@@ -67,6 +67,10 @@
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
.hk-diff-viewer__code {
|
|
70
|
+
/* The diff body is content (review text users copy into replies);
|
|
71
|
+
* the line-number gutter above and the sign column stay denied. */
|
|
72
|
+
-webkit-user-select: text;
|
|
73
|
+
user-select: text;
|
|
70
74
|
padding-inline-end: 1ch;
|
|
71
75
|
min-width: 0;
|
|
72
76
|
}
|
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
.s-jt-row {
|
|
48
|
+
/* Tree rows are content — values users copy out of a payload — while
|
|
49
|
+
* the pane chrome above stays denied (L2 selection policy, 2026-10-29;
|
|
50
|
+
* the copy-tree affordance remains the bulk path). */
|
|
51
|
+
-webkit-user-select: text;
|
|
52
|
+
user-select: text;
|
|
48
53
|
/* Baseline alignment keeps the small type hints (boolean/number/string)
|
|
49
54
|
on the value's text baseline instead of floating above it. */
|
|
50
55
|
display: flex;
|
|
@@ -117,6 +117,12 @@
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
.s-log-entry {
|
|
120
|
+
/* Log lines are the canonical copy-me content (diagnostics users
|
|
121
|
+
* select per-fragment, not just via the toolbar copy-all) — re-enable
|
|
122
|
+
* under the L2 selection policy (2026-10-29); the toolbar chrome
|
|
123
|
+
* around them stays denied. */
|
|
124
|
+
-webkit-user-select: text;
|
|
125
|
+
user-select: text;
|
|
120
126
|
padding: var(--space-2, 0.125rem) var(--space-4, 0.25rem);
|
|
121
127
|
font-family: var(--font-mono, vars.$hikari-font-family-mono);
|
|
122
128
|
font-size: var(--text-xs, 0.75rem);
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
@use "./hikari-vars" as vars;
|
|
3
3
|
|
|
4
4
|
.hk-markdown {
|
|
5
|
+
/* Rendered prose is content by definition — the whole subtree selects
|
|
6
|
+
* again under the L2 selection policy (2026-10-29); its code blocks
|
|
7
|
+
* are covered twice over (whitelist tier 3). */
|
|
8
|
+
-webkit-user-select: text;
|
|
9
|
+
user-select: text;
|
|
5
10
|
font-size: var(--text-base);
|
|
6
11
|
line-height: 1.7;
|
|
7
12
|
color: var(--hi-color-text-primary, #1e1e1e);
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
.hk-message-box-text {
|
|
28
|
+
/* Dialog message = content the user may need to copy (error detail),
|
|
29
|
+
* not chrome — whitelisted under the L2 selection policy (2026-10-29).
|
|
30
|
+
* The prompt's field below is covered by the editable-controls tier. */
|
|
31
|
+
-webkit-user-select: text;
|
|
32
|
+
user-select: text;
|
|
28
33
|
margin: 0;
|
|
29
34
|
color: rgb(var(--color-text));
|
|
30
35
|
font-size: var(--text-base, 15px);
|
|
@@ -420,6 +420,17 @@
|
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
/* Opt-out consumers get plain selectable cells back — explicit because
|
|
424
|
+
* the L2 selection policy (2026-10-29) denies at the root now: absence
|
|
425
|
+
* of the copy-cells deny rule no longer means selectable. Same shape as
|
|
426
|
+
* the copy layer, so "wholesale" is a per-cell mirror of the default. */
|
|
427
|
+
.hk-table:not([data-copy-cells]) {
|
|
428
|
+
.hk-table-cell {
|
|
429
|
+
-webkit-user-select: text;
|
|
430
|
+
user-select: text;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
423
434
|
/* The standard trailing row-actions column (HkRowActions trigger):
|
|
424
435
|
fixed narrow track, centered, never wrapping. */
|
|
425
436
|
.hk-table-manage-cell {
|
|
@@ -55,6 +55,12 @@
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
.hk-toast-message {
|
|
58
|
+
/* The message text is content — error detail a user copies out of a
|
|
59
|
+
* failing app — not chrome. Re-enable inside the denied toast shell
|
|
60
|
+
* (L2 selection policy, 2026-10-29): the shell keeps its none, the
|
|
61
|
+
* message subtree selects again. */
|
|
62
|
+
-webkit-user-select: text;
|
|
63
|
+
user-select: text;
|
|
58
64
|
flex: 1;
|
|
59
65
|
min-width: 0;
|
|
60
66
|
display: -webkit-box;
|
package/src/res/boot-chrome.js
CHANGED
|
@@ -720,10 +720,11 @@
|
|
|
720
720
|
// stays enabled even when the window controls degraded.
|
|
721
721
|
button.disabled = false;
|
|
722
722
|
button.removeAttribute("aria-disabled");
|
|
723
|
-
//
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
|
|
723
|
+
// Reload LEADS the cluster (2026-10-29 user direction): the affordance
|
|
724
|
+
// sits ahead of the window controls — [reload][minimize][maximize][close]
|
|
725
|
+
// — so the three caption buttons (and the close frame corner) keep their
|
|
726
|
+
// familiar trailing positions.
|
|
727
|
+
actions.insertBefore(button, minimizeBtn);
|
|
727
728
|
reloadBtn = button;
|
|
728
729
|
return button;
|
|
729
730
|
}
|
|
@@ -396,19 +396,20 @@ describe("boot-chrome.js handoff and watchdog", () => {
|
|
|
396
396
|
expect(control("close").disabled).toBe(false);
|
|
397
397
|
});
|
|
398
398
|
|
|
399
|
-
it("puts the reload affordance
|
|
399
|
+
it("puts the reload affordance ahead of the window controls (2026-10-29 user direction)", () => {
|
|
400
400
|
useWindowApi(fakeWindowApi());
|
|
401
401
|
setGlobal("__TAURI_INTERNALS__", { invoke: vi.fn() });
|
|
402
402
|
runBootChrome({ hangMs: 1000 });
|
|
403
403
|
|
|
404
404
|
vi.advanceTimersByTime(1000);
|
|
405
405
|
|
|
406
|
-
//
|
|
407
|
-
//
|
|
406
|
+
// Reload LEADS the cluster — [reload][min][max][close] — so the three
|
|
407
|
+
// caption buttons (and the close frame corner) keep their familiar
|
|
408
|
+
// trailing positions instead of the reload splitting max from close.
|
|
408
409
|
expect(controls().map((button) => button.getAttribute("data-act"))).toEqual([
|
|
410
|
+
"reload",
|
|
409
411
|
"minimize",
|
|
410
412
|
"maximize",
|
|
411
|
-
"reload",
|
|
412
413
|
"close",
|
|
413
414
|
]);
|
|
414
415
|
});
|
package/src/styles/index.scss
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// 1. theme/channels.scss — the static seed: --color-* channel palette,
|
|
7
7
|
// --hi-color-* derivations, --c-*/--shadow-*/--border-* aliases and
|
|
8
8
|
// the icon-button bridge (vendored from src/tokens.scss).
|
|
9
|
+
// 1b. selection.scss — the selection policy: deny-by-default at the
|
|
10
|
+
// root (chrome text never takes a drag selection), whitelisted back
|
|
11
|
+
// for editable controls and the .hk-selectable content hook.
|
|
9
12
|
// 2. theme/scale.scss — the L2 scale tokens: spacing, type, radius,
|
|
10
13
|
// blur, opacity, durations, easings, z bands, fonts (vendored from
|
|
11
14
|
// src/scale.scss).
|
|
@@ -28,6 +31,7 @@
|
|
|
28
31
|
// version of this file re-exported packages/theme + packages/components
|
|
29
32
|
// through paths that escape the npm package and did not compile.
|
|
30
33
|
@use "./reset.scss";
|
|
34
|
+
@use "./selection.scss";
|
|
31
35
|
@use "./theme/channels.scss";
|
|
32
36
|
@use "./theme/scale.scss";
|
|
33
37
|
@use "../overlay/overlayRoot.scss";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/* ── Selection policy (L2) — deny by default, whitelist back ───────────
|
|
2
|
+
* 2026-10-29 user direction: UI chrome text (workspace menus, buttons,
|
|
3
|
+
* tabs, labels, rails, caption bars) must not take a drag selection.
|
|
4
|
+
* The old model was a blacklist — every component sheet opted out
|
|
5
|
+
* piecemeal, and any surface a sheet forgot shipped selectable until a
|
|
6
|
+
* screenshot complained. This sheet flips the default: the root denies,
|
|
7
|
+
* and only whitelisted subtrees select again.
|
|
8
|
+
*
|
|
9
|
+
* How one rule covers everything: `user-select` is not inherited, but
|
|
10
|
+
* its initial value `auto` RESOLVES against the parent's used value.
|
|
11
|
+
* A single `none` on `html` therefore determines every descendant
|
|
12
|
+
* (shadow trees included), and a whitelisted subtree declaring a
|
|
13
|
+
* concrete value re-enables its own descendants the same way — no
|
|
14
|
+
* `*` sweep, and component/host rules keep winning by specificity or
|
|
15
|
+
* order wherever they speak.
|
|
16
|
+
*
|
|
17
|
+
* The whitelist:
|
|
18
|
+
* 1. Editable controls — input, textarea, select, contenteditable:
|
|
19
|
+
* the user authored this text; selecting it is the point.
|
|
20
|
+
* 2. `.hk-selectable` — the opt-in utility for hosts and components:
|
|
21
|
+
* put it on any subtree whose text is CONTENT (chat bodies, logs,
|
|
22
|
+
* error details, IDs, prose, standalone code blocks) rather than
|
|
23
|
+
* chrome. This is the contract that replaces the old per-component
|
|
24
|
+
* blacklist.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately NOT whitelisted: bare `pre` / `code` / `kbd` / `samp`.
|
|
27
|
+
* A concrete value on an element BEATS the ancestor's none-propagation,
|
|
28
|
+
* so an element-type whitelist would punch through chrome that embeds
|
|
29
|
+
* those elements — HkButton's HkKbd shortcut chip (denied `.hk-btn` on
|
|
30
|
+
* the button itself), or a `<code>` slotted into a click-to-copy table
|
|
31
|
+
* cell (the 2026-10-18 copy-cells contract). Content containers own
|
|
32
|
+
* their re-enables instead: hikari's prose/code components (markdown
|
|
33
|
+
* body, diff code, json-tree rows, log entries) self-declare
|
|
34
|
+
* `user-select: text` in their sheets, so vendored copies carry their
|
|
35
|
+
* own contract, and hosts mark standalone code/log surfaces with
|
|
36
|
+
* `.hk-selectable`.
|
|
37
|
+
*
|
|
38
|
+
* Order: SECOND in the canonical composition (index.scss), right after
|
|
39
|
+
* the reset — the same "only lowers defaults; component sheets and host
|
|
40
|
+
* styles override by specificity/order" discipline applies. A host that
|
|
41
|
+
* truly wants select-everything back sets `user-select: text` on its own
|
|
42
|
+
* root; marking content with `.hk-selectable` is the supported route.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
html {
|
|
46
|
+
-webkit-user-select: none;
|
|
47
|
+
user-select: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
input,
|
|
51
|
+
textarea,
|
|
52
|
+
select,
|
|
53
|
+
/* `i` flag: HTML enumerated attribute values are ASCII case-insensitive,
|
|
54
|
+
* so contenteditable="FALSE" (valid, means false) must be excluded too. */
|
|
55
|
+
[contenteditable]:not([contenteditable='false' i]),
|
|
56
|
+
.hk-selectable {
|
|
57
|
+
-webkit-user-select: text;
|
|
58
|
+
user-select: text;
|
|
59
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import * as sass from "sass";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Selection policy guard (2026-10-29 user direction).
|
|
10
|
+
*
|
|
11
|
+
* `selection.scss` flips the selection model from a blacklist (component
|
|
12
|
+
* sheets opting out piecemeal; anything a sheet forgot shipped selectable)
|
|
13
|
+
* to a whitelist: the root denies, editable controls / the `.hk-selectable`
|
|
14
|
+
* content hook / pre-code conventions select again, and hikari's own
|
|
15
|
+
* content-bearing components self-declare their re-enables in their sheets
|
|
16
|
+
* (toast message, markdown body, diff code, json-tree rows, table opt-out).
|
|
17
|
+
*
|
|
18
|
+
* The contract under test, per half:
|
|
19
|
+
* - the canonical composition loads the sheet SECOND (right after the
|
|
20
|
+
* reset, before any theme/component sheet can override);
|
|
21
|
+
* - the compiled policy carries the root deny and every whitelist tier
|
|
22
|
+
* (assertions run on the COMPILED sheet — compressed drops both
|
|
23
|
+
* comment styles, so raw-text matching cannot pass on a commented-out
|
|
24
|
+
* rule);
|
|
25
|
+
* - the component re-enables really exist in the compiled component
|
|
26
|
+
* sheets, with the chrome halves still denied.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const stylesDir = resolve(dirname(fileURLToPath(import.meta.url)));
|
|
30
|
+
const srcDir = resolve(stylesDir, "..");
|
|
31
|
+
|
|
32
|
+
function read(path: string): string {
|
|
33
|
+
return readFileSync(resolve(stylesDir, path), "utf8").replace(/\r\n/g, "\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compile(path: string): string {
|
|
37
|
+
return sass.compile(resolve(stylesDir, path), {
|
|
38
|
+
loadPaths: [srcDir, join(srcDir, "components"), join(srcDir, "styles"), join(srcDir, "styles", "theme")],
|
|
39
|
+
style: "compressed",
|
|
40
|
+
silenceDeprecations: ["import", "global-builtin", "legacy-js-api"],
|
|
41
|
+
}).css;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("selection policy sheet", () => {
|
|
45
|
+
it("loads second in the canonical composition (right after the reset)", () => {
|
|
46
|
+
const uses = read("index.scss")
|
|
47
|
+
.split("\n")
|
|
48
|
+
.filter((line) => line.startsWith('@use "./'))
|
|
49
|
+
.map((line) => line.match(/^@use "([^"]+)"/)?.[1]);
|
|
50
|
+
expect(uses[0]).toBe("./reset.scss");
|
|
51
|
+
expect(uses[1]).toBe("./selection.scss");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("denies selection at the root", () => {
|
|
55
|
+
const css = compile("selection.scss");
|
|
56
|
+
expect(css).toMatch(/html\s*\{[^}]*?-webkit-user-select:\s*none/);
|
|
57
|
+
expect(css).toMatch(/html\s*\{[^}]*?user-select:\s*none/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("whitelists exactly the editable controls and the content hook", () => {
|
|
61
|
+
const css = compile("selection.scss");
|
|
62
|
+
// Exact, comma-anchored selector list — substring matching would pass
|
|
63
|
+
// vacuously ("select" occurs inside every user-select declaration).
|
|
64
|
+
// Compressed output drops unnecessary attribute-quote characters.
|
|
65
|
+
const group = css.match(
|
|
66
|
+
/((?:input|textarea|select|\[[^\]]+\]|\.hk-selectable)[^{}]*)\{-webkit-user-select:\s*text;user-select:\s*text\}/,
|
|
67
|
+
);
|
|
68
|
+
expect(group).toBeTruthy();
|
|
69
|
+
expect(group![1].split(",").map((s) => s.trim())).toEqual([
|
|
70
|
+
"input",
|
|
71
|
+
"textarea",
|
|
72
|
+
"select",
|
|
73
|
+
"[contenteditable]:not([contenteditable=false i])",
|
|
74
|
+
".hk-selectable",
|
|
75
|
+
]);
|
|
76
|
+
// Element-type content conventions (pre/code/kbd/samp) stay OUT: a
|
|
77
|
+
// concrete value beats ancestor chrome denies, so a bare kbd/code
|
|
78
|
+
// whitelist punched through HkButton's denied shortcut chip and
|
|
79
|
+
// click-to-copy table cells (2026-10-29 review, S1).
|
|
80
|
+
expect(css).not.toMatch(/(^|[,\s])(pre|code|kbd|samp)([,\s]*\{|,)/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("re-enables the toast family message text (content, not chrome)", () => {
|
|
84
|
+
const css = compile("../components/HkToast.scss");
|
|
85
|
+
expect(css).toMatch(/\.hk-toast-message[^{]*\{[^}]*?-webkit-user-select:\s*text/);
|
|
86
|
+
// The shell stays denied — only the message subtree selects.
|
|
87
|
+
expect(css).toMatch(/\.hk-toast-item\s*\{[^}]*?user-select:\s*none/);
|
|
88
|
+
const cssBlocking = compile("../components/HkBlockingToast.scss");
|
|
89
|
+
expect(cssBlocking).toMatch(/\.hk-blocking-toast-title[^{]*\{[^}]*?user-select:\s*text/);
|
|
90
|
+
expect(cssBlocking).toMatch(/\.hk-blocking-toast-message[^{]*\{[^}]*?user-select:\s*text/);
|
|
91
|
+
const cssBox = compile("../components/HkMessageBox.scss");
|
|
92
|
+
expect(cssBox).toMatch(/\.hk-message-box-text[^{]*\{[^}]*?user-select:\s*text/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("re-enables the markdown body and the diff code, keeping the diff gutter denied", () => {
|
|
96
|
+
const css = compile("../components/HkMarkdownRenderer.scss");
|
|
97
|
+
expect(css).toMatch(/\.hk-markdown\s*\{[^}]*?user-select:\s*text/);
|
|
98
|
+
const cssDiff = compile("../components/HkDiffViewer.scss");
|
|
99
|
+
expect(cssDiff).toMatch(/\.hk-diff-viewer__code[^{]*\{[^}]*?user-select:\s*text/);
|
|
100
|
+
expect(cssDiff).toMatch(/\.hk-diff-viewer__no[^{]*\{[^}]*?user-select:\s*none/);
|
|
101
|
+
expect(cssDiff).toMatch(/\.hk-diff-viewer__sign[^{]*\{[^}]*?user-select:\s*none/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("re-enables json-tree rows inside the denied pane chrome", () => {
|
|
105
|
+
const css = compile("../components/HkJsonTree.scss");
|
|
106
|
+
expect(css).toMatch(/\.s-jt-row[^{]*\{[^}]*?user-select:\s*text/);
|
|
107
|
+
expect(css).toMatch(/\.s-tool-json-tree[^{]*\{[^}]*?user-select:\s*none/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("re-enables the alert body and the confirm-dialog message", () => {
|
|
111
|
+
// Same copy-this-error-detail content class as the toast/message-box
|
|
112
|
+
// surfaces (2026-10-29 review, S2: the parallel surfaces were missed
|
|
113
|
+
// in the first cut).
|
|
114
|
+
const cssAlert = compile("../components/HkAlert.scss");
|
|
115
|
+
expect(cssAlert).toMatch(/\.hk-alert-text[^{]*\{[^}]*?user-select:\s*text/);
|
|
116
|
+
const cssConfirm = compile("../components/HkConfirmDialog.scss");
|
|
117
|
+
expect(cssConfirm).toMatch(
|
|
118
|
+
/\.hk-confirm-dialog-message[^{]*\{[^}]*?user-select:\s*text/,
|
|
119
|
+
);
|
|
120
|
+
const cssLog = compile("../components/HkLogWindow.scss");
|
|
121
|
+
expect(cssLog).toMatch(/\.s-log-entry[^{]*\{[^}]*?user-select:\s*text/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("keeps the table contract two-sided under the deny-by-default root", () => {
|
|
125
|
+
// 2026-10-18 contract: copy-cells (default) denies cells with the
|
|
126
|
+
// click-to-copy cursor; the opt-out now needs an explicit mirror —
|
|
127
|
+
// absence of a deny no longer means selectable once the root denies.
|
|
128
|
+
const css = compile("../components/HkTable.scss");
|
|
129
|
+
expect(css).toMatch(/\.hk-table\[data-copy-cells\] \.hk-table-cell\{[^}]*?user-select:\s*none/);
|
|
130
|
+
expect(css).toMatch(
|
|
131
|
+
/\.hk-table:not\(\[data-copy-cells\]\) \.hk-table-cell\{[^}]*?user-select:\s*text/,
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
});
|