@celestia-island/hikari 0.55.162 → 0.55.163
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/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/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/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/index.ts +1 -0
- 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;
|
|
@@ -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
|
});
|
|
@@ -22,7 +22,7 @@ describe("HkFloatingLayer stylesheet contract", () => {
|
|
|
22
22
|
it("compiles the fixed, gesture-transparent top-layer shell", () => {
|
|
23
23
|
expect(css).toContain(".hk-floating-layer {");
|
|
24
24
|
expect(css).toContain("position: fixed;");
|
|
25
|
-
expect(css).toContain("z-index: var(--hk-
|
|
25
|
+
expect(css).toContain("z-index: var(--hk-overlay-z-popup, 200);");
|
|
26
26
|
// The layer never eats gestures; the widgets inside do.
|
|
27
27
|
expect(css).toContain("pointer-events: none;");
|
|
28
28
|
expect(css).toContain(".hk-floating-layer > * {");
|
|
@@ -50,7 +50,7 @@ describe("HkFloatingLayer", () => {
|
|
|
50
50
|
// tooltip band, strictly below the toast band.
|
|
51
51
|
const z = Number(root!.style.getPropertyValue("--hk-float-z"));
|
|
52
52
|
expect(z).toBeGreaterThanOrEqual(POPUP_Z_BANDS.tooltip);
|
|
53
|
-
|
|
53
|
+
// Tooltip is the topmost band (2026-10-15): no upper bound.
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
it("open=false renders nothing at all", () => {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
|
|
10
10
|
import { hasLiveWindow, usePopupManager } from "../runtime/usePopupManager";
|
|
11
11
|
import "./HkFloatingLayer.scss";
|
|
12
|
+
import { ensureOverlayRoot } from "../overlay/overlayRoot";
|
|
12
13
|
|
|
13
14
|
type FloatCorner = "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
14
15
|
|
|
@@ -99,7 +100,7 @@ export default defineComponent({
|
|
|
99
100
|
return () => {
|
|
100
101
|
if (!props.open || (props.yieldToWindows && hasLiveWindow.value)) return null;
|
|
101
102
|
return (
|
|
102
|
-
<Teleport to=
|
|
103
|
+
<Teleport to={ensureOverlayRoot()}>
|
|
103
104
|
<div
|
|
104
105
|
class="hk-floating-layer"
|
|
105
106
|
data-corner={props.corner}
|
|
@@ -82,7 +82,7 @@ export const HkLocalePicker = defineComponent({
|
|
|
82
82
|
// Local stacking inside the header's own context — the
|
|
83
83
|
// app-chrome header-popup band documents the intent (this
|
|
84
84
|
// value is scoped, it never races the popup stack).
|
|
85
|
-
zIndex: "var(--z-
|
|
85
|
+
zIndex: "var(--hk-z-float)",
|
|
86
86
|
overflow: "hidden",
|
|
87
87
|
padding: "4px",
|
|
88
88
|
}}
|
|
@@ -65,8 +65,12 @@ async function renderMarkdown(content: string): Promise<string> {
|
|
|
65
65
|
|
|
66
66
|
(renderer as Record<string, unknown>).code = function (tok: { text: string; lang?: string }) {
|
|
67
67
|
const highlighted = tryHighlight(tok.text, tok.lang);
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// Audit hikari-R1-5: whitelist the language tag before interpolation
|
|
69
|
+
// (depth-in-defence — the final DOMPurify pass remains the primary
|
|
70
|
+
// guard, but this keeps the raw template injection safe on its own).
|
|
71
|
+
const lang = tok.lang && /^[a-zA-Z0-9+#._-]{1,32}$/.test(tok.lang) ? tok.lang : "";
|
|
72
|
+
const langAttr = lang ? ` language-${lang}` : "";
|
|
73
|
+
const langLabel = lang ? `<span class="hk-md-code-lang">${lang}</span>` : "";
|
|
70
74
|
return `<div class="hk-md-code-wrap"><div class="hk-md-code">${langLabel}<pre><code class="hljs${langAttr}">${highlighted}</code></pre></div></div>`;
|
|
71
75
|
};
|
|
72
76
|
|
|
@@ -30,6 +30,7 @@ import HSpinner from "./HkSpinner";
|
|
|
30
30
|
import HIconButton from "./HkIconButton";
|
|
31
31
|
import HIcon from "./HkIcon";
|
|
32
32
|
import "./window-close.scss";
|
|
33
|
+
import { ensureOverlayRoot } from "../overlay/overlayRoot";
|
|
33
34
|
|
|
34
35
|
export interface ModalAction {
|
|
35
36
|
label: string;
|
|
@@ -1029,7 +1030,7 @@ export default defineComponent({
|
|
|
1029
1030
|
const headerShown = props.title || props.closable || slots.header || slots.headerLead;
|
|
1030
1031
|
|
|
1031
1032
|
const windowSurface = (
|
|
1032
|
-
<Teleport to=
|
|
1033
|
+
<Teleport to={ensureOverlayRoot()}>
|
|
1033
1034
|
<div
|
|
1034
1035
|
class="hk-modal-root"
|
|
1035
1036
|
style={{ zIndex: overlayZ.value }}
|
|
@@ -75,10 +75,9 @@ describe("HkModalBreadcrumb overflow fence", () => {
|
|
|
75
75
|
// The strip reaches over an anchored dropdown's panel on purpose (it
|
|
76
76
|
// must stay readable over the window it annotates); dropping it into
|
|
77
77
|
// the window band would bury it under every open sheet instead.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
expect(z).
|
|
81
|
-
expect(z).toBeLessThan(POPUP_Z_BANDS.tooltip);
|
|
78
|
+
// The canonical variable carries the band value (no inline fallback).
|
|
79
|
+
const hasVar = scss.includes("z-index: var(--hk-overlay-z-breadcrumb");
|
|
80
|
+
expect(hasVar, "the strip declares its z via the canonical variable").toBe(true);
|
|
82
81
|
});
|
|
83
82
|
|
|
84
83
|
it("opens the menu clear of the strip's own bar", () => {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/* Above every popup band's scrim (modal 1000+, dropdown 2000+; in-band
|
|
6
6
|
steps of 2 keep real stacks far below this) so multi-layer windows
|
|
7
7
|
never bury the strip; still below tooltip (3000) / toast (4000). */
|
|
8
|
-
z-index: var(--hk-
|
|
8
|
+
z-index: var(--hk-overlay-z-breadcrumb, 250);
|
|
9
9
|
display: flex;
|
|
10
10
|
align-items: center;
|
|
11
11
|
// --space-N contract tokens are N px across the library (HkTable,
|
|
@@ -20,6 +20,7 @@ import HkPopover from "./HkPopover";
|
|
|
20
20
|
import HkMenuPanel from "./HkMenuPanel";
|
|
21
21
|
import HkMenuActionItem from "./HkMenuActionItem";
|
|
22
22
|
import "./HkModalBreadcrumb.scss";
|
|
23
|
+
import { ensureOverlayRoot } from "../overlay/overlayRoot";
|
|
23
24
|
|
|
24
25
|
/** Window kinds always participate in the stack: modals and drawers block
|
|
25
26
|
* the page on every form factor, desktop and mobile alike. */
|
|
@@ -609,7 +610,7 @@ export default defineComponent({
|
|
|
609
610
|
|
|
610
611
|
return () =>
|
|
611
612
|
visible.value ? (
|
|
612
|
-
<Teleport to=
|
|
613
|
+
<Teleport to={ensureOverlayRoot()}>
|
|
613
614
|
<nav
|
|
614
615
|
ref={navRef}
|
|
615
616
|
class="hk-modal-breadcrumb"
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { usePopupManager, type PopupHandle } from "../runtime/usePopupManager";
|
|
14
14
|
import { useI18n } from "../i18n/context";
|
|
15
15
|
import "./HkPersistentToast.scss";
|
|
16
|
+
import { ensureOverlayRoot } from "../overlay/overlayRoot";
|
|
16
17
|
|
|
17
18
|
export type PersistentToastTone = "loading" | "info" | "warning" | "error";
|
|
18
19
|
|
|
@@ -266,7 +267,7 @@ export default defineComponent({
|
|
|
266
267
|
</button>
|
|
267
268
|
)}
|
|
268
269
|
{slots.detail && open.value && (
|
|
269
|
-
<Teleport to=
|
|
270
|
+
<Teleport to={ensureOverlayRoot()}>
|
|
270
271
|
<div
|
|
271
272
|
ref={cardRef}
|
|
272
273
|
id={popupId}
|
|
@@ -26,6 +26,7 @@ import HIconButton from "./HkIconButton";
|
|
|
26
26
|
import HIcon from "./HkIcon";
|
|
27
27
|
import "./window-close.scss";
|
|
28
28
|
import "./HkPopover.scss";
|
|
29
|
+
import { ensureOverlayRoot } from "../overlay/overlayRoot";
|
|
29
30
|
|
|
30
31
|
export type PopupPlacement =
|
|
31
32
|
| "top" | "top-start" | "top-end"
|
|
@@ -664,7 +665,7 @@ export default defineComponent({
|
|
|
664
665
|
const sheetBranch = activeBranch.value === "sheet";
|
|
665
666
|
const panelPrefix = sheetBranch ? "hk-popover-sheet" : "hk-popover";
|
|
666
667
|
return (
|
|
667
|
-
<Teleport to=
|
|
668
|
+
<Teleport to={ensureOverlayRoot()}>
|
|
668
669
|
{/* Window-layer contract (./_scrim-fade.scss): the sheet scrim
|
|
669
670
|
fades in place under its own transition name (machine output)
|
|
670
671
|
— it never mounts bare, so the dim curtain never snaps on or
|
|
@@ -58,7 +58,7 @@ describe("scroll-pin whitespace contract", () => {
|
|
|
58
58
|
it("the pin base is sticky with a floating-chrome surface", () => {
|
|
59
59
|
const base = cssBlock(pinScss, ".hk-scroll-pin {");
|
|
60
60
|
expect(base).toMatch(/position: sticky/);
|
|
61
|
-
expect(base).toMatch(/z-index: var\(--hk-scroll-pin-z/);
|
|
61
|
+
expect(base).toMatch(/z-index: var\(--hk-scroll-pin-z, var\(--hk-z-scrollbar/);
|
|
62
62
|
expect(base).toMatch(/background: var\(\s*--hk-scroll-pin-bg/);
|
|
63
63
|
});
|
|
64
64
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
.hk-scroll-pin {
|
|
19
19
|
position: sticky;
|
|
20
|
-
z-index: var(--hk-scroll-pin-z,
|
|
20
|
+
z-index: var(--hk-scroll-pin-z, var(--hk-z-scrollbar));
|
|
21
21
|
// Floating-chrome surface: near-opaque surface tint + blur so scrolled
|
|
22
22
|
// content never bleeds through the pinned row (same look the step-flow
|
|
23
23
|
// sticky header introduced; hosts retune through --hk-scroll-pin-bg).
|
|
@@ -3,7 +3,7 @@ import { createApp, defineComponent, h, nextTick, ref } from "vue";
|
|
|
3
3
|
import type { ComponentPublicInstance } from "vue";
|
|
4
4
|
|
|
5
5
|
import HkSelectPanel from "./HkSelectPanel";
|
|
6
|
-
import { usePopupManager } from "../runtime/usePopupManager";
|
|
6
|
+
import { usePopupManager, POPUP_Z_BANDS } from "../runtime/usePopupManager";
|
|
7
7
|
|
|
8
8
|
const mounts: ReturnType<typeof createApp>[] = [];
|
|
9
9
|
const containers: HTMLElement[] = [];
|
|
@@ -274,7 +274,7 @@ describe("HkSelectPanel custom invocation", () => {
|
|
|
274
274
|
const host = document.body.querySelector<HTMLElement>(".hk-select-popout-host")!;
|
|
275
275
|
expect(host).toBeTruthy();
|
|
276
276
|
const z = Number.parseInt(host.style.zIndex, 10);
|
|
277
|
-
expect(z).toBeGreaterThanOrEqual(
|
|
277
|
+
expect(z).toBeGreaterThanOrEqual(POPUP_Z_BANDS.dropdown);
|
|
278
278
|
});
|
|
279
279
|
|
|
280
280
|
it("places a top-start popout above the anchor and flips when cramped", async () => {
|
|
@@ -25,6 +25,7 @@ import HIconButton from "./HkIconButton";
|
|
|
25
25
|
import HIcon from "./HkIcon";
|
|
26
26
|
import "./window-close.scss";
|
|
27
27
|
import "./HkSelect.scss";
|
|
28
|
+
import { ensureOverlayRoot } from "../overlay/overlayRoot";
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* The dropdown panel of HkSelect, detached from the select's trigger so it
|
|
@@ -657,7 +658,7 @@ export default defineComponent({
|
|
|
657
658
|
if (!machine.mounted.value) return null;
|
|
658
659
|
if (activeBranch.value === "sheet") {
|
|
659
660
|
return (
|
|
660
|
-
<Teleport to=
|
|
661
|
+
<Teleport to={ensureOverlayRoot()}>
|
|
661
662
|
{/* Window-layer contract (./_scrim-fade.scss): the scrim fades
|
|
662
663
|
in place under its OWN transition name. It once shared the
|
|
663
664
|
panel's `hk-select-sheet` name, so the panel's
|
|
@@ -729,7 +730,7 @@ export default defineComponent({
|
|
|
729
730
|
// pop grows out of the edge the panel actually sits on (including
|
|
730
731
|
// after an auto-flip).
|
|
731
732
|
return (
|
|
732
|
-
<Teleport to=
|
|
733
|
+
<Teleport to={ensureOverlayRoot()}>
|
|
733
734
|
<div
|
|
734
735
|
ref={popoutHostRef}
|
|
735
736
|
class={["hk-select-popout-host", ...machine.classesFor("hk-select-popout")]}
|
|
@@ -732,3 +732,29 @@ describe("HkStatusBar", () => {
|
|
|
732
732
|
expect(scss).not.toMatch(/\.s-status-bar-version,\s*\n\s*\.s-status-bar-version-sep\s*\{/);
|
|
733
733
|
});
|
|
734
734
|
});
|
|
735
|
+
|
|
736
|
+
// ── tier "none": a local panel has no transport facts to show ────────────
|
|
737
|
+
describe("HkStatusBar tier none", () => {
|
|
738
|
+
it("hides the protocol and network rows for the local sentinel", async () => {
|
|
739
|
+
const container = mountBar({
|
|
740
|
+
connectionStatus: "connected",
|
|
741
|
+
connectionInfo: { ...INFO, tier: "none", region: "CN", latencyMs: 3 },
|
|
742
|
+
latencyMs: 3,
|
|
743
|
+
version: "0.1.0",
|
|
744
|
+
});
|
|
745
|
+
await nextTick();
|
|
746
|
+
const tag = tagOf(container);
|
|
747
|
+
tag.dispatchEvent(new MouseEvent("mouseenter"));
|
|
748
|
+
await nextTick();
|
|
749
|
+
const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel");
|
|
750
|
+
expect(panel, "popover opens on hover").toBeTruthy();
|
|
751
|
+
const text = panel?.textContent ?? "";
|
|
752
|
+
// Vacuity guard first: the connected status text renders in the panel.
|
|
753
|
+
expect(text).toContain("Connected");
|
|
754
|
+
// A local panel has no transport protocol nor a network region —
|
|
755
|
+
// those rows must not render at all for the sentinel tier.
|
|
756
|
+
expect(text).not.toContain("Protocol");
|
|
757
|
+
expect(text).not.toContain("Network");
|
|
758
|
+
expect(text).not.toContain("HTTP");
|
|
759
|
+
});
|
|
760
|
+
});
|
|
@@ -196,6 +196,10 @@ export const HkStatusBar = defineComponent({
|
|
|
196
196
|
compact: { type: Boolean, default: false },
|
|
197
197
|
onRetry: { type: Function as PropType<() => void>, default: undefined },
|
|
198
198
|
latencyMs: { type: Number, default: null },
|
|
199
|
+
// The sentinel tier "none" suppresses BOTH the protocol and the
|
|
200
|
+
// network rows — for hosts whose panel is local (the desktop shell:
|
|
201
|
+
// "connected" there only means the local face is up, so neither a
|
|
202
|
+
// transport protocol nor a network region is a fact about the link).
|
|
199
203
|
transportTier: { type: String as PropType<string>, default: undefined },
|
|
200
204
|
attemptNumber: { type: Number, default: undefined },
|
|
201
205
|
countdown: { type: Number, default: undefined },
|
|
@@ -569,6 +573,8 @@ export const HkStatusBar = defineComponent({
|
|
|
569
573
|
)}
|
|
570
574
|
</>
|
|
571
575
|
)}
|
|
576
|
+
{tier !== "none" && (
|
|
577
|
+
<>
|
|
572
578
|
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
573
579
|
<Plug size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
|
|
574
580
|
<span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.protocol", "Protocol")}</span>
|
|
@@ -585,6 +591,8 @@ export const HkStatusBar = defineComponent({
|
|
|
585
591
|
<span style={{ opacity: 0.5, marginRight: "auto" }}>{t("hikari::statusBar.network", "Network")}</span>
|
|
586
592
|
<span>{regionDisplayName(info.region, locale, t)}{info.asn != null ? ` · AS${info.asn}` : ""}{info.isLocalhost ? " · " + t("hikari::statusBar.local", "Local") : ""}</span>
|
|
587
593
|
</div>
|
|
594
|
+
</>
|
|
595
|
+
)}
|
|
588
596
|
{(props.extraDetails ?? []).map((row) => (
|
|
589
597
|
<div key={row.key} data-extra-detail={row.key} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: "6px", maxWidth: "400px" }}>
|
|
590
598
|
<span style={{ opacity: 0.5, flexShrink: 0, display: "inline-flex" }}>{row.icon}</span>
|
|
@@ -763,7 +763,7 @@ describe("HkTable grip CSS (source contract)", () => {
|
|
|
763
763
|
expect(scss).toMatch(/\.hk-table-drag-ghost\s*\{/);
|
|
764
764
|
expect(scss).toMatch(/position: fixed;/);
|
|
765
765
|
expect(scss).toMatch(/border-radius: var\(--radius-md/);
|
|
766
|
-
expect(scss).toMatch(/z-index: var\(--hk-z-drag
|
|
766
|
+
expect(scss).toMatch(/z-index: var\(--hk-z-drag\)/);
|
|
767
767
|
// The settle is a fade, driven by data-fading.
|
|
768
768
|
expect(scss).toMatch(/&\[data-fading]\s*\{/);
|
|
769
769
|
});
|
|
@@ -280,7 +280,7 @@
|
|
|
280
280
|
/* Drag ghost: above page chrome (--z-sidebar 900) but below every
|
|
281
281
|
popup band (overlay 1000 < dropdown 2000 < tooltip 3000 < toast 4000)
|
|
282
282
|
so a drag never buries toasts or open panels. */
|
|
283
|
-
z-index: var(--hk-z-drag
|
|
283
|
+
z-index: var(--hk-z-drag);
|
|
284
284
|
will-change: transform;
|
|
285
285
|
transition: opacity 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
286
286
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
inset-inline-end: var(--viewport-gutter, 16px);
|
|
7
7
|
/* Toast band — the topmost popup z band (POPUP_Z_BANDS.toast; mirror in
|
|
8
8
|
theme.scss). The live value arrives inline from the popup manager. */
|
|
9
|
-
z-index: var(--hk-z-toast,
|
|
9
|
+
z-index: var(--hk-overlay-z-toast, 100);
|
|
10
10
|
max-width: 24rem;
|
|
11
11
|
pointer-events: none;
|
|
12
12
|
}
|
|
@@ -10,6 +10,7 @@ import { useI18n } from "../i18n/context";
|
|
|
10
10
|
import { clearLeaveGeometry, pinLeaveGeometry, type LeaveBoxSnapshot } from "../utils/dom";
|
|
11
11
|
import { useSurfaceTransition } from "../composables/useSurfaceTransition";
|
|
12
12
|
import "./HkToast.scss";
|
|
13
|
+
import { ensureOverlayRoot } from "../overlay/overlayRoot";
|
|
13
14
|
|
|
14
15
|
const LONG_THRESHOLD = 50;
|
|
15
16
|
|
|
@@ -246,7 +247,7 @@ export default defineComponent({
|
|
|
246
247
|
);
|
|
247
248
|
|
|
248
249
|
return () => (
|
|
249
|
-
<Teleport to=
|
|
250
|
+
<Teleport to={ensureOverlayRoot()}>
|
|
250
251
|
<div class="hk-toast-container" style={containerStyle.value}>
|
|
251
252
|
<TransitionGroup
|
|
252
253
|
ref={hostRef}
|
|
@@ -29,7 +29,7 @@ const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement
|
|
|
29
29
|
|
|
30
30
|
function mount(props: Record<string, unknown> = {}) {
|
|
31
31
|
const container = document.createElement("div");
|
|
32
|
-
document.body.appendChild(container);
|
|
32
|
+
(document.querySelector('#hk-overlay-root') ?? document.body).appendChild(container);
|
|
33
33
|
const app = createApp({
|
|
34
34
|
render: () =>
|
|
35
35
|
h(HkTooltip, props as never, () => h("span", { class: "anchor-probe" }, "anchor")),
|
|
@@ -152,7 +152,7 @@ describe("HkTooltip show/hide", () => {
|
|
|
152
152
|
await nextTick();
|
|
153
153
|
const el = popup();
|
|
154
154
|
expect(document.body.contains(el)).toBe(true);
|
|
155
|
-
|
|
155
|
+
// The tooltip teleports into the overlay root (a child of body).
|
|
156
156
|
expect(el.className).not.toContain("hk-tooltip-visible");
|
|
157
157
|
expect(el.querySelector(".hk-tooltip-content")!.textContent).toBe("the tip");
|
|
158
158
|
// The anchor slot renders inside the wrapper's trigger span.
|
|
@@ -175,7 +175,7 @@ describe("HkTooltip show/hide", () => {
|
|
|
175
175
|
|
|
176
176
|
it("hands { popupId, visible } to the default slot for focus-target wiring", async () => {
|
|
177
177
|
const container = document.createElement("div");
|
|
178
|
-
document.body.appendChild(container);
|
|
178
|
+
(document.querySelector('#hk-overlay-root') ?? document.body).appendChild(container);
|
|
179
179
|
let seen: Partial<HkTooltipSlotProps> = {};
|
|
180
180
|
const app = createApp({
|
|
181
181
|
render: () =>
|
|
@@ -2,6 +2,7 @@ import { computed, defineComponent, onBeforeUnmount, ref, Teleport, type CSSProp
|
|
|
2
2
|
import { usePopupManager, type PopupHandle } from "../runtime/usePopupManager";
|
|
3
3
|
import { applyTooltipPosition, type TooltipPlacement } from "../runtime/tooltipPosition";
|
|
4
4
|
import "./HkTooltip.scss";
|
|
5
|
+
import { ensureOverlayRoot } from "../overlay/overlayRoot";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* The default-slot payload, handed to every render of the trigger. An
|
|
@@ -183,7 +184,7 @@ export default defineComponent({
|
|
|
183
184
|
<span class="hk-tooltip-trigger">
|
|
184
185
|
{slots.default?.({ popupId, visible: visible.value })}
|
|
185
186
|
</span>
|
|
186
|
-
<Teleport to=
|
|
187
|
+
<Teleport to={ensureOverlayRoot()}>
|
|
187
188
|
{everShown.value && (
|
|
188
189
|
<div
|
|
189
190
|
ref={popupRef}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Audit hikari-R1-5: the lang whitelist pattern used by HkMarkdownRenderer.
|
|
4
|
+
const LANG_OK = /^[a-zA-Z0-9+#._-]{1,32}$/;
|
|
5
|
+
|
|
6
|
+
describe("markdown lang whitelist", () => {
|
|
7
|
+
it("accepts common language tags", () => {
|
|
8
|
+
for (const lang of ["rust", "ts", "c++", "c#", "objective-c", "json5", "node.v2"]) {
|
|
9
|
+
expect(LANG_OK.test(lang)).toBe(true);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("rejects injection-shaped tags", () => {
|
|
14
|
+
for (const lang of [
|
|
15
|
+
"<script>alert(1)</script>",
|
|
16
|
+
'" onload="alert(1)',
|
|
17
|
+
"<img src=x onerror=alert(1)>",
|
|
18
|
+
"a b",
|
|
19
|
+
"a=b",
|
|
20
|
+
]) {
|
|
21
|
+
expect(LANG_OK.test(lang)).toBe(false);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("caps length", () => {
|
|
26
|
+
expect(LANG_OK.test("a".repeat(33))).toBe(false);
|
|
27
|
+
expect(LANG_OK.test("a".repeat(32))).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -799,3 +799,4 @@ export { type HAboutLinkFace as HkAboutLinkFace } from "./components/HkAboutModa
|
|
|
799
799
|
export { type HAboutLinkIcon as HkAboutLinkIcon } from "./components/HkAboutModal";
|
|
800
800
|
export { type HAboutComponentVersion as HkAboutComponentVersion } from "./components/HkAboutModal";
|
|
801
801
|
export { type HCustomTheme as HkCustomTheme } from "./components/HkColorSchemeDialog";
|
|
802
|
+
export { OVERLAY_ROOT_SELECTOR, OVERLAY_Z, ensureOverlayRoot } from "./overlay/overlayRoot";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlay root styles — the isolated stacking context and its internal
|
|
3
|
+
* ordinal z-index bands. These values NEVER compete with the document's
|
|
4
|
+
* global stacking space; the `isolation: isolate` fence guarantees it.
|
|
5
|
+
*
|
|
6
|
+
* The ordinal variables are defined on the root so every Teleported
|
|
7
|
+
* child inherits them — components use `var(--hk-overlay-z-toast)` etc.
|
|
8
|
+
* inside their sheets and never need to know the numeric value.
|
|
9
|
+
*/
|
|
10
|
+
#hk-overlay-root {
|
|
11
|
+
/* Fences the root's z-index space from the global stacking order —
|
|
12
|
+
* the entire reason this container exists. */
|
|
13
|
+
isolation: isolate;
|
|
14
|
+
|
|
15
|
+
/* The root itself doesn't intercept pointer events; each teleported
|
|
16
|
+
* child restores its own pointer-events. */
|
|
17
|
+
pointer-events: none;
|
|
18
|
+
|
|
19
|
+
/* Children are position: fixed overlays — the root itself is a
|
|
20
|
+
* zero-size anchor, not a visible surface. */
|
|
21
|
+
position: relative;
|
|
22
|
+
width: 0;
|
|
23
|
+
height: 0;
|
|
24
|
+
overflow: visible;
|
|
25
|
+
|
|
26
|
+
/* ── Internal ordinal bands (overlayRoot.ts is the source of truth) ── */
|
|
27
|
+
--hk-overlay-z-modal-backdrop: 5;
|
|
28
|
+
--hk-overlay-z-modal-frame: 10;
|
|
29
|
+
--hk-overlay-z-blocking-toast: 50;
|
|
30
|
+
--hk-overlay-z-toast: 100;
|
|
31
|
+
--hk-overlay-z-veil: 150;
|
|
32
|
+
--hk-overlay-z-popup: 200;
|
|
33
|
+
--hk-overlay-z-breadcrumb: 250;
|
|
34
|
+
--hk-overlay-z-tooltip: 300;
|
|
35
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The overlay root — a single `isolation: isolate` container appended to
|
|
3
|
+
* `<body>` that every floating layer (modal, toast, tooltip, select
|
|
4
|
+
* popout, context menu) Teleports into.
|
|
5
|
+
*
|
|
6
|
+
* Inside this root, z-index values only compete with each other (the
|
|
7
|
+
* `isolation` property creates a new stacking context), so overlay types
|
|
8
|
+
* use simple ordinals (1 = lowest … 10 = highest) instead of the old
|
|
9
|
+
* global thousand-scale bands. A tooltip at z-index 8 inside the root
|
|
10
|
+
* always paints above a modal at z-index 3 inside the root, regardless
|
|
11
|
+
* of what the global stacking space looks like outside.
|
|
12
|
+
*
|
|
13
|
+
* The root is created lazily on first use (`ensureOverlayRoot()`), so
|
|
14
|
+
* consumers don't need to include a bootstrap component — any Teleport
|
|
15
|
+
* target reference triggers creation via the module-level side effect
|
|
16
|
+
* in `OVERLAY_ROOT_SELECTOR`.
|
|
17
|
+
*
|
|
18
|
+
* ## Ordinal bands (internal to the root — never leak out)
|
|
19
|
+
*
|
|
20
|
+
* | z-index | Layer | Examples |
|
|
21
|
+
* |---------|----------------|---------------------------------------|
|
|
22
|
+
* | 1 | modal backdrop | HkModal's fixed backdrop |
|
|
23
|
+
* | 2 | modal frame | HkModal body, dialogs |
|
|
24
|
+
* | 3 | blocking toast | HkBlockingToast |
|
|
25
|
+
* | 4 | toast | HkToast, persistent toasts |
|
|
26
|
+
* | 5 | loading veil | HkLoadingVeil |
|
|
27
|
+
* | 6 | anchored popup | HkSelectPanel popout, menus, popover |
|
|
28
|
+
* | 7 | breadcrumb | HkModalBreadcrumb strip |
|
|
29
|
+
* | 8 | tooltip | HkTooltip (always on top) |
|
|
30
|
+
*
|
|
31
|
+
* Components that need to layer within the SAME band (e.g. multiple
|
|
32
|
+
* stacked modals) use their own local stacking mechanisms (the modal
|
|
33
|
+
* popup manager already assigns incremental z-values).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** The CSS selector for Teleport `to` attributes. */
|
|
37
|
+
export const OVERLAY_ROOT_SELECTOR = "#hk-overlay-root";
|
|
38
|
+
|
|
39
|
+
/** Ordinal z-index bands — the ONLY z-index values inside the root. */
|
|
40
|
+
export const OVERLAY_Z = {
|
|
41
|
+
modalBackdrop: 5,
|
|
42
|
+
modalFrame: 10,
|
|
43
|
+
blockingToast: 50,
|
|
44
|
+
toast: 100,
|
|
45
|
+
loadingVeil: 150,
|
|
46
|
+
anchoredPopup: 200,
|
|
47
|
+
breadcrumb: 250,
|
|
48
|
+
tooltip: 300,
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Idempotently create and append the overlay root to `<body>`.
|
|
53
|
+
* Called on module load — the first `import` of this module guarantees
|
|
54
|
+
* the DOM node exists before any Teleport resolves its target.
|
|
55
|
+
*/
|
|
56
|
+
export function ensureOverlayRoot(): HTMLElement {
|
|
57
|
+
if (typeof document === "undefined") return null as unknown as HTMLElement;
|
|
58
|
+
let root = document.querySelector<HTMLElement>(OVERLAY_ROOT_SELECTOR);
|
|
59
|
+
if (!root) {
|
|
60
|
+
root = document.createElement("div");
|
|
61
|
+
root.id = "hk-overlay-root";
|
|
62
|
+
root.setAttribute("aria-hidden", "true");
|
|
63
|
+
// Inline styles — this element is created BEFORE any stylesheet
|
|
64
|
+
// loads, so it must be self-sufficient. The `isolation` is the
|
|
65
|
+
// whole point: it fences the root's internal z-index space from
|
|
66
|
+
// the document's global stacking order.
|
|
67
|
+
root.style.cssText = "isolation:isolate;pointer-events:none;";
|
|
68
|
+
document.body.appendChild(root);
|
|
69
|
+
}
|
|
70
|
+
return root;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Module-level side effect: any import of this module ensures the root.
|
|
74
|
+
ensureOverlayRoot();
|
|
@@ -29,28 +29,31 @@ afterEach(() => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
describe("usePopupManager z bands", () => {
|
|
32
|
-
it("bands are ordered
|
|
32
|
+
it("bands are ordered modal < toast < dropdown < tooltip", () => {
|
|
33
|
+
// 2026-10-15 overlay-root ordinal bands: tooltips sit ABOVE toasts
|
|
34
|
+
// (user direction — tooltip text must be copyable while toasts show),
|
|
35
|
+
// and toasts sit above modals (notifications visible over dialogs).
|
|
33
36
|
expect(POPUP_Z_BANDS.modal).toBe(POPUP_Z_BANDS.drawer);
|
|
34
|
-
expect(POPUP_Z_BANDS.modal).toBeLessThan(POPUP_Z_BANDS.
|
|
37
|
+
expect(POPUP_Z_BANDS.modal).toBeLessThan(POPUP_Z_BANDS.toast);
|
|
38
|
+
expect(POPUP_Z_BANDS.toast).toBeLessThan(POPUP_Z_BANDS.dropdown);
|
|
35
39
|
expect(POPUP_Z_BANDS.dropdown).toBeLessThan(POPUP_Z_BANDS.tooltip);
|
|
36
|
-
expect(POPUP_Z_BANDS.tooltip).toBeLessThan(POPUP_Z_BANDS.toast);
|
|
37
40
|
});
|
|
38
41
|
|
|
39
42
|
it("keeps a toast registered first above overlays opened later", () => {
|
|
40
43
|
// The mobile regression: HkToast mounts once at shell level, so the
|
|
41
44
|
// old registration-order ladder gave every later modal a higher z and
|
|
42
45
|
// buried the toast under the phone bottom sheet. Bands must hold the
|
|
43
|
-
// toast on top regardless of
|
|
46
|
+
// toast on top of WINDOW kinds (modal/drawer) regardless of
|
|
47
|
+
// registration order. Anchored popups (dropdown) sit above toasts
|
|
48
|
+
// by design (2026-10-15: the user is interacting with them).
|
|
44
49
|
const m = freshManager();
|
|
45
50
|
const toast = m.register("toast", false);
|
|
46
51
|
expect(toast.zIndex).toBe(POPUP_Z_BANDS.toast);
|
|
47
52
|
|
|
48
53
|
const modal = m.register("modal", true);
|
|
49
54
|
const drawer = m.register("drawer", true);
|
|
50
|
-
const dropdown = m.register("dropdown", false);
|
|
51
55
|
expect(modal.zIndex).toBeLessThan(toast.zIndex);
|
|
52
56
|
expect(drawer.zIndex).toBeLessThan(toast.zIndex);
|
|
53
|
-
expect(dropdown.zIndex).toBeLessThan(toast.zIndex);
|
|
54
57
|
});
|
|
55
58
|
|
|
56
59
|
it("stacks in open order within a band and reclaims freed slots", () => {
|
|
@@ -108,14 +111,15 @@ describe("usePopupManager z bands", () => {
|
|
|
108
111
|
expect(modal2.zIndex).toBeLessThan(dropdown.zIndex);
|
|
109
112
|
});
|
|
110
113
|
|
|
111
|
-
it("keeps tooltips above overlays and
|
|
114
|
+
it("keeps tooltips above overlays and toasts", () => {
|
|
112
115
|
const m = freshManager();
|
|
113
116
|
const modal = m.register("modal", true);
|
|
114
117
|
const tooltip = m.register("tooltip", false);
|
|
115
118
|
const toast = m.register("toast", false);
|
|
116
119
|
expect(tooltip.zIndex).toBe(POPUP_Z_BANDS.tooltip);
|
|
117
120
|
expect(tooltip.zIndex).toBeGreaterThan(modal.zIndex);
|
|
118
|
-
expect(
|
|
121
|
+
expect(tooltip.zIndex).toBeGreaterThan(toast.zIndex);
|
|
122
|
+
// Removed: toast is BELOW tooltip in the 2026-10-15 band layout.
|
|
119
123
|
});
|
|
120
124
|
|
|
121
125
|
it("keeps the scroll lock counter balanced across register/unregister", () => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { computed, readonly, ref } from "vue";
|
|
2
2
|
|
|
3
3
|
import { reportHkRuntime, type HkRuntimeHandle } from "./registry";
|
|
4
|
+
// Side effect: ensures #hk-overlay-root exists before any popup registers.
|
|
5
|
+
import { ensureOverlayRoot } from "../overlay/overlayRoot";
|
|
4
6
|
|
|
5
7
|
export type PopupKind = "dropdown" | "modal" | "drawer" | "tooltip" | "toast";
|
|
6
8
|
|
|
@@ -51,12 +53,20 @@ export type PopupKind = "dropdown" | "modal" | "drawer" | "tooltip" | "toast";
|
|
|
51
53
|
* Keep the SCSS fallbacks (`--hi-z-*` / `--hk-z-*` in theme.scss and the
|
|
52
54
|
* component sheets) in sync with these numbers.
|
|
53
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* The band each popup KIND stacks in. These are ORDINAL values inside
|
|
58
|
+
* the overlay root's `isolation: isolate` fence — they never compete
|
|
59
|
+
* with the document's global stacking space. The overlay root module
|
|
60
|
+
* (`overlay/overlayRoot.ts`) is the single source of truth; these
|
|
61
|
+
* constants mirror `OVERLAY_Z` so the popup manager's arithmetic
|
|
62
|
+
* (band + slot × step) stays local.
|
|
63
|
+
*/
|
|
54
64
|
export const POPUP_Z_BANDS: Record<PopupKind, number> = {
|
|
55
|
-
modal:
|
|
56
|
-
drawer:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
modal: 10, // window band (modals + drawers) — STEP=2 gives 45 stack slots
|
|
66
|
+
drawer: 10, // drawers share the window band (same stacking semantics)
|
|
67
|
+
toast: 100, // toast band — far above any conceivable window stack
|
|
68
|
+
dropdown: 200, // anchored popup band — above toasts (user is interacting)
|
|
69
|
+
tooltip: 300, // tooltip band — always on top
|
|
60
70
|
};
|
|
61
71
|
|
|
62
72
|
/**
|
package/src/scale.scss
CHANGED
|
@@ -64,10 +64,29 @@
|
|
|
64
64
|
--hi-duration-fast: 0.15s;
|
|
65
65
|
--hi-ease-default: cubic-bezier(0.4, 0, 0.2, 1);
|
|
66
66
|
|
|
67
|
-
/* Z-index bands
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
/* Z-index bands — ONE definition per name; every component that needs
|
|
68
|
+
* a stacking layer consumes these variables. The bands are ordered from
|
|
69
|
+
* base content (0) to the window titlebar (100001); a higher band
|
|
70
|
+
* always paints above a lower one. Tooltips sit above toasts AND
|
|
71
|
+
* modals so their text stays readable and copyable while any overlay
|
|
72
|
+
* is showing (user direction 2026-10-15). */
|
|
73
|
+
--hk-z-base: 0; /* base content stacking context */
|
|
74
|
+
--hk-z-above: 1; /* local bump: checkbox marks, switch thumbs */
|
|
75
|
+
--hk-z-scrollbar: 10; /* scrollbar tracks and corner rulers */
|
|
76
|
+
--hk-z-fab: 40; /* floating action button */
|
|
77
|
+
--hk-z-toolbar: 50; /* zoom toolbar / local overlay strips */
|
|
78
|
+
--hk-z-header: 100; /* sticky header */
|
|
79
|
+
--hk-z-sidebar: 900; /* sidebar drawer */
|
|
80
|
+
--hk-z-drag: 950; /* element being dragged floats above its row */
|
|
81
|
+
--hk-z-breadcrumb: 2500; /* modal breadcrumb bar */
|
|
82
|
+
--hk-z-float: 3000; /* floating panels: select popout, popover, anchored layer */
|
|
83
|
+
--hk-z-toast: 4000; /* toast notifications */
|
|
84
|
+
--hk-z-veil: 4100; /* full-screen loading veil (above toast) */
|
|
85
|
+
--hk-z-modal: 5000; /* modal backdrop + frame */
|
|
86
|
+
--hk-z-tooltip: 6000; /* tooltip — above toasts and modals so its text
|
|
87
|
+
is always readable and copyable */
|
|
88
|
+
--hk-z-error: 95000; /* error landing overlay (covers everything) */
|
|
89
|
+
--hk-z-titlebar: 100001; /* window titlebar (Tauri shell) */
|
|
71
90
|
|
|
72
91
|
/* Viewport gutter — the minimum distance every floating layer (tooltip,
|
|
73
92
|
* popover, menu, select popout, modal frame, toasts) keeps from the
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
background: rgb(var(--color-surface));
|
|
87
87
|
backdrop-filter: blur(var(--blur-md));
|
|
88
88
|
border-top: 1px solid var(--border-faint, rgb(var(--color-border) / 10%));
|
|
89
|
-
z-index: var(--z-
|
|
89
|
+
z-index: var(--hk-z-toolbar);
|
|
90
90
|
flex-shrink: 0;
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -391,7 +391,7 @@
|
|
|
391
391
|
/* ── Glass header ─ */
|
|
392
392
|
.s-glass-header {
|
|
393
393
|
position: relative;
|
|
394
|
-
z-index: var(--z-header);
|
|
394
|
+
z-index: var(--hk-z-header);
|
|
395
395
|
display: flex;
|
|
396
396
|
align-items: center;
|
|
397
397
|
/* Horizontal rhythm + gap live in the class (not in host utilities):
|
package/src/styles/index.scss
CHANGED
|
@@ -66,10 +66,29 @@
|
|
|
66
66
|
--hi-duration-fast: 0.15s;
|
|
67
67
|
--hi-ease-default: cubic-bezier(0.4, 0, 0.2, 1);
|
|
68
68
|
|
|
69
|
-
/* Z-index bands
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
/* Z-index bands — ONE definition per name; every component that needs
|
|
70
|
+
* a stacking layer consumes these variables. The bands are ordered from
|
|
71
|
+
* base content (0) to the window titlebar (100001); a higher band
|
|
72
|
+
* always paints above a lower one. Tooltips sit above toasts AND
|
|
73
|
+
* modals so their text stays readable and copyable while any overlay
|
|
74
|
+
* is showing (user direction 2026-10-15). */
|
|
75
|
+
--hk-z-base: 0; /* base content stacking context */
|
|
76
|
+
--hk-z-above: 1; /* local bump: checkbox marks, switch thumbs */
|
|
77
|
+
--hk-z-scrollbar: 10; /* scrollbar tracks and corner rulers */
|
|
78
|
+
--hk-z-fab: 40; /* floating action button */
|
|
79
|
+
--hk-z-toolbar: 50; /* zoom toolbar / local overlay strips */
|
|
80
|
+
--hk-z-header: 100; /* sticky header */
|
|
81
|
+
--hk-z-sidebar: 900; /* sidebar drawer */
|
|
82
|
+
--hk-z-drag: 950; /* element being dragged floats above its row */
|
|
83
|
+
--hk-z-breadcrumb: 2500; /* modal breadcrumb bar */
|
|
84
|
+
--hk-z-float: 3000; /* floating panels: select popout, popover, anchored layer */
|
|
85
|
+
--hk-z-toast: 4000; /* toast notifications */
|
|
86
|
+
--hk-z-veil: 4100; /* full-screen loading veil (above toast) */
|
|
87
|
+
--hk-z-modal: 5000; /* modal backdrop + frame */
|
|
88
|
+
--hk-z-tooltip: 6000; /* tooltip — above toasts and modals so its text
|
|
89
|
+
is always readable and copyable */
|
|
90
|
+
--hk-z-error: 95000; /* error landing overlay (covers everything) */
|
|
91
|
+
--hk-z-titlebar: 100001; /* window titlebar (Tauri shell) */
|
|
73
92
|
|
|
74
93
|
/* Viewport gutter — the minimum distance every floating layer (tooltip,
|
|
75
94
|
* popover, menu, select popout, modal frame, toasts) keeps from the
|
package/src/theme/theme.scss
CHANGED
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
// values inline; these tokens are the static fallbacks used before a
|
|
12
12
|
// handle resolves or when a host renders the sheets without JS).
|
|
13
13
|
// overlay (modal/drawer) < dropdown < tooltip < toast (topmost).
|
|
14
|
-
--hi-z-overlay: 1000;
|
|
15
|
-
--hi-z-dropdown: 2000;
|
|
16
|
-
--hi-z-tooltip: 3000;
|
|
17
14
|
--hk-z-toast: 4000;
|
|
18
15
|
// Drag ghosts float above page chrome (--z-sidebar 900) but below
|
|
19
16
|
// every popup band.
|