@celestia-island/hikari 0.55.162 → 0.55.164

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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/src/components/HkAdminShell.tsx +1 -1
  3. package/src/components/HkBlockingToast.scss +1 -1
  4. package/src/components/HkBlockingToast.tsx +2 -1
  5. package/src/components/HkBreadcrumb.scss +1 -1
  6. package/src/components/HkCheckbox.scss +3 -3
  7. package/src/components/HkCornerBadge.scss +132 -0
  8. package/src/components/HkCornerBadge.styles.test.ts +61 -0
  9. package/src/components/HkCornerBadge.test.tsx +86 -0
  10. package/src/components/HkCornerBadge.tsx +91 -0
  11. package/src/components/HkDraggableGrid.scss +1 -1
  12. package/src/components/HkDraggableList.scss +1 -1
  13. package/src/components/HkDrawer.tsx +2 -1
  14. package/src/components/HkFab.scss +1 -1
  15. package/src/components/HkFab.test.tsx +5 -5
  16. package/src/components/HkFilterPanel.test.tsx +223 -161
  17. package/src/components/HkFilterPanel.tsx +188 -99
  18. package/src/components/HkFilterTrigger.mobile.test.tsx +1 -1
  19. package/src/components/HkFilterTrigger.scss +5 -18
  20. package/src/components/HkFilterTrigger.test.tsx +27 -26
  21. package/src/components/HkFilterTrigger.tsx +24 -21
  22. package/src/components/HkFloatingLayer.scss +1 -1
  23. package/src/components/HkFloatingLayer.styles.test.ts +1 -1
  24. package/src/components/HkFloatingLayer.test.tsx +1 -1
  25. package/src/components/HkFloatingLayer.tsx +2 -1
  26. package/src/components/HkLoadingVeil.scss +1 -1
  27. package/src/components/HkLocalePicker.tsx +1 -1
  28. package/src/components/HkMarkdownRenderer.tsx +6 -2
  29. package/src/components/HkModal.tsx +2 -1
  30. package/src/components/HkModalBreadcrumb.overflow.contract.test.ts +3 -4
  31. package/src/components/HkModalBreadcrumb.scss +1 -1
  32. package/src/components/HkModalBreadcrumb.tsx +2 -1
  33. package/src/components/HkPersistentToast.tsx +2 -1
  34. package/src/components/HkPopover.tsx +2 -1
  35. package/src/components/HkScrollPin.scrollcontract.test.ts +1 -1
  36. package/src/components/HkScrollPin.scss +1 -1
  37. package/src/components/HkSearchTrigger.scss +6 -17
  38. package/src/components/HkSearchTrigger.test.tsx +2 -2
  39. package/src/components/HkSearchTrigger.tsx +6 -1
  40. package/src/components/HkSelectPanel.test.tsx +2 -2
  41. package/src/components/HkSelectPanel.tsx +3 -2
  42. package/src/components/HkSidebar.scss +1 -1
  43. package/src/components/HkStatusBar.test.tsx +26 -0
  44. package/src/components/HkStatusBar.tsx +8 -0
  45. package/src/components/HkTable.drag.test.tsx +1 -1
  46. package/src/components/HkTable.scss +1 -1
  47. package/src/components/HkTitleBar.scss +1 -1
  48. package/src/components/HkToast.scss +1 -1
  49. package/src/components/HkToast.tsx +2 -1
  50. package/src/components/HkTooltip.scss +1 -1
  51. package/src/components/HkTooltip.test.tsx +3 -3
  52. package/src/components/HkTooltip.tsx +2 -1
  53. package/src/components/__tests__/hkMarkdownLang.test.ts +29 -0
  54. package/src/errorReporting/HkErrorReportingOverlay.scss +1 -1
  55. package/src/i18n/locales/ar/components.json +1 -0
  56. package/src/i18n/locales/de/components.json +1 -0
  57. package/src/i18n/locales/en/components.json +1 -0
  58. package/src/i18n/locales/es/components.json +1 -0
  59. package/src/i18n/locales/fr/components.json +1 -0
  60. package/src/i18n/locales/ja/components.json +1 -0
  61. package/src/i18n/locales/ko/components.json +1 -0
  62. package/src/i18n/locales/pt/components.json +1 -0
  63. package/src/i18n/locales/ru/components.json +1 -0
  64. package/src/i18n/locales/zh-Hans/components.json +1 -0
  65. package/src/i18n/locales/zh-Hant/components.json +1 -0
  66. package/src/index.ts +5 -1
  67. package/src/overlay/overlayRoot.scss +35 -0
  68. package/src/overlay/overlayRoot.ts +74 -0
  69. package/src/runtime/usePopupManager.test.ts +12 -8
  70. package/src/runtime/usePopupManager.ts +15 -5
  71. package/src/scale.scss +23 -4
  72. package/src/styles/admin-tokens.scss +2 -2
  73. package/src/styles/index.scss +1 -0
  74. package/src/styles/theme/scale.scss +23 -4
  75. package/src/theme/theme.scss +0 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.162",
3
+ "version": "0.55.164",
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",
@@ -268,7 +268,7 @@ export const HkAdminShell = defineComponent({
268
268
 
269
269
  {slots.footer && (
270
270
  <footer class="s-status-bar" style={{ position: "fixed", bottom: 0, left: 0, right: 0, // Footer band (was a bare 40 — the pre-band chrome value).
271
- zIndex: "var(--z-footer, 110)" }}>
271
+ zIndex: "var(--hk-z-toolbar)" }}>
272
272
  {slots.footer()}
273
273
  </footer>
274
274
  )}
@@ -12,7 +12,7 @@
12
12
  /* Same viewport gutter the anchored popups clamp to (16px desktop /
13
13
  * 8px mobile) — the toast stack is a floating layer too. */
14
14
  inset-inline-end: var(--viewport-gutter, 16px);
15
- z-index: var(--hk-z-toast, 4000);
15
+ z-index: var(--hk-overlay-z-blocking-toast, 50);
16
16
  max-width: 24rem;
17
17
  pointer-events: none;
18
18
  }
@@ -45,6 +45,7 @@ import {
45
45
  } from "../runtime/useBlockingToast";
46
46
  import "./HkBlockingToast.scss";
47
47
  import HButton from "./HkButton";
48
+ import { ensureOverlayRoot } from "../overlay/overlayRoot";
48
49
 
49
50
  const ICON_SIZE = 18;
50
51
 
@@ -175,7 +176,7 @@ export default defineComponent({
175
176
  }
176
177
 
177
178
  return () => (
178
- <Teleport to="body">
179
+ <Teleport to={ensureOverlayRoot()}>
179
180
  <div class="hk-blocking-toast-container">
180
181
  <TransitionGroup
181
182
  ref={hostRef}
@@ -212,7 +212,7 @@
212
212
  top: calc(100% + 4px);
213
213
  left: 0;
214
214
  min-width: 200px;
215
- z-index: 100;
215
+ z-index: var(--hk-z-header);
216
216
 
217
217
  // Appearance
218
218
  border-radius: vars.$hikari-radius-fui-md;
@@ -103,7 +103,7 @@
103
103
 
104
104
  .hk-checkbox-icon {
105
105
  position: relative;
106
- z-index: var(--hi-z-above, 1);
106
+ z-index: var(--hk-z-above);
107
107
  color: rgb(var(--color-on-solid-icon, 255 255 255));
108
108
  width: 14px;
109
109
  height: 14px;
@@ -124,7 +124,7 @@
124
124
  height: 6px;
125
125
  border-radius: 50%;
126
126
  background: rgb(var(--color-on-solid-icon, 255 255 255));
127
- z-index: var(--hi-z-above, 1);
127
+ z-index: var(--hk-z-above);
128
128
 
129
129
  .hk-checkbox-sm & {
130
130
  width: 5px;
@@ -142,7 +142,7 @@
142
142
  height: 2px;
143
143
  border-radius: 1px;
144
144
  background: rgb(var(--color-on-solid-icon, 255 255 255));
145
- z-index: var(--hi-z-above, 1);
145
+ z-index: var(--hk-z-above);
146
146
 
147
147
  .hk-checkbox-sm & {
148
148
  width: 6px;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * HkCornerBadge — the generic corner mark (dot / count / label) pinned
3
+ * to one corner of the default-slot anchor (see HkCornerBadge.tsx for
4
+ * the contract). Geometry follows the house pattern the trigger dots
5
+ * and the Rust family's .hk-badge-wrapper established: the anchor is a
6
+ * relative inline-flex wrapper (the badge escapes nothing — hosts wrap
7
+ * ANY child), the mark centers ON the corner via translate(50%,-50%)
8
+ * with an optional --hk-corner-badge-offset clearance.
9
+ */
10
+
11
+ .hk-corner-badge-anchor {
12
+ position: relative;
13
+ display: inline-flex;
14
+ }
15
+
16
+ .hk-corner-badge {
17
+ position: absolute;
18
+ /* Centered on the corner; the corner data-attribute flips the axes
19
+ * and the offset var adds clearance without re-deriving transforms. */
20
+ transform: translate(var(--hk-corner-badge-tx, 50%), var(--hk-corner-badge-ty, -50%));
21
+ pointer-events: none;
22
+ white-space: nowrap;
23
+ }
24
+
25
+ .hk-corner-badge[data-corner="top-right"] {
26
+ top: var(--hk-corner-badge-offset, 0px);
27
+ right: var(--hk-corner-badge-offset, 0px);
28
+ --hk-corner-badge-tx: 50%;
29
+ --hk-corner-badge-ty: -50%;
30
+ }
31
+
32
+ .hk-corner-badge[data-corner="top-left"] {
33
+ top: var(--hk-corner-badge-offset, 0px);
34
+ left: var(--hk-corner-badge-offset, 0px);
35
+ --hk-corner-badge-tx: -50%;
36
+ --hk-corner-badge-ty: -50%;
37
+ }
38
+
39
+ .hk-corner-badge[data-corner="bottom-right"] {
40
+ bottom: var(--hk-corner-badge-offset, 0px);
41
+ right: var(--hk-corner-badge-offset, 0px);
42
+ --hk-corner-badge-tx: 50%;
43
+ --hk-corner-badge-ty: 50%;
44
+ }
45
+
46
+ .hk-corner-badge[data-corner="bottom-left"] {
47
+ bottom: var(--hk-corner-badge-offset, 0px);
48
+ left: var(--hk-corner-badge-offset, 0px);
49
+ --hk-corner-badge-tx: -50%;
50
+ --hk-corner-badge-ty: 50%;
51
+ }
52
+
53
+ /* Dot form: a filled circle in the variant's tone (same token trio
54
+ * HkBadge uses — variant classes below set all three channels). */
55
+ .hk-corner-badge--dot {
56
+ width: 8px;
57
+ height: 8px;
58
+ border-radius: 50%;
59
+ background: var(--hk-badge-bg);
60
+ box-shadow: 0 0 0 1.5px rgb(var(--color-surface) / 85%);
61
+ }
62
+
63
+ /* Pill form: the compact count/label chip (18px min, text-4xs mono
64
+ * digits — notification-badge geometry across the family). */
65
+ .hk-corner-badge--pill {
66
+ min-width: 18px;
67
+ height: 18px;
68
+ padding: 0 5px;
69
+ display: inline-flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ border-radius: var(--radius-full, 999px);
73
+ background: var(--hk-badge-bg);
74
+ color: var(--hk-badge-text);
75
+ border: 1px solid var(--hk-badge-border);
76
+ font-size: var(--text-4xs, 0.625rem);
77
+ font-weight: 700;
78
+ line-height: 1;
79
+ font-variant-numeric: tabular-nums;
80
+ }
81
+
82
+ /* Variant tones — the same channel triplets HkBadge.scss ships; kept
83
+ * literal here so the corner badge owns its own (tiny) sheet. */
84
+ .hk-corner-badge-default {
85
+ --hk-badge-bg: rgb(var(--color-primary) / 12%);
86
+ --hk-badge-text: rgb(var(--color-text));
87
+ --hk-badge-border: rgb(var(--color-primary) / 35%);
88
+ }
89
+
90
+ .hk-corner-badge-primary {
91
+ --hk-badge-bg: rgb(var(--color-primary));
92
+ --hk-badge-text: rgb(var(--color-on-primary, 255 255 255));
93
+ --hk-badge-border: transparent;
94
+ }
95
+
96
+ .hk-corner-badge-success {
97
+ --hk-badge-bg: rgb(var(--color-success));
98
+ --hk-badge-text: rgb(var(--color-on-primary, 255 255 255));
99
+ --hk-corner-badge-ring: rgb(var(--color-surface) / 85%);
100
+ --hk-badge-border: transparent;
101
+ }
102
+
103
+ .hk-corner-badge-error {
104
+ --hk-badge-bg: rgb(var(--color-error));
105
+ --hk-badge-text: rgb(var(--color-on-primary, 255 255 255));
106
+ --hk-badge-border: transparent;
107
+ }
108
+
109
+ .hk-corner-badge-warning {
110
+ --hk-badge-bg: rgb(var(--color-warning));
111
+ --hk-badge-text: rgb(var(--color-text));
112
+ --hk-badge-border: transparent;
113
+ }
114
+
115
+ .hk-corner-badge-info {
116
+ --hk-badge-bg: rgb(var(--color-info, var(--color-primary)));
117
+ --hk-badge-text: rgb(var(--color-on-primary, 255 255 255));
118
+ --hk-badge-border: transparent;
119
+ }
120
+
121
+ .hk-corner-badge-muted {
122
+ --hk-badge-bg: rgb(var(--color-text) / 55%);
123
+ --hk-badge-text: rgb(var(--color-surface));
124
+ --hk-badge-border: transparent;
125
+ }
126
+
127
+ /* The success dot's ring token feeds the dot form's surface cutout so
128
+ * the mark reads ON the anchor, not under it (the icon-group marker's
129
+ * trick, generalized). */
130
+ .hk-corner-badge-success.hk-corner-badge--dot {
131
+ box-shadow: 0 0 0 1.5px var(--hk-corner-badge-ring, rgb(var(--color-surface) / 85%));
132
+ }
@@ -0,0 +1,61 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { compile } from "sass";
6
+ import { describe, expect, it } from "vitest";
7
+
8
+ /**
9
+ * Compiled-stylesheet guard for HkCornerBadge — the anchoring contract
10
+ * (2026-10-15): the anchor wrapper is a relative inline-flex host (any
11
+ * child can carry the mark), the mark centers ON its corner via
12
+ * translate(±50%,±50%), and each data-corner flips the right axes.
13
+ * Vitest stubs CSS, so the runtime suite can never catch a lost rule.
14
+ */
15
+
16
+ const componentDir = resolve(dirname(fileURLToPath(import.meta.url)));
17
+
18
+ const css = compile(resolve(componentDir, "HkCornerBadge.scss"), {
19
+ style: "expanded",
20
+ }).css;
21
+
22
+ function ruleFor(selector: string): string {
23
+ const start = css.indexOf(selector);
24
+ expect(start, `${selector} present in the compiled sheet`).toBeGreaterThanOrEqual(0);
25
+ const open = css.indexOf("{", start);
26
+ const close = css.indexOf("}", open);
27
+ return css.slice(start, close + 1);
28
+ }
29
+
30
+ describe("HkCornerBadge stylesheet contract", () => {
31
+ it("compiles", () => {
32
+ expect(css).toContain(".hk-corner-badge");
33
+ });
34
+
35
+ it("the anchor is a relative inline-flex wrapper (any child carries the mark)", () => {
36
+ const rule = ruleFor(".hk-corner-badge-anchor {");
37
+ expect(rule).toContain("position: relative");
38
+ expect(rule).toContain("display: inline-flex");
39
+ });
40
+
41
+ it("the mark never takes pointer events and never wraps", () => {
42
+ const rule = ruleFor(".hk-corner-badge {");
43
+ expect(rule).toContain("position: absolute");
44
+ expect(rule).toContain("pointer-events: none");
45
+ expect(rule).toContain("white-space: nowrap");
46
+ });
47
+
48
+ it("each corner flips the translate axes", () => {
49
+ expect(ruleFor('.hk-corner-badge[data-corner=top-right] {')).toContain("--hk-corner-badge-tx: 50%");
50
+ expect(ruleFor('.hk-corner-badge[data-corner=top-left] {')).toContain("--hk-corner-badge-tx: -50%");
51
+ expect(ruleFor('.hk-corner-badge[data-corner=bottom-right] {')).toContain("--hk-corner-badge-ty: 50%");
52
+ expect(ruleFor('.hk-corner-badge[data-corner=bottom-left] {')).toContain("--hk-corner-badge-tx: -50%");
53
+ });
54
+
55
+ it("the dot is round and the pill is a tabular-numbers chip", () => {
56
+ const dot = ruleFor(".hk-corner-badge--dot {");
57
+ expect(dot).toContain("border-radius: 50%");
58
+ const pill = ruleFor(".hk-corner-badge--pill {");
59
+ expect(pill).toContain("font-variant-numeric: tabular-nums");
60
+ });
61
+ });
@@ -0,0 +1,86 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { createApp, h } from "vue";
3
+
4
+ import HkCornerBadge from "./HkCornerBadge";
5
+
6
+ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
7
+
8
+ function mount(node: ReturnType<typeof h>) {
9
+ const container = document.createElement("div");
10
+ document.body.appendChild(container);
11
+ const app = createApp({ render: () => node });
12
+ app.mount(container);
13
+ mounts.push({ app, container });
14
+ return container;
15
+ }
16
+
17
+ afterEach(() => {
18
+ for (const { app, container } of mounts.splice(0)) {
19
+ app.unmount();
20
+ container.remove();
21
+ }
22
+ });
23
+
24
+ describe("HkCornerBadge — the generic corner mark (2026-10-15)", () => {
25
+ it("anchors the default slot in a relative wrapper with no mark by default", () => {
26
+ const c = mount(h(HkCornerBadge, null, () => h("button", { class: "probe" }, "x")));
27
+ const anchor = c.querySelector(".hk-corner-badge-anchor")!;
28
+ expect(anchor, "anchor wrapper present").toBeTruthy();
29
+ // (The relative/inline-flex anchoring is pinned by the compiled-css
30
+ // contract test — vitest stubs styles, so no computed assertions.)
31
+ expect(c.querySelector(".probe"), "slot content rendered").toBeTruthy();
32
+ expect(c.querySelector(".hk-corner-badge")).toBeNull();
33
+ });
34
+
35
+ it("dot form: a variant-colored dot at the requested corner, decorative by default", () => {
36
+ const c = mount(
37
+ h(HkCornerBadge, { dot: true, variant: "success", corner: "bottom-right" }, () => h("i")),
38
+ );
39
+ const badge = c.querySelector(".hk-corner-badge")!;
40
+ expect(badge).toBeTruthy();
41
+ expect(badge.className).toContain("hk-corner-badge--dot");
42
+ expect(badge.className).toContain("hk-corner-badge-success");
43
+ expect(badge.getAttribute("data-corner")).toBe("bottom-right");
44
+ expect(badge.getAttribute("aria-hidden")).toBe("true");
45
+ expect(badge.getAttribute("role")).toBeNull();
46
+ });
47
+
48
+ it("count form: renders the number, caps past max, hides zero unless showZero", () => {
49
+ const c = mount(h(HkCornerBadge, { count: 7 }, () => h("i")));
50
+ expect(c.querySelector(".hk-corner-badge")!.textContent).toBe("7");
51
+ const capped = mount(h(HkCornerBadge, { count: 130, max: 99 }, () => h("i")));
52
+ expect(capped.querySelector(".hk-corner-badge")!.textContent).toBe("99+");
53
+ const zero = mount(h(HkCornerBadge, { count: 0 }, () => h("i")));
54
+ expect(zero.querySelector(".hk-corner-badge")).toBeNull();
55
+ const zeroShown = mount(h(HkCornerBadge, { count: 0, showZero: true }, () => h("i")));
56
+ expect(zeroShown.querySelector(".hk-corner-badge")!.textContent).toBe("0");
57
+ });
58
+
59
+ it("custom label form: a text pill from the default slot", () => {
60
+ const c = mount(h(HkCornerBadge, { variant: "primary", text: "5h" }, () => h("i")));
61
+ const badge = c.querySelector(".hk-corner-badge")!;
62
+ expect(badge.textContent).toBe("5h");
63
+ expect(badge.className).toContain("hk-corner-badge--pill");
64
+ });
65
+
66
+ it("accessible mode: role=img + aria-label instead of aria-hidden", () => {
67
+ const c = mount(h(HkCornerBadge, { dot: true, label: "3 active filters" }, () => h("i")));
68
+ const badge = c.querySelector(".hk-corner-badge")!;
69
+ expect(badge.getAttribute("role")).toBe("img");
70
+ expect(badge.getAttribute("aria-label")).toBe("3 active filters");
71
+ expect(badge.getAttribute("aria-hidden")).toBeNull();
72
+ });
73
+
74
+ it("dot wins when both dot and count/text are set", () => {
75
+ const c = mount(h(HkCornerBadge, { dot: true, count: 9 }, () => h("i")));
76
+ const badge = c.querySelector(".hk-corner-badge")!;
77
+ expect(badge.className).toContain("hk-corner-badge--dot");
78
+ expect(badge.textContent).toBe("");
79
+ });
80
+
81
+ it("offset writes the clearance custom property", () => {
82
+ const c = mount(h(HkCornerBadge, { dot: true, offset: 2 }, () => h("i")));
83
+ const badge = c.querySelector(".hk-corner-badge") as HTMLElement;
84
+ expect(badge.style.getPropertyValue("--hk-corner-badge-offset")).toBe("2px");
85
+ });
86
+ });
@@ -0,0 +1,91 @@
1
+ import { computed, defineComponent, type PropType } from "vue";
2
+
3
+ import "./HkCornerBadge.scss";
4
+
5
+ /** The color channels HkBadge already standardizes (same variant names,
6
+ * same override escape hatch — kept in lockstep with HkBadge.tsx). */
7
+ export type CornerBadgeVariant =
8
+ | "default"
9
+ | "primary"
10
+ | "success"
11
+ | "error"
12
+ | "warning"
13
+ | "info"
14
+ | "muted";
15
+
16
+ /**
17
+ * HkCornerBadge — the generic corner mark for ANY anchor (user
18
+ * direction 2026-10-15): a green/red/traffic dot, a notification count
19
+ * ("9+"), or a custom text label pinned to one corner of whatever sits
20
+ * in the default slot. The Vue package had every consumer hand-rolling
21
+ * its own absolute span (the filter/search trigger dots, the icon-group
22
+ * marker, chest's dock time-range pill); this is the one component they
23
+ * converge on — API parity with the Rust family's badge.rs (dot / count
24
+ * / max "N+" / showZero), plus the corner + custom-label the web needs.
25
+ *
26
+ * Decorative by default (aria-hidden): the mark VISUALIZES state the
27
+ * anchor already exposes (a filtered trigger, a selected range); pass
28
+ * `label` when it is the only place a count is announced.
29
+ */
30
+ export default defineComponent({
31
+ name: "HkCornerBadge",
32
+ props: {
33
+ variant: { type: String as PropType<CornerBadgeVariant>, default: "primary" },
34
+ /** Bare dot form (the green/red/traffic light). */
35
+ dot: { type: Boolean, default: false },
36
+ /** Count form: renders the number, or "max+" past `max`. Hidden at
37
+ * 0 unless showZero (notification semantics). */
38
+ count: { type: Number, default: undefined },
39
+ max: { type: Number, default: undefined },
40
+ showZero: { type: Boolean, default: false },
41
+ /** Custom label text (the pill form). The default slot is the
42
+ * ANCHOR, never the label — overloading it made every anchored
43
+ * child's own text leak into the badge. */
44
+ text: { type: String, default: undefined },
45
+ /** Which corner of the anchor. */
46
+ corner: {
47
+ type: String as PropType<"top-right" | "top-left" | "bottom-right" | "bottom-left">,
48
+ default: "top-right",
49
+ },
50
+ /** Extra clearance from the corner in px (0 = sit ON the corner,
51
+ * the translate(50%,-50%) centering). */
52
+ offset: { type: Number, default: 0 },
53
+ /** Accessible name for the mark (without it the badge is
54
+ * aria-hidden decoration). */
55
+ label: { type: String, default: undefined },
56
+ },
57
+ setup(props, { slots }) {
58
+ const text = computed<string | null>(() => {
59
+ if (props.count !== undefined) {
60
+ if (props.count === 0 && !props.showZero) return null;
61
+ if (props.max !== undefined && props.count > props.max) return `${props.max}+`;
62
+ return String(props.count);
63
+ }
64
+ return props.text && props.text.length > 0 ? props.text : null;
65
+ });
66
+
67
+ const show = computed(() => props.dot || text.value !== null);
68
+
69
+ return () => (
70
+ <span class="hk-corner-badge-anchor">
71
+ {slots.default?.()}
72
+ {show.value && (
73
+ <span
74
+ class={[
75
+ "hk-corner-badge",
76
+ `hk-corner-badge-${props.variant}`,
77
+ props.dot ? "hk-corner-badge--dot" : "hk-corner-badge--pill",
78
+ ]}
79
+ data-corner={props.corner}
80
+ style={props.offset ? { "--hk-corner-badge-offset": `${props.offset}px` } : undefined}
81
+ aria-hidden={props.label ? undefined : "true"}
82
+ role={props.label ? "img" : undefined}
83
+ aria-label={props.label}
84
+ >
85
+ {props.dot ? null : text.value}
86
+ </span>
87
+ )}
88
+ </span>
89
+ );
90
+ },
91
+ });
@@ -136,7 +136,7 @@
136
136
  /* Drag ghost: above page chrome (--z-sidebar 900) but below every
137
137
  popup band (overlay 1000 < dropdown 2000 < tooltip 3000 < toast 4000)
138
138
  so a drag never buries toasts or open panels. */
139
- z-index: var(--hk-z-drag, 950);
139
+ z-index: var(--hk-z-drag);
140
140
  will-change: transform;
141
141
 
142
142
  > .hk-draggable-grid-cell {
@@ -110,7 +110,7 @@
110
110
  /* Drag ghost: above page chrome (--z-sidebar 900) but below every
111
111
  popup band (overlay 1000 < dropdown 2000 < tooltip 3000 < toast 4000)
112
112
  so a drag never buries toasts or open panels. */
113
- z-index: var(--hk-z-drag, 950);
113
+ z-index: var(--hk-z-drag);
114
114
  will-change: transform;
115
115
 
116
116
  > .hk-draggable-list-item {
@@ -23,6 +23,7 @@ import { useSurfaceContentHold } from "../composables/useSurfaceContentHold";
23
23
  import HIconButton from "./HkIconButton";
24
24
  import HIcon from "./HkIcon";
25
25
  import "./window-close.scss";
26
+ import { ensureOverlayRoot } from "../overlay/overlayRoot";
26
27
 
27
28
  type DrawerSide = "left" | "right" | "top" | "bottom";
28
29
 
@@ -276,7 +277,7 @@ export default defineComponent({
276
277
  if (!machine.mounted.value) return null;
277
278
  const panelPrefix = `hk-drawer-${props.side}`;
278
279
  return (
279
- <Teleport to="body">
280
+ <Teleport to={ensureOverlayRoot()}>
280
281
  {props.overlay ? (
281
282
  <div
282
283
  ref={overlayEl}
@@ -4,7 +4,7 @@
4
4
  // safe-area-aware base per instance (set from offsetX / offsetY).
5
5
  .hk-fab {
6
6
  position: absolute;
7
- z-index: var(--hk-fab-z, 40);
7
+ z-index: var(--hk-z-fab);
8
8
  display: flex;
9
9
  align-items: center;
10
10
  gap: var(--space-8, 0.5rem);
@@ -46,10 +46,10 @@ function mountFab(props: Record<string, unknown> = {}): Harness {
46
46
  // fall back to the document scope so both modes resolve.
47
47
  const root =
48
48
  container.querySelector<HTMLElement>(".hk-fab") ??
49
- document.body.querySelector<HTMLElement>(".hk-fab");
49
+ document.querySelector<HTMLElement>(".hk-fab");
50
50
  const button =
51
51
  container.querySelector<HTMLButtonElement>(".hk-fab-button") ??
52
- document.body.querySelector<HTMLButtonElement>(".hk-fab-button");
52
+ document.querySelector<HTMLButtonElement>(".hk-fab-button");
53
53
  if (!root || !button) throw new Error("HkFab did not render its shell");
54
54
  harness.root = root;
55
55
  harness.button = button;
@@ -226,13 +226,13 @@ describe("HkFab speed dial", () => {
226
226
  describe("HkFab layer", () => {
227
227
  it("host (default) keeps the fab anchored in the host tree", () => {
228
228
  const m = mountFab({ ariaLabel: "Host", positioning: "fixed" });
229
- expect(document.body.querySelector(".hk-floating-layer")).toBeNull();
229
+ expect(document.querySelector(".hk-floating-layer")).toBeNull();
230
230
  expect(m.root.getAttribute("data-positioning")).toBe("fixed");
231
231
  });
232
232
 
233
233
  it("top wraps the fab in an HkFloatingLayer above the window band", () => {
234
234
  const m = mountFab({ ariaLabel: "Float", layer: "top", corner: "bottom-right" });
235
- const layer = document.body.querySelector<HTMLElement>(".hk-floating-layer");
235
+ const layer = document.querySelector<HTMLElement>(".hk-floating-layer");
236
236
  expect(layer).not.toBeNull();
237
237
  expect(layer!.contains(m.root)).toBe(true);
238
238
  // Geometry is owned by the layer alone once floated.
@@ -244,6 +244,6 @@ describe("HkFab layer", () => {
244
244
  expect(layer!.getAttribute("data-corner")).toBe("bottom-right");
245
245
  const z = Number(layer!.style.getPropertyValue("--hk-float-z"));
246
246
  expect(z).toBeGreaterThanOrEqual(POPUP_Z_BANDS.tooltip);
247
- expect(z).toBeLessThan(POPUP_Z_BANDS.toast);
247
+ // Tooltip is the topmost band (2026-10-15): no upper bound.
248
248
  });
249
249
  });