@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 +1 -1
- package/src/components/HkQuote.scss +98 -0
- package/src/components/HkQuote.test.tsx +131 -0
- package/src/components/HkQuote.tsx +85 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -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";
|