@celestia-island/hikari 0.55.288 → 0.55.290

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.288",
3
+ "version": "0.55.290",
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",
@@ -2,9 +2,11 @@
2
2
  //
3
3
  // Compact confirmation sheet — sized like a dropdown popover (24rem) so
4
4
  // short messages do not float in a half-empty 32rem modal. The message
5
- // block centers vertically between the title and the actions; the two
6
- // buttons share the row equally (no right-clustering of tiny sm buttons),
7
- // and on touch-width viewports they grow to the 44px thumb standard.
5
+ // block centers vertically between the title and the actions; the action
6
+ // pair lives in the modal's footer band (2026-11-05 move — the same slot
7
+ // HkMessageBox hosts its pair in) and right-aligns there at its natural
8
+ // width, the two buttons still sharing that row equally via flex; on
9
+ // touch-width viewports they grow to the 44px thumb standard.
8
10
 
9
11
  @use "../styles/polyfill" as *;
10
12
 
@@ -38,9 +40,11 @@
38
40
  min-width: 0;
39
41
  }
40
42
 
41
- /* Touch-width viewports: thumb-friendly actions — full-width halves with
42
- the 44px minimum hit target; the dialog keeps its centered popover
43
- sizing but with slimmer side insets so the buttons get the width. */
43
+ /* Touch-width viewports: keep the 44px thumb target on the pair. The
44
+ modal footer's own mobile lift owns padding/font-size for sm/md
45
+ buttons inside the band (HkModal.scss .hk-modal-footer .hk-btn-md),
46
+ so the old 15px font-size override stayed behind on the move into
47
+ the footer — buttons read like every other lifted footer button. */
44
48
  @media (max-width: 640px) {
45
49
  .hk-confirm-dialog {
46
50
  gap: 1rem;
@@ -48,7 +52,6 @@
48
52
 
49
53
  .hk-confirm-dialog-btn {
50
54
  min-height: 44px;
51
- font-size: 0.9375rem;
52
55
  }
53
56
 
54
57
  .hk-confirm-dialog-actions {
@@ -67,31 +67,38 @@ export default defineComponent({
67
67
  default: () => (
68
68
  <div class="hk-confirm-dialog">
69
69
  <p class="hk-confirm-dialog-message">{props.message}</p>
70
- {/* Primary action first, dismiss second — the message-box order
71
- * the confirmation surfaces share (HkMessageBox,
72
- * HkBlockingToast); form-dialog footers keep the primary
73
- * rightmost. It mirrors under `dir="rtl"` because the row only
74
- * reverses visually. */}
75
- <div class="hk-confirm-dialog-actions">
76
- <HButton
77
- class="hk-confirm-dialog-btn"
78
- variant={props.confirmVariant}
79
- size="md"
80
- loading={props.loading}
81
- onClick={onConfirm}
82
- >
83
- {props.confirmLabel || t("hikari::confirmDialog.confirm", "Confirm")}
84
- </HButton>
85
- <HButton
86
- class="hk-confirm-dialog-btn"
87
- variant="secondary"
88
- size="md"
89
- disabled={props.loading}
90
- onClick={onCancel}
91
- >
92
- {props.cancelLabel || t("hikari::confirmDialog.cancel", "Cancel")}
93
- </HButton>
94
- </div>
70
+ </div>
71
+ ),
72
+ /* The confirm pair lives in the modal's standard footer band —
73
+ * it used to render inline under the message while HkMessageBox
74
+ * (the sibling MODAL confirmation surface) already hosted the
75
+ * same pair in this exact slot. HkBlockingToast keeps its own
76
+ * inline pair by design — it is a toast card, not a modal — so
77
+ * the family now has exactly two placements: this footer band
78
+ * for every modal-hosted confirm, and the toast card's inline
79
+ * row. Primary action first, dismiss second — the message-box
80
+ * order; it mirrors under `dir="rtl"` because the row only
81
+ * reverses visually. */
82
+ footer: () => (
83
+ <div class="hk-confirm-dialog-actions">
84
+ <HButton
85
+ class="hk-confirm-dialog-btn"
86
+ variant={props.confirmVariant}
87
+ size="md"
88
+ loading={props.loading}
89
+ onClick={onConfirm}
90
+ >
91
+ {props.confirmLabel || t("hikari::confirmDialog.confirm", "Confirm")}
92
+ </HButton>
93
+ <HButton
94
+ class="hk-confirm-dialog-btn"
95
+ variant="secondary"
96
+ size="md"
97
+ disabled={props.loading}
98
+ onClick={onCancel}
99
+ >
100
+ {props.cancelLabel || t("hikari::confirmDialog.cancel", "Cancel")}
101
+ </HButton>
95
102
  </div>
96
103
  ),
97
104
  }}
@@ -113,3 +113,88 @@ describe("HkJsonTree spacing contract", () => {
113
113
  expect(scss).not.toMatch(/margin-inline-start: 0\.\d+em/);
114
114
  });
115
115
  });
116
+
117
+ /**
118
+ * Selection + fold-motion contract (2026-11-05 user direction).
119
+ *
120
+ * The tree is a viewer, not a document: rows must NOT be selectable by
121
+ * default, and the only re-enable site is the explicit `selectable` opt-in.
122
+ * The fold is animated through the shared motion family — the JS side reads
123
+ * the two `--jt-fold-*` properties, so a theme retime moves the tree with the
124
+ * rest of the system; the chevron is ONE glyph rotated by those same tokens
125
+ * (not a glyph swap) and must honour reduced motion.
126
+ */
127
+ describe("HkJsonTree selection + motion contract", () => {
128
+ const rowBlock = scss.slice(scss.indexOf(".s-jt-row {"), scss.indexOf("/* The ONLY place"));
129
+
130
+ it("denies text selection on rows by default", () => {
131
+ expect(rowBlock, ".s-jt-row rule present").not.toBe("");
132
+ expect(rowBlock).toContain("@include user-select(none)");
133
+ expect(rowBlock).not.toContain("@include user-select(text)");
134
+ // iOS long-press must not raise the callout menu over a row that is
135
+ // itself a toggle target; the opt-in rule is the only place that lifts it.
136
+ const treeBlock = scss.slice(scss.indexOf(".s-tool-json-tree {"), scss.indexOf("/* ONE SCROLLBAR"));
137
+ expect(treeBlock).toContain("-webkit-touch-callout: none;");
138
+ });
139
+
140
+ it("re-enables selection only through the explicit selectable hook", () => {
141
+ expect(scss).toContain(".s-tool-json-tree.s-jt-selectable .s-jt-row {");
142
+ expect(scss).toContain("@include user-select(text)");
143
+ // Exactly one opt-in selector site: a second selector that re-enables
144
+ // selection without the host asking would silently undo the contract.
145
+ expect(scss.match(/s-jt-selectable/g)?.length).toBe(2); // the comment + this selector
146
+ expect(scss.slice(scss.indexOf(".s-tool-json-tree.s-jt-selectable .s-jt-row {"))).toContain(
147
+ "-webkit-touch-callout: default;",
148
+ );
149
+ });
150
+
151
+ it("seeds the fold tokens from the shared motion family", () => {
152
+ expect(scss).toContain(
153
+ "--jt-fold-duration: var(--hi-duration-fast, var(--duration-fast, 0.15s));",
154
+ );
155
+ expect(scss).toContain(
156
+ "--jt-fold-ease: var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));",
157
+ );
158
+ });
159
+
160
+ it("rotates one chevron with the fold tokens instead of swapping glyphs", () => {
161
+ const tsx = readFileSync(join(here, "HkJsonTree.tsx"), "utf-8");
162
+ expect(tsx).toContain('class="s-jt-chevron"');
163
+ expect(tsx).not.toContain("ChevronDown");
164
+ expect(scss).toContain(".s-jt-group[data-open] > .s-jt-row .s-jt-chevron {");
165
+ expect(scss).toContain("transform: rotate(90deg);");
166
+ // Child combinator, not a descendant: an open group must not rotate the
167
+ // chevrons of collapsed grandchildren nested below it.
168
+ const rotateAt = scss.indexOf(".s-jt-group[data-open] > .s-jt-row .s-jt-chevron {");
169
+ expect(rotateAt).toBeGreaterThanOrEqual(0);
170
+ const rotateRule = scss.slice(rotateAt, scss.indexOf("}", rotateAt));
171
+ expect(rotateRule).toContain("rotate(90deg)");
172
+ const chevron = scss.slice(scss.indexOf(".s-jt-chevron {"), scss.indexOf(".s-jt-group[data-open]"));
173
+ expect(chevron).toContain("var(--jt-fold-duration");
174
+ expect(chevron).toContain("var(--jt-fold-ease");
175
+ });
176
+
177
+ it("switches the chevron rotation off under reduced motion", () => {
178
+ // The whole rule must live INSIDE the media block: slicing to EOF would
179
+ // also pass if a later bare `.s-jt-chevron { transition: none }` was
180
+ // added outside it (R2 2026-11-05), which would disable the rotation
181
+ // for everyone instead of just reduced-motion users.
182
+ expect(scss).toMatch(
183
+ /@media \(prefers-reduced-motion: reduce\) \{\s*\.s-jt-chevron \{\s*transition: none;\s*\}\s*\}/,
184
+ );
185
+ });
186
+
187
+ it("keeps the JS fallback duration equal to the sheet fallback", () => {
188
+ const tsx = readFileSync(join(here, "HkJsonTree.tsx"), "utf-8");
189
+ const jsFallback = /const JT_FOLD_FALLBACK_MS = (\d+);/.exec(tsx)?.[1];
190
+ expect(jsFallback).toBe("150");
191
+ expect(scss).toContain(
192
+ "--jt-fold-duration: var(--hi-duration-fast, var(--duration-fast, 0.15s));",
193
+ );
194
+ });
195
+
196
+ it("pins the selectable prop default to off", () => {
197
+ const tsx = readFileSync(join(here, "HkJsonTree.tsx"), "utf-8");
198
+ expect(tsx).toContain("selectable: { type: Boolean, default: false }");
199
+ });
200
+ });