@celestia-island/hikari 0.55.143 → 0.55.145
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/HkAdaptiveDialog.tsx +4 -0
- package/src/components/HkDatePicker.test.ts +21 -20
- package/src/components/HkDatePicker.tsx +6 -2
- package/src/components/HkDateTimePicker.test.ts +15 -14
- package/src/components/HkDateTimePicker.tsx +1 -1
- package/src/components/HkFilterBar.tsx +1 -0
- package/src/components/HkFilterPanel.tsx +13 -1
- package/src/components/HkModal.draggable.styles.test.ts +19 -0
- package/src/components/HkModal.scss +34 -5
- package/src/components/HkModal.tsx +79 -0
- package/src/components/HkTokenField.scss +105 -0
- package/src/components/HkTokenField.test.tsx +403 -0
- package/src/components/HkTokenField.tsx +357 -0
- package/src/i18n/locales/ar/components.json +3 -1
- package/src/i18n/locales/de/components.json +3 -1
- package/src/i18n/locales/en/components.json +3 -1
- package/src/i18n/locales/es/components.json +3 -1
- package/src/i18n/locales/fr/components.json +3 -1
- package/src/i18n/locales/ja/components.json +3 -1
- package/src/i18n/locales/ko/components.json +3 -1
- package/src/i18n/locales/pt/components.json +3 -1
- package/src/i18n/locales/ru/components.json +3 -1
- package/src/i18n/locales/zh-Hans/components.json +3 -1
- package/src/i18n/locales/zh-Hant/components.json +3 -1
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -48,6 +48,9 @@ export default defineComponent({
|
|
|
48
48
|
closable: { type: Boolean, default: true },
|
|
49
49
|
/** Desktop modal width (HkModal `width`). */
|
|
50
50
|
width: { type: String as PropType<ModalWidth>, default: "32rem" },
|
|
51
|
+
/** Desktop-only: the modal header becomes a drag handle (see
|
|
52
|
+
* HkModal `draggable`); the mobile sheet never drags. */
|
|
53
|
+
draggable: { type: Boolean, default: false },
|
|
51
54
|
/** Mobile drawer height (HkDrawer `size`). HkDrawer's bottom-side CSS
|
|
52
55
|
* clamps panels at maxHeight 70vh, so the default equals the cap;
|
|
53
56
|
* pass `panelClass` with a custom max-height for taller sheets. */
|
|
@@ -153,6 +156,7 @@ export default defineComponent({
|
|
|
153
156
|
title={props.title}
|
|
154
157
|
closable={props.closable}
|
|
155
158
|
width={props.width}
|
|
159
|
+
draggable={props.draggable}
|
|
156
160
|
footerActions={props.footerActions}
|
|
157
161
|
>
|
|
158
162
|
{{
|
|
@@ -194,7 +194,7 @@ describe("HkDatePicker", () => {
|
|
|
194
194
|
});
|
|
195
195
|
|
|
196
196
|
it("disables days outside the inclusive min/max bounds", async () => {
|
|
197
|
-
const p = mountPicker({ modelValue: "2026-08-16", min: "2026-08-10", max: "2026-08-20" });
|
|
197
|
+
const p = mountPicker({ modelValue: "2026-08-16", min: "2026-08-10", max: "2026-08-20", nativeOnMobile: true });
|
|
198
198
|
openViaEnter(p);
|
|
199
199
|
await nextTick();
|
|
200
200
|
const byDay = new Map(
|
|
@@ -453,22 +453,12 @@ describe("HkDatePicker", () => {
|
|
|
453
453
|
expect(stage?.querySelectorAll<HTMLButtonElement>(".hk-dp-cell:not([data-variant])").length).toBe(42);
|
|
454
454
|
});
|
|
455
455
|
|
|
456
|
-
// ── Mobile
|
|
456
|
+
// ── Mobile: the CALENDAR is the default; the native input is the
|
|
457
|
+
// ── explicit opt-out (2026-10-11 direction: no native controls) ──
|
|
457
458
|
|
|
458
|
-
it("
|
|
459
|
+
it("keeps the calendar on mobile by default (no native input)", async () => {
|
|
459
460
|
useMobileViewport();
|
|
460
461
|
const p = mountPicker({ modelValue: "2026-08-16" });
|
|
461
|
-
const input = nativeInput(p.container);
|
|
462
|
-
expect(input).not.toBeNull();
|
|
463
|
-
expect(input?.type).toBe("date");
|
|
464
|
-
expect(input?.value).toBe("2026-08-16");
|
|
465
|
-
expect(p.container.querySelector(".hk-dp-trigger")).toBeNull();
|
|
466
|
-
expect(p.container.querySelector(".hk-dp-panel")).toBeNull();
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it("keeps the custom calendar on mobile when nativeOnMobile is false", async () => {
|
|
470
|
-
useMobileViewport();
|
|
471
|
-
const p = mountPicker({ modelValue: "2026-08-16", nativeOnMobile: false });
|
|
472
462
|
expect(p.container.querySelector(".hk-dp-trigger")).not.toBeNull();
|
|
473
463
|
expect(nativeInput(p.container)).toBeNull();
|
|
474
464
|
openViaEnter(p);
|
|
@@ -479,17 +469,28 @@ describe("HkDatePicker", () => {
|
|
|
479
469
|
expect(panel()?.classList.contains("is-touch")).toBe(true);
|
|
480
470
|
});
|
|
481
471
|
|
|
482
|
-
it("
|
|
472
|
+
it("renders a native date input on mobile only when nativeOnMobile opts in", () => {
|
|
473
|
+
useMobileViewport();
|
|
474
|
+
const p = mountPicker({ modelValue: "2026-08-16", nativeOnMobile: true });
|
|
475
|
+
const input = nativeInput(p.container);
|
|
476
|
+
expect(input).not.toBeNull();
|
|
477
|
+
expect(input?.type).toBe("date");
|
|
478
|
+
expect(input?.value).toBe("2026-08-16");
|
|
479
|
+
expect(p.container.querySelector(".hk-dp-trigger")).toBeNull();
|
|
480
|
+
expect(p.container.querySelector(".hk-dp-panel")).toBeNull();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("passes min/max through to the native input (explicit opt-in)", () => {
|
|
483
484
|
useMobileViewport();
|
|
484
|
-
const p = mountPicker({ modelValue: "2026-08-16", min: "2026-08-10", max: "2026-08-20" });
|
|
485
|
+
const p = mountPicker({ modelValue: "2026-08-16", min: "2026-08-10", max: "2026-08-20", nativeOnMobile: true });
|
|
485
486
|
const input = nativeInput(p.container);
|
|
486
487
|
expect(input?.min).toBe("2026-08-10");
|
|
487
488
|
expect(input?.max).toBe("2026-08-20");
|
|
488
489
|
});
|
|
489
490
|
|
|
490
|
-
it("native input edits emit the ISO date and clearing emits null", async () => {
|
|
491
|
+
it("native input edits emit the ISO date and clearing emits null (explicit opt-in)", async () => {
|
|
491
492
|
useMobileViewport();
|
|
492
|
-
const p = mountPicker({ modelValue: "2026-08-16" });
|
|
493
|
+
const p = mountPicker({ modelValue: "2026-08-16", nativeOnMobile: true });
|
|
493
494
|
const input = nativeInput(p.container);
|
|
494
495
|
input!.value = "2026-08-20";
|
|
495
496
|
input!.dispatchEvent(new Event("input", { bubbles: true }));
|
|
@@ -503,9 +504,9 @@ describe("HkDatePicker", () => {
|
|
|
503
504
|
expect(p.emitted).toEqual(["2026-08-20", null]);
|
|
504
505
|
});
|
|
505
506
|
|
|
506
|
-
it("native input edits outside the bounds are rejected and re-synced", async () => {
|
|
507
|
+
it("native input edits outside the bounds are rejected and re-synced (explicit opt-in)", async () => {
|
|
507
508
|
useMobileViewport();
|
|
508
|
-
const p = mountPicker({ modelValue: "2026-08-16", max: "2026-08-20" });
|
|
509
|
+
const p = mountPicker({ modelValue: "2026-08-16", max: "2026-08-20", nativeOnMobile: true });
|
|
509
510
|
const input = nativeInput(p.container);
|
|
510
511
|
input!.value = "2026-09-01";
|
|
511
512
|
input!.dispatchEvent(new Event("input", { bubbles: true }));
|
|
@@ -73,8 +73,12 @@ export default defineComponent({
|
|
|
73
73
|
min: { type: String as PropType<string | undefined>, default: undefined },
|
|
74
74
|
/** Inclusive upper bound as an ISO date; later days render disabled. */
|
|
75
75
|
max: { type: String as PropType<string | undefined>, default: undefined },
|
|
76
|
-
/** Render the OS native `<input type="date">` on touch-sized viewports.
|
|
77
|
-
|
|
76
|
+
/** Render the OS native `<input type="date">` on touch-sized viewports.
|
|
77
|
+
* DEFAULTS FALSE since 2026-10-11 (user direction: no native controls
|
|
78
|
+
* anywhere) — the calendar serves phones as a bottom sheet with the
|
|
79
|
+
* enlarged touch chrome. The prop stays for one-off hosts that truly
|
|
80
|
+
* want the OS picker; new callers should not set it. */
|
|
81
|
+
nativeOnMobile: { type: Boolean, default: false },
|
|
78
82
|
},
|
|
79
83
|
emits: {
|
|
80
84
|
"update:modelValue": (_value: string | null) => true,
|
|
@@ -324,11 +324,12 @@ describe("HkDateTimePicker", () => {
|
|
|
324
324
|
expect(document.querySelectorAll(".hk-dtp-popup .hk-dtp-cell").length).toBe(42);
|
|
325
325
|
});
|
|
326
326
|
|
|
327
|
-
// ── Mobile
|
|
327
|
+
// ── Mobile: the calendar is the default; native is the explicit ──
|
|
328
|
+
// ── opt-out (2026-10-11 direction: no native controls) ───────────
|
|
328
329
|
|
|
329
|
-
it("renders a native datetime-local input
|
|
330
|
+
it("renders a native datetime-local input on mobile only when nativeOnMobile opts in", () => {
|
|
330
331
|
useMobileViewport();
|
|
331
|
-
const p = mountPicker();
|
|
332
|
+
const p = mountPicker({ nativeOnMobile: true });
|
|
332
333
|
const input = nativeInput(p.container);
|
|
333
334
|
expect(input).not.toBeNull();
|
|
334
335
|
expect(input?.type).toBe("datetime-local");
|
|
@@ -337,9 +338,9 @@ describe("HkDateTimePicker", () => {
|
|
|
337
338
|
expect(p.container.querySelector(".hk-dtp-grid")).toBeNull();
|
|
338
339
|
});
|
|
339
340
|
|
|
340
|
-
it("uses a native date input when showTime is false", () => {
|
|
341
|
+
it("uses a native date input when showTime is false (explicit opt-in)", () => {
|
|
341
342
|
useMobileViewport();
|
|
342
|
-
const p = mountPicker({ showTime: false });
|
|
343
|
+
const p = mountPicker({ showTime: false, nativeOnMobile: true });
|
|
343
344
|
const input = nativeInput(p.container);
|
|
344
345
|
expect(input?.type).toBe("date");
|
|
345
346
|
expect(input?.value).toBe(isoDay(BASE));
|
|
@@ -347,15 +348,15 @@ describe("HkDateTimePicker", () => {
|
|
|
347
348
|
|
|
348
349
|
it("passes min/max through to the native input in its wire format", () => {
|
|
349
350
|
useMobileViewport();
|
|
350
|
-
const p = mountPicker({ min: dayOf(10, 8, 0), max: dayOf(20, 18, 30) });
|
|
351
|
+
const p = mountPicker({ min: dayOf(10, 8, 0), max: dayOf(20, 18, 30), nativeOnMobile: true });
|
|
351
352
|
const input = nativeInput(p.container);
|
|
352
353
|
expect(input?.min).toBe(isoMinute(dayOf(10, 8, 0)));
|
|
353
354
|
expect(input?.max).toBe(isoMinute(dayOf(20, 18, 30)));
|
|
354
355
|
});
|
|
355
356
|
|
|
356
|
-
it("keeps the custom calendar on mobile
|
|
357
|
+
it("keeps the custom calendar on mobile by default", () => {
|
|
357
358
|
useMobileViewport();
|
|
358
|
-
const p = mountPicker(
|
|
359
|
+
const p = mountPicker();
|
|
359
360
|
expect(p.container.querySelector(".hk-dtp")).not.toBeNull();
|
|
360
361
|
expect(dayCells().length).toBe(42);
|
|
361
362
|
expect(nativeInput(p.container)).toBeNull();
|
|
@@ -365,7 +366,7 @@ describe("HkDateTimePicker", () => {
|
|
|
365
366
|
|
|
366
367
|
it("native input edits update the model as a local-time Date", async () => {
|
|
367
368
|
useMobileViewport();
|
|
368
|
-
const p = mountPicker();
|
|
369
|
+
const p = mountPicker({ nativeOnMobile: true });
|
|
369
370
|
const input = nativeInput(p.container);
|
|
370
371
|
input!.value = isoMinute(dayOf(20, 14, 5));
|
|
371
372
|
input!.dispatchEvent(new Event("input", { bubbles: true }));
|
|
@@ -377,7 +378,7 @@ describe("HkDateTimePicker", () => {
|
|
|
377
378
|
|
|
378
379
|
it("native date edits preserve the clock time when showTime is false", async () => {
|
|
379
380
|
useMobileViewport();
|
|
380
|
-
const p = mountPicker({ showTime: false });
|
|
381
|
+
const p = mountPicker({ showTime: false, nativeOnMobile: true });
|
|
381
382
|
const input = nativeInput(p.container);
|
|
382
383
|
input!.value = isoDay(dayOf(20));
|
|
383
384
|
input!.dispatchEvent(new Event("input", { bubbles: true }));
|
|
@@ -387,7 +388,7 @@ describe("HkDateTimePicker", () => {
|
|
|
387
388
|
|
|
388
389
|
it("native input edits outside the bounds are rejected and re-synced", async () => {
|
|
389
390
|
useMobileViewport();
|
|
390
|
-
const p = mountPicker({ max: dayOf(20, 23, 59) });
|
|
391
|
+
const p = mountPicker({ max: dayOf(20, 23, 59), nativeOnMobile: true });
|
|
391
392
|
const input = nativeInput(p.container);
|
|
392
393
|
// Next month, past the max bound.
|
|
393
394
|
input!.value = isoMinute(new Date(BASE.getFullYear(), BASE.getMonth() + 1, 1, 10, 0));
|
|
@@ -399,7 +400,7 @@ describe("HkDateTimePicker", () => {
|
|
|
399
400
|
|
|
400
401
|
it("clearing the native input falls back to the model value", async () => {
|
|
401
402
|
useMobileViewport();
|
|
402
|
-
const p = mountPicker();
|
|
403
|
+
const p = mountPicker({ nativeOnMobile: true });
|
|
403
404
|
const input = nativeInput(p.container);
|
|
404
405
|
input!.value = "";
|
|
405
406
|
input!.dispatchEvent(new Event("input", { bubbles: true }));
|
|
@@ -408,9 +409,9 @@ describe("HkDateTimePicker", () => {
|
|
|
408
409
|
expect(input?.value).toBe(isoMinute(BASE));
|
|
409
410
|
});
|
|
410
411
|
|
|
411
|
-
it("native input replaces even the popup chrome on mobile", () => {
|
|
412
|
+
it("native input replaces even the popup chrome on mobile (explicit opt-in)", () => {
|
|
412
413
|
useMobileViewport();
|
|
413
|
-
const p = mountPicker({ mode: "popup", confirmLabel: "Confirm" });
|
|
414
|
+
const p = mountPicker({ mode: "popup", confirmLabel: "Confirm", nativeOnMobile: true });
|
|
414
415
|
expect(nativeInput(p.container)).not.toBeNull();
|
|
415
416
|
expect(p.container.querySelector(".hk-dtp-trigger")).toBeNull();
|
|
416
417
|
});
|
|
@@ -95,7 +95,7 @@ export default defineComponent({
|
|
|
95
95
|
confirmLabel: { type: String, default: undefined },
|
|
96
96
|
showTime: { type: Boolean, default: true },
|
|
97
97
|
/** Render the OS native `<input type="datetime-local">` on touch-sized viewports. */
|
|
98
|
-
nativeOnMobile: { type: Boolean, default:
|
|
98
|
+
nativeOnMobile: { type: Boolean, default: false }, // 2026-10-11: no native controls — the picker sheet serves phones
|
|
99
99
|
},
|
|
100
100
|
emits: {
|
|
101
101
|
"update:modelValue": (_d: Date) => true,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { computed, defineComponent, type PropType } from "vue";
|
|
2
2
|
|
|
3
|
+
import HkDatePicker from "./HkDatePicker";
|
|
3
4
|
import HkInput from "./HkInput";
|
|
4
5
|
import HkPopupSelect, { type HkPopupSelectOption } from "./HkPopupSelect";
|
|
5
6
|
import { useI18n } from "../i18n/context";
|
|
@@ -168,9 +169,20 @@ export default defineComponent({
|
|
|
168
169
|
disabled={props.disabled}
|
|
169
170
|
options={selectOptions(field)}
|
|
170
171
|
/>
|
|
172
|
+
) : field.kind === "date" ? (
|
|
173
|
+
// The calendar picker — never the OS native input
|
|
174
|
+
// (2026-10-11 direction); the wire format is the same
|
|
175
|
+
// ISO YYYY-MM-DD the native input spoke.
|
|
176
|
+
<HkDatePicker
|
|
177
|
+
modelValue={condition.value || null}
|
|
178
|
+
onUpdate:modelValue={(v: string | null) =>
|
|
179
|
+
emitCondition(field, { value: v ?? "" })
|
|
180
|
+
}
|
|
181
|
+
disabled={props.disabled}
|
|
182
|
+
/>
|
|
171
183
|
) : (
|
|
172
184
|
<HkInput
|
|
173
|
-
type=
|
|
185
|
+
type="text"
|
|
174
186
|
variant={field.kind === "number" ? "number" : "text"}
|
|
175
187
|
modelValue={condition.value}
|
|
176
188
|
onUpdate:modelValue={(v: string) =>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
/** The drag affordance contract: the grab cursor/touch lock MUST ride
|
|
7
|
+
* the frame selector — an earlier iteration targeted `.hk-modal[...]`
|
|
8
|
+
* (a class no element carries) and shipped dead CSS (2026-10-11 R2
|
|
9
|
+
* mutation M1 caught it with zero red). */
|
|
10
|
+
describe("HkModal draggable affordance (styles contract)", () => {
|
|
11
|
+
it("scopes the grab handle to the frame's data attribute", () => {
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const scss = readFileSync(join(here, "HkModal.scss"), "utf-8");
|
|
14
|
+
const block = scss.match(/\.hk-modal-content\[data-draggable\]\s+\.hk-modal-header\s*{[^}]*}/)?.[0] ?? "";
|
|
15
|
+
expect(block, "the live drag-affordance selector").toContain("cursor: grab");
|
|
16
|
+
expect(block).toContain("touch-action: none");
|
|
17
|
+
expect(scss).not.toMatch(/\.hk-modal\[data-draggable\]/);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -59,7 +59,10 @@
|
|
|
59
59
|
// The centering translate is part of EVERY transform state — the
|
|
60
60
|
// enter/leave choreography scales on top of it and must never re-seat
|
|
61
61
|
// the frame.
|
|
62
|
-
transform: translate(
|
|
62
|
+
transform: translate(
|
|
63
|
+
calc(-50% + var(--hk-modal-shift-x, 0px)),
|
|
64
|
+
calc(-50% + var(--hk-modal-shift-y, 0px))
|
|
65
|
+
);
|
|
63
66
|
width: 100%;
|
|
64
67
|
// Size changes animate instead of snapping: `height: auto` growth
|
|
65
68
|
// cannot fire a CSS transition (verified in Chromium — neither auto
|
|
@@ -147,21 +150,31 @@
|
|
|
147
150
|
|
|
148
151
|
.hk-modal-content-enter-from {
|
|
149
152
|
opacity: 0;
|
|
150
|
-
transform: translate(
|
|
153
|
+
transform: translate(
|
|
154
|
+
calc(-50% + var(--hk-modal-shift-x, 0px)),
|
|
155
|
+
calc(-50% + var(--hk-modal-shift-y, 0px))
|
|
156
|
+
)
|
|
157
|
+
translateY(5%);
|
|
151
158
|
clip-path: inset(0 0 10% 0 round 0 0 var(--hk-modal-radius, var(--radius-lg, 12px))
|
|
152
159
|
var(--hk-modal-radius, var(--radius-lg, 12px)));
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
.hk-modal-content-enter-to {
|
|
156
163
|
opacity: 1;
|
|
157
|
-
transform: translate(
|
|
164
|
+
transform: translate(
|
|
165
|
+
calc(-50% + var(--hk-modal-shift-x, 0px)),
|
|
166
|
+
calc(-50% + var(--hk-modal-shift-y, 0px))
|
|
167
|
+
);
|
|
158
168
|
clip-path: inset(0 0 0 0 round 0 0 var(--hk-modal-radius, var(--radius-lg, 12px))
|
|
159
169
|
var(--hk-modal-radius, var(--radius-lg, 12px)));
|
|
160
170
|
}
|
|
161
171
|
|
|
162
172
|
.hk-modal-content-leave-from {
|
|
163
173
|
opacity: 1;
|
|
164
|
-
transform: translate(
|
|
174
|
+
transform: translate(
|
|
175
|
+
calc(-50% + var(--hk-modal-shift-x, 0px)),
|
|
176
|
+
calc(-50% + var(--hk-modal-shift-y, 0px))
|
|
177
|
+
);
|
|
165
178
|
// The pinned identity clip: identical to enter-to so the leave's
|
|
166
179
|
// clip interpolates (never jumps) — see the close note above.
|
|
167
180
|
clip-path: inset(0 0 0 0 round 0 0 var(--hk-modal-radius, var(--radius-lg, 12px))
|
|
@@ -173,7 +186,11 @@
|
|
|
173
186
|
// Mirror of enter-from: shifted +5% with the bottom 10% clipped, the
|
|
174
187
|
// visible box is 90% and centered — both edges ease toward the
|
|
175
188
|
// center line, exactly reversing the unveil.
|
|
176
|
-
transform: translate(
|
|
189
|
+
transform: translate(
|
|
190
|
+
calc(-50% + var(--hk-modal-shift-x, 0px)),
|
|
191
|
+
calc(-50% + var(--hk-modal-shift-y, 0px))
|
|
192
|
+
)
|
|
193
|
+
translateY(5%);
|
|
177
194
|
clip-path: inset(0 0 10% 0 round 0 0 var(--hk-modal-radius, var(--radius-lg, 12px))
|
|
178
195
|
var(--hk-modal-radius, var(--radius-lg, 12px)));
|
|
179
196
|
}
|
|
@@ -647,3 +664,15 @@
|
|
|
647
664
|
transition: none;
|
|
648
665
|
}
|
|
649
666
|
}
|
|
667
|
+
|
|
668
|
+
/* ── Draggable desktop windows (opt-in `draggable` prop) ──────────── */
|
|
669
|
+
@media (min-width: 768px) {
|
|
670
|
+
.hk-modal-content[data-draggable] .hk-modal-header {
|
|
671
|
+
cursor: grab;
|
|
672
|
+
touch-action: none;
|
|
673
|
+
user-select: none;
|
|
674
|
+
}
|
|
675
|
+
.hk-modal-content[data-draggable] .hk-modal-header:active {
|
|
676
|
+
cursor: grabbing;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
@@ -173,6 +173,14 @@ export default defineComponent({
|
|
|
173
173
|
* own history entries.
|
|
174
174
|
*/
|
|
175
175
|
backGuard: { type: Boolean, default: true },
|
|
176
|
+
/**
|
|
177
|
+
* Desktop-only drag: the header becomes a grab handle that moves
|
|
178
|
+
* the centered frame (composited into the transform via the
|
|
179
|
+
* --hk-modal-shift-x/y vars, so the enter/leave choreography is
|
|
180
|
+
* untouched). The mobile bottom sheet never drags. Reopening
|
|
181
|
+
* resets the shift.
|
|
182
|
+
*/
|
|
183
|
+
draggable: { type: Boolean, default: false },
|
|
176
184
|
},
|
|
177
185
|
emits: {
|
|
178
186
|
"update:modelValue": (_value: boolean) => true,
|
|
@@ -197,6 +205,72 @@ export default defineComponent({
|
|
|
197
205
|
const handle = ref<{ id: string; zIndex: number } | null>(null);
|
|
198
206
|
const bodyRef = ref<HTMLElement>();
|
|
199
207
|
const contentRef = ref<HTMLElement>();
|
|
208
|
+
/** Live drag shift in px (null = not dragged); mirrored onto the
|
|
209
|
+
* frame as CSS vars so the class-driven transforms stay intact. */
|
|
210
|
+
const dragShift = ref<{ x: number; y: number } | null>(null);
|
|
211
|
+
let dragStart: { px: number; py: number; x: number; y: number } | null = null;
|
|
212
|
+
|
|
213
|
+
function applyShift() {
|
|
214
|
+
const el = contentRef.value;
|
|
215
|
+
if (!el) return;
|
|
216
|
+
if (dragShift.value) {
|
|
217
|
+
el.style.setProperty("--hk-modal-shift-x", `${dragShift.value.x}px`);
|
|
218
|
+
el.style.setProperty("--hk-modal-shift-y", `${dragShift.value.y}px`);
|
|
219
|
+
} else {
|
|
220
|
+
el.style.removeProperty("--hk-modal-shift-x");
|
|
221
|
+
el.style.removeProperty("--hk-modal-shift-y");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function onHeaderPointerDown(e: PointerEvent) {
|
|
226
|
+
// Desktop handle only: the sheet branch (<768px) never consumes
|
|
227
|
+
// the vars, and touch drag would fight the sheet's own gestures.
|
|
228
|
+
if (!props.draggable || e.button !== 0) return;
|
|
229
|
+
if (window.matchMedia("(max-width: 767px)").matches) return;
|
|
230
|
+
// Interactive chrome inside the header (the close button) keeps
|
|
231
|
+
// its own pointer semantics.
|
|
232
|
+
if ((e.target as HTMLElement).closest("button, a, input, select, textarea")) return;
|
|
233
|
+
const el = contentRef.value;
|
|
234
|
+
if (!el) return;
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
const base = dragShift.value ?? { x: 0, y: 0 };
|
|
237
|
+
dragStart = { px: e.clientX, py: e.clientY, x: base.x, y: base.y };
|
|
238
|
+
(e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function onHeaderPointerMove(e: PointerEvent) {
|
|
242
|
+
if (!dragStart) return;
|
|
243
|
+
// Re-check the guards: `draggable` may have been toggled off
|
|
244
|
+
// mid-drag, and a hover with no button pressed must never move
|
|
245
|
+
// the frame (a stale dragStart after a mid-drag close would
|
|
246
|
+
// otherwise ghost-drag on the next open).
|
|
247
|
+
if (!props.draggable || e.buttons === 0) {
|
|
248
|
+
dragStart = null;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
dragShift.value = {
|
|
252
|
+
x: dragStart.x + (e.clientX - dragStart.px),
|
|
253
|
+
y: dragStart.y + (e.clientY - dragStart.py),
|
|
254
|
+
};
|
|
255
|
+
applyShift();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function onHeaderPointerUp() {
|
|
259
|
+
dragStart = null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Reopening reseats the frame at center — and drops any stale
|
|
263
|
+
// dragStart (a close mid-capture never fires pointerup).
|
|
264
|
+
watch(
|
|
265
|
+
() => props.modelValue,
|
|
266
|
+
(open) => {
|
|
267
|
+
if (open) {
|
|
268
|
+
dragStart = null;
|
|
269
|
+
dragShift.value = null;
|
|
270
|
+
applyShift();
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
);
|
|
200
274
|
/** Natural-height probe inside the scroll container: the body's
|
|
201
275
|
* content wrapper, whose height is the content's intrinsic height
|
|
202
276
|
* regardless of scroll — the size morph measures this (see
|
|
@@ -972,6 +1046,7 @@ export default defineComponent({
|
|
|
972
1046
|
role="dialog"
|
|
973
1047
|
aria-modal="true"
|
|
974
1048
|
aria-label={resolvedSurfaceName.value}
|
|
1049
|
+
data-draggable={props.draggable || undefined}
|
|
975
1050
|
style={{
|
|
976
1051
|
maxWidth: resolvedWidth.value,
|
|
977
1052
|
zIndex: contentZ.value,
|
|
@@ -991,6 +1066,10 @@ export default defineComponent({
|
|
|
991
1066
|
"hk-modal-header",
|
|
992
1067
|
slots.headerLead ? "hk-modal-header--lead" : "",
|
|
993
1068
|
]}
|
|
1069
|
+
onPointerdown={onHeaderPointerDown}
|
|
1070
|
+
onPointermove={onHeaderPointerMove}
|
|
1071
|
+
onPointerup={onHeaderPointerUp}
|
|
1072
|
+
onPointercancel={onHeaderPointerUp}
|
|
994
1073
|
>
|
|
995
1074
|
{slots.headerLead && (
|
|
996
1075
|
<div class="hk-modal-header-lead">{slots.headerLead()}</div>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* HkTokenField — one box: slider ladder (major) + value + unit suffix.
|
|
2
|
+
* The track flexes; the value and the unit keep their content width so
|
|
3
|
+
* the slider takes whatever the row can spare (the "left bigger part"
|
|
4
|
+
* layout the model editor specified). */
|
|
5
|
+
.hk-token-field {
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: var(--space-8, 8px);
|
|
9
|
+
min-width: 0;
|
|
10
|
+
padding: var(--space-4, 4px) var(--space-8, 8px);
|
|
11
|
+
border: 1px solid color-mix(in srgb, rgb(var(--color-text)) 14%, transparent);
|
|
12
|
+
border-radius: var(--radius-sm);
|
|
13
|
+
background: rgb(var(--color-surface));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.hk-token-field.is-disabled {
|
|
17
|
+
opacity: 0.55;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.hk-token-field-slider {
|
|
21
|
+
flex: 1 1 auto;
|
|
22
|
+
min-width: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.hk-token-field-value {
|
|
26
|
+
flex: 0 0 auto;
|
|
27
|
+
min-width: 3.25rem;
|
|
28
|
+
padding: 2px var(--space-8, 8px);
|
|
29
|
+
border: none;
|
|
30
|
+
border-radius: var(--radius-sm);
|
|
31
|
+
background: rgb(var(--color-muted) / 12%);
|
|
32
|
+
color: rgb(var(--color-text));
|
|
33
|
+
font-size: 0.8125rem;
|
|
34
|
+
font-variant-numeric: tabular-nums;
|
|
35
|
+
text-align: center;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
font-family: inherit;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.hk-token-field-value:hover:not(:disabled) {
|
|
41
|
+
background: rgb(var(--color-muted) / 20%);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.hk-token-field-value:focus-visible {
|
|
45
|
+
outline: none;
|
|
46
|
+
box-shadow: 0 0 0 2px rgb(var(--color-primary) / 40%);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* An off-ladder (typed) value gets the primary tint — the thumb rests
|
|
50
|
+
* at the nearest stop, the exact budget is what stands. */
|
|
51
|
+
.hk-token-field[data-off-ladder] .hk-token-field-value {
|
|
52
|
+
background: rgb(var(--color-primary) / 14%);
|
|
53
|
+
color: var(--c-primary-strong, rgb(var(--color-primary)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.hk-token-field-unit {
|
|
57
|
+
flex: 0 0 auto;
|
|
58
|
+
width: 4.25rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* The precision editor body: value input (major) + unit (minor). */
|
|
62
|
+
.hk-token-field-editor-body {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: flex-start;
|
|
65
|
+
gap: var(--space-8, 8px);
|
|
66
|
+
padding: var(--space-8, 8px) 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.hk-token-editor {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: var(--space-8, 8px);
|
|
73
|
+
flex: 1 1 auto;
|
|
74
|
+
min-width: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.hk-token-editor-input {
|
|
78
|
+
flex: 1 1 auto;
|
|
79
|
+
min-width: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.hk-token-editor-unit {
|
|
83
|
+
flex: 0 0 auto;
|
|
84
|
+
width: 5rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.hk-token-field-clear {
|
|
88
|
+
flex: 0 0 auto;
|
|
89
|
+
display: inline-flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
margin-top: var(--space-4, 4px);
|
|
93
|
+
width: 26px;
|
|
94
|
+
height: 26px;
|
|
95
|
+
border: none;
|
|
96
|
+
border-radius: var(--radius-sm);
|
|
97
|
+
background: transparent;
|
|
98
|
+
color: rgb(var(--color-muted));
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.hk-token-field-clear:hover {
|
|
103
|
+
background: rgb(var(--color-error) / 10%);
|
|
104
|
+
color: rgb(var(--color-error));
|
|
105
|
+
}
|