@keyfunc/keybricks-ds 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPLOY.md +3 -3
- package/INTEGRATION.md +6 -5
- package/_adherence.oxlintrc.json +21 -6
- package/components/_cards/radio.card.html +46 -0
- package/components/_cards/segmented-control.card.html +39 -0
- package/components/_cards/toggles.card.html +44 -55
- package/components/_shared/src-forms.js +1 -1
- package/components/_shared/src-tabs.js +1 -1
- package/components/_shared/view-source.js +9 -4
- package/components/forms/Radio.d.ts +28 -0
- package/components/forms/Radio.jsx +67 -0
- package/components/forms/Radio.vue +17 -8
- package/components/forms/RadioGroup.vue +22 -4
- package/components/forms/Switch.d.ts +32 -24
- package/components/forms/Switch.jsx +87 -35
- package/components/forms/Switch.vue +56 -11
- package/components/forms/forms.prompt.md +4 -1
- package/components/index.d.ts +1 -0
- package/components/index.js +2 -1
- package/components/tabs/SegmentedControl.d.ts +10 -3
- package/components/tabs/SegmentedControl.jsx +24 -9
- package/components/tabs/SegmentedControl.vue +19 -7
- package/components/tabs/tabs.prompt.md +3 -1
- package/package.json +1 -1
- package/readme.md +0 -1
- package/tokens/tokens.json +1 -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/** 16px (sm: 13px) checkbox — 1.5px #d1d5db border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. */\nexport function Checkbox({ checked, indeterminate, onChange, label, size = \"md\", disabled, style }) {\n const box = size === \"sm\" ? 13 : 16;\n const filled = checked || indeterminate;\n return (\n <label style={{ display: \"inline-flex\", alignItems: \"center\", verticalAlign: \"middle\", lineHeight: \"var(--leading-sm)\", gap: 6, 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(--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={box - 4} strokeWidth={3} /> : indeterminate ? <Icon name=\"minus\" size={box - 4} strokeWidth={3} /> : null}\n </span>\n {label ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", 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// 16px (sm: 13px) checkbox — 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity.\nconst props = defineProps({ checked: Boolean, indeterminate: Boolean, label: String, size: { type: String, default: 'md' }, disabled: Boolean, style: Object })\nconst emit = defineEmits(['change'])\nconst box = computed(() => props.size === 'sm' ? 13 : 16)\nconst filled = computed(() => props.checked || props.indeterminate)\n</script>\n<template>\n <label :style=\"{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--leading-sm)', gap: 6, cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, ...style }\" @click=\"!disabled && emit('change', !checked)\">\n <span :style=\"{ width: box, height: box, borderRadius: 4, boxSizing: 'border-box', border: filled ? 'none' : '1.5px solid 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=\"box - 4\" :stroke-width=\"3\" />\n <Icon v-else-if=\"indeterminate\" name=\"minus\" :size=\"box - 4\" :stroke-width=\"3\" />\n </span>\n <span v-if=\"label\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);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\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. */\nexport function Combobox({ options = [], value = \"\", placeholder = \"Select...\", disabled, error, width, 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} style={{ position: \"relative\", width, ...style }}>\n <div 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: 32, borderColor: error ? \"var(--danger)\" : open ? \"var(--focus-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: 4, top: \"50%\", transform: \"translateY(-50%)\", width: 26, height: 26, 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={14} /></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={{ padding: \"8px 10px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", 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%\", padding: \"7px 10px\", borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: \"pointer\", background: hov === i ? \"var(--bg-hover)\" : \"transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", color: \"var(--text-heading)\", textAlign: \"left\" }}>\n {o}{o === value && <span style={{ color: \"var(--action-primary)\", display: \"inline-flex\" }}><Icon name=\"check\" size={13} /></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.\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select...' }, disabled: Boolean, error: Boolean, width: [Number, String], style: Object })\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\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div 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: '32px', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-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:4px;top:50%;transform:translateY(-50%);width:26px;height:26px;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=\"14\" /></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=\"padding:8px 10px;font-family:var(--font-sans);font-size:var(--text-sm);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%', padding: '7px 10px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: 'pointer', background: hov === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', 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=\"13\" /></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 MONTHS = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"];\nconst pad = (n) => String(n).padStart(2, \"0\");\nconst iso = (d) => d.getFullYear() + \"-\" + pad(d.getMonth() + 1) + \"-\" + pad(d.getDate());\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\". */\nexport function DatePicker({ value = \"\", placeholder = \"Select date\", disabled, error, width = 180, 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 ? MONTHS[sel.getMonth()].slice(0, 3) + \" \" + sel.getDate() + \", \" + sel.getFullYear() : \"\";\n return (\n <div ref={root} style={{ position: \"relative\", width, ...style }}>\n <div 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(--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={14} /></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\" size=\"sm\" label=\"Previous month\" onClick={() => nav(-1)} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", color: \"var(--text-heading)\" }}>{MONTHS[view.m]} {view.y}</span>\n <IconButton icon=\"chevron-right\" size=\"sm\" label=\"Next month\" onClick={() => nav(1)} />\n </div>\n <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(7,1fr)\", gap: 2 }}>\n {[\"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\", \"Su\"].map(d => <span key={d} style={{ textAlign: \"center\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-2xs)\", letterSpacing: \"var(--tracking-2xs)\", color: \"var(--text-muted)\", padding: \"2px 0\" }}>{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(--text-sm)\", 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\".\nconst MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']\nconst pad = (n) => String(n).padStart(2, '0')\nconst props = defineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select date' }, disabled: Boolean, error: Boolean, width: { type: [Number, String], default: 180 }, 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 ? MONTHS[sel.value.getMonth()].slice(0, 3) + ' ' + sel.value.getDate() + ', ' + sel.value.getFullYear() : '')\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\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div @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(--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=\"14\" /></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\" size=\"sm\" label=\"Previous month\" @click=\"nav(-1)\" />\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);color:var(--text-heading)\">{{ MONTHS[view.m] }} {{ view.y }}</span>\n <IconButton icon=\"chevron-right\" size=\"sm\" 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 in ['Mo','Tu','We','Th','Fr','Sa','Su']\" :key=\"d\" style=\"text-align:center;font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding:2px 0\">{{ 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(--text-sm)', 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} style={{ display: \"flex\", alignItems: \"center\", gap: 4, border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--danger)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", padding: 0 }}>\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-sm)\", lineHeight: \"var(--leading-sm)\", 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: 8, 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\" @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-sm);padding:0\"><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: 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\" :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-sm)', lineHeight: 'var(--leading-sm)', 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: 12px/500 label (red + * when required) above the control, optional hint/error below. */\nexport function Field({ label, required, hint, error, children, style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", ...style }}>\n <label style={{ paddingBottom: 6, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", 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: 12px/500 label (red + * when required) above the control, optional hint/error below.\ndefineProps({ label: String, required: Boolean, hint: String, error: String, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', ...style }\">\n <label style=\"padding-bottom:6px;font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);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, 36px r8. readOnly = #fafafa inset; disabled = 50% opacity; error = red border + ring. */\nexport function Input({ readOnly, disabled, error, style, ...rest }) {\n const [focus, setFocus] = React.useState(false);\n return <input readOnly={readOnly} disabled={disabled} 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. showCount + maxLength renders a bottom-right \"n/max\" counter (red past the limit). */\nexport function Textarea({ style, rows = 4, error, disabled, showCount, maxLength, 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} 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\", 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-mono)\", 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, 36px r8. 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, 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\" @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\n/** Multi-select dropdown: checkbox option list, selected values render as removable tags in the field (overflow collapses to \"+n\"), clear-all ×. */\nexport function MultiSelect({ options = [], value = [], placeholder = \"Select...\", maxTags = 2, disabled, width, 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} style={{ position: \"relative\", width, ...style }}>\n <div onClick={() => !disabled && setOpen(!open)} style={{ minHeight: \"var(--h-input)\", borderRadius: \"var(--radius-md)\", border: `1px solid ${open ? \"var(--focus-border)\" : \"var(--border-control)\"}`, boxShadow: open ? \"0 0 0 2px var(--focus-ring)\" : \"none\", background: \"var(--bg-surface)\", padding: \"5px 30px 5px 8px\", 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(--text-sm)\", 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: 8, top: \"50%\", transform: \"translateY(-50%)\", display: \"flex\", alignItems: \"center\", gap: 2, color: \"var(--text-muted)\" }}>\n {value.length > 0 && <button type=\"button\" onClick={(e) => { e.stopPropagation(); onChange && onChange([]); }} style={{ border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"inherit\", display: \"flex\", padding: 0 }}><Icon name=\"x\" size={13} /></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={14} /></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, padding: \"7px 10px\", 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 size=\"sm\" checked={value.includes(o)} onChange={() => toggle(o)} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", 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 ×.\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], style: Object })\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\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div @click=\"!disabled && (open = !open)\" :style=\"{ minHeight: 'var(--h-input)', borderRadius: 'var(--radius-md)', border: `1px solid ${open ? 'var(--focus-border)' : 'var(--border-control)'}`, boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', background: 'var(--bg-surface)', padding: '5px 30px 5px 8px', 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(--text-sm);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:8px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:2px;color:var(--text-muted)\">\n <button v-if=\"modelValue.length\" type=\"button\" @click.stop=\"emit('update:modelValue', [])\" style=\"border:none;background:transparent;cursor:pointer;color:inherit;display:flex;padding:0\"><Icon name=\"x\" :size=\"13\" /></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=\"14\" /></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;padding:7px 10px;border-radius:var(--radius-sm);cursor:pointer\">\n <Checkbox size=\"sm\" :checked=\"modelValue.includes(o)\" @change=\"toggle(o)\" />\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);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, 25.5px tall, r4 (\"Preview rows\"). Buttons disable themselves at min/max. */\nexport function NumberStepper({ value = 100, onChange, step = 10, min = 0, max, disabled, style }) {\n const atMin = value <= min, atMax = max != null && value >= max;\n const seg = (dis) => ({ width: 30, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", cursor: dis ? \"not-allowed\" : \"pointer\", color: dis ? \"var(--text-muted)\" : \"var(--text-body)\", alignSelf: \"stretch\", background: \"transparent\", border: \"none\", padding: 0 });\n return (\n <div style={{ display: \"inline-flex\", alignItems: \"center\", height: 25.5, borderRadius: \"var(--radius-sm)\", border: \"1px solid var(--border-control)\", overflow: \"hidden\", background: \"var(--bg-surface)\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, ...style }}>\n <button type=\"button\" disabled={disabled || atMin} style={seg(disabled || atMin)} onClick={() => onChange && onChange(Math.max(min, value - step))}><Icon name=\"minus\" size={12} /></button>\n <span style={{ minWidth: 34, textAlign: \"center\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", 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} style={seg(disabled || atMax)} onClick={() => onChange && onChange(max != null ? Math.min(max, value + step) : value + step)}><Icon name=\"plus\" size={12} /></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, 25.5px tall, r4 (\"Preview rows\"). 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, style: Object })\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: 30, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: dis ? 'not-allowed' : 'pointer', color: dis ? 'var(--text-muted)' : 'var(--text-body)', alignSelf: 'stretch', background: 'transparent', border: 'none', padding: 0 } }\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', alignItems: 'center', height: 25.5, borderRadius: 'var(--radius-sm)', border: '1px solid var(--border-control)', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }\">\n <button type=\"button\" :disabled=\"disabled || atMin\" :style=\"seg(disabled || atMin)\" @click=\"emit('update:modelValue', Math.max(min, modelValue - step))\"><Icon name=\"minus\" :size=\"12\" /></button>\n <span style=\"min-width:34px;text-align:center;font-family:var(--font-sans);font-size:var(--text-sm);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\" :style=\"seg(disabled || atMax)\" @click=\"emit('update:modelValue', max != null ? Math.min(max, modelValue + step) : modelValue + step)\"><Icon name=\"plus\" :size=\"12\" /></button>\n </div>\n</template>\n","forms/Radio.vue":"<script setup>\nimport { computed } from 'vue'\n// Radio — 16px (sm: 13px) circle, 1.5px border, filled dot when selected.\nconst dims = { sm: { box: 13, dot: 6 }, md: { box: 16, dot: 8 } }\nconst props = defineProps({ checked: Boolean, label: String, size: { type: String, default: 'md' }, disabled: Boolean, style: Object })\nconst emit = defineEmits(['change'])\nconst d = computed(() => dims[props.size] || dims.md)\n</script>\n<template>\n <label :style=\"{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--leading-sm)', gap: 6, cursor: disabled ? 'default' : 'pointer', opacity: disabled ? .5 : 1, ...style }\" @click=\"!disabled && emit('change', true)\">\n <span :style=\"{ width: d.box + 'px', height: d.box + 'px', borderRadius: 'var(--radius-full)', boxSizing: 'border-box', border: `1.5px solid ${checked ? 'var(--action-primary)' : 'var(--checkbox-border)'}`, background: 'var(--bg-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, transition: 'var(--t-hover)' }\">\n <span v-if=\"checked\" :style=\"{ width: d.dot + 'px', height: d.dot + 'px', borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }\" />\n </span>\n <span v-if=\"label\" :style=\"{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: 'var(--text-body)' }\">{{ label }}</span>\n </label>\n</template>\n","forms/RadioGroup.vue":"<script setup>\nimport Radio from './Radio.vue'\n// Vertical or horizontal group of Radios.\ndefineProps({ options: { type: Array, default: () => [] }, modelValue: String, row: Boolean, size: { type: String, default: 'md' }, style: Object })\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: (row ? 16 : 10) + 'px', ...style }\">\n <Radio v-for=\"o in options\" :key=\"o\" :label=\"o\" :size=\"size\" :checked=\"modelValue === o\" @change=\"emit('update:modelValue', o)\" />\n </div>\n</template>\n","forms/SearchInput.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Bordered search field (r10) 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, 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 style={{ display: \"flex\", alignItems: \"center\", gap: bare ? 12 : 8, opacity: disabled ? .5 : 1, ...(bare ? null : { border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-lg)\", padding: \"6px 12px\", background: \"var(--bg-surface)\" }), ...style }}>\n <span style={{ color: \"var(--text-muted)\" }}><Icon name=\"search\" size={bare ? 16 : 14} /></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, fontFamily: \"var(--font-sans)\", fontSize: bare ? \"var(--text-base)\" : \"var(--text-sm)\", letterSpacing: bare ? \"var(--tracking-base)\" : 0, color: \"var(--text-primary)\", cursor: disabled ? \"not-allowed\" : \"text\" }} />\n {val && !disabled ? <button type=\"button\" onClick={() => set(\"\")} style={{ border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--text-muted)\", display: \"flex\", padding: 0 }}><Icon name=\"x\" size={13} /></button> : null}\n </div>\n );\n}\n","forms/SearchInput.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Bordered search field (r10) 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, disabled: Boolean, style: Object })\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'center', gap: bare ? 12 : 8, opacity: disabled ? .5 : 1, ...(bare ? null : { border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', padding: '6px 12px', background: 'var(--bg-surface)' }), ...style }\">\n <span style=\"color:var(--text-muted)\"><Icon name=\"search\" :size=\"bare ? 16 : 14\" /></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, fontFamily: 'var(--font-sans)', fontSize: bare ? 'var(--text-base)' : 'var(--text-sm)', letterSpacing: bare ? 'var(--tracking-base)' : 0, color: 'var(--text-primary)', cursor: disabled ? 'not-allowed' : 'text' }\" />\n <button v-if=\"modelValue && !disabled\" type=\"button\" @click=\"emit('update:modelValue', '')\" style=\"border:none;background:transparent;cursor:pointer;color:var(--text-muted);display:flex;padding:0\"><Icon name=\"x\" :size=\"13\" /></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. size md 36/r8 · sm 25/r4 (table rows). disabled = #fafafa inset fill (matches Input readOnly), 50% opacity; error = red border. */\nexport function Select({ options = [], size = \"md\", value, onChange, placeholder, disabled, error, style }) {\n const sm = size === \"sm\";\n return (\n <div style={{ position: \"relative\", display: \"inline-flex\", ...style }}>\n <select value={value} onChange={onChange} disabled={disabled}\n style={{ appearance: \"none\", WebkitAppearance: \"none\", height: sm ? 25 : 36, borderRadius: sm ? \"var(--radius-sm)\" : \"var(--radius-md)\", border: `1px solid ${error ? \"var(--danger)\" : \"var(--border-control)\"}`, background: disabled ? \"var(--bg-inset)\" : \"var(--bg-surface)\", padding: sm ? \"0 26px 0 10px\" : \"0 32px 0 12px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", 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: sm ? 8 : 10, top: \"50%\", transform: \"translateY(-50%)\", pointerEvents: \"none\", color: \"var(--text-muted)\" }}><Icon name=\"chevron-down\" size={sm ? 12 : 14} /></span>\n </div>\n );\n}\n","forms/Select.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Native select styled per Keybricks. size md 36/r8 · sm 25/r4 (table rows). disabled = inset fill (matches Input readonly), 50% opacity; error = red border.\nconst props = defineProps({ modelValue: String, options: { type: Array, default: () => [] }, size: { type: String, default: 'md' }, placeholder: String, disabled: Boolean, error: Boolean, style: Object })\ndefineEmits(['update:modelValue'])\n</script>\n<template>\n <div :style=\"{ position: 'relative', display: 'inline-flex', ...style }\">\n <select :value=\"modelValue\" @change=\"$emit('update:modelValue', $event.target.value)\" :disabled=\"disabled\"\n :style=\"{ appearance: 'none', height: size === 'sm' ? 25 : 36, borderRadius: size === 'sm' ? 'var(--radius-sm)' : 'var(--radius-md)', border: `1px solid ${error ? 'var(--danger)' : 'var(--border-control)'}`, background: disabled ? 'var(--bg-inset)' : 'var(--bg-surface)', padding: size === 'sm' ? '0 26px 0 10px' : '0 32px 0 12px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', 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: size === 'sm' ? 8 : 10, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none', color: 'var(--text-muted)' }\"><Icon name=\"chevron-down\" :size=\"size === 'sm' ? 12 : 14\" /></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} 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: \"0 1px 2px rgba(0,0,0,.15)\" }}></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-mono)\", fontSize: \"var(--text-2xs)\", 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\" @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: '0 1px 2px rgba(0,0,0,.15)' }\" />\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-mono)', fontSize: 'var(--text-2xs)', 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\";\n\nconst switchDims = { sm: { w: 28, h: 16, knob: 12 }, md: { w: 34, h: 20, knob: 16 } };\n/** Switch — 34×20 (sm: 28×16) track, knob. Keybricks has no switch in the source file; this follows the button radii + action color. */\nexport function Switch({ checked, onChange, label, size = \"md\", disabled, style }) {\n const d = switchDims[size] || switchDims.md;\n return (\n <label style={{ display: \"inline-flex\", alignItems: \"center\", verticalAlign: \"middle\", lineHeight: \"var(--leading-sm)\", gap: 8, cursor: disabled ? \"default\" : \"pointer\", opacity: disabled ? .5 : 1, ...style }}\n onClick={() => !disabled && onChange && onChange(!checked)}>\n <span style={{ width: d.w, height: d.h, borderRadius: \"var(--radius-full)\", background: checked ? \"var(--action-primary)\" : \"var(--track)\", position: \"relative\", flexShrink: 0, transition: \"var(--t-hover)\" }}>\n <span style={{ position: \"absolute\", top: 2, left: checked ? d.w - d.knob - 2 : 2, width: d.knob, height: d.knob, borderRadius: \"var(--radius-full)\", background: \"var(--on-accent)\", boxShadow: \"0 1px 2px rgba(0,0,0,.2)\", transition: \"left var(--dur-fast) var(--ease-out)\" }} />\n </span>\n {label ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: size === \"sm\" ? \"var(--text-xs)\" : \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-body)\" }}>{label}</span> : null}\n </label>\n );\n}\n\nconst radioDims = { sm: { box: 13, dot: 6 }, md: { box: 16, dot: 8 } };\n/** Radio — 16px (sm: 13px) circle, 1.5px border, filled dot when selected. */\nexport function Radio({ checked, onChange, label, size = \"md\", disabled, style }) {\n const d = radioDims[size] || radioDims.md;\n return (\n <label style={{ display: \"inline-flex\", alignItems: \"center\", verticalAlign: \"middle\", lineHeight: \"var(--leading-sm)\", gap: 6, cursor: disabled ? \"default\" : \"pointer\", opacity: disabled ? .5 : 1, ...style }}\n onClick={() => !disabled && onChange && onChange(true)}>\n <span style={{ width: d.box, height: d.box, borderRadius: \"var(--radius-full)\", boxSizing: \"border-box\", border: `1.5px solid ${checked ? \"var(--action-primary)\" : \"var(--checkbox-border)\"}`, background: \"var(--bg-surface)\", 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)\" }} /> : null}\n </span>\n {label ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: size === \"sm\" ? \"var(--text-xs)\" : \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: \"var(--text-body)\" }}>{label}</span> : null}\n </label>\n );\n}\n\n/** Vertical or horizontal group of Radios. */\nexport function RadioGroup({ options = [], value, onChange, row, size = \"md\", style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: row ? \"row\" : \"column\", gap: row ? 16 : 10, ...style }}>\n {options.map((o) => <Radio key={o} label={o} size={size} checked={value === o} onChange={() => onChange && onChange(o)} />)}\n </div>\n );\n}\n","forms/Switch.vue":"<script setup>\nimport { computed } from 'vue'\n// Switch — 34×20 (sm: 28×16) track, knob.\nconst dims = { sm: { w: 28, h: 16, knob: 12 }, md: { w: 34, h: 20, knob: 16 } }\nconst props = defineProps({ modelValue: Boolean, label: String, size: { type: String, default: 'md' }, disabled: Boolean, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst d = computed(() => dims[props.size] || dims.md)\n</script>\n<template>\n <label :style=\"{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--leading-sm)', gap: 8, cursor: disabled ? 'default' : 'pointer', opacity: disabled ? .5 : 1, ...style }\" @click=\"!disabled && emit('update:modelValue', !modelValue)\">\n <span :style=\"{ width: d.w + 'px', height: d.h + 'px', borderRadius: 'var(--radius-full)', background: modelValue ? 'var(--action-primary)' : 'var(--track)', position: 'relative', flexShrink: 0, transition: 'var(--t-hover)' }\">\n <span :style=\"{ position: 'absolute', top: '2px', left: (modelValue ? d.w - d.knob - 2 : 2) + 'px', width: d.knob + 'px', height: d.knob + 'px', borderRadius: 'var(--radius-full)', background: 'var(--on-accent)', boxShadow: '0 1px 2px rgba(0,0,0,.2)', transition: 'left var(--dur-fast) var(--ease-out)' }\" />\n </span>\n <span v-if=\"label\" :style=\"{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: 'var(--text-body)' }\">{{ label }}</span>\n </label>\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. 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, 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', 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\" :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-mono)', 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.\nexport const inputBase = { height: 'var(--h-input)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-control)', background: 'var(--bg-surface)', padding: '8px 12px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: 'var(--text-primary)', outline: 'none', boxSizing: 'border-box', width: '100%' }\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/** 16px (sm: 13px) checkbox — 1.5px #d1d5db border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. */\nexport function Checkbox({ checked, indeterminate, onChange, label, size = \"md\", disabled, style }) {\n const box = size === \"sm\" ? 13 : 16;\n const filled = checked || indeterminate;\n return (\n <label style={{ display: \"inline-flex\", alignItems: \"center\", verticalAlign: \"middle\", lineHeight: \"var(--leading-sm)\", gap: 6, 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(--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={box - 4} strokeWidth={3} /> : indeterminate ? <Icon name=\"minus\" size={box - 4} strokeWidth={3} /> : null}\n </span>\n {label ? <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", 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// 16px (sm: 13px) checkbox — 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity.\nconst props = defineProps({ checked: Boolean, indeterminate: Boolean, label: String, size: { type: String, default: 'md' }, disabled: Boolean, style: Object })\nconst emit = defineEmits(['change'])\nconst box = computed(() => props.size === 'sm' ? 13 : 16)\nconst filled = computed(() => props.checked || props.indeterminate)\n</script>\n<template>\n <label :style=\"{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--leading-sm)', gap: 6, cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, ...style }\" @click=\"!disabled && emit('change', !checked)\">\n <span :style=\"{ width: box, height: box, borderRadius: 4, boxSizing: 'border-box', border: filled ? 'none' : '1.5px solid 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=\"box - 4\" :stroke-width=\"3\" />\n <Icon v-else-if=\"indeterminate\" name=\"minus\" :size=\"box - 4\" :stroke-width=\"3\" />\n </span>\n <span v-if=\"label\" style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);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\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. */\nexport function Combobox({ options = [], value = \"\", placeholder = \"Select...\", disabled, error, width, 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} style={{ position: \"relative\", width, ...style }}>\n <div 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: 32, borderColor: error ? \"var(--danger)\" : open ? \"var(--focus-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: 4, top: \"50%\", transform: \"translateY(-50%)\", width: 26, height: 26, 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={14} /></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={{ padding: \"8px 10px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", 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%\", padding: \"7px 10px\", borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: \"pointer\", background: hov === i ? \"var(--bg-hover)\" : \"transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", color: \"var(--text-heading)\", textAlign: \"left\" }}>\n {o}{o === value && <span style={{ color: \"var(--action-primary)\", display: \"inline-flex\" }}><Icon name=\"check\" size={13} /></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.\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select...' }, disabled: Boolean, error: Boolean, width: [Number, String], style: Object })\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\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div 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: '32px', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-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:4px;top:50%;transform:translateY(-50%);width:26px;height:26px;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=\"14\" /></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=\"padding:8px 10px;font-family:var(--font-sans);font-size:var(--text-sm);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%', padding: '7px 10px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: 'pointer', background: hov === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', 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=\"13\" /></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 MONTHS = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"];\nconst pad = (n) => String(n).padStart(2, \"0\");\nconst iso = (d) => d.getFullYear() + \"-\" + pad(d.getMonth() + 1) + \"-\" + pad(d.getDate());\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\". */\nexport function DatePicker({ value = \"\", placeholder = \"Select date\", disabled, error, width = 180, 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 ? MONTHS[sel.getMonth()].slice(0, 3) + \" \" + sel.getDate() + \", \" + sel.getFullYear() : \"\";\n return (\n <div ref={root} style={{ position: \"relative\", width, ...style }}>\n <div 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(--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={14} /></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\" size=\"sm\" label=\"Previous month\" onClick={() => nav(-1)} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", color: \"var(--text-heading)\" }}>{MONTHS[view.m]} {view.y}</span>\n <IconButton icon=\"chevron-right\" size=\"sm\" label=\"Next month\" onClick={() => nav(1)} />\n </div>\n <div style={{ display: \"grid\", gridTemplateColumns: \"repeat(7,1fr)\", gap: 2 }}>\n {[\"Mo\", \"Tu\", \"We\", \"Th\", \"Fr\", \"Sa\", \"Su\"].map(d => <span key={d} style={{ textAlign: \"center\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-2xs)\", letterSpacing: \"var(--tracking-2xs)\", color: \"var(--text-muted)\", padding: \"2px 0\" }}>{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(--text-sm)\", 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\".\nconst MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']\nconst pad = (n) => String(n).padStart(2, '0')\nconst props = defineProps({ modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select date' }, disabled: Boolean, error: Boolean, width: { type: [Number, String], default: 180 }, 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 ? MONTHS[sel.value.getMonth()].slice(0, 3) + ' ' + sel.value.getDate() + ', ' + sel.value.getFullYear() : '')\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\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div @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(--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=\"14\" /></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\" size=\"sm\" label=\"Previous month\" @click=\"nav(-1)\" />\n <span style=\"font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);color:var(--text-heading)\">{{ MONTHS[view.m] }} {{ view.y }}</span>\n <IconButton icon=\"chevron-right\" size=\"sm\" 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 in ['Mo','Tu','We','Th','Fr','Sa','Su']\" :key=\"d\" style=\"text-align:center;font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding:2px 0\">{{ 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(--text-sm)', 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} style={{ display: \"flex\", alignItems: \"center\", gap: 4, border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--danger)\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", padding: 0 }}>\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-sm)\", lineHeight: \"var(--leading-sm)\", 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: 8, 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\" @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-sm);padding:0\"><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: 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\" :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-sm)', lineHeight: 'var(--leading-sm)', 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: 12px/500 label (red + * when required) above the control, optional hint/error below. */\nexport function Field({ label, required, hint, error, children, style }) {\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", ...style }}>\n <label style={{ paddingBottom: 6, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", 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: 12px/500 label (red + * when required) above the control, optional hint/error below.\ndefineProps({ label: String, required: Boolean, hint: String, error: String, style: Object })\n</script>\n<template>\n <div :style=\"{ display: 'flex', flexDirection: 'column', ...style }\">\n <label style=\"padding-bottom:6px;font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-sm);line-height:var(--leading-sm);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, 36px r8. readOnly = #fafafa inset; disabled = 50% opacity; error = red border + ring. */\nexport function Input({ readOnly, disabled, error, style, ...rest }) {\n const [focus, setFocus] = React.useState(false);\n return <input readOnly={readOnly} disabled={disabled} 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. showCount + maxLength renders a bottom-right \"n/max\" counter (red past the limit). */\nexport function Textarea({ style, rows = 4, error, disabled, showCount, maxLength, 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} 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\", 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-mono)\", 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, 36px r8. 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, 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\" @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\n/** Multi-select dropdown: checkbox option list, selected values render as removable tags in the field (overflow collapses to \"+n\"), clear-all ×. */\nexport function MultiSelect({ options = [], value = [], placeholder = \"Select...\", maxTags = 2, disabled, width, 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} style={{ position: \"relative\", width, ...style }}>\n <div onClick={() => !disabled && setOpen(!open)} style={{ minHeight: \"var(--h-input)\", borderRadius: \"var(--radius-md)\", border: `1px solid ${open ? \"var(--focus-border)\" : \"var(--border-control)\"}`, boxShadow: open ? \"0 0 0 2px var(--focus-ring)\" : \"none\", background: \"var(--bg-surface)\", padding: \"5px 30px 5px 8px\", 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(--text-sm)\", 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: 8, top: \"50%\", transform: \"translateY(-50%)\", display: \"flex\", alignItems: \"center\", gap: 2, color: \"var(--text-muted)\" }}>\n {value.length > 0 && <button type=\"button\" onClick={(e) => { e.stopPropagation(); onChange && onChange([]); }} style={{ border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"inherit\", display: \"flex\", padding: 0 }}><Icon name=\"x\" size={13} /></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={14} /></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, padding: \"7px 10px\", 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 size=\"sm\" checked={value.includes(o)} onChange={() => toggle(o)} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", 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 ×.\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], style: Object })\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\" :style=\"{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }\">\n <div @click=\"!disabled && (open = !open)\" :style=\"{ minHeight: 'var(--h-input)', borderRadius: 'var(--radius-md)', border: `1px solid ${open ? 'var(--focus-border)' : 'var(--border-control)'}`, boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', background: 'var(--bg-surface)', padding: '5px 30px 5px 8px', 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(--text-sm);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:8px;top:50%;transform:translateY(-50%);display:flex;align-items:center;gap:2px;color:var(--text-muted)\">\n <button v-if=\"modelValue.length\" type=\"button\" @click.stop=\"emit('update:modelValue', [])\" style=\"border:none;background:transparent;cursor:pointer;color:inherit;display:flex;padding:0\"><Icon name=\"x\" :size=\"13\" /></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=\"14\" /></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;padding:7px 10px;border-radius:var(--radius-sm);cursor:pointer\">\n <Checkbox size=\"sm\" :checked=\"modelValue.includes(o)\" @change=\"toggle(o)\" />\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);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, 25.5px tall, r4 (\"Preview rows\"). Buttons disable themselves at min/max. */\nexport function NumberStepper({ value = 100, onChange, step = 10, min = 0, max, disabled, style }) {\n const atMin = value <= min, atMax = max != null && value >= max;\n const seg = (dis) => ({ width: 30, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", cursor: dis ? \"not-allowed\" : \"pointer\", color: dis ? \"var(--text-muted)\" : \"var(--text-body)\", alignSelf: \"stretch\", background: \"transparent\", border: \"none\", padding: 0 });\n return (\n <div style={{ display: \"inline-flex\", alignItems: \"center\", height: 25.5, borderRadius: \"var(--radius-sm)\", border: \"1px solid var(--border-control)\", overflow: \"hidden\", background: \"var(--bg-surface)\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, ...style }}>\n <button type=\"button\" disabled={disabled || atMin} style={seg(disabled || atMin)} onClick={() => onChange && onChange(Math.max(min, value - step))}><Icon name=\"minus\" size={12} /></button>\n <span style={{ minWidth: 34, textAlign: \"center\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", 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} style={seg(disabled || atMax)} onClick={() => onChange && onChange(max != null ? Math.min(max, value + step) : value + step)}><Icon name=\"plus\" size={12} /></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, 25.5px tall, r4 (\"Preview rows\"). 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, style: Object })\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: 30, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: dis ? 'not-allowed' : 'pointer', color: dis ? 'var(--text-muted)' : 'var(--text-body)', alignSelf: 'stretch', background: 'transparent', border: 'none', padding: 0 } }\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', alignItems: 'center', height: 25.5, borderRadius: 'var(--radius-sm)', border: '1px solid var(--border-control)', overflow: 'hidden', background: 'var(--bg-surface)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }\">\n <button type=\"button\" :disabled=\"disabled || atMin\" :style=\"seg(disabled || atMin)\" @click=\"emit('update:modelValue', Math.max(min, modelValue - step))\"><Icon name=\"minus\" :size=\"12\" /></button>\n <span style=\"min-width:34px;text-align:center;font-family:var(--font-sans);font-size:var(--text-sm);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\" :style=\"seg(disabled || atMax)\" @click=\"emit('update:modelValue', max != null ? Math.min(max, modelValue + step) : modelValue + step)\"><Icon name=\"plus\" :size=\"12\" /></button>\n </div>\n</template>\n","forms/Radio.vue":"<script setup>\nimport { ref, computed } from 'vue'\n// Radio — 16px (sm: 13px) circle, 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 dims = { sm: { box: 13, dot: 6, font: 'var(--text-xs)' }, md: { box: 16, dot: 8, font: 'var(--text-sm)' } }\nconst props = defineProps({ checked: Boolean, label: String, description: String, size: { type: String, default: 'md' }, disabled: Boolean, style: Object })\nconst emit = defineEmits(['change'])\nconst d = computed(() => dims[props.size] || dims.md)\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\" :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(--leading-sm)', gap: '8px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, outline: 'none', ...style }\">\n <span :style=\"{ width: d.box + 'px', height: d.box + 'px', marginTop: description ? '1px' : 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 + 'px', height: d.dot + 'px', 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=\"{ fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: d.font, lineHeight: 'var(--leading-sm)', color: 'var(--text-body)' }\">{{ label }}</span>\n <span v-if=\"description\" style=\"font-family:var(--font-sans);font-size:var(--text-xs);line-height:16px;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: { type: String, default: 'md' }, 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\" @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\" :size=\"size\" :disabled=\"o.disabled || disabled\" :style=\"{ opacity: 1, cursor: 'inherit' }\" @change=\"pick(o)\" />\n </div>\n </div>\n <div v-else role=\"radiogroup\" @keydown=\"onKey\" :style=\"{ display: 'flex', flexDirection: row ? 'row' : 'column', gap: (row ? 16 : 10) + 'px', ...style }\">\n <Radio v-for=\"o in opts\" :key=\"o.value\" :label=\"o.label\" :description=\"o.description\" :size=\"size\" :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 (r10) 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, 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 style={{ display: \"flex\", alignItems: \"center\", gap: bare ? 12 : 8, opacity: disabled ? .5 : 1, ...(bare ? null : { border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-lg)\", padding: \"6px 12px\", background: \"var(--bg-surface)\" }), ...style }}>\n <span style={{ color: \"var(--text-muted)\" }}><Icon name=\"search\" size={bare ? 16 : 14} /></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, fontFamily: \"var(--font-sans)\", fontSize: bare ? \"var(--text-base)\" : \"var(--text-sm)\", letterSpacing: bare ? \"var(--tracking-base)\" : 0, color: \"var(--text-primary)\", cursor: disabled ? \"not-allowed\" : \"text\" }} />\n {val && !disabled ? <button type=\"button\" onClick={() => set(\"\")} style={{ border: \"none\", background: \"transparent\", cursor: \"pointer\", color: \"var(--text-muted)\", display: \"flex\", padding: 0 }}><Icon name=\"x\" size={13} /></button> : null}\n </div>\n );\n}\n","forms/SearchInput.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Bordered search field (r10) 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, disabled: Boolean, style: Object })\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', alignItems: 'center', gap: bare ? 12 : 8, opacity: disabled ? .5 : 1, ...(bare ? null : { border: '1px solid var(--border-control)', borderRadius: 'var(--radius-lg)', padding: '6px 12px', background: 'var(--bg-surface)' }), ...style }\">\n <span style=\"color:var(--text-muted)\"><Icon name=\"search\" :size=\"bare ? 16 : 14\" /></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, fontFamily: 'var(--font-sans)', fontSize: bare ? 'var(--text-base)' : 'var(--text-sm)', letterSpacing: bare ? 'var(--tracking-base)' : 0, color: 'var(--text-primary)', cursor: disabled ? 'not-allowed' : 'text' }\" />\n <button v-if=\"modelValue && !disabled\" type=\"button\" @click=\"emit('update:modelValue', '')\" style=\"border:none;background:transparent;cursor:pointer;color:var(--text-muted);display:flex;padding:0\"><Icon name=\"x\" :size=\"13\" /></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. size md 36/r8 · sm 25/r4 (table rows). disabled = #fafafa inset fill (matches Input readOnly), 50% opacity; error = red border. */\nexport function Select({ options = [], size = \"md\", value, onChange, placeholder, disabled, error, style }) {\n const sm = size === \"sm\";\n return (\n <div style={{ position: \"relative\", display: \"inline-flex\", ...style }}>\n <select value={value} onChange={onChange} disabled={disabled}\n style={{ appearance: \"none\", WebkitAppearance: \"none\", height: sm ? 25 : 36, borderRadius: sm ? \"var(--radius-sm)\" : \"var(--radius-md)\", border: `1px solid ${error ? \"var(--danger)\" : \"var(--border-control)\"}`, background: disabled ? \"var(--bg-inset)\" : \"var(--bg-surface)\", padding: sm ? \"0 26px 0 10px\" : \"0 32px 0 12px\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", 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: sm ? 8 : 10, top: \"50%\", transform: \"translateY(-50%)\", pointerEvents: \"none\", color: \"var(--text-muted)\" }}><Icon name=\"chevron-down\" size={sm ? 12 : 14} /></span>\n </div>\n );\n}\n","forms/Select.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Native select styled per Keybricks. size md 36/r8 · sm 25/r4 (table rows). disabled = inset fill (matches Input readonly), 50% opacity; error = red border.\nconst props = defineProps({ modelValue: String, options: { type: Array, default: () => [] }, size: { type: String, default: 'md' }, placeholder: String, disabled: Boolean, error: Boolean, style: Object })\ndefineEmits(['update:modelValue'])\n</script>\n<template>\n <div :style=\"{ position: 'relative', display: 'inline-flex', ...style }\">\n <select :value=\"modelValue\" @change=\"$emit('update:modelValue', $event.target.value)\" :disabled=\"disabled\"\n :style=\"{ appearance: 'none', height: size === 'sm' ? 25 : 36, borderRadius: size === 'sm' ? 'var(--radius-sm)' : 'var(--radius-md)', border: `1px solid ${error ? 'var(--danger)' : 'var(--border-control)'}`, background: disabled ? 'var(--bg-inset)' : 'var(--bg-surface)', padding: size === 'sm' ? '0 26px 0 10px' : '0 32px 0 12px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', 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: size === 'sm' ? 8 : 10, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none', color: 'var(--text-muted)' }\"><Icon name=\"chevron-down\" :size=\"size === 'sm' ? 12 : 14\" /></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} 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: \"0 1px 2px rgba(0,0,0,.15)\" }}></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-mono)\", fontSize: \"var(--text-2xs)\", 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\" @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: '0 1px 2px rgba(0,0,0,.15)' }\" />\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-mono)', fontSize: 'var(--text-2xs)', 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 — 36×20 pill, 16px knob, 160ms ease-out slide.\n *\n * checked / onChange(next) controlled boolean\n * disabled 50% opacity, no pointer\n * size \"md\" (36×20, default) | \"lg\" (44×24 — needed 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\nconst SIZES = {\n sm: { w: 28, h: 16, knob: 12, pad: 2, inside: 9, glyph: 8, label: \"var(--text-xs)\" },\n md: { w: 36, h: 20, knob: 16, pad: 2, inside: 10, glyph: 10, label: \"var(--text-sm)\" },\n lg: { w: 44, h: 24, knob: 20, pad: 2, inside: 11, glyph: 12, label: \"var(--text-sm)\" }\n};\n\nconst pick = (v, on) => (v && typeof v === \"object\" ? (on ? v.on : v.off) : v);\n\nexport function Switch({ checked = false, onChange, disabled, size = \"md\", 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 const s = SIZES[size] || SIZES.md;\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\n const insideExtra = hasInside ? Math.max(0, Math.max(String(insideOn ?? \"\").length, String(insideOff ?? \"\").length) * (s.inside * .68) + 6 - (s.w - s.knob - s.pad * 2)) : 0;\n const w = s.w + insideExtra;\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 ? w - s.knob - s.pad : s.pad;\n\n const knob = <span aria-hidden=\"true\" style={{ position: \"absolute\", top: s.pad, left: knobX, width: s.knob, height: s.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: s.glyph, lineHeight: 1, fontWeight: 600, fontFamily: \"var(--font-sans)\", overflow: \"hidden\" }}>\n {loading\n ? <span style={{ width: s.glyph, height: s.glyph, borderRadius: \"50%\", border: `1.5px solid ${track}`, borderRightColor: \"transparent\", animation: \"kb-spin 720ms linear infinite\" }}></span>\n : glyphIcon ? <Icon name={glyphIcon} size={s.glyph} strokeWidth={2.5} />\n : glyphText ? <span>{String(glyphText).slice(0, 1)}</span> : null}\n </span>;\n\n const insideStyle = (side) => ({ position: \"absolute\", top: 0, bottom: 0, [side]: s.pad + 4, display: \"flex\", alignItems: \"center\", fontSize: s.inside, lineHeight: 1, fontWeight: 600, 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: s.h, borderRadius: \"var(--radius-full)\", background: 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 if (single) return <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 8, opacity: disabled ? .5 : 1, ...style }}>{pill}<span onClick={flip} style={{ fontSize: s.label, lineHeight: 1, fontFamily: \"var(--font-sans)\", fontWeight: 500, color: \"var(--text-body)\", cursor: inert ? \"default\" : \"pointer\", userSelect: \"none\" }}>{label}</span></span>;\n if (labelOn == null && labelOff == null) return <span style={{ display: \"inline-flex\", alignItems: \"center\", opacity: disabled ? .5 : 1, ...style }}>{pill}</span>;\n\n const cap = (text, active) => text == null ? null : <span style={{ fontSize: s.label, lineHeight: 1, fontFamily: \"var(--font-sans)\", fontWeight: active ? 500 : 400, color: active ? \"var(--text-heading)\" : \"var(--text-muted)\", transition: \"color 160ms ease-out\", cursor: inert ? \"default\" : \"pointer\", userSelect: \"none\" }} onClick={flip}>{text}</span>;\n return <span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 8, 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 — 36×20 pill (lg 44×24), 16/20px knob, 160ms slide. v-model:checked.\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: { type: String, default: 'md' }, tone: { type: String, default: 'primary' },\n onColor: String, offColor: String,\n knobIcon: [String, Object], knobText: [String, Object],\n insideOn: String, insideOff: String, label: String, labelOn: String, labelOff: String,\n beforeChange: Function, style: Object\n})\nconst emit = defineEmits(['update:checked', 'change'])\nconst SIZES = { sm: { w: 28, h: 16, knob: 12, pad: 2, inside: 9, glyph: 8 }, md: { w: 36, h: 20, knob: 16, pad: 2, inside: 10, glyph: 10 }, lg: { w: 44, h: 24, knob: 20, pad: 2, inside: 11, glyph: 12 } }\nconst s = computed(() => SIZES[props.size] || SIZES.md)\nconst busy = ref(false); let alive = true; onBeforeUnmount(() => { alive = false })\nconst isLoading = computed(() => props.loading || busy.value)\nconst inert = computed(() => props.disabled || isLoading.value)\nconst hasInside = computed(() => props.insideOn != null || props.insideOff != null)\nconst width = computed(() => { if (!hasInside.value) return s.value.w; const n = Math.max((props.insideOn || '').length, (props.insideOff || '').length); return s.value.w + Math.max(0, n * (s.value.inside * .68) + 6 - (s.value.w - s.value.knob - s.value.pad * 2)) })\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 ? width.value - s.value.knob - s.value.pad : s.value.pad)\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]: (s.value.pad + 4) + 'px', display: 'flex', alignItems: 'center', fontSize: s.value.inside + 'px', lineHeight: 1, fontWeight: 600, 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: props.size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', lineHeight: 1, fontFamily: 'var(--font-sans)', fontWeight: active ? 500 : 400, 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 :style=\"{ display: 'inline-flex', alignItems: 'center', gap: '8px', 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 + 'px', height: s.h + 'px', borderRadius: 'var(--radius-full)', background: 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)\">{{ insideOn }}</span>\n <span v-if=\"hasInside\" :style=\"insideStyle('right', false)\">{{ insideOff }}</span>\n <span aria-hidden=\"true\" :style=\"{ position: 'absolute', top: s.pad + 'px', left: knobX + 'px', width: s.knob + 'px', height: s.knob + 'px', 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: s.glyph + 'px', lineHeight: 1, fontWeight: 600, fontFamily: 'var(--font-sans)', overflow: 'hidden' }\">\n <span v-if=\"isLoading\" :style=\"{ width: s.glyph + 'px', height: s.glyph + 'px', 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=\"s.glyph\" :stroke-width=\"2.5\" />\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: size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', lineHeight: 1, fontFamily: 'var(--font-sans)', fontWeight: 500, 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. 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, 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', 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\" :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-mono)', 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.\nexport const inputBase = { height: 'var(--h-input)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-control)', background: 'var(--bg-surface)', padding: '8px 12px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', 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\nconst radioDims = { sm: { box: 13, dot: 6, font: \"var(--text-xs)\" }, md: { box: 16, dot: 8, font: \"var(--text-sm)\" } };\n\n/**\n * Radio — 16px (sm: 13px) circle, 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 = \"md\", disabled, style }) {\n const d = radioDims[size] || radioDims.md;\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\" 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(--leading-sm)\", gap: 8, 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 ? 1 : 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-medium)\", fontSize: d.font, lineHeight: \"var(--leading-sm)\", color: \"var(--text-body)\" }}>{label}</span> : null}\n {description ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", lineHeight: \"16px\", 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 = \"md\", 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\" 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} size={size} disabled={off} style={{ opacity: 1, cursor: \"inherit\" }} />\n </div>;\n })}\n </div>\n );\n }\n return (\n <div role=\"radiogroup\" onKeyDown={onKey} style={{ display: \"flex\", flexDirection: row ? \"row\" : \"column\", gap: row ? 16 : 10, ...style }}>\n {opts.map((o) => <Radio key={o.value} label={o.label} description={o.description} size={size} disabled={o.disabled || disabled} checked={value === o.value} onChange={() => onChange && onChange(o.value)} />)}\n </div>\n );\n}\n"});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"tabs/SegmentedControl.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/** Segmented control — inset track, raised active segment. options: string[] or [{label,icon}]. */\nexport function SegmentedControl({ options = [], value, onChange, size = \"md\", style }) {\n const h = size === \"sm\" ? 26 : 32;\n return (\n <div style={{ display: \"inline-flex\", padding: 2, gap: 2, background: \"var(--bg-inset)\", border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-md)\", boxSizing: \"border-box\", ...style }}>\n {options.map((o) => {\n const label = typeof o === \"string\" ? o : o.label;\n const is = value === label;\n return (\n <button key={label} type=\"button\" onClick={() => onChange && onChange(label)}\n style={{ height: h - 6, padding: \"0 10px\", display: \"inline-flex\", alignItems: \"center\", gap: 6, borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: \"pointer\", background: is ? \"var(--bg-surface)\" : \"transparent\", boxShadow: is ? \"0 1px 2px rgba(0,0,0,.08)\" : \"none\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: size === \"sm\" ? \"var(--text-xs)\" : \"var(--text-sm)\", color: is ? \"var(--text-heading)\" : \"var(--text-secondary)\", whiteSpace: \"nowrap\", transition: \"var(--t-hover)\" }}>\n {typeof o === \"object\" && o.icon ? <Icon name={o.icon} size={13} /> : null}{label}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/SegmentedControl.vue":"<script setup>\nimport Icon from '../icons/Icon.vue'\n// Segmented control — inset track, raised active segment. options: string[] or [{label,icon}].\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, size: { type: String, default: 'md' }, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nfunction label(o) { return typeof o === 'string' ? o : o.label }\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', padding: 2, gap: 2, background: 'var(--bg-inset)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box', ...style }\">\n <button v-for=\"o in options\" :key=\"label(o)\" type=\"button\" @click=\"emit('update:modelValue', label(o))\"\n :style=\"{ height: (size === 'sm' ? 26 : 32) - 6, padding: '0 10px', display: 'inline-flex', alignItems: 'center', gap: 6, borderRadius: 'var(--radius-sm)', border: 'none', cursor: 'pointer', background: modelValue === label(o) ? 'var(--bg-surface)' : 'transparent', boxShadow: modelValue === label(o) ? '0 1px 2px rgba(0,0,0,.08)' : 'none', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', color: modelValue === label(o) ? 'var(--text-heading)' : 'var(--text-secondary)', whiteSpace: 'nowrap', transition: 'var(--t-hover)' }\">\n <Icon v-if=\"typeof o === 'object' && o.icon\" :name=\"o.icon\" :size=\"13\" />{{ label(o) }}\n </button>\n </div>\n</template>\n","tabs/Tabs.jsx":"import React from \"react\";\n\n/** Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. */\nexport function Tabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", gap: 4, borderBottom: \"1px solid var(--border-control)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", padding: \"9px 16px\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: is ? \"var(--action-primary)\" : \"var(--text-body)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Compact panel tabs (Node Info · Calc Rules · Output Fields · SQL Preview), 12px. */\nexport function PanelTabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", borderBottom: \"1px solid var(--border-subtle)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", padding: \"8px 12px\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: is ? \"var(--action-primary)\" : \"var(--text-secondary)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Text pill filter tabs (For Review · To be Submitted · For Approval). Active = #f5f5f5 pill. */\nexport function PillTabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", gap: 8, ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: is ? \"var(--bg-active)\" : \"transparent\", border: \"none\", cursor: \"pointer\", height: 28, padding: \"0 11px\", borderRadius: \"var(--radius-md)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: is ? \"var(--text-heading)\" : \"var(--text-secondary)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/Tabs.vue":"<script setup>\n// Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline.\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: 4, borderBottom: '1px solid var(--border-control)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', padding: '9px 16px', marginBottom: -1, borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: t === active ? 'var(--action-primary)' : 'var(--text-body)' }\">{{ t }}</button>\n </div>\n</template>\n","tabs/ViewToggle.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Two-state list/grid icon toggle (palette header). */\nexport function ViewToggle({ value = \"grid\", onChange, style }) {\n return (\n <div style={{ display: \"inline-flex\", gap: 2, ...style }}>\n <IconButton icon=\"list\" size=\"sm\" active={value === \"list\"} onClick={() => onChange && onChange(\"list\")} label=\"List view\" />\n <IconButton icon=\"layout-grid\" size=\"sm\" active={value === \"grid\"} onClick={() => onChange && onChange(\"grid\")} label=\"Grid view\" />\n </div>\n );\n}\n","tabs/ViewToggle.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\n// Two-state list/grid icon toggle (palette header).\ndefineProps({ modelValue: { type: String, default: 'grid' }, style: Object })\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', gap: 2, ...style }\">\n <IconButton icon=\"list\" size=\"sm\" :active=\"modelValue === 'list'\" label=\"List view\" @click=\"emit('update:modelValue', 'list')\" />\n <IconButton icon=\"layout-grid\" size=\"sm\" :active=\"modelValue === 'grid'\" label=\"Grid view\" @click=\"emit('update:modelValue', 'grid')\" />\n </div>\n</template>\n","tabs/PanelTabs.vue":"<script setup>\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', borderBottom: '1px solid var(--border-subtle)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', padding: '8px 12px', marginBottom: -1, borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: t === active ? 'var(--action-primary)' : 'var(--text-secondary)' }\">{{ t }}</button>\n </div>\n</template>\n","tabs/PillTabs.vue":"<script setup>\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: 8, ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: t === active ? 'var(--bg-active)' : 'transparent', border: 'none', cursor: 'pointer', height: 28, padding: '0 11px', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: t === active ? 'var(--text-heading)' : 'var(--text-secondary)' }\">{{ t }}</button>\n </div>\n</template>\n"});
|
|
1
|
+
window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"tabs/SegmentedControl.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\n/**\n * Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.\n * options: string[] | { label, icon?, disabled? }[] value/onChange by label\n * size \"md\" (32) | \"sm\" (26) · iconOnly hides labels (label becomes aria-label / title)\n * disabled greys the whole control · fullWidth stretches segments evenly\n * Keyboard: ← → move between enabled segments.\n */\nexport function SegmentedControl({ options = [], value, onChange, size = \"md\", iconOnly, disabled, fullWidth, style }) {\n const h = size === \"sm\" ? 26 : 32;\n const opts = options.map((o) => (typeof o === \"string\" ? { label: o } : o));\n const enabled = opts.filter((o) => !o.disabled);\n const onKey = (e) => {\n if (e.key !== \"ArrowLeft\" && e.key !== \"ArrowRight\" || disabled || !enabled.length) return;\n e.preventDefault();\n const i = enabled.findIndex((o) => o.label === value);\n const n = e.key === \"ArrowRight\" ? enabled[(i + 1) % enabled.length] : enabled[(i - 1 + enabled.length) % enabled.length];\n onChange && onChange(n.label);\n };\n return (\n <div role=\"tablist\" aria-disabled={disabled || undefined} onKeyDown={onKey} style={{ display: fullWidth ? \"flex\" : \"inline-flex\", padding: 2, gap: 2, background: \"var(--bg-inset)\", border: \"1px solid var(--border-control)\", borderRadius: \"var(--radius-md)\", boxSizing: \"border-box\", opacity: disabled ? .5 : 1, ...style }}>\n {opts.map((o) => {\n const is = value === o.label, off = disabled || o.disabled;\n return (\n <button key={o.label} type=\"button\" role=\"tab\" aria-selected={is} aria-label={iconOnly ? o.label : undefined} title={iconOnly ? o.label : undefined} disabled={off} tabIndex={is ? 0 : -1}\n onClick={() => !off && onChange && onChange(o.label)}\n style={{ height: h - 6, padding: iconOnly ? \"0 8px\" : \"0 10px\", flex: fullWidth ? 1 : \"none\", justifyContent: \"center\", display: \"inline-flex\", alignItems: \"center\", gap: 6, borderRadius: \"var(--radius-sm)\", border: \"none\", cursor: off ? \"not-allowed\" : \"pointer\", background: is ? \"var(--bg-surface)\" : \"transparent\", boxShadow: is ? \"0 1px 2px rgba(0,0,0,.08)\" : \"none\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: size === \"sm\" ? \"var(--text-xs)\" : \"var(--text-sm)\", color: is ? \"var(--text-heading)\" : \"var(--text-secondary)\", opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: \"nowrap\", transition: \"var(--t-hover)\" }}>\n {o.icon ? <Icon name={o.icon} size={13} /> : null}{iconOnly ? null : o.label}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/SegmentedControl.vue":"<script setup>\nimport { computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\n// Segmented control — inset track, raised active segment. Switches views of the same data, applies instantly.\n// options: string[] | { label, icon?, disabled? }[] · v-model by label · size md (32) | sm (26)\n// iconOnly hides labels (label → aria-label/title) · disabled greys the control · fullWidth stretches segments · ← → keyboard.\nconst props = defineProps({ options: { type: Array, default: () => [] }, modelValue: String, size: { type: String, default: 'md' }, iconOnly: Boolean, disabled: Boolean, fullWidth: Boolean, style: Object })\nconst emit = defineEmits(['update:modelValue'])\nconst opts = computed(() => props.options.map(o => typeof o === 'string' ? { label: o } : o))\nconst enabled = computed(() => opts.value.filter(o => !o.disabled))\nconst h = computed(() => props.size === 'sm' ? 26 : 32)\nconst pick = (o) => { if (!(props.disabled || o.disabled)) emit('update:modelValue', o.label) }\nfunction onKey(e) {\n if ((e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') || props.disabled || !enabled.value.length) return\n e.preventDefault()\n const i = enabled.value.findIndex(o => o.label === props.modelValue), n = enabled.value.length\n emit('update:modelValue', (e.key === 'ArrowRight' ? enabled.value[(i + 1) % n] : enabled.value[(i - 1 + n) % n]).label)\n}\n</script>\n<template>\n <div role=\"tablist\" :aria-disabled=\"disabled || undefined\" @keydown=\"onKey\" :style=\"{ display: fullWidth ? 'flex' : 'inline-flex', padding: '2px', gap: '2px', background: 'var(--bg-inset)', border: '1px solid var(--border-control)', borderRadius: 'var(--radius-md)', boxSizing: 'border-box', opacity: disabled ? .5 : 1, ...style }\">\n <button v-for=\"o in opts\" :key=\"o.label\" type=\"button\" role=\"tab\" :aria-selected=\"modelValue === o.label\" :aria-label=\"iconOnly ? o.label : undefined\" :title=\"iconOnly ? o.label : undefined\" :disabled=\"disabled || o.disabled\" :tabindex=\"modelValue === o.label ? 0 : -1\" @click=\"pick(o)\"\n :style=\"{ height: (h - 6) + 'px', padding: iconOnly ? '0 8px' : '0 10px', flex: fullWidth ? 1 : 'none', justifyContent: 'center', display: 'inline-flex', alignItems: 'center', gap: '6px', borderRadius: 'var(--radius-sm)', border: 'none', cursor: (disabled || o.disabled) ? 'not-allowed' : 'pointer', background: modelValue === o.label ? 'var(--bg-surface)' : 'transparent', boxShadow: modelValue === o.label ? '0 1px 2px rgba(0,0,0,.08)' : 'none', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: size === 'sm' ? 'var(--text-xs)' : 'var(--text-sm)', color: modelValue === o.label ? 'var(--text-heading)' : 'var(--text-secondary)', opacity: o.disabled && !disabled ? .45 : 1, whiteSpace: 'nowrap', transition: 'var(--t-hover)' }\">\n <Icon v-if=\"o.icon\" :name=\"o.icon\" :size=\"13\" /><template v-if=\"!iconOnly\">{{ o.label }}</template>\n </button>\n </div>\n</template>\n","tabs/Tabs.jsx":"import React from \"react\";\n\n/** Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline. */\nexport function Tabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", gap: 4, borderBottom: \"1px solid var(--border-control)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", padding: \"9px 16px\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", color: is ? \"var(--action-primary)\" : \"var(--text-body)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Compact panel tabs (Node Info · Calc Rules · Output Fields · SQL Preview), 12px. */\nexport function PanelTabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", borderBottom: \"1px solid var(--border-subtle)\", ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: \"transparent\", border: \"none\", cursor: \"pointer\", padding: \"8px 12px\", marginBottom: -1, borderBottom: is ? \"2px solid var(--action-primary)\" : \"2px solid transparent\", fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: is ? \"var(--action-primary)\" : \"var(--text-secondary)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n\n/** Text pill filter tabs (For Review · To be Submitted · For Approval). Active = #f5f5f5 pill. */\nexport function PillTabs({ tabs = [], active, onChange, style }) {\n return (\n <div style={{ display: \"flex\", gap: 8, ...style }}>\n {tabs.map((t) => {\n const is = t === active;\n return (\n <button key={t} type=\"button\" onClick={() => onChange && onChange(t)}\n style={{ background: is ? \"var(--bg-active)\" : \"transparent\", border: \"none\", cursor: \"pointer\", height: 28, padding: \"0 11px\", borderRadius: \"var(--radius-md)\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", color: is ? \"var(--text-heading)\" : \"var(--text-secondary)\" }}>\n {t}\n </button>\n );\n })}\n </div>\n );\n}\n","tabs/Tabs.vue":"<script setup>\n// Underline tabs on a hairline baseline (Import/Export). Active = blue text + 2px blue underline.\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: 4, borderBottom: '1px solid var(--border-control)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', padding: '9px 16px', marginBottom: -1, borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: t === active ? 'var(--action-primary)' : 'var(--text-body)' }\">{{ t }}</button>\n </div>\n</template>\n","tabs/ViewToggle.jsx":"import React from \"react\";\nimport { IconButton } from \"../buttons/IconButton.jsx\";\n\n/** Two-state list/grid icon toggle (palette header). */\nexport function ViewToggle({ value = \"grid\", onChange, style }) {\n return (\n <div style={{ display: \"inline-flex\", gap: 2, ...style }}>\n <IconButton icon=\"list\" size=\"sm\" active={value === \"list\"} onClick={() => onChange && onChange(\"list\")} label=\"List view\" />\n <IconButton icon=\"layout-grid\" size=\"sm\" active={value === \"grid\"} onClick={() => onChange && onChange(\"grid\")} label=\"Grid view\" />\n </div>\n );\n}\n","tabs/ViewToggle.vue":"<script setup>\nimport IconButton from '../buttons/IconButton.vue'\n// Two-state list/grid icon toggle (palette header).\ndefineProps({ modelValue: { type: String, default: 'grid' }, style: Object })\nconst emit = defineEmits(['update:modelValue'])\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', gap: 2, ...style }\">\n <IconButton icon=\"list\" size=\"sm\" :active=\"modelValue === 'list'\" label=\"List view\" @click=\"emit('update:modelValue', 'list')\" />\n <IconButton icon=\"layout-grid\" size=\"sm\" :active=\"modelValue === 'grid'\" label=\"Grid view\" @click=\"emit('update:modelValue', 'grid')\" />\n </div>\n</template>\n","tabs/PanelTabs.vue":"<script setup>\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', borderBottom: '1px solid var(--border-subtle)', ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: 'transparent', border: 'none', cursor: 'pointer', padding: '8px 12px', marginBottom: -1, borderBottom: t === active ? '2px solid var(--action-primary)' : '2px solid transparent', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: t === active ? 'var(--action-primary)' : 'var(--text-secondary)' }\">{{ t }}</button>\n </div>\n</template>\n","tabs/PillTabs.vue":"<script setup>\ndefineProps({ tabs: { type: Array, default: () => [] }, active: String, style: Object })\nconst emit = defineEmits(['change'])\n</script>\n<template>\n <div :style=\"{ display: 'flex', gap: 8, ...style }\">\n <button v-for=\"t in tabs\" :key=\"t\" type=\"button\" @click=\"emit('change', t)\"\n :style=\"{ background: t === active ? 'var(--bg-active)' : 'transparent', border: 'none', cursor: 'pointer', height: 28, padding: '0 11px', borderRadius: 'var(--radius-md)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', color: t === active ? 'var(--text-heading)' : 'var(--text-secondary)' }\">{{ t }}</button>\n </div>\n</template>\n"});
|
|
@@ -33,6 +33,7 @@ function prettyHtml(html){
|
|
|
33
33
|
}
|
|
34
34
|
function resolveSrc(path, framework, cb){
|
|
35
35
|
var p = framework === 'vue' ? path.replace(/\.jsx$/, '.vue') : path;
|
|
36
|
+
if (framework === 'vue' && /\.jsx$/.test(path)) { var src = window.__KB_SRC_MANIFEST__ && window.__KB_SRC_MANIFEST__[path]; if (src && src.vue) p = src.vue; }
|
|
36
37
|
var key = p.replace(/^\.\.\//, '');
|
|
37
38
|
function hit(){ return window.__KB_SRC__ && window.__KB_SRC__[key] != null ? window.__KB_SRC__[key] : null; }
|
|
38
39
|
var t = hit(); if (t != null) return cb(t);
|
|
@@ -45,7 +46,8 @@ function resolveSrc(path, framework, cb){
|
|
|
45
46
|
function serve(files){
|
|
46
47
|
var framed; try { framed = window.parent !== window; } catch(e){ framed = true; }
|
|
47
48
|
if (!framed) return false;
|
|
48
|
-
var list = files.map(function(f){
|
|
49
|
+
var list = files.map(function(f){ var o = { name: f.name, path: f.path }; if (f.vue) o.vue = f.vue; if (f.vueName) o.vueName = f.vueName; if (f.vueOnly) o.vueOnly = true; if (f.reactOnly) o.reactOnly = true; return o; });
|
|
50
|
+
window.__KB_SRC_MANIFEST__ = {}; list.forEach(function(o){ window.__KB_SRC_MANIFEST__[o.path] = o; });
|
|
49
51
|
window.addEventListener('message', function(e){
|
|
50
52
|
var m = e.data; if (!m) return;
|
|
51
53
|
if (m.kb === 'src-get') {
|
|
@@ -98,9 +100,11 @@ function mount(rootSelector, files){
|
|
|
98
100
|
});
|
|
99
101
|
Array.prototype.forEach.call(tabBar.children, function(el, i){
|
|
100
102
|
if (tabs[i].html) return;
|
|
101
|
-
el.textContent = framework === 'vue' ? tabs[i].name.replace(/\.jsx$/, '.vue') : tabs[i].name;
|
|
103
|
+
el.textContent = framework === 'vue' ? (tabs[i].vueName || tabs[i].name.replace(/\.jsx$/, '.vue')) : tabs[i].name;
|
|
104
|
+
el.style.display = (framework === 'vue' && tabs[i].reactOnly) || (framework === 'react' && tabs[i].vueOnly) ? 'none' : '';
|
|
102
105
|
});
|
|
103
|
-
if (active !== -1) selectTab(
|
|
106
|
+
if (active !== -1 && tabBar.children[active].style.display === 'none') { for (var k = 0; k < tabs.length; k++) if (tabBar.children[k].style.display !== 'none') { selectTab(k); break; } }
|
|
107
|
+
else if (active !== -1) selectTab(active);
|
|
104
108
|
};
|
|
105
109
|
fwBar.appendChild(fb);
|
|
106
110
|
});
|
|
@@ -130,7 +134,7 @@ function mount(rootSelector, files){
|
|
|
130
134
|
};
|
|
131
135
|
}
|
|
132
136
|
{
|
|
133
|
-
var p = framework === 'vue' ? t.path.replace(/\.jsx$/, '.vue') : t.path;
|
|
137
|
+
var p = framework === 'vue' ? (t.vue || t.path.replace(/\.jsx$/, '.vue')) : t.path;
|
|
134
138
|
var key = p.replace(/^\.\.\//, '');
|
|
135
139
|
var fromCache = function(){
|
|
136
140
|
if (window.__KB_SRC__ && window.__KB_SRC__[key] != null) { setText(window.__KB_SRC__[key]); return true; }
|
|
@@ -164,6 +168,7 @@ function mount(rootSelector, files){
|
|
|
164
168
|
var tb = document.createElement('button');
|
|
165
169
|
tb.type = 'button';
|
|
166
170
|
tb.textContent = t.name;
|
|
171
|
+
if (t.vueOnly) tb.style.display = 'none';
|
|
167
172
|
tb.style.cssText = 'padding:8px 12px;border:none;background:transparent;font-size:12px;font-family:var(--font-sans,sans-serif);cursor:pointer;white-space:nowrap;margin-bottom:-1px;border-bottom:2px solid transparent;';
|
|
168
173
|
tb.onclick = function(){ selectTab(i); };
|
|
169
174
|
tabBar.appendChild(tb);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Radio — 16px (sm: 13px) circle; filled action-blue dot when selected. Hover darkens the ring, focus shows the 2px ring. */
|
|
3
|
+
export interface RadioProps {
|
|
4
|
+
checked?: boolean;
|
|
5
|
+
onChange?: (checked: true) => void;
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Muted second line under the label */
|
|
8
|
+
description?: string;
|
|
9
|
+
size?: "sm" | "md";
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
}
|
|
13
|
+
export declare function Radio(props: RadioProps): JSX.Element;
|
|
14
|
+
/** Single choice from a small set. Arrow keys move between enabled options. */
|
|
15
|
+
export interface RadioGroupProps {
|
|
16
|
+
options?: Array<string | { label: string; value: string; description?: string; disabled?: boolean }>;
|
|
17
|
+
value?: string;
|
|
18
|
+
onChange?: (value: string) => void;
|
|
19
|
+
/** Horizontal layout */
|
|
20
|
+
row?: boolean;
|
|
21
|
+
size?: "sm" | "md";
|
|
22
|
+
/** plain (default) · cards — each option is a bordered selectable card */
|
|
23
|
+
variant?: "plain" | "cards";
|
|
24
|
+
/** Disables the whole group */
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
style?: React.CSSProperties;
|
|
27
|
+
}
|
|
28
|
+
export declare function RadioGroup(props: RadioGroupProps): JSX.Element;
|