@celestia-island/hikari 0.55.11 → 0.55.12
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/DemoApp.tsx +2 -1
- package/src/components/HkAdminTablePage.tsx +1 -1
- package/src/components/HkEmptyState.scss +7 -1
- package/src/components/HkEmptyState.surface.contract.test.ts +63 -0
- package/src/components/HkEmptyState.test.tsx +12 -0
- package/src/components/HkEmptyState.tsx +7 -0
package/package.json
CHANGED
package/src/DemoApp.tsx
CHANGED
|
@@ -537,13 +537,14 @@ export default defineComponent({
|
|
|
537
537
|
<h2>HEmptyState</h2>
|
|
538
538
|
<div class="col">
|
|
539
539
|
<HEmptyState
|
|
540
|
+
boxed
|
|
540
541
|
title="No items"
|
|
541
542
|
description="Nothing to show here yet."
|
|
542
543
|
v-slots={{ action: () => <HButton size="sm">Create item</HButton> }}
|
|
543
544
|
/>
|
|
544
545
|
<HEmptyState
|
|
545
546
|
title="Nothing found"
|
|
546
|
-
description="
|
|
547
|
+
description="Bare by default — the host (card/modal/panel) provides the surface; pass boxed when the empty state stands on the page alone."
|
|
547
548
|
icon={PackageOpen}
|
|
548
549
|
/>
|
|
549
550
|
<HEmptyState
|
|
@@ -53,7 +53,7 @@ export const HkAdminTablePage = defineComponent({
|
|
|
53
53
|
) : props.loading && !props.rows.length ? (
|
|
54
54
|
<HSpinner center />
|
|
55
55
|
) : !props.rows.length ? (
|
|
56
|
-
<HEmptyState title={emptyTitle} description={props.emptyDescription} />
|
|
56
|
+
<HEmptyState boxed title={emptyTitle} description={props.emptyDescription} />
|
|
57
57
|
) : (
|
|
58
58
|
<HCard padded={false}>
|
|
59
59
|
<HTable columns={props.columns} rows={props.rows} rowKey={props.rowKey}>
|
|
@@ -5,11 +5,17 @@
|
|
|
5
5
|
justify-content: center;
|
|
6
6
|
padding: var(--space-48, 3rem) var(--space-16, 1rem);
|
|
7
7
|
text-align: center;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* The component's own surface. Opt-in (`boxed`): the host — card, modal,
|
|
12
|
+
panel — usually already provides a surface, and an always-on box turned
|
|
13
|
+
every in-card empty state into a nested placeholder layer. */
|
|
14
|
+
.hk-empty-state--boxed {
|
|
8
15
|
background: var(--hk-empty-bg, var(--hi-color-surface, rgba(240,244,248,0.95)));
|
|
9
16
|
backdrop-filter: blur(var(--blur-sm, 8px));
|
|
10
17
|
border: 1px solid var(--hi-color-border, rgba(100,100,100,0.06));
|
|
11
18
|
border-radius: var(--radius-md, 0.5rem);
|
|
12
|
-
box-sizing: border-box;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
.hk-empty-icon {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source contract: the empty state's surface is OPT-IN (`boxed`). The
|
|
3
|
+
* base `.hk-empty-state` class must stay a bare layout block — the four
|
|
4
|
+
* surface declarations (background, blur, border, radius) live only in
|
|
5
|
+
* `.hk-empty-state--boxed`. Pinned because every in-card empty state in
|
|
6
|
+
* the family reads as a second nested placeholder layer the moment the
|
|
7
|
+
* surface creeps back into the base class (hikari #567); a class-name
|
|
8
|
+
* test alone cannot catch that, the compiled contract lives here.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
const SURFACE_PROPERTIES = [
|
|
18
|
+
"background:",
|
|
19
|
+
"backdrop-filter:",
|
|
20
|
+
"border:",
|
|
21
|
+
"border-radius:",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
/** Extract one top-level rule block's body (`{ ... }`) by selector. */
|
|
25
|
+
function ruleBody(css: string, selector: string): string {
|
|
26
|
+
const start = css.indexOf(`${selector} {`);
|
|
27
|
+
expect(start, `rule ${selector} present`).toBeGreaterThanOrEqual(0);
|
|
28
|
+
const open = css.indexOf("{", start);
|
|
29
|
+
const close = css.indexOf("}", open);
|
|
30
|
+
return css.slice(open + 1, close);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("HkEmptyState surface contract", () => {
|
|
34
|
+
const css = readFileSync(join(here, "HkEmptyState.scss"), "utf-8");
|
|
35
|
+
|
|
36
|
+
it("keeps the base class free of surface declarations", () => {
|
|
37
|
+
const base = ruleBody(css, ".hk-empty-state");
|
|
38
|
+
for (const property of SURFACE_PROPERTIES) {
|
|
39
|
+
expect(base).not.toContain(property);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("declares the full surface on the opt-in --boxed rule only", () => {
|
|
44
|
+
const boxed = ruleBody(css, ".hk-empty-state--boxed");
|
|
45
|
+
for (const property of SURFACE_PROPERTIES) {
|
|
46
|
+
expect(boxed).toContain(property);
|
|
47
|
+
}
|
|
48
|
+
// The surface is the ONLY rule allowed to paint: with the --boxed rule
|
|
49
|
+
// (and comments, whose prose may name the properties) removed, no
|
|
50
|
+
// declaration of a surface property may remain anywhere in the sheet.
|
|
51
|
+
// (The sr-only reset's `border: 0` and box-sizing's `border-box` are
|
|
52
|
+
// not surfaces — the assertions match the painted forms.)
|
|
53
|
+
const commentFree = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
54
|
+
const withoutBoxed = commentFree.replace(
|
|
55
|
+
/\.hk-empty-state--boxed\s*\{[^}]*\}/g,
|
|
56
|
+
"",
|
|
57
|
+
);
|
|
58
|
+
expect(withoutBoxed).not.toContain("background:");
|
|
59
|
+
expect(withoutBoxed).not.toContain("backdrop-filter:");
|
|
60
|
+
expect(withoutBoxed).not.toContain("border: 1px");
|
|
61
|
+
expect(withoutBoxed).not.toContain("border-radius:");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -135,4 +135,16 @@ describe("HkEmptyState", () => {
|
|
|
135
135
|
expect(cls).toContain("hk-empty-state--fill");
|
|
136
136
|
expect(cls).not.toContain("hk-empty-state--page");
|
|
137
137
|
});
|
|
138
|
+
|
|
139
|
+
it("does not draw its own surface by default (the host provides it)", () => {
|
|
140
|
+
const c = mount(h(HkEmptyState, { title: "x" }));
|
|
141
|
+
const cls = (c.querySelector(".hk-empty-state") as HTMLElement).className;
|
|
142
|
+
expect(cls).not.toContain("hk-empty-state--boxed");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("opt-in boxed restores the surface modifier", () => {
|
|
146
|
+
const c = mount(h(HkEmptyState, { title: "x", boxed: true }));
|
|
147
|
+
const cls = (c.querySelector(".hk-empty-state") as HTMLElement).className;
|
|
148
|
+
expect(cls).toContain("hk-empty-state--boxed");
|
|
149
|
+
});
|
|
138
150
|
});
|
|
@@ -19,6 +19,12 @@ export default defineComponent({
|
|
|
19
19
|
type: String as PropType<"page" | "fill">,
|
|
20
20
|
default: "page",
|
|
21
21
|
},
|
|
22
|
+
/** Draw the component's own surface (background, border, blur). Off by
|
|
23
|
+
* default: most hosts (cards, modals, panels) already provide a
|
|
24
|
+
* surface, and a boxed empty state inside them reads as a second
|
|
25
|
+
* nested placeholder layer. Set this only when the empty state stands
|
|
26
|
+
* directly on the page with no outer frame of its own. */
|
|
27
|
+
boxed: { type: Boolean, default: false },
|
|
22
28
|
},
|
|
23
29
|
setup(props, { slots }) {
|
|
24
30
|
const { t } = useI18n();
|
|
@@ -28,6 +34,7 @@ export default defineComponent({
|
|
|
28
34
|
"hk-empty-state",
|
|
29
35
|
props.fit === "fill" ? "hk-empty-state--fill" : "hk-empty-state--page",
|
|
30
36
|
props.loading && "hk-empty-state--loading",
|
|
37
|
+
props.boxed && "hk-empty-state--boxed",
|
|
31
38
|
]}
|
|
32
39
|
role={props.loading ? "status" : undefined}
|
|
33
40
|
aria-busy={props.loading ? "true" : undefined}
|