@celestia-island/hikari 0.55.189 → 0.55.190

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.189",
3
+ "version": "0.55.190",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -196,8 +196,8 @@
196
196
 
197
197
  &:hover,
198
198
  &:focus-visible {
199
- color: rgb(var(--color-danger, 240 70 80));
200
- background: rgb(var(--color-danger, 240 70 80) / 12%);
199
+ color: rgb(var(--color-error, 247 118 142));
200
+ background: rgb(var(--color-error, 247 118 142) / 12%);
201
201
  }
202
202
  }
203
203
 
@@ -8,5 +8,5 @@
8
8
  .s-auth-submit-error {
9
9
  margin: 0;
10
10
  font-size: var(--text-sm, 0.8125rem);
11
- color: rgb(var(--color-error, 240 70 80));
11
+ color: rgb(var(--color-error, 247 118 142));
12
12
  }
@@ -7,8 +7,8 @@
7
7
  align-items: center;
8
8
  justify-content: center;
9
9
  overflow: hidden;
10
- background-color: var(--hi-color-gray-200, #E5E7EB);
11
- color: var(--hi-color-gray-600, #4B5563);
10
+ background-color: rgb(var(--color-muted, 108 108 108) / 15%);
11
+ color: rgb(var(--color-text-secondary, 102 102 102));
12
12
  transition: all 0.2s ease;
13
13
 
14
14
  img {
@@ -80,17 +80,21 @@
80
80
  // dark-on-amber warning text for contrast).
81
81
  .hk-blocking-toast-info {
82
82
  background: var(--hi-color-info, #6abed6);
83
- color: #fff;
83
+ /* On-solid content: ride the canonical slot (HkToast family), keep the
84
+ * #fff fallback so themeless hosts render as before. */
85
+ color: var(--hi-color-text-on-solid, #fff);
84
86
  }
85
87
 
86
88
  .hk-blocking-toast-warning {
87
89
  background: var(--hi-color-warning, #eed677);
90
+ /* Deliberate DARK-on-amber: the warning band is light by design, and
91
+ * the on-solid slot is white — a white glyph here would be unreadable. */
88
92
  color: #1c1c1e;
89
93
  }
90
94
 
91
95
  .hk-blocking-toast-danger {
92
96
  background: var(--hi-color-danger, #f85149);
93
- color: #fff;
97
+ color: var(--hi-color-text-on-danger, #fff);
94
98
  }
95
99
 
96
100
  // ── Actions on a tinted surface ─────────────────────────────────────
@@ -17,7 +17,7 @@
17
17
  .hk-confirm-dialog-message {
18
18
  margin: 0;
19
19
  color: color-mix(in srgb, var(--hi-color-text-secondary) 85%, transparent);
20
- font-size: var(--hi-text-sm, 0.875rem);
20
+ font-size: var(--text-sm, 0.8125rem);
21
21
  line-height: 1.6;
22
22
  overflow-wrap: anywhere;
23
23
  }
@@ -73,7 +73,7 @@
73
73
  background: var(--hk-badge-bg);
74
74
  color: var(--hk-badge-text);
75
75
  border: 1px solid var(--hk-badge-border);
76
- font-size: var(--text-4xs, 0.625rem);
76
+ font-size: var(--text-2xs, 0.625rem);
77
77
  font-weight: 700;
78
78
  line-height: 1;
79
79
  font-variant-numeric: tabular-nums;
@@ -117,8 +117,14 @@
117
117
  border: 1px solid var(--border-subtle);
118
118
  border-radius: var(--radius-md);
119
119
  /* Reverse soft light — dark mode: white halo; light mode: soft black
120
- * shadow. Strength scales with --glow-scale (Style → Shadow). */
121
- box-shadow: var(--glow-dock);
120
+ * shadow. Strength scales with --glow-scale (Style → Shadow). The
121
+ * default derives the halo from the theme's own ink channel, which IS
122
+ * mode-reversed — before 2026-10-19 the bare var(--glow-dock) had no
123
+ * publisher and no fallback, so the dock halo silently never rendered. */
124
+ box-shadow: var(
125
+ --glow-dock,
126
+ 0 8px 32px rgb(var(--color-text, 30 30 30) / calc(8% * var(--glow-scale, 1)))
127
+ );
122
128
  }
123
129
 
124
130
  /* Vertical side docks (west / east edges) stack along the viewport's
@@ -0,0 +1,111 @@
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
+ * Side-drawer motion contract (2026-10-19 phone recording).
10
+ *
11
+ * The left/right drawer used to share the scrim's opacity fade: at
12
+ * halfway through the close the panel was still on screen but its right
13
+ * half was transparent, so the page behind showed through and the right
14
+ * rounded corners dissolved — it read as a width shrink. Side drawers
15
+ * must slide as an opaque surface (transform only); the scrim keeps its
16
+ * own fade, and top/bottom sheets keep the fade.
17
+ */
18
+
19
+ const here = dirname(fileURLToPath(import.meta.url));
20
+ const src = readFileSync(join(here, "HkDrawer.scss"), "utf-8");
21
+
22
+ // The side-drawer overrides sit after the generic blocks and before the
23
+ // reduced-motion block (which also lists the active classes with
24
+ // `transition: none`, so the full-file last occurrence would find that).
25
+ const sideStart = src.indexOf("/* Side drawers slide as an OPAQUE surface.");
26
+ const reduceStart = src.indexOf("@media (prefers-reduced-motion: reduce)");
27
+ const side = src.slice(sideStart, reduceStart);
28
+
29
+ /** Body of the LAST rule for a selector — the side overrides are declared
30
+ * after the generic four-direction blocks. */
31
+ function lastRule(selector: string, from = src): string {
32
+ const at = from.lastIndexOf(selector);
33
+ expect(at, `${selector} exists`).toBeGreaterThanOrEqual(0);
34
+ const open = from.indexOf("{", at);
35
+ const close = from.indexOf("}", open);
36
+ return from.slice(open + 1, close);
37
+ }
38
+
39
+ describe("HkDrawer side motion (opaque slide)", () => {
40
+ it("does not fade the left/right panel out", () => {
41
+ for (const sel of [
42
+ ".hk-drawer-left-enter-active",
43
+ ".hk-drawer-left-leave-active",
44
+ ".hk-drawer-right-enter-active",
45
+ ".hk-drawer-right-leave-active",
46
+ ]) {
47
+ const rule = lastRule(sel, side);
48
+ expect(rule, sel).toContain("transform");
49
+ expect(rule, sel).not.toContain("opacity");
50
+ }
51
+ for (const sel of [
52
+ ".hk-drawer-left-enter-from",
53
+ ".hk-drawer-left-leave-to",
54
+ ".hk-drawer-right-enter-from",
55
+ ".hk-drawer-right-leave-to",
56
+ ]) {
57
+ expect(lastRule(sel, side), sel).toContain("opacity: 1");
58
+ }
59
+ });
60
+
61
+ it("keeps the opaque side overrides after the generic fade rules (compiled order)", () => {
62
+ // The source slices above are anchored on a comment marker; this
63
+ // compiled assertion pins the cascade itself: the side override must
64
+ // come AFTER the generic four-direction block in the emitted CSS.
65
+ const fullCss = sass.compile(resolve(here, "HkDrawer.scss")).css;
66
+ // The reduced-motion block repeats the active selectors with
67
+ // `transition: none`; exclude it so the last occurrence is the side
68
+ // override under test.
69
+ const reduceAt = fullCss.lastIndexOf(
70
+ "@media (prefers-reduced-motion: reduce)",
71
+ );
72
+ const css = fullCss.slice(0, reduceAt > 0 ? reduceAt : fullCss.length);
73
+ const genericAt = css.indexOf(".hk-drawer-left-enter-active");
74
+ const overrideAt = css.lastIndexOf(".hk-drawer-left-enter-active");
75
+ expect(overrideAt).toBeGreaterThan(genericAt);
76
+ const overrideBody = css.slice(overrideAt, css.indexOf("}", overrideAt));
77
+ expect(overrideBody).toContain("transform");
78
+ expect(overrideBody).not.toContain("opacity");
79
+
80
+ const genericFromAt = css.indexOf(".hk-drawer-left-enter-from");
81
+ const sideFromAt = css.lastIndexOf(".hk-drawer-left-enter-from");
82
+ expect(sideFromAt).toBeGreaterThan(genericFromAt);
83
+ const sideFromBody = css.slice(sideFromAt, css.indexOf("}", sideFromAt));
84
+ expect(sideFromBody).toContain("opacity: 1");
85
+ });
86
+
87
+ it("keeps the sheet active opacity transition", () => {
88
+ // R3 mutation: deleting the generic active opacity transition kept the
89
+ // suite green. Guard that top/bottom sheets still animate opacity.
90
+ const generic = src.slice(0, sideStart);
91
+ expect(lastRule(".hk-drawer-top-enter-active", generic)).toContain("opacity");
92
+ expect(lastRule(".hk-drawer-bottom-leave-active", generic)).toContain(
93
+ "opacity",
94
+ );
95
+ });
96
+
97
+ it("keeps the off-screen side transforms", () => {
98
+ // The side override only resets opacity to 1; the generic from/to
99
+ // blocks must keep the translateX(±100%) that carries the panel in
100
+ // and out of the screen.
101
+ const left = src.slice(src.indexOf(".hk-drawer-left-enter-from"));
102
+ expect(left.slice(0, left.indexOf("}") + 1)).toContain("translateX(-100%)");
103
+ const right = src.slice(src.indexOf(".hk-drawer-right-enter-from"));
104
+ expect(right.slice(0, right.indexOf("}") + 1)).toContain("translateX(100%)");
105
+ });
106
+
107
+ it("keeps the fade for top/bottom sheets", () => {
108
+ expect(lastRule(".hk-drawer-top-enter-from")).toContain("opacity: 0");
109
+ expect(lastRule(".hk-drawer-bottom-enter-from")).toContain("opacity: 0");
110
+ });
111
+ });
@@ -123,6 +123,30 @@
123
123
  transform: translateY(100%);
124
124
  }
125
125
 
126
+ /* Side drawers slide as an OPAQUE surface. The panel used to share the
127
+ * scrim's fade (opacity: 0 in the enter/leave states); on a phone the
128
+ * drawer was still partly on screen when its right half went transparent
129
+ * — the page behind showed through and the right rounded corners
130
+ * dissolved, which reads as a width shrink (screen recording
131
+ * 2026-10-19). The scrim keeps its own fade; the panel only translates.
132
+ * Top/bottom sheets keep the fade above. */
133
+ .hk-drawer-left-enter-active,
134
+ .hk-drawer-right-enter-active {
135
+ transition: transform var(--duration-normal, 0.3s) cubic-bezier(0.16, 1, 0.3, 1);
136
+ }
137
+
138
+ .hk-drawer-left-leave-active,
139
+ .hk-drawer-right-leave-active {
140
+ transition: transform var(--duration-normal, 0.3s) cubic-bezier(0.4, 0, 1, 1);
141
+ }
142
+
143
+ .hk-drawer-left-enter-from,
144
+ .hk-drawer-left-leave-to,
145
+ .hk-drawer-right-enter-from,
146
+ .hk-drawer-right-leave-to {
147
+ opacity: 1;
148
+ }
149
+
126
150
  .hk-drawer-header {
127
151
  display: flex;
128
152
  align-items: center;
@@ -104,7 +104,7 @@
104
104
  (var(--hk-icon-group-item-size) + var(--hk-icon-group-slider-gap))
105
105
  )
106
106
  );
107
- transition: transform var(--duration, 0.18s) ease;
107
+ transition: transform var(--duration-short, 0.15s) ease;
108
108
  pointer-events: none;
109
109
  }
110
110
 
@@ -132,7 +132,7 @@
132
132
  padding: 0 var(--space-4, 4px);
133
133
  border-radius: var(--radius-full, 9999px);
134
134
  background: rgb(var(--color-error, 247 118 142));
135
- color: #fff;
135
+ color: var(--hi-color-text-on-danger, #fff);
136
136
  font-size: var(--text-2xs, 10px);
137
137
  font-weight: 700;
138
138
  line-height: 1;
@@ -20,7 +20,7 @@
20
20
 
21
21
  [data-tone="warning"] &,
22
22
  [data-tone="danger"] & {
23
- color: rgb(var(--color-danger, 240 70 80));
23
+ color: rgb(var(--color-error, 247 118 142));
24
24
  }
25
25
  }
26
26
 
@@ -115,8 +115,10 @@
115
115
  }
116
116
 
117
117
  .hk-toast-loading-spinner {
118
- border: 2px solid rgba(255, 255, 255, 0.3);
119
- border-top-color: #fff;
118
+ /* The spinner rides the toast's SOLID band, so its glyph is on-solid
119
+ * content: ride the on-solid slot (white by default, theme-following) */
120
+ border: 2px solid rgb(var(--color-on-solid-text, 255 255 255) / 30%);
121
+ border-top-color: rgb(var(--color-on-solid-text, 255 255 255));
120
122
  border-radius: 50%;
121
123
  animation: hk-toast-spin 0.7s linear infinite;
122
124
  box-sizing: border-box;
@@ -168,14 +168,14 @@
168
168
  }
169
169
 
170
170
  &[data-danger] {
171
- color: rgb(var(--color-danger, 240 70 80) / 90%);
171
+ color: rgb(var(--color-error, 247 118 142) / 90%);
172
172
 
173
173
  /* Same checked-row guard as the base hover above: a checked danger
174
174
  * row keeps its checked face instead of flashing the hover wash. */
175
175
  &:hover:not(:disabled):not([data-disabled]):not([data-active]):not([data-checked]) {
176
- background: rgb(var(--color-danger, 240 70 80) / 10%);
177
- border-color: rgb(var(--color-danger, 240 70 80) / 25%);
178
- color: rgb(var(--color-danger, 240 70 80));
176
+ background: rgb(var(--color-error, 247 118 142) / 10%);
177
+ border-color: rgb(var(--color-error, 247 118 142) / 25%);
178
+ color: rgb(var(--color-error, 247 118 142));
179
179
  }
180
180
  }
181
181
  }
@@ -1179,7 +1179,7 @@
1179
1179
 
1180
1180
  /* ── Arona app utility classes ── */
1181
1181
  .a-text-primary {
1182
- color: var(--color-text-primary);
1182
+ color: var(--color-text);
1183
1183
  }
1184
1184
 
1185
1185
  .a-text-secondary {
@@ -1207,11 +1207,11 @@
1207
1207
  }
1208
1208
 
1209
1209
  .a-bg-secondary {
1210
- background: var(--color-bg-secondary, #18181b);
1210
+ background: rgb(var(--color-background, 214 236 240));
1211
1211
  }
1212
1212
 
1213
1213
  .a-bg-tertiary {
1214
- background: var(--color-bg-tertiary, #27272a);
1214
+ background: rgb(var(--color-surface, 240 244 248));
1215
1215
  }
1216
1216
 
1217
1217
  .a-latency-text {
@@ -33,18 +33,20 @@ import { describe, expect, it } from "vitest";
33
33
  * is still the regression this guard exists to catch.
34
34
  *
35
35
  * Slots with no seed are listed in UNSEEDED rather than skipped silently:
36
- * `--color-bg-secondary` / `--color-bg-tertiary` are retired spellings
37
- * (their `#18181b` / `#27272a` fallbacks always win) and `--color-danger` is
38
- * defined nowhere, so there is no seed value to compare against — recorded
39
- * debt, not a passing check.
36
+ * The retired spellings that used to live in UNSEEDED —
37
+ * `--color-bg-secondary` / `--color-bg-tertiary` / `--color-danger` — were
38
+ * paid on 2026-10-19 (their last usages re-pointed to the seeded channels;
39
+ * see ghostChannels.contract.test.ts, which now forbids them outright).
40
40
  */
41
41
 
42
42
  const stylesDir = resolve(dirname(fileURLToPath(import.meta.url)));
43
43
  const packageDir = resolve(stylesDir, "..", "..");
44
44
  const legacyDir = resolve(packageDir, "..", "theme", "styles");
45
45
 
46
- /** Slots this guard cannot hold to the seed, with the reason above. */
47
- const UNSEEDED = ["--color-bg-secondary", "--color-bg-tertiary", "--color-danger"];
46
+ /** Slots this guard cannot hold to the seed. The 2026-10-19 re-pointing
47
+ * paid the debt this list used to record; it stays as an empty-seeded
48
+ * invariant — a NEW unseeded slot must be justified here, not smuggled in. */
49
+ const UNSEEDED: string[] = [];
48
50
 
49
51
  function read(path: string): string {
50
52
  return readFileSync(path, "utf8");
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Retired color spellings — source contract (2026-10-19 color audit).
3
+ *
4
+ * The audit swept every var() reference across the shipped sheets and the
5
+ * components crate against the two real emitters (vue's tokensToCSSVars
6
+ * channels + the Rust css.rs alias emissions) and found a family of ghost
7
+ * spellings: names that LOOK like theme tokens, were never emitted by
8
+ * anything, and therefore always resolved to their (often one-mode-biased)
9
+ * literal fallbacks — or, with no fallback, silently killed the face
10
+ * (hover washes → transparent, glows → none). Each entry below was
11
+ * re-pointed to its canonical channel in the same change that added this
12
+ * guard; this file exists so none of them can quietly come back.
13
+ *
14
+ * Scope: shipped sheets and crate style strings only (tests, demos and
15
+ * comments are stripped). Not a general resolver — a name lands here only
16
+ * when it is permanently retired, never to be emitted; names that ARE
17
+ * emitted are guarded by their own contracts (see
18
+ * theme::css::interaction_alias_tests in the crate).
19
+ */
20
+ import { readFileSync, readdirSync, statSync } from "node:fs";
21
+ import { dirname, join, resolve } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ import { describe, expect, it } from "vitest";
25
+
26
+ const here = dirname(fileURLToPath(import.meta.url));
27
+ const vueDir = resolve(here, "..", "..");
28
+ const repoDir = resolve(vueDir, "..", "..");
29
+ const crateSrc = join(repoDir, "packages", "components", "src");
30
+ const themeStyles = join(repoDir, "packages", "theme", "styles");
31
+
32
+ /** Permanently retired spellings — a var() reference to any of these is a
33
+ * regression. (Names that are merely host-tunable knobs live elsewhere;
34
+ * this list is only for spellings the emitters must never grow.) */
35
+ const RETIRED = [
36
+ // wrong channel: the danger channel is --color-error (seed 247 118 142);
37
+ // --color-danger was emitted by nothing and its 240 70 80 fallback was a
38
+ // second, drifting palette decision (the 2026-09-16 bug class again).
39
+ "--color-danger",
40
+ // retired spellings: the audit's admin-surface backgrounds ride
41
+ // --color-background / --color-surface now.
42
+ "--color-bg-secondary",
43
+ "--color-bg-tertiary",
44
+ // the vue-channel namespace has no --color-text-primary slot (the primary
45
+ // ink channel is --color-text); the admin .a-text-primary utility rode
46
+ // this phantom spelling with no fallback and inherited its parent ink.
47
+ "--color-text-primary",
48
+ // the alias family spells the primary ink slot --hi-color-text-primary;
49
+ // plain --hi-color-text never existed, and its #333 fallback was unreadable
50
+ // on dark surfaces (the exact bug class of the 2026-10-18 breadcrumb fix).
51
+ "--hi-color-text",
52
+ // the crate emits --hi-border-light; the --hi-color-border-light spelling
53
+ // was a ghost whose #f3f4f6 fallback was light-biased.
54
+ "--hi-color-border-light",
55
+ // spelling typo that was never emitted anywhere; the canonical scale token
56
+ // is --text-sm.
57
+ "--hi-text-sm",
58
+ // invented family prefix nothing emits; the values were already inline
59
+ // (unwrapped to the same literals).
60
+ "--hikari-duration-fast",
61
+ "--hikari-duration-normal",
62
+ "--hikari-ease-smooth",
63
+ ];
64
+
65
+ function stripComments(source: string): string {
66
+ return source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:])\/\/[^\n]*/g, "$1");
67
+ }
68
+
69
+ /** Boundary-aware retired-spelling probe: `var(--name` NOT followed by
70
+ * another name character, so --hi-color-text does not collide with the
71
+ * legitimate --hi-color-text-primary/-secondary emissions. */
72
+ function referencesRetired(source: string, name: string): boolean {
73
+ const re = new RegExp(`var\\(\\s*${name}(?![a-zA-Z-])`);
74
+ return re.test(source);
75
+ }
76
+
77
+ function* sheetsUnder(dir: string): Generator<string> {
78
+ for (const entry of readdirSync(dir)) {
79
+ const p = join(dir, entry);
80
+ if (statSync(p).isDirectory()) {
81
+ if (entry === "node_modules" || entry === "target" || entry.includes(".test.")) continue;
82
+ yield* sheetsUnder(p);
83
+ } else if (entry.endsWith(".scss") || entry.endsWith(".css")) {
84
+ yield p;
85
+ } else if (entry.endsWith(".rs") && !entry.endsWith(".test.rs")) {
86
+ yield p;
87
+ }
88
+ }
89
+ }
90
+
91
+ describe("retired color spellings", () => {
92
+ it("never appears in a shipped vue sheet", () => {
93
+ for (const file of sheetsUnder(join(vueDir, "src", "styles"))) {
94
+ const text = stripComments(readFileSync(file, "utf8"));
95
+ for (const name of RETIRED) {
96
+ expect(
97
+ referencesRetired(text, name),
98
+ `${file.replace(vueDir, "packages/vue")} references retired spelling ${name}`,
99
+ ).toBe(false);
100
+ }
101
+ }
102
+ });
103
+
104
+ it("never appears in a shipped component scss", () => {
105
+ for (const file of sheetsUnder(join(vueDir, "src", "components"))) {
106
+ const text = stripComments(readFileSync(file, "utf8"));
107
+ for (const name of RETIRED) {
108
+ expect(
109
+ referencesRetired(text, name),
110
+ `${file.replace(vueDir, "packages/vue")} references retired spelling ${name}`,
111
+ ).toBe(false);
112
+ }
113
+ }
114
+ });
115
+
116
+ it("never appears in the components crate styles or rs style strings", () => {
117
+ for (const root of [crateSrc, themeStyles]) {
118
+ for (const file of sheetsUnder(root)) {
119
+ const text = stripComments(readFileSync(file, "utf8"));
120
+ for (const name of RETIRED) {
121
+ expect(
122
+ referencesRetired(text, name),
123
+ `${file.replace(repoDir, "packages")} references retired spelling ${name}`,
124
+ ).toBe(false);
125
+ }
126
+ }
127
+ }
128
+ });
129
+
130
+ it("keeps its own denylist honest (positive control)", () => {
131
+ // The sweep above must be able to see a violation: feed it a synthetic
132
+ // line carrying a retired spelling and assert the detector matches.
133
+ const probe = "color: var(--color-danger, 240 70 80);";
134
+ expect(referencesRetired(probe, "--color-danger")).toBe(true);
135
+ // and the boundary holds: a legit longer name must NOT trip the probe
136
+ expect(referencesRetired("color: var(--color-dangerous);", "--color-danger")).toBe(false);
137
+ });
138
+ });