@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.
- package/package.json +1 -1
- package/src/components/HkAdminShell.tsx +1 -1
- package/src/components/HkBlockingToast.scss +1 -1
- package/src/components/HkBlockingToast.tsx +2 -1
- package/src/components/HkBreadcrumb.scss +1 -1
- package/src/components/HkCheckbox.scss +3 -3
- package/src/components/HkCornerBadge.scss +132 -0
- package/src/components/HkCornerBadge.styles.test.ts +61 -0
- package/src/components/HkCornerBadge.test.tsx +86 -0
- package/src/components/HkCornerBadge.tsx +91 -0
- package/src/components/HkDraggableGrid.scss +1 -1
- package/src/components/HkDraggableList.scss +1 -1
- package/src/components/HkDrawer.tsx +2 -1
- package/src/components/HkFab.scss +1 -1
- package/src/components/HkFab.test.tsx +5 -5
- package/src/components/HkFilterPanel.test.tsx +223 -161
- package/src/components/HkFilterPanel.tsx +188 -99
- package/src/components/HkFilterTrigger.mobile.test.tsx +1 -1
- package/src/components/HkFilterTrigger.scss +5 -18
- package/src/components/HkFilterTrigger.test.tsx +27 -26
- package/src/components/HkFilterTrigger.tsx +24 -21
- package/src/components/HkFloatingLayer.scss +1 -1
- package/src/components/HkFloatingLayer.styles.test.ts +1 -1
- package/src/components/HkFloatingLayer.test.tsx +1 -1
- package/src/components/HkFloatingLayer.tsx +2 -1
- package/src/components/HkLoadingVeil.scss +1 -1
- package/src/components/HkLocalePicker.tsx +1 -1
- package/src/components/HkMarkdownRenderer.tsx +6 -2
- package/src/components/HkModal.tsx +2 -1
- package/src/components/HkModalBreadcrumb.overflow.contract.test.ts +3 -4
- package/src/components/HkModalBreadcrumb.scss +1 -1
- package/src/components/HkModalBreadcrumb.tsx +2 -1
- package/src/components/HkPersistentToast.tsx +2 -1
- package/src/components/HkPopover.tsx +2 -1
- package/src/components/HkScrollPin.scrollcontract.test.ts +1 -1
- package/src/components/HkScrollPin.scss +1 -1
- package/src/components/HkSearchTrigger.scss +6 -17
- package/src/components/HkSearchTrigger.test.tsx +2 -2
- package/src/components/HkSearchTrigger.tsx +6 -1
- package/src/components/HkSelectPanel.test.tsx +2 -2
- package/src/components/HkSelectPanel.tsx +3 -2
- package/src/components/HkSidebar.scss +1 -1
- package/src/components/HkStatusBar.test.tsx +26 -0
- package/src/components/HkStatusBar.tsx +8 -0
- package/src/components/HkTable.drag.test.tsx +1 -1
- package/src/components/HkTable.scss +1 -1
- package/src/components/HkTitleBar.scss +1 -1
- package/src/components/HkToast.scss +1 -1
- package/src/components/HkToast.tsx +2 -1
- package/src/components/HkTooltip.scss +1 -1
- package/src/components/HkTooltip.test.tsx +3 -3
- package/src/components/HkTooltip.tsx +2 -1
- package/src/components/__tests__/hkMarkdownLang.test.ts +29 -0
- package/src/errorReporting/HkErrorReportingOverlay.scss +1 -1
- package/src/i18n/locales/ar/components.json +1 -0
- package/src/i18n/locales/de/components.json +1 -0
- package/src/i18n/locales/en/components.json +1 -0
- package/src/i18n/locales/es/components.json +1 -0
- package/src/i18n/locales/fr/components.json +1 -0
- package/src/i18n/locales/ja/components.json +1 -0
- package/src/i18n/locales/ko/components.json +1 -0
- package/src/i18n/locales/pt/components.json +1 -0
- package/src/i18n/locales/ru/components.json +1 -0
- package/src/i18n/locales/zh-Hans/components.json +1 -0
- package/src/i18n/locales/zh-Hant/components.json +1 -0
- package/src/index.ts +5 -1
- package/src/overlay/overlayRoot.scss +35 -0
- package/src/overlay/overlayRoot.ts +74 -0
- package/src/runtime/usePopupManager.test.ts +12 -8
- package/src/runtime/usePopupManager.ts +15 -5
- package/src/scale.scss +23 -4
- package/src/styles/admin-tokens.scss +2 -2
- package/src/styles/index.scss +1 -0
- package/src/styles/theme/scale.scss +23 -4
- package/src/theme/theme.scss +0 -3
package/package.json
CHANGED
|
@@ -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-
|
|
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,
|
|
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=
|
|
179
|
+
<Teleport to={ensureOverlayRoot()}>
|
|
179
180
|
<div class="hk-blocking-toast-container">
|
|
180
181
|
<TransitionGroup
|
|
181
182
|
ref={hostRef}
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
|
|
104
104
|
.hk-checkbox-icon {
|
|
105
105
|
position: relative;
|
|
106
|
-
z-index: var(--
|
|
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(--
|
|
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(--
|
|
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
|
|
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
|
|
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=
|
|
280
|
+
<Teleport to={ensureOverlayRoot()}>
|
|
280
281
|
{props.overlay ? (
|
|
281
282
|
<div
|
|
282
283
|
ref={overlayEl}
|
|
@@ -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.
|
|
49
|
+
document.querySelector<HTMLElement>(".hk-fab");
|
|
50
50
|
const button =
|
|
51
51
|
container.querySelector<HTMLButtonElement>(".hk-fab-button") ??
|
|
52
|
-
document.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
247
|
+
// Tooltip is the topmost band (2026-10-15): no upper bound.
|
|
248
248
|
});
|
|
249
249
|
});
|