@celestia-island/hikari 0.55.122 → 0.55.124
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/HkButton.scss +129 -0
- package/src/components/HkButton.test.tsx +131 -0
- package/src/components/HkButton.tsx +41 -4
- package/src/components/HkTemplateField.scss +238 -0
- package/src/components/HkTemplateField.test.tsx +388 -0
- package/src/components/HkTemplateField.tsx +971 -0
- package/src/components/HkTemplateText.test.tsx +77 -0
- package/src/components/HkTemplateText.tsx +70 -0
- package/src/components/templateGrammar.test.ts +110 -0
- package/src/components/templateGrammar.ts +98 -0
- package/src/composables/iconRegistry.ts +2 -2
- package/src/i18n/locales/ar/components.json +5 -0
- package/src/i18n/locales/de/components.json +5 -0
- package/src/i18n/locales/en/components.json +5 -0
- package/src/i18n/locales/es/components.json +5 -0
- package/src/i18n/locales/fr/components.json +5 -0
- package/src/i18n/locales/ja/components.json +5 -0
- package/src/i18n/locales/ko/components.json +5 -0
- package/src/i18n/locales/pt/components.json +5 -0
- package/src/i18n/locales/ru/components.json +5 -0
- package/src/i18n/locales/zh-Hans/components.json +5 -0
- package/src/i18n/locales/zh-Hant/components.json +5 -0
- package/src/index.ts +12 -0
package/package.json
CHANGED
|
@@ -168,6 +168,135 @@
|
|
|
168
168
|
width: 100%;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/* ---- Responsive label collapse ---- */
|
|
172
|
+
/* A `collapseLabel` button carries icon + text at wide viewports and
|
|
173
|
+
collapses to the SAME icon-only square below the chosen breakpoint
|
|
174
|
+
(chest 2026-10-05 direction: the ad-hoc `hidden sm:inline` label-span
|
|
175
|
+
pattern left the button as a padded icon rectangle on phones — the
|
|
176
|
+
exact squat shape the icon-only contract exists to prevent). The
|
|
177
|
+
geometry mirrors the icon-only ladder above per size; the breakpoint
|
|
178
|
+
px values mirror the runtime useBreakpoint scale (sm 640 / md 768 /
|
|
179
|
+
lg 1024 / xl 1280) — a media query cannot read the TS constant, keep
|
|
180
|
+
the two tables in sync. The TS side suppresses the collapse class
|
|
181
|
+
together with `block` (a full-width button has nothing to collapse
|
|
182
|
+
into), so no block override is needed here. */
|
|
183
|
+
.hk-btn-label {
|
|
184
|
+
/* Inline-flex so multi-node labels (text + inline marks) stay on one
|
|
185
|
+
row and center against the glyph box. */
|
|
186
|
+
display: inline-flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
min-width: 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@mixin hk-btn-collapse-geometry($square) {
|
|
192
|
+
.hk-btn-label {
|
|
193
|
+
display: none;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* The exact icon-only geometry (all-side padding zero, width locked),
|
|
197
|
+
mirrored per size from the ladder above. */
|
|
198
|
+
padding: 0;
|
|
199
|
+
box-sizing: border-box;
|
|
200
|
+
width: $square;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.hk-btn-label-collapse-sm.hk-btn-xs {
|
|
204
|
+
@media (max-width: 639.98px) {
|
|
205
|
+
@include hk-btn-collapse-geometry(1.375rem);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.hk-btn-label-collapse-sm.hk-btn-sm {
|
|
210
|
+
@media (max-width: 639.98px) {
|
|
211
|
+
@include hk-btn-collapse-geometry(1.75rem);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.hk-btn-label-collapse-sm.hk-btn-md {
|
|
216
|
+
@media (max-width: 639.98px) {
|
|
217
|
+
@include hk-btn-collapse-geometry(2.5rem);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.hk-btn-label-collapse-sm.hk-btn-lg {
|
|
222
|
+
@media (max-width: 639.98px) {
|
|
223
|
+
@include hk-btn-collapse-geometry(2.75rem);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.hk-btn-label-collapse-md.hk-btn-xs {
|
|
228
|
+
@media (max-width: 767.98px) {
|
|
229
|
+
@include hk-btn-collapse-geometry(1.375rem);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.hk-btn-label-collapse-md.hk-btn-sm {
|
|
234
|
+
@media (max-width: 767.98px) {
|
|
235
|
+
@include hk-btn-collapse-geometry(1.75rem);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.hk-btn-label-collapse-md.hk-btn-md {
|
|
240
|
+
@media (max-width: 767.98px) {
|
|
241
|
+
@include hk-btn-collapse-geometry(2.5rem);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.hk-btn-label-collapse-md.hk-btn-lg {
|
|
246
|
+
@media (max-width: 767.98px) {
|
|
247
|
+
@include hk-btn-collapse-geometry(2.75rem);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.hk-btn-label-collapse-lg.hk-btn-xs {
|
|
252
|
+
@media (max-width: 1023.98px) {
|
|
253
|
+
@include hk-btn-collapse-geometry(1.375rem);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.hk-btn-label-collapse-lg.hk-btn-sm {
|
|
258
|
+
@media (max-width: 1023.98px) {
|
|
259
|
+
@include hk-btn-collapse-geometry(1.75rem);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.hk-btn-label-collapse-lg.hk-btn-md {
|
|
264
|
+
@media (max-width: 1023.98px) {
|
|
265
|
+
@include hk-btn-collapse-geometry(2.5rem);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.hk-btn-label-collapse-lg.hk-btn-lg {
|
|
270
|
+
@media (max-width: 1023.98px) {
|
|
271
|
+
@include hk-btn-collapse-geometry(2.75rem);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.hk-btn-label-collapse-xl.hk-btn-xs {
|
|
276
|
+
@media (max-width: 1279.98px) {
|
|
277
|
+
@include hk-btn-collapse-geometry(1.375rem);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.hk-btn-label-collapse-xl.hk-btn-sm {
|
|
282
|
+
@media (max-width: 1279.98px) {
|
|
283
|
+
@include hk-btn-collapse-geometry(1.75rem);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.hk-btn-label-collapse-xl.hk-btn-md {
|
|
288
|
+
@media (max-width: 1279.98px) {
|
|
289
|
+
@include hk-btn-collapse-geometry(2.5rem);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.hk-btn-label-collapse-xl.hk-btn-lg {
|
|
294
|
+
@media (max-width: 1279.98px) {
|
|
295
|
+
@include hk-btn-collapse-geometry(2.75rem);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
171
300
|
/* ---- Primary ---- */
|
|
172
301
|
|
|
173
302
|
.hk-btn-primary {
|
|
@@ -255,3 +255,134 @@ describe("HkButton attr fallthrough (inheritAttrs: false)", () => {
|
|
|
255
255
|
expect(cls).toContain("extra-class");
|
|
256
256
|
});
|
|
257
257
|
});
|
|
258
|
+
|
|
259
|
+
describe("HkButton label prop + responsive collapse contract", () => {
|
|
260
|
+
it("renders the label prop in a .hk-btn-label span and keeps the button textual", () => {
|
|
261
|
+
const c = mount(h(HkButton, { icon: "gauge", label: "Test all" }));
|
|
262
|
+
const btn = button(c);
|
|
263
|
+
const label = c.querySelector(".hk-btn-label");
|
|
264
|
+
expect(label).not.toBeNull();
|
|
265
|
+
expect(label!.textContent).toBe("Test all");
|
|
266
|
+
// icon + label = a text button at wide viewports, never icon-only.
|
|
267
|
+
expect(btn.className).not.toContain("hk-btn-icon-only");
|
|
268
|
+
// The label prop is the accessible name unless one is given.
|
|
269
|
+
expect(btn.getAttribute("aria-label")).toBe("Test all");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("prefers the label prop over the default slot and an explicit ariaLabel over the label", () => {
|
|
273
|
+
const c = mount(h(HkButton, { label: "From prop" }, () => "From slot"));
|
|
274
|
+
expect(c.querySelector(".hk-btn-label")!.textContent).toBe("From prop");
|
|
275
|
+
expect(c.textContent).not.toContain("From slot");
|
|
276
|
+
|
|
277
|
+
const named = mount(h(HkButton, { label: "Visible", ariaLabel: "Custom" }));
|
|
278
|
+
expect(button(named).getAttribute("aria-label")).toBe("Custom");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("emits the collapse modifier class from collapseLabel", () => {
|
|
282
|
+
const c = mount(h(HkButton, { icon: "gauge", label: "Probe", collapseLabel: "sm" }));
|
|
283
|
+
expect(button(c).className).toContain("hk-btn-label-collapse-sm");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("suppresses the collapse class with block or shortcut (nothing to collapse into)", () => {
|
|
287
|
+
const blocked = mount(
|
|
288
|
+
h(HkButton, { icon: "gauge", label: "Probe", collapseLabel: "sm", block: true }),
|
|
289
|
+
);
|
|
290
|
+
expect(button(blocked).className).not.toContain("hk-btn-label-collapse");
|
|
291
|
+
|
|
292
|
+
const withShortcut = mount(
|
|
293
|
+
h(HkButton, { icon: "gauge", label: "Probe", collapseLabel: "sm", shortcut: "Ctrl+B" }),
|
|
294
|
+
);
|
|
295
|
+
expect(button(withShortcut).className).not.toContain("hk-btn-label-collapse");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("keeps label-prop buttons out of the icon-only square class", () => {
|
|
299
|
+
// The narrow-end square is the SCSS media query's job; the class
|
|
300
|
+
// must not flip at mount time or the wide viewport would square a
|
|
301
|
+
// labeled button.
|
|
302
|
+
const c = mount(h(HkButton, { icon: "gauge", label: "Probe", collapseLabel: "sm" }));
|
|
303
|
+
expect(button(c).className).not.toContain("hk-btn-icon-only");
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// SCSS geometry contract for the responsive collapse (same textual style
|
|
308
|
+
// as the icon-only contract above — happy-dom does not lay out): every
|
|
309
|
+
// collapse breakpoint × size pair must exist, must collapse below the
|
|
310
|
+
// useBreakpoint px scale (sm 640 / md 768 / lg 1024 / xl 1280), and must
|
|
311
|
+
// land on the exact icon-only square width of its size.
|
|
312
|
+
describe("HkButton label-collapse SCSS contract", () => {
|
|
313
|
+
const scss = readFileSync(join(here, "HkButton.scss"), "utf-8");
|
|
314
|
+
|
|
315
|
+
const breakpoints: Array<[string, string]> = [
|
|
316
|
+
["sm", "639.98px"],
|
|
317
|
+
["md", "767.98px"],
|
|
318
|
+
["lg", "1023.98px"],
|
|
319
|
+
["xl", "1279.98px"],
|
|
320
|
+
];
|
|
321
|
+
const squares: Array<[string, string]> = [
|
|
322
|
+
["xs", "1.375rem"],
|
|
323
|
+
["sm", "1.75rem"],
|
|
324
|
+
["md", "2.5rem"],
|
|
325
|
+
["lg", "2.75rem"],
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
it("collapses every breakpoint × size pair to the icon-only square", () => {
|
|
329
|
+
for (const [bp, query] of breakpoints) {
|
|
330
|
+
for (const [size, square] of squares) {
|
|
331
|
+
const rule = scss.match(
|
|
332
|
+
new RegExp(
|
|
333
|
+
`\\.hk-btn-label-collapse-${bp}\\.hk-btn-${size}\\s*{\\s*@media \\(max-width: ${query}\\) {`,
|
|
334
|
+
),
|
|
335
|
+
)?.[0];
|
|
336
|
+
expect(rule, `${bp}×${size} collapse rule must exist`).toBeTruthy();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("hides the label and lands on the exact square inside each media block", () => {
|
|
342
|
+
// The per-pair rules @include one shared mixin; the geometry lives in
|
|
343
|
+
// its definition (the compiled CSS materializes it per media block).
|
|
344
|
+
const start = scss.indexOf("@mixin hk-btn-collapse-geometry($square) {");
|
|
345
|
+
expect(start, "collapse mixin must exist").toBeGreaterThan(-1);
|
|
346
|
+
// Slice to the mixin's closing brace (nested rules make a [^}] scan
|
|
347
|
+
// stop early, so index from the header to the documented tail line).
|
|
348
|
+
const end = scss.indexOf("width: $square;", start);
|
|
349
|
+
const mixin = scss.slice(start, end + "width: $square;".length);
|
|
350
|
+
expect(mixin).toContain(".hk-btn-label");
|
|
351
|
+
expect(mixin).toContain("display: none;");
|
|
352
|
+
expect(mixin).toContain("padding: 0;");
|
|
353
|
+
expect(mixin).toContain("box-sizing: border-box;");
|
|
354
|
+
expect(mixin).toContain("width: $square");
|
|
355
|
+
|
|
356
|
+
const smBlock = scss.match(
|
|
357
|
+
/\.hk-btn-label-collapse-sm\.hk-btn-sm\s*{\s*@media \(max-width: 639\.98px\) {([^}]*)}/,
|
|
358
|
+
)?.[1];
|
|
359
|
+
expect(smBlock, "sm×sm collapse body must exist").toBeTruthy();
|
|
360
|
+
expect(smBlock).toContain("@include hk-btn-collapse-geometry(1.75rem)");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("mirrors the useBreakpoint scale breakpoints in order", () => {
|
|
364
|
+
// Keep the SCSS table honest against the runtime scale document —
|
|
365
|
+
// the px values appear in ascending breakpoint order.
|
|
366
|
+
const queries = [...scss.matchAll(/@media \(max-width: (\d+(?:\.\d+)?px)\)/g)].map(
|
|
367
|
+
(m) => m[1],
|
|
368
|
+
);
|
|
369
|
+
expect(queries).toEqual([
|
|
370
|
+
"639.98px",
|
|
371
|
+
"639.98px",
|
|
372
|
+
"639.98px",
|
|
373
|
+
"639.98px",
|
|
374
|
+
"767.98px",
|
|
375
|
+
"767.98px",
|
|
376
|
+
"767.98px",
|
|
377
|
+
"767.98px",
|
|
378
|
+
"1023.98px",
|
|
379
|
+
"1023.98px",
|
|
380
|
+
"1023.98px",
|
|
381
|
+
"1023.98px",
|
|
382
|
+
"1279.98px",
|
|
383
|
+
"1279.98px",
|
|
384
|
+
"1279.98px",
|
|
385
|
+
"1279.98px",
|
|
386
|
+
]);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
@@ -13,6 +13,14 @@ export type ButtonVariant =
|
|
|
13
13
|
| "outline";
|
|
14
14
|
export type ButtonSize = "xs" | "sm" | "md" | "lg";
|
|
15
15
|
|
|
16
|
+
/** Breakpoints below which a `collapseLabel` button hides its label and
|
|
17
|
+
* collapses to the icon-only square. Mirrors the runtime useBreakpoint
|
|
18
|
+
* scale (sm 640 / md 768 / lg 1024 / xl 1280) — the SCSS side repeats
|
|
19
|
+
* the px values because a media query cannot read a TS constant; keep
|
|
20
|
+
* the two in sync. "xs" is deliberately absent: collapse-at-xs means
|
|
21
|
+
* "always collapsed", which is just the icon prop + label-less button. */
|
|
22
|
+
export type ButtonLabelCollapse = "sm" | "md" | "lg" | "xl";
|
|
23
|
+
|
|
16
24
|
export default defineComponent({
|
|
17
25
|
name: "HkButton",
|
|
18
26
|
inheritAttrs: false,
|
|
@@ -26,6 +34,24 @@ export default defineComponent({
|
|
|
26
34
|
shortcut: { type: String, default: undefined },
|
|
27
35
|
icon: { type: String, default: undefined },
|
|
28
36
|
suffix: { type: String, default: undefined },
|
|
37
|
+
/** Plain-text label, rendered in a `.hk-btn-label` span after the
|
|
38
|
+
* glyph. Pairs with `collapseLabel`: at wide viewports the button
|
|
39
|
+
* reads icon + text; below the chosen breakpoint the label hides
|
|
40
|
+
* and the button collapses to the icon-only square. When set, the
|
|
41
|
+
* default slot is ignored (one label source per button) and the
|
|
42
|
+
* accessible name falls back to this text, so the button keeps its
|
|
43
|
+
* name at breakpoints where the visible label is display:none. */
|
|
44
|
+
label: { type: String, default: undefined },
|
|
45
|
+
/** Breakpoint below which the label hides and the button collapses
|
|
46
|
+
* to the icon-only square (see `ButtonLabelCollapse`). Requires
|
|
47
|
+
* `label`; ignored together with `block` (a full-width button has
|
|
48
|
+
* nothing to collapse into) and with `shortcut` (the shortcut chip
|
|
49
|
+
* is an extra child the square width would visibly clip — the same
|
|
50
|
+
* exclusion the icon-only contract applies). */
|
|
51
|
+
collapseLabel: {
|
|
52
|
+
type: String as PropType<ButtonLabelCollapse>,
|
|
53
|
+
default: undefined,
|
|
54
|
+
},
|
|
29
55
|
},
|
|
30
56
|
emits: {
|
|
31
57
|
click: (_e: MouseEvent) => true,
|
|
@@ -40,6 +66,9 @@ export default defineComponent({
|
|
|
40
66
|
props.block ? "hk-btn-block" : "",
|
|
41
67
|
props.loading ? "hk-btn-loading" : "",
|
|
42
68
|
props.shortcut ? "hk-btn-has-shortcut" : "",
|
|
69
|
+
props.collapseLabel && props.label && !props.block && !props.shortcut
|
|
70
|
+
? `hk-btn-label-collapse-${props.collapseLabel}`
|
|
71
|
+
: "",
|
|
43
72
|
]);
|
|
44
73
|
|
|
45
74
|
return () => {
|
|
@@ -51,9 +80,13 @@ export default defineComponent({
|
|
|
51
80
|
// reactive, so a cached flag would go stale when a parent toggles
|
|
52
81
|
// the label slot without touching the icon props. A shortcut chip
|
|
53
82
|
// is excluded: it renders as an extra flex child the fixed square
|
|
54
|
-
// width would visibly clip.
|
|
83
|
+
// width would visibly clip. A `label` prop counts as text (the
|
|
84
|
+
// responsive collapse handles the narrow end via CSS instead).
|
|
55
85
|
const iconOnly =
|
|
56
|
-
!slots.default &&
|
|
86
|
+
!slots.default &&
|
|
87
|
+
props.label === undefined &&
|
|
88
|
+
Boolean(props.icon || props.suffix) &&
|
|
89
|
+
!props.shortcut;
|
|
57
90
|
|
|
58
91
|
return (
|
|
59
92
|
<button
|
|
@@ -62,7 +95,7 @@ export default defineComponent({
|
|
|
62
95
|
disabled={props.disabled || props.loading}
|
|
63
96
|
class={[buttonClass.value, iconOnly ? "hk-btn-icon-only" : "", attrs.class]}
|
|
64
97
|
style={attrs.style || undefined}
|
|
65
|
-
aria-label={props.ariaLabel}
|
|
98
|
+
aria-label={props.ariaLabel ?? props.label}
|
|
66
99
|
aria-busy={props.loading || undefined}
|
|
67
100
|
onClick={(e) => emit("click", e)}
|
|
68
101
|
>
|
|
@@ -116,7 +149,11 @@ export default defineComponent({
|
|
|
116
149
|
<HIcon name={props.icon} size={16} />
|
|
117
150
|
</span>
|
|
118
151
|
) : null}
|
|
119
|
-
{
|
|
152
|
+
{props.label !== undefined && props.label !== "" ? (
|
|
153
|
+
<span class="hk-btn-label">{props.label}</span>
|
|
154
|
+
) : (
|
|
155
|
+
slots.default?.()
|
|
156
|
+
)}
|
|
120
157
|
{props.suffix ? (
|
|
121
158
|
<span class="hk-btn-suffix">
|
|
122
159
|
<HIcon name={props.suffix} size={16} />
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// HkTemplateField / HkTemplateText — the fill-template surfaces.
|
|
2
|
+
// Self-sufficient styling (house rule): the field box, label, hint and
|
|
3
|
+
// error rows live here rather than borrowing HkInput's sheet, and the
|
|
4
|
+
// chip is THE shared token visual — the editor's chips and the
|
|
5
|
+
// read-only renderer's chips are the same class, so a stored template
|
|
6
|
+
// reads identically wherever it appears.
|
|
7
|
+
|
|
8
|
+
.hk-template-field {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: var(--space-4, 4px);
|
|
12
|
+
width: 100%;
|
|
13
|
+
|
|
14
|
+
&[data-disabled] {
|
|
15
|
+
opacity: 0.55;
|
|
16
|
+
pointer-events: none;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.hk-template-field-label {
|
|
21
|
+
font-size: var(--text-xs, 0.75rem);
|
|
22
|
+
font-weight: 500;
|
|
23
|
+
color: var(--hi-color-text-primary);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.hk-template-field-hint,
|
|
27
|
+
.hk-template-field-error {
|
|
28
|
+
margin: 0;
|
|
29
|
+
font-size: var(--text-2xs, 0.625rem);
|
|
30
|
+
line-height: 1.4;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.hk-template-field-hint {
|
|
34
|
+
color: var(--hi-color-muted);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.hk-template-field-error {
|
|
38
|
+
color: var(--hi-color-danger);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.hk-template-field-box {
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
display: block;
|
|
44
|
+
width: 100%;
|
|
45
|
+
padding: var(--space-6, 0.375rem) var(--space-10, 0.625rem);
|
|
46
|
+
background: var(--hi-color-surface);
|
|
47
|
+
border: 1px solid var(--hi-color-border);
|
|
48
|
+
border-radius: 6px;
|
|
49
|
+
transition:
|
|
50
|
+
border-color var(--hi-transition-base, 200ms cubic-bezier(0.4, 0, 0.2, 1)),
|
|
51
|
+
box-shadow var(--hi-transition-base, 200ms cubic-bezier(0.4, 0, 0.2, 1));
|
|
52
|
+
|
|
53
|
+
&:hover {
|
|
54
|
+
border-color: var(--hi-color-primary);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
&:focus-within {
|
|
58
|
+
border-color: var(--hi-color-primary);
|
|
59
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--hi-color-primary) 18%, transparent);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.hk-template-field-box-error {
|
|
64
|
+
border-color: var(--hi-color-danger);
|
|
65
|
+
|
|
66
|
+
&:focus-within {
|
|
67
|
+
border-color: var(--hi-color-danger);
|
|
68
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--hi-color-danger) 15%, transparent);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.hk-template-field-edit {
|
|
73
|
+
// Slightly tighter than a plain input: chips carry their own padding,
|
|
74
|
+
// and the field must not read taller than the HkInput beside it.
|
|
75
|
+
min-height: calc(1.5rem + var(--space-4, 4px));
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-wrap: wrap;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 2px 3px;
|
|
80
|
+
font-size: var(--text-base, 0.875rem);
|
|
81
|
+
line-height: 1.5;
|
|
82
|
+
color: var(--hi-color-text-primary);
|
|
83
|
+
outline: none;
|
|
84
|
+
word-break: break-all;
|
|
85
|
+
cursor: text;
|
|
86
|
+
|
|
87
|
+
&:empty::before {
|
|
88
|
+
content: attr(data-ph);
|
|
89
|
+
color: var(--hi-color-muted);
|
|
90
|
+
opacity: 0.6;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── The chip ─────────────────────────────────────────────────────────
|
|
95
|
+
// Monospace (it IS code — the exact text the host fills), primary
|
|
96
|
+
// tinted, unbreakable. Editable context adds the click affordance and
|
|
97
|
+
// the unknown marker both surfaces share.
|
|
98
|
+
|
|
99
|
+
.hk-tpl-chip {
|
|
100
|
+
display: inline-flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
padding: 0 var(--space-6, 6px);
|
|
103
|
+
border-radius: var(--radius-sm, 4px);
|
|
104
|
+
border: 1px solid color-mix(in srgb, var(--hi-color-primary) 32%, transparent);
|
|
105
|
+
background: color-mix(in srgb, var(--hi-color-primary) 12%, transparent);
|
|
106
|
+
color: color-mix(in srgb, var(--hi-color-primary) 70%, rgb(0 0 0));
|
|
107
|
+
font-family: var(--font-mono, monospace);
|
|
108
|
+
font-size: 0.8em;
|
|
109
|
+
line-height: 1.5;
|
|
110
|
+
white-space: nowrap;
|
|
111
|
+
user-select: none;
|
|
112
|
+
|
|
113
|
+
[data-theme="dark"] & {
|
|
114
|
+
color: color-mix(in srgb, var(--hi-color-primary) 60%, rgb(255 255 255));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
&[data-unknown] {
|
|
118
|
+
border-color: var(--hi-color-border);
|
|
119
|
+
background: color-mix(in srgb, var(--hi-color-warning) 12%, transparent);
|
|
120
|
+
color: var(--hi-color-muted);
|
|
121
|
+
font-style: italic;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.hk-template-field-edit .hk-tpl-chip {
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
|
|
128
|
+
&:hover {
|
|
129
|
+
border-color: var(--hi-color-primary);
|
|
130
|
+
background: color-mix(in srgb, var(--hi-color-primary) 20%, transparent);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
&:focus-visible {
|
|
134
|
+
outline: 2px solid var(--hi-color-primary);
|
|
135
|
+
outline-offset: 1px;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.hk-template-text {
|
|
140
|
+
display: inline;
|
|
141
|
+
max-width: 100%;
|
|
142
|
+
|
|
143
|
+
&[data-multiline] {
|
|
144
|
+
display: block;
|
|
145
|
+
overflow-wrap: anywhere;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.hk-template-text-run {
|
|
150
|
+
white-space: pre-wrap;
|
|
151
|
+
overflow-wrap: anywhere;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Vocabulary rows (panel + chip editor share them) ────────────────
|
|
155
|
+
|
|
156
|
+
.hk-tpl-rows,
|
|
157
|
+
.hk-tpl-editor-rows {
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
gap: 2px;
|
|
161
|
+
padding: var(--space-4, 4px);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.hk-tpl-row {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
align-items: flex-start;
|
|
168
|
+
gap: 1px;
|
|
169
|
+
padding: var(--space-4, 4px) var(--space-8, 8px);
|
|
170
|
+
border: none;
|
|
171
|
+
border-radius: var(--radius-sm, 4px);
|
|
172
|
+
background: transparent;
|
|
173
|
+
color: var(--hi-color-text-primary);
|
|
174
|
+
text-align: start;
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
|
|
177
|
+
&:hover,
|
|
178
|
+
&[data-active] {
|
|
179
|
+
background: color-mix(in srgb, var(--hi-color-primary) 10%, transparent);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.hk-tpl-row-name {
|
|
184
|
+
font-family: var(--font-mono, monospace);
|
|
185
|
+
font-size: var(--text-xs, 0.75rem);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.hk-tpl-row-desc {
|
|
189
|
+
font-size: var(--text-2xs, 0.625rem);
|
|
190
|
+
color: var(--hi-color-muted);
|
|
191
|
+
line-height: 1.4;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.hk-tpl-rows-empty {
|
|
195
|
+
padding: var(--space-8, 8px) var(--space-12, 12px);
|
|
196
|
+
margin: 0;
|
|
197
|
+
font-size: var(--text-xs, 0.75rem);
|
|
198
|
+
color: var(--hi-color-muted);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Chip editor form ────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
.hk-tpl-editor {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-direction: column;
|
|
206
|
+
gap: var(--space-8, 8px);
|
|
207
|
+
min-width: 16rem;
|
|
208
|
+
max-width: min(22rem, 90vw);
|
|
209
|
+
padding: var(--space-8, 8px);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.hk-tpl-editor-current {
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: baseline;
|
|
215
|
+
gap: var(--space-8, 8px);
|
|
216
|
+
flex-wrap: wrap;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.hk-tpl-editor-desc {
|
|
220
|
+
font-size: var(--text-2xs, 0.625rem);
|
|
221
|
+
color: var(--hi-color-muted);
|
|
222
|
+
line-height: 1.4;
|
|
223
|
+
flex: 1 1 10rem;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ONE scrollbar per window (family contract): the mobile sheet's panel
|
|
227
|
+
// already scrolls the whole form through its own overlay scrollbar, so
|
|
228
|
+
// the rows cap exists ONLY in the desktop popover (which sizes to
|
|
229
|
+
// content and otherwise has no scroll region at all).
|
|
230
|
+
.hk-popover-panel:not(.hk-is-sheet) .hk-tpl-editor-rows {
|
|
231
|
+
max-height: min(16rem, 40dvh);
|
|
232
|
+
overflow-y: auto;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.hk-tpl-editor-footer {
|
|
236
|
+
display: flex;
|
|
237
|
+
justify-content: flex-end;
|
|
238
|
+
}
|