@celestia-island/hikari 0.53.1 → 0.54.0

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.53.1",
3
+ "version": "0.54.0",
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",
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { createApp, h, nextTick } from "vue";
2
+ import { createApp, h, nextTick, ref } from "vue";
3
3
 
4
4
  import { HkThemeToggle } from "./HkThemeToggle";
5
5
  import { themePresets, useTheme, type CustomThemePreset } from "../theme";
@@ -356,3 +356,65 @@ describe("HkThemeToggle item slots", () => {
356
356
  }
357
357
  });
358
358
  });
359
+
360
+ describe("HkThemeToggle exposed closeMenu", () => {
361
+ /** Public surface HkThemeToggle exposes via setup expose() —
362
+ * InstanceType does not carry expose() members, so type it
363
+ * structurally (the consumer-side contract this ships for). */
364
+ interface ThemeTogglePublic {
365
+ closeMenu: () => void;
366
+ }
367
+
368
+ function mountToggleWithRef(): {
369
+ container: HTMLElement;
370
+ toggle: () => ThemeTogglePublic;
371
+ } {
372
+ const container = document.createElement("div");
373
+ document.body.appendChild(container);
374
+ const handle = ref<ThemeTogglePublic | null>(null);
375
+ const app = createApp({
376
+ render: () => h(HkThemeToggle, { ref: handle, externalCustomize: true }),
377
+ });
378
+ app.mount(container);
379
+ mounts.push({ app, container });
380
+ return {
381
+ container,
382
+ toggle: () => {
383
+ const t = handle.value;
384
+ if (!t) throw new Error("HkThemeToggle ref never populated");
385
+ return t;
386
+ },
387
+ };
388
+ }
389
+
390
+ it("closes the open menu through the exposed closeMenu", async () => {
391
+ const { container, toggle } = mountToggleWithRef();
392
+ await settle();
393
+
394
+ openMenu(container);
395
+ await settle();
396
+ expect(document.body.querySelector(".s-theme-menu")).toBeTruthy();
397
+
398
+ // Host-row affordances (e.g. a per-theme edit button) open delegated
399
+ // second-level windows: the desktop popover must be dismissable
400
+ // programmatically — the same teardown path as every internal close.
401
+ toggle().closeMenu();
402
+ await settle();
403
+
404
+ expect(document.body.querySelector(".s-theme-menu")).toBeNull();
405
+ });
406
+
407
+ it("closeMenu is a no-op while the menu is closed", async () => {
408
+ const { container, toggle } = mountToggleWithRef();
409
+ await settle();
410
+
411
+ expect(() => toggle().closeMenu()).not.toThrow();
412
+ await settle();
413
+ expect(document.body.querySelector(".s-theme-menu")).toBeNull();
414
+
415
+ // The menu still opens normally afterwards.
416
+ openMenu(container);
417
+ await settle();
418
+ expect(document.body.querySelector(".s-theme-menu")).toBeTruthy();
419
+ });
420
+ });
@@ -56,6 +56,15 @@ export interface ThemeItemScope {
56
56
  * group (before the divider). Mode-adjacent host chrome that is NOT a
57
57
  * theme-editor concern lives here (e.g. display-scale control).
58
58
  *
59
+ * `closeMenu()` (setup expose) — programmatic close for host content
60
+ * living INSIDE the menu (row slot affordances). A host that opens a
61
+ * delegated second-level window from a row (e.g. a per-theme editor)
62
+ * calls it to dismiss the menu; the menu is ONE state in both its forms
63
+ * (desktop popover, mobile bottom sheet), so this closes whichever is
64
+ * showing — a mobile-aware host decides whether to call it at all, no
65
+ * automatic close fires from here. InstanceType does not carry
66
+ * expose() members — type the ref structurally at the consumer.
67
+ *
59
68
  * Color-mode group: the unified HTabs strip in segmented (radiogroup)
60
69
  * working mode (Auto | Light | Dark) — same pill chrome as every other
61
70
  * group. In AUTO mode the Light/Dark halves merge into the strip's
@@ -84,7 +93,7 @@ export const HkThemeToggle = defineComponent({
84
93
  "update:scheme": (_theme: HCustomTheme) => true,
85
94
  "open-customize": () => true,
86
95
  },
87
- setup(props, { emit, slots }) {
96
+ setup(props, { emit, slots, expose }) {
88
97
  const { t } = useI18n();
89
98
  const { currentTheme, currentMode, effectiveMode, geo, setTheme, setMode, toggleMode, allThemeList, addCustomTheme, removeCustomTheme, customThemes } = useTheme();
90
99
 
@@ -104,6 +113,19 @@ export const HkThemeToggle = defineComponent({
104
113
  const triggerRef = ref<HTMLElement | null>(null);
105
114
  const schemeDialogOpen = ref(false);
106
115
 
116
+ /** Programmatic close for host content inside the menu (row slot
117
+ * affordances): a host opening a delegated second-level window from
118
+ * a theme row calls this to dismiss the menu. The menu is ONE state
119
+ * in both its forms — desktop popover and mobile bottom sheet — so
120
+ * this closes whichever is showing, with no breakpoint branching
121
+ * here; a mobile-aware host decides whether to call it at all.
122
+ * Same teardown as every internal close — the alt-timer cleanup
123
+ * rides the menuOpen watcher. */
124
+ function closeMenu() {
125
+ menuOpen.value = false;
126
+ }
127
+ expose({ closeMenu });
128
+
107
129
  // ── Solar-altitude readout (auto-mode merged cell) ──────────────
108
130
  // `geo` comes from the theme clock (timezone estimate until the real
109
131
  // fix lands) — the engine and this readout share one resolution.
@@ -189,7 +211,7 @@ export const HkThemeToggle = defineComponent({
189
211
 
190
212
  function onSelectTheme(id: ThemeId) {
191
213
  setTheme(id);
192
- menuOpen.value = false;
214
+ closeMenu();
193
215
  }
194
216
 
195
217
  function onConfirmScheme(theme: HCustomTheme) {
@@ -334,7 +356,7 @@ export const HkThemeToggle = defineComponent({
334
356
  type="button"
335
357
  class="s-theme-item-btn s-theme-item-customize"
336
358
  onClick={() => {
337
- menuOpen.value = false;
359
+ closeMenu();
338
360
  if (props.externalCustomize) {
339
361
  emit("open-customize");
340
362
  } else {