@celestia-island/hikari 0.43.15 → 0.43.17

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.43.15",
3
+ "version": "0.43.17",
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",
@@ -103,20 +103,6 @@ registerCssAnimation("s-voice-wave-bounce", { infinite: true });
103
103
  // components/HkPlaceholderMarquee.scss — overflowing placeholder sweep
104
104
  // (loops forever; pure CSS, loop geometry on inline custom properties).
105
105
  registerCssAnimation("hk-placeholder-marquee-scroll", { infinite: true });
106
- // styles/theme/mixins.scss — ambient/glow pulses (loop forever) and the
107
- // one-shot entrance/exit grammar, vendored byte-identical from
108
- // packages/theme. #353 brought the file under src/ without registering
109
- // its keyframes — the parity test caught the gap 6 days later.
110
- registerCssAnimation("pulse", { infinite: true });
111
- registerCssAnimation("ambient-move", { infinite: true });
112
- registerCssAnimation("pulse-glow-animation", { infinite: true });
113
- registerCssAnimation("expand-horizontal");
114
- registerCssAnimation("collapse-vertical");
115
- registerCssAnimation("slide-in-left");
116
- registerCssAnimation("slide-in-right");
117
- registerCssAnimation("slide-in-top");
118
- registerCssAnimation("slide-in-bottom");
119
- registerCssAnimation("fade-in-scale");
120
- registerCssAnimation("fade-out-scale");
121
- registerCssAnimation("blur-in");
122
- registerCssAnimation("bounce-in");
106
+ // The styles/theme/mixins.scss ambient/glow/entrance keyframes were
107
+ // retired with the legacy Gen-1 sheet vendoring (hikari #476) — their
108
+ // registrar entries went with them.
@@ -97,20 +97,25 @@ describe("HkAuthMethodList", () => {
97
97
  await nextTick();
98
98
  const wrapper = c.querySelector<HTMLElement>(".s-auth-methods-list")!;
99
99
  const spans = [...wrapper.querySelectorAll<HTMLElement>(".s-auth-methods-label")];
100
- Object.defineProperty(spans[0]!, "getBoundingClientRect", { configurable: true, value: () => ({ width: 58 }) });
101
- Object.defineProperty(spans[1]!, "getBoundingClientRect", { configurable: true, value: () => ({ width: 61.4 }) });
102
- // The list is drawn twice the size it is laid out at.
103
- Object.defineProperty(wrapper, "getBoundingClientRect", {
104
- configurable: true,
105
- value: () => ({ left: 0, top: 0, right: 400, bottom: 200, width: 400, height: 200, x: 0, y: 0 }) as DOMRect,
106
- });
107
- Object.defineProperty(wrapper, "offsetWidth", { configurable: true, get: () => 200 });
108
- Object.defineProperty(wrapper, "offsetHeight", { configurable: true, get: () => 100 });
100
+ // The scale has to come from a LABEL: the list is `display: contents` and
101
+ // generates no box at all (Blink reports offsetWidth 0 and a 0x0 rect), so
102
+ // stubbing a box on the list would only ever be green by accident.
103
+ for (const [i, drawn] of [58, 61.4].entries()) {
104
+ Object.defineProperty(spans[i]!, "getBoundingClientRect", {
105
+ configurable: true,
106
+ value: () => ({ left: 0, top: 0, right: drawn * 2, bottom: 40, width: drawn * 2, height: 40, x: 0, y: 0 }) as DOMRect,
107
+ });
108
+ // Laid out at half: the label is drawn twice the size it is laid out at.
109
+ Object.defineProperty(spans[i]!, "offsetWidth", { configurable: true, get: () => drawn });
110
+ Object.defineProperty(spans[i]!, "offsetHeight", { configurable: true, get: () => 20 });
111
+ }
109
112
  const instance = (wrapper as unknown as { __vueParentComponent?: { exposed?: Record<string, unknown> } })
110
113
  .__vueParentComponent?.exposed;
111
114
  (instance!.measure as () => void)();
112
- // 61.4 drawn are 30.7 of the column's own: ceil 31, plus the 1px headroom.
113
- expect(wrapper.style.getPropertyValue("--auth-methods-label-width")).toBe("32px");
115
+ // 122.8 drawn are 61.4 of the column's own: ceil 62, plus the 1px headroom.
116
+ // Reading the scale off the LIST instead — the shape this shipped with —
117
+ // yields 1 for a `display: contents` element and publishes the drawn 124px.
118
+ expect(wrapper.style.getPropertyValue("--auth-methods-label-width")).toBe("63px");
114
119
  });
115
120
 
116
121
  it("leaves the column fallback intact when no text metrics are available", async () => {
@@ -86,12 +86,21 @@ export default defineComponent({
86
86
  // published as a CSS length the column is laid out with: in a scaled or
87
87
  // zoomed root the two differ by the scale of the space, so the column
88
88
  // would come out that many times too wide (see `layoutGeometry`).
89
- const space = drawnScale(root);
90
- const factor = root.offsetWidth > 0 ? space.x : root.offsetHeight > 0 ? space.y : 1;
89
+ //
90
+ // The scale is read from the LABEL — the element the box was measured
91
+ // on. Reading it from the list itself cannot work: the list is
92
+ // `display: contents`, so it generates no box at all (measured in Blink:
93
+ // `offsetWidth` 0, a 0x0 rect, computed width `auto`), and every reader
94
+ // of a box reports nothing to divide by.
91
95
  for (const label of root.querySelectorAll<HTMLElement>(".s-auth-methods-label")) {
92
96
  const previous = label.style.width;
93
97
  label.style.width = "max-content";
94
- widest = Math.max(widest, label.getBoundingClientRect().width / (factor > 0 ? factor : 1));
98
+ const drawn = label.getBoundingClientRect().width;
99
+ const scale = drawnScale(label);
100
+ const factor = Number.isFinite(scale.x) && scale.x > 0 ? scale.x : 1;
101
+ // `offsetWidth` is the laid-out fallback — already in the units the
102
+ // published length is used in, so it needs no division.
103
+ widest = Math.max(widest, factor > 0 ? drawn / factor : label.offsetWidth);
95
104
  label.style.width = previous;
96
105
  }
97
106
  // Guard: only publish when real text metrics were observed, so a
@@ -110,6 +110,13 @@ export function layoutOffset(el: HTMLElement, frame?: HTMLElement | null): Layou
110
110
  * while the drawn-box equivalent (`rect.top - frameRect.top + frame.scrollTop`)
111
111
  * is `k` times too large at zoom `k`.
112
112
  *
113
+ * The frame's own border is excluded exactly when the walk passed THROUGH it
114
+ * (`through`): a positioned frame measures its children from its padding edge,
115
+ * so the difference does too, while a frame that is not in the chain (a
116
+ * `position: static` scroller) is measured from its border box. The result is
117
+ * the value `scrollTop`/`scrollLeft` want in both cases — those are offsets
118
+ * into the scrollable area, whose origin is the padding edge.
119
+ *
113
120
  * `y` (and `x`) are `NaN` when the chain cannot be added up or when `frame` is
114
121
  * not an ancestor of `el` — the difference is meaningless then, and callers
115
122
  * check `Number.isFinite` before scrolling with it. */
@@ -208,6 +208,34 @@ describe("attachOverlayScrollbars", () => {
208
208
  expect(viewport.scrollTop).toBe(150);
209
209
  });
210
210
 
211
+ it("maps a horizontal track click onto the RTL scroll range", () => {
212
+ const { viewport, wrapper, handle } = mountViewport("horizontal");
213
+ stubGeometry(viewport, {
214
+ scrollHeight: 100, clientHeight: 100, scrollTop: 0,
215
+ scrollWidth: 400, clientWidth: 100, scrollLeft: 0,
216
+ });
217
+ // An RTL scroller counts `scrollLeft` down from 0 to −max (see the thumb
218
+ // mirroring): the track's LEFT end is the content's end, so a click at the
219
+ // halfway point must land at −150, not +150.
220
+ viewport.style.direction = "rtl";
221
+ handle.update();
222
+ const track = wrapper.querySelector<HTMLElement>(".hk-scrollbar-track")!;
223
+ vi.spyOn(track, "getBoundingClientRect").mockReturnValue({
224
+ top: 0, left: 0, width: 100, height: 6,
225
+ bottom: 6, right: 100, x: 0, y: 0, toJSON: () => ({}),
226
+ } as DOMRect);
227
+ track.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX: 25 }));
228
+ // A quarter along the track, measured from its left = three quarters into
229
+ // the content from its (right-hand) start: −0.75 × 300.
230
+ expect(viewport.scrollLeft).toBe(-225);
231
+ // The magnitude is what the thumb's own mirroring reads back as progress
232
+ // (|scrollLeft| / max), so click and thumb now agree on the position.
233
+ // The LTR bar keeps mapping straight through.
234
+ viewport.style.direction = "ltr";
235
+ track.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX: 25 }));
236
+ expect(viewport.scrollLeft).toBe(75);
237
+ });
238
+
211
239
  it("marks and unmarks a static parent with inline position:relative (guard)", () => {
212
240
  const { wrapper, handle } = mountViewport("vertical");
213
241
  // happy-dom computes no CSS → position computes "static" → the
@@ -266,8 +266,16 @@ export function attachOverlayScrollbars(
266
266
  : (e.clientY - rect.top) / rect.height;
267
267
  const { scrollSize, clientSize } = axisMetrics(viewport, horizontal);
268
268
  const max = scrollSize - clientSize;
269
- if (horizontal) viewport.scrollLeft = ratio * max;
270
- else viewport.scrollTop = ratio * max;
269
+ if (horizontal) {
270
+ // RTL: the track's left end is the content's END, and `scrollLeft`
271
+ // runs 0 → −max (see the thumb mirroring in `update`), so a click at
272
+ // `ratio` from the left asks for `(ratio - 1) * max`. Without this the
273
+ // thumb and the click disagreed in RTL: the thumb was mirrored but the
274
+ // click still mapped LTR, so clicking right of the thumb scrolled the
275
+ // other way.
276
+ const rtl = isRtlAxis(viewport, true);
277
+ viewport.scrollLeft = rtl ? (ratio - 1) * max : ratio * max;
278
+ } else viewport.scrollTop = ratio * max;
271
279
  };
272
280
  const onEnter = () => s.track.classList.add("is-hovering");
273
281
  const onLeave = () => s.track.classList.remove("is-hovering");
@@ -19,7 +19,6 @@
19
19
  --c-primary-light: rgb(var(--color-primary, 255 107 157) / 15%);
20
20
  --c-primary-subtle: rgb(var(--color-primary, 255 107 157) / 8%);
21
21
  --c-primary-overlay: rgb(var(--color-primary, 255 107 157) / 15%);
22
- --border-faint: rgb(var(--color-border, 100 100 100) / 10%);
23
22
 
24
23
  /* ── Hikari color bridge ─ */
25
24
  --hi-color-primary: rgb(var(--color-primary, 255 107 157));
@@ -70,13 +70,6 @@ const scaleSource = stripProvenanceHeader(
70
70
  read(resolve(stylesDir, "theme/scale.scss")),
71
71
  );
72
72
  const adminSource = read(resolve(stylesDir, "admin-tokens.scss"));
73
- const legacySheets = [
74
- "base.scss",
75
- "foundation.scss",
76
- "themes.scss",
77
- "_tokens.scss",
78
- "_layout.scss",
79
- ].map((b) => [`theme/${b}`, stripProvenanceHeader(read(resolve(stylesDir, "theme", b)))] as [string, string]);
80
73
 
81
74
  const seedFiles: Array<[string, string]> = [
82
75
  ["theme/channels.scss", seedSource],
@@ -151,80 +144,19 @@ describe("theme token inventory", () => {
151
144
 
152
145
  it("scale tokens are single-defined across the normalized sheets", () => {
153
146
  // Current legacy debt: these names still multi-define across the
154
- // legacy sheets + admin-tokens (frozen 2026-09-12 census, 56 names).
147
+ // legacy sheets + admin-tokens (frozen 2026-09-12 census).
155
148
  // Each entry must die in its wave; adding a NEW name here is a
156
149
  // regression.
150
+ // Intentional dual faces: the plana-ui brand aliases in admin-tokens
151
+ // (pink no-JS face) vs the channel seed (standalone face).
157
152
  const KNOWN_MULTI_SCALE = new Set([
158
- "--blur-lg",
159
- "--blur-md",
160
- "--blur-sm",
161
- "--border-faint",
162
- "--border-input",
163
- "--border-subtle",
164
- "--c-primary",
165
153
  "--c-primary-light",
166
154
  "--c-primary-overlay",
167
155
  "--c-primary-subtle",
168
- "--duration-fast",
169
- "--duration-instant",
170
- "--duration-normal",
171
- "--duration-short",
172
- "--ease-in-expo",
173
- "--ease-in-out",
174
- "--ease-out-expo",
175
- "--ease-standard",
176
- "--font-mono",
177
- "--font-reading",
178
- "--font-sans",
179
- "--hi-bg-surface-dark",
180
- "--hi-border-color-focus",
181
- "--hi-duration-instant",
182
- "--hi-ease-default",
183
- "--hi-icon-color",
184
- "--hi-icon-size-lg",
185
- "--hi-icon-size-md",
186
- "--hi-icon-size-sm",
187
- "--hi-icon-size-xs",
188
- "--hi-radius-lg",
189
- "--hi-radius-md",
190
- "--hi-radius-sm",
191
- "--hi-radius-xl",
192
- "--hi-shadow-button",
193
- "--hi-shadow-elevated",
194
- "--hi-shadow-lg",
195
- "--hi-shadow-md",
196
- "--hi-shadow-sm",
197
- "--hi-shadow-xl",
198
- "--opacity-half",
199
- "--radius-md",
200
- "--radius-sm",
201
- "--space-10",
202
- "--space-12",
203
- "--space-14",
204
- "--space-16",
205
- "--space-2",
206
- "--space-20",
207
- "--space-24",
208
- "--space-28",
209
- "--space-32",
210
- "--space-4",
211
- "--space-40",
212
- "--space-6",
213
- "--space-8",
214
- "--text-2xs",
215
- "--text-base",
216
- "--text-lg",
217
- "--text-md",
218
- "--text-sm",
219
- "--text-xs",
220
- "--z-base",
221
- "--z-header",
222
- "--z-sidebar",
223
156
  ]);
224
157
 
225
158
  const defs = harvestDefinitions([
226
- seedFiles[0],
227
- ...legacySheets,
159
+ ...seedFiles,
228
160
  ["admin-tokens.scss", adminSource],
229
161
  ]);
230
162
  const scaleRe = /^--(space-|text-|radius-|blur-|duration-|ease-|z-|border-|c-|opacity-|shadow-(?!dropdown|focus|button-danger)|font-|hi-(duration|ease|radius|icon|blur|opacity|shadow-(?!dropdown|focus)|z-|border-color))/;
@@ -286,5 +218,38 @@ describe("theme token inventory", () => {
286
218
  }
287
219
  }
288
220
  expect(deviants, "non-canonical hex fallbacks").toEqual([]);
221
+
222
+ // rgba() fallbacks: the color channels must equal the seed triplet
223
+ // (alpha is the caller's choice and stays untouched).
224
+ const seedTripletValues: Record<string, [number, number, number]> = {};
225
+ for (const [name, sites] of seedTriplets) {
226
+ if (!name.startsWith("--color-")) continue;
227
+ const [r, g, b] = sites[0].value.split(" ").map((n) => Number(n));
228
+ seedTripletValues[name] = [r, g, b];
229
+ }
230
+ const rgbaDeviants: string[] = [];
231
+ for (const f of files) {
232
+ const css = read(resolve(componentsDir, f));
233
+ for (const m of css.matchAll(
234
+ /var\((--hi-color-[a-z0-9-]+),\s*rgba\((\d+),\s*(\d+),\s*(\d+),/g,
235
+ )) {
236
+ const token = m[1];
237
+ // danger is the one alias whose channel is named differently.
238
+ const channel = token
239
+ .replace("--hi-color-", "--color-")
240
+ .replace("danger", "error")
241
+ .replace(/-dark$/, "");
242
+ const seed = seedTripletValues[channel];
243
+ if (!seed) continue;
244
+ if (
245
+ Number(m[2]) !== seed[0] ||
246
+ Number(m[3]) !== seed[1] ||
247
+ Number(m[4]) !== seed[2]
248
+ ) {
249
+ rgbaDeviants.push(`${f}: var(${token}, rgba(${m[2]},${m[3]},${m[4]},…))`);
250
+ }
251
+ }
252
+ }
253
+ expect(rgbaDeviants, "non-canonical rgba fallbacks").toEqual([]);
289
254
  });
290
255
  });
@@ -7,12 +7,12 @@ import { describe, expect, it } from "vitest";
7
7
  /**
8
8
  * Vendored-copy drift guard.
9
9
  *
10
- * Two file families in src/styles are byte-identical re-copies of upstream
11
- * sources (registry installs can only resolve paths inside this package):
10
+ * The vendored theme copies in src/styles are byte-identical re-copies of
11
+ * upstream sources (registry installs can only resolve paths inside this
12
+ * package):
12
13
  *
13
- * 1. styles/theme/channels.scss ← src/tokens.scss
14
- * 2. styles/theme/{base,foundation,themes,_tokens,_layout,variables,
15
- * mixins,_glass,_scrollbar}.scss ← packages/theme/styles/*
14
+ * styles/theme/channels.scss ← src/tokens.scss
15
+ * styles/theme/scale.scss ← src/scale.scss
16
16
  *
17
17
  * The re-copy adds exactly one provenance header comment block; everything
18
18
  * after it must match the upstream byte-for-byte. Edit the UPSTREAM, then
@@ -50,17 +50,6 @@ const VENDORED_FROM_VUE: Array<[string, string]> = [
50
50
  ["theme/scale.scss", "scale.scss"],
51
51
  ];
52
52
 
53
- const VENDORED_FROM_THEME_PKG = [
54
- "base.scss",
55
- "foundation.scss",
56
- "themes.scss",
57
- "_tokens.scss",
58
- "_layout.scss",
59
- "variables.scss",
60
- "mixins.scss",
61
- "_glass.scss",
62
- "_scrollbar.scss",
63
- ];
64
53
 
65
54
  describe("vendored theme copies match their upstream sources", () => {
66
55
  it.each(VENDORED_FROM_VUE)(
@@ -76,16 +65,4 @@ describe("vendored theme copies match their upstream sources", () => {
76
65
  },
77
66
  );
78
67
 
79
- it.each(VENDORED_FROM_THEME_PKG)(
80
- "styles/theme/%s is the headered copy of packages/theme/styles/%s",
81
- (basename) => {
82
- const upstreamSource = readTrimmed(
83
- resolve(stylesDir, "../../../theme/styles", basename),
84
- );
85
- const vendoredBody = stripProvenanceHeader(
86
- readTrimmed(resolve(stylesDir, "theme", basename)),
87
- );
88
- expect(vendoredBody).toBe(upstreamSource);
89
- },
90
- );
91
68
  });
@@ -1,34 +0,0 @@
1
- // Vendored byte-identical from packages/theme/styles/_glass.scss (hikari repo) so npm registry installs can resolve it — edit the upstream file, then re-copy; do not hand-edit here.
2
-
3
- // _glass.scss — Glass-morphism utility classes adopted from shittim-chest.
4
- // These provide the frosted-glass aesthetic that makes panels feel layered
5
- // without heavy borders.
6
-
7
- .hk-glass {
8
- background: rgb(30 30 30 / var(--hi-opacity-half, 0.92));
9
- backdrop-filter: blur(var(--hi-blur-md, 16px));
10
- -webkit-backdrop-filter: blur(var(--hi-blur-md, 16px));
11
- }
12
-
13
- .hk-glass-header {
14
- background: rgb(30 30 30 / var(--hi-opacity-more, 0.96));
15
- backdrop-filter: blur(var(--hi-blur-sm, 8px));
16
- -webkit-backdrop-filter: blur(var(--hi-blur-sm, 8px));
17
- }
18
-
19
- .hk-glass-panel {
20
- background: rgb(30 30 30 / var(--hi-opacity-half, 0.92));
21
- backdrop-filter: blur(var(--hi-blur-lg, 24px));
22
- -webkit-backdrop-filter: blur(var(--hi-blur-lg, 24px));
23
- border: 1px solid rgb(255 255 255 / 8%);
24
- border-radius: var(--hi-radius-lg, 12px);
25
- box-shadow: var(--hi-shadow-panel);
26
- }
27
-
28
- .hk-glass-float {
29
- background: rgb(30 30 30 / var(--hi-opacity-more, 0.96));
30
- backdrop-filter: blur(var(--hi-blur-md, 16px));
31
- -webkit-backdrop-filter: blur(var(--hi-blur-md, 16px));
32
- border: 1px solid rgb(255 255 255 / 6%);
33
- box-shadow: var(--hi-shadow-elevated);
34
- }
@@ -1,124 +0,0 @@
1
- // Vendored byte-identical from packages/theme/styles/_layout.scss (hikari repo) so npm registry installs can resolve it — edit the upstream file, then re-copy; do not hand-edit here.
2
-
3
- // _layout.scss — Shared unprefixed design tokens for hikari + plana components.
4
- // Originally sourced from shittim-chest's theme system; extracted here so
5
- // any project importing @celestia-island/hikari/styles gets them automatically.
6
- //
7
- // Prefixed --hi-* equivalents exist in _tokens.scss and foundation.scss;
8
- // these unprefixed tokens are what hikari Hk* components and plana Plana*
9
- // components reference at call sites (with var(--token, fallback) pattern).
10
-
11
- :root {
12
- // ── Typography scale (HkTabs, HkButton, and plana components depend on these) ─
13
- --text-2xs: 0.625rem;
14
- --text-xs: 0.75rem;
15
- --text-sm: 0.8125rem;
16
- --text-base: 0.875rem;
17
- --text-md: 1rem;
18
- --text-lg: 1.125rem;
19
- --text-xl: 1.25rem;
20
- --text-2xl: 1.5rem;
21
-
22
- // ── Font families (body text, mono for version/numbers) ─
23
- /* Apple-style stack + CJK UI tail — keep in sync with packages/vue/src/theme/fontContext.ts */
24
- --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
25
- --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", monospace;
26
- --font-reading: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
27
-
28
- // ── Spacing scale (0—96, named by px value for self-documenting call sites) ─
29
- --space-1: 0.0625rem;
30
- --space-2: 0.125rem;
31
- --space-4: 0.25rem;
32
- --space-6: 0.375rem;
33
- --space-8: 0.5rem;
34
- --space-10: 0.625rem;
35
- --space-12: 0.75rem;
36
- --space-14: 0.875rem;
37
- --space-16: 1rem;
38
- --space-20: 1.25rem;
39
- --space-24: 1.5rem;
40
- --space-28: 1.75rem;
41
- --space-32: 2rem;
42
- --space-40: 2.5rem;
43
- --space-48: 3rem;
44
- --space-64: 4rem;
45
- --space-80: 5rem;
46
- --space-96: 6rem;
47
-
48
- // ── Border radius scale ─
49
- --radius-xs: 2px;
50
- --radius-sm: 4px;
51
- --radius-md: 8px;
52
- --radius-lg: 12px;
53
- --radius-full: 9999px;
54
-
55
- // ── Blur scale (glass/backdrop effects) ─
56
- --blur-xs: 4px;
57
- --blur-sm: 8px;
58
- --blur-md: 16px;
59
- --blur-lg: 24px;
60
-
61
- // ── Opacity tiers ─
62
- --opacity-faded: 0.45;
63
- --opacity-less: 0.85;
64
- --opacity-half: 0.92;
65
- --opacity-more: 0.96;
66
-
67
- // ── Z-index hierarchy (group bands) ─
68
- // One registered context: every layer family owns a band. Members inside
69
- // a band stack by DOM order (browser default) — only the band boundaries
70
- // are managed. Overlay-class components (modal/drawer/popover/select
71
- // popout/menu sheet/locale picker) MUST register with usePopupManager,
72
- // which allocates from the popup band (1000 + 2n, reset when the stack
73
- // empties); fixed layers above the stack (toast/tooltip/fatal/titlebar)
74
- // never register.
75
- // 0–9 local — intra-component stacking (self-contained contexts)
76
- // 10–99 content float — sticky columns, in-page lifts, minimaps
77
- // 100–199 app chrome — header, footer, header popups
78
- // 200–899 app overlay — reserved for app-level decorations
79
- // 900–999 app sheet — drawer sidebars, bottom sheets
80
- // 1000–1999 popup stack — manager-allocated (modal/drawer/popover/…)
81
- // 9999 toast 10000 tooltip 10001 fatal
82
- // 100001 native titlebar (Tauri caption) 100002 modal breadcrumb
83
- --z-base: 0;
84
- --z-above: 1;
85
- --z-overlay: 2;
86
- --z-top: 3;
87
- --z-content: 10;
88
- --z-floating: 50;
89
- --z-header: 100;
90
- --z-footer: 110;
91
- --z-header-popup: 150;
92
- --z-sheet: 900;
93
- --z-sidebar: 900;
94
- --z-modal: 1000;
95
- --z-modal-base: 1000;
96
- --z-modal-step: 2;
97
- --z-toast: 9999;
98
- --z-tooltip: 10000;
99
-
100
- // ── Layout dimensions ─
101
- --s-header-height: 3rem;
102
- --s-footer-height: 3rem;
103
-
104
- // ── Semantic border tokens ─
105
- --border-faint: rgb(var(--color-border) / 10%);
106
- --border-subtle: rgb(var(--color-border) / 12%);
107
-
108
- // ── Duration / easing ─
109
- --duration-fast: 0.15s;
110
- --duration-short: 0.15s;
111
- --duration-normal: 0.3s;
112
- --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
113
- --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
114
- --ease-in-expo: cubic-bezier(0.7, 0, 0.84, 0);
115
-
116
- // ── Semantic accent colors (used by plana components) ─
117
- --c-primary: rgb(var(--color-primary));
118
- --c-primary-light: rgb(var(--color-primary) / 15%);
119
- --c-primary-subtle: rgb(var(--color-primary) / 8%);
120
- --c-primary-overlay: rgb(var(--color-primary) / 15%);
121
-
122
- // ── Shadow scale ─
123
- --shadow-modal: 0 12px 40px rgb(0 0 0 / 20%), 0 0 0 1px rgb(255 255 255 / 4%);
124
- }
@@ -1,74 +0,0 @@
1
- // Vendored byte-identical from packages/theme/styles/_scrollbar.scss (hikari repo) so npm registry installs can resolve it — edit the upstream file, then re-copy; do not hand-edit here.
2
-
3
- // _scrollbar.scss — the shared overlay scrollbar chrome.
4
- //
5
- // ONE scrollbar system for the whole library: the `.hk-scrollbar-track` /
6
- // `.hk-scrollbar-thumb` DOM is created imperatively by
7
- // packages/vue/src/composables/useOverlayScrollbar.ts
8
- // (attachOverlayScrollbars) and styled here. HkScrollContainer keeps its
9
- // container-specific rules in HkScrollContainer.scss; every other scroll
10
- // region in the library attaches the same overlay and hides its native
11
- // bar with the standard two-liner:
12
- //
13
- // scrollbar-width: none;
14
- // &::-webkit-scrollbar { display: none; }
15
-
16
- .hk-scrollbar-track {
17
- position: absolute;
18
- z-index: 10;
19
- background: transparent;
20
- /* Click-through while faded: an invisible rail must never steal
21
- clicks from the content beneath it (the track hugs the region's
22
- edge, exactly where end-of-line links/buttons live). The rail
23
- becomes interactive only while it is visible — the flash window
24
- after a scroll (and while hovered/dragged within that window). */
25
- pointer-events: none;
26
- opacity: 0;
27
- transition: opacity var(--hi-duration-fast, 0.15s) ease;
28
-
29
- &[data-axis="horizontal"] {
30
- left: 4px;
31
- right: 4px;
32
- bottom: 2px;
33
- height: 6px;
34
- }
35
-
36
- &:not([data-axis="horizontal"]) {
37
- top: 4px;
38
- bottom: 4px;
39
- right: 2px;
40
- width: 6px;
41
- }
42
-
43
- &.is-scrolling,
44
- &.is-hovering {
45
- opacity: 1;
46
- pointer-events: auto;
47
- }
48
- }
49
-
50
- .hk-scrollbar-thumb {
51
- position: absolute;
52
- border-radius: 3px;
53
- background: color-mix(in srgb, var(--hi-color-muted) 35%, transparent);
54
- transition: background var(--hi-duration-fast, 0.15s) ease;
55
-
56
- .is-hovering &,
57
- &.is-dragging {
58
- background: color-mix(in srgb, var(--hi-color-muted) 55%, transparent);
59
- }
60
- }
61
-
62
- .hk-scrollbar-track[data-axis="horizontal"] .hk-scrollbar-thumb {
63
- top: 0;
64
- left: 0;
65
- height: 100%;
66
- min-width: 20px;
67
- }
68
-
69
- .hk-scrollbar-track:not([data-axis="horizontal"]) .hk-scrollbar-thumb {
70
- top: 0;
71
- left: 0;
72
- width: 100%;
73
- min-height: 20px;
74
- }