@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.11",
3
+ "version": "0.55.12",
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",
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="Search returned no matches — try a different query."
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}