@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.143",
3
+ "version": "0.55.145",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari Vue 3 component library — production-grade UI components based on shittim-chest design system",
@@ -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 pass-through to the native OS control ────────────────
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("renders a native date input instead of the calendar on mobile", () => {
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("passes min/max through to the native input", () => {
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
- nativeOnMobile: { type: Boolean, default: true },
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 pass-through to the native OS control ────────────────
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 instead of the calendar on mobile", () => {
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 when nativeOnMobile is false", () => {
357
+ it("keeps the custom calendar on mobile by default", () => {
357
358
  useMobileViewport();
358
- const p = mountPicker({ nativeOnMobile: false });
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: true },
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,
@@ -99,6 +99,7 @@ export default defineComponent({
99
99
  anchorRef={anchorRef.value}
100
100
  placement={props.placement}
101
101
  offset={6}
102
+ sheetOnMobile
102
103
  title={label}
103
104
  >
104
105
  <div class="hk-filter-bar-panel" data-testid="hk-filter-bar-panel">
@@ -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={field.kind === "date" ? "date" : "text"}
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(-50%, -50%);
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(-50%, -50%) translateY(5%);
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(-50%, -50%);
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(-50%, -50%);
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(-50%, -50%) translateY(5%);
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
+ }