@celestia-island/hikari 0.55.186 → 0.55.188

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.186",
3
+ "version": "0.55.188",
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",
@@ -9,7 +9,10 @@
9
9
  min-width: 0;
10
10
  }
11
11
 
12
- .hk-header-picker-trigger {
12
+ /* Double-class on purpose: the ghost chrome must beat the base
13
+ * `.hk-select-trigger` field chrome regardless of stylesheet import
14
+ * order (both would otherwise sit at one class of specificity). */
15
+ .hk-select-trigger.hk-header-picker-trigger {
13
16
  width: auto;
14
17
  /* Header-band compact: the button group beside it runs ~28px; the
15
18
  * trigger stays a touch taller for its label but must not drive the
@@ -20,6 +23,28 @@
20
23
  font-size: var(--text-sm);
21
24
  font-weight: 600;
22
25
  max-width: 16rem;
26
+ /* Ghost chrome (2026-10-18 user direction: the top-centre dropdown
27
+ * must read like the bottom-centre platform pill — no field border,
28
+ * hover tint only). Mirrors HkButton's ghost variant. */
29
+ background: transparent;
30
+ border-color: transparent;
31
+ }
32
+
33
+ .hk-select-trigger.hk-header-picker-trigger:hover:not([data-disabled]) {
34
+ background: var(--c-primary-light);
35
+ border-color: transparent;
36
+ }
37
+
38
+ .hk-select-trigger.hk-header-picker-trigger:active:not([data-disabled]) {
39
+ filter: brightness(0.95);
40
+ }
41
+
42
+ /* Keyboard / open ring survives — the ring rides the existing
43
+ * --shadow-focus; only the field border stays gone. */
44
+ .hk-select-trigger.hk-header-picker-trigger:focus-visible,
45
+ .hk-select-trigger.hk-header-picker-trigger[data-state="open"] {
46
+ background: var(--c-primary-light);
47
+ border-color: rgb(var(--color-focused-border));
23
48
  }
24
49
 
25
50
  .hk-header-picker-trigger .hk-select-value {
@@ -1,8 +1,13 @@
1
1
  import { afterEach, describe, expect, it, vi } from "vitest";
2
2
  import { createApp, defineComponent, h, ref } from "vue";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
 
4
7
  import HkHeaderPicker, { type HkPickerOption } from "./HkHeaderPicker";
5
8
 
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+
6
11
  /**
7
12
  * HkHeaderPicker — the header-centre selector (the button group's
8
13
  * sibling). These pins hold the standardized contract: trigger chrome,
@@ -122,13 +127,30 @@ describe("HkHeaderPicker", () => {
122
127
  expect(document.querySelectorAll('.hk-draggable-list-handle[data-hidden]').length).toBe(1);
123
128
  });
124
129
 
125
- it("declares the placement prop with the top-down default (mirror-of-bottom contract)", () => {
130
+ it("declares the placement prop with the centered-down default (mirror-of-footer contract)", () => {
126
131
  // The panel is teleported to body, so the through-wiring is pinned at
127
132
  // the contract level: the prop exists, defaults to the header's
128
- // downward growth, and the render passes it straight through
133
+ // downward growth CENTERED on the trigger (2026-10-18 user
134
+ // direction — the footer platform pill's symmetric pop, mirrored
135
+ // up top), and the render passes it straight through
129
136
  // (`placement={props.placement}` in HkHeaderPicker.tsx).
130
137
  expect(HkHeaderPicker.props!.placement).toBeDefined();
131
- expect((HkHeaderPicker.props!.placement as { default: string }).default).toBe("bottom-start");
138
+ expect((HkHeaderPicker.props!.placement as { default: string }).default).toBe("bottom-center");
139
+ });
140
+
141
+ it("keeps the popout content-sized and the trigger chrome ghost", () => {
142
+ // 2026-10-18 user direction: the header-centre dropdown must read
143
+ // like the bottom-centre platform pill — no field border, hover
144
+ // tint only, and a compact centered menu (not a stretched field
145
+ // panel). Pinned at source level like the popout-width contract.
146
+ const tsx = readFileSync(join(here, "HkHeaderPicker.tsx"), "utf-8");
147
+ expect(tsx).toContain("matchAnchorWidth={false}");
148
+ const scss = readFileSync(join(here, "HkHeaderPicker.scss"), "utf-8");
149
+ // The ghost override must be double-classed to beat the base
150
+ // `.hk-select-trigger` chrome regardless of import order.
151
+ expect(scss).toContain(".hk-select-trigger.hk-header-picker-trigger");
152
+ expect(scss).toContain("border-color: transparent;");
153
+ expect(scss).toContain("background: var(--c-primary-light);");
132
154
  });
133
155
 
134
156
  it("disables the trigger when disabled", () => {
@@ -75,10 +75,15 @@ export default defineComponent({
75
75
  /** Desktop-popout side relative to the trigger. The header sits at
76
76
  * the viewport's top, so the popout grows DOWNWARD (bottom-*) —
77
77
  * the mirror of bottom-pinned pickers; auto-flip still rescues a
78
- * viewport that cannot fit it. */
78
+ * viewport that cannot fit it. CENTERED on the trigger by default
79
+ * (2026-10-18 user direction): the header pill mirrors the
80
+ * footer's platform pill, whose menu pops symmetrically on the
81
+ * trigger's midpoint — a start-anchored menu reads lopsided off a
82
+ * narrow pill. Callers with edge-pinned triggers may still pin a
83
+ * side explicitly. */
79
84
  placement: {
80
85
  type: String as PropType<SelectPanelPlacement>,
81
- default: "bottom-start",
86
+ default: "bottom-center",
82
87
  },
83
88
  /** Accessible name of the trigger itself. */
84
89
  ariaLabel: { type: String, default: undefined },
@@ -302,6 +307,10 @@ export default defineComponent({
302
307
  title={props.panelTitle ?? ""}
303
308
  placement={props.placement}
304
309
  offset={4}
310
+ // The popout hugs its rows (not the trigger's width): the
311
+ // header pill is narrow, and the mirror-of-the-footer look
312
+ // means a compact centered menu, not a stretched field panel.
313
+ matchAnchorWidth={false}
305
314
  sheetOnMobile
306
315
  onKeydown={onPopoutKeydown}
307
316
  >
@@ -69,6 +69,32 @@ describe("HkRowActions", () => {
69
69
  expect(btn.getAttribute("aria-expanded")).toBe("false");
70
70
  });
71
71
 
72
+ it("menu mode: a leaf row click emits select with the item and closes", async () => {
73
+ const items: HkMenuItem[] = [
74
+ { key: "edit", label: "Edit" },
75
+ { key: "delete", label: "Delete", danger: true },
76
+ ];
77
+ const selected: HkMenuItem[] = [];
78
+ const c = mount(h(HkRowActions, { items, onSelect: (item: HkMenuItem) => selected.push(item) }));
79
+ const btn = c.querySelector("button.hk-icon-button") as HTMLButtonElement;
80
+ btn.click();
81
+ await Promise.resolve();
82
+ await Promise.resolve();
83
+ // The menu rows render through HkSelectPanel's teleported popout.
84
+ const row = Array.from(
85
+ document.body.querySelectorAll<HTMLButtonElement>("button.hk-menu-row"),
86
+ ).find((b) => b.textContent?.trim() === "Edit");
87
+ expect(row).toBeTruthy();
88
+ row!.click();
89
+ await Promise.resolve();
90
+ await Promise.resolve();
91
+ expect(selected).toHaveLength(1);
92
+ expect(selected[0]!.key).toBe("edit");
93
+ expect(selected[0]!.label).toBe("Edit");
94
+ // The leaf path closed the popup inside HkMenu; aria follows.
95
+ expect(btn.getAttribute("aria-expanded")).toBe("false");
96
+ });
97
+
72
98
  it("stops the click from reaching clickable ancestors", () => {
73
99
  const outer = vi.fn();
74
100
  const c = mount(
@@ -89,7 +89,11 @@ export default defineComponent({
89
89
  emit("open", openerEl() ?? (e.currentTarget as HTMLElement));
90
90
  }
91
91
 
92
- function onSelect(item: HkMenuItem) {
92
+ function onSelect(key: string, item: HkMenuItem) {
93
+ // HkMenu's select emit is (key, item) — the key FIRST, matching the
94
+ // HkMenu contract; the consumer-facing select carries the item.
95
+ // The popup is already closed by HkMenu's leaf path (closeAll runs
96
+ // before the emit), so `open` only needs to follow on paper.
93
97
  open.value = false;
94
98
  emit("select", item);
95
99
  }