@celestia-island/hikari 0.41.2 → 0.41.5

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.41.2",
3
+ "version": "0.41.5",
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",
@@ -11,6 +11,15 @@
11
11
  margin: 0 0 0.5rem;
12
12
  }
13
13
 
14
+ /* Foot note under the grid — same register as .hk-input-hint so the
15
+ helper line reads identically wherever it appears in a form. */
16
+ .hk-selection-grid-hint {
17
+ margin: var(--space-4, 0.25rem) 0 0;
18
+ font-size: var(--text-xs, 0.75rem);
19
+ color: color-mix(in srgb, rgb(var(--color-muted)) 70%, rgb(var(--color-background)));
20
+ text-align: start;
21
+ }
22
+
14
23
  .hk-selection-grid-grid {
15
24
  display: grid;
16
25
  gap: 0.5rem;
@@ -0,0 +1,119 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { createApp, h } from "vue";
3
+
4
+ import HkSelectionGrid, { type SelectionGridItem } from "./HkSelectionGrid";
5
+
6
+ /**
7
+ * HkSelectionGrid contract tests:
8
+ * - single mode keys the lit state off `selectedId` and lights exactly one card
9
+ * - multi mode keys it off `selectedIds` membership and lights every listed id
10
+ * - multi mode never lets `selectedId` leak through (the two selectors stay
11
+ * independent, so a stale single id cannot double-light a card)
12
+ * - `select` fires for already-selected cards too — the consumer owns the
13
+ * toggle, the grid only reports the click
14
+ * - the optional `hint` renders under the grid and is absent when unset
15
+ *
16
+ * House style: raw createApp mounts on a shared container list torn down
17
+ * after each case; DOM assertions via document queries.
18
+ */
19
+
20
+ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
21
+
22
+ function mount(node: ReturnType<typeof h>) {
23
+ const container = document.createElement("div");
24
+ document.body.appendChild(container);
25
+ const app = createApp({ render: () => node });
26
+ app.mount(container);
27
+ mounts.push({ app, container });
28
+ return container;
29
+ }
30
+
31
+ afterEach(() => {
32
+ for (const { app, container } of mounts.splice(0)) {
33
+ app.unmount();
34
+ container.remove();
35
+ }
36
+ });
37
+
38
+ const ITEMS: SelectionGridItem[] = [
39
+ { id: "text", title: "Text" },
40
+ { id: "vision", title: "Vision" },
41
+ { id: "gen", title: "Generation" },
42
+ ];
43
+
44
+ function cards(container: HTMLElement) {
45
+ return Array.from(container.querySelectorAll<HTMLElement>(".hk-selection-grid-item"));
46
+ }
47
+
48
+ function litIds(container: HTMLElement) {
49
+ return cards(container)
50
+ .filter((el) => el.hasAttribute("data-selected"))
51
+ .map((el) => el.querySelector(".hk-selection-grid-name")?.textContent);
52
+ }
53
+
54
+ describe("HkSelectionGrid single mode", () => {
55
+ it("lights only the card matching selectedId", () => {
56
+ const container = mount(h(HkSelectionGrid, { items: ITEMS, selectedId: "vision" }));
57
+ expect(litIds(container)).toEqual(["Vision"]);
58
+ });
59
+
60
+ it("lights nothing when selectedId is undefined", () => {
61
+ const container = mount(h(HkSelectionGrid, { items: ITEMS }));
62
+ expect(litIds(container)).toEqual([]);
63
+ });
64
+
65
+ it("ignores selectedIds unless multi is on", () => {
66
+ const container = mount(
67
+ h(HkSelectionGrid, { items: ITEMS, selectedIds: ["text", "gen"], selectedId: "vision" }),
68
+ );
69
+ expect(litIds(container)).toEqual(["Vision"]);
70
+ });
71
+ });
72
+
73
+ describe("HkSelectionGrid multi mode", () => {
74
+ it("lights every id listed in selectedIds", () => {
75
+ const container = mount(
76
+ h(HkSelectionGrid, { items: ITEMS, multi: true, selectedIds: ["text", "gen"] }),
77
+ );
78
+ expect(litIds(container)).toEqual(["Text", "Generation"]);
79
+ });
80
+
81
+ it("lights nothing when selectedIds is empty", () => {
82
+ const container = mount(h(HkSelectionGrid, { items: ITEMS, multi: true }));
83
+ expect(litIds(container)).toEqual([]);
84
+ });
85
+
86
+ it("does not read selectedId while multi is on", () => {
87
+ const container = mount(
88
+ h(HkSelectionGrid, { items: ITEMS, multi: true, selectedIds: ["gen"], selectedId: "text" }),
89
+ );
90
+ expect(litIds(container)).toEqual(["Generation"]);
91
+ });
92
+
93
+ it("emits select for an already-selected card so the consumer can toggle it off", () => {
94
+ const seen: string[] = [];
95
+ const container = mount(
96
+ h(HkSelectionGrid, {
97
+ items: ITEMS,
98
+ multi: true,
99
+ selectedIds: ["vision"],
100
+ onSelect: (item: SelectionGridItem) => seen.push(item.id),
101
+ }),
102
+ );
103
+ cards(container)[1]!.click();
104
+ expect(seen).toEqual(["vision"]);
105
+ });
106
+ });
107
+
108
+ describe("HkSelectionGrid hint", () => {
109
+ it("renders the hint line under the grid when provided", () => {
110
+ const container = mount(h(HkSelectionGrid, { items: ITEMS, hint: "Pick one or more" }));
111
+ const hint = container.querySelector(".hk-selection-grid-hint");
112
+ expect(hint?.textContent).toBe("Pick one or more");
113
+ });
114
+
115
+ it("renders no hint node when the prop is unset", () => {
116
+ const container = mount(h(HkSelectionGrid, { items: ITEMS }));
117
+ expect(container.querySelector(".hk-selection-grid-hint")).toBeNull();
118
+ });
119
+ });
@@ -19,8 +19,14 @@ export default defineComponent({
19
19
  props: {
20
20
  items: { type: Array as PropType<SelectionGridItem[]>, required: true },
21
21
  selectedId: { type: String, default: undefined },
22
+ selectedIds: {
23
+ type: Array as PropType<string[]>,
24
+ default: () => [],
25
+ },
26
+ multi: { type: Boolean, default: false },
22
27
  columns: { type: Number as PropType<SelectionGridCols>, default: 2 },
23
28
  groupTitle: { type: String, default: undefined },
29
+ hint: { type: String, default: undefined },
24
30
  dense: { type: Boolean, default: false },
25
31
  },
26
32
  emits: {
@@ -41,7 +47,9 @@ export default defineComponent({
41
47
  data-dense={props.dense || undefined}
42
48
  >
43
49
  {props.items.map((item) => {
44
- const isSelected = props.selectedId === item.id;
50
+ const isSelected = props.multi
51
+ ? props.selectedIds.includes(item.id)
52
+ : props.selectedId === item.id;
45
53
  const ItemIcon = item.icon;
46
54
 
47
55
  return (
@@ -93,6 +101,9 @@ export default defineComponent({
93
101
  );
94
102
  })}
95
103
  </div>
104
+ {props.hint && (
105
+ <p class="hk-selection-grid-hint">{props.hint}</p>
106
+ )}
96
107
  </div>
97
108
  );
98
109
  };