@celestia-island/hikari 0.55.328 → 0.55.330
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/HkBreadcrumb.scss +3 -3
- package/src/components/HkTable.sections.test.tsx +25 -0
- package/src/components/HkTable.tsx +8 -0
- package/src/components/HkTerminal.scss +11 -3
- package/src/components/HkTerminal.test.tsx +40 -0
- package/src/components/HkTerminal.tsx +22 -9
- package/src/components/_hikari-vars.scss +1 -0
- package/src/styles/tokenInventory.test.ts +32 -0
package/package.json
CHANGED
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
// Padding
|
|
85
85
|
padding: var(--space-4) var(--space-8);
|
|
86
86
|
margin: calc(-1 * var(--space-4)) calc(-1 * var(--space-8));
|
|
87
|
-
border-radius:
|
|
87
|
+
border-radius: var(--radius-sm, 4px);
|
|
88
88
|
|
|
89
89
|
// Display
|
|
90
90
|
display: inline-flex;
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
z-index: var(--hk-z-header);
|
|
217
217
|
|
|
218
218
|
// Appearance
|
|
219
|
-
border-radius:
|
|
219
|
+
border-radius: var(--radius-lg, 12px);
|
|
220
220
|
border: 1px solid var(--hi-color-border);
|
|
221
221
|
background: var(--hi-surface);
|
|
222
222
|
@include backdrop-filter(blur(4px));
|
|
@@ -321,7 +321,7 @@
|
|
|
321
321
|
.hk-breadcrumb-item {
|
|
322
322
|
background: var(--hi-card-bg);
|
|
323
323
|
padding: 0.375rem 0.75rem;
|
|
324
|
-
border-radius:
|
|
324
|
+
border-radius: var(--radius-sm, 4px);
|
|
325
325
|
|
|
326
326
|
&:not(:last-child):hover {
|
|
327
327
|
background: var(--hi-primary);
|
|
@@ -270,6 +270,31 @@ describe("HkTable sections", () => {
|
|
|
270
270
|
expect(empty!.getAttribute("colspan")).toBe(String(COLS.length));
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
+
it("names an empty-title header cell via headerLabel without rendering text", async () => {
|
|
274
|
+
// Utility columns (row actions) intentionally carry `title: ""` —
|
|
275
|
+
// but an empty `<th>` is an UNNAMED one for assistive tech. The
|
|
276
|
+
// headerLabel contract: aria-label published on the th, visible
|
|
277
|
+
// header stays empty, and columns without the label render no
|
|
278
|
+
// aria-label at all (no behavior change for existing tables).
|
|
279
|
+
const columns = [
|
|
280
|
+
{ key: "name", title: "Name" },
|
|
281
|
+
{ key: "actions", title: "", headerLabel: "Actions", align: "right" as const },
|
|
282
|
+
];
|
|
283
|
+
const sections: HkTableSection[] = [
|
|
284
|
+
{ key: "panel-a", label: "Panel A", rows: [{ name: "alpha", actions: "…" }] },
|
|
285
|
+
];
|
|
286
|
+
const c = mount(h(HTable, { columns, sections }));
|
|
287
|
+
|
|
288
|
+
const heads = Array.from(c.querySelectorAll("thead th"));
|
|
289
|
+
const named = heads.find((th) => th.getAttribute("aria-label") === "Actions");
|
|
290
|
+
expect(named, "the actions th carries the accessible name").toBeTruthy();
|
|
291
|
+
const plain = heads.find((th) => th.getAttribute("aria-label") === null) as HTMLElement;
|
|
292
|
+
expect(plain.textContent).toContain("Name");
|
|
293
|
+
// visible header of the named column stays EMPTY — the label is
|
|
294
|
+
// assistive-only
|
|
295
|
+
expect(named!.textContent!.trim()).toBe("");
|
|
296
|
+
});
|
|
297
|
+
|
|
273
298
|
it("sorts inside each section and keeps the section order", async () => {
|
|
274
299
|
const columns = [{ key: "name", title: "Name", sortable: true }];
|
|
275
300
|
const sections: HkTableSection[] = [
|
|
@@ -18,6 +18,13 @@ import "./HkTable.scss";
|
|
|
18
18
|
interface Column {
|
|
19
19
|
key: string;
|
|
20
20
|
title: string;
|
|
21
|
+
/** Accessible name for the header cell, WITHOUT visible text. Utility
|
|
22
|
+
* columns (row actions, ⋯ menus) intentionally render an empty
|
|
23
|
+
* `title` — but an empty `<th>` is also an unnamed one for screen
|
|
24
|
+
* readers. Set `headerLabel` to publish `aria-label` on that `<th>`
|
|
25
|
+
* while the visible header stays empty; it never renders text and
|
|
26
|
+
* never participates in sorting. */
|
|
27
|
+
headerLabel?: string;
|
|
21
28
|
width?: string;
|
|
22
29
|
sortable?: boolean;
|
|
23
30
|
align?: "left" | "center" | "right";
|
|
@@ -1161,6 +1168,7 @@ export default defineComponent({
|
|
|
1161
1168
|
return (
|
|
1162
1169
|
<th
|
|
1163
1170
|
key={col.key}
|
|
1171
|
+
aria-label={col.headerLabel}
|
|
1164
1172
|
class={[
|
|
1165
1173
|
"hk-table-header-cell",
|
|
1166
1174
|
col.align ? `hk-text-${col.align}` : "",
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
// Use theme variables with namespace to avoid conflicts
|
|
2
2
|
@use "./hikari-vars" as vars;
|
|
3
3
|
|
|
4
|
+
/* The terminal is a solid dark canvas by convention (like a code block):
|
|
5
|
+
the surface IS its own frame — no border, one radius tier. The canvas
|
|
6
|
+
stays dark in every app theme (a light-mode terminal reads as a web
|
|
7
|
+
pane, not a terminal); hosts may retune it via --color-terminal-bg /
|
|
8
|
+
--color-terminal-fg without touching the component. Nested hosts must
|
|
9
|
+
NOT wrap it in their own framed box — that stacking is what produced
|
|
10
|
+
the round-outer/square-inner double frame reported 2026-11-09. */
|
|
4
11
|
.hk-terminal {
|
|
5
12
|
position: relative;
|
|
6
13
|
display: block;
|
|
7
14
|
width: 100%;
|
|
8
15
|
height: 100%;
|
|
9
16
|
min-height: 8rem;
|
|
10
|
-
padding: 0.
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
padding: 0.6rem 0.75rem;
|
|
18
|
+
/* Terminal channels seed as r g b TRIPLETS (a hex seed passes the TSX
|
|
19
|
+
layer but invalidates rgb(var()) here — computed value drops out). */
|
|
20
|
+
background: rgb(var(--color-terminal-bg, 20 22 26));
|
|
13
21
|
border-radius: vars.$hikari-radius-fine-md;
|
|
14
22
|
box-sizing: border-box;
|
|
15
23
|
overflow: hidden;
|
|
@@ -159,6 +159,46 @@ afterEach(() => {
|
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
describe("HkTerminal", () => {
|
|
162
|
+
it("derives the canvas from the TERMINAL channels, not the app theme", async () => {
|
|
163
|
+
// The canvas is a terminal and stays dark in every app theme: the
|
|
164
|
+
// 2026-11-09 double-frame report was a light theme-background panel
|
|
165
|
+
// inside chest's black rounded host. The palette must read
|
|
166
|
+
// --color-terminal-bg/-fg (fixed dark defaults) and must NOT read
|
|
167
|
+
// --color-background/--color-text.
|
|
168
|
+
const theme = __testables.deriveXtermTheme(document.createElement("div")) as Record<
|
|
169
|
+
string,
|
|
170
|
+
string
|
|
171
|
+
>;
|
|
172
|
+
expect(theme.background).toBe("#14161a");
|
|
173
|
+
expect(theme.foreground).toBe("#d6dde6");
|
|
174
|
+
expect(theme.cursorAccent).toBe("#14161a");
|
|
175
|
+
expect(theme.cursor).toBe("#7aa2f7");
|
|
176
|
+
expect(theme.selectionBackground).toBe("#3b435688");
|
|
177
|
+
// Host-side retune wins over the defaults (happy-dom's computed
|
|
178
|
+
// style doesn't resolve inline custom properties, so the retune arm
|
|
179
|
+
// is exercised through a stubbed window computed-style).
|
|
180
|
+
const fakeHost = { tag: "stub" } as unknown as HTMLElement;
|
|
181
|
+
const resolver = (el: Element) =>
|
|
182
|
+
el === fakeHost
|
|
183
|
+
? ({
|
|
184
|
+
getPropertyValue: (name: string) =>
|
|
185
|
+
name === "--color-terminal-bg"
|
|
186
|
+
? "10 12 16"
|
|
187
|
+
: name === "--color-terminal-fg"
|
|
188
|
+
? "#c0c8d4"
|
|
189
|
+
: name === "--color-terminal-selection"
|
|
190
|
+
? "#2e3a4e"
|
|
191
|
+
: "",
|
|
192
|
+
} as CSSStyleDeclaration)
|
|
193
|
+
: window.getComputedStyle(el);
|
|
194
|
+
const spy = vi.spyOn(window, "getComputedStyle").mockImplementation(resolver);
|
|
195
|
+
const tuned = __testables.deriveXtermTheme(fakeHost) as Record<string, string>;
|
|
196
|
+
spy.mockRestore();
|
|
197
|
+
expect(tuned.background).toBe("#0a0c10");
|
|
198
|
+
expect(tuned.foreground).toBe("#c0c8d4");
|
|
199
|
+
expect(tuned.selectionBackground).toBe("#2e3a4e88");
|
|
200
|
+
});
|
|
201
|
+
|
|
162
202
|
it("passes the typography props into the Terminal options", async () => {
|
|
163
203
|
mountTerminal({ fontSize: 15, fontFamily: "MyMono", scrollback: 500, cursorBlink: false });
|
|
164
204
|
await nextTick();
|
|
@@ -47,10 +47,16 @@ import "./HkTerminal.scss";
|
|
|
47
47
|
* - `write(data: string | Uint8Array)` — binary-safe feed.
|
|
48
48
|
* - `writeln`, `clear`, `reset`, `fit`, `focus`, `dimensions`.
|
|
49
49
|
*
|
|
50
|
-
* Theming: the
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
50
|
+
* Theming: the canvas is a terminal and stays DARK in every app theme —
|
|
51
|
+
* background/foreground default to the fixed terminal palette and may be
|
|
52
|
+
* retuned per host via `--color-terminal-bg` / `--color-terminal-fg` /
|
|
53
|
+
* `--color-terminal-selection`; the cursor accent still derives from the
|
|
54
|
+
* hikari channel (`--color-primary`). The palette re-derives
|
|
55
|
+
* when the document element's class or style changes (the light/dark
|
|
56
|
+
* switch mechanism consumers use). Surface grammar: the component owns
|
|
57
|
+
* border+radius+background (borderless dark canvas, one radius tier) —
|
|
58
|
+
* consumers size the host box and must not wrap it in their own framed
|
|
59
|
+
* surface.
|
|
54
60
|
*/
|
|
55
61
|
|
|
56
62
|
/** Resolve an `r g b` triplet channel (with fallback) to `#rrggbb`. */
|
|
@@ -72,11 +78,18 @@ function channelToHex(host: HTMLElement, name: string, fallback: string): string
|
|
|
72
78
|
|
|
73
79
|
function deriveXtermTheme(host: HTMLElement): ITheme {
|
|
74
80
|
return {
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
// The canvas is a terminal: fixed dark defaults, NOT the app theme's
|
|
82
|
+
// background/text (those flip with light/dark and would render a
|
|
83
|
+
// light terminal). Selection too — a theme-coupled pale wash on the
|
|
84
|
+
// dark canvas collapses selected-text contrast to ~3:1 in light
|
|
85
|
+
// themes (R2 of the 2026-11-09 chain). Hosts retune via
|
|
86
|
+
// --color-terminal-bg/-fg/-selection.
|
|
87
|
+
background: channelToHex(host, "--color-terminal-bg", "#14161a"),
|
|
88
|
+
foreground: channelToHex(host, "--color-terminal-fg", "#d6dde6"),
|
|
77
89
|
cursor: channelToHex(host, "--color-primary", "#7aa2f7"),
|
|
78
|
-
cursorAccent: channelToHex(host, "--color-
|
|
79
|
-
selectionBackground:
|
|
90
|
+
cursorAccent: channelToHex(host, "--color-terminal-bg", "#14161a"),
|
|
91
|
+
selectionBackground:
|
|
92
|
+
channelToHex(host, "--color-terminal-selection", "#3b4356") + "88",
|
|
80
93
|
};
|
|
81
94
|
}
|
|
82
95
|
|
|
@@ -238,4 +251,4 @@ export default defineComponent({
|
|
|
238
251
|
});
|
|
239
252
|
|
|
240
253
|
/** Test seam: pure helpers (no DOM dependency beyond getComputedStyle). */
|
|
241
|
-
export const __testables = { channelToHex };
|
|
254
|
+
export const __testables = { channelToHex, deriveXtermTheme };
|
|
@@ -171,6 +171,38 @@ describe("theme token inventory", () => {
|
|
|
171
171
|
expect(unexpected, "new multi-defined scale tokens").toEqual([]);
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
it("scale seed defines the full radius ladder with canonical values", () => {
|
|
175
|
+
// Existence + value pin for the runtime radius scale. Before the
|
|
176
|
+
// 2026-11 radius wave, deleting a `--radius-*` definition stayed
|
|
177
|
+
// green everywhere (no read face checked for presence); this test is
|
|
178
|
+
// that read face. Values mirror hikari scale.scss — the same ladder
|
|
179
|
+
// chest mirrors into its own `:root`, so a value change here is a
|
|
180
|
+
// cross-repo contract change and must ride a dedicated wave.
|
|
181
|
+
const defs = harvestDefinitions(seedFiles);
|
|
182
|
+
const ladder: Array<[string, string]> = [
|
|
183
|
+
["--radius-xs", "2px"],
|
|
184
|
+
["--radius-sm", "4px"],
|
|
185
|
+
["--radius-md", "8px"],
|
|
186
|
+
["--radius-lg", "12px"],
|
|
187
|
+
["--radius-xl", "16px"],
|
|
188
|
+
["--radius-full", "9999px"],
|
|
189
|
+
];
|
|
190
|
+
for (const [name, value] of ladder) {
|
|
191
|
+
const sites = defs.get(name);
|
|
192
|
+
expect(sites, `${name} missing from theme/scale.scss`).toBeDefined();
|
|
193
|
+
// Every site must carry the canonical value — not just the first.
|
|
194
|
+
// A same-file duplicate would slip past the multi-file guard above
|
|
195
|
+
// (files.size stays 1) and win the CSS cascade as the LAST
|
|
196
|
+
// definition; iterating all sites closes that bypass.
|
|
197
|
+
for (const site of sites!) {
|
|
198
|
+
expect(
|
|
199
|
+
site.value,
|
|
200
|
+
`${name} non-canonical site in ${site.file}`,
|
|
201
|
+
).toBe(value);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
174
206
|
it("component --hi-color-* fallbacks stay on the canonical literals", () => {
|
|
175
207
|
// The standalone-render contract: when a host loads nothing, a
|
|
176
208
|
// component renders through these literals. They are DERIVED FROM THE
|