@celestia-island/hikari 0.55.183 → 0.55.184

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.183",
3
+ "version": "0.55.184",
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",
@@ -0,0 +1,98 @@
1
+ /* HkQuote — the Markdown-blockquote-style info block.
2
+ *
3
+ * Two layers: the OUTER box (`.hk-quote`) only positions the block
4
+ * (centered in its host for fit="fill", a capped centered column for
5
+ * fit="page"); the INNER mark (`.hk-quote-mark`) is the blockquote
6
+ * itself — the 3px accent bar on the inline-start edge plus quiet
7
+ * prose, sized to its content like a real Markdown `>` quote. No
8
+ * surface of its own (no background, no radius, no shadow): the bar IS
9
+ * the visual. The tone class only selects the bar color; text colors
10
+ * stay in the standard primary/secondary text slots for every tone. */
11
+
12
+ .hk-quote {
13
+ --hk-quote-bar: var(--hi-color-primary, #7aa2f7);
14
+
15
+ display: flex;
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ .hk-quote--neutral {
20
+ --hk-quote-bar: var(--hi-color-text-tertiary, #8b949e);
21
+ }
22
+
23
+ .hk-quote--danger {
24
+ --hk-quote-bar: var(--hi-color-danger, #f7768e);
25
+ }
26
+
27
+ .hk-quote--warning {
28
+ --hk-quote-bar: var(--hi-color-warning, #eed677);
29
+ }
30
+
31
+ .hk-quote--success {
32
+ --hk-quote-bar: var(--hi-color-success, #0eb840);
33
+ }
34
+
35
+ /* fit="fill": the quote block centers itself inside its host (an empty
36
+ * panel body, a split view) — the host keeps its own size, the quote
37
+ * floats mid-host as an info block. */
38
+ .hk-quote--fill {
39
+ width: 100%;
40
+ height: 100%;
41
+ align-items: center;
42
+ justify-content: center;
43
+ }
44
+
45
+ /* fit="page": a standalone block on a page — capped width, centered
46
+ * horizontally, never spanning the whole column. */
47
+ .hk-quote--page {
48
+ width: 100%;
49
+ margin-inline: auto;
50
+ justify-content: center;
51
+ }
52
+
53
+ @media (min-width: 48rem) {
54
+ .hk-quote--page {
55
+ max-width: clamp(20rem, 30vw, 35rem);
56
+ }
57
+ }
58
+
59
+ .hk-quote-mark {
60
+ display: flex;
61
+ flex-direction: column;
62
+ align-items: flex-start;
63
+ gap: var(--space-8, 0.5rem);
64
+ padding: 0.5rem 0.875rem;
65
+ border-inline-start: 3px solid var(--hk-quote-bar);
66
+ text-align: start;
67
+ max-width: 100%;
68
+ }
69
+
70
+ .hk-quote-body {
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: var(--space-4, 0.25rem);
74
+ max-width: 100%;
75
+ }
76
+
77
+ .hk-quote-title {
78
+ margin: 0;
79
+ font-size: var(--text-base, 0.875rem);
80
+ font-weight: 600;
81
+ line-height: 1.5;
82
+ color: var(--hi-color-text-primary, #1e1e1e);
83
+ }
84
+
85
+ .hk-quote-desc {
86
+ margin: 0;
87
+ font-size: var(--text-sm, 0.8125rem);
88
+ line-height: 1.55;
89
+ color: var(--hi-color-text-secondary, #666666);
90
+ max-width: 36rem;
91
+ }
92
+
93
+ .hk-quote-action {
94
+ display: flex;
95
+ flex-wrap: wrap;
96
+ align-items: center;
97
+ gap: var(--space-8, 0.5rem);
98
+ }
@@ -0,0 +1,131 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { createApp, h, type VNode } from "vue";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import HkQuote from "./HkQuote";
8
+
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+
11
+ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
12
+
13
+ function mount(renderNode: () => VNode) {
14
+ const container = document.createElement("div");
15
+ document.body.appendChild(container);
16
+ const app = createApp({ render: renderNode });
17
+ app.mount(container);
18
+ mounts.push({ app, container });
19
+ return container;
20
+ }
21
+
22
+ afterEach(() => {
23
+ for (const { app, container } of mounts.splice(0)) {
24
+ app.unmount();
25
+ container.remove();
26
+ }
27
+ });
28
+
29
+ /**
30
+ * HkQuote is the Markdown-blockquote grammar as a component: an accent
31
+ * bar on the inline-start edge (tone-colored), quiet prose, NO card
32
+ * chrome — no icon, no background, no boxed surface (user direction
33
+ * 2026-10-18: the no-content/error states must read as a quoted info
34
+ * block with a primary-color left bar, not an icon card).
35
+ */
36
+ describe("HkQuote", () => {
37
+ it("renders the quoted title and description inside a note region", () => {
38
+ const c = mount(() =>
39
+ h(HkQuote, {
40
+ title: "This panel has no content yet",
41
+ description: "Configure the data source, or let the agent generate content.",
42
+ }),
43
+ );
44
+ const root = c.querySelector(".hk-quote")!;
45
+ expect(root.getAttribute("role")).toBe("note");
46
+ expect(c.querySelector(".hk-quote-title")!.textContent).toBe("This panel has no content yet");
47
+ expect(c.querySelector(".hk-quote-desc")!.textContent).toContain("Configure the data source");
48
+ });
49
+
50
+ it("defaults to the primary tone and fill fit; every tone has a class", () => {
51
+ const c = mount(() => h(HkQuote, { title: "t" }));
52
+ const root = c.querySelector(".hk-quote")!;
53
+ expect(root.classList.contains("hk-quote--primary")).toBe(true);
54
+ expect(root.classList.contains("hk-quote--fill")).toBe(true);
55
+
56
+ for (const tone of ["neutral", "danger", "warning", "success"] as const) {
57
+ const toneRoot = mount(() => h(HkQuote, { title: "t", tone })).querySelector(".hk-quote")!;
58
+ expect(toneRoot.classList.contains(`hk-quote--${tone}`)).toBe(true);
59
+ }
60
+ });
61
+
62
+ it("centers itself for fill and renders a width-capped page block for page", () => {
63
+ const fill = mount(() => h(HkQuote, { title: "t" })).querySelector(".hk-quote")!;
64
+ expect(fill.classList.contains("hk-quote--fill")).toBe(true);
65
+ const page = mount(() => h(HkQuote, { title: "t", fit: "page" })).querySelector(".hk-quote")!;
66
+ expect(page.classList.contains("hk-quote--page")).toBe(true);
67
+ });
68
+
69
+ it("renders the action row and the default body slot", () => {
70
+ const c = mount(() =>
71
+ h(HkQuote, { title: "t" }, {
72
+ default: () => h("p", { class: "extra" }, "extra line"),
73
+ action: () => h("button", { type: "button" }, "Edit panel"),
74
+ }),
75
+ );
76
+ expect(c.querySelector(".hk-quote-body .extra")!.textContent).toBe("extra line");
77
+ const action = c.querySelector<HTMLButtonElement>(".hk-quote-action button")!;
78
+ expect(action.textContent).toBe("Edit panel");
79
+ // The action row is a SIBLING of the body inside the mark (pinned:
80
+ // nesting it would halve the desc→action gap to the body's --space-4).
81
+ expect(action.closest(".hk-quote-body")).toBeNull();
82
+ expect(action.closest(".hk-quote-mark")).not.toBeNull();
83
+ });
84
+
85
+ it("passes call-site classes onto the root (no card chrome of its own)", () => {
86
+ const c = mount(() => h(HkQuote, { title: "t", class: "host-skin" }));
87
+ const root = c.querySelector(".hk-quote")!;
88
+ expect(root.classList.contains("host-skin")).toBe(true);
89
+ // The mark is the blockquote itself — the component adds no icon,
90
+ // no boxed surface of its own (see the styles contract below).
91
+ expect(c.querySelector(".hk-empty-icon")).toBeNull();
92
+ expect(c.querySelector("svg")).toBeNull();
93
+ });
94
+
95
+ it("lets the call site override the role (error tones announce as alert)", () => {
96
+ // Consumers rendering an error quote pass role="alert" — Vue's attr
97
+ // fallthrough must win over the rendered default (pinned because the
98
+ // chest error-state story depends on it).
99
+ const c = mount(() => h(HkQuote, { title: "Couldn't load", role: "alert" }));
100
+ expect(c.querySelector(".hk-quote")!.getAttribute("role")).toBe("alert");
101
+ });
102
+ });
103
+
104
+ /** The visual grammar contract: the bar IS the visual. The mark carries
105
+ * the accent bar (border-inline-start, tone variable); it must NOT
106
+ * paint a background, border-radius, or shadow — anything card-like
107
+ * would turn the quote back into the placeholder card the component
108
+ * exists to replace. */
109
+ describe("HkQuote styles contract", () => {
110
+ const scss = readFileSync(join(here, "HkQuote.scss"), "utf8");
111
+
112
+ it("draws the accent bar with the tone variable on the mark", () => {
113
+ expect(scss).toMatch(/\.hk-quote-mark[\s\S]*?border-inline-start:\s*3px solid var\(--hk-quote-bar\)/);
114
+ expect(scss).toMatch(/--hk-quote-bar:\s*var\(--hi-color-primary/);
115
+ });
116
+
117
+ it("maps every tone class onto a house color token", () => {
118
+ expect(scss).toMatch(/hk-quote--neutral[\s\S]*?--hi-color-text-tertiary/);
119
+ expect(scss).toMatch(/hk-quote--danger[\s\S]*?--hi-color-danger/);
120
+ expect(scss).toMatch(/hk-quote--warning[\s\S]*?--hi-color-warning/);
121
+ expect(scss).toMatch(/hk-quote--success[\s\S]*?--hi-color-success/);
122
+ });
123
+
124
+ it("ships no card chrome on the mark (no background / radius / shadow)", () => {
125
+ const markBlock = scss.match(/\.hk-quote-mark \{[\s\S]*?\n\}/)?.[0] ?? "";
126
+ expect(markBlock, "the .hk-quote-mark rule exists").toMatch(/^\./);
127
+ expect(markBlock).not.toMatch(/background/);
128
+ expect(markBlock).not.toMatch(/border-radius/);
129
+ expect(markBlock).not.toMatch(/box-shadow/);
130
+ });
131
+ });
@@ -0,0 +1,85 @@
1
+ import { defineComponent, type PropType } from "vue";
2
+
3
+ import "./HkQuote.scss";
4
+
5
+ /** The tone grammar of the quote's accent bar. */
6
+ export type HkQuoteTone = "primary" | "neutral" | "danger" | "warning" | "success";
7
+
8
+ /**
9
+ * HkQuote — the Markdown-blockquote-style info block.
10
+ *
11
+ * The house grammar for "this surface has something to SAY, not
12
+ * something to show": a vertical accent bar in the tone color on the
13
+ * inline-start edge, the message set as quoted prose beside it — the
14
+ * same visual language a Markdown `>` blockquote renders with, promoted
15
+ * to a first-class component with tone and centering. The bar hugs the
16
+ * prose (the block sizes to its content, like a real blockquote); the
17
+ * OUTER box centers that block inside the host.
18
+ *
19
+ * Deliberately NOT an empty-state card: there is no icon, no boxed
20
+ * surface, no chrome of its own — hosts that want the message centered
21
+ * in an otherwise empty body pass `fit="fill"` (the block centers
22
+ * itself in the host) or `fit="page"` (a standalone, width-capped,
23
+ * horizontally centered block). The optional `action` slot renders a
24
+ * quiet row of buttons under the prose (e.g. "Edit panel", "Retry") —
25
+ * affordances belong to the host's story, never chrome bolted onto the
26
+ * quote.
27
+ *
28
+ * ```tsx
29
+ * // A panel that was never generated: quote it, don't card it.
30
+ * <HkQuote tone="primary" fit="fill"
31
+ * title={t("viewPanel.empty.panelNoContent")}
32
+ * description={t("viewPanel.empty.panelNoContentDesc")}>
33
+ * {{ action: () => <HkButton size="sm" onClick={edit}>Edit panel</HkButton> }}
34
+ * </HkQuote>
35
+ * ```
36
+ *
37
+ * A11y: the block is a `role="note"` region by default; pass
38
+ * `role="alert"` (or any other attrs) from the call site for error
39
+ * tones — attrs land on the root element.
40
+ */
41
+ export default defineComponent({
42
+ name: "HkQuote",
43
+ props: {
44
+ /** Lead line — the quoted statement itself. Bold, primary text. */
45
+ title: { type: String, default: undefined },
46
+ /** Follow-up prose — context or the way out. Muted, smaller. */
47
+ description: { type: String, default: undefined },
48
+ /** The accent bar's color grammar. Default: the primary tone. */
49
+ tone: {
50
+ type: String as PropType<HkQuoteTone>,
51
+ default: "primary",
52
+ },
53
+ /** page = standalone, centered, width-capped block; fill = the
54
+ * block centers itself inside its host (panels, split views). */
55
+ fit: {
56
+ type: String as PropType<"page" | "fill">,
57
+ default: "fill",
58
+ },
59
+ },
60
+ setup(props, { slots }) {
61
+ return () => (
62
+ <div
63
+ class={[
64
+ "hk-quote",
65
+ `hk-quote--${props.tone}`,
66
+ props.fit === "fill" ? "hk-quote--fill" : "hk-quote--page",
67
+ ]}
68
+ role="note"
69
+ >
70
+ <div class="hk-quote-mark">
71
+ <div class="hk-quote-body">
72
+ {props.title ? <p class="hk-quote-title">{props.title}</p> : null}
73
+ {props.description ? (
74
+ <p class="hk-quote-desc">{props.description}</p>
75
+ ) : null}
76
+ {slots.default?.()}
77
+ </div>
78
+ {slots.action ? (
79
+ <div class="hk-quote-action">{slots.action()}</div>
80
+ ) : null}
81
+ </div>
82
+ </div>
83
+ );
84
+ },
85
+ });
package/src/index.ts CHANGED
@@ -54,6 +54,7 @@ export { default as HPhaseTransition } from "./components/HkPhaseTransition";
54
54
  export { default as HGaugeRing } from "./components/HkGaugeRing";
55
55
  export { default as HProgressRing } from "./components/HkProgressRing";
56
56
  export { default as HQrCode } from "./components/HkQrCode";
57
+ export { default as HQuote, type HkQuoteTone as HQuoteTone } from "./components/HkQuote";
57
58
  export { default as HRollingNumber } from "./components/HkRollingNumber";
58
59
  export { default as HLocalePickerPopup } from "./components/HkLocalePickerPopup";
59
60
  export { default as HHoverRevealAction } from "./components/HkHoverRevealAction";
@@ -641,6 +642,8 @@ export { default as HkPhaseTransition } from "./components/HkPhaseTransition";
641
642
  export { default as HkGaugeRing } from "./components/HkGaugeRing";
642
643
  export { default as HkProgressRing } from "./components/HkProgressRing";
643
644
  export { default as HkQrCode } from "./components/HkQrCode";
645
+ export { default as HkQuote } from "./components/HkQuote";
646
+ export type { HkQuoteTone } from "./components/HkQuote";
644
647
  export { default as HkRollingNumber } from "./components/HkRollingNumber";
645
648
  export { default as HkLocalePickerPopup } from "./components/HkLocalePickerPopup";
646
649
  export { default as HkHoverRevealAction } from "./components/HkHoverRevealAction";