@celestia-island/hikari 0.55.192 → 0.55.193

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,9 +1,9 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.192",
3
+ "version": "0.55.193",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
6
+ "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
7
7
  "license": "SySL-1.0",
8
8
  "repository": {
9
9
  "type": "git",
@@ -83,7 +83,7 @@
83
83
  // Padding
84
84
  padding: var(--space-4) var(--space-8);
85
85
  margin: calc(-1 * var(--space-4)) calc(-1 * var(--space-8));
86
- border-radius: vars.$hikari-radius-fui-sm;
86
+ border-radius: vars.$hikari-radius-fine-sm;
87
87
 
88
88
  // Display
89
89
  display: inline-flex;
@@ -215,7 +215,7 @@
215
215
  z-index: var(--hk-z-header);
216
216
 
217
217
  // Appearance
218
- border-radius: vars.$hikari-radius-fui-md;
218
+ border-radius: vars.$hikari-radius-fine-md;
219
219
  border: 1px solid var(--hi-color-border);
220
220
  background: var(--hi-surface);
221
221
  backdrop-filter: blur(4px);
@@ -320,7 +320,7 @@
320
320
  .hk-breadcrumb-item {
321
321
  background: var(--hi-card-bg);
322
322
  padding: 0.375rem 0.75rem;
323
- border-radius: vars.$hikari-radius-fui-sm;
323
+ border-radius: vars.$hikari-radius-fine-sm;
324
324
 
325
325
  &:not(:last-child):hover {
326
326
  background: var(--hi-primary);
@@ -3,7 +3,7 @@
3
3
  @use "./HkIconButtonVars.scss";
4
4
 
5
5
  // ------
6
- // Hikari IconButton Component - FUI Styling
6
+ // Hikari IconButton Component styling
7
7
  // Three-layer CSS variable system:
8
8
  // - Layer1: Foundation variables (foundation.scss)
9
9
  // - Layer2: Component variables (HkIconButtonVars.scss)
@@ -141,6 +141,13 @@
141
141
  matching the blur/select hints' style. */
142
142
  font-style: italic;
143
143
  color: var(--hi-color-text-secondary, #666666);
144
+ /* Italic final strokes lean past the glyph advance; the base rule's
145
+ overflow:hidden shaves them at the padding edge. The cushion is
146
+ clipping room inside the box (HkAffixPicker's data-unset fix), and
147
+ symmetric because this box centers its placeholder — one-sided room
148
+ would read as a small optical shift (same reasoning as the select
149
+ trigger values). */
150
+ padding-inline: 0.2em;
144
151
  }
145
152
 
146
153
  @keyframes hk-pwd-breathe {
@@ -104,6 +104,13 @@
104
104
  white-space: nowrap;
105
105
  overflow: hidden;
106
106
  text-overflow: ellipsis;
107
+ /* Italic overhang cushion (menu-item label rule): the value truncates
108
+ * at the padding edge and an italicized value's trailing stroke leans
109
+ * past the last advance. Symmetric because the trigger centers its
110
+ * value — one-sided room would read as a 0.1em optical shift. The
111
+ * token is declared on menu-item rows, and the trigger sits outside
112
+ * that grammar — hence the fallback. */
113
+ padding-inline: var(--hk-menu-item-label-cushion, 0.2em);
107
114
  }
108
115
 
109
116
  .hk-popup-select-arrow {
@@ -91,6 +91,13 @@
91
91
  white-space: nowrap;
92
92
  overflow: hidden;
93
93
  text-overflow: ellipsis;
94
+ /* Italic overhang cushion (menu-item label rule): the value truncates
95
+ * at the padding edge and an italicized value's trailing stroke leans
96
+ * past the last advance. Symmetric because the trigger centers its
97
+ * value — one-sided room would read as a 0.1em optical shift. The
98
+ * token is declared on menu-item rows, and the trigger sits outside
99
+ * that grammar — hence the fallback. */
100
+ padding-inline: var(--hk-menu-item-label-cushion, 0.2em);
94
101
  }
95
102
 
96
103
  .hk-select-arrow {
@@ -10,7 +10,7 @@
10
10
  padding: 0.35rem 0.45rem;
11
11
  background: rgb(var(--color-background, 20 22 26));
12
12
  border: 1px solid rgb(var(--color-border, 128 128 128) / 15%);
13
- border-radius: vars.$hikari-radius-fui-md;
13
+ border-radius: vars.$hikari-radius-fine-md;
14
14
  box-sizing: border-box;
15
15
  overflow: hidden;
16
16
 
@@ -22,6 +22,6 @@
22
22
 
23
23
  /* The viewport scrollbar inherits the shared chrome shape. */
24
24
  .xterm-viewport {
25
- border-radius: vars.$hikari-radius-fui-sm;
25
+ border-radius: vars.$hikari-radius-fine-sm;
26
26
  }
27
27
  }
@@ -248,6 +248,11 @@
248
248
  .s-theme-item-row[data-name-suffix="slot"] .s-theme-item-name {
249
249
  flex: 0 1 auto;
250
250
  white-space: normal;
251
+ /* The suffix mark is pinned DIRECTLY against the name's trailing edge
252
+ * (the variant's whole point), and this variant wraps instead of
253
+ * truncating — the label mixin's italic overhang cushion would open a
254
+ * dead 0.2em gap ahead of the mark for no clipping room in return. */
255
+ padding-inline-end: 0;
251
256
  }
252
257
 
253
258
  .s-theme-item-name-suffix {
@@ -2,5 +2,5 @@ $hikari-font-family-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
2
2
  // Keep in sync with HIKARI_FONT_MONO in packages/vue/src/theme/fontContext.ts.
3
3
  $hikari-font-family-mono: ui-monospace, 'SF Mono', Menlo, Consolas, 'DejaVu Sans Mono', 'Liberation Mono', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', monospace;
4
4
  $hikari-font-size-sm: 0.875rem;
5
- $hikari-radius-fui-sm: 4px;
6
- $hikari-radius-fui-md: 6px;
5
+ $hikari-radius-fine-sm: 4px;
6
+ $hikari-radius-fine-md: 6px;
@@ -111,4 +111,19 @@ describe("menu-item grammar contract", () => {
111
111
  /\.s-user-header \{[^}]*padding: var\(--space-10\) var\(--space-14\) var\(--space-12\)/,
112
112
  );
113
113
  });
114
+
115
+ it("spends the italic overhang cushion in the label mixin only", () => {
116
+ // Italic final strokes lean past the last glyph's advance and the
117
+ // label's overflow:hidden clips at the padding edge — an italic row
118
+ // label (host-styled sentinel rows) loses its trailing stroke
119
+ // (reported from the field: wowsp's all-clients sentinel row). The
120
+ // cushion is declared once with the grammar tokens and spent by the
121
+ // label mixin only — it is clipping room for the truncation box, not
122
+ // row spacing, so the item/icon mixins must stay cushion-free.
123
+ expect(grammar).toMatch(/--hk-menu-item-label-cushion: 0\.2em/);
124
+ expect(grammar).toMatch(
125
+ /@mixin label[^}]*padding-inline-end: var\(--hk-menu-item-label-cushion\)/,
126
+ );
127
+ expect(grammar.match(/var\(--hk-menu-item-label-cushion\)/g)?.length).toBe(1);
128
+ });
114
129
  });
@@ -50,6 +50,10 @@
50
50
  --hk-menu-item-icon-box: 16px;
51
51
  /* Label optical shift — see header comment. */
52
52
  --hk-menu-item-optical: 0.5px;
53
+ /* Italic overhang cushion — the inline-end clipping room the label
54
+ * spends (see @mixin label). em-based, so it scales with the sheet's
55
+ * larger item font without a sheet-token override. */
56
+ --hk-menu-item-label-cushion: 0.2em;
53
57
  }
54
58
 
55
59
  /* Mobile bottom-sheet geometry. */
@@ -207,6 +211,15 @@
207
211
  white-space: nowrap;
208
212
  position: relative;
209
213
  top: var(--hk-menu-item-optical);
214
+ /* Italic final strokes lean past the last glyph's advance, and the
215
+ * overflow:hidden pair above clips at the padding edge — an italic
216
+ * label (host-styled sentinel rows, emphasized picks) loses its
217
+ * trailing stroke (reported from the field: wowsp's all-clients
218
+ * sentinel row). The cushion is clipping room INSIDE the padding box
219
+ * (HkAffixPicker's `data-unset` fix, generalized to the one row
220
+ * spec): fitting text never gains an ellipsis, overflowing text
221
+ * truncates one cushion earlier. */
222
+ padding-inline-end: var(--hk-menu-item-label-cushion);
210
223
  }
211
224
 
212
225
  /* ── Divider row ──────────────────────────────────────────────────────
@@ -235,3 +235,135 @@ describe("stylesheet descender-clipping contract", () => {
235
235
  expect(unmatched).toEqual([]);
236
236
  });
237
237
  });
238
+
239
+ /**
240
+ * Horizontal twin: the italic-overhang contract.
241
+ *
242
+ * The same `overflow: hidden` that clips descenders vertically also clips
243
+ * ITALIC ink horizontally: synthetic-oblique strokes lean past the last
244
+ * glyph's advance, the truncation box ends at the padding edge, and the
245
+ * trailing stroke is shaved flat (reported from the field: wowsp's italic
246
+ * all-clients sentinel row lost the right edge of its last glyph). The fix
247
+ * is an inline-end cushion — clipping room INSIDE the padding box, which
248
+ * never triggers the ellipsis for fitting text (HkAffixPicker's
249
+ * `data-unset` rule is the in-tree precedent).
250
+ *
251
+ * The scan flags a truncation box (`overflow` clip + `text-overflow:
252
+ * ellipsis` + `white-space: nowrap`, all three in one block) whose element
253
+ * resolves to `font-style: italic` somewhere in the same sheet, unless an
254
+ * inline-end cushion of >= 0.2em also resolves for it. The pinned list
255
+ * below covers the boxes NO in-tree rule italicizes but hosts do — their
256
+ * cushion must be unconditional, because host CSS is invisible here.
257
+ */
258
+
259
+ /** Italic overhang floor: HkAffixPicker's shipped cushion, in em. */
260
+ const SAFE_ITALIC_CUSHION_EM = 0.2;
261
+
262
+ function truncates(body: string): boolean {
263
+ return (
264
+ clipsOverflow(body) &&
265
+ declaration(body, "text-overflow") !== null &&
266
+ /nowrap/i.test(declaration(body, "white-space") ?? "")
267
+ );
268
+ }
269
+
270
+ /** The em figure inside a padding value, var() fallback included. */
271
+ function emCushion(value: string | null): number | null {
272
+ if (value === null) return null;
273
+ // The grammar label's compiled shape is a bare var() with no literal em
274
+ // — its 0.2em value is pinned at the source level by
275
+ // _menu-item.contract.test.ts, so the token counts as the floor it
276
+ // contracts to.
277
+ if (/--hk-menu-item-label-cushion/i.test(value)) return SAFE_ITALIC_CUSHION_EM;
278
+ const match = value.match(/([\d.]+)em/i);
279
+ return match ? Number(match[1]) : null;
280
+ }
281
+
282
+ /**
283
+ * Winning declaration for one element across the sheet, resolving the
284
+ * cascade exactly like `effectiveRatio` does (same-subject blocks, highest
285
+ * specificity then latest order).
286
+ */
287
+ function effectiveDecl(clip: Block, blocks: Block[], property: string): string | null {
288
+ const element = subject(clip.selector);
289
+ const candidates = blocks
290
+ .filter((block) => block.selector === clip.selector || subject(block.selector) === element)
291
+ .map((block) => ({ block, value: declaration(block.body, property) }))
292
+ .filter((entry): entry is { block: Block; value: string } => entry.value !== null);
293
+ if (!candidates.length) return null;
294
+ candidates.sort(
295
+ (a, b) => specificity(b.block.selector) - specificity(a.block.selector) || b.block.order - a.block.order,
296
+ );
297
+ return candidates[0]!.value;
298
+ }
299
+
300
+ describe("stylesheet italic-overhang contract", () => {
301
+ it("never truncates an italic line without the overhang cushion", { timeout: 60_000 }, () => {
302
+ const findings: string[] = [];
303
+ for (const file of sheets()) {
304
+ const name = file.slice(file.indexOf("/src/") + 1);
305
+ const css = compile(file, { style: "expanded", loadPaths: [componentDir, stylesDir] }).css.replace(
306
+ /\/\*[\s\S]*?\*\//g,
307
+ "",
308
+ );
309
+ const blocks = [...blocksOf(css, name)];
310
+ for (const block of blocks) {
311
+ if (!truncates(block.body)) continue;
312
+ if (!/^italic$/i.test(effectiveDecl(block, blocks, "font-style") ?? "")) continue;
313
+ // Either longhand satisfies the contract — centered boxes ship the
314
+ // symmetric `padding-inline` shorthand instead.
315
+ const cushion =
316
+ emCushion(effectiveDecl(block, blocks, "padding-inline-end")) ??
317
+ emCushion(effectiveDecl(block, blocks, "padding-inline"));
318
+ if (cushion !== null && cushion >= SAFE_ITALIC_CUSHION_EM) continue;
319
+ findings.push(
320
+ `${block.file} — ${block.selector} — truncates a line that resolves to italic without a >= ${SAFE_ITALIC_CUSHION_EM}em inline-end cushion: the trailing stroke is shaved`,
321
+ );
322
+ }
323
+ }
324
+ expect(findings.sort(), "italic truncation boxes missing the overhang cushion").toEqual([]);
325
+ });
326
+
327
+ it("keeps the host-italic-exposed truncation boxes cushioned", { timeout: 60_000 }, () => {
328
+ // Host CSS italicizes trigger values from OUTSIDE any hikari block,
329
+ // so no in-tree italic backs these — the scan above cannot see the
330
+ // risk and the cushion must be pinned unconditionally instead.
331
+ const pinned: ReadonlyArray<{ file: string; selector: string; property: string }> = [
332
+ { file: "HkSelect.scss", selector: ".hk-select-value", property: "padding-inline" },
333
+ { file: "HkPopupSelect.scss", selector: ".hk-popup-select-value", property: "padding-inline" },
334
+ // The original precedent this contract generalizes: its italic lives
335
+ // on the [data-unset] subject while the truncation trio sits on the
336
+ // base rule — a shape the scan's subject pairing cannot see, so the
337
+ // shipped cushion is pinned instead.
338
+ { file: "HkAffixPicker.scss", selector: ".hk-affix-tag-text[data-unset]", property: "padding-inline-end" },
339
+ ];
340
+ const compiled = new Map<string, ReturnType<typeof compile>["css"]>();
341
+ const found: string[] = [];
342
+ for (const pin of pinned) {
343
+ const file = resolve(componentDir, pin.file);
344
+ if (!compiled.has(file)) {
345
+ compiled.set(
346
+ file,
347
+ compile(file, { style: "expanded", loadPaths: [componentDir, stylesDir] }).css.replace(
348
+ /\/\*[\s\S]*?\*\//g,
349
+ "",
350
+ ),
351
+ );
352
+ }
353
+ const block = [...blocksOf(compiled.get(file)!, pin.file)].find(
354
+ (entry) => entry.selector === pin.selector,
355
+ );
356
+ if (!block) {
357
+ found.push(`${pin.file} — ${pin.selector} — the pinned rule no longer exists`);
358
+ continue;
359
+ }
360
+ const cushion = emCushion(declaration(block.body, pin.property));
361
+ if (cushion === null || cushion < SAFE_ITALIC_CUSHION_EM) {
362
+ found.push(
363
+ `${pin.file} — ${pin.selector} — lost its ${SAFE_ITALIC_CUSHION_EM}em italic overhang cushion`,
364
+ );
365
+ }
366
+ }
367
+ expect(found.sort(), "pinned italic-cushioned truncation boxes regressed").toEqual([]);
368
+ });
369
+ });