@keyfunc/keybricks-ds 2.3.1 → 2.4.2

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.
Files changed (83) hide show
  1. package/DEPLOY.md +11 -5
  2. package/DESIGN.md +50 -1
  3. package/INTEGRATION.md +33 -6
  4. package/_adherence.oxlintrc.json +35 -13
  5. package/assets/logos/datasource/dat.png +0 -0
  6. package/assets/logos/datasource/excel.png +0 -0
  7. package/assets/logos/datasource/ftp.png +0 -0
  8. package/assets/logos/datasource/hierarchy.png +0 -0
  9. package/assets/logos/datasource/hive.png +0 -0
  10. package/assets/logos/datasource/json.png +0 -0
  11. package/assets/logos/datasource/mariadb.png +0 -0
  12. package/assets/logos/datasource/mongodb.png +0 -0
  13. package/assets/logos/datasource/mysql.png +0 -0
  14. package/assets/logos/datasource/odbc.png +0 -0
  15. package/assets/logos/datasource/oracle.png +0 -0
  16. package/assets/logos/datasource/postgresql.png +0 -0
  17. package/assets/logos/datasource/rest-api.png +0 -0
  18. package/assets/logos/datasource/sap-alt.png +0 -0
  19. package/assets/logos/datasource/sap.png +0 -0
  20. package/assets/logos/datasource/soap.png +0 -0
  21. package/assets/logos/datasource/sql-server.png +0 -0
  22. package/components/_cards/app-chrome.card.html +33 -21
  23. package/components/_cards/context-menu.card.html +7 -2
  24. package/components/_cards/icons-0-usage.card.html +88 -0
  25. package/components/_cards/icons-4-datasource.card.html +5 -0
  26. package/components/_cards/relative-time-picker.card.html +45 -0
  27. package/components/_cards/segmented-control.card.html +9 -3
  28. package/components/_cards/smart-table.card.html +4 -1
  29. package/components/_cards/tabs.card.html +5 -2
  30. package/components/_shared/src-data.js +1 -1
  31. package/components/_shared/src-feedback.js +1 -1
  32. package/components/_shared/src-forms.js +1 -1
  33. package/components/_shared/src-navigation.js +1 -1
  34. package/components/_shared/src-tabs.js +1 -1
  35. package/components/_shared/view-source.js +12 -12
  36. package/components/_vue/index.mjs +5 -1
  37. package/components/data/SmartTable.d.ts +7 -2
  38. package/components/data/SmartTable.jsx +12 -4
  39. package/components/data/SmartTable.vue +12 -4
  40. package/components/feedback/ContextMenu.d.ts +3 -2
  41. package/components/feedback/ContextMenu.jsx +8 -4
  42. package/components/feedback/ContextMenu.vue +9 -6
  43. package/components/feedback/Menu.d.ts +1 -1
  44. package/components/feedback/feedback.prompt.md +1 -1
  45. package/components/forms/RelativeTimePicker.d.ts +39 -0
  46. package/components/forms/RelativeTimePicker.jsx +89 -0
  47. package/components/forms/RelativeTimePicker.prompt.md +72 -0
  48. package/components/forms/RelativeTimePicker.vue +81 -0
  49. package/components/forms/forms.prompt.md +3 -0
  50. package/components/forms/relative-time.d.ts +12 -0
  51. package/components/forms/relative-time.js +90 -0
  52. package/components/index.d.ts +2 -0
  53. package/components/index.js +5 -3
  54. package/components/navigation/SidePanel.d.ts +22 -0
  55. package/components/navigation/SidePanel.jsx +23 -0
  56. package/components/navigation/SidePanel.vue +21 -0
  57. package/components/navigation/Sidebar.d.ts +23 -7
  58. package/components/navigation/Sidebar.jsx +38 -37
  59. package/components/navigation/Sidebar.vue +40 -39
  60. package/components/navigation/TopBar.d.ts +28 -3
  61. package/components/navigation/TopBar.jsx +54 -7
  62. package/components/navigation/TopBar.vue +26 -8
  63. package/components/navigation/UserMenu.vue +27 -0
  64. package/components/navigation/chrome-tones.js +22 -0
  65. package/components/navigation/nav-model.js +21 -18
  66. package/components/navigation/navigation.prompt.md +2 -0
  67. package/components/tabs/CountBadge.vue +12 -0
  68. package/components/tabs/PanelTabs.vue +7 -3
  69. package/components/tabs/PillTabs.vue +7 -3
  70. package/components/tabs/SegmentedControl.d.ts +4 -2
  71. package/components/tabs/SegmentedControl.jsx +5 -3
  72. package/components/tabs/SegmentedControl.vue +4 -3
  73. package/components/tabs/Tabs.d.ts +14 -4
  74. package/components/tabs/Tabs.jsx +35 -20
  75. package/components/tabs/Tabs.vue +7 -3
  76. package/components/tabs/tabs.prompt.md +2 -2
  77. package/package.json +2 -2
  78. package/readme.md +6 -5
  79. package/readme.zh-CN.md +4 -3
  80. package/tokens/layout.css +4 -3
  81. package/tokens/tokens.d.ts +2 -0
  82. package/tokens/tokens.js +4 -1
  83. package/tokens/tokens.json +8 -1
@@ -1 +1 @@
1
- window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"forms/Checkbox.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text. */\nexport function Checkbox({ checked, indeterminate, onChange, label, size, disabled, style }) {\n const box = \"var(--check-size)\", glyph = { width: \"calc(var(--check-size) - 4px)\", height: \"calc(var(--check-size) - 4px)\" };\n const filled = checked || indeterminate;\n return (\n <label data-size={size} data-check={disabled ? undefined : \"\"} style={{ display: \"inline-flex\", alignItems: \"center\", verticalAlign: \"middle\", lineHeight: \"var(--control-leading)\", gap: \"var(--control-gap)\", cursor: disabled ? \"not-allowed\" : \"pointer\", opacity: disabled ? .5 : 1, ...style }}\n onClick={() => !disabled && onChange && onChange(!checked)}>\n <span style={{ width: box, height: box, borderRadius: 4, boxSizing: \"border-box\", border: filled ? \"none\" : \"1.5px solid var(--check-border, var(--checkbox-border))\", background: filled ? \"var(--action-primary)\" : \"var(--bg-surface)\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", color: \"var(--on-accent)\", flexShrink: 0, transition: \"var(--t-hover)\" }}>\n {checked ? <Icon name=\"check\" size={12} strokeWidth={3} style={glyph} /> : indeterminate ? <Icon name=\"minus\" size={12} strokeWidth={3} style={glyph} /> : null}\n </span>\n {label ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-regular)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-body)\" }}>{label}</span> : null}\n </label>\n );\n}\n","forms/Checkbox.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text.\nconst props = defineProps({ checked: Boolean, indeterminate: Boolean, label: String, size: String, disabled: Boolean, style: Object })\nconst emit = defineEmits(['change'])\nconst box = 'var(--check-size)', glyph = { width: 'calc(var(--check-size) - 4px)', height: 'calc(var(--check-size) - 4px)' }\nconst filled = computed(() => props.checked || props.indeterminate)\n</script>\n<template>\n <label :data-size=\"size\" :data-check=\"disabled ? undefined : ''\" :style=\"{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--control-leading)', gap: 'var(--control-gap)', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, ...style }\" @click=\"!disabled && emit('change', !checked)\">\n <span :style=\"{ width: box, height: box, borderRadius: '4px', boxSizing: 'border-box', border: filled ? 'none' : '1.5px solid var(--check-border, var(--checkbox-border))', background: filled ? 'var(--action-primary)' : 'var(--bg-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--on-accent)', flexShrink: 0, transition: 'var(--t-hover)' }\">\n <Icon v-if=\"checked\" name=\"check\" :size=\"12\" :stroke-width=\"3\" :style=\"glyph\" />\n <Icon v-else-if=\"indeterminate\" name=\"minus\" :size=\"12\" :stroke-width=\"3\" :style=\"glyph\" />\n </span>\n <span v-if=\"label\" style=\"font-family:var(--font-sans);font-weight:var(--weight-regular);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-body)\">{{ label }}</span>\n </label>\n</template>\n","forms/Combobox.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { inputBase } from \"./Input.jsx\";\n\nconst tierIcon = { width: \"var(--control-icon)\", height: \"var(--control-icon)\" };\n\n/** Typeahead single-select: type to filter options, click or Enter to pick. Chevron toggles the list; shows \"No results\" when the filter matches nothing. Field and option rows take the region's tier height (omit `size` to inherit). */\nexport function Combobox({ options = [], value = \"\", placeholder = \"Select...\", disabled, error, width, size, onChange, style }) {\n const [open, setOpen] = React.useState(false);\n const [query, setQuery] = React.useState(null);\n const [hov, setHov] = React.useState(-1);\n const root = React.useRef(null);\n React.useEffect(() => { const h = (e) => { if (root.current && !root.current.contains(e.target)) { setOpen(false); setQuery(null); } }; document.addEventListener(\"mousedown\", h); return () => document.removeEventListener(\"mousedown\", h); }, []);\n const shown = query == null ? options : options.filter(o => o.toLowerCase().includes(query.toLowerCase()));\n const pick = (o) => { onChange && onChange(o); setOpen(false); setQuery(null); };\n return (\n <div ref={root} data-size={size} style={{ position: \"relative\", width, ...style }}>\n <div data-field={disabled ? undefined : \"\"} style={{ position: \"relative\" }}>\n <input value={query != null ? query : value} placeholder={placeholder} disabled={disabled}\n onFocus={() => setOpen(true)} onChange={(e) => { setQuery(e.target.value); setOpen(true); setHov(0); }}\n onKeyDown={(e) => { if (e.key === \"ArrowDown\") setHov(Math.min(hov + 1, shown.length - 1)); if (e.key === \"ArrowUp\") setHov(Math.max(hov - 1, 0)); if (e.key === \"Enter\" && shown[hov]) pick(shown[hov]); if (e.key === \"Escape\") { setOpen(false); setQuery(null); } }}\n style={{ ...inputBase, paddingRight: \"calc(var(--control-px) + var(--control-icon) + 6px)\", borderColor: error ? \"var(--danger)\" : open ? \"var(--focus-border)\" : \"var(--field-border, var(--border-control))\", boxShadow: open ? \"0 0 0 2px var(--focus-ring)\" : \"none\", opacity: disabled ? .5 : 1, cursor: disabled ? \"not-allowed\" : \"text\" }} />\n <button type=\"button\" disabled={disabled} onClick={() => { setOpen(!open); setQuery(null); }} style={{ position: \"absolute\", right: 1, top: \"50%\", transform: \"translateY(-50%)\", width: \"calc(var(--control-h) - 2px)\", height: \"calc(var(--control-h) - 2px)\", border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--text-muted)\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", padding: 0 }}>\n <span style={{ display: \"inline-flex\", transform: open ? \"rotate(180deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}><Icon name=\"chevron-down\" size={16} style={tierIcon} /></span>\n </button>\n </div>\n {open && !disabled && <div style={{ position: \"absolute\", top: \"calc(100% + 4px)\", left: 0, right: 0, zIndex: \"var(--z-menu)\", background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: 4, maxHeight: 208, overflowY: \"auto\" }}>\n {shown.length === 0 && <div style={{ height: \"var(--control-h)\", display: \"flex\", alignItems: \"center\", padding: \"0 10px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-muted)\" }}>No results</div>}\n {shown.map((o, i) => <button key={o} type=\"button\" onClick={() => pick(o)} onMouseEnter={() => setHov(i)}\n style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", width: \"100%\", height: \"var(--control-h)\", padding: \"0 10px\", borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: \"pointer\", background: hov === i ? \"var(--bg-hover)\" : \"transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-heading)\", textAlign: \"left\" }}>\n {o}{o === value && <span style={{ color: \"var(--action-primary)\", display: \"inline-flex\" }}><Icon name=\"check\" size={16} style={tierIcon} /></span>}\n </button>)}\n </div>}\n </div>\n );\n}\n","forms/Combobox.vue":"<script setup>\nimport { ref, computed, onMounted, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { inputBase } from './input-base.js'\n// Typeahead single-select: type to filter options, click or Enter to pick; \"No results\" empty row. Field and option rows take the region's tier height (omit `size` to inherit).\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select...' }, disabled: Boolean, error: Boolean, width: [Number, String], size: String, style: Object })\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst emit = defineEmits(['update:modelValue'])\nconst open = ref(false), query = ref(null), hov = ref(-1), root = ref(null)\nconst shown = computed(() => query.value == null ? props.options : props.options.filter(o => o.toLowerCase().includes(query.value.toLowerCase())))\nfunction pick(o) { emit('update:modelValue', o); open.value = false; query.value = null }\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) { open.value = false; query.value = null } }\nonMounted(() => document.addEventListener('mousedown', onDoc))\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\n</script>\n<template>\n <div ref=\"root\" :data-size=\"size\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div :data-field=\"disabled ? undefined : ''\" style=\"position:relative\">\n <input :value=\"query != null ? query : modelValue\" :placeholder=\"placeholder\" :disabled=\"disabled\"\n @focus=\"open = true\" @input=\"query = $event.target.value; open = true; hov = 0\"\n @keydown.down=\"hov = Math.min(hov + 1, shown.length - 1)\" @keydown.up=\"hov = Math.max(hov - 1, 0)\"\n @keydown.enter=\"shown[hov] && pick(shown[hov])\" @keydown.esc=\"open = false; query = null\"\n :style=\"{ ...inputBase, paddingRight: 'calc(var(--control-px) + var(--control-icon) + 6px)', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1 }\" />\n <button type=\"button\" :disabled=\"disabled\" @click=\"open = !open; query = null\" style=\"position:absolute;right:1px;top:50%;transform:translateY(-50%);width:calc(var(--control-h) - 2px);height:calc(var(--control-h) - 2px);border:none;background:transparent;cursor:pointer;color:var(--text-muted);display:flex;align-items:center;justify-content:center;padding:0\">\n <span :style=\"{ display: 'inline-flex', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\"><Icon name=\"chevron-down\" :size=\"16\" :style=\"tierIcon\" /></span>\n </button>\n </div>\n <div v-if=\"open && !disabled\" style=\"position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:var(--z-menu);background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;max-height:208px;overflow-y:auto\">\n <div v-if=\"shown.length === 0\" style=\"height:var(--control-h);display:flex;align-items:center;padding:0 10px;font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);color:var(--text-muted)\">No results</div>\n <button v-for=\"(o, i) in shown\" :key=\"o\" type=\"button\" @click=\"pick(o)\" @mouseenter=\"hov = i\"\n :style=\"{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', height: 'var(--control-h)', padding: '0 10px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: 'pointer', background: hov === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-heading)', textAlign: 'left' }\">\n {{ o }}<span v-if=\"o === modelValue\" style=\"color:var(--action-primary);display:inline-flex\"><Icon name=\"check\" :size=\"16\" :style=\"tierIcon\" /></span>\n </button>\n </div>\n </div>\n</template>\n","forms/DatePicker.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { inputBase } from \"./Input.jsx\";\n\nconst pad = (n) => String(n).padStart(2, \"0\");\nconst iso = (d) => d.getFullYear() + \"-\" + pad(d.getMonth() + 1) + \"-\" + pad(d.getDate());\nconst tierIcon = { width: \"var(--control-icon)\", height: \"var(--control-icon)\" };\n/** Month title, weekday initials and the field label come from Intl for the given locale (en-US, zh-CN, ja-JP…), so the picker reads right in every site language. */\nconst fmt = (locale, opts) => { try { return new Intl.DateTimeFormat(locale, opts); } catch (e) { return new Intl.DateTimeFormat(\"en-US\", opts); } };\nconst weekdays = (locale) => Array.from({ length: 7 }, (_, i) => fmt(locale, { weekday: \"short\" }).format(new Date(2024, 0, 1 + i))); // 2024-01-01 is a Monday\n\n/** Date picker: input with calendar glyph opening a month-grid popover — chevron month nav, today outlined, selected day filled blue. value/onChange use \"YYYY-MM-DD\"; `locale` localises the labels; the field takes the region's tier (omit `size` to inherit). */\nexport function DatePicker({ value = \"\", placeholder = \"Select date\", disabled, error, width = 180, size, locale = \"en-US\", onChange, style }) {\n const [open, setOpen] = React.useState(false);\n const sel = value ? new Date(value + \"T00:00:00\") : null;\n const [view, setView] = React.useState(() => { const d = sel || new Date(); return { y: d.getFullYear(), m: d.getMonth() }; });\n const root = React.useRef(null);\n React.useEffect(() => { const h = (e) => { if (root.current && !root.current.contains(e.target)) setOpen(false); }; document.addEventListener(\"mousedown\", h); return () => document.removeEventListener(\"mousedown\", h); }, []);\n const first = new Date(view.y, view.m, 1);\n const start = (first.getDay() + 6) % 7; // Monday-first\n const days = new Date(view.y, view.m + 1, 0).getDate();\n const todayIso = iso(new Date());\n const nav = (d) => { let m = view.m + d, y = view.y; if (m < 0) { m = 11; y--; } if (m > 11) { m = 0; y++; } setView({ y, m }); };\n const label = sel ? fmt(locale, { year: \"numeric\", month: \"short\", day: \"numeric\" }).format(sel) : \"\";\n const monthTitle = fmt(locale, { year: \"numeric\", month: \"long\" }).format(new Date(view.y, view.m, 1));\n return (\n <div ref={root} data-size={size} style={{ position: \"relative\", width, ...style }}>\n <div data-field={disabled ? undefined : \"\"} onClick={() => !disabled && setOpen(!open)} style={{ ...inputBase, display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", cursor: disabled ? \"not-allowed\" : \"pointer\", borderColor: error ? \"var(--danger)\" : open ? \"var(--focus-border)\" : \"var(--field-border, var(--border-control))\", boxShadow: open ? \"0 0 0 2px var(--focus-ring)\" : \"none\", opacity: disabled ? .5 : 1, color: label ? \"var(--text-primary)\" : \"var(--text-muted)\" }}>\n {label || placeholder}<span style={{ color: \"var(--text-muted)\", display: \"inline-flex\" }}><Icon name=\"calendar\" size={16} style={tierIcon} /></span>\n </div>\n {open && <div style={{ position: \"absolute\", top: \"calc(100% + 4px)\", left: 0, zIndex: \"var(--z-menu)\", width: 252, background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-lg)\", boxShadow: \"var(--shadow-menu)\", padding: 12, boxSizing: \"border-box\" }}>\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", paddingBottom: 8 }}>\n <IconButton icon=\"chevron-left\" label=\"Previous month\" onClick={() => nav(-1)} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-heading)\" }}>{monthTitle}</span>\n <IconButton icon=\"chevron-right\" label=\"Next month\" onClick={() => nav(1)} />\n </div>\n <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(7,1fr)\", gap: 2 }}>\n {weekdays(locale).map((d, i) => <span key={i} style={{ textAlign: \"center\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\", padding: \"2px 0\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{d}</span>)}\n {Array.from({ length: start }, (_, i) => <span key={\"e\" + i}></span>)}\n {Array.from({ length: days }, (_, i) => {\n const dIso = view.y + \"-\" + pad(view.m + 1) + \"-\" + pad(i + 1);\n const isSel = value === dIso, isToday = todayIso === dIso;\n return <button key={i} type=\"button\" onClick={() => { onChange && onChange(dIso); setOpen(false); }}\n style={{ height: 28, borderRadius: \"var(--radius-sm)\", border: isToday && !isSel ? \"1px solid var(--action-primary)\" : \"1px solid transparent\", cursor: \"pointer\", background: isSel ? \"var(--action-primary)\" : \"transparent\", color: isSel ? \"var(--on-accent)\" : \"var(--text-body)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", fontVariantNumeric: \"tabular-nums\", padding: 0, boxSizing: \"border-box\" }}\n onMouseEnter={(e) => { if (!isSel) e.currentTarget.style.background = \"var(--bg-hover)\"; }} onMouseLeave={(e) => { if (!isSel) e.currentTarget.style.background = \"transparent\"; }}>{i + 1}</button>;\n })}\n </div>\n </div>}\n </div>\n );\n}\n","forms/DatePicker.vue":"<script setup>\nimport { ref, computed, onMounted, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport { inputBase } from './input-base.js'\n// Date picker: input with calendar glyph opening a month-grid popover — Monday-first, today outlined, selected filled. v-model is \"YYYY-MM-DD\".\n// `locale` localises month title, weekday initials and the field label through Intl; the field takes the region's tier (omit `size` to inherit).\nconst pad = (n) => String(n).padStart(2, '0')\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst fmt = (locale, opts) => { try { return new Intl.DateTimeFormat(locale, opts) } catch (e) { return new Intl.DateTimeFormat('en-US', opts) } }\nconst props = defineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select date' }, disabled: Boolean, error: Boolean, width: { type: [Number, String], default: 180 }, size: String, locale: { type: String, default: 'en-US' }, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst open = ref(false), root = ref(null)\nconst sel = computed(() => props.modelValue ? new Date(props.modelValue + 'T00:00:00') : null)\nconst d0 = sel.value || new Date()\nconst view = ref({ y: d0.getFullYear(), m: d0.getMonth() })\nconst start = computed(() => (new Date(view.value.y, view.value.m, 1).getDay() + 6) % 7)\nconst days = computed(() => new Date(view.value.y, view.value.m + 1, 0).getDate())\nconst todayIso = (() => { const d = new Date(); return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) })()\nconst label = computed(() => sel.value ? fmt(props.locale, { year: 'numeric', month: 'short', day: 'numeric' }).format(sel.value) : '')\nconst monthTitle = computed(() => fmt(props.locale, { year: 'numeric', month: 'long' }).format(new Date(view.value.y, view.value.m, 1)))\nconst weekdays = computed(() => Array.from({ length: 7 }, (_, i) => fmt(props.locale, { weekday: 'short' }).format(new Date(2024, 0, 1 + i)))) // 2024-01-01 is a Monday\nfunction nav(d) { let m = view.value.m + d, y = view.value.y; if (m < 0) { m = 11; y-- } if (m > 11) { m = 0; y++ } view.value = { y, m } }\nfunction dayIso(i) { return view.value.y + '-' + pad(view.value.m + 1) + '-' + pad(i) }\nfunction pick(i) { emit('update:modelValue', dayIso(i)); open.value = false }\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }\nonMounted(() => document.addEventListener('mousedown', onDoc))\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\n</script>\n<template>\n <div ref=\"root\" :data-size=\"size\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div :data-field=\"disabled ? undefined : ''\" @click=\"!disabled && (open = !open)\" :style=\"{ ...inputBase, display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: disabled ? 'not-allowed' : 'pointer', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1, color: label ? 'var(--text-primary)' : 'var(--text-muted)' }\">\n {{ label || placeholder }}<span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"calendar\" :size=\"16\" :style=\"tierIcon\" /></span>\n </div>\n <div v-if=\"open\" style=\"position:absolute;top:calc(100% + 4px);left:0;z-index:var(--z-menu);width:252px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);padding:12px;box-sizing:border-box\">\n <div style=\"display:flex;align-items:center;justify-content:space-between;padding-bottom:8px\">\n <IconButton icon=\"chevron-left\" label=\"Previous month\" @click=\"nav(-1)\" />\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--control-text);letter-spacing:var(--control-tracking);color:var(--text-heading)\">{{ monthTitle }}</span>\n <IconButton icon=\"chevron-right\" label=\"Next month\" @click=\"nav(1)\" />\n </div>\n <div style=\"display:grid;grid-template-columns:repeat(7,1fr);gap:2px\">\n <span v-for=\"(d, i) in weekdays\" :key=\"i\" style=\"text-align:center;font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted);padding:2px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ d }}</span>\n <span v-for=\"i in start\" :key=\"'e' + i\" />\n <button v-for=\"i in days\" :key=\"i\" type=\"button\" @click=\"pick(i)\" class=\"kb-dp-day\"\n :style=\"{ height: '28px', borderRadius: 'var(--radius-sm)', border: todayIso === dayIso(i) && modelValue !== dayIso(i) ? '1px solid var(--action-primary)' : '1px solid transparent', cursor: 'pointer', background: modelValue === dayIso(i) ? 'var(--action-primary)' : 'transparent', color: modelValue === dayIso(i) ? 'var(--on-accent)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', letterSpacing: 'var(--control-tracking)', fontVariantNumeric: 'tabular-nums', padding: 0, boxSizing: 'border-box' }\">{{ i }}</button>\n </div>\n </div>\n </div>\n</template>\n<style scoped>.kb-dp-day:hover{background:var(--bg-hover)!important}</style>\n","forms/Dropzone.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst toneColor = { default: \"var(--dropzone-border)\", hover: \"var(--dropzone-border-hover)\", error: \"var(--danger)\", drag: \"var(--focus-border)\" };\n\n/** Dashed upload dropzone (2px, r10). States: default/hover, real drag-and-drop (blue border + tint while a file is dragged over), error, disabled, and a filled \"uploaded\" state showing the file name + remove. */\nexport function Dropzone({ title = \"Click to upload or drag and drop\", hint = \"JSON files only\", height = 185.5, error, disabled, dragOver, file, onRemove, onClick, onDropFiles, style }) {\n const [hover, setHover] = React.useState(false);\n const [dragging, setDragging] = React.useState(false);\n const dragActive = dragOver || dragging;\n if (file) {\n return (\n <div style={{ height, borderRadius: \"var(--radius-lg)\", border: \"1px solid var(--border-control)\", background: \"var(--bg-inset)\", display: \"flex\", flexDirection: \"column\", alignItems: \"center\", justifyContent: \"center\", gap: 8, boxSizing: \"border-box\", ...style }}>\n <span style={{ color: \"var(--success-strong)\" }}><Icon name=\"file-check\" size={22} strokeWidth={1.5} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", color: \"var(--text-heading)\" }}>{file}</span>\n <button type=\"button\" onClick={onRemove} data-link=\"\" style={{ display: \"flex\", alignItems: \"center\", gap: 4, border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--danger)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", padding: 0, textDecorationLine: \"var(--link-line, none)\", textUnderlineOffset: 2 }}>\n <Icon name=\"trash-2\" size={13} />Remove\n </button>\n </div>\n );\n }\n const tone = disabled ? \"default\" : error ? \"error\" : dragActive ? \"drag\" : hover ? \"hover\" : \"default\";\n const stop = (e) => { e.preventDefault(); e.stopPropagation(); };\n return (\n <div onClick={disabled ? undefined : onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n onDragEnter={disabled ? undefined : (e) => { stop(e); setDragging(true); }}\n onDragOver={disabled ? undefined : stop}\n onDragLeave={disabled ? undefined : (e) => { stop(e); setDragging(false); }}\n onDrop={disabled ? undefined : (e) => { stop(e); setDragging(false); onDropFiles && onDropFiles(e.dataTransfer.files); }}\n style={{ height, borderRadius: \"var(--radius-lg)\", border: `1px dashed ${toneColor[tone]}`, background: error ? \"var(--danger-bg)\" : dragActive ? \"var(--soft-bg)\" : \"transparent\", display: \"flex\", flexDirection: \"column\", alignItems: \"center\", justifyContent: \"center\", gap: 4, cursor: disabled ? \"not-allowed\" : \"pointer\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, transition: \"var(--t-hover)\", ...style }}>\n <span style={{ color: error ? \"var(--danger)\" : dragActive ? \"var(--action-primary)\" : \"var(--text-muted)\", marginBottom: 6 }}><Icon name={error ? \"triangle-alert\" : \"upload\"} size={22} strokeWidth={1.5} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: error ? \"var(--danger-text)\" : dragActive ? \"var(--action-primary)\" : \"var(--text-heading)\" }}>{error || (dragActive ? \"Drop file to upload\" : title)}</span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: dragActive ? \"var(--action-primary)\" : \"var(--text-secondary)\" }}>{hint}</span>\n </div>\n );\n}\n","forms/Dropzone.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst toneColor = { default: 'var(--dropzone-border)', hover: 'var(--dropzone-border-hover)', error: 'var(--danger)', drag: 'var(--focus-border)' }\n// Dashed upload dropzone (2px, r10). States: default/hover, real drag-and-drop (blue border + tint while a file is dragged over), error, disabled, and a filled \"uploaded\" state.\nconst props = defineProps({ title: { type: String, default: 'Click to upload or drag and drop' }, hint: { type: String, default: 'JSON files only' }, height: { type: [Number, String], default: 185.5 }, error: String, disabled: Boolean, dragOver: Boolean, file: String, style: Object })\nconst emit = defineEmits(['remove', 'click', 'dropFiles'])\nconst hover = ref(false), dragging = ref(false)\nconst dragActive = computed(() => props.dragOver || dragging.value)\nconst tone = computed(() => props.disabled ? 'default' : props.error ? 'error' : dragActive.value ? 'drag' : hover.value ? 'hover' : 'default')\nfunction stop(e) { e.preventDefault(); e.stopPropagation() }\nfunction onDrop(e) { stop(e); dragging.value = false; emit('dropFiles', e.dataTransfer.files) }\n</script>\n<template>\n <div v-if=\"file\" :style=\"{ height, borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-control)', background: 'var(--bg-inset)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', boxSizing: 'border-box', ...style }\">\n <span style=\"color:var(--success-strong)\"><Icon name=\"file-check\" :size=\"22\" :stroke-width=\"1.5\" /></span>\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);color:var(--text-heading)\">{{ file }}</span>\n <button type=\"button\" data-link=\"\" @click=\"$emit('remove')\" style=\"display:flex;align-items:center;gap:4px;border:none;background:transparent;cursor:pointer;color:var(--danger);font-family:var(--font-sans);font-size:var(--text-base);padding:0;text-decoration-line:var(--link-line, none);text-underline-offset:2px\"><Icon name=\"trash-2\" :size=\"13\" />Remove</button>\n </div>\n <div v-else @click=\"!disabled && $emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n @dragenter=\"!disabled && ((e) => { stop(e); dragging = true })($event)\" @dragover=\"!disabled && stop($event)\"\n @dragleave=\"!disabled && ((e) => { stop(e); dragging = false })($event)\" @drop=\"!disabled && onDrop($event)\"\n :style=\"{ height, borderRadius: 'var(--radius-lg)', border: `1px dashed ${toneColor[tone]}`, background: error ? 'var(--danger-bg)' : dragActive ? 'var(--soft-bg)' : 'transparent', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', boxSizing: 'border-box', opacity: disabled ? .5 : 1, transition: 'var(--t-hover)', ...style }\">\n <span :style=\"{ color: error ? 'var(--danger)' : dragActive ? 'var(--action-primary)' : 'var(--text-muted)', marginBottom: '6px' }\"><Icon :name=\"error ? 'triangle-alert' : 'upload'\" :size=\"22\" :stroke-width=\"1.5\" /></span>\n <span :style=\"{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: error ? 'var(--danger-text)' : dragActive ? 'var(--action-primary)' : 'var(--text-heading)' }\">{{ error || (dragActive ? 'Drop file to upload' : title) }}</span>\n <span :style=\"{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', color: dragActive ? 'var(--action-primary)' : 'var(--text-secondary)' }\">{{ hint }}</span>\n </div>\n</template>\n","forms/Field.jsx":"import React from \"react\";\n\n/** Form field wrapper: label (tier control text, weight 500; red + * when required) above the control, optional hint/error below. `size` sets the tier for the whole field; `inline` puts the label beside the control for filter bars. */\nexport function Field({ label, required, hint, error, size, inline, children, style }) {\n return (\n <div data-size={size} style={{ display: \"flex\", flexDirection: inline ? \"row\" : \"column\", ...(inline ? { alignItems: \"center\", gap: \"var(--control-gap)\" } : null), ...style }}>\n <label style={{ paddingBottom: inline ? 0 : 8, whiteSpace: inline ? \"nowrap\" : undefined, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-body)\" }}>{label}{required ? <span style={{ color: \"var(--danger)\" }}> *</span> : null}</label>\n {children}\n {error ? <span style={{ paddingTop: 5, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--danger)\" }}>{error}</span>\n : hint ? <span style={{ paddingTop: 5, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-muted)\" }}>{hint}</span> : null}\n </div>\n );\n}\n","forms/Field.vue":"<script setup>\n// Form field wrapper: label (tier control text, weight 500; red + * when required) above the control, optional hint/error below. `size` sets the tier for the whole field; `inline` puts the label beside the control for filter bars.\ndefineProps({ label: String, required: Boolean, hint: String, error: String, size: String, inline: Boolean, style: Object })\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ display: 'flex', flexDirection: inline ? 'row' : 'column', ...(inline ? { alignItems: 'center', gap: 'var(--control-gap)' } : null), ...style }\">\n <label :style=\"{ paddingBottom: inline ? 0 : '8px', whiteSpace: inline ? 'nowrap' : undefined, fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-body)' }\">{{ label }}<span v-if=\"required\" style=\"color:var(--danger)\"> *</span></label>\n <slot />\n <span v-if=\"error\" style=\"padding-top:5px;font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--danger)\">{{ error }}</span>\n <span v-else-if=\"hint\" style=\"padding-top:5px;font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-muted)\">{{ hint }}</span>\n </div>\n</template>\n","forms/Input.jsx":"import React from \"react\";\n\nimport { inputBase } from \"./input-base.js\";\nexport { inputBase };\n\n/** Text input, r8, tier height (sm 28 · md 32 · lg 40 — omit `size` to inherit the region's tier). readOnly = inset fill; disabled = 50% opacity; error = red border + ring. */\nexport function Input({ readOnly, disabled, error, size, style, ...rest }) {\n const [focus, setFocus] = React.useState(false);\n return <input readOnly={readOnly} disabled={disabled} data-size={size} data-field={readOnly || disabled ? undefined : \"\"} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}\n style={{ ...inputBase, ...(readOnly ? { background: \"var(--bg-inset)\", color: \"var(--text-secondary)\" } : null),\n ...(error ? { borderColor: \"var(--danger)\" } : null),\n ...(focus && !readOnly && !disabled ? { borderColor: error ? \"var(--danger)\" : \"var(--focus-border)\", boxShadow: `0 0 0 2px ${error ? \"var(--danger-bg)\" : \"var(--focus-ring)\"}` } : null),\n ...(disabled ? { opacity: .5, cursor: \"not-allowed\", background: \"var(--bg-inset)\" } : null), ...style }} {...rest} />;\n}\n\n/** Multiline textarea, r8, default 90px tall; text follows the region's tier. showCount + maxLength renders a bottom-right \"n/max\" counter (red past the limit). */\nexport function Textarea({ style, rows = 4, error, disabled, showCount, maxLength, size, value, defaultValue, onChange, ...rest }) {\n const [focus, setFocus] = React.useState(false);\n const [inner, setInner] = React.useState(defaultValue || \"\");\n const val = value != null ? value : inner;\n const len = (val || \"\").length;\n const over = maxLength != null && len > maxLength;\n return (\n <div style={{ position: \"relative\" }}>\n <textarea rows={rows} disabled={disabled} data-size={size} data-field={disabled ? undefined : \"\"} maxLength={showCount ? undefined : maxLength}\n value={value} defaultValue={value == null ? defaultValue : undefined}\n onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}\n onChange={(e) => { if (value == null) setInner(e.target.value); onChange && onChange(e); }}\n style={{ ...inputBase, height: 90, resize: \"vertical\", paddingTop: 8, paddingBottom: showCount ? 22 : 8,\n ...(error || over ? { borderColor: \"var(--danger)\" } : null),\n ...(focus && !disabled ? { borderColor: error || over ? \"var(--danger)\" : \"var(--focus-border)\", boxShadow: `0 0 0 2px ${error || over ? \"var(--danger-bg)\" : \"var(--focus-ring)\"}` } : null),\n ...(disabled ? { opacity: .5, cursor: \"not-allowed\", background: \"var(--bg-inset)\" } : null), ...style }} {...rest} />\n {showCount ? <span style={{ position: \"absolute\", right: 10, bottom: 7, fontFamily: \"var(--font-sans)\", fontVariantNumeric: \"tabular-nums\", fontSize: \"var(--text-xs)\", color: over ? \"var(--danger)\" : \"var(--text-muted)\" }}>{len}{maxLength != null ? `/${maxLength}` : \"\"}</span> : null}\n </div>\n );\n}\n","forms/Input.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { inputBase } from './input-base.js'\n// Text input, r8, tier height (sm 28 · md 32 · lg 40 — omit `size` to inherit the region's tier). v-model. readonly = inset fill; disabled = 50% opacity; error = red border + ring.\n// Multiline variant lives in Textarea.vue.\nconst props = defineProps({ modelValue: String, readonly: Boolean, disabled: Boolean, error: Boolean, size: String, style: Object })\ndefineEmits(['update:modelValue'])\nconst focus = ref(false)\nconst s = computed(() => ({ ...inputBase, ...(props.readonly ? { background: 'var(--bg-inset)', color: 'var(--text-secondary)' } : null),\n ...(props.error ? { borderColor: 'var(--danger)' } : null),\n ...(focus.value && !props.readonly && !props.disabled ? { borderColor: props.error ? 'var(--danger)' : 'var(--focus-border)', boxShadow: `0 0 0 2px ${props.error ? 'var(--danger-bg)' : 'var(--focus-ring)'}` } : null),\n ...(props.disabled ? { opacity: .5, cursor: 'not-allowed', background: 'var(--bg-inset)' } : null), ...props.style }))\n</script>\n<template>\n <input :value=\"modelValue\" @input=\"$emit('update:modelValue', $event.target.value)\" :readonly=\"readonly\" :disabled=\"disabled\" :data-size=\"size\" :data-field=\"readonly || disabled ? undefined : ''\" @focus=\"focus = true\" @blur=\"focus = false\" :style=\"s\" />\n</template>\n","forms/MultiSelect.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { Checkbox } from \"./Checkbox.jsx\";\nimport { Tag } from \"../display/Tag.jsx\";\n\nconst tierIcon = { width: \"var(--control-icon)\", height: \"var(--control-icon)\" };\n\n/** Multi-select dropdown: checkbox option list, selected values render as removable tags in the field (overflow collapses to \"+n\"), clear-all ×. Field, tags and option rows take the region's tier (omit `size` to inherit). */\nexport function MultiSelect({ options = [], value = [], placeholder = \"Select...\", maxTags = 2, disabled, width, size, onChange, style }) {\n const [open, setOpen] = React.useState(false);\n const root = React.useRef(null);\n React.useEffect(() => { const h = (e) => { if (root.current && !root.current.contains(e.target)) setOpen(false); }; document.addEventListener(\"mousedown\", h); return () => document.removeEventListener(\"mousedown\", h); }, []);\n const toggle = (o) => { const next = value.includes(o) ? value.filter(x => x !== o) : [...value, o]; onChange && onChange(next); };\n const shown = value.slice(0, maxTags), extra = value.length - shown.length;\n return (\n <div ref={root} data-size={size} style={{ position: \"relative\", width, ...style }}>\n <div data-field={disabled ? undefined : \"\"} onClick={() => !disabled && setOpen(!open)} style={{ minHeight: \"var(--control-h)\", borderRadius: \"var(--radius-md)\", border: `1px solid ${open ? \"var(--focus-border)\" : \"var(--field-border, var(--border-control))\"}`, boxShadow: open ? \"0 0 0 2px var(--focus-ring)\" : \"none\", background: \"var(--bg-surface)\", padding: \"calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)\", boxSizing: \"border-box\", display: \"flex\", alignItems: \"center\", flexWrap: \"wrap\", gap: 4, cursor: disabled ? \"not-allowed\" : \"pointer\", opacity: disabled ? .5 : 1, position: \"relative\" }}>\n {value.length === 0 && <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-muted)\", paddingLeft: 4 }}>{placeholder}</span>}\n {shown.map(v => <Tag key={v} removable onRemove={() => toggle(v)}>{v}</Tag>)}\n {extra > 0 && <Tag>+{extra}</Tag>}\n <span style={{ position: \"absolute\", right: \"var(--control-px)\", top: \"50%\", transform: \"translateY(-50%)\", display: \"flex\", alignItems: \"center\", gap: 4, color: \"var(--text-muted)\" }}>\n {value.length > 0 && <button type=\"button\" aria-label=\"Clear\" data-ink=\"\" onClick={(e) => { e.stopPropagation(); onChange && onChange([]); }} style={{ border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--ink, var(--text-muted))\", display: \"flex\", padding: 0 }}><Icon name=\"x\" size={16} style={tierIcon} /></button>}\n <span style={{ display: \"inline-flex\", transform: open ? \"rotate(180deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}><Icon name=\"chevron-down\" size={16} style={tierIcon} /></span>\n </span>\n </div>\n {open && <div style={{ position: \"absolute\", top: \"calc(100% + 4px)\", left: 0, right: 0, zIndex: \"var(--z-menu)\", background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: 4, maxHeight: 208, overflowY: \"auto\" }}>\n {options.map(o => <div key={o} role=\"option\" aria-selected={value.includes(o)} tabIndex={0} onClick={() => toggle(o)} style={{ display: \"flex\", alignItems: \"center\", gap: 8, height: \"var(--control-h)\", padding: \"0 10px\", boxSizing: \"border-box\", borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", background: \"transparent\" }}\n onMouseEnter={(e) => e.currentTarget.style.background = \"var(--bg-hover)\"} onMouseLeave={(e) => e.currentTarget.style.background = \"transparent\"}>\n <Checkbox checked={value.includes(o)} onChange={() => toggle(o)} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-heading)\" }}>{o}</span>\n </div>)}\n </div>}\n </div>\n );\n}\n","forms/MultiSelect.vue":"<script setup>\nimport { ref, computed, onMounted, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport Checkbox from './Checkbox.vue'\nimport Tag from '../display/Tag.vue'\n// Multi-select dropdown: checkbox options; picks render as removable tags with \"+n\" overflow and clear-all ×. Field, tags and option rows take the region's tier (omit `size` to inherit).\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: Array, default: () => [] }, placeholder: { type: String, default: 'Select...' }, maxTags: { type: Number, default: 2 }, disabled: Boolean, width: [Number, String], size: String, style: Object })\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst emit = defineEmits(['update:modelValue'])\nconst open = ref(false), root = ref(null)\nfunction toggle(o) { emit('update:modelValue', props.modelValue.includes(o) ? props.modelValue.filter(x => x !== o) : [...props.modelValue, o]) }\nconst shown = computed(() => props.modelValue.slice(0, props.maxTags))\nconst extra = computed(() => props.modelValue.length - shown.value.length)\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }\nonMounted(() => document.addEventListener('mousedown', onDoc))\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\n</script>\n<template>\n <div ref=\"root\" :data-size=\"size\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div :data-field=\"disabled ? undefined : ''\" @click=\"!disabled && (open = !open)\" :style=\"{ minHeight: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: `1px solid ${open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))'}`, boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', background: 'var(--bg-surface)', padding: 'calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)', boxSizing: 'border-box', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, position: 'relative' }\">\n <span v-if=\"modelValue.length === 0\" style=\"font-family:var(--font-sans);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-muted);padding-left:4px\">{{ placeholder }}</span>\n <Tag v-for=\"v in shown\" :key=\"v\" removable @remove=\"toggle(v)\">{{ v }}</Tag>\n <Tag v-if=\"extra > 0\">+{{ extra }}</Tag>\n <span style=\"position:absolute;right:var(--control-px);top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:4px;color:var(--text-muted)\">\n <button v-if=\"modelValue.length\" type=\"button\" aria-label=\"Clear\" data-ink=\"\" @click.stop=\"emit('update:modelValue', [])\" style=\"border:none;background:transparent;cursor:pointer;color:var(--ink, var(--text-muted));display:flex;padding:0\"><Icon name=\"x\" :size=\"16\" :style=\"tierIcon\" /></button>\n <span :style=\"{ display: 'inline-flex', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\"><Icon name=\"chevron-down\" :size=\"16\" :style=\"tierIcon\" /></span>\n </span>\n </div>\n <div v-if=\"open\" style=\"position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:var(--z-menu);background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;max-height:208px;overflow-y:auto\">\n <div v-for=\"o in options\" :key=\"o\" role=\"option\" :aria-selected=\"modelValue.includes(o)\" tabindex=\"0\" @click=\"toggle(o)\" class=\"kb-ms-row\" style=\"display:flex;align-items:center;gap:8px;height:var(--control-h);padding:0 10px;box-sizing:border-box;border-radius:var(--radius-sm);cursor:pointer\">\n <Checkbox :checked=\"modelValue.includes(o)\" @change=\"toggle(o)\" />\n <span style=\"font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);color:var(--text-heading)\">{{ o }}</span>\n </div>\n </div>\n </div>\n</template>\n<style scoped>.kb-ms-row:hover{background:var(--bg-hover)}</style>\n","forms/NumberStepper.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Segmented − / value / + stepper, r8, tier height (\"Preview rows\" — omit `size` to inherit the region's tier). Buttons disable themselves at min/max. */\nexport function NumberStepper({ value = 100, onChange, step = 10, min = 0, max, disabled, size, style }) {\n const atMin = value <= min, atMax = max != null && value >= max;\n const seg = (dis) => ({ width: \"var(--control-h)\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", cursor: dis ? \"not-allowed\" : \"pointer\", color: dis ? \"var(--text-muted)\" : \"var(--text-body)\", alignSelf: \"stretch\", background: dis ? \"transparent\" : \"var(--wash, transparent)\", transition: \"var(--t-hover)\", border: \"none\", padding: 0 });\n return (\n <div data-size={size} data-field={disabled ? undefined : \"\"} style={{ display: \"inline-flex\", alignItems: \"center\", height: \"var(--control-h)\", borderRadius: \"var(--radius-md)\", border: \"1px solid var(--field-border, var(--border-control))\", overflow: \"hidden\", background: \"var(--bg-surface)\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, ...style }}>\n <button type=\"button\" disabled={disabled || atMin} data-wash=\"\" aria-label=\"Decrease\" style={seg(disabled || atMin)} onClick={() => onChange && onChange(Math.max(min, value - step))}><Icon name=\"minus\" size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /></button>\n <span style={{ minWidth: 34, textAlign: \"center\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", fontVariantNumeric: \"tabular-nums\", color: \"var(--text-primary)\", borderLeft: \"1px solid var(--border-control)\", borderRight: \"1px solid var(--border-control)\", alignSelf: \"stretch\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", padding: \"0 6px\" }}>{value}</span>\n <button type=\"button\" disabled={disabled || atMax} data-wash=\"\" aria-label=\"Increase\" style={seg(disabled || atMax)} onClick={() => onChange && onChange(max != null ? Math.min(max, value + step) : value + step)}><Icon name=\"plus\" size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /></button>\n </div>\n );\n}\n","forms/NumberStepper.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Segmented − / value / + stepper, r8, tier height (\"Preview rows\" — omit `size` to inherit the region's tier). Buttons disable themselves at min/max.\nconst props = defineProps({ modelValue: { type: Number, default: 100 }, step: { type: Number, default: 10 }, min: { type: Number, default: 0 }, max: Number, disabled: Boolean, size: String, style: Object })\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst emit = defineEmits(['update:modelValue'])\nconst atMin = computed(() => props.modelValue <= props.min)\nconst atMax = computed(() => props.max != null && props.modelValue >= props.max)\nfunction seg(dis) { return { width: 'var(--control-h)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: dis ? 'not-allowed' : 'pointer', color: dis ? 'var(--text-muted)' : 'var(--text-body)', alignSelf: 'stretch', background: dis ? 'transparent' : 'var(--wash, transparent)', transition: 'var(--t-hover)', border: 'none', padding: 0 } }\n</script>\n<template>\n <div :data-size=\"size\" :data-field=\"disabled ? undefined : ''\" :style=\"{ display: 'inline-flex', alignItems: 'center', height: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: '1px solid var(--field-border, var(--border-control))', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }\">\n <button type=\"button\" :disabled=\"disabled || atMin\" data-wash=\"\" aria-label=\"Decrease\" :style=\"seg(disabled || atMin)\" @click=\"emit('update:modelValue', Math.max(min, modelValue - step))\"><Icon name=\"minus\" :size=\"16\" :style=\"tierIcon\" /></button>\n <span style=\"min-width:34px;text-align:center;font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);font-variant-numeric:tabular-nums;color:var(--text-primary);border-left:1px solid var(--border-control);border-right:1px solid var(--border-control);align-self:stretch;display:inline-flex;align-items:center;justify-content:center;padding:0 6px\">{{ modelValue }}</span>\n <button type=\"button\" :disabled=\"disabled || atMax\" data-wash=\"\" aria-label=\"Increase\" :style=\"seg(disabled || atMax)\" @click=\"emit('update:modelValue', max != null ? Math.min(max, modelValue + step) : modelValue + step)\"><Icon name=\"plus\" :size=\"16\" :style=\"tierIcon\" /></button>\n </div>\n</template>\n","forms/Radio.vue":"<script setup>\nimport { ref, computed } from 'vue'\n// Radio — circle at the region's tier (sm 14 · md/lg 16, --check-size), 1.5px border, filled dot when selected. Hover darkens the ring, focus shows the 2px ring, disabled = 50% opacity.\n// description renders a muted second line under the label.\nconst props = defineProps({ checked: Boolean, label: String, description: String, size: String, disabled: Boolean, style: Object })\nconst emit = defineEmits(['change'])\nconst d = { box: 'var(--check-size)', dot: 'calc(var(--check-size) / 2)' }\nconst hover = ref(false), focus = ref(false)\nconst ring = computed(() => props.checked ? 'var(--action-primary)' : hover.value && !props.disabled ? 'var(--text-muted)' : 'var(--checkbox-border)')\nconst pick = () => { if (!props.disabled) emit('change', true) }\n</script>\n<template>\n <label role=\"radio\" :data-size=\"size\" :aria-checked=\"checked\" :aria-disabled=\"disabled || undefined\" :tabindex=\"disabled ? -1 : 0\"\n @mouseenter=\"hover = true\" @mouseleave=\"hover = false\" @focus=\"focus = true\" @blur=\"focus = false\" @click=\"pick\" @keydown.space.prevent=\"pick\" @keydown.enter.prevent=\"pick\"\n :style=\"{ display: 'inline-flex', alignItems: description ? 'flex-start' : 'center', verticalAlign: 'middle', lineHeight: 'var(--control-leading)', gap: 'var(--control-gap)', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, outline: 'none', ...style }\">\n <span :style=\"{ width: d.box, height: d.box, marginTop: description ? '2px' : 0, borderRadius: 'var(--radius-full)', boxSizing: 'border-box', border: `1.5px solid ${ring}`, background: 'var(--bg-surface)', boxShadow: focus && !disabled ? '0 0 0 2px var(--focus-ring)' : 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, transition: 'var(--t-hover)' }\">\n <span v-if=\"checked\" :style=\"{ width: d.dot, height: d.dot, borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }\"></span>\n </span>\n <span v-if=\"label || description\" style=\"display:flex;flex-direction:column;gap:2px\">\n <span v-if=\"label\" style=\"font-family:var(--font-sans);font-weight:var(--weight-regular);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-body)\">{{ label }}</span>\n <span v-if=\"description\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-muted)\">{{ description }}</span>\n </span>\n </label>\n</template>\n","forms/RadioGroup.vue":"<script setup>\nimport { computed } from 'vue'\nimport Radio from './Radio.vue'\n// RadioGroup — single choice. Vertical (default) or `row`. options: string[] | { label, value, description?, disabled? }[].\n// Arrow keys move between enabled options. variant=\"cards\" renders bordered selectable cards. v-model.\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, row: Boolean, size: String, variant: { type: String, default: 'plain' }, disabled: Boolean, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst opts = computed(() => props.options.map(o => typeof o === 'string' ? { label: o, value: o } : o))\nconst enabled = computed(() => opts.value.filter(o => !o.disabled && !props.disabled))\nfunction onKey(e) {\n if (!['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft'].includes(e.key) || !enabled.value.length) return\n e.preventDefault()\n const i = enabled.value.findIndex(o => o.value === props.modelValue), n = enabled.value.length\n const next = (e.key === 'ArrowDown' || e.key === 'ArrowRight') ? enabled.value[(i + 1) % n] : enabled.value[(i - 1 + n) % n]\n emit('update:modelValue', next.value)\n}\nconst pick = (o) => { if (!(o.disabled || props.disabled)) emit('update:modelValue', o.value) }\n</script>\n<template>\n <div v-if=\"variant === 'cards'\" role=\"radiogroup\" :data-size=\"size\" @keydown=\"onKey\" :style=\"{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: '8px', ...style }\">\n <div v-for=\"o in opts\" :key=\"o.value\" @click=\"pick(o)\"\n :style=\"{ flex: row ? 1 : 'none', padding: '10px 12px', borderRadius: 'var(--radius-md)', border: `1px solid ${modelValue === o.value ? 'var(--action-primary)' : 'var(--border-card)'}`, background: modelValue === o.value ? 'var(--soft-bg)' : 'var(--bg-surface)', cursor: (o.disabled || disabled) ? 'not-allowed' : 'pointer', opacity: (o.disabled || disabled) ? .5 : 1, transition: 'var(--t-hover)' }\">\n <Radio :checked=\"modelValue === o.value\" :label=\"o.label\" :description=\"o.description\" :disabled=\"o.disabled || disabled\" :style=\"{ opacity: 1, cursor: 'inherit' }\" @change=\"pick(o)\" />\n </div>\n </div>\n <div v-else role=\"radiogroup\" :data-size=\"size\" @keydown=\"onKey\" :style=\"{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: row ? 'var(--group-gap)' : '10px', ...style }\">\n <Radio v-for=\"o in opts\" :key=\"o.value\" :label=\"o.label\" :description=\"o.description\" :disabled=\"o.disabled || disabled\" :checked=\"modelValue === o.value\" @change=\"pick(o)\" />\n </div>\n</template>\n","forms/SearchInput.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Bordered search field (r8, tier height — omit `size` to inherit the region's tier) with leading search glyph; bare=true renders the borderless top-bar flavor. Shows a clear (×) button once there's a value. */\nexport function SearchInput({ placeholder = \"Search...\", bare, size, value, onChange, disabled, style }) {\n const [inner, setInner] = React.useState(\"\");\n const val = value != null ? value : inner;\n const set = (v) => { if (value == null) setInner(v); onChange && onChange(v); };\n return (\n <div data-size={size} data-field={bare || disabled ? undefined : \"\"} style={{ display: \"flex\", alignItems: \"center\", gap: bare ? 12 : 8, height: \"var(--control-h)\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, ...(bare ? null : { border: \"1px solid var(--field-border, var(--border-control))\", borderRadius: \"var(--radius-md)\", padding: \"0 var(--control-px)\", background: \"var(--bg-surface)\" }), ...style }}>\n <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\" }}><Icon name=\"search\" size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /></span>\n <input value={val} disabled={disabled} onChange={(e) => set(e.target.value)} placeholder={placeholder}\n style={{ border: \"none\", outline: \"none\", background: \"transparent\", flex: 1, minWidth: 0, padding: 0, fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-primary)\", cursor: disabled ? \"not-allowed\" : \"text\" }} />\n {val && !disabled ? <button type=\"button\" onClick={() => set(\"\")} aria-label=\"Clear\" data-ink=\"\" style={{ border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--ink, var(--text-muted))\", display: \"flex\", padding: 0 }}><Icon name=\"x\" size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /></button> : null}\n </div>\n );\n}\n","forms/SearchInput.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Bordered search field (r8, tier height — omit `size` to inherit the region's tier) with leading search glyph; bare=true renders the borderless top-bar flavor. Shows a clear (×) button once there's a value.\ndefineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Search...' }, bare: Boolean, size: String, disabled: Boolean, style: Object })\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :data-size=\"size\" :data-field=\"bare || disabled ? undefined : ''\" :style=\"{ display: 'flex', alignItems: 'center', gap: bare ? '12px' : '8px', height: 'var(--control-h)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...(bare ? null : { border: '1px solid var(--field-border, var(--border-control))', borderRadius: 'var(--radius-md)', padding: '0 var(--control-px)', background: 'var(--bg-surface)' }), ...style }\">\n <span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"search\" :size=\"16\" :style=\"tierIcon\" /></span>\n <input :value=\"modelValue\" :disabled=\"disabled\" @input=\"emit('update:modelValue', $event.target.value)\" :placeholder=\"placeholder\"\n :style=\"{ border: 'none', outline: 'none', background: 'transparent', flex: 1, minWidth: 0, padding: 0, fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-primary)', cursor: disabled ? 'not-allowed' : 'text' }\" />\n <button v-if=\"modelValue && !disabled\" type=\"button\" aria-label=\"Clear\" data-ink=\"\" @click=\"emit('update:modelValue', '')\" style=\"border:none;background:transparent;cursor:pointer;color:var(--ink, var(--text-muted));display:flex;padding:0\"><Icon name=\"x\" :size=\"16\" :style=\"tierIcon\" /></button>\n </div>\n</template>\n","forms/Select.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Native select styled per Keybricks: r8, tier height (sm 28 · md 32 · lg 40 — omit `size` to inherit the region's tier). disabled = inset fill (matches Input readOnly), 50% opacity; error = red border. */\nexport function Select({ options = [], size, value, onChange, placeholder, disabled, error, style }) {\n return (\n <div data-size={size} style={{ position: \"relative\", display: \"inline-flex\", ...style }}>\n <select value={value} onChange={onChange} disabled={disabled} data-field={disabled ? undefined : \"\"}\n style={{ appearance: \"none\", WebkitAppearance: \"none\", height: \"var(--control-h)\", borderRadius: \"var(--radius-md)\", border: `1px solid ${error ? \"var(--danger)\" : \"var(--field-border, var(--border-control))\"}`, background: disabled ? \"var(--bg-inset)\" : \"var(--bg-surface)\", padding: \"0 calc(var(--control-px) + var(--control-icon) + 6px) 0 var(--control-px)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", color: disabled ? \"var(--text-secondary)\" : value ? \"var(--text-primary)\" : \"var(--text-muted)\", outline: \"none\", cursor: disabled ? \"not-allowed\" : \"pointer\", width: \"100%\", opacity: disabled ? .5 : 1 }}>\n {placeholder ? <option value=\"\" disabled hidden>{placeholder}</option> : null}\n {options.map((o) => <option key={o} value={o}>{o}</option>)}\n </select>\n <span style={{ position: \"absolute\", right: \"var(--control-px)\", top: \"50%\", transform: \"translateY(-50%)\", pointerEvents: \"none\", color: \"var(--text-muted)\", display: \"inline-flex\" }}><Icon name=\"chevron-down\" size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /></span>\n </div>\n );\n}\n","forms/Select.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Native select styled per Keybricks: r8, tier height (sm 28 · md 32 · lg 40 — omit `size` to inherit the region's tier). disabled = inset fill (matches Input readonly), 50% opacity; error = red border.\nconst props = defineProps({ modelValue: String, options: { type: Array, default: () => [] }, size: String, placeholder: String, disabled: Boolean, error: Boolean, style: Object })\ndefineEmits(['update:modelValue'])\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ position: 'relative', display: 'inline-flex', ...style }\">\n <select :value=\"modelValue\" @change=\"$emit('update:modelValue', $event.target.value)\" :disabled=\"disabled\" :data-field=\"disabled ? undefined : ''\"\n :style=\"{ appearance: 'none', height: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: `1px solid ${error ? 'var(--danger)' : 'var(--field-border, var(--border-control))'}`, background: disabled ? 'var(--bg-inset)' : 'var(--bg-surface)', padding: '0 calc(var(--control-px) + var(--control-icon) + 6px) 0 var(--control-px)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', letterSpacing: 'var(--control-tracking)', color: disabled ? 'var(--text-secondary)' : modelValue ? 'var(--text-primary)' : 'var(--text-muted)', outline: 'none', cursor: disabled ? 'not-allowed' : 'pointer', width: '100%', opacity: disabled ? .5 : 1 }\">\n <option v-if=\"placeholder\" value=\"\" disabled hidden>{{ placeholder }}</option>\n <option v-for=\"o in options\" :key=\"o\" :value=\"o\">{{ o }}</option>\n </select>\n <span :style=\"{ position: 'absolute', right: 'var(--control-px)', top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none', color: 'var(--text-muted)', display: 'inline-flex' }\"><Icon name=\"chevron-down\" :size=\"16\" :style=\"{ width: 'var(--control-icon)', height: 'var(--control-icon)' }\" /></span>\n </div>\n</template>\n","forms/Slider.jsx":"import React from \"react\";\n\n/** Range slider: blue filled track, 16px knob, optional min/max labels and a value bubble while dragging (or always with showValue). */\nexport function Slider({ value = 50, min = 0, max = 100, step = 1, labels, showValue, unit = \"\", disabled, width, onChange, style }) {\n const [drag, setDrag] = React.useState(false);\n const pct = ((value - min) / (max - min)) * 100;\n const track = React.useRef(null);\n const set = (clientX) => { const r = track.current.getBoundingClientRect(); const p = Math.min(1, Math.max(0, (clientX - r.left) / r.width)); const raw = min + p * (max - min); const snapped = Math.round(raw / step) * step; onChange && onChange(Math.min(max, Math.max(min, snapped))); };\n React.useEffect(() => { if (!drag) return; const mv = (e) => set(e.clientX), up = () => setDrag(false); document.addEventListener(\"mousemove\", mv); document.addEventListener(\"mouseup\", up); return () => { document.removeEventListener(\"mousemove\", mv); document.removeEventListener(\"mouseup\", up); }; }, [drag]);\n return (\n <div style={{ width, opacity: disabled ? .5 : 1, ...style }}>\n <div ref={track} data-thumb={disabled ? undefined : \"\"} onMouseDown={disabled ? undefined : (e) => { setDrag(true); set(e.clientX); }} style={{ position: \"relative\", height: 20, display: \"flex\", alignItems: \"center\", cursor: disabled ? \"not-allowed\" : \"pointer\" }}>\n <div style={{ position: \"absolute\", left: 0, right: 0, height: 6, borderRadius: \"var(--radius-full)\", background: \"var(--track)\" }}></div>\n <div style={{ position: \"absolute\", left: 0, width: pct + \"%\", height: 6, borderRadius: \"var(--radius-full)\", background: \"var(--action-primary)\" }}></div>\n <span style={{ position: \"absolute\", left: pct + \"%\", transform: \"translateX(-50%)\", width: 16, height: 16, borderRadius: \"var(--radius-full)\", background: \"var(--bg-surface)\", border: \"2px solid var(--action-primary)\", boxSizing: \"border-box\", boxShadow: drag ? \"0 0 0 4px var(--focus-ring), 0 1px 2px rgba(0,0,0,.15)\" : \"var(--thumb-ring, 0 1px 2px rgba(0,0,0,.15))\", transition: \"box-shadow var(--dur-fast) var(--ease-out)\" }}></span>\n {(drag || showValue) && <span style={{ position: \"absolute\", left: pct + \"%\", transform: \"translateX(-50%)\", bottom: 22, background: \"var(--tooltip-bg)\", color: \"var(--tooltip-fg)\", borderRadius: \"var(--radius-sm)\", padding: \"2px 6px\", fontFamily: \"var(--font-sans)\", fontVariantNumeric: \"tabular-nums\", fontSize: \"var(--text-xs)\", whiteSpace: \"nowrap\", pointerEvents: \"none\" }}>{value}{unit}</span>}\n </div>\n {labels && <div style={{ display: \"flex\", justifyContent: \"space-between\", paddingTop: 4, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-muted)\" }}><span>{min}{unit}</span><span>{max}{unit}</span></div>}\n </div>\n );\n}\n","forms/Slider.vue":"<script setup>\nimport { ref, computed, onUnmounted, watch } from 'vue'\n// Range slider: blue filled track, 16px knob, drag value bubble, optional min/max labels.\nconst props = defineProps({ modelValue: { type: Number, default: 50 }, min: { type: Number, default: 0 }, max: { type: Number, default: 100 }, step: { type: Number, default: 1 }, labels: Boolean, showValue: Boolean, unit: { type: String, default: '' }, disabled: Boolean, width: [Number, String], style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst drag = ref(false), track = ref(null)\nconst pct = computed(() => ((props.modelValue - props.min) / (props.max - props.min)) * 100)\nfunction set(clientX) { const r = track.value.getBoundingClientRect(); const p = Math.min(1, Math.max(0, (clientX - r.left) / r.width)); const raw = props.min + p * (props.max - props.min); emit('update:modelValue', Math.min(props.max, Math.max(props.min, Math.round(raw / props.step) * props.step))) }\nconst mv = (e) => set(e.clientX), up = () => drag.value = false\nwatch(drag, (on) => { if (on) { document.addEventListener('mousemove', mv); document.addEventListener('mouseup', up) } else { document.removeEventListener('mousemove', mv); document.removeEventListener('mouseup', up) } })\nonUnmounted(up)\n</script>\n<template>\n <div :style=\"{ width: typeof width === 'number' ? width + 'px' : width, opacity: disabled ? .5 : 1, ...style }\">\n <div ref=\"track\" :data-thumb=\"disabled ? undefined : ''\" @mousedown=\"!disabled && (drag = true, set($event.clientX))\" :style=\"{ position: 'relative', height: '20px', display: 'flex', alignItems: 'center', cursor: disabled ? 'not-allowed' : 'pointer' }\">\n <div style=\"position:absolute;left:0;right:0;height:6px;border-radius:var(--radius-full);background:var(--track)\" />\n <div :style=\"{ position: 'absolute', left: 0, width: pct + '%', height: '6px', borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }\" />\n <span :style=\"{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', width: '16px', height: '16px', borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', border: '2px solid var(--action-primary)', boxSizing: 'border-box', boxShadow: drag ? '0 0 0 4px var(--focus-ring), 0 1px 2px rgba(0,0,0,.15)' : 'var(--thumb-ring, 0 1px 2px rgba(0,0,0,.15))', transition: 'box-shadow var(--dur-fast) var(--ease-out)' }\" />\n <span v-if=\"drag || showValue\" :style=\"{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', bottom: '22px', background: 'var(--tooltip-bg)', color: 'var(--tooltip-fg)', borderRadius: 'var(--radius-sm)', padding: '2px 6px', fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums', fontSize: 'var(--text-xs)', whiteSpace: 'nowrap', pointerEvents: 'none' }\">{{ modelValue }}{{ unit }}</span>\n </div>\n <div v-if=\"labels\" style=\"display:flex;justify-content:space-between;padding-top:4px;font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-muted)\"><span>{{ min }}{{ unit }}</span><span>{{ max }}{{ unit }}</span></div>\n </div>\n</template>\n","forms/Switch.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/**\n * Switch — pill track sized by the region's tier (sm 28×16 · md 36×20 · lg 44×24), knob 4px smaller, 160ms ease-out slide.\n * Track, knob, glyph and label all read the tier tokens (tokens/layout.css), so a Switch in a form row matches its neighbours.\n *\n * checked / onChange(next) controlled boolean\n * disabled 50% opacity, no pointer\n * size \"sm\" | \"md\" | \"lg\" — omit to inherit the region's tier; lg is roomy enough for knobIcon / knobText\n * tone \"primary\" (default, action blue) | \"semantic\" (red off → green on)\n * onColor / offColor explicit track colors; override tone\n * knobIcon / knobText glyph inside the knob: string | { on, off }. knobText is ONE letter or ONE CJK character.\n * insideOn / insideOff text inside the pill on the far side of the knob (short — \"ON\"/\"OFF\", \"开\"/\"关\", \"Y\"/\"N\")\n * labelOn / labelOff captions beside the pill; active caption takes text-heading, inactive takes text-muted\n * loading controlled: knob shows a spinner, input is inert\n * beforeChange(next) => Promise|boolean\n * async gate — Switch enters loading, flips only if the promise resolves truthy\n * inlinePrompt (Vue-friendly alias of insideOn/insideOff pair {on,off})\n */\n\n/* Geometry in CSS so it follows the tier without JavaScript: knob = track height − 4, glyph = half the track height. */\nconst H = \"var(--switch-h)\", W = \"var(--switch-w)\", KNOB = \"calc(var(--switch-h) - 4px)\", GLYPH = \"calc(var(--switch-h) * .5)\", TEXT = \"max(calc(var(--switch-h) * .5), var(--text-glyph-min))\" /* text never drops below the glyph floor (12px in CJK) */;\n\nconst pick = (v, on) => (v && typeof v === \"object\" ? (on ? v.on : v.off) : v);\n\nexport function Switch({ checked = false, onChange, disabled, size, tone = \"primary\", onColor, offColor, knobIcon, knobText, insideOn, insideOff, inlinePrompt, label, labelOn, labelOff, loading: loadingProp, beforeChange, style }) {\n // `label` = single caption to the right, shown in text-body regardless of state (settings-row usage)\n const single = label != null && labelOn == null && labelOff == null;\n const [busy, setBusy] = React.useState(false);\n const alive = React.useRef(true);\n React.useEffect(() => () => { alive.current = false; }, []);\n const loading = loadingProp || busy;\n if (inlinePrompt) { insideOn = insideOn ?? inlinePrompt.on; insideOff = insideOff ?? inlinePrompt.off; }\n const hasInside = insideOn != null || insideOff != null;\n // inside text needs a wider track so the knob never covers the word: text runs at half the track height, ~.68em per character\n const insideChars = hasInside ? Math.max(String(insideOn ?? \"\").length, String(insideOff ?? \"\").length) : 0;\n const w = hasInside ? `max(${W}, calc(${H} * ${(1 + insideChars * .34).toFixed(2)} + 6px))` : W;\n\n const onTrack = onColor || (tone === \"semantic\" ? \"var(--success)\" : \"var(--action-primary)\");\n const offTrack = offColor || (tone === \"semantic\" ? \"var(--danger)\" : \"var(--border-strong)\");\n const track = checked ? onTrack : offTrack;\n const inert = disabled || loading;\n\n const flip = async () => {\n if (inert) return;\n const next = !checked;\n if (beforeChange) {\n setBusy(true);\n let ok = false;\n try { ok = await beforeChange(next); } catch (e) { ok = false; }\n if (!alive.current) return;\n setBusy(false);\n if (!ok) return;\n }\n onChange && onChange(next);\n };\n\n const glyphIcon = pick(knobIcon, checked);\n const glyphText = pick(knobText, checked);\n const knobX = checked ? `calc(100% - ${H} + 2px)` : 2;\n\n const knob = <span aria-hidden=\"true\" style={{ position: \"absolute\", top: 2, left: knobX, width: KNOB, height: KNOB, borderRadius: \"var(--radius-full)\", background: \"var(--bg-surface)\", boxShadow: \"0 1px 2px rgba(0,0,0,.18)\", transition: \"left 160ms ease-out\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", color: track, fontSize: TEXT, lineHeight: 1, fontWeight: \"var(--weight-semibold)\", fontFamily: \"var(--font-sans)\", overflow: \"hidden\" }}>\n {loading\n ? <span style={{ width: GLYPH, height: GLYPH, borderRadius: \"50%\", border: `1.5px solid ${track}`, borderRightColor: \"transparent\", animation: \"kb-spin 720ms linear infinite\" }}></span>\n : glyphIcon ? <Icon name={glyphIcon} size={10} strokeWidth={2.5} style={{ width: GLYPH, height: GLYPH }} />\n : glyphText ? <span>{String(glyphText).slice(0, 1)}</span> : null}\n </span>;\n\n const insideStyle = (side) => ({ position: \"absolute\", top: 0, bottom: 0, [side]: 6, display: \"flex\", alignItems: \"center\", fontSize: TEXT, lineHeight: 1, fontWeight: \"var(--weight-semibold)\", fontFamily: \"var(--font-sans)\", letterSpacing: \".2px\", color: \"var(--on-accent)\", transition: \"opacity 160ms ease-out\", pointerEvents: \"none\", whiteSpace: \"nowrap\" });\n\n const pill = <span role=\"switch\" aria-checked={checked} aria-busy={loading || undefined} aria-disabled={disabled || undefined} tabIndex={inert ? -1 : 0} onClick={flip}\n onKeyDown={e => { if (e.key === \" \" || e.key === \"Enter\") { e.preventDefault(); flip(); } }}\n style={{ position: \"relative\", display: \"inline-block\", width: w, height: H, borderRadius: \"var(--radius-full)\", \"--sw-track\": track, background: \"var(--sw-bg, var(--sw-track))\", transition: \"background 160ms ease-out\", cursor: inert ? (loading ? \"progress\" : \"not-allowed\") : \"pointer\", flexShrink: 0, verticalAlign: \"middle\", outline: \"none\" }}>\n {hasInside && <span style={{ ...insideStyle(\"left\"), opacity: checked ? 1 : 0 }}>{insideOn}</span>}\n {hasInside && <span style={{ ...insideStyle(\"right\"), opacity: checked ? 0 : 1 }}>{insideOff}</span>}\n {knob}\n </span>;\n\n const capBase = { fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", fontFamily: \"var(--font-sans)\", cursor: inert ? \"default\" : \"pointer\", userSelect: \"none\" };\n if (single) return <span data-size={size} data-sw=\"\" style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"var(--control-gap)\", opacity: disabled ? .5 : 1, ...style }}>{pill}<span onClick={flip} style={{ ...capBase, fontWeight: \"var(--weight-regular)\", color: \"var(--text-body)\" }}>{label}</span></span>;\n if (labelOn == null && labelOff == null) return <span data-size={size} style={{ display: \"inline-flex\", alignItems: \"center\", opacity: disabled ? .5 : 1, ...style }}>{pill}</span>;\n\n const cap = (text, active) => text == null ? null : <span style={{ ...capBase, fontWeight: active ? \"var(--weight-medium)\" : \"var(--weight-regular)\", color: active ? \"var(--text-heading)\" : \"var(--text-muted)\", transition: \"color 160ms ease-out\" }} onClick={flip}>{text}</span>;\n return <span data-size={size} data-sw=\"\" style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"var(--control-gap)\", opacity: disabled ? .5 : 1, ...style }}>\n {cap(labelOff, !checked)}{pill}{cap(labelOn, checked)}\n </span>;\n}\n\nexport default Switch;\n","forms/Switch.vue":"<script setup>\nimport { ref, computed, onBeforeUnmount } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Switch — pill track sized by the region's tier (sm 28×16 · md 36×20 · lg 44×24), knob 4px smaller, 160ms slide. v-model:checked.\n// Geometry is CSS (tokens/layout.css --switch-w/-h), so it follows the tier without JavaScript; `size` stamps data-size to override.\n// tone \"primary\" | \"semantic\" (red off → green on); onColor/offColor override.\n// knobIcon / knobText: string | { on, off } — knobText is ONE letter or ONE CJK char.\n// insideOn / insideOff: text inside the pill opposite the knob. labelOn / labelOff: captions beside the pill.\n// loading: controlled spinner. beforeChange(next) → Promise|boolean gates the flip and shows loading meanwhile.\nconst props = defineProps({\n checked: Boolean, disabled: Boolean, loading: Boolean,\n size: String, tone: { type: String, default: 'primary' },\n onColor: String, offColor: String,\n knobIcon: [String, Object], knobText: [String, Object],\n insideOn: String, insideOff: String, inlinePrompt: Object, label: String, labelOn: String, labelOff: String,\n beforeChange: Function, style: Object\n})\nconst emit = defineEmits(['update:checked', 'change'])\nconst H = 'var(--switch-h)', W = 'var(--switch-w)', KNOB = 'calc(var(--switch-h) - 4px)', GLYPH = 'calc(var(--switch-h) * .5)', TEXT = 'max(calc(var(--switch-h) * .5), var(--text-glyph-min))' /* text never drops below the glyph floor (12px in CJK) */\nconst busy = ref(false); let alive = true; onBeforeUnmount(() => { alive = false })\nconst isLoading = computed(() => props.loading || busy.value)\nconst inert = computed(() => props.disabled || isLoading.value)\n// inlinePrompt { on, off } is an alias of the insideOn / insideOff pair (same as Switch.jsx)\nconst inOn = computed(() => props.insideOn ?? (props.inlinePrompt ? props.inlinePrompt.on : undefined))\nconst inOff = computed(() => props.insideOff ?? (props.inlinePrompt ? props.inlinePrompt.off : undefined))\nconst hasInside = computed(() => inOn.value != null || inOff.value != null)\n// inside text needs a wider track so the knob never covers the word: text runs at half the track height, ~.68em per character\nconst width = computed(() => { if (!hasInside.value) return W; const n = Math.max((inOn.value || '').length, (inOff.value || '').length); return `max(${W}, calc(${H} * ${(1 + n * .34).toFixed(2)} + 6px))` })\nconst track = computed(() => props.checked ? (props.onColor || (props.tone === 'semantic' ? 'var(--success)' : 'var(--action-primary)')) : (props.offColor || (props.tone === 'semantic' ? 'var(--danger)' : 'var(--border-strong)')))\nconst pick = (v) => v && typeof v === 'object' ? (props.checked ? v.on : v.off) : v\nconst glyphIcon = computed(() => pick(props.knobIcon))\nconst glyphText = computed(() => { const t = pick(props.knobText); return t ? String(t).slice(0, 1) : '' })\nconst knobX = computed(() => props.checked ? `calc(100% - ${H} + 2px)` : '2px')\nasync function flip() {\n if (inert.value) return\n const next = !props.checked\n if (props.beforeChange) {\n busy.value = true; let ok = false\n try { ok = await props.beforeChange(next) } catch (e) { ok = false }\n if (!alive) return\n busy.value = false\n if (!ok) return\n }\n emit('update:checked', next); emit('change', next)\n}\nconst insideStyle = (side, on) => ({ position: 'absolute', top: 0, bottom: 0, [side]: '6px', display: 'flex', alignItems: 'center', fontSize: TEXT, lineHeight: 1, fontWeight: 'var(--weight-semibold)', fontFamily: 'var(--font-sans)', letterSpacing: '.2px', color: 'var(--on-accent)', transition: 'opacity 160ms ease-out', pointerEvents: 'none', whiteSpace: 'nowrap', opacity: on === props.checked ? 1 : 0 })\nconst cap = (active) => ({ fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', fontFamily: 'var(--font-sans)', fontWeight: active ? 'var(--weight-medium)' : 'var(--weight-regular)', color: active ? 'var(--text-heading)' : 'var(--text-muted)', transition: 'color 160ms ease-out', cursor: inert.value ? 'default' : 'pointer', userSelect: 'none' })\n</script>\n<template>\n <span :data-size=\"size\" data-sw=\"\" :style=\"{ display: 'inline-flex', alignItems: 'center', gap: 'var(--control-gap)', opacity: disabled ? .5 : 1, ...style }\">\n <span v-if=\"labelOff != null\" :style=\"cap(!checked)\" @click=\"flip\">{{ labelOff }}</span>\n <span role=\"switch\" :aria-checked=\"checked\" :aria-busy=\"isLoading || undefined\" :aria-disabled=\"disabled || undefined\" :tabindex=\"inert ? -1 : 0\" @click=\"flip\" @keydown.space.prevent=\"flip\" @keydown.enter.prevent=\"flip\"\n :style=\"{ position: 'relative', display: 'inline-block', width: width, height: H, borderRadius: 'var(--radius-full)', '--sw-track': track, background: 'var(--sw-bg, var(--sw-track))', transition: 'background 160ms ease-out', cursor: inert ? (isLoading ? 'progress' : 'not-allowed') : 'pointer', flexShrink: 0, verticalAlign: 'middle', outline: 'none' }\">\n <span v-if=\"hasInside\" :style=\"insideStyle('left', true)\">{{ inOn }}</span>\n <span v-if=\"hasInside\" :style=\"insideStyle('right', false)\">{{ inOff }}</span>\n <span aria-hidden=\"true\" :style=\"{ position: 'absolute', top: '2px', left: knobX, width: KNOB, height: KNOB, borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', boxShadow: '0 1px 2px rgba(0,0,0,.18)', transition: 'left 160ms ease-out', display: 'flex', alignItems: 'center', justifyContent: 'center', color: track, fontSize: TEXT, lineHeight: 1, fontWeight: 'var(--weight-semibold)', fontFamily: 'var(--font-sans)', overflow: 'hidden' }\">\n <span v-if=\"isLoading\" :style=\"{ width: GLYPH, height: GLYPH, borderRadius: '50%', border: '1.5px solid ' + track, borderRightColor: 'transparent', animation: 'kb-spin 720ms linear infinite' }\"></span>\n <Icon v-else-if=\"glyphIcon\" :name=\"glyphIcon\" :size=\"10\" :stroke-width=\"2.5\" :style=\"{ width: GLYPH, height: GLYPH }\" />\n <span v-else-if=\"glyphText\">{{ glyphText }}</span>\n </span>\n </span>\n <span v-if=\"labelOn != null\" :style=\"cap(checked)\" @click=\"flip\">{{ labelOn }}</span>\n <span v-if=\"label != null && labelOn == null && labelOff == null\" :style=\"{ fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-regular)', color: 'var(--text-body)', cursor: inert ? 'default' : 'pointer', userSelect: 'none' }\" @click=\"flip\">{{ label }}</span>\n </span>\n</template>\n","forms/Textarea.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { inputBase } from './input-base.js'\n// Multiline textarea, r8, default 90px tall; text follows the region's tier. v-model. showCount + maxLength renders a bottom-right \"n/max\" counter (red past the limit).\nconst props = defineProps({ modelValue: String, rows: { type: Number, default: 4 }, error: Boolean, disabled: Boolean, showCount: Boolean, maxLength: Number, size: String, style: Object })\ndefineEmits(['update:modelValue'])\nconst focus = ref(false)\nconst len = computed(() => (props.modelValue || '').length)\nconst over = computed(() => props.maxLength != null && len.value > props.maxLength)\nconst s = computed(() => ({ ...inputBase, height: '90px', resize: 'vertical', paddingTop: '8px', paddingBottom: props.showCount ? '22px' : '8px',\n ...((props.error || over.value) ? { borderColor: 'var(--danger)' } : null),\n ...(focus.value && !props.disabled ? { borderColor: (props.error || over.value) ? 'var(--danger)' : 'var(--focus-border)', boxShadow: `0 0 0 2px ${(props.error || over.value) ? 'var(--danger-bg)' : 'var(--focus-ring)'}` } : null),\n ...(props.disabled ? { opacity: .5, cursor: 'not-allowed', background: 'var(--bg-inset)' } : null), ...props.style }))\n</script>\n<template>\n <div style=\"position:relative\">\n <textarea :rows=\"rows\" :disabled=\"disabled\" :data-size=\"size\" :data-field=\"disabled ? undefined : ''\" :maxlength=\"showCount ? undefined : maxLength\" :value=\"modelValue\" @input=\"$emit('update:modelValue', $event.target.value)\" @focus=\"focus = true\" @blur=\"focus = false\" :style=\"s\"></textarea>\n <span v-if=\"showCount\" :style=\"{ position: 'absolute', right: '10px', bottom: '7px', fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums', fontSize: 'var(--text-xs)', color: over ? 'var(--danger)' : 'var(--text-muted)' }\">{{ len }}{{ maxLength != null ? `/${maxLength}` : '' }}</span>\n </div>\n</template>\n","forms/input-base.js":"// Shared base style for text-like controls (Input, Textarea, Combobox, DatePicker).\n// Lives in a plain module because <script setup> cannot export values.\n// Height, text and padding come from the size tier of the enclosing region (tokens/layout.css): sm 28 · md 32 · lg 40.\nexport const inputBase = { height: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: '1px solid var(--field-border, var(--border-control))', background: 'var(--bg-surface)', padding: '0 var(--control-px)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-primary)', outline: 'none', boxSizing: 'border-box', width: '100%' }\n","forms/Radio.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/**\n * Radio — circle at the region's tier (sm 14 · md/lg 16, --check-size), 1.5px border, filled action-blue dot when selected.\n * hover darkens the ring; keyboard focus shows the 2px focus ring; disabled = 50% opacity.\n * description renders a muted second line under the label (settings-style choices).\n */\nexport function Radio({ checked, onChange, label, description, size, disabled, style }) {\n const d = { box: \"var(--check-size)\", dot: \"calc(var(--check-size) / 2)\" };\n const [hover, setHover] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const ring = checked ? \"var(--action-primary)\" : hover && !disabled ? \"var(--text-muted)\" : \"var(--checkbox-border)\";\n return (\n <label role=\"radio\" data-size={size} aria-checked={!!checked} aria-disabled={disabled || undefined} tabIndex={disabled ? -1 : 0}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}\n onKeyDown={(e) => { if (e.key === \" \" || e.key === \"Enter\") { e.preventDefault(); !disabled && onChange && onChange(true); } }}\n style={{ display: \"inline-flex\", alignItems: description ? \"flex-start\" : \"center\", verticalAlign: \"middle\", lineHeight: \"var(--control-leading)\", gap: \"var(--control-gap)\", cursor: disabled ? \"not-allowed\" : \"pointer\", opacity: disabled ? .5 : 1, outline: \"none\", ...style }}\n onClick={() => !disabled && onChange && onChange(true)}>\n <span style={{ width: d.box, height: d.box, marginTop: description ? 2 : 0, borderRadius: \"var(--radius-full)\", boxSizing: \"border-box\", border: `1.5px solid ${ring}`, background: \"var(--bg-surface)\", boxShadow: focus && !disabled ? \"0 0 0 2px var(--focus-ring)\" : \"none\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", flexShrink: 0, transition: \"var(--t-hover)\" }}>\n {checked ? <span style={{ width: d.dot, height: d.dot, borderRadius: \"var(--radius-full)\", background: \"var(--action-primary)\" }}></span> : null}\n </span>\n {label || description ? <span style={{ display: \"flex\", flexDirection: \"column\", gap: 2 }}>\n {label ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-regular)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-body)\" }}>{label}</span> : null}\n {description ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-muted)\" }}>{description}</span> : null}\n </span> : null}\n </label>\n );\n}\n\n/**\n * RadioGroup — single choice from a small set. Vertical (default) or `row`.\n * options: string[] | { label, value, description?, disabled? }[]\n * Arrow keys move the selection between enabled options (WAI-ARIA radiogroup).\n * variant=\"cards\" renders each option as a bordered selectable card (plan pickers, migration strategies).\n */\nexport function RadioGroup({ options = [], value, onChange, row, size, variant = \"plain\", disabled, style }) {\n const opts = options.map((o) => (typeof o === \"string\" ? { label: o, value: o } : o));\n const enabled = opts.filter((o) => !o.disabled && !disabled);\n const onKey = (e) => {\n if (![\"ArrowDown\", \"ArrowRight\", \"ArrowUp\", \"ArrowLeft\"].includes(e.key) || !enabled.length) return;\n e.preventDefault();\n const i = enabled.findIndex((o) => o.value === value);\n const next = e.key === \"ArrowDown\" || e.key === \"ArrowRight\" ? enabled[(i + 1) % enabled.length] : enabled[(i - 1 + enabled.length) % enabled.length];\n onChange && onChange(next.value);\n };\n if (variant === \"cards\") {\n return (\n <div role=\"radiogroup\" data-size={size} onKeyDown={onKey} style={{ display: \"flex\", flexDirection: row ? \"row\" : \"column\", gap: 8, ...style }}>\n {opts.map((o) => {\n const on = value === o.value, off = o.disabled || disabled;\n return <div key={o.value} onClick={() => !off && onChange && onChange(o.value)}\n style={{ flex: row ? 1 : \"none\", padding: \"10px 12px\", borderRadius: \"var(--radius-md)\", border: `1px solid ${on ? \"var(--action-primary)\" : \"var(--border-card)\"}`, background: on ? \"var(--soft-bg)\" : \"var(--bg-surface)\", cursor: off ? \"not-allowed\" : \"pointer\", opacity: off ? .5 : 1, transition: \"var(--t-hover)\" }}>\n <Radio checked={on} onChange={() => onChange && onChange(o.value)} label={o.label} description={o.description} disabled={off} style={{ opacity: 1, cursor: \"inherit\" }} />\n </div>;\n })}\n </div>\n );\n }\n return (\n <div role=\"radiogroup\" data-size={size} onKeyDown={onKey} style={{ display: \"flex\", flexDirection: row ? \"row\" : \"column\", gap: row ? \"var(--group-gap)\" : 10, ...style }}>\n {opts.map((o) => <Radio key={o.value} label={o.label} description={o.description} disabled={o.disabled || disabled} checked={value === o.value} onChange={() => onChange && onChange(o.value)} />)}\n </div>\n );\n}\n"});
1
+ window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"forms/Checkbox.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text. */\nexport function Checkbox({ checked, indeterminate, onChange, label, size, disabled, style }) {\n const box = \"var(--check-size)\", glyph = { width: \"calc(var(--check-size) - 4px)\", height: \"calc(var(--check-size) - 4px)\" };\n const filled = checked || indeterminate;\n return (\n <label data-size={size} data-check={disabled ? undefined : \"\"} style={{ display: \"inline-flex\", alignItems: \"center\", verticalAlign: \"middle\", lineHeight: \"var(--control-leading)\", gap: \"var(--control-gap)\", cursor: disabled ? \"not-allowed\" : \"pointer\", opacity: disabled ? .5 : 1, ...style }}\n onClick={() => !disabled && onChange && onChange(!checked)}>\n <span style={{ width: box, height: box, borderRadius: 4, boxSizing: \"border-box\", border: filled ? \"none\" : \"1.5px solid var(--check-border, var(--checkbox-border))\", background: filled ? \"var(--action-primary)\" : \"var(--bg-surface)\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", color: \"var(--on-accent)\", flexShrink: 0, transition: \"var(--t-hover)\" }}>\n {checked ? <Icon name=\"check\" size={12} strokeWidth={3} style={glyph} /> : indeterminate ? <Icon name=\"minus\" size={12} strokeWidth={3} style={glyph} /> : null}\n </span>\n {label ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-regular)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-body)\" }}>{label}</span> : null}\n </label>\n );\n}\n","forms/Checkbox.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text.\nconst props = defineProps({ checked: Boolean, indeterminate: Boolean, label: String, size: String, disabled: Boolean, style: Object })\nconst emit = defineEmits(['change'])\nconst box = 'var(--check-size)', glyph = { width: 'calc(var(--check-size) - 4px)', height: 'calc(var(--check-size) - 4px)' }\nconst filled = computed(() => props.checked || props.indeterminate)\n</script>\n<template>\n <label :data-size=\"size\" :data-check=\"disabled ? undefined : ''\" :style=\"{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--control-leading)', gap: 'var(--control-gap)', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, ...style }\" @click=\"!disabled && emit('change', !checked)\">\n <span :style=\"{ width: box, height: box, borderRadius: '4px', boxSizing: 'border-box', border: filled ? 'none' : '1.5px solid var(--check-border, var(--checkbox-border))', background: filled ? 'var(--action-primary)' : 'var(--bg-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--on-accent)', flexShrink: 0, transition: 'var(--t-hover)' }\">\n <Icon v-if=\"checked\" name=\"check\" :size=\"12\" :stroke-width=\"3\" :style=\"glyph\" />\n <Icon v-else-if=\"indeterminate\" name=\"minus\" :size=\"12\" :stroke-width=\"3\" :style=\"glyph\" />\n </span>\n <span v-if=\"label\" style=\"font-family:var(--font-sans);font-weight:var(--weight-regular);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-body)\">{{ label }}</span>\n </label>\n</template>\n","forms/Combobox.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { inputBase } from \"./Input.jsx\";\n\nconst tierIcon = { width: \"var(--control-icon)\", height: \"var(--control-icon)\" };\n\n/** Typeahead single-select: type to filter options, click or Enter to pick. Chevron toggles the list; shows \"No results\" when the filter matches nothing. Field and option rows take the region's tier height (omit `size` to inherit). */\nexport function Combobox({ options = [], value = \"\", placeholder = \"Select...\", disabled, error, width, size, onChange, style }) {\n const [open, setOpen] = React.useState(false);\n const [query, setQuery] = React.useState(null);\n const [hov, setHov] = React.useState(-1);\n const root = React.useRef(null);\n React.useEffect(() => { const h = (e) => { if (root.current && !root.current.contains(e.target)) { setOpen(false); setQuery(null); } }; document.addEventListener(\"mousedown\", h); return () => document.removeEventListener(\"mousedown\", h); }, []);\n const shown = query == null ? options : options.filter(o => o.toLowerCase().includes(query.toLowerCase()));\n const pick = (o) => { onChange && onChange(o); setOpen(false); setQuery(null); };\n return (\n <div ref={root} data-size={size} style={{ position: \"relative\", width, ...style }}>\n <div data-field={disabled ? undefined : \"\"} style={{ position: \"relative\" }}>\n <input value={query != null ? query : value} placeholder={placeholder} disabled={disabled}\n onFocus={() => setOpen(true)} onChange={(e) => { setQuery(e.target.value); setOpen(true); setHov(0); }}\n onKeyDown={(e) => { if (e.key === \"ArrowDown\") setHov(Math.min(hov + 1, shown.length - 1)); if (e.key === \"ArrowUp\") setHov(Math.max(hov - 1, 0)); if (e.key === \"Enter\" && shown[hov]) pick(shown[hov]); if (e.key === \"Escape\") { setOpen(false); setQuery(null); } }}\n style={{ ...inputBase, paddingRight: \"calc(var(--control-px) + var(--control-icon) + 6px)\", borderColor: error ? \"var(--danger)\" : open ? \"var(--focus-border)\" : \"var(--field-border, var(--border-control))\", boxShadow: open ? \"0 0 0 2px var(--focus-ring)\" : \"none\", opacity: disabled ? .5 : 1, cursor: disabled ? \"not-allowed\" : \"text\" }} />\n <button type=\"button\" disabled={disabled} onClick={() => { setOpen(!open); setQuery(null); }} style={{ position: \"absolute\", right: 1, top: \"50%\", transform: \"translateY(-50%)\", width: \"calc(var(--control-h) - 2px)\", height: \"calc(var(--control-h) - 2px)\", border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--text-muted)\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", padding: 0 }}>\n <span style={{ display: \"inline-flex\", transform: open ? \"rotate(180deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}><Icon name=\"chevron-down\" size={16} style={tierIcon} /></span>\n </button>\n </div>\n {open && !disabled && <div style={{ position: \"absolute\", top: \"calc(100% + 4px)\", left: 0, right: 0, zIndex: \"var(--z-menu)\", background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: 4, maxHeight: 208, overflowY: \"auto\" }}>\n {shown.length === 0 && <div style={{ height: \"var(--control-h)\", display: \"flex\", alignItems: \"center\", padding: \"0 10px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-muted)\" }}>No results</div>}\n {shown.map((o, i) => <button key={o} type=\"button\" onClick={() => pick(o)} onMouseEnter={() => setHov(i)}\n style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", width: \"100%\", height: \"var(--control-h)\", padding: \"0 10px\", borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: \"pointer\", background: hov === i ? \"var(--bg-hover)\" : \"transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-heading)\", textAlign: \"left\" }}>\n {o}{o === value && <span style={{ color: \"var(--action-primary)\", display: \"inline-flex\" }}><Icon name=\"check\" size={16} style={tierIcon} /></span>}\n </button>)}\n </div>}\n </div>\n );\n}\n","forms/Combobox.vue":"<script setup>\nimport { ref, computed, onMounted, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport { inputBase } from './input-base.js'\n// Typeahead single-select: type to filter options, click or Enter to pick; \"No results\" empty row. Field and option rows take the region's tier height (omit `size` to inherit).\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select...' }, disabled: Boolean, error: Boolean, width: [Number, String], size: String, style: Object })\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst emit = defineEmits(['update:modelValue'])\nconst open = ref(false), query = ref(null), hov = ref(-1), root = ref(null)\nconst shown = computed(() => query.value == null ? props.options : props.options.filter(o => o.toLowerCase().includes(query.value.toLowerCase())))\nfunction pick(o) { emit('update:modelValue', o); open.value = false; query.value = null }\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) { open.value = false; query.value = null } }\nonMounted(() => document.addEventListener('mousedown', onDoc))\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\n</script>\n<template>\n <div ref=\"root\" :data-size=\"size\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div :data-field=\"disabled ? undefined : ''\" style=\"position:relative\">\n <input :value=\"query != null ? query : modelValue\" :placeholder=\"placeholder\" :disabled=\"disabled\"\n @focus=\"open = true\" @input=\"query = $event.target.value; open = true; hov = 0\"\n @keydown.down=\"hov = Math.min(hov + 1, shown.length - 1)\" @keydown.up=\"hov = Math.max(hov - 1, 0)\"\n @keydown.enter=\"shown[hov] && pick(shown[hov])\" @keydown.esc=\"open = false; query = null\"\n :style=\"{ ...inputBase, paddingRight: 'calc(var(--control-px) + var(--control-icon) + 6px)', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1 }\" />\n <button type=\"button\" :disabled=\"disabled\" @click=\"open = !open; query = null\" style=\"position:absolute;right:1px;top:50%;transform:translateY(-50%);width:calc(var(--control-h) - 2px);height:calc(var(--control-h) - 2px);border:none;background:transparent;cursor:pointer;color:var(--text-muted);display:flex;align-items:center;justify-content:center;padding:0\">\n <span :style=\"{ display: 'inline-flex', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\"><Icon name=\"chevron-down\" :size=\"16\" :style=\"tierIcon\" /></span>\n </button>\n </div>\n <div v-if=\"open && !disabled\" style=\"position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:var(--z-menu);background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;max-height:208px;overflow-y:auto\">\n <div v-if=\"shown.length === 0\" style=\"height:var(--control-h);display:flex;align-items:center;padding:0 10px;font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);color:var(--text-muted)\">No results</div>\n <button v-for=\"(o, i) in shown\" :key=\"o\" type=\"button\" @click=\"pick(o)\" @mouseenter=\"hov = i\"\n :style=\"{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%', height: 'var(--control-h)', padding: '0 10px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: 'pointer', background: hov === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-heading)', textAlign: 'left' }\">\n {{ o }}<span v-if=\"o === modelValue\" style=\"color:var(--action-primary);display:inline-flex\"><Icon name=\"check\" :size=\"16\" :style=\"tierIcon\" /></span>\n </button>\n </div>\n </div>\n</template>\n","forms/DatePicker.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\nimport { inputBase } from \"./Input.jsx\";\n\nconst pad = (n) => String(n).padStart(2, \"0\");\nconst iso = (d) => d.getFullYear() + \"-\" + pad(d.getMonth() + 1) + \"-\" + pad(d.getDate());\nconst tierIcon = { width: \"var(--control-icon)\", height: \"var(--control-icon)\" };\n/** Month title, weekday initials and the field label come from Intl for the given locale (en-US, zh-CN, ja-JP…), so the picker reads right in every site language. */\nconst fmt = (locale, opts) => { try { return new Intl.DateTimeFormat(locale, opts); } catch (e) { return new Intl.DateTimeFormat(\"en-US\", opts); } };\nconst weekdays = (locale) => Array.from({ length: 7 }, (_, i) => fmt(locale, { weekday: \"short\" }).format(new Date(2024, 0, 1 + i))); // 2024-01-01 is a Monday\n\n/** Date picker: input with calendar glyph opening a month-grid popover — chevron month nav, today outlined, selected day filled blue. value/onChange use \"YYYY-MM-DD\"; `locale` localises the labels; the field takes the region's tier (omit `size` to inherit). */\nexport function DatePicker({ value = \"\", placeholder = \"Select date\", disabled, error, width = 180, size, locale = \"en-US\", onChange, style }) {\n const [open, setOpen] = React.useState(false);\n const sel = value ? new Date(value + \"T00:00:00\") : null;\n const [view, setView] = React.useState(() => { const d = sel || new Date(); return { y: d.getFullYear(), m: d.getMonth() }; });\n const root = React.useRef(null);\n React.useEffect(() => { const h = (e) => { if (root.current && !root.current.contains(e.target)) setOpen(false); }; document.addEventListener(\"mousedown\", h); return () => document.removeEventListener(\"mousedown\", h); }, []);\n const first = new Date(view.y, view.m, 1);\n const start = (first.getDay() + 6) % 7; // Monday-first\n const days = new Date(view.y, view.m + 1, 0).getDate();\n const todayIso = iso(new Date());\n const nav = (d) => { let m = view.m + d, y = view.y; if (m < 0) { m = 11; y--; } if (m > 11) { m = 0; y++; } setView({ y, m }); };\n const label = sel ? fmt(locale, { year: \"numeric\", month: \"short\", day: \"numeric\" }).format(sel) : \"\";\n const monthTitle = fmt(locale, { year: \"numeric\", month: \"long\" }).format(new Date(view.y, view.m, 1));\n return (\n <div ref={root} data-size={size} style={{ position: \"relative\", width, ...style }}>\n <div data-field={disabled ? undefined : \"\"} onClick={() => !disabled && setOpen(!open)} style={{ ...inputBase, display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", cursor: disabled ? \"not-allowed\" : \"pointer\", borderColor: error ? \"var(--danger)\" : open ? \"var(--focus-border)\" : \"var(--field-border, var(--border-control))\", boxShadow: open ? \"0 0 0 2px var(--focus-ring)\" : \"none\", opacity: disabled ? .5 : 1, color: label ? \"var(--text-primary)\" : \"var(--text-muted)\" }}>\n {label || placeholder}<span style={{ color: \"var(--text-muted)\", display: \"inline-flex\" }}><Icon name=\"calendar\" size={16} style={tierIcon} /></span>\n </div>\n {open && <div style={{ position: \"absolute\", top: \"calc(100% + 4px)\", left: 0, zIndex: \"var(--z-menu)\", width: 252, background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-lg)\", boxShadow: \"var(--shadow-menu)\", padding: 12, boxSizing: \"border-box\" }}>\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\", paddingBottom: 8 }}>\n <IconButton icon=\"chevron-left\" label=\"Previous month\" onClick={() => nav(-1)} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-heading)\" }}>{monthTitle}</span>\n <IconButton icon=\"chevron-right\" label=\"Next month\" onClick={() => nav(1)} />\n </div>\n <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(7,1fr)\", gap: 2 }}>\n {weekdays(locale).map((d, i) => <span key={i} style={{ textAlign: \"center\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", letterSpacing: \"var(--tracking-xs)\", color: \"var(--text-muted)\", padding: \"2px 0\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{d}</span>)}\n {Array.from({ length: start }, (_, i) => <span key={\"e\" + i}></span>)}\n {Array.from({ length: days }, (_, i) => {\n const dIso = view.y + \"-\" + pad(view.m + 1) + \"-\" + pad(i + 1);\n const isSel = value === dIso, isToday = todayIso === dIso;\n return <button key={i} type=\"button\" onClick={() => { onChange && onChange(dIso); setOpen(false); }}\n style={{ height: 28, borderRadius: \"var(--radius-sm)\", border: isToday && !isSel ? \"1px solid var(--action-primary)\" : \"1px solid transparent\", cursor: \"pointer\", background: isSel ? \"var(--action-primary)\" : \"transparent\", color: isSel ? \"var(--on-accent)\" : \"var(--text-body)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", fontVariantNumeric: \"tabular-nums\", padding: 0, boxSizing: \"border-box\" }}\n onMouseEnter={(e) => { if (!isSel) e.currentTarget.style.background = \"var(--bg-hover)\"; }} onMouseLeave={(e) => { if (!isSel) e.currentTarget.style.background = \"transparent\"; }}>{i + 1}</button>;\n })}\n </div>\n </div>}\n </div>\n );\n}\n","forms/DatePicker.vue":"<script setup>\nimport { ref, computed, onMounted, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport IconButton from '../buttons/IconButton.vue'\nimport { inputBase } from './input-base.js'\n// Date picker: input with calendar glyph opening a month-grid popover — Monday-first, today outlined, selected filled. v-model is \"YYYY-MM-DD\".\n// `locale` localises month title, weekday initials and the field label through Intl; the field takes the region's tier (omit `size` to inherit).\nconst pad = (n) => String(n).padStart(2, '0')\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst fmt = (locale, opts) => { try { return new Intl.DateTimeFormat(locale, opts) } catch (e) { return new Intl.DateTimeFormat('en-US', opts) } }\nconst props = defineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select date' }, disabled: Boolean, error: Boolean, width: { type: [Number, String], default: 180 }, size: String, locale: { type: String, default: 'en-US' }, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst open = ref(false), root = ref(null)\nconst sel = computed(() => props.modelValue ? new Date(props.modelValue + 'T00:00:00') : null)\nconst d0 = sel.value || new Date()\nconst view = ref({ y: d0.getFullYear(), m: d0.getMonth() })\nconst start = computed(() => (new Date(view.value.y, view.value.m, 1).getDay() + 6) % 7)\nconst days = computed(() => new Date(view.value.y, view.value.m + 1, 0).getDate())\nconst todayIso = (() => { const d = new Date(); return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) })()\nconst label = computed(() => sel.value ? fmt(props.locale, { year: 'numeric', month: 'short', day: 'numeric' }).format(sel.value) : '')\nconst monthTitle = computed(() => fmt(props.locale, { year: 'numeric', month: 'long' }).format(new Date(view.value.y, view.value.m, 1)))\nconst weekdays = computed(() => Array.from({ length: 7 }, (_, i) => fmt(props.locale, { weekday: 'short' }).format(new Date(2024, 0, 1 + i)))) // 2024-01-01 is a Monday\nfunction nav(d) { let m = view.value.m + d, y = view.value.y; if (m < 0) { m = 11; y-- } if (m > 11) { m = 0; y++ } view.value = { y, m } }\nfunction dayIso(i) { return view.value.y + '-' + pad(view.value.m + 1) + '-' + pad(i) }\nfunction pick(i) { emit('update:modelValue', dayIso(i)); open.value = false }\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }\nonMounted(() => document.addEventListener('mousedown', onDoc))\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\n</script>\n<template>\n <div ref=\"root\" :data-size=\"size\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div :data-field=\"disabled ? undefined : ''\" @click=\"!disabled && (open = !open)\" :style=\"{ ...inputBase, display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: disabled ? 'not-allowed' : 'pointer', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1, color: label ? 'var(--text-primary)' : 'var(--text-muted)' }\">\n {{ label || placeholder }}<span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"calendar\" :size=\"16\" :style=\"tierIcon\" /></span>\n </div>\n <div v-if=\"open\" style=\"position:absolute;top:calc(100% + 4px);left:0;z-index:var(--z-menu);width:252px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);padding:12px;box-sizing:border-box\">\n <div style=\"display:flex;align-items:center;justify-content:space-between;padding-bottom:8px\">\n <IconButton icon=\"chevron-left\" label=\"Previous month\" @click=\"nav(-1)\" />\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--control-text);letter-spacing:var(--control-tracking);color:var(--text-heading)\">{{ monthTitle }}</span>\n <IconButton icon=\"chevron-right\" label=\"Next month\" @click=\"nav(1)\" />\n </div>\n <div style=\"display:grid;grid-template-columns:repeat(7,1fr);gap:2px\">\n <span v-for=\"(d, i) in weekdays\" :key=\"i\" style=\"text-align:center;font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted);padding:2px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ d }}</span>\n <span v-for=\"i in start\" :key=\"'e' + i\" />\n <button v-for=\"i in days\" :key=\"i\" type=\"button\" @click=\"pick(i)\" class=\"kb-dp-day\"\n :style=\"{ height: '28px', borderRadius: 'var(--radius-sm)', border: todayIso === dayIso(i) && modelValue !== dayIso(i) ? '1px solid var(--action-primary)' : '1px solid transparent', cursor: 'pointer', background: modelValue === dayIso(i) ? 'var(--action-primary)' : 'transparent', color: modelValue === dayIso(i) ? 'var(--on-accent)' : 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', letterSpacing: 'var(--control-tracking)', fontVariantNumeric: 'tabular-nums', padding: 0, boxSizing: 'border-box' }\">{{ i }}</button>\n </div>\n </div>\n </div>\n</template>\n<style scoped>.kb-dp-day:hover{background:var(--bg-hover)!important}</style>\n","forms/Dropzone.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst toneColor = { default: \"var(--dropzone-border)\", hover: \"var(--dropzone-border-hover)\", error: \"var(--danger)\", drag: \"var(--focus-border)\" };\n\n/** Dashed upload dropzone (2px, r10). States: default/hover, real drag-and-drop (blue border + tint while a file is dragged over), error, disabled, and a filled \"uploaded\" state showing the file name + remove. */\nexport function Dropzone({ title = \"Click to upload or drag and drop\", hint = \"JSON files only\", height = 185.5, error, disabled, dragOver, file, onRemove, onClick, onDropFiles, style }) {\n const [hover, setHover] = React.useState(false);\n const [dragging, setDragging] = React.useState(false);\n const dragActive = dragOver || dragging;\n if (file) {\n return (\n <div style={{ height, borderRadius: \"var(--radius-lg)\", border: \"1px solid var(--border-control)\", background: \"var(--bg-inset)\", display: \"flex\", flexDirection: \"column\", alignItems: \"center\", justifyContent: \"center\", gap: 8, boxSizing: \"border-box\", ...style }}>\n <span style={{ color: \"var(--success-strong)\" }}><Icon name=\"file-check\" size={22} strokeWidth={1.5} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", color: \"var(--text-heading)\" }}>{file}</span>\n <button type=\"button\" onClick={onRemove} data-link=\"\" style={{ display: \"flex\", alignItems: \"center\", gap: 4, border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--danger)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-base)\", padding: 0, textDecorationLine: \"var(--link-line, none)\", textUnderlineOffset: 2 }}>\n <Icon name=\"trash-2\" size={13} />Remove\n </button>\n </div>\n );\n }\n const tone = disabled ? \"default\" : error ? \"error\" : dragActive ? \"drag\" : hover ? \"hover\" : \"default\";\n const stop = (e) => { e.preventDefault(); e.stopPropagation(); };\n return (\n <div onClick={disabled ? undefined : onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n onDragEnter={disabled ? undefined : (e) => { stop(e); setDragging(true); }}\n onDragOver={disabled ? undefined : stop}\n onDragLeave={disabled ? undefined : (e) => { stop(e); setDragging(false); }}\n onDrop={disabled ? undefined : (e) => { stop(e); setDragging(false); onDropFiles && onDropFiles(e.dataTransfer.files); }}\n style={{ height, borderRadius: \"var(--radius-lg)\", border: `1px dashed ${toneColor[tone]}`, background: error ? \"var(--danger-bg)\" : dragActive ? \"var(--soft-bg)\" : \"transparent\", display: \"flex\", flexDirection: \"column\", alignItems: \"center\", justifyContent: \"center\", gap: 4, cursor: disabled ? \"not-allowed\" : \"pointer\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, transition: \"var(--t-hover)\", ...style }}>\n <span style={{ color: error ? \"var(--danger)\" : dragActive ? \"var(--action-primary)\" : \"var(--text-muted)\", marginBottom: 6 }}><Icon name={error ? \"triangle-alert\" : \"upload\"} size={22} strokeWidth={1.5} /></span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: error ? \"var(--danger-text)\" : dragActive ? \"var(--action-primary)\" : \"var(--text-heading)\" }}>{error || (dragActive ? \"Drop file to upload\" : title)}</span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", color: dragActive ? \"var(--action-primary)\" : \"var(--text-secondary)\" }}>{hint}</span>\n </div>\n );\n}\n","forms/Dropzone.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst toneColor = { default: 'var(--dropzone-border)', hover: 'var(--dropzone-border-hover)', error: 'var(--danger)', drag: 'var(--focus-border)' }\n// Dashed upload dropzone (2px, r10). States: default/hover, real drag-and-drop (blue border + tint while a file is dragged over), error, disabled, and a filled \"uploaded\" state.\nconst props = defineProps({ title: { type: String, default: 'Click to upload or drag and drop' }, hint: { type: String, default: 'JSON files only' }, height: { type: [Number, String], default: 185.5 }, error: String, disabled: Boolean, dragOver: Boolean, file: String, style: Object })\nconst emit = defineEmits(['remove', 'click', 'dropFiles'])\nconst hover = ref(false), dragging = ref(false)\nconst dragActive = computed(() => props.dragOver || dragging.value)\nconst tone = computed(() => props.disabled ? 'default' : props.error ? 'error' : dragActive.value ? 'drag' : hover.value ? 'hover' : 'default')\nfunction stop(e) { e.preventDefault(); e.stopPropagation() }\nfunction onDrop(e) { stop(e); dragging.value = false; emit('dropFiles', e.dataTransfer.files) }\n</script>\n<template>\n <div v-if=\"file\" :style=\"{ height, borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-control)', background: 'var(--bg-inset)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '8px', boxSizing: 'border-box', ...style }\">\n <span style=\"color:var(--success-strong)\"><Icon name=\"file-check\" :size=\"22\" :stroke-width=\"1.5\" /></span>\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);color:var(--text-heading)\">{{ file }}</span>\n <button type=\"button\" data-link=\"\" @click=\"$emit('remove')\" style=\"display:flex;align-items:center;gap:4px;border:none;background:transparent;cursor:pointer;color:var(--danger);font-family:var(--font-sans);font-size:var(--text-base);padding:0;text-decoration-line:var(--link-line, none);text-underline-offset:2px\"><Icon name=\"trash-2\" :size=\"13\" />Remove</button>\n </div>\n <div v-else @click=\"!disabled && $emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\"\n @dragenter=\"!disabled && ((e) => { stop(e); dragging = true })($event)\" @dragover=\"!disabled && stop($event)\"\n @dragleave=\"!disabled && ((e) => { stop(e); dragging = false })($event)\" @drop=\"!disabled && onDrop($event)\"\n :style=\"{ height, borderRadius: 'var(--radius-lg)', border: `1px dashed ${toneColor[tone]}`, background: error ? 'var(--danger-bg)' : dragActive ? 'var(--soft-bg)' : 'transparent', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', boxSizing: 'border-box', opacity: disabled ? .5 : 1, transition: 'var(--t-hover)', ...style }\">\n <span :style=\"{ color: error ? 'var(--danger)' : dragActive ? 'var(--action-primary)' : 'var(--text-muted)', marginBottom: '6px' }\"><Icon :name=\"error ? 'triangle-alert' : 'upload'\" :size=\"22\" :stroke-width=\"1.5\" /></span>\n <span :style=\"{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: error ? 'var(--danger-text)' : dragActive ? 'var(--action-primary)' : 'var(--text-heading)' }\">{{ error || (dragActive ? 'Drop file to upload' : title) }}</span>\n <span :style=\"{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', color: dragActive ? 'var(--action-primary)' : 'var(--text-secondary)' }\">{{ hint }}</span>\n </div>\n</template>\n","forms/Field.jsx":"import React from \"react\";\n\n/** Form field wrapper: label (tier control text, weight 500; red + * when required) above the control, optional hint/error below. `size` sets the tier for the whole field; `inline` puts the label beside the control for filter bars. */\nexport function Field({ label, required, hint, error, size, inline, children, style }) {\n return (\n <div data-size={size} style={{ display: \"flex\", flexDirection: inline ? \"row\" : \"column\", ...(inline ? { alignItems: \"center\", gap: \"var(--control-gap)\" } : null), ...style }}>\n <label style={{ paddingBottom: inline ? 0 : 8, whiteSpace: inline ? \"nowrap\" : undefined, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-body)\" }}>{label}{required ? <span style={{ color: \"var(--danger)\" }}> *</span> : null}</label>\n {children}\n {error ? <span style={{ paddingTop: 5, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--danger)\" }}>{error}</span>\n : hint ? <span style={{ paddingTop: 5, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-muted)\" }}>{hint}</span> : null}\n </div>\n );\n}\n","forms/Field.vue":"<script setup>\n// Form field wrapper: label (tier control text, weight 500; red + * when required) above the control, optional hint/error below. `size` sets the tier for the whole field; `inline` puts the label beside the control for filter bars.\ndefineProps({ label: String, required: Boolean, hint: String, error: String, size: String, inline: Boolean, style: Object })\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ display: 'flex', flexDirection: inline ? 'row' : 'column', ...(inline ? { alignItems: 'center', gap: 'var(--control-gap)' } : null), ...style }\">\n <label :style=\"{ paddingBottom: inline ? 0 : '8px', whiteSpace: inline ? 'nowrap' : undefined, fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-body)' }\">{{ label }}<span v-if=\"required\" style=\"color:var(--danger)\"> *</span></label>\n <slot />\n <span v-if=\"error\" style=\"padding-top:5px;font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--danger)\">{{ error }}</span>\n <span v-else-if=\"hint\" style=\"padding-top:5px;font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-muted)\">{{ hint }}</span>\n </div>\n</template>\n","forms/Input.jsx":"import React from \"react\";\n\nimport { inputBase } from \"./input-base.js\";\nexport { inputBase };\n\n/** Text input, r8, tier height (sm 28 · md 32 · lg 40 — omit `size` to inherit the region's tier). readOnly = inset fill; disabled = 50% opacity; error = red border + ring. */\nexport function Input({ readOnly, disabled, error, size, style, ...rest }) {\n const [focus, setFocus] = React.useState(false);\n return <input readOnly={readOnly} disabled={disabled} data-size={size} data-field={readOnly || disabled ? undefined : \"\"} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}\n style={{ ...inputBase, ...(readOnly ? { background: \"var(--bg-inset)\", color: \"var(--text-secondary)\" } : null),\n ...(error ? { borderColor: \"var(--danger)\" } : null),\n ...(focus && !readOnly && !disabled ? { borderColor: error ? \"var(--danger)\" : \"var(--focus-border)\", boxShadow: `0 0 0 2px ${error ? \"var(--danger-bg)\" : \"var(--focus-ring)\"}` } : null),\n ...(disabled ? { opacity: .5, cursor: \"not-allowed\", background: \"var(--bg-inset)\" } : null), ...style }} {...rest} />;\n}\n\n/** Multiline textarea, r8, default 90px tall; text follows the region's tier. showCount + maxLength renders a bottom-right \"n/max\" counter (red past the limit). */\nexport function Textarea({ style, rows = 4, error, disabled, showCount, maxLength, size, value, defaultValue, onChange, ...rest }) {\n const [focus, setFocus] = React.useState(false);\n const [inner, setInner] = React.useState(defaultValue || \"\");\n const val = value != null ? value : inner;\n const len = (val || \"\").length;\n const over = maxLength != null && len > maxLength;\n return (\n <div style={{ position: \"relative\" }}>\n <textarea rows={rows} disabled={disabled} data-size={size} data-field={disabled ? undefined : \"\"} maxLength={showCount ? undefined : maxLength}\n value={value} defaultValue={value == null ? defaultValue : undefined}\n onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}\n onChange={(e) => { if (value == null) setInner(e.target.value); onChange && onChange(e); }}\n style={{ ...inputBase, height: 90, resize: \"vertical\", paddingTop: 8, paddingBottom: showCount ? 22 : 8,\n ...(error || over ? { borderColor: \"var(--danger)\" } : null),\n ...(focus && !disabled ? { borderColor: error || over ? \"var(--danger)\" : \"var(--focus-border)\", boxShadow: `0 0 0 2px ${error || over ? \"var(--danger-bg)\" : \"var(--focus-ring)\"}` } : null),\n ...(disabled ? { opacity: .5, cursor: \"not-allowed\", background: \"var(--bg-inset)\" } : null), ...style }} {...rest} />\n {showCount ? <span style={{ position: \"absolute\", right: 10, bottom: 7, fontFamily: \"var(--font-sans)\", fontVariantNumeric: \"tabular-nums\", fontSize: \"var(--text-xs)\", color: over ? \"var(--danger)\" : \"var(--text-muted)\" }}>{len}{maxLength != null ? `/${maxLength}` : \"\"}</span> : null}\n </div>\n );\n}\n","forms/Input.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { inputBase } from './input-base.js'\n// Text input, r8, tier height (sm 28 · md 32 · lg 40 — omit `size` to inherit the region's tier). v-model. readonly = inset fill; disabled = 50% opacity; error = red border + ring.\n// Multiline variant lives in Textarea.vue.\nconst props = defineProps({ modelValue: String, readonly: Boolean, disabled: Boolean, error: Boolean, size: String, style: Object })\ndefineEmits(['update:modelValue'])\nconst focus = ref(false)\nconst s = computed(() => ({ ...inputBase, ...(props.readonly ? { background: 'var(--bg-inset)', color: 'var(--text-secondary)' } : null),\n ...(props.error ? { borderColor: 'var(--danger)' } : null),\n ...(focus.value && !props.readonly && !props.disabled ? { borderColor: props.error ? 'var(--danger)' : 'var(--focus-border)', boxShadow: `0 0 0 2px ${props.error ? 'var(--danger-bg)' : 'var(--focus-ring)'}` } : null),\n ...(props.disabled ? { opacity: .5, cursor: 'not-allowed', background: 'var(--bg-inset)' } : null), ...props.style }))\n</script>\n<template>\n <input :value=\"modelValue\" @input=\"$emit('update:modelValue', $event.target.value)\" :readonly=\"readonly\" :disabled=\"disabled\" :data-size=\"size\" :data-field=\"readonly || disabled ? undefined : ''\" @focus=\"focus = true\" @blur=\"focus = false\" :style=\"s\" />\n</template>\n","forms/MultiSelect.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { Checkbox } from \"./Checkbox.jsx\";\nimport { Tag } from \"../display/Tag.jsx\";\n\nconst tierIcon = { width: \"var(--control-icon)\", height: \"var(--control-icon)\" };\n\n/** Multi-select dropdown: checkbox option list, selected values render as removable tags in the field (overflow collapses to \"+n\"), clear-all ×. Field, tags and option rows take the region's tier (omit `size` to inherit). */\nexport function MultiSelect({ options = [], value = [], placeholder = \"Select...\", maxTags = 2, disabled, width, size, onChange, style }) {\n const [open, setOpen] = React.useState(false);\n const root = React.useRef(null);\n React.useEffect(() => { const h = (e) => { if (root.current && !root.current.contains(e.target)) setOpen(false); }; document.addEventListener(\"mousedown\", h); return () => document.removeEventListener(\"mousedown\", h); }, []);\n const toggle = (o) => { const next = value.includes(o) ? value.filter(x => x !== o) : [...value, o]; onChange && onChange(next); };\n const shown = value.slice(0, maxTags), extra = value.length - shown.length;\n return (\n <div ref={root} data-size={size} style={{ position: \"relative\", width, ...style }}>\n <div data-field={disabled ? undefined : \"\"} onClick={() => !disabled && setOpen(!open)} style={{ minHeight: \"var(--control-h)\", borderRadius: \"var(--radius-md)\", border: `1px solid ${open ? \"var(--focus-border)\" : \"var(--field-border, var(--border-control))\"}`, boxShadow: open ? \"0 0 0 2px var(--focus-ring)\" : \"none\", background: \"var(--bg-surface)\", padding: \"calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)\", boxSizing: \"border-box\", display: \"flex\", alignItems: \"center\", flexWrap: \"wrap\", gap: 4, cursor: disabled ? \"not-allowed\" : \"pointer\", opacity: disabled ? .5 : 1, position: \"relative\" }}>\n {value.length === 0 && <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-muted)\", paddingLeft: 4 }}>{placeholder}</span>}\n {shown.map(v => <Tag key={v} removable onRemove={() => toggle(v)}>{v}</Tag>)}\n {extra > 0 && <Tag>+{extra}</Tag>}\n <span style={{ position: \"absolute\", right: \"var(--control-px)\", top: \"50%\", transform: \"translateY(-50%)\", display: \"flex\", alignItems: \"center\", gap: 4, color: \"var(--text-muted)\" }}>\n {value.length > 0 && <button type=\"button\" aria-label=\"Clear\" data-ink=\"\" onClick={(e) => { e.stopPropagation(); onChange && onChange([]); }} style={{ border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--ink, var(--text-muted))\", display: \"flex\", padding: 0 }}><Icon name=\"x\" size={16} style={tierIcon} /></button>}\n <span style={{ display: \"inline-flex\", transform: open ? \"rotate(180deg)\" : \"none\", transition: \"transform var(--dur-fast) var(--ease-out)\" }}><Icon name=\"chevron-down\" size={16} style={tierIcon} /></span>\n </span>\n </div>\n {open && <div style={{ position: \"absolute\", top: \"calc(100% + 4px)\", left: 0, right: 0, zIndex: \"var(--z-menu)\", background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: 4, maxHeight: 208, overflowY: \"auto\" }}>\n {options.map(o => <div key={o} role=\"option\" aria-selected={value.includes(o)} tabIndex={0} onClick={() => toggle(o)} style={{ display: \"flex\", alignItems: \"center\", gap: 8, height: \"var(--control-h)\", padding: \"0 10px\", boxSizing: \"border-box\", borderRadius: \"var(--radius-sm)\", cursor: \"pointer\", background: \"transparent\" }}\n onMouseEnter={(e) => e.currentTarget.style.background = \"var(--bg-hover)\"} onMouseLeave={(e) => e.currentTarget.style.background = \"transparent\"}>\n <Checkbox checked={value.includes(o)} onChange={() => toggle(o)} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-heading)\" }}>{o}</span>\n </div>)}\n </div>}\n </div>\n );\n}\n","forms/MultiSelect.vue":"<script setup>\nimport { ref, computed, onMounted, onUnmounted } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport Checkbox from './Checkbox.vue'\nimport Tag from '../display/Tag.vue'\n// Multi-select dropdown: checkbox options; picks render as removable tags with \"+n\" overflow and clear-all ×. Field, tags and option rows take the region's tier (omit `size` to inherit).\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: Array, default: () => [] }, placeholder: { type: String, default: 'Select...' }, maxTags: { type: Number, default: 2 }, disabled: Boolean, width: [Number, String], size: String, style: Object })\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst emit = defineEmits(['update:modelValue'])\nconst open = ref(false), root = ref(null)\nfunction toggle(o) { emit('update:modelValue', props.modelValue.includes(o) ? props.modelValue.filter(x => x !== o) : [...props.modelValue, o]) }\nconst shown = computed(() => props.modelValue.slice(0, props.maxTags))\nconst extra = computed(() => props.modelValue.length - shown.value.length)\nfunction onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }\nonMounted(() => document.addEventListener('mousedown', onDoc))\nonUnmounted(() => document.removeEventListener('mousedown', onDoc))\n</script>\n<template>\n <div ref=\"root\" :data-size=\"size\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div :data-field=\"disabled ? undefined : ''\" @click=\"!disabled && (open = !open)\" :style=\"{ minHeight: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: `1px solid ${open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))'}`, boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', background: 'var(--bg-surface)', padding: 'calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)', boxSizing: 'border-box', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, position: 'relative' }\">\n <span v-if=\"modelValue.length === 0\" style=\"font-family:var(--font-sans);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-muted);padding-left:4px\">{{ placeholder }}</span>\n <Tag v-for=\"v in shown\" :key=\"v\" removable @remove=\"toggle(v)\">{{ v }}</Tag>\n <Tag v-if=\"extra > 0\">+{{ extra }}</Tag>\n <span style=\"position:absolute;right:var(--control-px);top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:4px;color:var(--text-muted)\">\n <button v-if=\"modelValue.length\" type=\"button\" aria-label=\"Clear\" data-ink=\"\" @click.stop=\"emit('update:modelValue', [])\" style=\"border:none;background:transparent;cursor:pointer;color:var(--ink, var(--text-muted));display:flex;padding:0\"><Icon name=\"x\" :size=\"16\" :style=\"tierIcon\" /></button>\n <span :style=\"{ display: 'inline-flex', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }\"><Icon name=\"chevron-down\" :size=\"16\" :style=\"tierIcon\" /></span>\n </span>\n </div>\n <div v-if=\"open\" style=\"position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:var(--z-menu);background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;max-height:208px;overflow-y:auto\">\n <div v-for=\"o in options\" :key=\"o\" role=\"option\" :aria-selected=\"modelValue.includes(o)\" tabindex=\"0\" @click=\"toggle(o)\" class=\"kb-ms-row\" style=\"display:flex;align-items:center;gap:8px;height:var(--control-h);padding:0 10px;box-sizing:border-box;border-radius:var(--radius-sm);cursor:pointer\">\n <Checkbox :checked=\"modelValue.includes(o)\" @change=\"toggle(o)\" />\n <span style=\"font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);color:var(--text-heading)\">{{ o }}</span>\n </div>\n </div>\n </div>\n</template>\n<style scoped>.kb-ms-row:hover{background:var(--bg-hover)}</style>\n","forms/NumberStepper.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Segmented − / value / + stepper, r8, tier height (\"Preview rows\" — omit `size` to inherit the region's tier). Buttons disable themselves at min/max. */\nexport function NumberStepper({ value = 100, onChange, step = 10, min = 0, max, disabled, size, style }) {\n const atMin = value <= min, atMax = max != null && value >= max;\n const seg = (dis) => ({ width: \"var(--control-h)\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", cursor: dis ? \"not-allowed\" : \"pointer\", color: dis ? \"var(--text-muted)\" : \"var(--text-body)\", alignSelf: \"stretch\", background: dis ? \"transparent\" : \"var(--wash, transparent)\", transition: \"var(--t-hover)\", border: \"none\", padding: 0 });\n return (\n <div data-size={size} data-field={disabled ? undefined : \"\"} style={{ display: \"inline-flex\", alignItems: \"center\", height: \"var(--control-h)\", borderRadius: \"var(--radius-md)\", border: \"1px solid var(--field-border, var(--border-control))\", overflow: \"hidden\", background: \"var(--bg-surface)\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, ...style }}>\n <button type=\"button\" disabled={disabled || atMin} data-wash=\"\" aria-label=\"Decrease\" style={seg(disabled || atMin)} onClick={() => onChange && onChange(Math.max(min, value - step))}><Icon name=\"minus\" size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /></button>\n <span style={{ minWidth: 34, textAlign: \"center\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", fontVariantNumeric: \"tabular-nums\", color: \"var(--text-primary)\", borderLeft: \"1px solid var(--border-control)\", borderRight: \"1px solid var(--border-control)\", alignSelf: \"stretch\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", padding: \"0 6px\" }}>{value}</span>\n <button type=\"button\" disabled={disabled || atMax} data-wash=\"\" aria-label=\"Increase\" style={seg(disabled || atMax)} onClick={() => onChange && onChange(max != null ? Math.min(max, value + step) : value + step)}><Icon name=\"plus\" size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /></button>\n </div>\n );\n}\n","forms/NumberStepper.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Segmented − / value / + stepper, r8, tier height (\"Preview rows\" — omit `size` to inherit the region's tier). Buttons disable themselves at min/max.\nconst props = defineProps({ modelValue: { type: Number, default: 100 }, step: { type: Number, default: 10 }, min: { type: Number, default: 0 }, max: Number, disabled: Boolean, size: String, style: Object })\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst emit = defineEmits(['update:modelValue'])\nconst atMin = computed(() => props.modelValue <= props.min)\nconst atMax = computed(() => props.max != null && props.modelValue >= props.max)\nfunction seg(dis) { return { width: 'var(--control-h)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: dis ? 'not-allowed' : 'pointer', color: dis ? 'var(--text-muted)' : 'var(--text-body)', alignSelf: 'stretch', background: dis ? 'transparent' : 'var(--wash, transparent)', transition: 'var(--t-hover)', border: 'none', padding: 0 } }\n</script>\n<template>\n <div :data-size=\"size\" :data-field=\"disabled ? undefined : ''\" :style=\"{ display: 'inline-flex', alignItems: 'center', height: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: '1px solid var(--field-border, var(--border-control))', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }\">\n <button type=\"button\" :disabled=\"disabled || atMin\" data-wash=\"\" aria-label=\"Decrease\" :style=\"seg(disabled || atMin)\" @click=\"emit('update:modelValue', Math.max(min, modelValue - step))\"><Icon name=\"minus\" :size=\"16\" :style=\"tierIcon\" /></button>\n <span style=\"min-width:34px;text-align:center;font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);font-variant-numeric:tabular-nums;color:var(--text-primary);border-left:1px solid var(--border-control);border-right:1px solid var(--border-control);align-self:stretch;display:inline-flex;align-items:center;justify-content:center;padding:0 6px\">{{ modelValue }}</span>\n <button type=\"button\" :disabled=\"disabled || atMax\" data-wash=\"\" aria-label=\"Increase\" :style=\"seg(disabled || atMax)\" @click=\"emit('update:modelValue', max != null ? Math.min(max, modelValue + step) : modelValue + step)\"><Icon name=\"plus\" :size=\"16\" :style=\"tierIcon\" /></button>\n </div>\n</template>\n","forms/Radio.vue":"<script setup>\nimport { ref, computed } from 'vue'\n// Radio — circle at the region's tier (sm 14 · md/lg 16, --check-size), 1.5px border, filled dot when selected. Hover darkens the ring, focus shows the 2px ring, disabled = 50% opacity.\n// description renders a muted second line under the label.\nconst props = defineProps({ checked: Boolean, label: String, description: String, size: String, disabled: Boolean, style: Object })\nconst emit = defineEmits(['change'])\nconst d = { box: 'var(--check-size)', dot: 'calc(var(--check-size) / 2)' }\nconst hover = ref(false), focus = ref(false)\nconst ring = computed(() => props.checked ? 'var(--action-primary)' : hover.value && !props.disabled ? 'var(--text-muted)' : 'var(--checkbox-border)')\nconst pick = () => { if (!props.disabled) emit('change', true) }\n</script>\n<template>\n <label role=\"radio\" :data-size=\"size\" :aria-checked=\"checked\" :aria-disabled=\"disabled || undefined\" :tabindex=\"disabled ? -1 : 0\"\n @mouseenter=\"hover = true\" @mouseleave=\"hover = false\" @focus=\"focus = true\" @blur=\"focus = false\" @click=\"pick\" @keydown.space.prevent=\"pick\" @keydown.enter.prevent=\"pick\"\n :style=\"{ display: 'inline-flex', alignItems: description ? 'flex-start' : 'center', verticalAlign: 'middle', lineHeight: 'var(--control-leading)', gap: 'var(--control-gap)', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, outline: 'none', ...style }\">\n <span :style=\"{ width: d.box, height: d.box, marginTop: description ? '2px' : 0, borderRadius: 'var(--radius-full)', boxSizing: 'border-box', border: `1.5px solid ${ring}`, background: 'var(--bg-surface)', boxShadow: focus && !disabled ? '0 0 0 2px var(--focus-ring)' : 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, transition: 'var(--t-hover)' }\">\n <span v-if=\"checked\" :style=\"{ width: d.dot, height: d.dot, borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }\"></span>\n </span>\n <span v-if=\"label || description\" style=\"display:flex;flex-direction:column;gap:2px\">\n <span v-if=\"label\" style=\"font-family:var(--font-sans);font-weight:var(--weight-regular);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-body)\">{{ label }}</span>\n <span v-if=\"description\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-muted)\">{{ description }}</span>\n </span>\n </label>\n</template>\n","forms/RadioGroup.vue":"<script setup>\nimport { computed } from 'vue'\nimport Radio from './Radio.vue'\n// RadioGroup — single choice. Vertical (default) or `row`. options: string[] | { label, value, description?, disabled? }[].\n// Arrow keys move between enabled options. variant=\"cards\" renders bordered selectable cards. v-model.\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, row: Boolean, size: String, variant: { type: String, default: 'plain' }, disabled: Boolean, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst opts = computed(() => props.options.map(o => typeof o === 'string' ? { label: o, value: o } : o))\nconst enabled = computed(() => opts.value.filter(o => !o.disabled && !props.disabled))\nfunction onKey(e) {\n if (!['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft'].includes(e.key) || !enabled.value.length) return\n e.preventDefault()\n const i = enabled.value.findIndex(o => o.value === props.modelValue), n = enabled.value.length\n const next = (e.key === 'ArrowDown' || e.key === 'ArrowRight') ? enabled.value[(i + 1) % n] : enabled.value[(i - 1 + n) % n]\n emit('update:modelValue', next.value)\n}\nconst pick = (o) => { if (!(o.disabled || props.disabled)) emit('update:modelValue', o.value) }\n</script>\n<template>\n <div v-if=\"variant === 'cards'\" role=\"radiogroup\" :data-size=\"size\" @keydown=\"onKey\" :style=\"{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: '8px', ...style }\">\n <div v-for=\"o in opts\" :key=\"o.value\" @click=\"pick(o)\"\n :style=\"{ flex: row ? 1 : 'none', padding: '10px 12px', borderRadius: 'var(--radius-md)', border: `1px solid ${modelValue === o.value ? 'var(--action-primary)' : 'var(--border-card)'}`, background: modelValue === o.value ? 'var(--soft-bg)' : 'var(--bg-surface)', cursor: (o.disabled || disabled) ? 'not-allowed' : 'pointer', opacity: (o.disabled || disabled) ? .5 : 1, transition: 'var(--t-hover)' }\">\n <Radio :checked=\"modelValue === o.value\" :label=\"o.label\" :description=\"o.description\" :disabled=\"o.disabled || disabled\" :style=\"{ opacity: 1, cursor: 'inherit' }\" @change=\"pick(o)\" />\n </div>\n </div>\n <div v-else role=\"radiogroup\" :data-size=\"size\" @keydown=\"onKey\" :style=\"{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: row ? 'var(--group-gap)' : '10px', ...style }\">\n <Radio v-for=\"o in opts\" :key=\"o.value\" :label=\"o.label\" :description=\"o.description\" :disabled=\"o.disabled || disabled\" :checked=\"modelValue === o.value\" @change=\"pick(o)\" />\n </div>\n</template>\n","forms/SearchInput.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Bordered search field (r8, tier height — omit `size` to inherit the region's tier) with leading search glyph; bare=true renders the borderless top-bar flavor. Shows a clear (×) button once there's a value. */\nexport function SearchInput({ placeholder = \"Search...\", bare, size, value, onChange, disabled, style }) {\n const [inner, setInner] = React.useState(\"\");\n const val = value != null ? value : inner;\n const set = (v) => { if (value == null) setInner(v); onChange && onChange(v); };\n return (\n <div data-size={size} data-field={bare || disabled ? undefined : \"\"} style={{ display: \"flex\", alignItems: \"center\", gap: bare ? 12 : 8, height: \"var(--control-h)\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, ...(bare ? null : { border: \"1px solid var(--field-border, var(--border-control))\", borderRadius: \"var(--radius-md)\", padding: \"0 var(--control-px)\", background: \"var(--bg-surface)\" }), ...style }}>\n <span style={{ color: \"var(--text-muted)\", display: \"inline-flex\" }}><Icon name=\"search\" size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /></span>\n <input value={val} disabled={disabled} onChange={(e) => set(e.target.value)} placeholder={placeholder}\n style={{ border: \"none\", outline: \"none\", background: \"transparent\", flex: 1, minWidth: 0, padding: 0, fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-primary)\", cursor: disabled ? \"not-allowed\" : \"text\" }} />\n {val && !disabled ? <button type=\"button\" onClick={() => set(\"\")} aria-label=\"Clear\" data-ink=\"\" style={{ border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--ink, var(--text-muted))\", display: \"flex\", padding: 0 }}><Icon name=\"x\" size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /></button> : null}\n </div>\n );\n}\n","forms/SearchInput.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Bordered search field (r8, tier height — omit `size` to inherit the region's tier) with leading search glyph; bare=true renders the borderless top-bar flavor. Shows a clear (×) button once there's a value.\ndefineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Search...' }, bare: Boolean, size: String, disabled: Boolean, style: Object })\nconst tierIcon = { width: 'var(--control-icon)', height: 'var(--control-icon)' }\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :data-size=\"size\" :data-field=\"bare || disabled ? undefined : ''\" :style=\"{ display: 'flex', alignItems: 'center', gap: bare ? '12px' : '8px', height: 'var(--control-h)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...(bare ? null : { border: '1px solid var(--field-border, var(--border-control))', borderRadius: 'var(--radius-md)', padding: '0 var(--control-px)', background: 'var(--bg-surface)' }), ...style }\">\n <span style=\"color:var(--text-muted);display:inline-flex\"><Icon name=\"search\" :size=\"16\" :style=\"tierIcon\" /></span>\n <input :value=\"modelValue\" :disabled=\"disabled\" @input=\"emit('update:modelValue', $event.target.value)\" :placeholder=\"placeholder\"\n :style=\"{ border: 'none', outline: 'none', background: 'transparent', flex: 1, minWidth: 0, padding: 0, fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-primary)', cursor: disabled ? 'not-allowed' : 'text' }\" />\n <button v-if=\"modelValue && !disabled\" type=\"button\" aria-label=\"Clear\" data-ink=\"\" @click=\"emit('update:modelValue', '')\" style=\"border:none;background:transparent;cursor:pointer;color:var(--ink, var(--text-muted));display:flex;padding:0\"><Icon name=\"x\" :size=\"16\" :style=\"tierIcon\" /></button>\n </div>\n</template>\n","forms/Select.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Native select styled per Keybricks: r8, tier height (sm 28 · md 32 · lg 40 — omit `size` to inherit the region's tier). disabled = inset fill (matches Input readOnly), 50% opacity; error = red border. */\nexport function Select({ options = [], size, value, onChange, placeholder, disabled, error, style }) {\n return (\n <div data-size={size} style={{ position: \"relative\", display: \"inline-flex\", ...style }}>\n <select value={value} onChange={onChange} disabled={disabled} data-field={disabled ? undefined : \"\"}\n style={{ appearance: \"none\", WebkitAppearance: \"none\", height: \"var(--control-h)\", borderRadius: \"var(--radius-md)\", border: `1px solid ${error ? \"var(--danger)\" : \"var(--field-border, var(--border-control))\"}`, background: disabled ? \"var(--bg-inset)\" : \"var(--bg-surface)\", padding: \"0 calc(var(--control-px) + var(--control-icon) + 6px) 0 var(--control-px)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", letterSpacing: \"var(--control-tracking)\", color: disabled ? \"var(--text-secondary)\" : value ? \"var(--text-primary)\" : \"var(--text-muted)\", outline: \"none\", cursor: disabled ? \"not-allowed\" : \"pointer\", width: \"100%\", opacity: disabled ? .5 : 1 }}>\n {placeholder ? <option value=\"\" disabled hidden>{placeholder}</option> : null}\n {options.map((o) => <option key={o} value={o}>{o}</option>)}\n </select>\n <span style={{ position: \"absolute\", right: \"var(--control-px)\", top: \"50%\", transform: \"translateY(-50%)\", pointerEvents: \"none\", color: \"var(--text-muted)\", display: \"inline-flex\" }}><Icon name=\"chevron-down\" size={16} style={{ width: \"var(--control-icon)\", height: \"var(--control-icon)\" }} /></span>\n </div>\n );\n}\n","forms/Select.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Native select styled per Keybricks: r8, tier height (sm 28 · md 32 · lg 40 — omit `size` to inherit the region's tier). disabled = inset fill (matches Input readonly), 50% opacity; error = red border.\nconst props = defineProps({ modelValue: String, options: { type: Array, default: () => [] }, size: String, placeholder: String, disabled: Boolean, error: Boolean, style: Object })\ndefineEmits(['update:modelValue'])\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ position: 'relative', display: 'inline-flex', ...style }\">\n <select :value=\"modelValue\" @change=\"$emit('update:modelValue', $event.target.value)\" :disabled=\"disabled\" :data-field=\"disabled ? undefined : ''\"\n :style=\"{ appearance: 'none', height: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: `1px solid ${error ? 'var(--danger)' : 'var(--field-border, var(--border-control))'}`, background: disabled ? 'var(--bg-inset)' : 'var(--bg-surface)', padding: '0 calc(var(--control-px) + var(--control-icon) + 6px) 0 var(--control-px)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', letterSpacing: 'var(--control-tracking)', color: disabled ? 'var(--text-secondary)' : modelValue ? 'var(--text-primary)' : 'var(--text-muted)', outline: 'none', cursor: disabled ? 'not-allowed' : 'pointer', width: '100%', opacity: disabled ? .5 : 1 }\">\n <option v-if=\"placeholder\" value=\"\" disabled hidden>{{ placeholder }}</option>\n <option v-for=\"o in options\" :key=\"o\" :value=\"o\">{{ o }}</option>\n </select>\n <span :style=\"{ position: 'absolute', right: 'var(--control-px)', top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none', color: 'var(--text-muted)', display: 'inline-flex' }\"><Icon name=\"chevron-down\" :size=\"16\" :style=\"{ width: 'var(--control-icon)', height: 'var(--control-icon)' }\" /></span>\n </div>\n</template>\n","forms/Slider.jsx":"import React from \"react\";\n\n/** Range slider: blue filled track, 16px knob, optional min/max labels and a value bubble while dragging (or always with showValue). */\nexport function Slider({ value = 50, min = 0, max = 100, step = 1, labels, showValue, unit = \"\", disabled, width, onChange, style }) {\n const [drag, setDrag] = React.useState(false);\n const pct = ((value - min) / (max - min)) * 100;\n const track = React.useRef(null);\n const set = (clientX) => { const r = track.current.getBoundingClientRect(); const p = Math.min(1, Math.max(0, (clientX - r.left) / r.width)); const raw = min + p * (max - min); const snapped = Math.round(raw / step) * step; onChange && onChange(Math.min(max, Math.max(min, snapped))); };\n React.useEffect(() => { if (!drag) return; const mv = (e) => set(e.clientX), up = () => setDrag(false); document.addEventListener(\"mousemove\", mv); document.addEventListener(\"mouseup\", up); return () => { document.removeEventListener(\"mousemove\", mv); document.removeEventListener(\"mouseup\", up); }; }, [drag]);\n return (\n <div style={{ width, opacity: disabled ? .5 : 1, ...style }}>\n <div ref={track} data-thumb={disabled ? undefined : \"\"} onMouseDown={disabled ? undefined : (e) => { setDrag(true); set(e.clientX); }} style={{ position: \"relative\", height: 20, display: \"flex\", alignItems: \"center\", cursor: disabled ? \"not-allowed\" : \"pointer\" }}>\n <div style={{ position: \"absolute\", left: 0, right: 0, height: 6, borderRadius: \"var(--radius-full)\", background: \"var(--track)\" }}></div>\n <div style={{ position: \"absolute\", left: 0, width: pct + \"%\", height: 6, borderRadius: \"var(--radius-full)\", background: \"var(--action-primary)\" }}></div>\n <span style={{ position: \"absolute\", left: pct + \"%\", transform: \"translateX(-50%)\", width: 16, height: 16, borderRadius: \"var(--radius-full)\", background: \"var(--bg-surface)\", border: \"2px solid var(--action-primary)\", boxSizing: \"border-box\", boxShadow: drag ? \"0 0 0 4px var(--focus-ring), 0 1px 2px rgba(0,0,0,.15)\" : \"var(--thumb-ring, 0 1px 2px rgba(0,0,0,.15))\", transition: \"box-shadow var(--dur-fast) var(--ease-out)\" }}></span>\n {(drag || showValue) && <span style={{ position: \"absolute\", left: pct + \"%\", transform: \"translateX(-50%)\", bottom: 22, background: \"var(--tooltip-bg)\", color: \"var(--tooltip-fg)\", borderRadius: \"var(--radius-sm)\", padding: \"2px 6px\", fontFamily: \"var(--font-sans)\", fontVariantNumeric: \"tabular-nums\", fontSize: \"var(--text-xs)\", whiteSpace: \"nowrap\", pointerEvents: \"none\" }}>{value}{unit}</span>}\n </div>\n {labels && <div style={{ display: \"flex\", justifyContent: \"space-between\", paddingTop: 4, fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-muted)\" }}><span>{min}{unit}</span><span>{max}{unit}</span></div>}\n </div>\n );\n}\n","forms/Slider.vue":"<script setup>\nimport { ref, computed, onUnmounted, watch } from 'vue'\n// Range slider: blue filled track, 16px knob, drag value bubble, optional min/max labels.\nconst props = defineProps({ modelValue: { type: Number, default: 50 }, min: { type: Number, default: 0 }, max: { type: Number, default: 100 }, step: { type: Number, default: 1 }, labels: Boolean, showValue: Boolean, unit: { type: String, default: '' }, disabled: Boolean, width: [Number, String], style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst drag = ref(false), track = ref(null)\nconst pct = computed(() => ((props.modelValue - props.min) / (props.max - props.min)) * 100)\nfunction set(clientX) { const r = track.value.getBoundingClientRect(); const p = Math.min(1, Math.max(0, (clientX - r.left) / r.width)); const raw = props.min + p * (props.max - props.min); emit('update:modelValue', Math.min(props.max, Math.max(props.min, Math.round(raw / props.step) * props.step))) }\nconst mv = (e) => set(e.clientX), up = () => drag.value = false\nwatch(drag, (on) => { if (on) { document.addEventListener('mousemove', mv); document.addEventListener('mouseup', up) } else { document.removeEventListener('mousemove', mv); document.removeEventListener('mouseup', up) } })\nonUnmounted(up)\n</script>\n<template>\n <div :style=\"{ width: typeof width === 'number' ? width + 'px' : width, opacity: disabled ? .5 : 1, ...style }\">\n <div ref=\"track\" :data-thumb=\"disabled ? undefined : ''\" @mousedown=\"!disabled && (drag = true, set($event.clientX))\" :style=\"{ position: 'relative', height: '20px', display: 'flex', alignItems: 'center', cursor: disabled ? 'not-allowed' : 'pointer' }\">\n <div style=\"position:absolute;left:0;right:0;height:6px;border-radius:var(--radius-full);background:var(--track)\" />\n <div :style=\"{ position: 'absolute', left: 0, width: pct + '%', height: '6px', borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }\" />\n <span :style=\"{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', width: '16px', height: '16px', borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', border: '2px solid var(--action-primary)', boxSizing: 'border-box', boxShadow: drag ? '0 0 0 4px var(--focus-ring), 0 1px 2px rgba(0,0,0,.15)' : 'var(--thumb-ring, 0 1px 2px rgba(0,0,0,.15))', transition: 'box-shadow var(--dur-fast) var(--ease-out)' }\" />\n <span v-if=\"drag || showValue\" :style=\"{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', bottom: '22px', background: 'var(--tooltip-bg)', color: 'var(--tooltip-fg)', borderRadius: 'var(--radius-sm)', padding: '2px 6px', fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums', fontSize: 'var(--text-xs)', whiteSpace: 'nowrap', pointerEvents: 'none' }\">{{ modelValue }}{{ unit }}</span>\n </div>\n <div v-if=\"labels\" style=\"display:flex;justify-content:space-between;padding-top:4px;font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-muted)\"><span>{{ min }}{{ unit }}</span><span>{{ max }}{{ unit }}</span></div>\n </div>\n</template>\n","forms/Switch.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/**\n * Switch — pill track sized by the region's tier (sm 28×16 · md 36×20 · lg 44×24), knob 4px smaller, 160ms ease-out slide.\n * Track, knob, glyph and label all read the tier tokens (tokens/layout.css), so a Switch in a form row matches its neighbours.\n *\n * checked / onChange(next) controlled boolean\n * disabled 50% opacity, no pointer\n * size \"sm\" | \"md\" | \"lg\" — omit to inherit the region's tier; lg is roomy enough for knobIcon / knobText\n * tone \"primary\" (default, action blue) | \"semantic\" (red off → green on)\n * onColor / offColor explicit track colors; override tone\n * knobIcon / knobText glyph inside the knob: string | { on, off }. knobText is ONE letter or ONE CJK character.\n * insideOn / insideOff text inside the pill on the far side of the knob (short — \"ON\"/\"OFF\", \"开\"/\"关\", \"Y\"/\"N\")\n * labelOn / labelOff captions beside the pill; active caption takes text-heading, inactive takes text-muted\n * loading controlled: knob shows a spinner, input is inert\n * beforeChange(next) => Promise|boolean\n * async gate — Switch enters loading, flips only if the promise resolves truthy\n * inlinePrompt (Vue-friendly alias of insideOn/insideOff pair {on,off})\n */\n\n/* Geometry in CSS so it follows the tier without JavaScript: knob = track height − 4, glyph = half the track height. */\nconst H = \"var(--switch-h)\", W = \"var(--switch-w)\", KNOB = \"calc(var(--switch-h) - 4px)\", GLYPH = \"calc(var(--switch-h) * .5)\", TEXT = \"max(calc(var(--switch-h) * .5), var(--text-glyph-min))\" /* text never drops below the glyph floor (12px in CJK) */;\n\nconst pick = (v, on) => (v && typeof v === \"object\" ? (on ? v.on : v.off) : v);\n\nexport function Switch({ checked = false, onChange, disabled, size, tone = \"primary\", onColor, offColor, knobIcon, knobText, insideOn, insideOff, inlinePrompt, label, labelOn, labelOff, loading: loadingProp, beforeChange, style }) {\n // `label` = single caption to the right, shown in text-body regardless of state (settings-row usage)\n const single = label != null && labelOn == null && labelOff == null;\n const [busy, setBusy] = React.useState(false);\n const alive = React.useRef(true);\n React.useEffect(() => () => { alive.current = false; }, []);\n const loading = loadingProp || busy;\n if (inlinePrompt) { insideOn = insideOn ?? inlinePrompt.on; insideOff = insideOff ?? inlinePrompt.off; }\n const hasInside = insideOn != null || insideOff != null;\n // inside text needs a wider track so the knob never covers the word: text runs at half the track height, ~.68em per character\n const insideChars = hasInside ? Math.max(String(insideOn ?? \"\").length, String(insideOff ?? \"\").length) : 0;\n const w = hasInside ? `max(${W}, calc(${H} * ${(1 + insideChars * .34).toFixed(2)} + 6px))` : W;\n\n const onTrack = onColor || (tone === \"semantic\" ? \"var(--success)\" : \"var(--action-primary)\");\n const offTrack = offColor || (tone === \"semantic\" ? \"var(--danger)\" : \"var(--border-strong)\");\n const track = checked ? onTrack : offTrack;\n const inert = disabled || loading;\n\n const flip = async () => {\n if (inert) return;\n const next = !checked;\n if (beforeChange) {\n setBusy(true);\n let ok = false;\n try { ok = await beforeChange(next); } catch (e) { ok = false; }\n if (!alive.current) return;\n setBusy(false);\n if (!ok) return;\n }\n onChange && onChange(next);\n };\n\n const glyphIcon = pick(knobIcon, checked);\n const glyphText = pick(knobText, checked);\n const knobX = checked ? `calc(100% - ${H} + 2px)` : 2;\n\n const knob = <span aria-hidden=\"true\" style={{ position: \"absolute\", top: 2, left: knobX, width: KNOB, height: KNOB, borderRadius: \"var(--radius-full)\", background: \"var(--bg-surface)\", boxShadow: \"0 1px 2px rgba(0,0,0,.18)\", transition: \"left 160ms ease-out\", display: \"flex\", alignItems: \"center\", justifyContent: \"center\", color: track, fontSize: TEXT, lineHeight: 1, fontWeight: \"var(--weight-semibold)\", fontFamily: \"var(--font-sans)\", overflow: \"hidden\" }}>\n {loading\n ? <span style={{ width: GLYPH, height: GLYPH, borderRadius: \"50%\", border: `1.5px solid ${track}`, borderRightColor: \"transparent\", animation: \"kb-spin 720ms linear infinite\" }}></span>\n : glyphIcon ? <Icon name={glyphIcon} size={10} strokeWidth={2.5} style={{ width: GLYPH, height: GLYPH }} />\n : glyphText ? <span>{String(glyphText).slice(0, 1)}</span> : null}\n </span>;\n\n const insideStyle = (side) => ({ position: \"absolute\", top: 0, bottom: 0, [side]: 6, display: \"flex\", alignItems: \"center\", fontSize: TEXT, lineHeight: 1, fontWeight: \"var(--weight-semibold)\", fontFamily: \"var(--font-sans)\", letterSpacing: \".2px\", color: \"var(--on-accent)\", transition: \"opacity 160ms ease-out\", pointerEvents: \"none\", whiteSpace: \"nowrap\" });\n\n const pill = <span role=\"switch\" aria-checked={checked} aria-busy={loading || undefined} aria-disabled={disabled || undefined} tabIndex={inert ? -1 : 0} onClick={flip}\n onKeyDown={e => { if (e.key === \" \" || e.key === \"Enter\") { e.preventDefault(); flip(); } }}\n style={{ position: \"relative\", display: \"inline-block\", width: w, height: H, borderRadius: \"var(--radius-full)\", \"--sw-track\": track, background: \"var(--sw-bg, var(--sw-track))\", transition: \"background 160ms ease-out\", cursor: inert ? (loading ? \"progress\" : \"not-allowed\") : \"pointer\", flexShrink: 0, verticalAlign: \"middle\", outline: \"none\" }}>\n {hasInside && <span style={{ ...insideStyle(\"left\"), opacity: checked ? 1 : 0 }}>{insideOn}</span>}\n {hasInside && <span style={{ ...insideStyle(\"right\"), opacity: checked ? 0 : 1 }}>{insideOff}</span>}\n {knob}\n </span>;\n\n const capBase = { fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", fontFamily: \"var(--font-sans)\", cursor: inert ? \"default\" : \"pointer\", userSelect: \"none\" };\n if (single) return <span data-size={size} data-sw=\"\" style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"var(--control-gap)\", opacity: disabled ? .5 : 1, ...style }}>{pill}<span onClick={flip} style={{ ...capBase, fontWeight: \"var(--weight-regular)\", color: \"var(--text-body)\" }}>{label}</span></span>;\n if (labelOn == null && labelOff == null) return <span data-size={size} style={{ display: \"inline-flex\", alignItems: \"center\", opacity: disabled ? .5 : 1, ...style }}>{pill}</span>;\n\n const cap = (text, active) => text == null ? null : <span style={{ ...capBase, fontWeight: active ? \"var(--weight-medium)\" : \"var(--weight-regular)\", color: active ? \"var(--text-heading)\" : \"var(--text-muted)\", transition: \"color 160ms ease-out\" }} onClick={flip}>{text}</span>;\n return <span data-size={size} data-sw=\"\" style={{ display: \"inline-flex\", alignItems: \"center\", gap: \"var(--control-gap)\", opacity: disabled ? .5 : 1, ...style }}>\n {cap(labelOff, !checked)}{pill}{cap(labelOn, checked)}\n </span>;\n}\n\nexport default Switch;\n","forms/Switch.vue":"<script setup>\nimport { ref, computed, onBeforeUnmount } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Switch — pill track sized by the region's tier (sm 28×16 · md 36×20 · lg 44×24), knob 4px smaller, 160ms slide. v-model:checked.\n// Geometry is CSS (tokens/layout.css --switch-w/-h), so it follows the tier without JavaScript; `size` stamps data-size to override.\n// tone \"primary\" | \"semantic\" (red off → green on); onColor/offColor override.\n// knobIcon / knobText: string | { on, off } — knobText is ONE letter or ONE CJK char.\n// insideOn / insideOff: text inside the pill opposite the knob. labelOn / labelOff: captions beside the pill.\n// loading: controlled spinner. beforeChange(next) → Promise|boolean gates the flip and shows loading meanwhile.\nconst props = defineProps({\n checked: Boolean, disabled: Boolean, loading: Boolean,\n size: String, tone: { type: String, default: 'primary' },\n onColor: String, offColor: String,\n knobIcon: [String, Object], knobText: [String, Object],\n insideOn: String, insideOff: String, inlinePrompt: Object, label: String, labelOn: String, labelOff: String,\n beforeChange: Function, style: Object\n})\nconst emit = defineEmits(['update:checked', 'change'])\nconst H = 'var(--switch-h)', W = 'var(--switch-w)', KNOB = 'calc(var(--switch-h) - 4px)', GLYPH = 'calc(var(--switch-h) * .5)', TEXT = 'max(calc(var(--switch-h) * .5), var(--text-glyph-min))' /* text never drops below the glyph floor (12px in CJK) */\nconst busy = ref(false); let alive = true; onBeforeUnmount(() => { alive = false })\nconst isLoading = computed(() => props.loading || busy.value)\nconst inert = computed(() => props.disabled || isLoading.value)\n// inlinePrompt { on, off } is an alias of the insideOn / insideOff pair (same as Switch.jsx)\nconst inOn = computed(() => props.insideOn ?? (props.inlinePrompt ? props.inlinePrompt.on : undefined))\nconst inOff = computed(() => props.insideOff ?? (props.inlinePrompt ? props.inlinePrompt.off : undefined))\nconst hasInside = computed(() => inOn.value != null || inOff.value != null)\n// inside text needs a wider track so the knob never covers the word: text runs at half the track height, ~.68em per character\nconst width = computed(() => { if (!hasInside.value) return W; const n = Math.max((inOn.value || '').length, (inOff.value || '').length); return `max(${W}, calc(${H} * ${(1 + n * .34).toFixed(2)} + 6px))` })\nconst track = computed(() => props.checked ? (props.onColor || (props.tone === 'semantic' ? 'var(--success)' : 'var(--action-primary)')) : (props.offColor || (props.tone === 'semantic' ? 'var(--danger)' : 'var(--border-strong)')))\nconst pick = (v) => v && typeof v === 'object' ? (props.checked ? v.on : v.off) : v\nconst glyphIcon = computed(() => pick(props.knobIcon))\nconst glyphText = computed(() => { const t = pick(props.knobText); return t ? String(t).slice(0, 1) : '' })\nconst knobX = computed(() => props.checked ? `calc(100% - ${H} + 2px)` : '2px')\nasync function flip() {\n if (inert.value) return\n const next = !props.checked\n if (props.beforeChange) {\n busy.value = true; let ok = false\n try { ok = await props.beforeChange(next) } catch (e) { ok = false }\n if (!alive) return\n busy.value = false\n if (!ok) return\n }\n emit('update:checked', next); emit('change', next)\n}\nconst insideStyle = (side, on) => ({ position: 'absolute', top: 0, bottom: 0, [side]: '6px', display: 'flex', alignItems: 'center', fontSize: TEXT, lineHeight: 1, fontWeight: 'var(--weight-semibold)', fontFamily: 'var(--font-sans)', letterSpacing: '.2px', color: 'var(--on-accent)', transition: 'opacity 160ms ease-out', pointerEvents: 'none', whiteSpace: 'nowrap', opacity: on === props.checked ? 1 : 0 })\nconst cap = (active) => ({ fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', fontFamily: 'var(--font-sans)', fontWeight: active ? 'var(--weight-medium)' : 'var(--weight-regular)', color: active ? 'var(--text-heading)' : 'var(--text-muted)', transition: 'color 160ms ease-out', cursor: inert.value ? 'default' : 'pointer', userSelect: 'none' })\n</script>\n<template>\n <span :data-size=\"size\" data-sw=\"\" :style=\"{ display: 'inline-flex', alignItems: 'center', gap: 'var(--control-gap)', opacity: disabled ? .5 : 1, ...style }\">\n <span v-if=\"labelOff != null\" :style=\"cap(!checked)\" @click=\"flip\">{{ labelOff }}</span>\n <span role=\"switch\" :aria-checked=\"checked\" :aria-busy=\"isLoading || undefined\" :aria-disabled=\"disabled || undefined\" :tabindex=\"inert ? -1 : 0\" @click=\"flip\" @keydown.space.prevent=\"flip\" @keydown.enter.prevent=\"flip\"\n :style=\"{ position: 'relative', display: 'inline-block', width: width, height: H, borderRadius: 'var(--radius-full)', '--sw-track': track, background: 'var(--sw-bg, var(--sw-track))', transition: 'background 160ms ease-out', cursor: inert ? (isLoading ? 'progress' : 'not-allowed') : 'pointer', flexShrink: 0, verticalAlign: 'middle', outline: 'none' }\">\n <span v-if=\"hasInside\" :style=\"insideStyle('left', true)\">{{ inOn }}</span>\n <span v-if=\"hasInside\" :style=\"insideStyle('right', false)\">{{ inOff }}</span>\n <span aria-hidden=\"true\" :style=\"{ position: 'absolute', top: '2px', left: knobX, width: KNOB, height: KNOB, borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', boxShadow: '0 1px 2px rgba(0,0,0,.18)', transition: 'left 160ms ease-out', display: 'flex', alignItems: 'center', justifyContent: 'center', color: track, fontSize: TEXT, lineHeight: 1, fontWeight: 'var(--weight-semibold)', fontFamily: 'var(--font-sans)', overflow: 'hidden' }\">\n <span v-if=\"isLoading\" :style=\"{ width: GLYPH, height: GLYPH, borderRadius: '50%', border: '1.5px solid ' + track, borderRightColor: 'transparent', animation: 'kb-spin 720ms linear infinite' }\"></span>\n <Icon v-else-if=\"glyphIcon\" :name=\"glyphIcon\" :size=\"10\" :stroke-width=\"2.5\" :style=\"{ width: GLYPH, height: GLYPH }\" />\n <span v-else-if=\"glyphText\">{{ glyphText }}</span>\n </span>\n </span>\n <span v-if=\"labelOn != null\" :style=\"cap(checked)\" @click=\"flip\">{{ labelOn }}</span>\n <span v-if=\"label != null && labelOn == null && labelOff == null\" :style=\"{ fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-regular)', color: 'var(--text-body)', cursor: inert ? 'default' : 'pointer', userSelect: 'none' }\" @click=\"flip\">{{ label }}</span>\n </span>\n</template>\n","forms/Textarea.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { inputBase } from './input-base.js'\n// Multiline textarea, r8, default 90px tall; text follows the region's tier. v-model. showCount + maxLength renders a bottom-right \"n/max\" counter (red past the limit).\nconst props = defineProps({ modelValue: String, rows: { type: Number, default: 4 }, error: Boolean, disabled: Boolean, showCount: Boolean, maxLength: Number, size: String, style: Object })\ndefineEmits(['update:modelValue'])\nconst focus = ref(false)\nconst len = computed(() => (props.modelValue || '').length)\nconst over = computed(() => props.maxLength != null && len.value > props.maxLength)\nconst s = computed(() => ({ ...inputBase, height: '90px', resize: 'vertical', paddingTop: '8px', paddingBottom: props.showCount ? '22px' : '8px',\n ...((props.error || over.value) ? { borderColor: 'var(--danger)' } : null),\n ...(focus.value && !props.disabled ? { borderColor: (props.error || over.value) ? 'var(--danger)' : 'var(--focus-border)', boxShadow: `0 0 0 2px ${(props.error || over.value) ? 'var(--danger-bg)' : 'var(--focus-ring)'}` } : null),\n ...(props.disabled ? { opacity: .5, cursor: 'not-allowed', background: 'var(--bg-inset)' } : null), ...props.style }))\n</script>\n<template>\n <div style=\"position:relative\">\n <textarea :rows=\"rows\" :disabled=\"disabled\" :data-size=\"size\" :data-field=\"disabled ? undefined : ''\" :maxlength=\"showCount ? undefined : maxLength\" :value=\"modelValue\" @input=\"$emit('update:modelValue', $event.target.value)\" @focus=\"focus = true\" @blur=\"focus = false\" :style=\"s\"></textarea>\n <span v-if=\"showCount\" :style=\"{ position: 'absolute', right: '10px', bottom: '7px', fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums', fontSize: 'var(--text-xs)', color: over ? 'var(--danger)' : 'var(--text-muted)' }\">{{ len }}{{ maxLength != null ? `/${maxLength}` : '' }}</span>\n </div>\n</template>\n","forms/input-base.js":"// Shared base style for text-like controls (Input, Textarea, Combobox, DatePicker).\n// Lives in a plain module because <script setup> cannot export values.\n// Height, text and padding come from the size tier of the enclosing region (tokens/layout.css): sm 28 · md 32 · lg 40.\nexport const inputBase = { height: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: '1px solid var(--field-border, var(--border-control))', background: 'var(--bg-surface)', padding: '0 var(--control-px)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: 'var(--text-primary)', outline: 'none', boxSizing: 'border-box', width: '100%' }\n","forms/Radio.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/**\n * Radio — circle at the region's tier (sm 14 · md/lg 16, --check-size), 1.5px border, filled action-blue dot when selected.\n * hover darkens the ring; keyboard focus shows the 2px focus ring; disabled = 50% opacity.\n * description renders a muted second line under the label (settings-style choices).\n */\nexport function Radio({ checked, onChange, label, description, size, disabled, style }) {\n const d = { box: \"var(--check-size)\", dot: \"calc(var(--check-size) / 2)\" };\n const [hover, setHover] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const ring = checked ? \"var(--action-primary)\" : hover && !disabled ? \"var(--text-muted)\" : \"var(--checkbox-border)\";\n return (\n <label role=\"radio\" data-size={size} aria-checked={!!checked} aria-disabled={disabled || undefined} tabIndex={disabled ? -1 : 0}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}\n onKeyDown={(e) => { if (e.key === \" \" || e.key === \"Enter\") { e.preventDefault(); !disabled && onChange && onChange(true); } }}\n style={{ display: \"inline-flex\", alignItems: description ? \"flex-start\" : \"center\", verticalAlign: \"middle\", lineHeight: \"var(--control-leading)\", gap: \"var(--control-gap)\", cursor: disabled ? \"not-allowed\" : \"pointer\", opacity: disabled ? .5 : 1, outline: \"none\", ...style }}\n onClick={() => !disabled && onChange && onChange(true)}>\n <span style={{ width: d.box, height: d.box, marginTop: description ? 2 : 0, borderRadius: \"var(--radius-full)\", boxSizing: \"border-box\", border: `1.5px solid ${ring}`, background: \"var(--bg-surface)\", boxShadow: focus && !disabled ? \"0 0 0 2px var(--focus-ring)\" : \"none\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", flexShrink: 0, transition: \"var(--t-hover)\" }}>\n {checked ? <span style={{ width: d.dot, height: d.dot, borderRadius: \"var(--radius-full)\", background: \"var(--action-primary)\" }}></span> : null}\n </span>\n {label || description ? <span style={{ display: \"flex\", flexDirection: \"column\", gap: 2 }}>\n {label ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-regular)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", color: \"var(--text-body)\" }}>{label}</span> : null}\n {description ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"var(--leading-xs)\", color: \"var(--text-muted)\" }}>{description}</span> : null}\n </span> : null}\n </label>\n );\n}\n\n/**\n * RadioGroup — single choice from a small set. Vertical (default) or `row`.\n * options: string[] | { label, value, description?, disabled? }[]\n * Arrow keys move the selection between enabled options (WAI-ARIA radiogroup).\n * variant=\"cards\" renders each option as a bordered selectable card (plan pickers, migration strategies).\n */\nexport function RadioGroup({ options = [], value, onChange, row, size, variant = \"plain\", disabled, style }) {\n const opts = options.map((o) => (typeof o === \"string\" ? { label: o, value: o } : o));\n const enabled = opts.filter((o) => !o.disabled && !disabled);\n const onKey = (e) => {\n if (![\"ArrowDown\", \"ArrowRight\", \"ArrowUp\", \"ArrowLeft\"].includes(e.key) || !enabled.length) return;\n e.preventDefault();\n const i = enabled.findIndex((o) => o.value === value);\n const next = e.key === \"ArrowDown\" || e.key === \"ArrowRight\" ? enabled[(i + 1) % enabled.length] : enabled[(i - 1 + enabled.length) % enabled.length];\n onChange && onChange(next.value);\n };\n if (variant === \"cards\") {\n return (\n <div role=\"radiogroup\" data-size={size} onKeyDown={onKey} style={{ display: \"flex\", flexDirection: row ? \"row\" : \"column\", gap: 8, ...style }}>\n {opts.map((o) => {\n const on = value === o.value, off = o.disabled || disabled;\n return <div key={o.value} onClick={() => !off && onChange && onChange(o.value)}\n style={{ flex: row ? 1 : \"none\", padding: \"10px 12px\", borderRadius: \"var(--radius-md)\", border: `1px solid ${on ? \"var(--action-primary)\" : \"var(--border-card)\"}`, background: on ? \"var(--soft-bg)\" : \"var(--bg-surface)\", cursor: off ? \"not-allowed\" : \"pointer\", opacity: off ? .5 : 1, transition: \"var(--t-hover)\" }}>\n <Radio checked={on} onChange={() => onChange && onChange(o.value)} label={o.label} description={o.description} disabled={off} style={{ opacity: 1, cursor: \"inherit\" }} />\n </div>;\n })}\n </div>\n );\n }\n return (\n <div role=\"radiogroup\" data-size={size} onKeyDown={onKey} style={{ display: \"flex\", flexDirection: row ? \"row\" : \"column\", gap: row ? \"var(--group-gap)\" : 10, ...style }}>\n {opts.map((o) => <Radio key={o.value} label={o.label} description={o.description} disabled={o.disabled || disabled} checked={value === o.value} onChange={() => onChange && onChange(o.value)} />)}\n </div>\n );\n}\n","forms/RelativeTimePicker.jsx":"import React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { Icon } from \"../icons/Icon.jsx\";\nimport { Input } from \"./Input.jsx\";\nimport { Select } from \"./Select.jsx\";\nimport { Field } from \"./Field.jsx\";\nimport { RELATIVE_TIME_LABELS, RELATIVE_TIME_UNITS, changeRelativeAmount, changeRelativeUnit, normalizeRelativeTime, relativeTimePosition, relativeTimeText, relativeUnitLabel, resolveRelativeTime } from \"./relative-time.js\";\n\n/** Relative-time filter (\"1 hour ago\", \"any time\") for log, monitoring and inbox pages: a field-shaped trigger that opens a small non-modal dialog\n * with an amount input and a unit select, plus a live \"From\" preview. Controlled: value = { amount, unit }; onChange(value, date) — unlimited resolves to null.\n * Tier: the trigger is a field (sm 28 · md 32 · lg 40 — omit `size` to inherit the region's tier); the dialog is portalled to <body> and copies the trigger's\n * tier, language and theme so it keeps the region's density. Styling is inline from tokens like every other control; hover comes from data-field.\n * The amount is a plain numeric text field (no native spinners); unit labels may be { one, other } and follow Intl.PluralRules of `locale`. */\nconst glyph = { width: \"var(--control-icon)\", height: \"var(--control-icon)\", color: \"var(--text-secondary)\", flexShrink: 0 };\nexport function RelativeTimePicker({ value = { amount: 1, unit: \"Hour\" }, onChange, allowUnlimited = true, disabled, size, labels, locale, referenceDate, formatDate, formatValue, style }) {\n const [open, setOpen] = React.useState(false);\n const [now, setNow] = React.useState(() => new Date());\n const [position, setPosition] = React.useState({});\n const trigger = React.useRef(null);\n const panel = React.useRef(null);\n const current = normalizeRelativeTime(value);\n const text = { ...RELATIVE_TIME_LABELS, ...labels };\n const units = allowUnlimited ? [\"None\", ...RELATIVE_TIME_UNITS] : RELATIVE_TIME_UNITS;\n const unitLabel = (unit) => unit === \"None\" ? text.unlimited : relativeUnitLabel(text, unit, undefined, locale);\n const options = units.map(unitLabel);\n const summary = relativeTimeText(current, text, formatValue, locale);\n const date = resolveRelativeTime(current, referenceDate || now);\n const preview = !date ? text.unlimited : formatDate ? formatDate(date) : new Intl.DateTimeFormat(locale, { dateStyle: \"medium\", timeStyle: \"medium\" }).format(date);\n\n function close(restoreFocus = false) { setOpen(false); if (restoreFocus) trigger.current?.focus(); }\n function update(next) {\n const reference = referenceDate || new Date();\n if (next.unit !== \"None\" && !resolveRelativeTime(next, reference)) return;\n setNow(reference);\n onChange?.(next, resolveRelativeTime(next, reference));\n }\n React.useEffect(() => { if (disabled) setOpen(false); }, [disabled]);\n React.useEffect(() => {\n if (!open) return;\n const place = () => setPosition(relativeTimePosition(trigger.current, panel.current));\n const contains = (t) => trigger.current?.contains(t) || panel.current?.contains(t);\n const outside = (e) => { if (!contains(e.target)) close(); };\n const keydown = (e) => { if (e.key === \"Escape\") { e.preventDefault(); close(true); } };\n place();\n panel.current?.querySelector(\"input\")?.focus();\n document.addEventListener(\"pointerdown\", outside, true);\n document.addEventListener(\"focusin\", outside);\n document.addEventListener(\"keydown\", keydown);\n window.addEventListener(\"resize\", place);\n window.addEventListener(\"scroll\", place, true);\n return () => {\n document.removeEventListener(\"pointerdown\", outside, true);\n document.removeEventListener(\"focusin\", outside);\n document.removeEventListener(\"keydown\", keydown);\n window.removeEventListener(\"resize\", place);\n window.removeEventListener(\"scroll\", place, true);\n };\n }, [open]);\n\n const themeRoot = trigger.current?.closest(\"[data-theme]\");\n return (\n <div data-size={size} style={{ display: \"inline-flex\", width: \"100%\", minWidth: 0, ...style }}>\n <button ref={trigger} type=\"button\" data-field={disabled ? undefined : \"\"} disabled={disabled} aria-haspopup=\"dialog\" aria-expanded={open} aria-label={`${text.title}: ${summary}`}\n onClick={() => { if (open) return close(); setNow(new Date()); setPosition(relativeTimePosition(trigger.current)); setOpen(true); }}\n style={{ display: \"flex\", alignItems: \"center\", gap: \"var(--control-gap)\", height: \"var(--control-h)\", width: \"100%\", padding: \"0 var(--control-px)\", borderRadius: \"var(--radius-md)\",\n border: `1px solid ${open ? \"var(--focus-border)\" : \"var(--field-border, var(--border-control))\"}`, background: disabled ? \"var(--bg-inset)\" : \"var(--bg-surface)\", color: \"var(--text-primary)\",\n fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", textAlign: \"start\",\n cursor: disabled ? \"not-allowed\" : \"pointer\", opacity: disabled ? .5 : 1, transition: \"var(--t-hover)\", boxSizing: \"border-box\", outline: \"none\" }}>\n <Icon name=\"clock\" size={16} style={glyph} />\n <span style={{ flex: 1, overflow: \"hidden\", textOverflow: \"ellipsis\", whiteSpace: \"nowrap\" }}>{summary}</span>\n <Icon name=\"chevron-down\" size={16} style={{ ...glyph, color: \"var(--text-muted)\" }} />\n </button>\n {open && createPortal(\n <div ref={panel} role=\"dialog\" aria-label={text.title} lang={trigger.current?.closest(\"[lang]\")?.lang} data-theme={themeRoot && themeRoot !== document.documentElement ? themeRoot.dataset.theme : undefined}\n style={{ position: \"fixed\", zIndex: \"var(--z-menu)\", maxHeight: \"calc(100vh - 16px)\", overflowY: \"auto\", padding: \"var(--edge-pad)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-lg)\",\n background: \"var(--bg-raised)\", boxShadow: \"var(--shadow-menu)\", color: \"var(--text-body)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", letterSpacing: \"var(--control-tracking)\", boxSizing: \"border-box\", ...position }}>\n <h3 style={{ margin: \"0 0 var(--control-gap)\", color: \"var(--text-heading)\", fontSize: \"var(--control-text)\", lineHeight: \"var(--control-leading)\", fontWeight: \"var(--weight-medium)\" }}>{text.title}</h3>\n <div style={{ display: \"grid\", gridTemplateColumns: \"minmax(0, 1fr) minmax(0, 1.4fr)\", gap: \"var(--group-gap)\" }}>\n <Field label={text.amount}><Input inputMode=\"numeric\" pattern=\"[0-9]*\" value={current.amount} onChange={(e) => { const v = e.target.value.replace(/\\D/g, \"\"); if (v !== \"\" || e.target.value === \"\") update(changeRelativeAmount(current, v)); }} aria-label={text.amount} /></Field>\n <Field label={text.unit}><Select options={options} value={unitLabel(current.unit)} onChange={(e) => { const i = options.indexOf(e.target.value); if (i >= 0) update(changeRelativeUnit(current, units[i])); }} /></Field>\n </div>\n <div style={{ display: \"flex\", flexWrap: \"wrap\", justifyContent: \"space-between\", gap: \"var(--control-gap)\", marginTop: \"var(--group-gap)\", paddingTop: \"var(--control-gap)\", borderTop: \"1px solid var(--border-subtle)\", color: \"var(--text-secondary)\" }}>\n <span>{text.preview}</span>\n <span style={{ color: \"var(--text-body)\", fontVariantNumeric: \"tabular-nums\", overflowWrap: \"anywhere\" }}>{preview}</span>\n </div>\n </div>, document.body)}\n </div>\n );\n}\n","forms/RelativeTimePicker.vue":"<script setup>\nimport { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'\nimport Icon from '../icons/Icon.vue'\nimport Input from './Input.vue'\nimport Select from './Select.vue'\nimport Field from './Field.vue'\nimport { RELATIVE_TIME_LABELS, RELATIVE_TIME_UNITS, changeRelativeAmount, changeRelativeUnit, normalizeRelativeTime, relativeTimePosition, relativeTimeText, relativeUnitLabel, resolveRelativeTime } from './relative-time.js'\n// Relative-time filter (\"1 hour ago\", \"any time\") for log, monitoring and inbox pages: a field-shaped trigger that opens a small non-modal dialog\n// with an amount input and a unit select, plus a live \"From\" preview. v-model = { amount, unit }; @change(value, date) — unlimited resolves to null.\n// Tier: the trigger is a field (sm 28 · md 32 · lg 40 — omit `size` to inherit the region's tier); the dialog teleports to <body> and copies the trigger's\n// tier, language and theme so it keeps the region's density. Styling is inline from tokens like every other control; hover comes from data-field.\n// The amount is a plain numeric text field (no native spinners); unit labels may be { one, other } and follow Intl.PluralRules of `locale`.\nconst props = defineProps({ modelValue: { type: Object, default: () => ({ amount: 1, unit: 'Hour' }) }, allowUnlimited: { type: Boolean, default: true }, disabled: Boolean, size: String, labels: Object, locale: String, referenceDate: Date, formatDate: Function, formatValue: Function, style: Object })\nconst emit = defineEmits(['update:modelValue', 'change'])\nconst trigger = ref(null), panel = ref(null), open = ref(false), position = ref({}), now = ref(new Date()), lang = ref(undefined), theme = ref(undefined)\nconst value = computed(() => normalizeRelativeTime(props.modelValue))\nconst text = computed(() => ({ ...RELATIVE_TIME_LABELS, ...props.labels }))\nconst units = computed(() => props.allowUnlimited ? ['None', ...RELATIVE_TIME_UNITS] : RELATIVE_TIME_UNITS)\nconst unitLabel = (unit) => unit === 'None' ? text.value.unlimited : relativeUnitLabel(text.value, unit, undefined, props.locale)\nconst options = computed(() => units.value.map(unitLabel))\nconst summary = computed(() => relativeTimeText(value.value, text.value, props.formatValue, props.locale))\nconst date = computed(() => resolveRelativeTime(value.value, props.referenceDate || now.value))\nconst preview = computed(() => !date.value ? text.value.unlimited : props.formatDate ? props.formatDate(date.value) : new Intl.DateTimeFormat(props.locale, { dateStyle: 'medium', timeStyle: 'medium' }).format(date.value))\nconst glyph = { width: 'var(--control-icon)', height: 'var(--control-icon)', color: 'var(--text-secondary)', flexShrink: 0 }\nconst triggerStyle = computed(() => ({ display: 'flex', alignItems: 'center', gap: 'var(--control-gap)', height: 'var(--control-h)', width: '100%', padding: '0 var(--control-px)', borderRadius: 'var(--radius-md)',\n border: `1px solid ${open.value ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))'}`, background: props.disabled ? 'var(--bg-inset)' : 'var(--bg-surface)', color: 'var(--text-primary)',\n fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', textAlign: 'start',\n cursor: props.disabled ? 'not-allowed' : 'pointer', opacity: props.disabled ? .5 : 1, transition: 'var(--t-hover)', boxSizing: 'border-box', outline: 'none' }))\nconst panelStyle = computed(() => ({ position: 'fixed', zIndex: 'var(--z-menu)', maxHeight: 'calc(100vh - 16px)', overflowY: 'auto', padding: 'var(--edge-pad)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)',\n background: 'var(--bg-raised)', boxShadow: 'var(--shadow-menu)', color: 'var(--text-body)', fontFamily: 'var(--font-sans)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', boxSizing: 'border-box', ...position.value }))\nfunction update(next) {\n now.value = new Date()\n if (next.unit !== 'None' && !resolveRelativeTime(next, props.referenceDate || now.value)) return\n emit('update:modelValue', next); emit('change', next, resolveRelativeTime(next, props.referenceDate || now.value))\n}\nfunction updateAmount(raw) { const digits = String(raw).replace(/\\D/g, ''); if (digits !== '' || raw === '') update(changeRelativeAmount(value.value, digits)) }\nfunction updateUnit(label) { const i = options.value.indexOf(label); if (i >= 0) update(changeRelativeUnit(value.value, units.value[i])) }\nfunction place() { if (trigger.value) position.value = relativeTimePosition(trigger.value, panel.value) }\nfunction close(restoreFocus = false) { open.value = false; if (restoreFocus) trigger.value?.focus() }\nasync function toggle() {\n if (props.disabled) return\n if (open.value) return close()\n now.value = new Date()\n lang.value = trigger.value.closest('[lang]')?.lang\n const themeRoot = trigger.value.closest('[data-theme]'); theme.value = themeRoot === document.documentElement ? undefined : themeRoot?.dataset.theme\n place(); open.value = true\n await nextTick(); place(); panel.value?.querySelector('input')?.focus()\n}\nfunction outside(e) { if (!trigger.value?.contains(e.target) && !panel.value?.contains(e.target)) close() }\nfunction keydown(e) { if (e.key === 'Escape') { e.preventDefault(); close(true) } }\nfunction focusout() { nextTick(() => { if (!trigger.value?.contains(document.activeElement) && !panel.value?.contains(document.activeElement)) close() }) }\nfunction listen(add) {\n const m = add ? 'addEventListener' : 'removeEventListener'\n document[m]('pointerdown', outside, true); document[m]('keydown', keydown); document[m]('focusin', focusout); window[m]('resize', place); window[m]('scroll', place, true)\n}\nwatch(open, listen)\nwatch(() => props.disabled, (d) => { if (d) close() })\nonBeforeUnmount(() => listen(false))\n</script>\n<template>\n <div :data-size=\"size\" :style=\"{ display: 'inline-flex', width: '100%', minWidth: 0, ...style }\">\n <button ref=\"trigger\" type=\"button\" :data-field=\"disabled ? undefined : ''\" :disabled=\"disabled\" aria-haspopup=\"dialog\" :aria-expanded=\"open\" :aria-label=\"`${text.title}: ${summary}`\" :style=\"triggerStyle\" @click=\"toggle\">\n <Icon name=\"clock\" :size=\"16\" :style=\"glyph\" />\n <span style=\"flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\">{{ summary }}</span>\n <Icon name=\"chevron-down\" :size=\"16\" :style=\"{ ...glyph, color: 'var(--text-muted)' }\" />\n </button>\n <Teleport to=\"body\">\n <div v-if=\"open\" ref=\"panel\" role=\"dialog\" :aria-label=\"text.title\" :lang=\"lang\" :data-theme=\"theme\" :style=\"panelStyle\">\n <h3 style=\"margin:0 0 var(--control-gap);color:var(--text-heading);font-size:var(--control-text);line-height:var(--control-leading);font-weight:var(--weight-medium)\">{{ text.title }}</h3>\n <div style=\"display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr);gap:var(--group-gap)\">\n <Field :label=\"text.amount\"><Input inputmode=\"numeric\" pattern=\"[0-9]*\" :model-value=\"String(value.amount)\" :aria-label=\"text.amount\" @update:model-value=\"updateAmount\" /></Field>\n <Field :label=\"text.unit\"><Select :options=\"options\" :model-value=\"unitLabel(value.unit)\" @update:model-value=\"updateUnit\" /></Field>\n </div>\n <div style=\"display:flex;flex-wrap:wrap;justify-content:space-between;gap:var(--control-gap);margin-top:var(--group-gap);padding-top:var(--control-gap);border-top:1px solid var(--border-subtle);color:var(--text-secondary)\">\n <span>{{ text.preview }}</span>\n <span style=\"color:var(--text-body);font-variant-numeric:tabular-nums;overflow-wrap:anywhere\">{{ preview }}</span>\n </div>\n </div>\n </Teleport>\n </div>\n</template>\n","forms/relative-time.js":"// Framework-neutral relative-time values. No application store, translations or date library.\nexport const RELATIVE_TIME_UNITS = ['Minute', 'Hour', 'Day', 'Week', 'Month'];\n// A unit label is a string (\"小时前\" — the same for every amount) or { one, other } for languages that inflect (\"1 hour ago\" / \"2 hours ago\").\nexport const RELATIVE_TIME_LABELS = {\n title: 'Relative time', amount: 'Amount', unit: 'Unit', preview: 'From', unlimited: 'Any time',\n Minute: { one: 'minute ago', other: 'minutes ago' }, Hour: { one: 'hour ago', other: 'hours ago' },\n Day: { one: 'day ago', other: 'days ago' }, Week: { one: 'week ago', other: 'weeks ago' }, Month: { one: 'month ago', other: 'months ago' },\n};\n\n/** The label of a unit for a given amount (plural rules of `locale`); omit amount for the generic form shown in the unit select. */\nexport function relativeUnitLabel(labels, unit, amount, locale) {\n const l = labels[unit];\n if (l == null || typeof l === 'string') return l;\n if (amount == null) return l.other;\n let rule = 'other';\n try { rule = new Intl.PluralRules(locale).select(amount); } catch (e) { rule = amount === 1 ? 'one' : 'other'; }\n return l[rule] || l.other || l.one;\n}\n\nexport function normalizeRelativeTime(value) {\n const amount = Math.floor(Number(value?.amount));\n const unit = RELATIVE_TIME_UNITS.includes(value?.unit) ? value.unit : 'None';\n return Number.isFinite(amount) && amount > 0 && unit !== 'None'\n ? { amount, unit }\n : { amount: 0, unit: 'None' };\n}\n\nexport function changeRelativeAmount(value, amount) {\n return normalizeRelativeTime({ amount, unit: value.unit === 'None' ? 'Hour' : value.unit });\n}\n\nexport function changeRelativeUnit(value, unit) {\n return normalizeRelativeTime({ amount: unit === 'None' ? 0 : value.amount || 1, unit });\n}\n\n/** Unlimited is null; consumers decide whether their backend needs an epoch sentinel. */\nexport function resolveRelativeTime(value, referenceDate = new Date()) {\n const { amount, unit } = normalizeRelativeTime(value);\n if (!amount) return null;\n const date = new Date(referenceDate);\n if (!Number.isFinite(date.getTime())) return null;\n if (unit === 'Minute' || unit === 'Hour') {\n date.setTime(date.getTime() - amount * (unit === 'Minute' ? 60000 : 3600000));\n } else if (unit === 'Month') {\n // Match calendar-month subtraction: March 31 minus one month is February's last day.\n const day = date.getDate();\n date.setDate(1);\n date.setMonth(date.getMonth() - amount);\n const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();\n date.setDate(Math.min(day, lastDay));\n } else {\n // Calendar days/weeks preserve local wall time across daylight-saving boundaries.\n date.setDate(date.getDate() - amount * (unit === 'Week' ? 7 : 1));\n }\n return Number.isFinite(date.getTime()) ? date : null;\n}\n\nexport function relativeTimeText(value, labels, formatValue, locale) {\n return formatValue ? formatValue(value) : value.unit === 'None'\n ? labels.unlimited : `${value.amount} ${relativeUnitLabel(labels, value.unit, value.amount, locale)}`;\n}\n\n/** Fixed, viewport-clamped placement; retain the trigger's inherited density and language. */\nexport function relativeTimePosition(trigger, panel) {\n const rect = trigger.getBoundingClientRect();\n const computed = getComputedStyle(trigger);\n // 弹层挂载到 body 后,需要高于触发器所在的抽屉、对话框等层叠上下文。\n let zIndex = Number.parseFloat(computed.getPropertyValue('--z-menu')) || 0;\n for (let element = trigger; element; element = element.parentElement) {\n const ancestorZIndex = Number.parseFloat(getComputedStyle(element).zIndex);\n if (Number.isFinite(ancestorZIndex)) zIndex = Math.max(zIndex, ancestorZIndex + 1);\n }\n const width = Math.min(336, window.innerWidth - 16);\n const height = panel?.offsetHeight || 200;\n const below = rect.bottom + 4;\n const top = below + height <= window.innerHeight - 8\n ? below : Math.max(8, rect.top - height - 4);\n const style = {\n zIndex: String(zIndex),\n width: `${width}px`,\n left: `${Math.max(8, Math.min(rect.left, window.innerWidth - width - 8))}px`,\n top: `${top}px`,\n };\n for (const name of computed) {\n if (/^--(control-|font-|weight-|leading-|tracking-|group-gap|edge-pad)/.test(name)) {\n style[name] = computed.getPropertyValue(name);\n }\n }\n return style;\n}\n"});