@celestia-island/hikari 0.40.33 → 0.40.34

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.40.33",
3
+ "version": "0.40.34",
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",
@@ -29,6 +29,21 @@
29
29
  }
30
30
  }
31
31
 
32
+ /* ONE SCROLLBAR PER WINDOW (2026-09-08 audit): inside a window surface's
33
+ * own scroller the bounded tree cap lit a second scrollbar under the
34
+ * window's — the window's scrollbar is the only one there and the tree
35
+ * grows with its content. Pages and standalone cards (the error landing,
36
+ * chat tool blocks outside windows) keep the bounded viewport above.
37
+ * The host's deferred overlay pass self-hides its rails once the
38
+ * content fits, so no attach-side change is needed. */
39
+ .hk-modal-body-scroll .s-tool-json-tree,
40
+ .hk-select-sheet-list .s-tool-json-tree,
41
+ .hk-select-popout .s-tool-json-tree,
42
+ .hk-popover-panel .s-tool-json-tree,
43
+ .hk-drawer-body .s-tool-json-tree {
44
+ max-height: none;
45
+ }
46
+
32
47
  .s-jt-row {
33
48
  /* Baseline alignment keeps the small type hints (boolean/number/string)
34
49
  on the value's text baseline instead of floating above it. */
@@ -1,15 +1,7 @@
1
+ /* ONE SCROLLBAR PER WINDOW (2026-09-08 audit): the body grows with its
2
+ * content and the HkModal body scroller is THE scrollbar. The old
3
+ * `bodyHeight`-driven nested scroll region (with its own overlay chrome)
4
+ * was removed as dead API — no consumer in the family ever passed it. */
1
5
  .s-protocol-modal {
2
6
  position: relative;
3
7
  }
4
-
5
-
6
- /* With `bodyHeight` set the body becomes its own scroll region (nested
7
- * inside HkModal's body scroller) — overlay chrome only. */
8
- .s-protocol-modal {
9
- overflow-y: auto;
10
- scrollbar-width: none;
11
-
12
- &::-webkit-scrollbar {
13
- display: none;
14
- }
15
- }
@@ -1,4 +1,4 @@
1
- import { computed, defineComponent, onBeforeUnmount, ref, watch, type PropType } from "vue";
1
+ import { computed, defineComponent, type PropType } from "vue";
2
2
  import {
3
3
  HMarkdownRenderer,
4
4
  HModal,
@@ -9,7 +9,6 @@ import {
9
9
  } from "@celestia-island/hikari";
10
10
 
11
11
  import { useI18n } from "../i18n/context";
12
- import { attachOverlayScrollbars, type OverlayScrollbarHandle } from "../composables/useOverlayScrollbar";
13
12
 
14
13
  import "./HkProtocolModal.scss";
15
14
 
@@ -21,6 +20,12 @@ import "./HkProtocolModal.scss";
21
20
  * a Decline/Accept footer. `accept`/`decline` are the caller's commit
22
21
  * actions; closing (overlay click / ESC / X) only emits `update:modelValue`
23
22
  * so the caller can decide whether close === decline.
23
+ *
24
+ * ONE SCROLLBAR PER WINDOW: the body grows with its content and the
25
+ * HkModal body scroller is THE scrollbar. The old `bodyHeight` prop
26
+ * (a height-capped scroll region nested inside the modal's own scroller —
27
+ * the exact double-scroll this library eliminates) was removed as dead
28
+ * API: no consumer in the family ever passed it.
24
29
  */
25
30
  export const HkProtocolModal = defineComponent({
26
31
  name: "HkProtocolModal",
@@ -37,8 +42,6 @@ export const HkProtocolModal = defineComponent({
37
42
  /** Allow dismissing without a decision (overlay/ESC/X). Default true. */
38
43
  closable: { type: Boolean, default: true },
39
44
  width: { type: String as PropType<ModalWidth>, default: "48rem" },
40
- /** Cap the scroll body height (e.g. "60vh"). */
41
- bodyHeight: { type: String, default: undefined },
42
45
  },
43
46
  emits: {
44
47
  "update:modelValue": (_v: boolean) => true,
@@ -69,32 +72,6 @@ export const HkProtocolModal = defineComponent({
69
72
  },
70
73
  ]);
71
74
 
72
- // ── overlay scrollbar (shared chrome) ─────────────────────────
73
- // With `bodyHeight` set the body becomes a height-capped scroll
74
- // region NESTED inside HkModal's overlay-equipped body scroller —
75
- // without the overlay it would be a surviving native bar (the exact
76
- // double-scroll this library eliminates). Attach on mount, re-sync
77
- // when the cap flips, detach on unmount.
78
- const bodyRef = ref<HTMLElement | null>(null);
79
- let bodyScrollbar: OverlayScrollbarHandle | null = null;
80
- function syncBodyScrollbar(): void {
81
- if (props.bodyHeight && bodyRef.value) {
82
- if (!bodyScrollbar) bodyScrollbar = attachOverlayScrollbars(bodyRef.value);
83
- } else {
84
- bodyScrollbar?.detach();
85
- bodyScrollbar = null;
86
- }
87
- }
88
- function bindBodyRef(el: unknown): void {
89
- bodyRef.value = (el as HTMLElement) ?? null;
90
- syncBodyScrollbar();
91
- }
92
- watch(() => props.bodyHeight, () => syncBodyScrollbar());
93
- onBeforeUnmount(() => {
94
- bodyScrollbar?.detach();
95
- bodyScrollbar = null;
96
- });
97
-
98
75
  return () => (
99
76
  <HModal
100
77
  modelValue={props.modelValue}
@@ -104,11 +81,7 @@ export const HkProtocolModal = defineComponent({
104
81
  closable={props.closable}
105
82
  footerActions={footerActions.value}
106
83
  >
107
- <div
108
- ref={bindBodyRef}
109
- class="s-protocol-modal"
110
- style={props.bodyHeight ? { maxHeight: props.bodyHeight } : undefined}
111
- >
84
+ <div class="s-protocol-modal">
112
85
  <HMarkdownRenderer content={props.content} />
113
86
  </div>
114
87
  </HModal>
@@ -114,6 +114,22 @@
114
114
  }
115
115
  }
116
116
 
117
+ /* ONE SCROLLBAR PER WINDOW (2026-09-08 audit, HkJsonTree tool blocks and
118
+ code panes): inside a window surface's own scroller the bounded pane
119
+ cap lit a SECOND scrollbar under the window's — one per pane in pane
120
+ lists like the chest todo-log modal. There the window's scrollbar is
121
+ the only one and the pane grows with its content. Chat message cards
122
+ and pages are NOT window surfaces: the bounded viewport above stays.
123
+ (The panes' overlay rails self-hide once the content fits, so the
124
+ deferred attach pass needs no changes.) */
125
+ .hk-modal-body-scroll .s-tool-code,
126
+ .hk-select-sheet-list .s-tool-code,
127
+ .hk-select-popout .s-tool-code,
128
+ .hk-popover-panel .s-tool-code,
129
+ .hk-drawer-body .s-tool-code {
130
+ max-height: none;
131
+ }
132
+
117
133
  .s-tool-result {
118
134
  border-left: 2px solid rgb(var(--color-success) / 40%);
119
135
  padding-left: var(--space-8, 0.5rem);
@@ -167,6 +183,15 @@
167
183
  }
168
184
  }
169
185
 
186
+ /* Window-surface twin of the .s-tool-code rule above. */
187
+ .hk-modal-body-scroll .s-tool-code-block,
188
+ .hk-select-sheet-list .s-tool-code-block,
189
+ .hk-select-popout .s-tool-code-block,
190
+ .hk-popover-panel .s-tool-code-block,
191
+ .hk-drawer-body .s-tool-code-block {
192
+ max-height: none;
193
+ }
194
+
170
195
  .s-tool-code-table {
171
196
  border-collapse: collapse;
172
197
  width: 100%;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Source contract: ONE SCROLLBAR PER WINDOW — tool panes (2026-09-08
3
+ * audit follow-up). The tool block's code panes and the shared JSON tree
4
+ * keep a bounded widget viewport in chat cards and on pages (the exempt
5
+ * content-widget class, like markdown code blocks), but INSIDE a window
6
+ * surface's own scroller (modal body, select sheet, popout, popover
7
+ * sheet/panel, drawer body) that cap lit a second scrollbar under the
8
+ * window's — one per pane in pane lists like the chest todo-log modal.
9
+ * There the window's scrollbar is the only one and the pane grows with
10
+ * its content.
11
+ */
12
+ import { describe, expect, it } from "vitest";
13
+ import { readFileSync } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const here = dirname(fileURLToPath(import.meta.url));
18
+ const toolScss = readFileSync(join(here, "HkToolBlock.scss"), "utf-8");
19
+ const treeScss = readFileSync(join(here, "HkJsonTree.scss"), "utf-8");
20
+ const toolTsx = readFileSync(join(here, "HkToolBlock.tsx"), "utf-8");
21
+
22
+ /** Window-surface scrollers — the ONE-scrollbar owners. */
23
+ const SCROLLERS = [
24
+ ".hk-modal-body-scroll",
25
+ ".hk-select-sheet-list",
26
+ ".hk-select-popout",
27
+ ".hk-popover-panel",
28
+ ".hk-drawer-body",
29
+ ] as const;
30
+
31
+ const PANES = [".s-tool-code", ".s-tool-code-block", ".s-tool-json-tree"] as const;
32
+
33
+ /** Extract ONE balanced `{...}` declaration block following `selector`
34
+ * (a naive `[^}]*` would stop at the first nested `}` — see the
35
+ * HkAffixPicker single-scroll contract for the established pattern). */
36
+ function cssBlock(src: string, selector: string): string {
37
+ const at = src.indexOf(selector);
38
+ if (at < 0) return "";
39
+ const open = src.indexOf("{", at);
40
+ if (open < 0) return "";
41
+ let depth = 0;
42
+ for (let i = open; i < src.length; i++) {
43
+ if (src[i] === "{") depth++;
44
+ else if (src[i] === "}") {
45
+ depth--;
46
+ if (depth === 0) return src.slice(open, i + 1);
47
+ }
48
+ }
49
+ return "";
50
+ }
51
+
52
+ describe("tool pane single-scrollbar-per-window contract", () => {
53
+ it("keeps the bounded widget viewport outside windows", () => {
54
+ expect(cssBlock(toolScss, ".s-tool-code ")).toContain("max-height: 240px");
55
+ expect(cssBlock(toolScss, ".s-tool-code-block ")).toContain("max-height: 240px");
56
+ expect(cssBlock(treeScss, ".s-tool-json-tree ")).toContain("max-height: 320px");
57
+ });
58
+
59
+ it("drops the cap inside every window surface scroller", () => {
60
+ for (const pane of PANES) {
61
+ for (const scroller of SCROLLERS) {
62
+ // The uncap rule lives next to the pane's own file: tool panes in
63
+ // HkToolBlock.scss, the tree in HkJsonTree.scss. Search BOTH — a
64
+ // rule moved between the files must not fail the pin, a rule
65
+ // deleted anywhere must fail it.
66
+ const body =
67
+ cssBlock(toolScss, `${scroller} ${pane}`) ||
68
+ cssBlock(treeScss, `${scroller} ${pane}`);
69
+ expect(body, `${scroller} ${pane} uncap rule exists`).toBeTruthy();
70
+ expect(body, `${scroller} ${pane} uncaps via max-height: none`).toContain(
71
+ "max-height: none",
72
+ );
73
+ }
74
+ }
75
+ });
76
+
77
+ it("no window-scroller context ever re-caps a pane with a length", () => {
78
+ for (const scss of [toolScss, treeScss]) {
79
+ for (const scroller of SCROLLERS) {
80
+ // Any scroller-descendant rule for a pane carrying a px/vh/rem
81
+ // max-height would reintroduce the nested scrollbar.
82
+ const re = new RegExp(
83
+ `${scroller.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} [^{]*\\{[^}]*max-height:\\s*(?!none)[\\d.]`,
84
+ );
85
+ expect(scss.match(re), `${scroller} descendant px cap`).toBeNull();
86
+ }
87
+ }
88
+ });
89
+
90
+ it("the deferred overlay pass still targets the three panes (rails self-hide when uncapped)", () => {
91
+ expect(toolTsx).toContain('".s-tool-code, .s-tool-code-block, .s-tool-json-tree"');
92
+ });
93
+ });