@keyfunc/keybricks-ds 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/INTEGRATION.md +42 -1
  2. package/_scripts/build-tokens.mjs +3 -3
  3. package/components/_shared/frame-theme.js +5 -0
  4. package/components/_shared/src-buttons.js +1 -1
  5. package/components/_shared/src-data.js +1 -1
  6. package/components/_shared/src-display.js +1 -1
  7. package/components/_shared/src-feedback.js +1 -1
  8. package/components/_shared/src-forms.js +1 -1
  9. package/components/_shared/src-navigation.js +1 -1
  10. package/components/_shared/src-pipeline.js +1 -1
  11. package/components/_shared/src-tabs.js +1 -1
  12. package/components/_vue/index.mjs +21 -0
  13. package/components/buttons/Button.vue +0 -20
  14. package/components/buttons/ButtonGroup.vue +16 -0
  15. package/components/data/KeyValue.vue +0 -16
  16. package/components/data/KeyValueList.vue +12 -0
  17. package/components/data/ListRow.vue +1 -1
  18. package/components/display/AreaChart.vue +14 -0
  19. package/components/display/Avatar.vue +0 -17
  20. package/components/display/AvatarGroup.vue +13 -0
  21. package/components/display/Badge.vue +0 -34
  22. package/components/display/Card.vue +0 -15
  23. package/components/display/Donut.vue +17 -0
  24. package/components/display/IconTile.vue +13 -0
  25. package/components/display/MiniBars.vue +11 -0
  26. package/components/display/ProgressBar.vue +0 -54
  27. package/components/display/StatCard.vue +11 -0
  28. package/components/display/StatusBadge.vue +11 -0
  29. package/components/feedback/ActivityItem.vue +1 -32
  30. package/components/feedback/Alert.vue +0 -36
  31. package/components/feedback/DotActivity.vue +15 -0
  32. package/components/feedback/EmptyState.vue +12 -0
  33. package/components/feedback/LogViewer.vue +8 -0
  34. package/components/feedback/Modal.vue +0 -45
  35. package/components/feedback/Skeleton.vue +6 -0
  36. package/components/feedback/Spinner.vue +6 -0
  37. package/components/feedback/Toast.vue +19 -0
  38. package/components/forms/Combobox.vue +1 -1
  39. package/components/forms/DatePicker.vue +1 -1
  40. package/components/forms/Input.jsx +2 -1
  41. package/components/forms/Input.vue +2 -24
  42. package/components/forms/Textarea.vue +20 -0
  43. package/components/forms/input-base.js +3 -0
  44. package/components/navigation/Breadcrumb.vue +0 -22
  45. package/components/navigation/PageHeading.vue +9 -0
  46. package/components/navigation/PageToolbar.vue +15 -0
  47. package/components/navigation/ThemeToggle.vue +18 -0
  48. package/components/navigation/TopBar.vue +0 -32
  49. package/components/pipeline/NodeCard.vue +2 -1
  50. package/components/pipeline/PaletteItem.vue +1 -1
  51. package/components/pipeline/PalettePanel.jsx +2 -8
  52. package/components/pipeline/PalettePanel.vue +1 -8
  53. package/components/pipeline/pipeline-catalog.js +8 -0
  54. package/components/tabs/PanelTabs.vue +10 -0
  55. package/components/tabs/PillTabs.vue +10 -0
  56. package/components/tabs/Tabs.vue +0 -28
  57. package/package.json +1 -1
  58. package/readme.md +1 -0
  59. package/tokens/alpha.css +0 -4
  60. package/tokens/charts.css +0 -6
  61. package/tokens/colors-extended.css +0 -1
  62. package/tokens/dark.css +4 -46
  63. package/tokens/interaction.css +0 -1
  64. package/tokens/theme.js +11 -0
  65. package/tokens/tokens.d.ts +1 -1
  66. package/tokens/tokens.js +1 -1
  67. package/tokens/tokens.json +2 -2
package/INTEGRATION.md CHANGED
@@ -81,6 +81,7 @@ export function LoginForm() {
81
81
  import Button from '@keyfunc/keybricks-ds/components/buttons/Button.vue'
82
82
  import Field from '@keyfunc/keybricks-ds/components/forms/Field.vue'
83
83
  import Input from '@keyfunc/keybricks-ds/components/forms/Input.vue'
84
+ import Textarea from '@keyfunc/keybricks-ds/components/forms/Textarea.vue'
84
85
  </script>
85
86
 
86
87
  <template>
@@ -126,6 +127,23 @@ Import path is `@keyfunc/keybricks-ds/components/<group>/<Name>.jsx` (React) or
126
127
  Every component has a sibling `.d.ts` with its full props contract, and each group has a
127
128
  `.prompt.md` with usage notes.
128
129
 
130
+ **One difference between the two trees.** A `.jsx` file may export several related
131
+ components (`Input.jsx` exports `Input` and `Textarea`; `Badge.jsx` exports `Badge` and
132
+ `IconTile`). Vue single-file components hold exactly one component and `<script setup>`
133
+ cannot export values, so on the Vue side every such secondary is its own file with a default
134
+ export — `Textarea.vue`, `IconTile.vue`, `StatusBadge.vue`, `ButtonGroup.vue`, `StatCard.vue`, `Toast.vue`,
135
+ `Spinner.vue`, `PageHeading.vue`, and so on — always named exactly as the React export.
136
+ Shared non-component values live in plain modules: `forms/input-base.js` (`inputBase`),
137
+ `pipeline/pipeline-catalog.js` (`PIPELINE_CATALOG`), `icons/icon-paths.js`.
138
+
139
+ ```js
140
+ // React
141
+ import { Input, Textarea } from '@keyfunc/keybricks-ds/components/forms/Input.jsx'
142
+ // Vue
143
+ import Input from '@keyfunc/keybricks-ds/components/forms/Input.vue'
144
+ import Textarea from '@keyfunc/keybricks-ds/components/forms/Textarea.vue'
145
+ ```
146
+
129
147
  ### `brand/` — brand expression
130
148
  | Component | What it's for |
131
149
  | --- | --- |
@@ -346,7 +364,30 @@ the CSS.
346
364
 
347
365
  ## Dark mode
348
366
 
349
- Automatic via `prefers-color-scheme`, and overridable with `data-theme` on `<html>`.
367
+ Three states, decided by the `data-theme` attribute on `<html>`:
368
+
369
+ | `<html data-theme>` | Result |
370
+ | --- | --- |
371
+ | *(absent)* | light — or the OS preference, if `tokens/theme.js` is loaded |
372
+ | `"dark"` | dark, regardless of the OS |
373
+ | `"light"` | light, regardless of the OS |
374
+
375
+ ```js
376
+ document.documentElement.dataset.theme = 'light' // or 'dark'; delete it to follow the OS
377
+ ```
378
+
379
+ The stylesheet reads only the attribute — there is deliberately no `@media (prefers-color-scheme)`
380
+ in the tokens, because an OS media query on `:root` cannot be overridden by an attribute. To
381
+ follow the OS, load the bootstrap in `<head>` **before** `styles.css`:
382
+
383
+ ```html
384
+ <script src="@keyfunc/keybricks-ds/tokens/theme.js"></script>
385
+ ```
386
+
387
+ It stamps `data-theme` from the OS only when nothing is set, tracks OS changes live, and steps
388
+ aside the moment your app sets the attribute itself. Persist the user's choice yourself and
389
+ apply it before this script runs to avoid a flash.
390
+
350
391
  If you build custom UI beside Keybricks components, reference the same semantic tokens and
351
392
  it will flip for free. Two things to watch:
352
393
 
@@ -78,10 +78,10 @@ const q = s => JSON.stringify(s);
78
78
 
79
79
  const outJson = JSON.stringify({
80
80
  name: '@keyfunc/keybricks-ds', version, source: 'tokens/*.css',
81
- note: 'Generated by scripts/build-tokens.mjs — CSS is the source of truth.', tokens
81
+ note: 'Generated by _scripts/build-tokens.mjs — CSS is the source of truth.', tokens
82
82
  }, null, 2) + '\n';
83
83
 
84
- const outJs = `/* Generated from tokens/*.css by scripts/build-tokens.mjs — do not edit by hand.
84
+ const outJs = `/* Generated from tokens/*.css by _scripts/build-tokens.mjs — do not edit by hand.
85
85
  cssVar keeps theming live (preferred in components); light/dark are flattened
86
86
  literals for canvas, PDF, native and export pipelines. */
87
87
 
@@ -102,7 +102,7 @@ export const tokenNames = ${JSON.stringify(names)};
102
102
  export default { cssVar, light, dark, tokenNames };
103
103
  `;
104
104
 
105
- const outDts = `/* Generated from tokens/*.css by scripts/build-tokens.mjs — do not edit by hand. */
105
+ const outDts = `/* Generated from tokens/*.css by _scripts/build-tokens.mjs — do not edit by hand. */
106
106
  export type TokenName =
107
107
  | ${names.map(q).join('\n | ')};
108
108
  export type TokenKey =
@@ -10,6 +10,11 @@ window.addEventListener('message', function(e){
10
10
  var m = e.data;
11
11
  if (m && m.kb === 'theme') apply(m.theme);
12
12
  });
13
+ /* Standalone (no host): follow the OS until the host answers. */
14
+ if (!document.documentElement.hasAttribute('data-theme')) {
15
+ var mq = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
16
+ apply(mq && mq.matches ? 'dark' : 'light');
17
+ }
13
18
  try {
14
19
  if (window.parent !== window) window.parent.postMessage({ kb: 'theme-ask' }, '*');
15
20
  } catch(e){}
@@ -1 +1 @@
1
- window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"buttons/ActionChip.jsx":"import React from \"react\";\n\nconst chipTones = {\n success: { background: \"var(--success-bg)\", color: \"var(--success-text)\" },\n warning: { background: \"var(--warning-bg)\", color: \"var(--warning-text)\" },\n neutral: { background: \"var(--bg-hover)\", color: \"var(--text-body)\" },\n info: { background: \"var(--info-bg)\", color: \"var(--action-primary)\" },\n danger: { background: \"var(--danger-bg)\", color: \"var(--danger-text)\" },\n};\n\n/** Tinted bulk-action chip (\"Mark as New\", \"Mark as Overwrite\", \"Skip Selected\"). */\nexport function ActionChip({ tone = \"neutral\", children, onClick, style }) {\n const [hover, setHover] = React.useState(false);\n return (\n <button type=\"button\" onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ height: 30, padding: \"0 12px\", borderRadius: \"var(--radius-md)\", border: \"none\", cursor: \"pointer\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", display: \"inline-flex\", alignItems: \"center\", filter: hover ? \"brightness(1.06) saturate(1.05)\" : \"none\", transition: \"var(--t-hover),filter var(--dur-fast) var(--ease-out)\", ...chipTones[tone], ...style }}>\n {children}\n </button>\n );\n}\n","buttons/ActionChip.vue":"<script setup>\nimport { ref, computed } from 'vue'\nconst chipTones = {\n success: { background: 'var(--success-bg)', color: 'var(--success-text)' },\n warning: { background: 'var(--warning-bg)', color: 'var(--warning-text)' },\n neutral: { background: 'var(--bg-hover)', color: 'var(--text-body)' },\n info: { background: 'var(--info-bg)', color: 'var(--action-primary)' },\n danger: { background: 'var(--danger-bg)', color: 'var(--danger-text)' },\n}\n// Tinted bulk-action chip (\"Mark as New\", \"Mark as Overwrite\", \"Skip Selected\").\nconst props = defineProps({ tone: { type: String, default: 'neutral' }, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\nconst s = computed(() => ({ height: 30, padding: '0 12px', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', display: 'inline-flex', alignItems: 'center', filter: hover.value ? 'brightness(1.06) saturate(1.05)' : 'none', transition: 'var(--t-hover),filter var(--dur-fast) var(--ease-out)', ...chipTones[props.tone], ...props.style }))\n</script>\n<template>\n <button type=\"button\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\" :style=\"s\"><slot /></button>\n</template>\n","buttons/Button.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst btnBase = { display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-regular)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", border: \"1px solid transparent\", cursor: \"pointer\", whiteSpace: \"nowrap\", boxSizing: \"border-box\", background: \"transparent\", transition: \"var(--t-hover)\" };\n\nconst variants = {\n primary: { background: \"var(--action-primary)\", color: \"var(--on-accent)\" },\n dark: { background: \"var(--action-dark)\", color: \"var(--action-dark-fg)\" },\n outline: { background: \"var(--bg-surface)\", color: \"var(--text-body)\", borderColor: \"var(--border-control)\" },\n ghost: { color: \"var(--text-body)\" },\n \"primary-soft\": { background: \"var(--soft-bg)\", color: \"var(--action-primary)\", borderColor: \"var(--soft-border)\" },\n danger: { background: \"var(--danger)\", color: \"var(--on-accent)\" },\n \"danger-soft\": { background: \"var(--danger-bg)\", color: \"var(--danger-text)\" },\n text: { color: \"var(--text-secondary)\", padding: \"0\", height: \"auto\" },\n};\nconst hovers = {\n primary: \"var(--action-primary-hover)\", dark: \"var(--action-dark-hover)\",\n outline: \"var(--bg-hover)\", ghost: \"var(--bg-hover)\", \"primary-soft\": \"var(--soft-hover)\",\n danger: \"var(--danger-text)\", \"danger-soft\": \"var(--danger-bg)\",\n};\n/** Press (mouse-down) fills — one step past hover. */\nconst presses = {\n primary: \"var(--action-primary-active)\", dark: \"var(--action-dark-active)\",\n outline: \"var(--bg-press)\", ghost: \"var(--bg-press)\", \"primary-soft\": \"var(--soft-active)\",\n danger: \"var(--danger-active)\", \"danger-soft\": \"var(--danger-soft-active)\",\n};\n/** Variants that change text color (+ underline) on hover instead of a background wash. */\nconst colorHoverVariants = { text: \"var(--text-heading)\" };\nconst sizes = {\n sm: { height: 30, padding: \"6px 12px\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", letterSpacing: 0, borderRadius: \"var(--radius-md)\" },\n md: { height: 32, padding: \"6px 12px\", borderRadius: \"var(--radius-md)\" },\n lg: { height: 35.5, padding: \"8px 16px\", fontWeight: \"var(--weight-medium)\", borderRadius: \"var(--radius-lg)\" },\n};\nconst iconOnlyPad = { sm: 0, md: 0, lg: 0 };\nconst iconOnlyBox = { sm: 30, md: 32, lg: 36 };\n\nfunction Spinner({ size, color }) {\n return <span style={{ display: \"inline-block\", width: size, height: size, borderRadius: \"var(--radius-full)\", border: \"2px solid currentColor\", borderTopColor: \"transparent\", opacity: .9, color, animation: \"kb-spin .7s linear infinite\", flexShrink: 0 }} />;\n}\n\n/** Keybricks button. variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm | md | lg. Interaction states: hover (one step darker) · press (two steps, set via mouse-down) · keyboard focus ring · loading · disabled. */\nexport function Button({ variant = \"primary\", size = \"md\", icon, iconSize, iconPosition = \"left\", medium, disabled, loading, fullWidth, iconOnly, label, pressed, focused, children, style, onClick, ...rest }) {\n const [hover, setHover] = React.useState(false);\n const [press, setPress] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const busy = disabled || loading;\n const isPressed = press || pressed;\n const isFocused = focus || focused;\n const s = {\n ...btnBase, ...sizes[size], ...variants[variant],\n ...(medium ? { fontWeight: \"var(--weight-medium)\" } : null),\n ...(fullWidth ? { width: \"100%\" } : null),\n ...(iconOnly ? { width: iconOnlyBox[size], padding: iconOnlyPad[size], gap: 0 } : null),\n ...(busy ? { opacity: .55, cursor: \"default\" } : null),\n ...style,\n };\n if (!busy) {\n if (colorHoverVariants[variant]) {\n if (hover || isPressed) { s.color = colorHoverVariants[variant]; s.textDecoration = \"underline\"; }\n if (isPressed) s.opacity = .7;\n } else if (isPressed) {\n s.background = presses[variant];\n } else if (hover) {\n s.background = hovers[variant];\n }\n if (isFocused) s.boxShadow = \"var(--ring-focus)\";\n }\n const iSize = iconSize || (size === \"sm\" ? 12 : 14);\n const leading = loading ? <Spinner size={iSize} color={s.color} /> : (icon && iconPosition === \"left\" ? <Icon name={icon} size={iSize} /> : null);\n const trailing = !loading && icon && iconPosition === \"right\" ? <Icon name={icon} size={iSize} /> : null;\n return (\n <button type=\"button\" data-press=\"own\" style={s} disabled={busy} aria-label={iconOnly ? label : undefined} onClick={onClick}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}\n onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}\n onKeyDown={(e) => { if (e.key === \" \" || e.key === \"Enter\") setPress(true); }} onKeyUp={() => setPress(false)}\n onFocus={(e) => { try { setFocus(e.target.matches(\":focus-visible\")); } catch (err) { setFocus(true); } }} onBlur={() => setFocus(false)} {...rest}>\n {leading}\n {iconOnly ? null : children}\n {trailing}\n </button>\n );\n}\n\n/** Attached row of Buttons sharing one outline (segmented action group). Children should be <Button variant=\"outline\"> or similar. */\nexport function ButtonGroup({ children, style }) {\n const items = React.Children.toArray(children);\n return (\n <div style={{ display: \"inline-flex\", ...style }}>\n {items.map((child, i) => React.cloneElement(child, {\n key: i,\n style: {\n ...(child.props.style || {}),\n borderRadius: 0,\n ...(i === 0 ? { borderTopLeftRadius: \"var(--radius-md)\", borderBottomLeftRadius: \"var(--radius-md)\" } : null),\n ...(i === items.length - 1 ? { borderTopRightRadius: \"var(--radius-md)\", borderBottomRightRadius: \"var(--radius-md)\" } : null),\n marginLeft: i === 0 ? 0 : -1,\n },\n }))}\n </div>\n );\n}\n","buttons/Button.vue":"<script setup>\nimport { computed, ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n\nconst btnBase = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-regular)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', border: '1px solid transparent', cursor: 'pointer', whiteSpace: 'nowrap', boxSizing: 'border-box', background: 'transparent', transition: 'var(--t-hover)' }\nconst variants = {\n primary: { background: 'var(--action-primary)', color: 'var(--on-accent)' },\n dark: { background: 'var(--action-dark)', color: 'var(--action-dark-fg)' },\n outline: { background: 'var(--bg-surface)', color: 'var(--text-body)', borderColor: 'var(--border-control)' },\n ghost: { color: 'var(--text-body)' },\n 'primary-soft': { background: 'var(--soft-bg)', color: 'var(--action-primary)', borderColor: 'var(--soft-border)' },\n danger: { background: 'var(--danger)', color: 'var(--on-accent)' },\n 'danger-soft': { background: 'var(--danger-bg)', color: 'var(--danger-text)' },\n text: { color: 'var(--text-secondary)', padding: '0', height: 'auto' },\n}\nconst hovers = { primary: 'var(--action-primary-hover)', dark: 'var(--action-dark-hover)', outline: 'var(--bg-hover)', ghost: 'var(--bg-hover)', 'primary-soft': 'var(--soft-hover)', danger: 'var(--danger-text)', 'danger-soft': 'var(--danger-bg)' }\nconst presses = { primary: 'var(--action-primary-active)', dark: 'var(--action-dark-active)', outline: 'var(--bg-press)', ghost: 'var(--bg-press)', 'primary-soft': 'var(--soft-active)', danger: 'var(--danger-active)', 'danger-soft': 'var(--danger-soft-active)' }\nconst colorHoverVariants = { text: 'var(--text-heading)' }\nconst sizes = {\n sm: { height: 30, padding: '6px 12px', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', letterSpacing: 0, borderRadius: 'var(--radius-md)' },\n md: { height: 32, padding: '6px 12px', borderRadius: 'var(--radius-md)' },\n lg: { height: 35.5, padding: '8px 16px', fontWeight: 'var(--weight-medium)', borderRadius: 'var(--radius-lg)' },\n}\nconst iconOnlyPad = { sm: 0, md: 0, lg: 0 }\nconst iconOnlyBox = { sm: 30, md: 32, lg: 36 }\n\n// Button — variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm | md | lg.\nconst props = defineProps({\n variant: { type: String, default: 'primary' }, size: { type: String, default: 'md' },\n icon: String, iconSize: Number, iconPosition: { type: String, default: 'left' },\n medium: Boolean, disabled: Boolean, loading: Boolean, fullWidth: Boolean, iconOnly: Boolean,\n label: String, pressed: Boolean, focused: Boolean, style: Object,\n})\ndefineEmits(['click'])\nconst hover = ref(false), press = ref(false), focus = ref(false)\nconst busy = computed(() => props.disabled || props.loading)\nconst isPressed = computed(() => press.value || props.pressed)\nconst isFocused = computed(() => focus.value || props.focused)\nconst s = computed(() => {\n const base = { ...btnBase, ...sizes[props.size], ...variants[props.variant],\n ...(props.medium ? { fontWeight: 'var(--weight-medium)' } : null),\n ...(props.fullWidth ? { width: '100%' } : null),\n ...(props.iconOnly ? { width: iconOnlyBox[props.size], padding: iconOnlyPad[props.size], gap: 0 } : null),\n ...(busy.value ? { opacity: .55, cursor: 'default' } : null), ...props.style }\n if (!busy.value) {\n if (colorHoverVariants[props.variant]) {\n if (hover.value || isPressed.value) { base.color = colorHoverVariants[props.variant]; base.textDecoration = 'underline' }\n if (isPressed.value) base.opacity = .7\n } else if (isPressed.value) base.background = presses[props.variant]\n else if (hover.value) base.background = hovers[props.variant]\n if (isFocused.value) base.boxShadow = 'var(--ring-focus)'\n }\n return base\n})\nconst iSize = computed(() => props.iconSize || (props.size === 'sm' ? 12 : 14))\n</script>\n<template>\n <button type=\"button\" data-press=\"own\" :style=\"s\" :disabled=\"busy\" :aria-label=\"iconOnly ? label : undefined\"\n @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false; press = false\"\n @mousedown=\"press = true\" @mouseup=\"press = false\" @keydown.enter=\"press = true\" @keydown.space=\"press = true\" @keyup=\"press = false\"\n @focus=\"focus = $event.target.matches(':focus-visible')\" @blur=\"focus = false\">\n <span v-if=\"loading\" :style=\"{ display: 'inline-block', width: iSize, height: iSize, borderRadius: 'var(--radius-full)', border: '2px solid currentColor', borderTopColor: 'transparent', opacity: .9, animation: 'kb-spin .7s linear infinite', flexShrink: 0 }\" />\n <Icon v-else-if=\"icon && iconPosition === 'left'\" :name=\"icon\" :size=\"iSize\" />\n <slot v-if=\"!iconOnly\" />\n <Icon v-if=\"!loading && icon && iconPosition === 'right'\" :name=\"icon\" :size=\"iSize\" />\n </button>\n</template>\n\n<!-- ButtonGroup — attached row of Buttons sharing one outline (segmented action group). -->\n<!--\n<script setup name=\"ButtonGroup\">\nimport { useSlots, computed } from 'vue'\nconst props = defineProps({ style: Object })\nconst slots = useSlots()\nconst items = computed(() => slots.default ? slots.default() : [])\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', ...style }\">\n <template v-for=\"(child, i) in items\" :key=\"i\">\n <component :is=\"child\" :style=\"{ borderRadius: 0,\n ...(i === 0 ? { borderTopLeftRadius: 'var(--radius-md)', borderBottomLeftRadius: 'var(--radius-md)' } : null),\n ...(i === items.length - 1 ? { borderTopRightRadius: 'var(--radius-md)', borderBottomRightRadius: 'var(--radius-md)' } : null),\n marginLeft: i === 0 ? 0 : -1 }\" />\n </template>\n </div>\n</template>\n-->\n","buttons/IconButton.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst ibSizes = { xs: { box: 22, icon: 14, radius: \"var(--radius-md)\" }, sm: { box: 26, icon: 14, radius: \"var(--radius-sm)\" }, md: { box: 28, icon: 16, radius: \"var(--radius-md)\" }, lg: { box: 34, icon: 18, radius: \"var(--radius-md)\" } };\n\n/** Square ghost icon button (toolbar/back/bell/view toggles). size: xs 22 | sm 26 | md 28 | lg 34. danger tints red; loading shows a spinner and disables interaction. States: hover · press · keyboard focus ring · active (persistent toggle-on) · disabled. */\nexport function IconButton({ icon, size = \"md\", active, muted, danger, disabled, loading, label, pressed, focused, style, onClick, ...rest }) {\n const [hover, setHover] = React.useState(false);\n const [press, setPress] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const t = ibSizes[size];\n const busy = disabled || loading;\n const isPressed = (press || pressed) && !busy;\n const color = danger ? \"var(--danger)\" : muted ? \"var(--text-muted)\" : \"var(--text-body)\";\n const bg = isPressed ? (danger ? \"var(--danger-soft-active)\" : \"var(--bg-press)\")\n : active ? \"var(--bg-active)\"\n : hover && !busy ? (danger ? \"var(--danger-bg)\" : \"var(--bg-hover)\") : \"transparent\";\n return (\n <button type=\"button\" data-press=\"own\" aria-label={label} disabled={busy} onClick={onClick}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}\n onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}\n onKeyDown={(e) => { if (e.key === \" \" || e.key === \"Enter\") setPress(true); }} onKeyUp={() => setPress(false)}\n onFocus={(e) => { try { setFocus(e.target.matches(\":focus-visible\")); } catch (err) { setFocus(true); } }} onBlur={() => setFocus(false)}\n style={{ width: t.box, height: t.box, borderRadius: t.radius, border: \"none\", cursor: busy ? \"default\" : \"pointer\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", background: bg, color, opacity: busy ? .5 : 1, boxShadow: (focus || focused) && !busy ? \"var(--ring-focus)\" : \"none\", transition: \"var(--t-hover)\", padding: 0, flexShrink: 0, ...style }} {...rest}>\n {loading ? <span style={{ display: \"inline-block\", width: t.icon, height: t.icon, borderRadius: \"var(--radius-full)\", border: \"2px solid currentColor\", borderTopColor: \"transparent\", animation: \"kb-spin .7s linear infinite\" }} /> : <Icon name={icon} size={t.icon} />}\n </button>\n );\n}\n","buttons/IconButton.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst ibSizes = { xs: { box: 22, icon: 14, radius: 'var(--radius-md)' }, sm: { box: 26, icon: 14, radius: 'var(--radius-sm)' }, md: { box: 28, icon: 16, radius: 'var(--radius-md)' }, lg: { box: 34, icon: 18, radius: 'var(--radius-md)' } }\n// Square ghost icon button (toolbar/back/bell/view toggles). size: xs 22 | sm 26 | md 28 | lg 34.\nconst props = defineProps({ icon: String, size: { type: String, default: 'md' }, active: Boolean, muted: Boolean, danger: Boolean, disabled: Boolean, loading: Boolean, label: String, pressed: Boolean, focused: Boolean, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false), press = ref(false), focus = ref(false)\nconst t = computed(() => ibSizes[props.size])\nconst busy = computed(() => props.disabled || props.loading)\nconst isPressed = computed(() => (press.value || props.pressed) && !busy.value)\nconst color = computed(() => props.danger ? 'var(--danger)' : props.muted ? 'var(--text-muted)' : 'var(--text-body)')\nconst bg = computed(() => isPressed.value ? (props.danger ? 'var(--danger-soft-active)' : 'var(--bg-press)') : props.active ? 'var(--bg-active)' : (hover.value && !busy.value ? (props.danger ? 'var(--danger-bg)' : 'var(--bg-hover)') : 'transparent'))\nconst s = computed(() => ({ width: t.value.box, height: t.value.box, borderRadius: t.value.radius, border: 'none', cursor: busy.value ? 'default' : 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: bg.value, color: color.value, opacity: busy.value ? .5 : 1, boxShadow: (focus.value || props.focused) && !busy.value ? 'var(--ring-focus)' : 'none', transition: 'var(--t-hover)', padding: 0, flexShrink: 0, ...props.style }))\n</script>\n<template>\n <button type=\"button\" data-press=\"own\" :aria-label=\"label\" :disabled=\"busy\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false; press = false\" @mousedown=\"press = true\" @mouseup=\"press = false\" @keydown.enter=\"press = true\" @keydown.space=\"press = true\" @keyup=\"press = false\" @focus=\"focus = $event.target.matches(':focus-visible')\" @blur=\"focus = false\" :style=\"s\">\n <span v-if=\"loading\" :style=\"{ display: 'inline-block', width: t.icon, height: t.icon, borderRadius: 'var(--radius-full)', border: '2px solid currentColor', borderTopColor: 'transparent', animation: 'kb-spin .7s linear infinite' }\" />\n <Icon v-else :name=\"icon\" :size=\"t.icon\" />\n </button>\n</template>\n"});
1
+ window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"buttons/ActionChip.jsx":"import React from \"react\";\n\nconst chipTones = {\n success: { background: \"var(--success-bg)\", color: \"var(--success-text)\" },\n warning: { background: \"var(--warning-bg)\", color: \"var(--warning-text)\" },\n neutral: { background: \"var(--bg-hover)\", color: \"var(--text-body)\" },\n info: { background: \"var(--info-bg)\", color: \"var(--action-primary)\" },\n danger: { background: \"var(--danger-bg)\", color: \"var(--danger-text)\" },\n};\n\n/** Tinted bulk-action chip (\"Mark as New\", \"Mark as Overwrite\", \"Skip Selected\"). */\nexport function ActionChip({ tone = \"neutral\", children, onClick, style }) {\n const [hover, setHover] = React.useState(false);\n return (\n <button type=\"button\" onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ height: 30, padding: \"0 12px\", borderRadius: \"var(--radius-md)\", border: \"none\", cursor: \"pointer\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", display: \"inline-flex\", alignItems: \"center\", filter: hover ? \"brightness(1.06) saturate(1.05)\" : \"none\", transition: \"var(--t-hover),filter var(--dur-fast) var(--ease-out)\", ...chipTones[tone], ...style }}>\n {children}\n </button>\n );\n}\n","buttons/ActionChip.vue":"<script setup>\nimport { ref, computed } from 'vue'\nconst chipTones = {\n success: { background: 'var(--success-bg)', color: 'var(--success-text)' },\n warning: { background: 'var(--warning-bg)', color: 'var(--warning-text)' },\n neutral: { background: 'var(--bg-hover)', color: 'var(--text-body)' },\n info: { background: 'var(--info-bg)', color: 'var(--action-primary)' },\n danger: { background: 'var(--danger-bg)', color: 'var(--danger-text)' },\n}\n// Tinted bulk-action chip (\"Mark as New\", \"Mark as Overwrite\", \"Skip Selected\").\nconst props = defineProps({ tone: { type: String, default: 'neutral' }, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\nconst s = computed(() => ({ height: 30, padding: '0 12px', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', display: 'inline-flex', alignItems: 'center', filter: hover.value ? 'brightness(1.06) saturate(1.05)' : 'none', transition: 'var(--t-hover),filter var(--dur-fast) var(--ease-out)', ...chipTones[props.tone], ...props.style }))\n</script>\n<template>\n <button type=\"button\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\" :style=\"s\"><slot /></button>\n</template>\n","buttons/Button.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst btnBase = { display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-regular)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", border: \"1px solid transparent\", cursor: \"pointer\", whiteSpace: \"nowrap\", boxSizing: \"border-box\", background: \"transparent\", transition: \"var(--t-hover)\" };\n\nconst variants = {\n primary: { background: \"var(--action-primary)\", color: \"var(--on-accent)\" },\n dark: { background: \"var(--action-dark)\", color: \"var(--action-dark-fg)\" },\n outline: { background: \"var(--bg-surface)\", color: \"var(--text-body)\", borderColor: \"var(--border-control)\" },\n ghost: { color: \"var(--text-body)\" },\n \"primary-soft\": { background: \"var(--soft-bg)\", color: \"var(--action-primary)\", borderColor: \"var(--soft-border)\" },\n danger: { background: \"var(--danger)\", color: \"var(--on-accent)\" },\n \"danger-soft\": { background: \"var(--danger-bg)\", color: \"var(--danger-text)\" },\n text: { color: \"var(--text-secondary)\", padding: \"0\", height: \"auto\" },\n};\nconst hovers = {\n primary: \"var(--action-primary-hover)\", dark: \"var(--action-dark-hover)\",\n outline: \"var(--bg-hover)\", ghost: \"var(--bg-hover)\", \"primary-soft\": \"var(--soft-hover)\",\n danger: \"var(--danger-text)\", \"danger-soft\": \"var(--danger-bg)\",\n};\n/** Press (mouse-down) fills — one step past hover. */\nconst presses = {\n primary: \"var(--action-primary-active)\", dark: \"var(--action-dark-active)\",\n outline: \"var(--bg-press)\", ghost: \"var(--bg-press)\", \"primary-soft\": \"var(--soft-active)\",\n danger: \"var(--danger-active)\", \"danger-soft\": \"var(--danger-soft-active)\",\n};\n/** Variants that change text color (+ underline) on hover instead of a background wash. */\nconst colorHoverVariants = { text: \"var(--text-heading)\" };\nconst sizes = {\n sm: { height: 30, padding: \"6px 12px\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", letterSpacing: 0, borderRadius: \"var(--radius-md)\" },\n md: { height: 32, padding: \"6px 12px\", borderRadius: \"var(--radius-md)\" },\n lg: { height: 35.5, padding: \"8px 16px\", fontWeight: \"var(--weight-medium)\", borderRadius: \"var(--radius-lg)\" },\n};\nconst iconOnlyPad = { sm: 0, md: 0, lg: 0 };\nconst iconOnlyBox = { sm: 30, md: 32, lg: 36 };\n\nfunction Spinner({ size, color }) {\n return <span style={{ display: \"inline-block\", width: size, height: size, borderRadius: \"var(--radius-full)\", border: \"2px solid currentColor\", borderTopColor: \"transparent\", opacity: .9, color, animation: \"kb-spin .7s linear infinite\", flexShrink: 0 }} />;\n}\n\n/** Keybricks button. variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm | md | lg. Interaction states: hover (one step darker) · press (two steps, set via mouse-down) · keyboard focus ring · loading · disabled. */\nexport function Button({ variant = \"primary\", size = \"md\", icon, iconSize, iconPosition = \"left\", medium, disabled, loading, fullWidth, iconOnly, label, pressed, focused, children, style, onClick, ...rest }) {\n const [hover, setHover] = React.useState(false);\n const [press, setPress] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const busy = disabled || loading;\n const isPressed = press || pressed;\n const isFocused = focus || focused;\n const s = {\n ...btnBase, ...sizes[size], ...variants[variant],\n ...(medium ? { fontWeight: \"var(--weight-medium)\" } : null),\n ...(fullWidth ? { width: \"100%\" } : null),\n ...(iconOnly ? { width: iconOnlyBox[size], padding: iconOnlyPad[size], gap: 0 } : null),\n ...(busy ? { opacity: .55, cursor: \"default\" } : null),\n ...style,\n };\n if (!busy) {\n if (colorHoverVariants[variant]) {\n if (hover || isPressed) { s.color = colorHoverVariants[variant]; s.textDecoration = \"underline\"; }\n if (isPressed) s.opacity = .7;\n } else if (isPressed) {\n s.background = presses[variant];\n } else if (hover) {\n s.background = hovers[variant];\n }\n if (isFocused) s.boxShadow = \"var(--ring-focus)\";\n }\n const iSize = iconSize || (size === \"sm\" ? 12 : 14);\n const leading = loading ? <Spinner size={iSize} color={s.color} /> : (icon && iconPosition === \"left\" ? <Icon name={icon} size={iSize} /> : null);\n const trailing = !loading && icon && iconPosition === \"right\" ? <Icon name={icon} size={iSize} /> : null;\n return (\n <button type=\"button\" data-press=\"own\" style={s} disabled={busy} aria-label={iconOnly ? label : undefined} onClick={onClick}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}\n onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}\n onKeyDown={(e) => { if (e.key === \" \" || e.key === \"Enter\") setPress(true); }} onKeyUp={() => setPress(false)}\n onFocus={(e) => { try { setFocus(e.target.matches(\":focus-visible\")); } catch (err) { setFocus(true); } }} onBlur={() => setFocus(false)} {...rest}>\n {leading}\n {iconOnly ? null : children}\n {trailing}\n </button>\n );\n}\n\n/** Attached row of Buttons sharing one outline (segmented action group). Children should be <Button variant=\"outline\"> or similar. */\nexport function ButtonGroup({ children, style }) {\n const items = React.Children.toArray(children);\n return (\n <div style={{ display: \"inline-flex\", ...style }}>\n {items.map((child, i) => React.cloneElement(child, {\n key: i,\n style: {\n ...(child.props.style || {}),\n borderRadius: 0,\n ...(i === 0 ? { borderTopLeftRadius: \"var(--radius-md)\", borderBottomLeftRadius: \"var(--radius-md)\" } : null),\n ...(i === items.length - 1 ? { borderTopRightRadius: \"var(--radius-md)\", borderBottomRightRadius: \"var(--radius-md)\" } : null),\n marginLeft: i === 0 ? 0 : -1,\n },\n }))}\n </div>\n );\n}\n","buttons/Button.vue":"<script setup>\nimport { computed, ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n\nconst btnBase = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-regular)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', border: '1px solid transparent', cursor: 'pointer', whiteSpace: 'nowrap', boxSizing: 'border-box', background: 'transparent', transition: 'var(--t-hover)' }\nconst variants = {\n primary: { background: 'var(--action-primary)', color: 'var(--on-accent)' },\n dark: { background: 'var(--action-dark)', color: 'var(--action-dark-fg)' },\n outline: { background: 'var(--bg-surface)', color: 'var(--text-body)', borderColor: 'var(--border-control)' },\n ghost: { color: 'var(--text-body)' },\n 'primary-soft': { background: 'var(--soft-bg)', color: 'var(--action-primary)', borderColor: 'var(--soft-border)' },\n danger: { background: 'var(--danger)', color: 'var(--on-accent)' },\n 'danger-soft': { background: 'var(--danger-bg)', color: 'var(--danger-text)' },\n text: { color: 'var(--text-secondary)', padding: '0', height: 'auto' },\n}\nconst hovers = { primary: 'var(--action-primary-hover)', dark: 'var(--action-dark-hover)', outline: 'var(--bg-hover)', ghost: 'var(--bg-hover)', 'primary-soft': 'var(--soft-hover)', danger: 'var(--danger-text)', 'danger-soft': 'var(--danger-bg)' }\nconst presses = { primary: 'var(--action-primary-active)', dark: 'var(--action-dark-active)', outline: 'var(--bg-press)', ghost: 'var(--bg-press)', 'primary-soft': 'var(--soft-active)', danger: 'var(--danger-active)', 'danger-soft': 'var(--danger-soft-active)' }\nconst colorHoverVariants = { text: 'var(--text-heading)' }\nconst sizes = {\n sm: { height: 30, padding: '6px 12px', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', letterSpacing: 0, borderRadius: 'var(--radius-md)' },\n md: { height: 32, padding: '6px 12px', borderRadius: 'var(--radius-md)' },\n lg: { height: 35.5, padding: '8px 16px', fontWeight: 'var(--weight-medium)', borderRadius: 'var(--radius-lg)' },\n}\nconst iconOnlyPad = { sm: 0, md: 0, lg: 0 }\nconst iconOnlyBox = { sm: 30, md: 32, lg: 36 }\n\n// Button — variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm | md | lg.\nconst props = defineProps({\n variant: { type: String, default: 'primary' }, size: { type: String, default: 'md' },\n icon: String, iconSize: Number, iconPosition: { type: String, default: 'left' },\n medium: Boolean, disabled: Boolean, loading: Boolean, fullWidth: Boolean, iconOnly: Boolean,\n label: String, pressed: Boolean, focused: Boolean, style: Object,\n})\ndefineEmits(['click'])\nconst hover = ref(false), press = ref(false), focus = ref(false)\nconst busy = computed(() => props.disabled || props.loading)\nconst isPressed = computed(() => press.value || props.pressed)\nconst isFocused = computed(() => focus.value || props.focused)\nconst s = computed(() => {\n const base = { ...btnBase, ...sizes[props.size], ...variants[props.variant],\n ...(props.medium ? { fontWeight: 'var(--weight-medium)' } : null),\n ...(props.fullWidth ? { width: '100%' } : null),\n ...(props.iconOnly ? { width: iconOnlyBox[props.size], padding: iconOnlyPad[props.size], gap: 0 } : null),\n ...(busy.value ? { opacity: .55, cursor: 'default' } : null), ...props.style }\n if (!busy.value) {\n if (colorHoverVariants[props.variant]) {\n if (hover.value || isPressed.value) { base.color = colorHoverVariants[props.variant]; base.textDecoration = 'underline' }\n if (isPressed.value) base.opacity = .7\n } else if (isPressed.value) base.background = presses[props.variant]\n else if (hover.value) base.background = hovers[props.variant]\n if (isFocused.value) base.boxShadow = 'var(--ring-focus)'\n }\n return base\n})\nconst iSize = computed(() => props.iconSize || (props.size === 'sm' ? 12 : 14))\n</script>\n<template>\n <button type=\"button\" data-press=\"own\" :style=\"s\" :disabled=\"busy\" :aria-label=\"iconOnly ? label : undefined\"\n @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false; press = false\"\n @mousedown=\"press = true\" @mouseup=\"press = false\" @keydown.enter=\"press = true\" @keydown.space=\"press = true\" @keyup=\"press = false\"\n @focus=\"focus = $event.target.matches(':focus-visible')\" @blur=\"focus = false\">\n <span v-if=\"loading\" :style=\"{ display: 'inline-block', width: iSize, height: iSize, borderRadius: 'var(--radius-full)', border: '2px solid currentColor', borderTopColor: 'transparent', opacity: .9, animation: 'kb-spin .7s linear infinite', flexShrink: 0 }\" />\n <Icon v-else-if=\"icon && iconPosition === 'left'\" :name=\"icon\" :size=\"iSize\" />\n <slot v-if=\"!iconOnly\" />\n <Icon v-if=\"!loading && icon && iconPosition === 'right'\" :name=\"icon\" :size=\"iSize\" />\n </button>\n</template>\n","buttons/IconButton.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst ibSizes = { xs: { box: 22, icon: 14, radius: \"var(--radius-md)\" }, sm: { box: 26, icon: 14, radius: \"var(--radius-sm)\" }, md: { box: 28, icon: 16, radius: \"var(--radius-md)\" }, lg: { box: 34, icon: 18, radius: \"var(--radius-md)\" } };\n\n/** Square ghost icon button (toolbar/back/bell/view toggles). size: xs 22 | sm 26 | md 28 | lg 34. danger tints red; loading shows a spinner and disables interaction. States: hover · press · keyboard focus ring · active (persistent toggle-on) · disabled. */\nexport function IconButton({ icon, size = \"md\", active, muted, danger, disabled, loading, label, pressed, focused, style, onClick, ...rest }) {\n const [hover, setHover] = React.useState(false);\n const [press, setPress] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const t = ibSizes[size];\n const busy = disabled || loading;\n const isPressed = (press || pressed) && !busy;\n const color = danger ? \"var(--danger)\" : muted ? \"var(--text-muted)\" : \"var(--text-body)\";\n const bg = isPressed ? (danger ? \"var(--danger-soft-active)\" : \"var(--bg-press)\")\n : active ? \"var(--bg-active)\"\n : hover && !busy ? (danger ? \"var(--danger-bg)\" : \"var(--bg-hover)\") : \"transparent\";\n return (\n <button type=\"button\" data-press=\"own\" aria-label={label} disabled={busy} onClick={onClick}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}\n onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}\n onKeyDown={(e) => { if (e.key === \" \" || e.key === \"Enter\") setPress(true); }} onKeyUp={() => setPress(false)}\n onFocus={(e) => { try { setFocus(e.target.matches(\":focus-visible\")); } catch (err) { setFocus(true); } }} onBlur={() => setFocus(false)}\n style={{ width: t.box, height: t.box, borderRadius: t.radius, border: \"none\", cursor: busy ? \"default\" : \"pointer\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", background: bg, color, opacity: busy ? .5 : 1, boxShadow: (focus || focused) && !busy ? \"var(--ring-focus)\" : \"none\", transition: \"var(--t-hover)\", padding: 0, flexShrink: 0, ...style }} {...rest}>\n {loading ? <span style={{ display: \"inline-block\", width: t.icon, height: t.icon, borderRadius: \"var(--radius-full)\", border: \"2px solid currentColor\", borderTopColor: \"transparent\", animation: \"kb-spin .7s linear infinite\" }} /> : <Icon name={icon} size={t.icon} />}\n </button>\n );\n}\n","buttons/IconButton.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst ibSizes = { xs: { box: 22, icon: 14, radius: 'var(--radius-md)' }, sm: { box: 26, icon: 14, radius: 'var(--radius-sm)' }, md: { box: 28, icon: 16, radius: 'var(--radius-md)' }, lg: { box: 34, icon: 18, radius: 'var(--radius-md)' } }\n// Square ghost icon button (toolbar/back/bell/view toggles). size: xs 22 | sm 26 | md 28 | lg 34.\nconst props = defineProps({ icon: String, size: { type: String, default: 'md' }, active: Boolean, muted: Boolean, danger: Boolean, disabled: Boolean, loading: Boolean, label: String, pressed: Boolean, focused: Boolean, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false), press = ref(false), focus = ref(false)\nconst t = computed(() => ibSizes[props.size])\nconst busy = computed(() => props.disabled || props.loading)\nconst isPressed = computed(() => (press.value || props.pressed) && !busy.value)\nconst color = computed(() => props.danger ? 'var(--danger)' : props.muted ? 'var(--text-muted)' : 'var(--text-body)')\nconst bg = computed(() => isPressed.value ? (props.danger ? 'var(--danger-soft-active)' : 'var(--bg-press)') : props.active ? 'var(--bg-active)' : (hover.value && !busy.value ? (props.danger ? 'var(--danger-bg)' : 'var(--bg-hover)') : 'transparent'))\nconst s = computed(() => ({ width: t.value.box, height: t.value.box, borderRadius: t.value.radius, border: 'none', cursor: busy.value ? 'default' : 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: bg.value, color: color.value, opacity: busy.value ? .5 : 1, boxShadow: (focus.value || props.focused) && !busy.value ? 'var(--ring-focus)' : 'none', transition: 'var(--t-hover)', padding: 0, flexShrink: 0, ...props.style }))\n</script>\n<template>\n <button type=\"button\" data-press=\"own\" :aria-label=\"label\" :disabled=\"busy\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false; press = false\" @mousedown=\"press = true\" @mouseup=\"press = false\" @keydown.enter=\"press = true\" @keydown.space=\"press = true\" @keyup=\"press = false\" @focus=\"focus = $event.target.matches(':focus-visible')\" @blur=\"focus = false\" :style=\"s\">\n <span v-if=\"loading\" :style=\"{ display: 'inline-block', width: t.icon, height: t.icon, borderRadius: 'var(--radius-full)', border: '2px solid currentColor', borderTopColor: 'transparent', animation: 'kb-spin .7s linear infinite' }\" />\n <Icon v-else :name=\"icon\" :size=\"t.icon\" />\n </button>\n</template>\n","buttons/ButtonGroup.vue":"<script setup>\nimport { useSlots, computed } from 'vue'\nconst props = defineProps({ style: Object })\nconst slots = useSlots()\nconst items = computed(() => slots.default ? slots.default() : [])\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', ...style }\">\n <template v-for=\"(child, i) in items\" :key=\"i\">\n <component :is=\"child\" :style=\"{ borderRadius: 0,\n ...(i === 0 ? { borderTopLeftRadius: 'var(--radius-md)', borderBottomLeftRadius: 'var(--radius-md)' } : null),\n ...(i === items.length - 1 ? { borderTopRightRadius: 'var(--radius-md)', borderBottomRightRadius: 'var(--radius-md)' } : null),\n marginLeft: i === 0 ? 0 : -1 }\" />\n </template>\n </div>\n</template>\n"});