@celestia-island/hikari 0.40.7 → 0.40.9
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/HkContextRing.scss +9 -0
- package/src/components/HkModal.scss +10 -5
- package/src/components/HkModal.sheetgap.test.ts +6 -5
- package/src/components/HkModelCatalog.test.ts +51 -0
- package/src/components/HkModelCatalog.ts +53 -0
- package/src/components/HkPopover.scss +76 -8
- package/src/components/HkPopover.sheetdock.test.ts +5 -3
- package/src/components/HkPopover.tsx +40 -11
- package/src/components/HkPopoverSheet.test.tsx +46 -0
- package/src/components/HkSelect.scss +17 -8
- package/src/components/HkSelect.sheetdock.test.ts +5 -3
package/package.json
CHANGED
|
@@ -36,6 +36,15 @@
|
|
|
36
36
|
color: rgb(var(--color-text));
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/* Mobile sheet mode: the docked sheet spans the viewport, so the fixed
|
|
40
|
+
* 18rem pop would hug the sheet's left edge with dead space to the right.
|
|
41
|
+
* Fill the sheet instead — the panel chrome keeps its own symmetric
|
|
42
|
+
* horizontal gutters (the glass content padding), so the bar and legend
|
|
43
|
+
* get equal left/right insets. Desktop stays anchored at 18rem. */
|
|
44
|
+
.hk-popover-panel.hk-is-sheet .hk-ctx-pop {
|
|
45
|
+
width: 100%;
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
.hk-ctx-pop-model {
|
|
40
49
|
display: flex;
|
|
41
50
|
}
|
|
@@ -173,6 +173,10 @@
|
|
|
173
173
|
white-space: nowrap;
|
|
174
174
|
overflow: hidden;
|
|
175
175
|
text-overflow: ellipsis;
|
|
176
|
+
// Window chrome, not content — titles never participate in text
|
|
177
|
+
// selection (2026-09-04 user report alongside the sheet chrome pass).
|
|
178
|
+
user-select: none;
|
|
179
|
+
-webkit-user-select: none;
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
.hk-modal-close {
|
|
@@ -339,9 +343,10 @@
|
|
|
339
343
|
// modal rendered ~60vh of blank body under three rows). The top inset
|
|
340
344
|
// now only bounds the sheet via the max-height cap below.
|
|
341
345
|
top: auto;
|
|
342
|
-
// --hk-sheet-bottom-gap
|
|
343
|
-
// bottom edge
|
|
344
|
-
|
|
346
|
+
// --hk-sheet-bottom-gap: hosts may lift the docked sheet off the very
|
|
347
|
+
// bottom edge; the family default sits flush on it (2026-09-04 user
|
|
348
|
+
// report: even the tightened 0.375rem default still read as a seam).
|
|
349
|
+
bottom: var(--hk-sheet-bottom-gap, 0px);
|
|
345
350
|
left: 0;
|
|
346
351
|
right: 0;
|
|
347
352
|
transform: none;
|
|
@@ -356,10 +361,10 @@
|
|
|
356
361
|
// Plain-vh fallback first: dvh-less engines (older Android WebView /
|
|
357
362
|
// Tauri) would drop the whole dvh calc and leave the sheet uncapped.
|
|
358
363
|
max-height: calc(
|
|
359
|
-
100vh - var(--hk-sheet-top-inset, 4.25rem) - var(--hk-sheet-bottom-gap,
|
|
364
|
+
100vh - var(--hk-sheet-top-inset, 4.25rem) - var(--hk-sheet-bottom-gap, 0px)
|
|
360
365
|
);
|
|
361
366
|
max-height: calc(
|
|
362
|
-
100dvh - var(--hk-sheet-top-inset, 4.25rem) - var(--hk-sheet-bottom-gap,
|
|
367
|
+
100dvh - var(--hk-sheet-top-inset, 4.25rem) - var(--hk-sheet-bottom-gap, 0px)
|
|
363
368
|
);
|
|
364
369
|
}
|
|
365
370
|
|
|
@@ -21,13 +21,14 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
21
21
|
const src = readFileSync(join(here, "HkModal.scss"), "utf-8");
|
|
22
22
|
|
|
23
23
|
describe("HkModal mobile sheet spacing contract", () => {
|
|
24
|
-
it("sizes the docked sheet from --hk-sheet-bottom-gap (
|
|
24
|
+
it("sizes the docked sheet from --hk-sheet-bottom-gap (host-tunable hook)", () => {
|
|
25
25
|
const block = src.slice(src.indexOf("@media (max-width: 767px)"));
|
|
26
26
|
expect(block).toContain("bottom: var(--hk-sheet-bottom-gap");
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
27
|
+
// Family default sits flush on the bottom edge; hosts may re-add a
|
|
28
|
+
// gap via the same custom property. (History: 2026-08-29 tightened
|
|
29
|
+
// 0.5rem → 0.375rem; 2026-09-04 user report — the remaining sliver
|
|
30
|
+
// still read as a visible seam, so the default settled on 0.)
|
|
31
|
+
expect(block).toMatch(/--hk-sheet-bottom-gap,\s*0px/);
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
it("keeps the footer gap host-tunable and stacks the safe-area inset", () => {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { getModelMeta, registerModelCatalog } from "./HkModelCatalog";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The simulated-world roster (integration world_service `agent_models`):
|
|
7
|
+
* every model id a chest card can carry must resolve a context window, or
|
|
8
|
+
* its context ring degrades to the empty "–" state — the "only the sample
|
|
9
|
+
* card shows usage" regression. The builtin catalog is the fallback that
|
|
10
|
+
* keeps unknown-to-nothing lineups legible.
|
|
11
|
+
*/
|
|
12
|
+
const WORLD_LINEUP = [
|
|
13
|
+
"glm-5.3-flash",
|
|
14
|
+
"claude-opus-5",
|
|
15
|
+
"deepseek-v4-pro",
|
|
16
|
+
"deepseek-v4-flash",
|
|
17
|
+
"gemini-3.1-flash-preview",
|
|
18
|
+
"gpt-5.5-pro",
|
|
19
|
+
"gemini-3.1-pro-preview",
|
|
20
|
+
"gpt-5.4",
|
|
21
|
+
"gpt-5.4-nano",
|
|
22
|
+
"claude-haiku-4-5",
|
|
23
|
+
"gemini-3-flash",
|
|
24
|
+
"qwen3.7:14b",
|
|
25
|
+
"qwen3.8:27b",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
describe("HkModelCatalog builtin specs", () => {
|
|
29
|
+
it("resolves every world-lineup model id to a positive context window", () => {
|
|
30
|
+
for (const model of WORLD_LINEUP) {
|
|
31
|
+
const meta = getModelMeta(model);
|
|
32
|
+
expect(meta, `${model} resolves`).toBeDefined();
|
|
33
|
+
expect(meta?.contextWindow ?? 0, `${model} window`).toBeGreaterThan(0);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("keeps ollama-style colon tags whole (only # splits)", () => {
|
|
38
|
+
expect(getModelMeta("qwen3.7:14b")?.contextWindow).toBe(262_144);
|
|
39
|
+
expect(getModelMeta("qwen3.8:27b")?.contextWindow).toBe(262_144);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("still strips a #tag before lookup", () => {
|
|
43
|
+
expect(getModelMeta("deepseek-v4-pro#2")?.contextWindow).toBe(128_000);
|
|
44
|
+
expect(getModelMeta("claude-opus-5#7")?.contextWindow).toBe(200_000);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("lets the registered catalog win over the builtin entries", () => {
|
|
48
|
+
registerModelCatalog({ "claude-opus-5": { contextWindow: 999 } });
|
|
49
|
+
expect(getModelMeta("claude-opus-5")?.contextWindow).toBe(999);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -64,6 +64,59 @@ const BUILTIN_CATALOG: HModelCatalog = {
|
|
|
64
64
|
pricing: { in: 2, out: 8 },
|
|
65
65
|
vision: true, reasoning: true, tools: true,
|
|
66
66
|
},
|
|
67
|
+
"glm-5.3-flash": {
|
|
68
|
+
contextWindow: 128_000, maxOutput: 32_000,
|
|
69
|
+
pricing: { in: 0.5, out: 2 },
|
|
70
|
+
vision: false, reasoning: true, tools: true,
|
|
71
|
+
},
|
|
72
|
+
"gpt-5.5-pro": {
|
|
73
|
+
contextWindow: 400_000, maxOutput: 100_000,
|
|
74
|
+
pricing: { in: 10, out: 50, cached: 1 },
|
|
75
|
+
vision: true, reasoning: true, tools: true,
|
|
76
|
+
},
|
|
77
|
+
"gpt-5.4": {
|
|
78
|
+
contextWindow: 400_000, maxOutput: 64_000,
|
|
79
|
+
pricing: { in: 2.5, out: 15, cached: 0.25 },
|
|
80
|
+
vision: true, reasoning: true, tools: true,
|
|
81
|
+
},
|
|
82
|
+
"gpt-5.4-nano": {
|
|
83
|
+
contextWindow: 400_000, maxOutput: 16_000,
|
|
84
|
+
pricing: { in: 0.2, out: 1.2, cached: 0.05 },
|
|
85
|
+
vision: false, reasoning: false, tools: true,
|
|
86
|
+
},
|
|
87
|
+
"claude-opus-5": {
|
|
88
|
+
contextWindow: 200_000, maxOutput: 64_000,
|
|
89
|
+
pricing: { in: 5, out: 25, cached: 0.5 },
|
|
90
|
+
vision: true, reasoning: true, tools: true,
|
|
91
|
+
},
|
|
92
|
+
"claude-haiku-4-5": {
|
|
93
|
+
contextWindow: 200_000, maxOutput: 32_000,
|
|
94
|
+
pricing: { in: 0.8, out: 4, cached: 0.08 },
|
|
95
|
+
vision: false, reasoning: false, tools: true,
|
|
96
|
+
},
|
|
97
|
+
"gemini-3.1-pro-preview": {
|
|
98
|
+
contextWindow: 1_000_000, maxOutput: 64_000,
|
|
99
|
+
pricing: { in: 5, out: 30 },
|
|
100
|
+
vision: true, reasoning: true, tools: true,
|
|
101
|
+
},
|
|
102
|
+
"gemini-3.1-flash-preview": {
|
|
103
|
+
contextWindow: 1_000_000, maxOutput: 64_000,
|
|
104
|
+
pricing: { in: 1.5, out: 10 },
|
|
105
|
+
vision: true, reasoning: true, tools: true,
|
|
106
|
+
},
|
|
107
|
+
"gemini-3-flash": {
|
|
108
|
+
contextWindow: 1_000_000, maxOutput: 32_000,
|
|
109
|
+
pricing: { in: 1.5, out: 9 },
|
|
110
|
+
vision: true, reasoning: false, tools: true,
|
|
111
|
+
},
|
|
112
|
+
"qwen3.7:14b": {
|
|
113
|
+
contextWindow: 262_144, maxOutput: 32_768,
|
|
114
|
+
vision: false, reasoning: true, tools: true,
|
|
115
|
+
},
|
|
116
|
+
"qwen3.8:27b": {
|
|
117
|
+
contextWindow: 262_144, maxOutput: 32_768,
|
|
118
|
+
vision: false, reasoning: true, tools: true,
|
|
119
|
+
},
|
|
67
120
|
};
|
|
68
121
|
|
|
69
122
|
/**
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
72vh,
|
|
101
101
|
calc(
|
|
102
102
|
100vh - var(--hk-sheet-top-inset, 4.25rem) -
|
|
103
|
-
var(--hk-sheet-bottom-gap,
|
|
103
|
+
var(--hk-sheet-bottom-gap, 0px)
|
|
104
104
|
)
|
|
105
105
|
);
|
|
106
106
|
display: flex;
|
|
@@ -137,6 +137,10 @@
|
|
|
137
137
|
cursor: pointer;
|
|
138
138
|
display: flex;
|
|
139
139
|
justify-content: center;
|
|
140
|
+
// Window chrome, not content — a long-press must never start a text
|
|
141
|
+
// selection over the drag handle.
|
|
142
|
+
user-select: none;
|
|
143
|
+
-webkit-user-select: none;
|
|
140
144
|
|
|
141
145
|
&::before {
|
|
142
146
|
content: "";
|
|
@@ -147,17 +151,81 @@
|
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
|
|
154
|
+
/* Sheet heading row — title on the left, explicit close on the right,
|
|
155
|
+
* one vertically-centered flex line (2026-09-04 user report: the bare
|
|
156
|
+
* title band read too small, hung right of the content it named, and
|
|
157
|
+
* long-press selected it; every other window in the app carries an ✕).
|
|
158
|
+
* The title aligns to the CONTENT edge: the leading inset defaults to
|
|
159
|
+
* --space-12, matching the padded content blocks popover sheets host
|
|
160
|
+
* (e.g. HkContextRing's 0.75rem body). Hosts tune it via
|
|
161
|
+
* --hk-sheet-title-inset; the trailing inset hugs the close button. */
|
|
162
|
+
.hk-popover-sheet-header {
|
|
151
163
|
flex-shrink: 0;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
padding: 0 var(--space-
|
|
156
|
-
var(--hk-sheet-title-inset,
|
|
157
|
-
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
gap: var(--space-8, 0.5rem);
|
|
167
|
+
padding: 0 var(--space-8, 0.5rem) var(--space-8, 0.5rem)
|
|
168
|
+
var(--hk-sheet-title-inset, var(--space-12, 0.75rem));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.hk-popover-sheet-title {
|
|
172
|
+
flex: 1;
|
|
173
|
+
min-width: 0;
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
white-space: nowrap;
|
|
176
|
+
text-overflow: ellipsis;
|
|
177
|
+
// Same heading grammar as the select sheet title: semibold, slightly
|
|
178
|
+
// letterspaced — one step up from the old 0.75rem so the sheet names
|
|
179
|
+
// itself at body-text size, and window chrome never participates in
|
|
180
|
+
// text selection.
|
|
181
|
+
font-size: var(--text-base, 0.875rem);
|
|
158
182
|
font-weight: 600;
|
|
159
183
|
letter-spacing: 0.02em;
|
|
160
184
|
color: rgb(var(--color-muted, 140 140 140));
|
|
185
|
+
user-select: none;
|
|
186
|
+
-webkit-user-select: none;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.hk-popover-sheet-close {
|
|
190
|
+
flex-shrink: 0;
|
|
191
|
+
// Pushes to the right edge with or without a title beside it.
|
|
192
|
+
margin-left: auto;
|
|
193
|
+
width: 2rem;
|
|
194
|
+
height: 2rem;
|
|
195
|
+
display: inline-flex;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
padding: 0;
|
|
199
|
+
background: transparent;
|
|
200
|
+
border: none;
|
|
201
|
+
border-radius: var(--hi-radius-md, 0.5rem);
|
|
202
|
+
color: var(--hk-modal-close-color, var(--hi-color-text-secondary, #666));
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
user-select: none;
|
|
205
|
+
-webkit-user-select: none;
|
|
206
|
+
transition:
|
|
207
|
+
background 0.15s ease,
|
|
208
|
+
color 0.15s ease;
|
|
209
|
+
|
|
210
|
+
&:hover {
|
|
211
|
+
background: var(
|
|
212
|
+
--hk-modal-close-hover-bg,
|
|
213
|
+
var(--hi-secondary-bg, rgba(0, 0, 0, 0.06))
|
|
214
|
+
);
|
|
215
|
+
color: var(
|
|
216
|
+
--hk-modal-close-hover-color,
|
|
217
|
+
var(--hi-color-text-primary, #333)
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
&:active {
|
|
222
|
+
transform: scale(0.92);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
&:focus-visible {
|
|
226
|
+
outline: 2px solid var(--hi-color-primary, #ffc0cb);
|
|
227
|
+
outline-offset: 2px;
|
|
228
|
+
}
|
|
161
229
|
}
|
|
162
230
|
|
|
163
231
|
.hk-popover-sheet-enter-active {
|
|
@@ -17,11 +17,13 @@ const tsx = readFileSync(join(here, "HkPopover.tsx"), "utf-8");
|
|
|
17
17
|
const scss = readFileSync(join(here, "HkPopover.scss"), "utf-8");
|
|
18
18
|
|
|
19
19
|
describe("HkPopover mobile sheet docking contract", () => {
|
|
20
|
-
it("docks the sheet
|
|
20
|
+
it("docks the sheet at the shared bottom-gap hook (inline sheet style)", () => {
|
|
21
21
|
const block = tsx.slice(tsx.indexOf("panelStyle"));
|
|
22
22
|
expect(block).toContain("var(--hk-sheet-bottom-gap");
|
|
23
|
-
//
|
|
24
|
-
|
|
23
|
+
// Family default sits flush on the bottom edge (2026-09-04 user
|
|
24
|
+
// report: even the tightened 0.375rem lift read as a visible seam);
|
|
25
|
+
// hosts may re-add a gap via the same custom property.
|
|
26
|
+
expect(block).toMatch(/--hk-sheet-bottom-gap,\s*0px/);
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
it("keeps sheet content clear of the home bar via the safe-area inset", () => {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
|
|
16
16
|
import { usePopupManager, type PopupHandle } from "../runtime/usePopupManager";
|
|
17
17
|
import { useBreakpoint } from "../runtime/useBreakpoint";
|
|
18
|
+
import { useI18n } from "../i18n/context";
|
|
18
19
|
import { useSurfaceTransition } from "../composables/useSurfaceTransition";
|
|
19
20
|
import { attachOverlayScrollbars, type OverlayScrollbarHandle } from "../composables/useOverlayScrollbar";
|
|
20
21
|
import { onceFrame } from "../runtime/animationBus";
|
|
@@ -76,6 +77,7 @@ export default defineComponent({
|
|
|
76
77
|
setup(props, { emit, slots }) {
|
|
77
78
|
const attrs = useAttrs();
|
|
78
79
|
const manager = usePopupManager();
|
|
80
|
+
const { t } = useI18n();
|
|
79
81
|
const handle = ref<PopupHandle | null>(null);
|
|
80
82
|
const panelRef = ref<HTMLElement>();
|
|
81
83
|
const resolvedPlacement = ref<PopupPlacement>(props.placement);
|
|
@@ -489,15 +491,17 @@ export default defineComponent({
|
|
|
489
491
|
|
|
490
492
|
const panelStyle = computed(() => {
|
|
491
493
|
if (sheetMode.value) {
|
|
492
|
-
// Bottom sheet:
|
|
493
|
-
// host-tunable gap
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
//
|
|
494
|
+
// Bottom sheet: sits flush on the viewport bottom edge (the
|
|
495
|
+
// host-tunable --hk-sheet-bottom-gap lift is still honored so the
|
|
496
|
+
// sheet belongs to the HkModal/HkSelect sheet family — the family
|
|
497
|
+
// default is 0 since the 2026-09-04 report: even a 0.375rem lift
|
|
498
|
+
// read as a visible seam under the docked sheet); full width,
|
|
499
|
+
// inside the top inset reserved for the secondary-window
|
|
500
|
+
// breadcrumb strip.
|
|
497
501
|
return {
|
|
498
502
|
left: "0",
|
|
499
503
|
right: "0",
|
|
500
|
-
bottom: "var(--hk-sheet-bottom-gap,
|
|
504
|
+
bottom: "var(--hk-sheet-bottom-gap, 0px)",
|
|
501
505
|
top: "auto" as const,
|
|
502
506
|
position: "fixed" as const,
|
|
503
507
|
pointerEvents: "auto" as const,
|
|
@@ -568,11 +572,36 @@ export default defineComponent({
|
|
|
568
572
|
{sheetMode.value && (
|
|
569
573
|
<div class="hk-popover-sheet-grabber" aria-hidden="true" onClick={() => close()} />
|
|
570
574
|
)}
|
|
571
|
-
{sheetMode.value &&
|
|
572
|
-
// Sheet heading
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
|
|
575
|
+
{sheetMode.value && (
|
|
576
|
+
// Sheet heading row — the title and the close button on
|
|
577
|
+
// one vertically-centered line (2026-09-04 user report:
|
|
578
|
+
// the bare title band read too small and hung right of
|
|
579
|
+
// the content it named, and every other window in the
|
|
580
|
+
// app carries an explicit ✕). Same header grammar as the
|
|
581
|
+
// HkModal header: title left, close on the right edge.
|
|
582
|
+
<div class="hk-popover-sheet-header">
|
|
583
|
+
{props.title && (
|
|
584
|
+
<div class="hk-popover-sheet-title">{props.title}</div>
|
|
585
|
+
)}
|
|
586
|
+
<button
|
|
587
|
+
class="hk-popover-sheet-close"
|
|
588
|
+
aria-label={t("hikari::modal.close", "Close")}
|
|
589
|
+
onClick={close}
|
|
590
|
+
>
|
|
591
|
+
<svg
|
|
592
|
+
viewBox="0 0 24 24"
|
|
593
|
+
fill="none"
|
|
594
|
+
stroke="currentColor"
|
|
595
|
+
stroke-width="2"
|
|
596
|
+
stroke-linecap="round"
|
|
597
|
+
width="16"
|
|
598
|
+
height="16"
|
|
599
|
+
>
|
|
600
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
601
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
602
|
+
</svg>
|
|
603
|
+
</button>
|
|
604
|
+
</div>
|
|
576
605
|
)}
|
|
577
606
|
{slots.default?.()}
|
|
578
607
|
</div>
|
|
@@ -127,4 +127,50 @@ describe("HkPopover sheetOnMobile", () => {
|
|
|
127
127
|
expect(document.body.querySelector(".hk-popover-panel")!.classList.contains("hk-is-sheet")).toBe(true);
|
|
128
128
|
expect(document.body.querySelector(".hk-popover-scrim")).toBeNull();
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
// 2026-09-04 user report: the sheet heading was a bare small band —
|
|
132
|
+
// the sheet now names itself with a title + explicit close button on
|
|
133
|
+
// one vertically-centered line, like every other window in the app.
|
|
134
|
+
it("renders a title + close heading row when titled, close alone when not", async () => {
|
|
135
|
+
setViewport(375);
|
|
136
|
+
mountPopover({ sheetOnMobile: true, title: "Context usage" });
|
|
137
|
+
await nextTick();
|
|
138
|
+
|
|
139
|
+
const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
|
|
140
|
+
const header = panel.querySelector<HTMLElement>(".hk-popover-sheet-header")!;
|
|
141
|
+
expect(header).toBeTruthy();
|
|
142
|
+
expect(header.querySelector(".hk-popover-sheet-title")!.textContent).toBe("Context usage");
|
|
143
|
+
const close = header.querySelector<HTMLButtonElement>(".hk-popover-sheet-close")!;
|
|
144
|
+
expect(close).toBeTruthy();
|
|
145
|
+
expect(close.getAttribute("aria-label")).toBeTruthy();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("closes the sheet from the heading close button", async () => {
|
|
149
|
+
setViewport(375);
|
|
150
|
+
const { open } = mountPopover({ sheetOnMobile: true, title: "Context usage" });
|
|
151
|
+
await nextTick();
|
|
152
|
+
const close = document.body.querySelector<HTMLButtonElement>(".hk-popover-sheet-close")!;
|
|
153
|
+
close.click();
|
|
154
|
+
await nextTick();
|
|
155
|
+
expect(open.value).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("renders the close button even without a title, right-aligned", async () => {
|
|
159
|
+
setViewport(375);
|
|
160
|
+
mountPopover({ sheetOnMobile: true });
|
|
161
|
+
await nextTick();
|
|
162
|
+
const header = document.body.querySelector<HTMLElement>(".hk-popover-sheet-header")!;
|
|
163
|
+
expect(header.querySelector(".hk-popover-sheet-title")).toBeNull();
|
|
164
|
+
expect(header.querySelector(".hk-popover-sheet-close")).toBeTruthy();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("keeps the heading row out of anchored desktop mode", async () => {
|
|
168
|
+
setViewport(1200);
|
|
169
|
+
mountPopover({ sheetOnMobile: true, title: "Context usage" });
|
|
170
|
+
await nextTick();
|
|
171
|
+
const panel = document.body.querySelector<HTMLElement>(".hk-popover-panel")!;
|
|
172
|
+
expect(panel.querySelector(".hk-popover-sheet-header")).toBeNull();
|
|
173
|
+
expect(panel.querySelector(".hk-popover-sheet-title")).toBeNull();
|
|
174
|
+
expect(panel.querySelector(".hk-popover-sheet-close")).toBeNull();
|
|
175
|
+
});
|
|
130
176
|
});
|
|
@@ -244,11 +244,12 @@
|
|
|
244
244
|
position: fixed;
|
|
245
245
|
left: 0;
|
|
246
246
|
right: 0;
|
|
247
|
-
/*
|
|
248
|
-
* HkModal mobile docking
|
|
249
|
-
* window reads as one sheet family
|
|
250
|
-
*
|
|
251
|
-
|
|
247
|
+
/* Sit flush on the bottom edge; the host-tunable --hk-sheet-bottom-gap
|
|
248
|
+
* lift (shared with the HkModal mobile docking) stays available so
|
|
249
|
+
* every bottom-up window reads as one sheet family. Was glued to
|
|
250
|
+
* bottom: 0, then floated at the 0.375rem family default — both wrong
|
|
251
|
+
* for someone; the 2026-09-04 report settles the family on flush. */
|
|
252
|
+
bottom: var(--hk-sheet-bottom-gap, 0px);
|
|
252
253
|
display: flex;
|
|
253
254
|
flex-direction: column;
|
|
254
255
|
/* The panel height hugs its content (up to the cap below); the sheet
|
|
@@ -266,7 +267,7 @@
|
|
|
266
267
|
72vh,
|
|
267
268
|
calc(
|
|
268
269
|
100vh - var(--hk-sheet-top-inset, 4.25rem) -
|
|
269
|
-
var(--hk-sheet-bottom-gap,
|
|
270
|
+
var(--hk-sheet-bottom-gap, 0px)
|
|
270
271
|
)
|
|
271
272
|
);
|
|
272
273
|
background: rgb(var(--color-surface));
|
|
@@ -285,6 +286,10 @@
|
|
|
285
286
|
cursor: pointer;
|
|
286
287
|
display: flex;
|
|
287
288
|
justify-content: center;
|
|
289
|
+
/* Window chrome — never a text-selection start (matches the popover
|
|
290
|
+
* sheet grabber). */
|
|
291
|
+
user-select: none;
|
|
292
|
+
-webkit-user-select: none;
|
|
288
293
|
|
|
289
294
|
&::before {
|
|
290
295
|
content: "";
|
|
@@ -302,13 +307,17 @@
|
|
|
302
307
|
* roughly another --space-16 of leading padding inside, so the old
|
|
303
308
|
* single --space-16 left the title hanging left of every row it
|
|
304
309
|
* named instead of reading as the list's heading. Hosts tune it via
|
|
305
|
-
* --hk-sheet-title-inset; the trailing inset stays --space-16.
|
|
310
|
+
* --hk-sheet-title-inset; the trailing inset stays --space-16.
|
|
311
|
+
* Size/weight match the popover sheet heading (--text-base, window
|
|
312
|
+
* chrome never text-selectable — 2026-09-04 sheet chrome pass). */
|
|
306
313
|
padding: 0 var(--space-16) var(--space-8)
|
|
307
314
|
var(--hk-sheet-title-inset, calc(var(--space-16) * 2));
|
|
308
|
-
font-size: var(--text-
|
|
315
|
+
font-size: var(--text-base);
|
|
309
316
|
font-weight: 600;
|
|
310
317
|
letter-spacing: 0.02em;
|
|
311
318
|
color: rgb(var(--color-muted));
|
|
319
|
+
user-select: none;
|
|
320
|
+
-webkit-user-select: none;
|
|
312
321
|
}
|
|
313
322
|
|
|
314
323
|
/* Track host for the sheet list's overlay scrollbar — owns the flex
|
|
@@ -16,11 +16,13 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
16
16
|
const src = readFileSync(join(here, "HkSelect.scss"), "utf-8");
|
|
17
17
|
|
|
18
18
|
describe("HkSelectPanel mobile sheet docking contract", () => {
|
|
19
|
-
it("
|
|
19
|
+
it("docks the sheet on the --hk-sheet-bottom-gap hook (flush family default)", () => {
|
|
20
20
|
const block = src.match(/\.hk-select-sheet-panel\s*{[^}]*}/)?.[0] ?? "";
|
|
21
21
|
expect(block).toContain("bottom: var(--hk-sheet-bottom-gap");
|
|
22
|
-
//
|
|
23
|
-
|
|
22
|
+
// Family default sits flush on the bottom edge (2026-09-04 user
|
|
23
|
+
// report: the 0.375rem family default read as a seam); hosts may
|
|
24
|
+
// re-add a gap via the same custom property.
|
|
25
|
+
expect(block).toMatch(/--hk-sheet-bottom-gap,\s*0px/);
|
|
24
26
|
});
|
|
25
27
|
|
|
26
28
|
it("stacks the safe-area inset on the sheet list bottom padding", () => {
|