@celestia-island/hikari 0.55.100 → 0.55.101
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
|
@@ -218,8 +218,9 @@
|
|
|
218
218
|
// stretching the card. The 9rem term keeps short viewports (20vh < 9rem)
|
|
219
219
|
// at nine rem of scroll area. The pane div is the track's positioning
|
|
220
220
|
// context and wraps EXACTLY the scrolling body (useOverlayScrollbar
|
|
221
|
-
// host contract); the family overlay scrollbar
|
|
222
|
-
//
|
|
221
|
+
// host contract); the family overlay scrollbar — BOTH axes, the body
|
|
222
|
+
// below hides the native bar horizontally too — keeps the thumb
|
|
223
|
+
// geometry fresh on fold/expand.
|
|
223
224
|
.hk-error-landing__details-pane {
|
|
224
225
|
position: relative;
|
|
225
226
|
margin-top: var(--space-6, 0.375rem);
|
|
@@ -119,10 +119,20 @@ describe("HkErrorLanding", () => {
|
|
|
119
119
|
expect(el.querySelector(".hk-error-landing__desc")!.textContent).toBe("line1\nline2");
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
it("attaches the overlay scrollbar chrome inside the details pane", () => {
|
|
122
|
+
it("attaches the overlay scrollbar chrome inside the details pane on BOTH axes", () => {
|
|
123
123
|
const el = mountLanding({ details: () => h("pre", { class: "s-tool-json-tree" }, "raw") });
|
|
124
|
-
|
|
125
|
-
expect(
|
|
124
|
+
const pane = el.querySelector(".hk-error-landing__details-pane")!;
|
|
125
|
+
expect(pane.querySelector(".hk-scrollbar-track")).not.toBeNull();
|
|
126
|
+
expect(pane.querySelector(".hk-scrollbar-thumb")).not.toBeNull();
|
|
127
|
+
// The pane's CSS hides the NATIVE bar on both axes, so the overlay is
|
|
128
|
+
// the only scrollbar the raw payload can ever show. A vertical-only
|
|
129
|
+
// attach (the pre-fix default) leaves a horizontal overflow — deep
|
|
130
|
+
// JSON nesting, long unwrapped keys (preview rows ellipsize by
|
|
131
|
+
// design) — with no scrollbar at all: pin one track per axis.
|
|
132
|
+
expect(pane.querySelector(".hk-scrollbar-track[data-axis='horizontal']")).not.toBeNull();
|
|
133
|
+
expect(pane.querySelector(".hk-scrollbar-track:not([data-axis='horizontal'])")).not.toBeNull();
|
|
134
|
+
// Exactly the two tracks — no duplicate chrome from a double attach.
|
|
135
|
+
expect(pane.querySelectorAll(".hk-scrollbar-track")).toHaveLength(2);
|
|
126
136
|
});
|
|
127
137
|
|
|
128
138
|
it("renders no scrollbar chrome without a details pane", () => {
|
|
@@ -45,7 +45,8 @@ function normalizeSlotNodes(nodes: VNode | VNode[] | undefined): VNode[] {
|
|
|
45
45
|
* error code / HTTP status as HkBadge chips above the headline, an
|
|
46
46
|
* always-open raw-details pane (the default slot — hosts render HkJsonTree
|
|
47
47
|
* there) with a STABLE standing frame — the larger of 9rem and ~20vh,
|
|
48
|
-
* carried by the family overlay scrollbar
|
|
48
|
+
* carried by the family overlay scrollbar on BOTH axes (vertical for long
|
|
49
|
+
* stacks, horizontal for deep/wide JSON) — and an actions slot.
|
|
49
50
|
*
|
|
50
51
|
* The pane never breathes with its content: folding every JSON node only
|
|
51
52
|
* changes what renders INSIDE the standing frame, and a long stack trace
|
|
@@ -129,7 +130,12 @@ export const HkErrorLanding = defineComponent({
|
|
|
129
130
|
|
|
130
131
|
function ensureDetailsScrollbars() {
|
|
131
132
|
if (detailsScrollbars || !detailsBodyRef.value) return;
|
|
132
|
-
|
|
133
|
+
// BOTH axes: the raw payload overflows down (long stacks) and across
|
|
134
|
+
// (deep JSON nesting, long unwrapped keys — the tree's preview rows
|
|
135
|
+
// ellipsize by design and never push the row wide) alike, and the
|
|
136
|
+
// pane's CSS hides the native bar on both axes — a vertical-only
|
|
137
|
+
// attach leaves a horizontal overflow with no scrollbar at all.
|
|
138
|
+
detailsScrollbars = attachOverlayScrollbars(detailsBodyRef.value, { axis: "both" });
|
|
133
139
|
contentResizeObserver = new ResizeObserver(() => detailsScrollbars?.update());
|
|
134
140
|
observeDetailsContent();
|
|
135
141
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Source contract for the shared overlay-scrollbar chrome.
|
|
9
|
+
*
|
|
10
|
+
* The `.hk-scrollbar-track` / `.hk-scrollbar-thumb` DOM is created
|
|
11
|
+
* imperatively by useOverlayScrollbar; its styling lives in the theme
|
|
12
|
+
* partial `packages/theme/styles/_scrollbar.scss`, vendored byte-exact
|
|
13
|
+
* into `src/styles/theme/scrollbar.scss` (see vendoredSync.test.ts for
|
|
14
|
+
* the drift guard). This contract reads the SHIPPED vendored copy — the
|
|
15
|
+
* one npm consumers resolve through the `./styles/*` export — and pins
|
|
16
|
+
* the two properties the error landing's themeless contract depends on:
|
|
17
|
+
*
|
|
18
|
+
* 1. Thumb color must survive a missing `--hi-color-muted`: HkErrorLanding
|
|
19
|
+
* is designed to render standalone (server error page without hikari
|
|
20
|
+
* theme init; its SCSS carries `--hel-*` fallbacks for exactly that
|
|
21
|
+
* case). An unfallback'd `var(--hi-color-muted)` inside `color-mix()`
|
|
22
|
+
* computes the whole color to invalid-at-computed-value-time — the
|
|
23
|
+
* thumb renders invisible on BOTH axes while the pane's CSS hides the
|
|
24
|
+
* native bar, i.e. no scrollbar at all. (2026-10-01: found while
|
|
25
|
+
* giving the error details pane its horizontal overlay track.)
|
|
26
|
+
* 2. Both axis rails exist with their axis-keyed selectors — the engine
|
|
27
|
+
* mounts a `data-axis="horizontal"` track and an attribute-less
|
|
28
|
+
* vertical track; the chrome must style both.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const stylesDir = resolve(dirname(fileURLToPath(import.meta.url)), "../styles");
|
|
32
|
+
const chrome = readFileSync(resolve(stylesDir, "theme", "scrollbar.scss"), "utf8");
|
|
33
|
+
|
|
34
|
+
describe("overlay scrollbar chrome contract", () => {
|
|
35
|
+
it("ships real chrome (positive control for the reads below)", () => {
|
|
36
|
+
// Zero-yield guard: these extractions must prove they saw the file.
|
|
37
|
+
expect(chrome).toContain(".hk-scrollbar-track");
|
|
38
|
+
expect(chrome).toContain(".hk-scrollbar-thumb");
|
|
39
|
+
expect(chrome.length).toBeGreaterThan(200);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("styles both axis rails with their axis-keyed selectors", () => {
|
|
43
|
+
expect(chrome).toMatch(/\.hk-scrollbar-track\[data-axis="horizontal"\]/);
|
|
44
|
+
expect(chrome).toMatch(/\.hk-scrollbar-track:not\(\[data-axis="horizontal"\]\)/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("keeps the thumb visible without --hi-color-muted (themeless render)", () => {
|
|
48
|
+
// Every color-mix() over the muted channel must carry a fallback.
|
|
49
|
+
// (Match to the declaration's semicolon: the fallback's own `)` would
|
|
50
|
+
// truncate a paren-counting match.)
|
|
51
|
+
const mixes = chrome.match(/color-mix\([^;]+/g) ?? [];
|
|
52
|
+
// Positive control: the file is known to mix the muted channel at the
|
|
53
|
+
// resting and hovered/dragged opacities — a rename would 0-match and
|
|
54
|
+
// make this check vacuously green, so pin the count too.
|
|
55
|
+
expect(mixes.length).toBeGreaterThanOrEqual(2);
|
|
56
|
+
const muted = mixes.filter((m) => m.includes("--hi-color-muted"));
|
|
57
|
+
expect(muted.length).toBeGreaterThanOrEqual(2);
|
|
58
|
+
for (const mix of muted) {
|
|
59
|
+
expect(
|
|
60
|
+
mix,
|
|
61
|
+
"color-mix over --hi-color-muted must declare a fallback color",
|
|
62
|
+
).toMatch(/var\(--hi-color-muted,\s*rgb\(118 122 128\)\)/);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("keeps the rails pointer-transparent until they flash in", () => {
|
|
67
|
+
// An invisible rail must never steal clicks from the content hugging
|
|
68
|
+
// the region's edge (end-of-line links) — pointer-events: none at
|
|
69
|
+
// rest, auto only while visible.
|
|
70
|
+
expect(chrome).toMatch(/\.hk-scrollbar-track\s*\{[^}]*pointer-events:\s*none/);
|
|
71
|
+
expect(chrome).toMatch(/&?\.is-scrolling,\s*&?\.is-hovering\s*\{[^}]*pointer-events:\s*auto/);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -56,12 +56,17 @@
|
|
|
56
56
|
.hk-scrollbar-thumb {
|
|
57
57
|
position: absolute;
|
|
58
58
|
border-radius: 3px;
|
|
59
|
-
|
|
59
|
+
/* Fallback = the landing's neutral muted seed (HkErrorLanding's
|
|
60
|
+
--hel-muted 118 122 128): themeless standalone renders (the error
|
|
61
|
+
landing's no-theme-init contract) get a visible neutral thumb; an
|
|
62
|
+
undefined --hi-color-muted would make color-mix invalid at
|
|
63
|
+
computed-value time and the thumb invisible on BOTH axes. */
|
|
64
|
+
background: color-mix(in srgb, var(--hi-color-muted, rgb(118 122 128)) 35%, transparent);
|
|
60
65
|
transition: background var(--hi-duration-fast, 0.15s) ease;
|
|
61
66
|
|
|
62
67
|
.is-hovering &,
|
|
63
68
|
&.is-dragging {
|
|
64
|
-
background: color-mix(in srgb, var(--hi-color-muted) 55%, transparent);
|
|
69
|
+
background: color-mix(in srgb, var(--hi-color-muted, rgb(118 122 128)) 55%, transparent);
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
|