@celestia-island/hikari 0.55.203 → 0.55.204

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.203",
3
+ "version": "0.55.204",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -78,6 +78,43 @@
78
78
  margin: 0;
79
79
  }
80
80
 
81
+ /* Markdown tables size to their content, so a wide one (many columns, or
82
+ a single unbreakable cell) pushes every ancestor's min-content width
83
+ past its box — clipping hosts (the modal reading region, chat bubbles,
84
+ report cards) then leave the far columns unreachable. The renderer wraps
85
+ every table in the same two-level shape as code above: this wrap is the
86
+ rail HOST (track host for the scroller below — it contains ONLY the
87
+ scrolling box, so the rail hugs the table instead of spanning the whole
88
+ markdown body, useOverlayScrollbar's host contract) and carries the
89
+ table's vertical margin, because a scroll container never collapses
90
+ margins with its children and the margin must keep collapsing with the
91
+ surrounding flow exactly as the bare table's did. */
92
+ .hk-md-table-wrap {
93
+ position: relative;
94
+ margin: 0.75em 0;
95
+ }
96
+
97
+ /* The horizontal rail itself — the viewport the overlay scrollbar attaches
98
+ to. Only a genuinely overflowing table grows the scroll; normal tables
99
+ keep their full-width layout. Same hidden-native-chrome contract as
100
+ .hk-md-code (the 2026-10-01 native-bar regression rule). */
101
+ .hk-md-table-scroll {
102
+ overflow-x: auto;
103
+ scrollbar-width: none;
104
+
105
+ &::-webkit-scrollbar {
106
+ display: none;
107
+ }
108
+ }
109
+
110
+ /* The railed table renders flush inside its scroller — its old vertical
111
+ margin lives on the host wrap above. Outranks `.hk-markdown table`
112
+ (two classes + one type vs one class + one type); GFM tables cannot
113
+ nest, so no other table is ever matched. */
114
+ .hk-markdown .hk-md-table-wrap table {
115
+ margin: 0;
116
+ }
117
+
81
118
  .hk-md-code-lang {
82
119
  position: absolute;
83
120
  top: 0.25em;
@@ -61,7 +61,8 @@ async function renderMarkdown(content: string): Promise<string> {
61
61
  return renderPlain(content);
62
62
  }
63
63
 
64
- const renderer = new (marked as unknown as { Renderer: new () => Record<string, unknown> }).Renderer();
64
+ const RendererCtor = (marked as unknown as { Renderer: new () => Record<string, unknown> }).Renderer;
65
+ const renderer = new RendererCtor();
65
66
 
66
67
  (renderer as Record<string, unknown>).code = function (tok: { text: string; lang?: string }) {
67
68
  const highlighted = tryHighlight(tok.text, tok.lang);
@@ -74,6 +75,21 @@ async function renderMarkdown(content: string): Promise<string> {
74
75
  return `<div class="hk-md-code-wrap"><div class="hk-md-code">${langLabel}<pre><code class="hljs${langAttr}">${highlighted}</code></pre></div></div>`;
75
76
  };
76
77
 
78
+ (renderer as Record<string, unknown>).table = function (token: unknown) {
79
+ // Tables size to their content: a wide one (many columns, or a single
80
+ // unbreakable cell) pushes every ancestor's min-content width past its
81
+ // box, and clipping hosts (modal reading region, chat bubbles, report
82
+ // cards) leave the far columns unreachable. Same two-level shape as the
83
+ // code override above: `.hk-md-table-wrap` is the rail HOST (a plain
84
+ // positioned box wrapping only the scroller — the engine anchors the
85
+ // track in the viewport's parent), `.hk-md-table-scroll` is the
86
+ // viewport that actually rails. Rail the TABLE itself — normal tables
87
+ // keep their full-width layout; only a genuinely overflowing one grows
88
+ // the horizontal scroll (see .hk-md-table-scroll).
89
+ const inner = (RendererCtor.prototype as { table: (t: unknown) => string }).table.call(this, token);
90
+ return `<div class="hk-md-table-wrap"><div class="hk-md-table-scroll">${inner}</div></div>`;
91
+ };
92
+
77
93
  const raw = marked.parse(content, { renderer, async: false });
78
94
  const html = typeof raw === "string" ? raw : "";
79
95
  return DOMPurify.sanitize(html);
@@ -114,33 +130,34 @@ export default defineComponent({
114
130
  { immediate: true },
115
131
  );
116
132
 
117
- // Overlay scrollbars (shared chrome) on markdown code blocks. The
118
- // body is innerHTML-rendered and fully replaced on every parse, so
119
- // the attachment set is reconciled after each render pass: handles
120
- // whose block left the DOM detach, fresh blocks attach.
133
+ // Overlay scrollbars (shared chrome) on the markdown blocks that can
134
+ // rail horizontally: code blocks and table scrollers. The body is
135
+ // innerHTML-rendered and fully replaced on every parse, so the
136
+ // attachment set is reconciled after each render pass: handles whose
137
+ // block left the DOM detach, fresh blocks attach.
121
138
  const bodyRef = ref<HTMLElement>();
122
- const codeScrollbars: Array<{ el: HTMLElement; handle: OverlayScrollbarHandle }> = [];
139
+ const overlayScrollbars: Array<{ el: HTMLElement; handle: OverlayScrollbarHandle }> = [];
123
140
 
124
- function syncCodeScrollbars() {
125
- for (let i = codeScrollbars.length - 1; i >= 0; i--) {
126
- if (!codeScrollbars[i].el.isConnected) {
127
- codeScrollbars[i].handle.detach();
128
- codeScrollbars.splice(i, 1);
141
+ function syncOverlayScrollbars() {
142
+ for (let i = overlayScrollbars.length - 1; i >= 0; i--) {
143
+ if (!overlayScrollbars[i].el.isConnected) {
144
+ overlayScrollbars[i].handle.detach();
145
+ overlayScrollbars.splice(i, 1);
129
146
  }
130
147
  }
131
- for (const el of bodyRef.value?.querySelectorAll<HTMLElement>(".hk-md-code") ?? []) {
132
- if (codeScrollbars.some((c) => c.el === el)) continue;
133
- codeScrollbars.push({ el, handle: attachOverlayScrollbars(el, { axis: "horizontal" }) });
148
+ for (const el of bodyRef.value?.querySelectorAll<HTMLElement>(".hk-md-code, .hk-md-table-scroll") ?? []) {
149
+ if (overlayScrollbars.some((c) => c.el === el)) continue;
150
+ overlayScrollbars.push({ el, handle: attachOverlayScrollbars(el, { axis: "horizontal" }) });
134
151
  }
135
152
  }
136
153
 
137
154
  watch(renderedHtml, () => {
138
- void nextTick(syncCodeScrollbars);
155
+ void nextTick(syncOverlayScrollbars);
139
156
  });
140
157
 
141
158
  onBeforeUnmount(() => {
142
- for (const c of codeScrollbars) c.handle.detach();
143
- codeScrollbars.length = 0;
159
+ for (const c of overlayScrollbars) c.handle.detach();
160
+ overlayScrollbars.length = 0;
144
161
  });
145
162
 
146
163
  return () => (
@@ -0,0 +1,82 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ /**
7
+ * Source contract for the markdown table rail (2026-10-24: a wide table —
8
+ * many columns or one unbreakable cell — pushed every ancestor's
9
+ * min-content width past its box, and clipping hosts (the chest report
10
+ * modal's reading region, chat bubbles, report cards) left the far columns
11
+ * unreachable).
12
+ *
13
+ * vitest never resolves stylesheets, and the renderer's marked/DOMPurify
14
+ * are lazy dynamic imports, so this pins the moving parts as source
15
+ * contracts in the same idiom as hkMarkdownLang.test.ts:
16
+ *
17
+ * - the renderer overrides `table` to wrap the emitted table in the
18
+ * two-level rail shape (host + scroller), delegating to the base
19
+ * Renderer for the table markup itself;
20
+ * - the stylesheet gives the scroller the horizontal rail with the
21
+ * hidden-native-chrome contract (the 2026-10-01 native-bar rule) and
22
+ * makes the wrap a positioned rail host carrying the vertical margin;
23
+ * - the overlay-scrollbar reconciliation covers the scroller alongside
24
+ * `.hk-md-code`, so the rail gets the shared chrome and detaches with
25
+ * the block.
26
+ */
27
+ const here = dirname(fileURLToPath(import.meta.url));
28
+ const tsx = readFileSync(join(here, "../HkMarkdownRenderer.tsx"), "utf-8");
29
+ const scss = readFileSync(join(here, "../HkMarkdownRenderer.scss"), "utf-8");
30
+
31
+ describe("markdown table rail contract", () => {
32
+ it("the renderer wraps tables in the rail via the base renderer", () => {
33
+ // The override must DELEGATE (base table markup) and then wrap — a
34
+ // replacement that dropped the delegation would lose marked's table
35
+ // rendering; a wrap without the override is the dead-selector shape.
36
+ // The wrap is TWO-level (`.hk-md-table-wrap` host containing the
37
+ // `.hk-md-table-scroll` viewport): the overlay engine anchors its
38
+ // track in the viewport's PARENT, so a single-level wrap would rail
39
+ // against the whole `.hk-markdown-body` instead of the table.
40
+ expect(tsx).toMatch(
41
+ /return\s*`<div class="hk-md-table-wrap"><div class="hk-md-table-scroll">\$\{inner\}<\/div><\/div>`;/,
42
+ );
43
+ expect(tsx).toMatch(
44
+ /RendererCtor\.prototype as \{ table: \(t: unknown\) => string \}\)\.table\.call\(this, token\)/,
45
+ );
46
+ });
47
+
48
+ it("the rail stylesheet keeps native chrome hidden on the scroller", () => {
49
+ expect(scss).toMatch(
50
+ /\.hk-md-table-scroll\s*\{[^}]*overflow-x:\s*auto/,
51
+ );
52
+ expect(scss).toMatch(
53
+ /\.hk-md-table-scroll\s*\{[^}]*scrollbar-width:\s*none/,
54
+ );
55
+ });
56
+
57
+ it("the wrap is a positioned host carrying the table's vertical margin", () => {
58
+ // Host: wraps ONLY the scroller and is positioned, so the rail hugs
59
+ // the table (no guard promotion of `.hk-markdown-body`); the margin
60
+ // lives here because the scroll box never collapses margins with its
61
+ // children — this keeps normal tables' surrounding rhythm identical
62
+ // to the pre-wrap bare table.
63
+ expect(scss).toMatch(
64
+ /\.hk-md-table-wrap\s*\{[^}]*position:\s*relative/,
65
+ );
66
+ expect(scss).toMatch(
67
+ /\.hk-md-table-wrap\s*\{[^}]*margin:\s*0\.75em\s+0/,
68
+ );
69
+ expect(scss).toMatch(
70
+ // `0\s*;` — a bare `margin:\s*0` would also prefix-match the
71
+ // bare-table `margin: 0.75em 0` and false-pass the very double-margin
72
+ // regression this reset exists to prevent.
73
+ /\.hk-markdown \.hk-md-table-wrap table\s*\{[^}]*margin:\s*0\s*;/,
74
+ );
75
+ });
76
+
77
+ it("the overlay reconciliation covers the rail alongside code blocks", () => {
78
+ expect(tsx).toMatch(
79
+ /querySelectorAll<HTMLElement>\("\.hk-md-code, \.hk-md-table-scroll"\)/,
80
+ );
81
+ });
82
+ });
@@ -11,6 +11,22 @@ const componentsDir = here;
11
11
  * Pane-swap contract — the enforcement half of the family's transition
12
12
  * primitives (user audit 2026-10-23).
13
13
  *
14
+ * THE DECISION TABLE (which surface for which class of slot):
15
+ *
16
+ * ┌──────────────────────────────┬───────────────────────────────┐
17
+ * │ the content can UNMOUNT │ HkTransitionContainer (frame │
18
+ * │ when it leaves the stage │ must morph) / HkCrossfade │
19
+ * │ │ (same-box dissolve) │
20
+ * ├──────────────────────────────┼───────────────────────────────┤
21
+ * │ the instance must SURVIVE │ HkPaneHolder (draft stores, │
22
+ * │ (draft store, canvas, sock) │ canvases, sockets) │
23
+ * ├──────────────────────────────┼───────────────────────────────┤
24
+ * │ a WIZARD STEPPER inside a │ HkStepFlow — see its exception│
25
+ * │ modal sheet │ below; it is a sibling with a │
26
+ * │ │ different contract, not a │
27
+ * │ │ rogue pane slot │
28
+ * └──────────────────────────────┴───────────────────────────────┘
29
+ *
14
30
  * THE RULE. A "pane slot" is a container whose visible content is selected
15
31
  * by a state key while the container stays put: tabs, settings sections,
16
32
  * step bodies, view areas. Every such slot must swap through the standard
@@ -113,7 +129,34 @@ describe("pane-swap contract", () => {
113
129
  // wrapped by the standard above or justified here.
114
130
  "HkTabs.tsx", // wrapped by HkTransitionContainer (pinned above)
115
131
  "HkSettingsDialog.tsx", // wrapped by HkTransitionContainer (pinned above)
116
- "HkStepFlow.tsx", // justified: frozen-sheet two-window protocol + direction axis
132
+ // HkStepFlow is NOT a rogue pane slot — it is a sibling mechanism
133
+ // with a contract the generic primitives deliberately do not
134
+ // offer (2026-10-24 deep-dive ruling; do not re-litigate without
135
+ // reading this):
136
+ // 1. SEQUENTIAL two-window choreography (slide with the hosting
137
+ // sheet's height FROZEN, then the morph plays ALONE) — the
138
+ // inversion of HkTransitionContainer's concurrent morph, born
139
+ // from the 2026-09-23 round-16 user directive after the
140
+ // concurrent form overloaded phone GPU raster threads
141
+ // (whole-region black flashes no layering fix absorbed).
142
+ // 2. A direction axis (translateX travel + RTL mirroring) that
143
+ // HkCrossfade's contract forbids — its tests pin
144
+ // opacity-only, no transform, ever.
145
+ // 3. The hk-stepflow-swap sheet protocol: HkModal freezes its
146
+ // height pin on "swap", safety-unfreezes (ms+650), and
147
+ // height-morphs alone on "morph".
148
+ // 4. Six round-numbered field incidents live in its logic:
149
+ // r16 align-self:start (grid stretch zeroed every shrink's
150
+ // delta), r17 near-instant clip sweep (moving clip edges
151
+ // re-raster per frame), r18 announce-AFTER-patch (a shrink
152
+ // read the leaving body's geometry and no-opped), r22
153
+ // Android-autofill strobe (dropFlowFocus before the swap),
154
+ // r24 sticky-pin vs modal gutter-cover layering, and
155
+ // preemptSwap keeping whichever body is actually visible
156
+ // (opacity > 0.05) when interrupted mid-slide.
157
+ // Merging any of this into the generic container would change its
158
+ // semantics for every existing call site. Permanent exception.
159
+ "HkStepFlow.tsx",
117
160
  "HkAdminShell.tsx", // content slot is host-owned; hosts key it (chest wraps HkCrossfade)
118
161
  "HkThemePanelContent.tsx", // forwards to HTabs (HkTabs), inherits the mechanism
119
162
  "HkMenuPanel.tsx", // menu items, not a pane swap