@celestia-island/hikari 0.55.280 → 0.55.281

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.280",
3
+ "version": "0.55.281",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -0,0 +1,97 @@
1
+ @use "../styles/polyfill" as *;
2
+
3
+ /* HkModelCard — the dense model card for tier-grouped model grids: a
4
+ leading identity lane (the consumer's model tag), an optional actions
5
+ lane (the serving switch), an optional caption (which provider a
6
+ merged card came from) and an optional footer (usage + capability
7
+ hints). The tier rides the card as an inset leading stripe; disabled
8
+ dims without killing the controls inside.
9
+
10
+ Motion contract: hover feedback is BORDER COLOR ONLY. The card never
11
+ transforms — a hover lift displaces nothing, reflows nothing, and
12
+ (2026-11-04 user direction) never animates the card upward. */
13
+
14
+ .hk-model-card {
15
+ --hk-model-card-accent: transparent;
16
+
17
+ display: flex;
18
+ flex-direction: column;
19
+ gap: var(--space-4);
20
+ padding: var(--space-6) var(--space-8);
21
+ border: 1px solid var(--border-subtle);
22
+ border-radius: var(--radius-md);
23
+ background: rgb(var(--color-muted) / 4%);
24
+ min-width: 0;
25
+ box-shadow: inset 2px 0 0 0 var(--hk-model-card-accent);
26
+ }
27
+
28
+ .hk-model-card[data-tier="deep"] {
29
+ --hk-model-card-accent: rgb(var(--color-primary));
30
+ }
31
+
32
+ .hk-model-card[data-tier="normal"] {
33
+ --hk-model-card-accent: rgb(var(--color-success));
34
+ }
35
+
36
+ .hk-model-card[data-tier="basic"] {
37
+ --hk-model-card-accent: rgb(var(--color-muted) / 45%);
38
+ }
39
+
40
+ .hk-model-card[data-hoverable] {
41
+ transition: border-color var(--duration-normal) ease;
42
+ }
43
+
44
+ .hk-model-card[data-hoverable]:hover {
45
+ border-color: rgb(var(--color-muted) / 30%);
46
+ }
47
+
48
+ .hk-model-card[data-interactive] {
49
+ cursor: pointer;
50
+ @include user-select(none);
51
+ }
52
+
53
+ .hk-model-card[data-interactive]:focus-visible {
54
+ outline: 2px solid rgb(var(--color-primary));
55
+ outline-offset: 2px;
56
+ }
57
+
58
+ .hk-model-card[data-disabled] {
59
+ opacity: 0.5;
60
+ --hk-model-card-accent: transparent;
61
+ }
62
+
63
+ .hk-model-card-top {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: space-between;
67
+ gap: var(--space-6);
68
+ min-width: 0;
69
+ }
70
+
71
+ .hk-model-card-leading {
72
+ min-width: 0;
73
+ }
74
+
75
+ .hk-model-card-actions {
76
+ display: inline-flex;
77
+ align-items: center;
78
+ gap: var(--space-2);
79
+ flex-shrink: 0;
80
+ }
81
+
82
+ .hk-model-card-caption {
83
+ font-size: var(--text-2xs);
84
+ color: rgb(var(--color-muted));
85
+ overflow: hidden;
86
+ text-overflow: ellipsis;
87
+ white-space: nowrap;
88
+ }
89
+
90
+ .hk-model-card-footer {
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: space-between;
94
+ gap: var(--space-6);
95
+ min-width: 0;
96
+ color: rgb(var(--color-muted));
97
+ }
@@ -0,0 +1,142 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { createApp, defineComponent, h, ref } from "vue";
3
+
4
+ import HkModelCard from "./HkModelCard";
5
+
6
+ /**
7
+ * HkModelCard anatomy + state guards.
8
+ *
9
+ * (Repo test convention: raw createApp + document queries, no
10
+ * @vue/test-utils dependency.)
11
+ */
12
+
13
+ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
14
+
15
+ function mountCard(
16
+ props: Record<string, unknown> = {},
17
+ slots: Record<string, () => unknown> = {},
18
+ ): HTMLElement {
19
+ const container = document.createElement("div");
20
+ document.body.appendChild(container);
21
+ const Wrapper = defineComponent({
22
+ setup() {
23
+ return () => h(HkModelCard, props, slots);
24
+ },
25
+ });
26
+ const app = createApp(Wrapper);
27
+ app.mount(container);
28
+ mounts.push({ app, container });
29
+ const root = container.querySelector<HTMLElement>(".hk-model-card");
30
+ expect(root, ".hk-model-card renders").toBeTruthy();
31
+ return root!;
32
+ }
33
+
34
+ afterEach(() => {
35
+ for (const { app, container } of mounts) {
36
+ app.unmount();
37
+ container.remove();
38
+ }
39
+ mounts.length = 0;
40
+ });
41
+
42
+ describe("HkModelCard anatomy", () => {
43
+ it("renders the top lane with title and actions content", () => {
44
+ const root = mountCard(
45
+ {},
46
+ {
47
+ title: () => h("span", { class: "t" }, "GLM 5.3"),
48
+ actions: () => h("button", { class: "a" }, "on"),
49
+ },
50
+ );
51
+ expect(root.querySelector(".hk-model-card-top")).toBeTruthy();
52
+ expect(root.querySelector(".hk-model-card-leading .t")?.textContent).toBe("GLM 5.3");
53
+ expect(root.querySelector(".hk-model-card-actions .a")).toBeTruthy();
54
+ });
55
+
56
+ it("collapses caption and footer when their slots are empty", () => {
57
+ const root = mountCard({}, { title: () => h("span", "m") });
58
+ expect(root.querySelector(".hk-model-card-caption")).toBeNull();
59
+ expect(root.querySelector(".hk-model-card-footer")).toBeNull();
60
+ });
61
+
62
+ it("renders caption and footer lanes when they carry content", () => {
63
+ const root = mountCard(
64
+ {},
65
+ {
66
+ title: () => h("span", "m"),
67
+ caption: () => "zai",
68
+ footer: () => h("span", { class: "f" }, "200.0k"),
69
+ },
70
+ );
71
+ expect(root.querySelector(".hk-model-card-caption")?.textContent).toBe("zai");
72
+ expect(root.querySelector(".hk-model-card-footer .f")?.textContent).toBe("200.0k");
73
+ });
74
+
75
+ it("treats a comment-placeholder slot (consumer v-if) as empty", () => {
76
+ const show = ref(false);
77
+ const root = mountCard(
78
+ {},
79
+ {
80
+ title: () => h("span", "m"),
81
+ caption: () => (show.value ? h("span", "shown") : null),
82
+ },
83
+ );
84
+ // A null-render slot produces only the comment placeholder — the
85
+ // caption lane must collapse, not render an empty row.
86
+ expect(root.querySelector(".hk-model-card-caption")).toBeNull();
87
+ });
88
+
89
+ it("treats a fragment of dead v-if comments as empty (the JSX <>{cond && …}</> shape)", () => {
90
+ const show = ref(false);
91
+ const slotsOf = () => ({
92
+ title: () => h("span", "m"),
93
+ // The compiled shape of a consumer's conditional footer is a
94
+ // Fragment vnode whose children are dead comment placeholders —
95
+ // the guard must recurse into it, not stop at the Fragment.
96
+ footer: () => (<>{show.value && <span class="live" />}</>),
97
+ });
98
+ // All-dead fragment → the footer lane collapses.
99
+ expect(mountCard({}, slotsOf()).querySelector(".hk-model-card-footer")).toBeNull();
100
+ // A fragment with live content still renders its lane.
101
+ show.value = true;
102
+ const live = mountCard({}, slotsOf());
103
+ expect(live.querySelector(".hk-model-card-footer .live")?.textContent).toBe("");
104
+ });
105
+ });
106
+
107
+ describe("HkModelCard state attributes", () => {
108
+ it("lands the tier as data-tier", () => {
109
+ expect(mountCard({ tier: "deep" }, {}).dataset.tier).toBe("deep");
110
+ expect(mountCard({ tier: "normal" }, {}).dataset.tier).toBe("normal");
111
+ expect(mountCard({ tier: "basic" }, {}).dataset.tier).toBe("basic");
112
+ });
113
+
114
+ it("an absent tier lands no data-tier at all", () => {
115
+ expect(mountCard({}, {}).hasAttribute("data-tier")).toBe(false);
116
+ });
117
+
118
+ it("hoverable is the default and drops out when disabled", () => {
119
+ expect(mountCard({}, {}).hasAttribute("data-hoverable")).toBe(true);
120
+ expect(mountCard({ disabled: true }, {}).hasAttribute("data-hoverable")).toBe(false);
121
+ expect(mountCard({ hoverable: false }, {}).hasAttribute("data-hoverable")).toBe(false);
122
+ });
123
+
124
+ it("disabled lands the dim marker, interactive lands its marker", () => {
125
+ expect(mountCard({ disabled: true }, {}).hasAttribute("data-disabled")).toBe(true);
126
+ expect(mountCard({ interactive: true }, {}).hasAttribute("data-interactive")).toBe(true);
127
+ // interactive + disabled: the dim wins, the clickability marker goes.
128
+ expect(mountCard({ interactive: true, disabled: true }, {}).hasAttribute("data-interactive")).toBe(false);
129
+ });
130
+ });
131
+
132
+ describe("HkModelCard interaction", () => {
133
+ it("emits click with the native event", () => {
134
+ let seen: MouseEvent | null = null;
135
+ const root = mountCard(
136
+ { onClick: (e: MouseEvent) => (seen = e) },
137
+ { title: () => h("span", "m") },
138
+ );
139
+ root.querySelector<HTMLElement>(".hk-model-card-leading")!.click();
140
+ expect(seen).toBeInstanceOf(MouseEvent);
141
+ });
142
+ });
@@ -0,0 +1,80 @@
1
+ import { defineComponent, type PropType } from "vue";
2
+ import { Comment, Fragment, type VNode } from "vue";
3
+
4
+ import "./HkModelCard.scss";
5
+
6
+ /** The capability tier a model card belongs to — the family's shared
7
+ * vocabulary (arona serves it on every roster row, the consumers group
8
+ * by it). The tier only drives the inset accent stripe; the card never
9
+ * interprets it beyond color. */
10
+ export type ModelCardTier = "deep" | "normal" | "basic";
11
+
12
+ /** Whether a slot actually rendered anything. Testing the slot function
13
+ * for truthiness is not enough: a consumer's `v-if` slot renders a
14
+ * comment placeholder vnode, and a consumer's `<>{cond && …}</>`
15
+ * fragment compiles to a Fragment vnode carrying those same dead
16
+ * comments — both must read as empty (same guard as HkCardList's
17
+ * renderedRows, recursed into fragments). */
18
+ function hasRenderedContent(nodes: unknown): boolean {
19
+ if (nodes == null || nodes === false || nodes === "") return false;
20
+ if (Array.isArray(nodes)) return nodes.some((n) => hasRenderedContent(n));
21
+ if (typeof nodes === "object") {
22
+ const node = nodes as VNode;
23
+ if (node.type === Fragment) return hasRenderedContent(node.children);
24
+ return node.type !== Comment;
25
+ }
26
+ return true; // non-empty text content
27
+ }
28
+
29
+ export default defineComponent({
30
+ name: "HkModelCard",
31
+ props: {
32
+ /** Capability tier accent (inset leading stripe). Absent = no stripe. */
33
+ tier: { type: String as PropType<ModelCardTier>, default: undefined },
34
+ /** Dimmed, accent-free, hover-inert presentation for a switched-off
35
+ * model. Deliberately NOT pointer-events: none — the row's controls
36
+ * (the serving switch) stay operable exactly to un-disable it. */
37
+ disabled: { type: Boolean, default: false },
38
+ /** Border-emphasis hover feedback. The house standard is color only —
39
+ * the card never transforms, so hovering never displaces neighbors
40
+ * or reflows the grid (2026-11-04 user direction). */
41
+ hoverable: { type: Boolean, default: true },
42
+ /** Clickable-card presentation (cursor + focus ring) for surfaces
43
+ * that cycle state on click. Pure presentation — the consumer owns
44
+ * the actual click semantics and a11y wiring. */
45
+ interactive: { type: Boolean, default: false },
46
+ },
47
+ emits: {
48
+ click: (_e: MouseEvent) => true,
49
+ },
50
+ setup(props, { emit, slots }) {
51
+ return () => {
52
+ const caption = slots.caption?.();
53
+ const footer = slots.footer?.();
54
+ const actions = slots.actions?.();
55
+ return (
56
+ <div
57
+ class="hk-model-card"
58
+ data-tier={props.tier}
59
+ data-disabled={props.disabled || undefined}
60
+ data-hoverable={props.hoverable && !props.disabled ? "" : undefined}
61
+ data-interactive={props.interactive && !props.disabled ? "" : undefined}
62
+ onClick={(e: MouseEvent) => emit("click", e)}
63
+ >
64
+ <div class="hk-model-card-top">
65
+ <div class="hk-model-card-leading">{slots.title?.()}</div>
66
+ {hasRenderedContent(actions) && (
67
+ <div class="hk-model-card-actions">{actions}</div>
68
+ )}
69
+ </div>
70
+ {hasRenderedContent(caption) && (
71
+ <div class="hk-model-card-caption">{caption}</div>
72
+ )}
73
+ {hasRenderedContent(footer) && (
74
+ <div class="hk-model-card-footer">{footer}</div>
75
+ )}
76
+ </div>
77
+ );
78
+ };
79
+ },
80
+ });
package/src/index.ts CHANGED
@@ -817,6 +817,7 @@ export { default as HkMenuActionItem } from "./components/HkMenuActionItem";
817
817
  export { default as HkMenuIdentityItem } from "./components/HkMenuIdentityItem";
818
818
  export { default as HkMenuSection } from "./components/HkMenuSection";
819
819
  export { default as HkTagDotRow } from "./components/HkTagDotRow";
820
+ export { default as HkModelCard, type ModelCardTier } from "./components/HkModelCard";
820
821
  export { default as HkRowActions } from "./components/HkRowActions";
821
822
  export { default as HkPopupSelect } from "./components/HkPopupSelect";
822
823
  export { default as HkProgressBar } from "./components/HkProgressBar";