@celestia-island/hikari 0.55.142 → 0.55.144
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/HkModal.draggable.styles.test.ts +19 -0
- package/src/components/HkModal.scss +34 -5
- package/src/components/HkModal.tsx +79 -0
- package/src/components/HkModelTag.scss +31 -0
- package/src/components/HkModelTag.test.tsx +26 -0
- package/src/components/HkModelTag.tsx +24 -2
- 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
|
{{
|
|
@@ -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>
|
|
@@ -48,6 +48,37 @@
|
|
|
48
48
|
border-radius: var(--radius-sm, 4px);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Rate segment (the model-group capsule's `×N` tail, 2026-10-11): takes
|
|
52
|
+
// the inline-end rounding and provides the seam's divider from its own
|
|
53
|
+
// inline-start border; a success tint keeps it readable as a modifier,
|
|
54
|
+
// not a second name. With a rate present the NAME segment becomes a
|
|
55
|
+
// middle segment — square inline-end edge too (the join geometry below).
|
|
56
|
+
.s-model-tag-group[data-rate] .hk-badge.s-model-tag-name {
|
|
57
|
+
border-start-end-radius: 0;
|
|
58
|
+
border-end-end-radius: 0;
|
|
59
|
+
// The rate badge provides the seam's divider from its own
|
|
60
|
+
// inline-start border — the name's inline-end border must drop or
|
|
61
|
+
// the seam stacks ~2px (the num/name seam is 1px).
|
|
62
|
+
border-inline-end: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Tagless + rate (the pill starts bare before the roster refines the
|
|
66
|
+
// id): the name is the FIRST child — its leading corners keep the
|
|
67
|
+
// capsule's rounding (only the :only-child escape covered it before).
|
|
68
|
+
.s-model-tag-group[data-rate] .hk-badge.s-model-tag-name:first-child {
|
|
69
|
+
border-start-start-radius: var(--radius-sm, 4px);
|
|
70
|
+
border-end-start-radius: var(--radius-sm, 4px);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.s-model-tag-group .hk-badge.s-model-tag-rate {
|
|
74
|
+
border-start-start-radius: 0;
|
|
75
|
+
border-end-start-radius: 0;
|
|
76
|
+
border-start-end-radius: var(--radius-sm, 4px);
|
|
77
|
+
border-end-end-radius: var(--radius-sm, 4px);
|
|
78
|
+
border-inline-start-color: rgb(var(--color-muted) / 35%);
|
|
79
|
+
flex-shrink: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
51
82
|
.s-model-tag-name-text {
|
|
52
83
|
display: block;
|
|
53
84
|
overflow: hidden;
|
|
@@ -126,4 +126,30 @@ describe("HkModelTag", () => {
|
|
|
126
126
|
c.querySelector(".s-model-tag-group")?.getAttribute("data-expanded"),
|
|
127
127
|
).toBeNull();
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
it("renders the optional rate tail as the third joined segment", () => {
|
|
131
|
+
const c = mount(tagNode({ model: "gpt-5.5#2", rate: 1.5 }));
|
|
132
|
+
const group = c.querySelector(".s-model-tag-group");
|
|
133
|
+
expect(group?.hasAttribute("data-rate")).toBe(true);
|
|
134
|
+
expect(group?.querySelector(".s-model-tag-rate")?.textContent?.trim()).toBe("×1.5");
|
|
135
|
+
// The segments read in order: #2, name, rate.
|
|
136
|
+
expect(group?.textContent?.trim()).toBe("#2gpt-5.5×1.5");
|
|
137
|
+
|
|
138
|
+
// Trailing zeros trimmed, one decimal at most.
|
|
139
|
+
const whole = mount(tagNode({ model: "gpt-5.5#2", rate: 2 }));
|
|
140
|
+
expect(
|
|
141
|
+
whole.querySelector(".s-model-tag-rate")?.textContent?.trim(),
|
|
142
|
+
).toBe("×2");
|
|
143
|
+
const tenth = mount(tagNode({ model: "gpt-5.5#2", rate: 0.1 }));
|
|
144
|
+
expect(
|
|
145
|
+
tenth.querySelector(".s-model-tag-rate")?.textContent?.trim(),
|
|
146
|
+
).toBe("×0.1");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("keeps the classic two-part pill untouched without a rate", () => {
|
|
150
|
+
const c = mount(tagNode({ model: "gpt-5.5#2" }));
|
|
151
|
+
const group = c.querySelector(".s-model-tag-group");
|
|
152
|
+
expect(group?.getAttribute("data-rate")).toBeNull();
|
|
153
|
+
expect(group?.querySelector(".s-model-tag-rate")).toBeNull();
|
|
154
|
+
});
|
|
129
155
|
});
|
|
@@ -23,6 +23,10 @@ const ModelPill = defineComponent({
|
|
|
23
23
|
model: { type: String, required: true },
|
|
24
24
|
/** Render the name without truncation (used inside the hover card). */
|
|
25
25
|
expanded: { type: Boolean, default: false },
|
|
26
|
+
/** Optional third segment — the billing rate tail (`×1.5`), as the
|
|
27
|
+
* model-group capsule's multiplier (2026-10-11 direction). Absent
|
|
28
|
+
* keeps the classic two-part pill untouched. */
|
|
29
|
+
rate: { type: Number, default: undefined },
|
|
26
30
|
},
|
|
27
31
|
setup(props) {
|
|
28
32
|
// Split inside the render closure: capturing `splitModelId(props.model)`
|
|
@@ -32,10 +36,19 @@ const ModelPill = defineComponent({
|
|
|
32
36
|
// stale, untagged pill.
|
|
33
37
|
return () => {
|
|
34
38
|
const { base, tag } = splitModelId(props.model);
|
|
39
|
+
// ×1.5 / ×2 / ×0.1 — trailing zeros trimmed, one decimal at most
|
|
40
|
+
// (the editor offers tenths; hundredths would crowd the pill).
|
|
41
|
+
// Number.isFinite guards the catalog edge: a NaN/Infinity leaking
|
|
42
|
+
// in would otherwise render "×NaN" (R1 F3).
|
|
43
|
+
const rateText =
|
|
44
|
+
props.rate != null && Number.isFinite(props.rate)
|
|
45
|
+
? `×${Number(props.rate.toFixed(1)).toString()}`
|
|
46
|
+
: null;
|
|
35
47
|
return (
|
|
36
48
|
<span
|
|
37
49
|
class="s-model-tag-group"
|
|
38
50
|
data-expanded={props.expanded || undefined}
|
|
51
|
+
data-rate={rateText != null || undefined}
|
|
39
52
|
>
|
|
40
53
|
{tag && (
|
|
41
54
|
<HBadge variant="primary" size="sm" mono pill={false} class="s-model-tag-num">
|
|
@@ -45,6 +58,11 @@ const ModelPill = defineComponent({
|
|
|
45
58
|
<HBadge variant="muted" size="sm" mono pill={false} class="s-model-tag-name">
|
|
46
59
|
<span class="s-model-tag-name-text">{base}</span>
|
|
47
60
|
</HBadge>
|
|
61
|
+
{rateText != null && (
|
|
62
|
+
<HBadge variant="success" size="sm" mono pill={false} class="s-model-tag-rate">
|
|
63
|
+
{rateText}
|
|
64
|
+
</HBadge>
|
|
65
|
+
)}
|
|
48
66
|
</span>
|
|
49
67
|
);
|
|
50
68
|
};
|
|
@@ -57,6 +75,8 @@ const ModelCard = defineComponent({
|
|
|
57
75
|
props: {
|
|
58
76
|
model: { type: String, required: true },
|
|
59
77
|
meta: { type: Object as PropType<HModelMeta | undefined>, default: undefined },
|
|
78
|
+
/** Billing-rate tail passthrough (see ModelPill). */
|
|
79
|
+
rate: { type: Number, default: undefined },
|
|
60
80
|
currency: { type: String, default: "$" },
|
|
61
81
|
},
|
|
62
82
|
setup(props) {
|
|
@@ -99,7 +119,7 @@ const ModelCard = defineComponent({
|
|
|
99
119
|
return (
|
|
100
120
|
<div class="s-model-card">
|
|
101
121
|
<div class="s-model-card-title">
|
|
102
|
-
<ModelPill model={props.model} expanded />
|
|
122
|
+
<ModelPill model={props.model} expanded rate={props.rate} />
|
|
103
123
|
</div>
|
|
104
124
|
{rows.length > 0 && (
|
|
105
125
|
<div class="s-model-card-stats">
|
|
@@ -149,6 +169,8 @@ export const HkModelTag = defineComponent({
|
|
|
149
169
|
/** Per-instance catalog override. */
|
|
150
170
|
catalog: { type: Object as PropType<HModelCatalog | undefined>, default: undefined },
|
|
151
171
|
currency: { type: String, default: "$" },
|
|
172
|
+
/** Optional billing-rate tail on the pill (`×1.5`); see ModelPill. */
|
|
173
|
+
rate: { type: Number, default: undefined },
|
|
152
174
|
},
|
|
153
175
|
setup(props) {
|
|
154
176
|
const anchorRef = ref<HTMLElement | null>(null);
|
|
@@ -175,7 +197,7 @@ export const HkModelTag = defineComponent({
|
|
|
175
197
|
onMouseenter={onEnter}
|
|
176
198
|
onMouseleave={onLeave}
|
|
177
199
|
>
|
|
178
|
-
<ModelPill model={props.model} />
|
|
200
|
+
<ModelPill model={props.model} rate={props.rate} />
|
|
179
201
|
<HPopover
|
|
180
202
|
modelValue={open.value}
|
|
181
203
|
onUpdate:modelValue={(v: boolean) => { open.value = v; }}
|
|
@@ -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
|
+
}
|