@keyfunc/keybricks-ds 1.0.2 → 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.
Files changed (76) hide show
  1. package/DEPLOY.md +3 -3
  2. package/INTEGRATION.md +24 -5
  3. package/_adherence.oxlintrc.json +21 -6
  4. package/components/_cards/radio.card.html +46 -0
  5. package/components/_cards/segmented-control.card.html +39 -0
  6. package/components/_cards/toggles.card.html +44 -55
  7. package/components/_shared/src-buttons.js +1 -1
  8. package/components/_shared/src-data.js +1 -1
  9. package/components/_shared/src-display.js +1 -1
  10. package/components/_shared/src-feedback.js +1 -1
  11. package/components/_shared/src-forms.js +1 -1
  12. package/components/_shared/src-navigation.js +1 -1
  13. package/components/_shared/src-pipeline.js +1 -1
  14. package/components/_shared/src-tabs.js +1 -1
  15. package/components/_shared/view-source.js +9 -4
  16. package/components/_vue/index.mjs +21 -0
  17. package/components/buttons/Button.vue +0 -20
  18. package/components/buttons/ButtonGroup.vue +16 -0
  19. package/components/data/KeyValue.vue +0 -16
  20. package/components/data/KeyValueList.vue +12 -0
  21. package/components/data/ListRow.vue +1 -1
  22. package/components/display/AreaChart.vue +14 -0
  23. package/components/display/Avatar.vue +0 -17
  24. package/components/display/AvatarGroup.vue +13 -0
  25. package/components/display/Badge.vue +0 -34
  26. package/components/display/Card.vue +0 -15
  27. package/components/display/Donut.vue +17 -0
  28. package/components/display/IconTile.vue +13 -0
  29. package/components/display/MiniBars.vue +11 -0
  30. package/components/display/ProgressBar.vue +0 -54
  31. package/components/display/StatCard.vue +11 -0
  32. package/components/display/StatusBadge.vue +11 -0
  33. package/components/feedback/ActivityItem.vue +1 -32
  34. package/components/feedback/Alert.vue +0 -36
  35. package/components/feedback/DotActivity.vue +15 -0
  36. package/components/feedback/EmptyState.vue +12 -0
  37. package/components/feedback/LogViewer.vue +8 -0
  38. package/components/feedback/Modal.vue +0 -45
  39. package/components/feedback/Skeleton.vue +6 -0
  40. package/components/feedback/Spinner.vue +6 -0
  41. package/components/feedback/Toast.vue +19 -0
  42. package/components/forms/Combobox.vue +1 -1
  43. package/components/forms/DatePicker.vue +1 -1
  44. package/components/forms/Input.jsx +2 -1
  45. package/components/forms/Input.vue +2 -24
  46. package/components/forms/Radio.d.ts +28 -0
  47. package/components/forms/Radio.jsx +67 -0
  48. package/components/forms/Radio.vue +17 -8
  49. package/components/forms/RadioGroup.vue +22 -4
  50. package/components/forms/Switch.d.ts +32 -24
  51. package/components/forms/Switch.jsx +87 -35
  52. package/components/forms/Switch.vue +56 -11
  53. package/components/forms/Textarea.vue +20 -0
  54. package/components/forms/forms.prompt.md +4 -1
  55. package/components/forms/input-base.js +3 -0
  56. package/components/index.d.ts +1 -0
  57. package/components/index.js +2 -1
  58. package/components/navigation/Breadcrumb.vue +0 -22
  59. package/components/navigation/PageHeading.vue +9 -0
  60. package/components/navigation/PageToolbar.vue +15 -0
  61. package/components/navigation/ThemeToggle.vue +18 -0
  62. package/components/navigation/TopBar.vue +0 -32
  63. package/components/pipeline/NodeCard.vue +2 -1
  64. package/components/pipeline/PaletteItem.vue +1 -1
  65. package/components/pipeline/PalettePanel.jsx +2 -8
  66. package/components/pipeline/PalettePanel.vue +1 -8
  67. package/components/pipeline/pipeline-catalog.js +8 -0
  68. package/components/tabs/PanelTabs.vue +10 -0
  69. package/components/tabs/PillTabs.vue +10 -0
  70. package/components/tabs/SegmentedControl.d.ts +10 -3
  71. package/components/tabs/SegmentedControl.jsx +24 -9
  72. package/components/tabs/SegmentedControl.vue +19 -7
  73. package/components/tabs/Tabs.vue +0 -28
  74. package/components/tabs/tabs.prompt.md +3 -1
  75. package/package.json +1 -1
  76. package/tokens/tokens.json +1 -1
@@ -0,0 +1,13 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ import Avatar from './Avatar.vue'
4
+ const props = defineProps({ names: { type: Array, default: () => [] }, size: { type: Number, default: 24 }, max: { type: Number, default: 4 }, style: Object })
5
+ const shown = computed(() => props.names.slice(0, props.max))
6
+ const rest = computed(() => props.names.length - shown.value.length)
7
+ </script>
8
+ <template>
9
+ <div :style="{ display: 'flex', alignItems: 'center', ...style }">
10
+ <Avatar v-for="(n, i) in shown" :key="n + i" :name="n" :size="size" :style="{ marginLeft: i ? -8 : 0, boxShadow: 'var(--ring-surface)' }" />
11
+ <span v-if="rest > 0" :style="{ marginLeft: -8, width: size, height: size, borderRadius: 'var(--radius-full)', background: 'var(--bg-active)', color: 'var(--text-secondary)', boxShadow: 'var(--ring-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'var(--font-sans)', fontSize: Math.max(9, Math.round(size * .38)) }">+{{ rest }}</span>
12
+ </div>
13
+ </template>
@@ -1,20 +1,4 @@
1
1
  <script setup>
2
- import { computed } from 'vue'
3
- import Icon from '../icons/Icon.vue'
4
- const badgeTones = { success: 'var(--success)', danger: 'var(--danger)', warning: 'var(--warning)', pending: 'var(--dot-neutral)', info: 'var(--action-primary)' }
5
- // StatusBadge — circular status badge with white ring (node error/warning dots, step icons).
6
- const props = defineProps({ tone: { type: String, default: 'danger' }, icon: String, size: { type: Number, default: 20 }, ring: { type: Boolean, default: true }, style: Object })
7
- const iconSize = computed(() => Math.round(props.size * .6))
8
- </script>
9
- <template>
10
- <span :style="{ width: size, height: size, borderRadius: 'var(--radius-full)', background: badgeTones[tone], boxShadow: ring ? 'var(--ring-surface)' : 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--on-accent)', flexShrink: 0, ...style }">
11
- <Icon v-if="icon" :name="icon" :size="iconSize" :stroke-width="2.5" />
12
- </span>
13
- </template>
14
-
15
- <!-- Badge — inline tinted text badge (table statuses: Completed / Failed / Partial). -->
16
- <!--
17
- <script setup name="Badge">
18
2
  const tones = {
19
3
  success: { background: 'var(--success-bg)', color: 'var(--success-text)' },
20
4
  danger: { background: 'var(--danger-bg)', color: 'var(--danger-text)' },
@@ -27,21 +11,3 @@ defineProps({ tone: { type: String, default: 'neutral' }, style: Object })
27
11
  <template>
28
12
  <span :style="{ display: 'inline-flex', alignItems: 'center', height: 20, padding: '0 8px', borderRadius: 'var(--radius-full)', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-xs)', lineHeight: 1, letterSpacing: 'var(--tracking-xs)', ...tones[tone], ...style }"><slot /></span>
29
13
  </template>
30
- -->
31
-
32
- <!-- IconTile — category icon tile — the signature tinted square. kind: palette 40/r14 · node 48/r16 · row 32/r8 (hairline). -->
33
- <!--
34
- <script setup name="IconTile">
35
- import { computed } from 'vue'
36
- import Icon from '../icons/Icon.vue'
37
- const dims = { palette: { box: 40, r: 'var(--radius-tile)', icon: 18 }, node: { box: 48, r: 'var(--radius-node)', icon: 22 }, row: { box: 32, r: 'var(--radius-md)', icon: 16 } }
38
- const props = defineProps({ icon: String, hue: { type: String, default: 'source' }, kind: { type: String, default: 'palette' }, style: Object })
39
- const t = computed(() => dims[props.kind])
40
- const tinted = computed(() => props.kind !== 'row')
41
- </script>
42
- <template>
43
- <span :style="{ width: t.box, height: t.box, borderRadius: t.r, background: tinted ? `var(--cat-${hue}-bg)` : 'var(--bg-surface)', border: tinted ? 'none' : '1px solid var(--border-control)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: tinted ? `var(--cat-${hue})` : 'var(--text-body)', flexShrink: 0, boxSizing: 'border-box', ...style }">
44
- <Icon :name="icon" :size="t.icon" :stroke-width="2" />
45
- </span>
46
- </template>
47
- -->
@@ -11,18 +11,3 @@ defineProps({ title: String, dense: Boolean, pad: { type: Boolean, default: true
11
11
  <slot />
12
12
  </div>
13
13
  </template>
14
-
15
- <!-- StatCard — dashboard stat card: muted 13px label + light 24px number + optional chart slot. -->
16
- <!--
17
- <script setup name="StatCard">
18
- import Card from './Card.vue'
19
- defineProps({ label: String, value: [String, Number], dense: Boolean, style: Object })
20
- </script>
21
- <template>
22
- <Card :dense="dense" :style="{ display: 'flex', flexDirection: 'column', gap: 12, ...style }">
23
- <span style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-secondary)">{{ label }}</span>
24
- <slot />
25
- <span v-if="value != null" style="font-family:var(--font-sans);font-weight:var(--weight-light);font-size:var(--text-num);line-height:var(--leading-num);letter-spacing:var(--tracking-num);color:var(--text-primary)">{{ value }}</span>
26
- </Card>
27
- </template>
28
- -->
@@ -0,0 +1,17 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ const props = defineProps({ size: { type: Number, default: 128 }, thickness: { type: Number, default: 7.68 }, segments: Array, value: { type: Number, default: 75 }, label: [String, Number], style: Object })
4
+ const R = computed(() => props.size / 2 - props.thickness / 2 - props.size * .02)
5
+ const C = computed(() => 2 * Math.PI * R.value)
6
+ const segs = computed(() => props.segments || [{ value: props.value, color: 'var(--chart-blue)' }])
7
+ const arcs = computed(() => { let offset = 0; return segs.value.map(s => { const len = (s.value / 100) * C.value; const a = { ...s, len, offset }; offset += len; return a }) })
8
+ </script>
9
+ <template>
10
+ <div :style="{ position: 'relative', width: size, height: size, ...style }">
11
+ <svg :width="size" :height="size" style="transform:rotate(-90deg)">
12
+ <circle :cx="size / 2" :cy="size / 2" :r="R" fill="none" stroke="var(--track)" :stroke-width="thickness" />
13
+ <circle v-for="(s, i) in arcs" :key="i" :cx="size / 2" :cy="size / 2" :r="R" fill="none" :stroke="s.color" :stroke-width="thickness" :stroke-dasharray="`${s.len} ${C - s.len}`" :stroke-dashoffset="-s.offset" stroke-linecap="butt" />
14
+ </svg>
15
+ <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--font-sans);font-weight:var(--weight-light);font-size:20px;letter-spacing:var(--tracking-xl);color:var(--text-primary)">{{ label != null ? label : `${value}%` }}</div>
16
+ </div>
17
+ </template>
@@ -0,0 +1,13 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ import Icon from '../icons/Icon.vue'
4
+ const dims = { palette: { box: 40, r: 'var(--radius-tile)', icon: 18 }, node: { box: 48, r: 'var(--radius-node)', icon: 22 }, row: { box: 32, r: 'var(--radius-md)', icon: 16 } }
5
+ const props = defineProps({ icon: String, hue: { type: String, default: 'source' }, kind: { type: String, default: 'palette' }, style: Object })
6
+ const t = computed(() => dims[props.kind])
7
+ const tinted = computed(() => props.kind !== 'row')
8
+ </script>
9
+ <template>
10
+ <span :style="{ width: t.box, height: t.box, borderRadius: t.r, background: tinted ? `var(--cat-${hue}-bg)` : 'var(--bg-surface)', border: tinted ? 'none' : '1px solid var(--border-control)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: tinted ? `var(--cat-${hue})` : 'var(--text-body)', flexShrink: 0, boxSizing: 'border-box', ...style }">
11
+ <Icon :name="icon" :size="t.icon" :stroke-width="2" />
12
+ </span>
13
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ const props = defineProps({ bars: { type: Array, default: () => [40, 65, 50, 80, 60, 90, 70] }, width: { type: Number, default: 248 }, height: { type: Number, default: 96 }, color: { type: String, default: 'var(--chart-blue)' }, style: Object })
4
+ const gap = 6
5
+ const bw = computed(() => (props.width - gap * (props.bars.length - 1)) / props.bars.length)
6
+ </script>
7
+ <template>
8
+ <svg :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" :style="{ display: 'block', ...style }">
9
+ <rect v-for="(b, i) in bars" :key="i" :x="i * (bw + gap)" :y="height - (b / 100) * height" :width="bw" :height="(b / 100) * height" rx="2" :fill="color" opacity=".85" />
10
+ </svg>
11
+ </template>
@@ -13,57 +13,3 @@ defineProps({ label: String, value: { type: Number, default: 50 }, color: { type
13
13
  </div>
14
14
  </div>
15
15
  </template>
16
-
17
- <!-- Donut — ring/donut chart. segments: [{value, color}]; centered light numeral. -->
18
- <!--
19
- <script setup name="Donut">
20
- import { computed } from 'vue'
21
- const props = defineProps({ size: { type: Number, default: 128 }, thickness: { type: Number, default: 7.68 }, segments: Array, value: { type: Number, default: 75 }, label: [String, Number], style: Object })
22
- const R = computed(() => props.size / 2 - props.thickness / 2 - props.size * .02)
23
- const C = computed(() => 2 * Math.PI * R.value)
24
- const segs = computed(() => props.segments || [{ value: props.value, color: 'var(--chart-blue)' }])
25
- const arcs = computed(() => { let offset = 0; return segs.value.map(s => { const len = (s.value / 100) * C.value; const a = { ...s, len, offset }; offset += len; return a }) })
26
- </script>
27
- <template>
28
- <div :style="{ position: 'relative', width: size, height: size, ...style }">
29
- <svg :width="size" :height="size" style="transform:rotate(-90deg)">
30
- <circle :cx="size / 2" :cy="size / 2" :r="R" fill="none" stroke="var(--track)" :stroke-width="thickness" />
31
- <circle v-for="(s, i) in arcs" :key="i" :cx="size / 2" :cy="size / 2" :r="R" fill="none" :stroke="s.color" :stroke-width="thickness" :stroke-dasharray="`${s.len} ${C - s.len}`" :stroke-dashoffset="-s.offset" stroke-linecap="butt" />
32
- </svg>
33
- <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--font-sans);font-weight:var(--weight-light);font-size:20px;letter-spacing:var(--tracking-xl);color:var(--text-primary)">{{ label != null ? label : `${value}%` }}</div>
34
- </div>
35
- </template>
36
- -->
37
-
38
- <!-- AreaChart — area line chart (Revenue Trend): 1.22px blue line + fading gradient fill. points: 0–100 values. -->
39
- <!--
40
- <script setup name="AreaChart">
41
- import { computed } from 'vue'
42
- const props = defineProps({ points: { type: Array, default: () => [65, 75, 60, 85, 70, 90, 85, 95, 100] }, width: { type: Number, default: 350 }, height: { type: Number, default: 128 }, color: { type: String, default: 'var(--chart-blue)' }, style: Object })
43
- const gid = 'g' + Math.round(Math.random() * 1e6)
44
- const step = computed(() => props.width / (props.points.length - 1))
45
- const line = computed(() => props.points.map((p, i) => `${i ? 'L' : 'M'} ${i * step.value} ${props.height - (p / 100) * props.height * .48 - props.height * .5}`).join(' '))
46
- </script>
47
- <template>
48
- <svg :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" :style="{ display: 'block', ...style }">
49
- <defs><linearGradient :id="gid" x1="0" y1="0" x2="0" y2="1"><stop offset="0" :stop-color="color" stop-opacity=".3" /><stop offset="1" :stop-color="color" stop-opacity="0" /></linearGradient></defs>
50
- <path :d="`${line} L ${width} ${height} L 0 ${height} Z`" :fill="`url(#${gid})`" />
51
- <path :d="line" fill="none" :stroke="color" stroke-width="1.22" />
52
- </svg>
53
- </template>
54
- -->
55
-
56
- <!-- MiniBars — mini bar chart used in stat cards. bars: 0–100 values. -->
57
- <!--
58
- <script setup name="MiniBars">
59
- import { computed } from 'vue'
60
- const props = defineProps({ bars: { type: Array, default: () => [40, 65, 50, 80, 60, 90, 70] }, width: { type: Number, default: 248 }, height: { type: Number, default: 96 }, color: { type: String, default: 'var(--chart-blue)' }, style: Object })
61
- const gap = 6
62
- const bw = computed(() => (props.width - gap * (props.bars.length - 1)) / props.bars.length)
63
- </script>
64
- <template>
65
- <svg :width="width" :height="height" :viewBox="`0 0 ${width} ${height}`" :style="{ display: 'block', ...style }">
66
- <rect v-for="(b, i) in bars" :key="i" :x="i * (bw + gap)" :y="height - (b / 100) * height" :width="bw" :height="(b / 100) * height" rx="2" :fill="color" opacity=".85" />
67
- </svg>
68
- </template>
69
- -->
@@ -0,0 +1,11 @@
1
+ <script setup>
2
+ import Card from './Card.vue'
3
+ defineProps({ label: String, value: [String, Number], dense: Boolean, style: Object })
4
+ </script>
5
+ <template>
6
+ <Card :dense="dense" :style="{ display: 'flex', flexDirection: 'column', gap: 12, ...style }">
7
+ <span style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-secondary)">{{ label }}</span>
8
+ <slot />
9
+ <span v-if="value != null" style="font-family:var(--font-sans);font-weight:var(--weight-light);font-size:var(--text-num);line-height:var(--leading-num);letter-spacing:var(--tracking-num);color:var(--text-primary)">{{ value }}</span>
10
+ </Card>
11
+ </template>
@@ -0,0 +1,11 @@
1
+ <script setup>
2
+ import Icon from '../icons/Icon.vue'
3
+ // Circular status badge with white ring (node error/warning dots, step icons).
4
+ const badgeTones = { success: 'var(--success)', danger: 'var(--danger)', warning: 'var(--warning)', pending: 'var(--dot-neutral)', info: 'var(--action-primary)' }
5
+ const props = defineProps({ tone: { type: String, default: 'danger' }, icon: String, size: { type: Number, default: 20 }, ring: { type: Boolean, default: true }, style: Object })
6
+ </script>
7
+ <template>
8
+ <span :style="{ width: size + 'px', height: size + 'px', borderRadius: 'var(--radius-full)', background: badgeTones[tone], boxShadow: ring ? 'var(--ring-surface)' : 'none', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--on-accent)', flexShrink: 0, ...style }">
9
+ <Icon v-if="icon" :name="icon" :size="Math.round(size * .6)" :stroke-width="2.5" />
10
+ </span>
11
+ </template>
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { IconTile } from '../display/Badge.vue'
2
+ import IconTile from '../display/Badge.vue'
3
3
  // Activity feed row: 32px icon tile + 13px/500 title + 12px meta + optional right action.
4
4
  defineProps({ icon: { type: String, default: 'file-text' }, title: String, meta: String, action: String, style: Object })
5
5
  </script>
@@ -13,34 +13,3 @@ defineProps({ icon: { type: String, default: 'file-text' }, title: String, meta:
13
13
  <span v-if="action" style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);letter-spacing:var(--tracking-base);color:var(--text-secondary);cursor:pointer">{{ action }}</span>
14
14
  </div>
15
15
  </template>
16
-
17
- <!-- DotActivity — compact dot activity row (Recent Activities card): 8px status dot + 12px text + 11px time. -->
18
- <!--
19
- <script setup name="DotActivity">
20
- import { computed } from 'vue'
21
- const tonesMap = { success: 'var(--success)', warning: 'var(--warning)', danger: 'var(--danger)', info: 'var(--action-primary)', neutral: 'var(--dot-neutral)' }
22
- const props = defineProps({ tone: { type: String, default: 'success' }, text: String, time: String, style: Object })
23
- const dot = computed(() => tonesMap[props.tone])
24
- </script>
25
- <template>
26
- <div :style="{ display: 'flex', gap: 8, alignItems: 'flex-start', ...style }">
27
- <span :style="{ width: 8, height: 8, borderRadius: 'var(--radius-full)', background: dot, marginTop: 5, flexShrink: 0 }" />
28
- <div style="display:flex;flex-direction:column;min-width:0">
29
- <span style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-heading);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ text }}</span>
30
- <span style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted)">{{ time }}</span>
31
- </div>
32
- </div>
33
- </template>
34
- -->
35
-
36
- <!-- LogViewer — mono log lines ("[14:30] Task started: Import") — Menlo 11px on inset panel. -->
37
- <!--
38
- <script setup name="LogViewer">
39
- defineProps({ lines: { type: Array, default: () => [] }, height: [Number, String], style: Object })
40
- </script>
41
- <template>
42
- <div :style="{ background: 'var(--bg-inset)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-subtle)', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 6, overflowY: 'auto', height, boxSizing: 'border-box', ...style }">
43
- <span v-for="(l, i) in lines" :key="i" :style="{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', color: typeof l === 'object' ? (l.color || 'var(--text-body)') : 'var(--text-body)', whiteSpace: 'pre' }">{{ typeof l === 'object' ? l.text : l }}</span>
44
- </div>
45
- </template>
46
- -->
@@ -21,39 +21,3 @@ const t = computed(() => alertTones[props.tone])
21
21
  <slot name="action" />
22
22
  </div>
23
23
  </template>
24
-
25
- <!-- EmptyState — 22px muted glyph, 13px/500 title, 12px hint, optional action. -->
26
- <!--
27
- <script setup name="EmptyState">
28
- import Icon from '../icons/Icon.vue'
29
- defineProps({ icon: { type: String, default: 'file-text' }, title: String, hint: String, style: Object })
30
- </script>
31
- <template>
32
- <div :style="{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6, padding: '40px 24px', textAlign: 'center', ...style }">
33
- <span style="color:var(--text-muted);margin-bottom:6px"><Icon :name="icon" :size="22" :stroke-width="1.5" /></span>
34
- <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)">{{ title }}</span>
35
- <span v-if="hint" style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary);max-width:320px">{{ hint }}</span>
36
- <span v-if="$slots.action" style="padding-top:10px"><slot name="action" /></span>
37
- </div>
38
- </template>
39
- -->
40
-
41
- <!-- Skeleton — loading placeholder block. -->
42
- <!--
43
- <script setup name="Skeleton">
44
- defineProps({ width: { type: [String, Number], default: '100%' }, height: { type: [String, Number], default: 12 }, radius: { type: String, default: 'var(--radius-sm)' }, style: Object })
45
- </script>
46
- <template>
47
- <span :style="{ display: 'block', width, height, borderRadius: radius, background: 'var(--skeleton)', animation: 'kb-pulse 1.4s var(--ease-in-out) infinite', ...style }" />
48
- </template>
49
- -->
50
-
51
- <!-- Spinner — indeterminate spinner (task-running affordance). -->
52
- <!--
53
- <script setup name="Spinner">
54
- defineProps({ size: { type: Number, default: 16 }, color: { type: String, default: 'var(--action-primary)' }, style: Object })
55
- </script>
56
- <template>
57
- <span :style="{ display: 'inline-block', width: size, height: size, borderRadius: 'var(--radius-full)', border: '2px solid var(--track)', borderTopColor: color, animation: 'kb-spin .7s linear infinite', ...style }" />
58
- </template>
59
- -->
@@ -0,0 +1,15 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ const tonesMap = { success: 'var(--success)', warning: 'var(--warning)', danger: 'var(--danger)', info: 'var(--action-primary)', neutral: 'var(--dot-neutral)' }
4
+ const props = defineProps({ tone: { type: String, default: 'success' }, text: String, time: String, style: Object })
5
+ const dot = computed(() => tonesMap[props.tone])
6
+ </script>
7
+ <template>
8
+ <div :style="{ display: 'flex', gap: 8, alignItems: 'flex-start', ...style }">
9
+ <span :style="{ width: 8, height: 8, borderRadius: 'var(--radius-full)', background: dot, marginTop: 5, flexShrink: 0 }" />
10
+ <div style="display:flex;flex-direction:column;min-width:0">
11
+ <span style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-heading);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ text }}</span>
12
+ <span style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);letter-spacing:var(--tracking-xs);color:var(--text-muted)">{{ time }}</span>
13
+ </div>
14
+ </div>
15
+ </template>
@@ -0,0 +1,12 @@
1
+ <script setup>
2
+ import Icon from '../icons/Icon.vue'
3
+ defineProps({ icon: { type: String, default: 'file-text' }, title: String, hint: String, style: Object })
4
+ </script>
5
+ <template>
6
+ <div :style="{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 6, padding: '40px 24px', textAlign: 'center', ...style }">
7
+ <span style="color:var(--text-muted);margin-bottom:6px"><Icon :name="icon" :size="22" :stroke-width="1.5" /></span>
8
+ <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)">{{ title }}</span>
9
+ <span v-if="hint" style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary);max-width:320px">{{ hint }}</span>
10
+ <span v-if="$slots.action" style="padding-top:10px"><slot name="action" /></span>
11
+ </div>
12
+ </template>
@@ -0,0 +1,8 @@
1
+ <script setup>
2
+ defineProps({ lines: { type: Array, default: () => [] }, height: [Number, String], style: Object })
3
+ </script>
4
+ <template>
5
+ <div :style="{ background: 'var(--bg-inset)', borderRadius: 'var(--radius-md)', border: '1px solid var(--border-subtle)', padding: '10px 12px', display: 'flex', flexDirection: 'column', gap: 6, overflowY: 'auto', height, boxSizing: 'border-box', ...style }">
6
+ <span v-for="(l, i) in lines" :key="i" :style="{ fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', color: typeof l === 'object' ? (l.color || 'var(--text-body)') : 'var(--text-body)', whiteSpace: 'pre' }">{{ typeof l === 'object' ? l.text : l }}</span>
7
+ </div>
8
+ </template>
@@ -1,25 +1,4 @@
1
1
  <script setup>
2
- import { ref, computed } from 'vue'
3
- // Tooltip — hover tooltip — 11px, inverted surface, 6px offset. Wraps its children.
4
- const props = defineProps({ label: String, placement: { type: String, default: 'top' }, style: Object })
5
- const show = ref(false)
6
- const pos = computed(() => ({
7
- top: { bottom: '100%', left: '50%', transform: 'translateX(-50%)', marginBottom: 6 },
8
- bottom: { top: '100%', left: '50%', transform: 'translateX(-50%)', marginTop: 6 },
9
- left: { right: '100%', top: '50%', transform: 'translateY(-50%)', marginRight: 6 },
10
- right: { left: '100%', top: '50%', transform: 'translateY(-50%)', marginLeft: 6 },
11
- }[props.placement]))
12
- </script>
13
- <template>
14
- <span :style="{ position: 'relative', display: 'inline-flex', ...style }" @mouseenter="show = true" @mouseleave="show = false">
15
- <slot />
16
- <span v-if="show" :style="{ position: 'absolute', zIndex: 'var(--z-tooltip)', whiteSpace: 'nowrap', background: 'var(--tooltip-bg)', color: 'var(--tooltip-fg)', borderRadius: 'var(--radius-md)', padding: '5px 8px', fontFamily: 'var(--font-sans)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', boxShadow: 'var(--shadow-menu)', pointerEvents: 'none', animation: 'kb-fade-in var(--dur-fast) var(--ease-out)', ...pos }">{{ label }}</span>
17
- </span>
18
- </template>
19
-
20
- <!-- Modal — centered modal dialog with scrim. width defaults to 440. -->
21
- <!--
22
- <script setup name="Modal">
23
2
  import IconButton from '../buttons/IconButton.vue'
24
3
  defineProps({ open: Boolean, title: String, subtitle: String, width: { type: Number, default: 440 }, style: Object })
25
4
  const emit = defineEmits(['close'])
@@ -39,27 +18,3 @@ const emit = defineEmits(['close'])
39
18
  </div>
40
19
  </div>
41
20
  </template>
42
- -->
43
-
44
- <!-- Toast — single toast row. -->
45
- <!--
46
- <script setup name="Toast">
47
- import { computed } from 'vue'
48
- import Icon from '../icons/Icon.vue'
49
- import IconButton from '../buttons/IconButton.vue'
50
- const toastTones = { success: { icon: 'circle-check', color: 'var(--success-strong)' }, danger: { icon: 'triangle-alert', color: 'var(--danger)' }, warning: { icon: 'triangle-alert', color: 'var(--warning)' }, info: { icon: 'info', color: 'var(--action-primary)' } }
51
- const props = defineProps({ tone: { type: String, default: 'success' }, title: String, meta: String, dismissible: Boolean, style: Object })
52
- const emit = defineEmits(['close'])
53
- const t = computed(() => toastTones[props.tone])
54
- </script>
55
- <template>
56
- <div :style="{ display: 'flex', alignItems: 'flex-start', gap: 10, minWidth: 280, maxWidth: 380, background: 'var(--bg-raised)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', padding: '12px 12px 12px 14px', animation: 'kb-toast-in var(--dur) var(--ease-out)', ...style }">
57
- <span :style="{ color: t.color, display: 'inline-flex', marginTop: 1 }"><Icon :name="t.icon" :size="16" /></span>
58
- <div style="display:flex;flex-direction:column;flex:1;min-width:0">
59
- <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)">{{ title }}</span>
60
- <span v-if="meta" style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)">{{ meta }}</span>
61
- </div>
62
- <IconButton v-if="dismissible" icon="x" size="xs" muted label="Dismiss" @click="emit('close')" />
63
- </div>
64
- </template>
65
- -->
@@ -0,0 +1,6 @@
1
+ <script setup>
2
+ defineProps({ width: { type: [String, Number], default: '100%' }, height: { type: [String, Number], default: 12 }, radius: { type: String, default: 'var(--radius-sm)' }, style: Object })
3
+ </script>
4
+ <template>
5
+ <span :style="{ display: 'block', width, height, borderRadius: radius, background: 'var(--skeleton)', animation: 'kb-pulse 1.4s var(--ease-in-out) infinite', ...style }" />
6
+ </template>
@@ -0,0 +1,6 @@
1
+ <script setup>
2
+ defineProps({ size: { type: Number, default: 16 }, color: { type: String, default: 'var(--action-primary)' }, style: Object })
3
+ </script>
4
+ <template>
5
+ <span :style="{ display: 'inline-block', width: size, height: size, borderRadius: 'var(--radius-full)', border: '2px solid var(--track)', borderTopColor: color, animation: 'kb-spin .7s linear infinite', ...style }" />
6
+ </template>
@@ -0,0 +1,19 @@
1
+ <script setup>
2
+ import { computed } from 'vue'
3
+ import Icon from '../icons/Icon.vue'
4
+ import IconButton from '../buttons/IconButton.vue'
5
+ const toastTones = { success: { icon: 'circle-check', color: 'var(--success-strong)' }, danger: { icon: 'triangle-alert', color: 'var(--danger)' }, warning: { icon: 'triangle-alert', color: 'var(--warning)' }, info: { icon: 'info', color: 'var(--action-primary)' } }
6
+ const props = defineProps({ tone: { type: String, default: 'success' }, title: String, meta: String, dismissible: Boolean, style: Object })
7
+ const emit = defineEmits(['close'])
8
+ const t = computed(() => toastTones[props.tone])
9
+ </script>
10
+ <template>
11
+ <div :style="{ display: 'flex', alignItems: 'flex-start', gap: 10, minWidth: 280, maxWidth: 380, background: 'var(--bg-raised)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', padding: '12px 12px 12px 14px', animation: 'kb-toast-in var(--dur) var(--ease-out)', ...style }">
12
+ <span :style="{ color: t.color, display: 'inline-flex', marginTop: 1 }"><Icon :name="t.icon" :size="16" /></span>
13
+ <div style="display:flex;flex-direction:column;flex:1;min-width:0">
14
+ <span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);line-height:var(--leading-base);letter-spacing:var(--tracking-base);color:var(--text-heading)">{{ title }}</span>
15
+ <span v-if="meta" style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-secondary)">{{ meta }}</span>
16
+ </div>
17
+ <IconButton v-if="dismissible" icon="x" size="xs" muted label="Dismiss" @click="emit('close')" />
18
+ </div>
19
+ </template>
@@ -1,7 +1,7 @@
1
1
  <script setup>
2
2
  import { ref, computed, onMounted, onUnmounted } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
- import { inputBase } from './Input.vue'
4
+ import { inputBase } from './input-base.js'
5
5
  // Typeahead single-select: type to filter options, click or Enter to pick; "No results" empty row.
6
6
  const props = defineProps({ options: { type: Array, default: () => [] }, modelValue: { type: String, default: '' }, placeholder: { type: String, default: 'Select...' }, disabled: Boolean, error: Boolean, width: [Number, String], style: Object })
7
7
  const emit = defineEmits(['update:modelValue'])
@@ -2,7 +2,7 @@
2
2
  import { ref, computed, onMounted, onUnmounted } from 'vue'
3
3
  import Icon from '../icons/Icon.vue'
4
4
  import IconButton from '../buttons/IconButton.vue'
5
- import { inputBase } from './Input.vue'
5
+ import { inputBase } from './input-base.js'
6
6
  // Date picker: input with calendar glyph opening a month-grid popover — Monday-first, today outlined, selected filled. v-model is "YYYY-MM-DD".
7
7
  const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']
8
8
  const pad = (n) => String(n).padStart(2, '0')
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
 
3
- export 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%" };
3
+ import { inputBase } from "./input-base.js";
4
+ export { inputBase };
4
5
 
5
6
  /** Text input, 36px r8. readOnly = #fafafa inset; disabled = 50% opacity; error = red border + ring. */
6
7
  export function Input({ readOnly, disabled, error, style, ...rest }) {
@@ -1,7 +1,8 @@
1
1
  <script setup>
2
2
  import { ref, computed } from 'vue'
3
- 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%' }
3
+ import { inputBase } from './input-base.js'
4
4
  // Text input, 36px r8. v-model. readonly = inset fill; disabled = 50% opacity; error = red border + ring.
5
+ // Multiline variant lives in Textarea.vue.
5
6
  const props = defineProps({ modelValue: String, readonly: Boolean, disabled: Boolean, error: Boolean, style: Object })
6
7
  defineEmits(['update:modelValue'])
7
8
  const focus = ref(false)
@@ -13,26 +14,3 @@ const s = computed(() => ({ ...inputBase, ...(props.readonly ? { background: 'va
13
14
  <template>
14
15
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" :readonly="readonly" :disabled="disabled" @focus="focus = true" @blur="focus = false" :style="s" />
15
16
  </template>
16
-
17
- <!-- Textarea — multiline textarea, r8, default 90px tall. showCount + maxLength renders a bottom-right "n/max" counter (red past the limit). -->
18
- <!--
19
- <script setup name="Textarea">
20
- import { ref, computed } from 'vue'
21
- import { inputBase } from './Input.vue'
22
- const props = defineProps({ modelValue: String, rows: { type: Number, default: 4 }, error: Boolean, disabled: Boolean, showCount: Boolean, maxLength: Number, style: Object })
23
- defineEmits(['update:modelValue'])
24
- const focus = ref(false)
25
- const len = computed(() => (props.modelValue || '').length)
26
- const over = computed(() => props.maxLength != null && len.value > props.maxLength)
27
- const s = computed(() => ({ ...inputBase, height: 90, resize: 'vertical', paddingBottom: props.showCount ? 22 : 8,
28
- ...((props.error || over.value) ? { borderColor: 'var(--danger)' } : null),
29
- ...(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),
30
- ...(props.disabled ? { opacity: .5, cursor: 'not-allowed', background: 'var(--bg-inset)' } : null), ...props.style }))
31
- </script>
32
- <template>
33
- <div style="position:relative">
34
- <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" />
35
- <span v-if="showCount" :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>
36
- </div>
37
- </template>
38
- -->
@@ -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;
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+ import { Icon } from "../icons/Icon.jsx";
3
+
4
+ const radioDims = { sm: { box: 13, dot: 6, font: "var(--text-xs)" }, md: { box: 16, dot: 8, font: "var(--text-sm)" } };
5
+
6
+ /**
7
+ * Radio — 16px (sm: 13px) circle, 1.5px border, filled action-blue dot when selected.
8
+ * hover darkens the ring; keyboard focus shows the 2px focus ring; disabled = 50% opacity.
9
+ * description renders a muted second line under the label (settings-style choices).
10
+ */
11
+ export function Radio({ checked, onChange, label, description, size = "md", disabled, style }) {
12
+ const d = radioDims[size] || radioDims.md;
13
+ const [hover, setHover] = React.useState(false);
14
+ const [focus, setFocus] = React.useState(false);
15
+ const ring = checked ? "var(--action-primary)" : hover && !disabled ? "var(--text-muted)" : "var(--checkbox-border)";
16
+ return (
17
+ <label role="radio" aria-checked={!!checked} aria-disabled={disabled || undefined} tabIndex={disabled ? -1 : 0}
18
+ onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
19
+ onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") { e.preventDefault(); !disabled && onChange && onChange(true); } }}
20
+ 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 }}
21
+ onClick={() => !disabled && onChange && onChange(true)}>
22
+ <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)" }}>
23
+ {checked ? <span style={{ width: d.dot, height: d.dot, borderRadius: "var(--radius-full)", background: "var(--action-primary)" }}></span> : null}
24
+ </span>
25
+ {label || description ? <span style={{ display: "flex", flexDirection: "column", gap: 2 }}>
26
+ {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}
27
+ {description ? <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "16px", color: "var(--text-muted)" }}>{description}</span> : null}
28
+ </span> : null}
29
+ </label>
30
+ );
31
+ }
32
+
33
+ /**
34
+ * RadioGroup — single choice from a small set. Vertical (default) or `row`.
35
+ * options: string[] | { label, value, description?, disabled? }[]
36
+ * Arrow keys move the selection between enabled options (WAI-ARIA radiogroup).
37
+ * variant="cards" renders each option as a bordered selectable card (plan pickers, migration strategies).
38
+ */
39
+ export function RadioGroup({ options = [], value, onChange, row, size = "md", variant = "plain", disabled, style }) {
40
+ const opts = options.map((o) => (typeof o === "string" ? { label: o, value: o } : o));
41
+ const enabled = opts.filter((o) => !o.disabled && !disabled);
42
+ const onKey = (e) => {
43
+ if (!["ArrowDown", "ArrowRight", "ArrowUp", "ArrowLeft"].includes(e.key) || !enabled.length) return;
44
+ e.preventDefault();
45
+ const i = enabled.findIndex((o) => o.value === value);
46
+ const next = e.key === "ArrowDown" || e.key === "ArrowRight" ? enabled[(i + 1) % enabled.length] : enabled[(i - 1 + enabled.length) % enabled.length];
47
+ onChange && onChange(next.value);
48
+ };
49
+ if (variant === "cards") {
50
+ return (
51
+ <div role="radiogroup" onKeyDown={onKey} style={{ display: "flex", flexDirection: row ? "row" : "column", gap: 8, ...style }}>
52
+ {opts.map((o) => {
53
+ const on = value === o.value, off = o.disabled || disabled;
54
+ return <div key={o.value} onClick={() => !off && onChange && onChange(o.value)}
55
+ 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)" }}>
56
+ <Radio checked={on} onChange={() => onChange && onChange(o.value)} label={o.label} description={o.description} size={size} disabled={off} style={{ opacity: 1, cursor: "inherit" }} />
57
+ </div>;
58
+ })}
59
+ </div>
60
+ );
61
+ }
62
+ return (
63
+ <div role="radiogroup" onKeyDown={onKey} style={{ display: "flex", flexDirection: row ? "row" : "column", gap: row ? 16 : 10, ...style }}>
64
+ {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)} />)}
65
+ </div>
66
+ );
67
+ }