@celestia-island/hikari 0.55.215 → 0.55.216

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.215",
3
+ "version": "0.55.216",
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",
@@ -21,6 +21,10 @@
21
21
  align-items: center;
22
22
  justify-content: center;
23
23
  flex-shrink: 0;
24
+ // The status dot's anchor layer (.hk-icon-button-dot below) — a
25
+ // relative button keeps the mark INSIDE the control's own silhouette
26
+ // no matter what wrappers or flex rows do around it.
27
+ position: relative;
24
28
  padding: 0;
25
29
  line-height: 0;
26
30
 
@@ -141,3 +145,25 @@
141
145
  transition: none;
142
146
  }
143
147
  }
148
+
149
+ // ------
150
+ // Button-owned status dot (dot prop)
151
+ // ------
152
+
153
+ /* The mark lives INSIDE the button's own box (bottom-right inset), not
154
+ * straddling a wrapper corner: on a borderless ghost trigger inside a
155
+ * centered flex row, the corner-straddling badge read as a stray dot
156
+ * floating below the glyph (2026-10-28 user report). Tone channels come
157
+ * from HkCornerBadge's variant classes (one badge grammar); the surface
158
+ * ring keeps the dot legible on any button fill. */
159
+ .hk-icon-button-dot {
160
+ position: absolute;
161
+ right: var(--hk-icon-button-dot-inset, 2px);
162
+ bottom: var(--hk-icon-button-dot-inset, 2px);
163
+ width: 8px;
164
+ height: 8px;
165
+ border-radius: 50%;
166
+ background: var(--hk-badge-bg, rgb(var(--color-success)));
167
+ box-shadow: 0 0 0 1.5px rgb(var(--color-surface) / 85%);
168
+ pointer-events: none;
169
+ }
@@ -151,3 +151,78 @@ describe("HkIconButton glyph step", () => {
151
151
  expect(block24).toContain("var(--hi-icon-size-xs)");
152
152
  });
153
153
  });
154
+
155
+ describe("HkIconButton status dot", () => {
156
+ it("renders no dot by default", () => {
157
+ const c = mountComp(() =>
158
+ h(HkIconButton, { "aria-label": "Filter" }, { icon: () => h("span", "x") }),
159
+ );
160
+ expect(c.querySelector(".hk-icon-button-dot")).toBeNull();
161
+ });
162
+
163
+ it("dot renders inside the button with the tone class and stays decorative", () => {
164
+ const c = mountComp(() =>
165
+ h(HkIconButton, { dot: true, "aria-label": "Filter" }, { icon: () => h("span", "x") }),
166
+ );
167
+ const dot = c.querySelector(".hk-icon-button-dot")!;
168
+ expect(dot).not.toBeNull();
169
+ // Tone channels ride HkCornerBadge's variant classes (one grammar).
170
+ expect(dot.classList.contains("hk-corner-badge-success")).toBe(true);
171
+ // Decorative by default: the button's own label carries the state.
172
+ expect(dot.getAttribute("aria-hidden")).toBe("true");
173
+ expect(dot.getAttribute("role")).toBeNull();
174
+ // Inside the button's own silhouette — never a detached wrapper mark.
175
+ expect(dot.closest("button")).not.toBeNull();
176
+ expect(dot.closest(".hk-corner-badge-anchor")).toBeNull();
177
+ });
178
+
179
+ it("dotVariant switches the tone class", () => {
180
+ const c = mountComp(() =>
181
+ h(HkIconButton, { dot: true, dotVariant: "error", "aria-label": "Fail" }, { icon: () => h("span", "x") }),
182
+ );
183
+ expect(c.querySelector(".hk-icon-button-dot")!.classList.contains("hk-corner-badge-error")).toBe(true);
184
+ });
185
+
186
+ it("dotLabel promotes the dot to a named mark", () => {
187
+ const c = mountComp(() =>
188
+ h(HkIconButton, { dot: true, dotLabel: "Filter engaged", "aria-label": "Filter" }, { icon: () => h("span", "x") }),
189
+ );
190
+ const dot = c.querySelector(".hk-icon-button-dot")!;
191
+ expect(dot.getAttribute("aria-hidden")).toBeNull();
192
+ expect(dot.getAttribute("role")).toBe("img");
193
+ expect(dot.getAttribute("aria-label")).toBe("Filter engaged");
194
+ });
195
+ });
196
+
197
+ /**
198
+ * Compiled-stylesheet guard for the button-owned status dot (R3-style
199
+ * contract, HkCornerBadge.styles.test.ts pattern): the button is the
200
+ * anchor layer (position: relative) and the dot is an absolutely
201
+ * positioned INSET mark with pointer-events off — the declarations are
202
+ * the contract where happy-dom has no cascade.
203
+ */
204
+ describe("HkIconButton status dot stylesheet contract", () => {
205
+ const here = dirname(fileURLToPath(import.meta.url));
206
+ const scss = readFileSync(join(here, "HkIconButton.scss"), "utf8");
207
+
208
+ it("the button is the dot's anchor layer (position: relative)", () => {
209
+ const start = scss.indexOf(".hk-icon-button {");
210
+ const close = scss.indexOf("}", start);
211
+ expect(scss.slice(start, close + 1)).toContain("position: relative");
212
+ });
213
+
214
+ it("the dot is an absolute inset mark with pointer-events off and a round cutout ring", () => {
215
+ const start = scss.indexOf(".hk-icon-button-dot {");
216
+ const close = scss.indexOf("}", start);
217
+ const rule = scss.slice(start, close + 1);
218
+ expect(rule).toContain("position: absolute");
219
+ expect(rule).toContain("right: var(--hk-icon-button-dot-inset");
220
+ expect(rule).toContain("bottom: var(--hk-icon-button-dot-inset");
221
+ expect(rule).toContain("pointer-events: none");
222
+ expect(rule).toContain("border-radius: 50%");
223
+ expect(rule).toContain("box-shadow: 0 0 0 1.5px");
224
+ // Tone rides HkCornerBadge's variant classes; the declaration (not
225
+ // just the class) is pinned so a tone swap cannot slip silently.
226
+ expect(rule).toContain("background: var(--hk-badge-bg");
227
+ });
228
+ });
@@ -1,7 +1,12 @@
1
1
  import { computed, defineComponent, type PropType } from "vue";
2
2
  import "./HkIconButton.scss";
3
3
  import "./HkIconButtonVars.scss";
4
+ // The status dot borrows HkCornerBadge's tone channels (--hk-badge-bg)
5
+ // so the mark on the button and the badge family stay one grammar; the
6
+ // bundler dedupes the sheet (single source of tones).
7
+ import "./HkCornerBadge.scss";
4
8
  import HIcon from "./HkIcon";
9
+ import type { CornerBadgeVariant } from "./HkCornerBadge";
5
10
 
6
11
  export default defineComponent({
7
12
  name: "HkIconButton",
@@ -16,6 +21,21 @@ export default defineComponent({
16
21
  // (user report 2026-09-28: 前台全局搜索与主题切换按钮不同尺寸).
17
22
  size: { type: Number as PropType<16 | 24 | 28 | 32 | 36 | 40>, default: 32 },
18
23
  disabled: { type: Boolean, default: false },
24
+ /** Button-owned status dot (2026-10-28 user direction: 改进图标按钮
25
+ * 本身). The former pattern — HkCornerBadge wrapped AROUND the
26
+ * trigger — anchors to the WRAPPER's corner (translate 50% straddles
27
+ * the box edge), which on a borderless ghost trigger inside a
28
+ * centered flex row reads as a stray mark floating below the glyph.
29
+ * This dot is laid out INSIDE the button's own box (bottom-right
30
+ * inset), so the mark and the control are one silhouette. Intended
31
+ * for the ≥24 chrome steps — at 16 the 8px dot would crowd the
32
+ * glyph (R2 note; no such consumer today). */
33
+ dot: { type: Boolean, default: false },
34
+ /** Tone of the status dot (same variant names as HkCornerBadge). */
35
+ dotVariant: { type: String as PropType<CornerBadgeVariant>, default: "success" },
36
+ /** Accessible name for the dot (without it the dot is aria-hidden
37
+ * decoration for state the button already announces). */
38
+ dotLabel: { type: String, default: undefined },
19
39
  },
20
40
  emits: {
21
41
  click: (_e: MouseEvent) => true,
@@ -59,6 +79,14 @@ export default defineComponent({
59
79
  </svg>
60
80
  )}
61
81
  </span>
82
+ {props.dot && (
83
+ <span
84
+ class={["hk-icon-button-dot", `hk-corner-badge-${props.dotVariant}`]}
85
+ aria-hidden={props.dotLabel ? undefined : "true"}
86
+ role={props.dotLabel ? "img" : undefined}
87
+ aria-label={props.dotLabel}
88
+ />
89
+ )}
62
90
  </button>
63
91
  );
64
92
  },