@celestia-island/hikari 0.55.83 → 0.55.84

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.83",
3
+ "version": "0.55.84",
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",
@@ -82,4 +82,27 @@ describe("HkThemeToggle row lead-cell contract", () => {
82
82
  expect(svgBlock).not.toBeNull();
83
83
  expect(svgBlock![0]).toContain("var(--hk-menu-item-icon-box)");
84
84
  });
85
+
86
+ it("pins the name-suffix mark against the name instead of the trailing edge", () => {
87
+ const css = read("HkThemeToggle.scss");
88
+ // happy-dom has no layout engine, so the "reads as part of the name"
89
+ // claim is pinned as the two declarations that produce it: the name
90
+ // stops stretching (the shared mixin's `flex: 1` would push the mark
91
+ // to the row's far end, next to the trailing affordances) and the
92
+ // suffix eats the leftover width on its leading side. Losing either
93
+ // one silently turns the marker into a trailing icon.
94
+ const nameBlock = css.match(
95
+ /\.s-theme-item-row\[data-name-suffix="slot"\] \.s-theme-item-name\s*\{[^}]*\}/,
96
+ );
97
+ expect(nameBlock).not.toBeNull();
98
+ expect(nameBlock![0]).toContain("flex: 0 1 auto");
99
+ // Long names must WRAP, not push the mark out of the row: the mixin's
100
+ // nowrap is exactly what would clip the marker away at narrow widths.
101
+ expect(nameBlock![0]).toContain("white-space: normal");
102
+
103
+ const suffixBlock = css.match(/\.s-theme-item-name-suffix\s*\{[^}]*\}/);
104
+ expect(suffixBlock).not.toBeNull();
105
+ expect(suffixBlock![0]).toContain("margin-inline-end: auto");
106
+ expect(suffixBlock![0]).toContain("flex-shrink: 0");
107
+ });
85
108
  });
@@ -201,6 +201,36 @@
201
201
  @include mi.label;
202
202
  }
203
203
 
204
+ /* `item-name-suffix` slot — a host mark that must read as PART OF the
205
+ * name, not as a trailing affordance. Layout is what makes that true:
206
+ * the shared label mixin is `flex: 1`, which would stretch the name to
207
+ * the full row and leave the mark floating at the far right, next to the
208
+ * trailing column (and, in the host's cloud/local marker case, next to
209
+ * the edit button — reading as a per-row action). On suffix rows the
210
+ * name therefore shrinks to its content (`flex: 0 1 auto`, keeping the
211
+ * mixin's `min-width: 0` so long names still wrap inside the available
212
+ * box) and the suffix claims the leftover space on its leading side
213
+ * (`margin-inline-end: auto`), pinning the mark directly against the
214
+ * name's trailing edge. `white-space: normal` overrides the mixin's
215
+ * nowrap so a long name WRAPS instead of pushing the mark out of the
216
+ * row — a name that clips its own provenance mark defeats the point.
217
+ * Rows without the slot are untouched (the mixin's behaviour stands). */
218
+ .s-theme-item-row[data-name-suffix="slot"] .s-theme-item-name {
219
+ flex: 0 1 auto;
220
+ white-space: normal;
221
+ }
222
+
223
+ .s-theme-item-name-suffix {
224
+ display: inline-flex;
225
+ align-items: center;
226
+ flex-shrink: 0;
227
+ margin-inline-start: var(--space-4, 0.25rem);
228
+ /* Trailing auto-margin eats the row's leftover width, so the mark sits
229
+ * with the name at the leading edge and the trailing column stays
230
+ * flush right. */
231
+ margin-inline-end: auto;
232
+ }
233
+
204
234
  /* Leading slot glyphs share the spec's fixed icon cell — sized for the
205
235
  * WIDEST lead mark hosts render there, not the generic 16px box: chest's
206
236
  * three-dot palette swatch is 28px (3×8px dots + 2px gaps), and a mark
@@ -269,6 +269,7 @@ describe("HkThemeToggle item slots", () => {
269
269
  function mountSlotted(
270
270
  lead: (scope: { id: string }) => unknown,
271
271
  trail: (scope: { id: string }) => unknown,
272
+ nameSuffix?: (scope: { id: string }) => unknown,
272
273
  ): HTMLElement {
273
274
  const container = document.createElement("div");
274
275
  document.body.appendChild(container);
@@ -279,6 +280,9 @@ describe("HkThemeToggle item slots", () => {
279
280
  }, {
280
281
  "item-leading": lead,
281
282
  "item-trailing": trail,
283
+ // Only provided when the case exercises it: the slot's ABSENCE is
284
+ // itself a contract (rows must stay exactly as before).
285
+ ...(nameSuffix ? { "item-name-suffix": nameSuffix } : {}),
282
286
  }),
283
287
  });
284
288
  app.mount(container);
@@ -342,6 +346,58 @@ describe("HkThemeToggle item slots", () => {
342
346
  expect(customRows).toBe(2);
343
347
  });
344
348
 
349
+ it("renders the name-suffix cell INSIDE the row button, right after the name", async () => {
350
+ // The whole point of the slot (vs. reusing item-trailing) is that the
351
+ // mark reads as part of the NAME. That is a DOM-position claim, so it
352
+ // is asserted structurally: inside `.s-theme-item-btn`, in the same
353
+ // row, and immediately after `.s-theme-item-name` — not merely
354
+ // "somewhere in the row".
355
+ useTheme().addCustomTheme(anyPresetTokens());
356
+ const container = mountSlotted(
357
+ () => null,
358
+ () => null,
359
+ (scope) => h("i", { class: "suffix-mark", "data-id": scope.id }),
360
+ );
361
+ await settle();
362
+ openMenu(container);
363
+ await settle();
364
+
365
+ const rows = [...document.body.querySelectorAll<HTMLElement>(".s-theme-menu .s-theme-item-row")];
366
+ expect(rows.length).toBeGreaterThanOrEqual(2);
367
+ for (const row of rows) {
368
+ expect(row.getAttribute("data-name-suffix")).toBe("slot");
369
+ const cell = row.querySelector<HTMLElement>(".s-theme-item-name-suffix .suffix-mark");
370
+ expect(cell).toBeTruthy();
371
+ expect(row.querySelectorAll(".suffix-mark")).toHaveLength(1);
372
+ const name = row.querySelector<HTMLElement>(".s-theme-item-name");
373
+ expect(name).toBeTruthy();
374
+ // Inside the row BUTTON (so a click on the row still selects the
375
+ // theme) and the name's immediate next sibling.
376
+ expect(name!.closest(".s-theme-item-btn")).toBeTruthy();
377
+ expect(cell!.closest(".s-theme-item-name-suffix")!.parentElement).toBe(name!.parentElement);
378
+ expect(name!.nextElementSibling).toBe(cell!.closest(".s-theme-item-name-suffix"));
379
+ }
380
+ });
381
+
382
+ it("leaves suffix rows untouched when the host provides no name-suffix slot", async () => {
383
+ // Absence is a contract: without the slot the row must keep the
384
+ // unmodified DOM (no empty cell, no data attribute), or every
385
+ // existing host gains a phantom cell.
386
+ useTheme().addCustomTheme(anyPresetTokens());
387
+ const container = mountSlotted(() => null, () => null);
388
+ await settle();
389
+ openMenu(container);
390
+ await settle();
391
+
392
+ const rows = [...document.body.querySelectorAll<HTMLElement>(".s-theme-menu .s-theme-item-row")];
393
+ expect(rows.length).toBeGreaterThanOrEqual(2);
394
+ for (const row of rows) {
395
+ expect(row.hasAttribute("data-name-suffix")).toBe(false);
396
+ expect(row.querySelector(".s-theme-item-name-suffix")).toBeNull();
397
+ expect(row.querySelector(".s-theme-item-name")).toBeTruthy();
398
+ }
399
+ });
400
+
345
401
  it("scopes each row with its resolved preset definition", async () => {
346
402
  const presets: Array<unknown> = [];
347
403
  // Two rows are the point: with a single built-in row the >= 2 bound
@@ -45,11 +45,16 @@ export interface ThemeItemScope {
45
45
  * color-mode group and preset/custom theme selection (custom themes are
46
46
  * removable), and opens HColorSchemeDialog to create a new custom scheme.
47
47
  *
48
- * Theme rows are host-customizable through two scoped slots (both render
48
+ * Theme rows are host-customizable through three scoped slots (all render
49
49
  * in the popover AND the mobile bottom sheet — same DOM):
50
50
  * · `item-leading` — replaces the selected-check cell; renders for
51
51
  * every row in the fixed icon cell so names align regardless of the
52
52
  * active row. Scope: `ThemeItemScope` (id/name/isCustom/preset).
53
+ * · `item-name-suffix` — an inline cell rendered INSIDE the row button,
54
+ * immediately after the (catalog-resolved) name, so a host mark reads
55
+ * as part of the name rather than as an affordance. Renders only when
56
+ * the host provides the slot; the cell is `aria-hidden`-free and
57
+ * layout-neutral (does not grow, never pushes the trailing column).
53
58
  * · `item-trailing` — reserves a trailing column on every row and owns
54
59
  * it entirely; the built-in custom-delete overlay is suppressed, so
55
60
  * the host renders delete/edit itself where it wants them.
@@ -301,7 +306,10 @@ export const HkThemeToggle = defineComponent({
301
306
  {allThemeList.value.map((th) => {
302
307
  // Host-customizable row anatomy. `item-leading` replaces the
303
308
  // selected-check cell and renders for EVERY row (fixed icon
304
- // cell, so names stay aligned); `item-trailing` reserves a
309
+ // cell, so names stay aligned); `item-name-suffix` renders
310
+ // INSIDE the row button right after the name, so a host mark
311
+ // (e.g. a provenance/sync glyph) reads as part of the name
312
+ // instead of as an affordance; `item-trailing` reserves a
305
313
  // real trailing column for every row and takes full
306
314
  // ownership of the affordances there — the built-in custom
307
315
  // delete overlay is suppressed in that mode, so the host
@@ -315,6 +323,7 @@ export const HkThemeToggle = defineComponent({
315
323
  preset: presetOf(th.id),
316
324
  };
317
325
  const leadingSlot = slots["item-leading"];
326
+ const nameSuffixSlot = slots["item-name-suffix"];
318
327
  const trailingSlot = slots["item-trailing"];
319
328
  // A preset/custom name may be a message key into hikari's own
320
329
  // catalog (the documented ThemePreset.name convention — hosts
@@ -329,6 +338,7 @@ export const HkThemeToggle = defineComponent({
329
338
  class="s-theme-item-row"
330
339
  data-custom={th.isCustom || undefined}
331
340
  data-trailing={trailingSlot ? "slot" : undefined}
341
+ data-name-suffix={nameSuffixSlot ? "slot" : undefined}
332
342
  >
333
343
  <button
334
344
  type="button"
@@ -344,6 +354,9 @@ export const HkThemeToggle = defineComponent({
344
354
  <span class="hk-menu-item-icon s-theme-item-check"><Check size={14} /></span>
345
355
  ) : null}
346
356
  <span class="s-theme-item-name">{displayName}</span>
357
+ {nameSuffixSlot ? (
358
+ <span class="s-theme-item-name-suffix">{nameSuffixSlot(scope)}</span>
359
+ ) : null}
347
360
  </button>
348
361
  {trailingSlot ? (
349
362
  <span class="s-theme-item-trailing">{trailingSlot(scope)}</span>