@keyfunc/keybricks-ds 1.0.2 → 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.
- package/INTEGRATION.md +18 -0
- package/components/_shared/src-buttons.js +1 -1
- package/components/_shared/src-data.js +1 -1
- package/components/_shared/src-display.js +1 -1
- package/components/_shared/src-feedback.js +1 -1
- package/components/_shared/src-forms.js +1 -1
- package/components/_shared/src-navigation.js +1 -1
- package/components/_shared/src-pipeline.js +1 -1
- package/components/_shared/src-tabs.js +1 -1
- package/components/_vue/index.mjs +21 -0
- package/components/buttons/Button.vue +0 -20
- package/components/buttons/ButtonGroup.vue +16 -0
- package/components/data/KeyValue.vue +0 -16
- package/components/data/KeyValueList.vue +12 -0
- package/components/data/ListRow.vue +1 -1
- package/components/display/AreaChart.vue +14 -0
- package/components/display/Avatar.vue +0 -17
- package/components/display/AvatarGroup.vue +13 -0
- package/components/display/Badge.vue +0 -34
- package/components/display/Card.vue +0 -15
- package/components/display/Donut.vue +17 -0
- package/components/display/IconTile.vue +13 -0
- package/components/display/MiniBars.vue +11 -0
- package/components/display/ProgressBar.vue +0 -54
- package/components/display/StatCard.vue +11 -0
- package/components/display/StatusBadge.vue +11 -0
- package/components/feedback/ActivityItem.vue +1 -32
- package/components/feedback/Alert.vue +0 -36
- package/components/feedback/DotActivity.vue +15 -0
- package/components/feedback/EmptyState.vue +12 -0
- package/components/feedback/LogViewer.vue +8 -0
- package/components/feedback/Modal.vue +0 -45
- package/components/feedback/Skeleton.vue +6 -0
- package/components/feedback/Spinner.vue +6 -0
- package/components/feedback/Toast.vue +19 -0
- package/components/forms/Combobox.vue +1 -1
- package/components/forms/DatePicker.vue +1 -1
- package/components/forms/Input.jsx +2 -1
- package/components/forms/Input.vue +2 -24
- package/components/forms/Textarea.vue +20 -0
- package/components/forms/input-base.js +3 -0
- package/components/navigation/Breadcrumb.vue +0 -22
- package/components/navigation/PageHeading.vue +9 -0
- package/components/navigation/PageToolbar.vue +15 -0
- package/components/navigation/ThemeToggle.vue +18 -0
- package/components/navigation/TopBar.vue +0 -32
- package/components/pipeline/NodeCard.vue +2 -1
- package/components/pipeline/PaletteItem.vue +1 -1
- package/components/pipeline/PalettePanel.jsx +2 -8
- package/components/pipeline/PalettePanel.vue +1 -8
- package/components/pipeline/pipeline-catalog.js +8 -0
- package/components/tabs/PanelTabs.vue +10 -0
- package/components/tabs/PillTabs.vue +10 -0
- package/components/tabs/Tabs.vue +0 -28
- package/package.json +1 -1
- package/readme.md +1 -0
- package/tokens/tokens.json +1 -1
|
@@ -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
|
|
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 './
|
|
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 './
|
|
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
|
-
|
|
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
|
-
|
|
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,20 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { inputBase } from './input-base.js'
|
|
4
|
+
// Multiline textarea, r8, default 90px tall. v-model. showCount + maxLength renders a bottom-right "n/max" counter (red past the limit).
|
|
5
|
+
const props = defineProps({ modelValue: String, rows: { type: Number, default: 4 }, error: Boolean, disabled: Boolean, showCount: Boolean, maxLength: Number, style: Object })
|
|
6
|
+
defineEmits(['update:modelValue'])
|
|
7
|
+
const focus = ref(false)
|
|
8
|
+
const len = computed(() => (props.modelValue || '').length)
|
|
9
|
+
const over = computed(() => props.maxLength != null && len.value > props.maxLength)
|
|
10
|
+
const s = computed(() => ({ ...inputBase, height: '90px', resize: 'vertical', paddingBottom: props.showCount ? '22px' : '8px',
|
|
11
|
+
...((props.error || over.value) ? { borderColor: 'var(--danger)' } : null),
|
|
12
|
+
...(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),
|
|
13
|
+
...(props.disabled ? { opacity: .5, cursor: 'not-allowed', background: 'var(--bg-inset)' } : null), ...props.style }))
|
|
14
|
+
</script>
|
|
15
|
+
<template>
|
|
16
|
+
<div style="position:relative">
|
|
17
|
+
<textarea :rows="rows" :disabled="disabled" :maxlength="showCount ? undefined : maxLength" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" @focus="focus = true" @blur="focus = false" :style="s"></textarea>
|
|
18
|
+
<span v-if="showCount" :style="{ position: 'absolute', right: '10px', bottom: '7px', fontFamily: 'var(--font-mono)', fontSize: 'var(--text-xs)', color: over ? 'var(--danger)' : 'var(--text-muted)' }">{{ len }}{{ maxLength != null ? `/${maxLength}` : '' }}</span>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// Shared base style for text-like controls (Input, Textarea, Combobox, DatePicker).
|
|
2
|
+
// Lives in a plain module because <script setup> cannot export values.
|
|
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%' }
|
|
@@ -62,28 +62,6 @@ const crumbStyle = (i, it) => ({ fontFamily: 'var(--font-sans)', fontSize: 'var(
|
|
|
62
62
|
</nav>
|
|
63
63
|
</template>
|
|
64
64
|
|
|
65
|
-
<!-- ThemeToggle — flips <html data-theme> and persists to localStorage("kb-theme"). Pass `dark` (v-model) to control it externally. -->
|
|
66
|
-
<!--
|
|
67
|
-
<script setup name="ThemeToggle">
|
|
68
|
-
import { ref, computed, watch, onMounted } from 'vue'
|
|
69
|
-
import Icon from '../icons/Icon.vue'
|
|
70
|
-
const props = defineProps({ size: { type: Number, default: 34 }, dark: { type: Boolean, default: null }, style: Object })
|
|
71
|
-
const emit = defineEmits(['update:dark'])
|
|
72
|
-
const darkState = ref(false)
|
|
73
|
-
const dark = computed(() => props.dark != null ? props.dark : darkState.value)
|
|
74
|
-
const hover = ref(false)
|
|
75
|
-
onMounted(() => { darkState.value = document.documentElement.dataset.theme === 'dark' })
|
|
76
|
-
watch(dark, (v) => { if (props.dark != null) return; document.documentElement.dataset.theme = v ? 'dark' : 'light'; try { localStorage.setItem('kb-theme', v ? 'dark' : 'light') } catch (e) {} })
|
|
77
|
-
function toggle() { if (props.dark != null) emit('update:dark', !dark.value); else darkState.value = !darkState.value }
|
|
78
|
-
</script>
|
|
79
|
-
<template>
|
|
80
|
-
<button type="button" :aria-label="dark ? 'Switch to light mode' : 'Switch to dark mode'" @click="toggle" @mouseenter="hover = true" @mouseleave="hover = false"
|
|
81
|
-
:style="{ width: size, height: size, borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: hover ? 'var(--bg-hover)' : 'transparent', color: 'var(--text-body)', padding: 0, transition: 'var(--t-hover)', ...style }">
|
|
82
|
-
<Icon :name="dark ? 'sparkles' : 'moon'" :size="18" />
|
|
83
|
-
</button>
|
|
84
|
-
</template>
|
|
85
|
-
-->
|
|
86
|
-
|
|
87
65
|
<!-- applyStoredTheme() — reads the persisted theme and applies it before first paint. Call once at app start (main.js): -->
|
|
88
66
|
<!--
|
|
89
67
|
export function applyStoredTheme() {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
defineProps({ title: String, subtitle: String, style: Object })
|
|
3
|
+
</script>
|
|
4
|
+
<template>
|
|
5
|
+
<div :style="{ display: 'flex', flexDirection: 'column', gap: 4, ...style }">
|
|
6
|
+
<span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-lg);line-height:var(--leading-lg);letter-spacing:var(--tracking-lg);color:var(--text-primary)">{{ title }}</span>
|
|
7
|
+
<span v-if="subtitle" 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)">{{ subtitle }}</span>
|
|
8
|
+
</div>
|
|
9
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import IconButton from '../buttons/IconButton.vue'
|
|
3
|
+
defineProps({ title: String, status: { type: String, default: '• Editing' }, showBack: Boolean, style: Object })
|
|
4
|
+
const emit = defineEmits(['back'])
|
|
5
|
+
</script>
|
|
6
|
+
<template>
|
|
7
|
+
<div :style="{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 16px', boxSizing: 'border-box', flexShrink: 0, ...style }">
|
|
8
|
+
<div style="display:flex;align-items:center;gap:8px">
|
|
9
|
+
<IconButton v-if="showBack" icon="chevron-left" size="md" @click="emit('back')" />
|
|
10
|
+
<span style="font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)">{{ title }}</span>
|
|
11
|
+
<span v-if="status" style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-muted);margin-left:6px">{{ status }}</span>
|
|
12
|
+
</div>
|
|
13
|
+
<div style="display:flex;align-items:center;gap:8px"><slot name="actions" /></div>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, watch, onMounted } from 'vue'
|
|
3
|
+
import Icon from '../icons/Icon.vue'
|
|
4
|
+
const props = defineProps({ size: { type: Number, default: 34 }, dark: { type: Boolean, default: null }, style: Object })
|
|
5
|
+
const emit = defineEmits(['update:dark'])
|
|
6
|
+
const darkState = ref(false)
|
|
7
|
+
const dark = computed(() => props.dark != null ? props.dark : darkState.value)
|
|
8
|
+
const hover = ref(false)
|
|
9
|
+
onMounted(() => { darkState.value = document.documentElement.dataset.theme === 'dark' })
|
|
10
|
+
watch(dark, (v) => { if (props.dark != null) return; document.documentElement.dataset.theme = v ? 'dark' : 'light'; try { localStorage.setItem('kb-theme', v ? 'dark' : 'light') } catch (e) {} })
|
|
11
|
+
function toggle() { if (props.dark != null) emit('update:dark', !dark.value); else darkState.value = !darkState.value }
|
|
12
|
+
</script>
|
|
13
|
+
<template>
|
|
14
|
+
<button type="button" :aria-label="dark ? 'Switch to light mode' : 'Switch to dark mode'" @click="toggle" @mouseenter="hover = true" @mouseleave="hover = false"
|
|
15
|
+
:style="{ width: size, height: size, borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: hover ? 'var(--bg-hover)' : 'transparent', color: 'var(--text-body)', padding: 0, transition: 'var(--t-hover)', ...style }">
|
|
16
|
+
<Icon :name="dark ? 'sparkles' : 'moon'" :size="18" />
|
|
17
|
+
</button>
|
|
18
|
+
</template>
|
|
@@ -17,35 +17,3 @@ const emit = defineEmits(['search'])
|
|
|
17
17
|
</div>
|
|
18
18
|
</header>
|
|
19
19
|
</template>
|
|
20
|
-
|
|
21
|
-
<!-- PageToolbar — editor toolbar: back chevron, 14px/600 title, "• Editing" status, right action slot. -->
|
|
22
|
-
<!--
|
|
23
|
-
<script setup name="PageToolbar">
|
|
24
|
-
import IconButton from '../buttons/IconButton.vue'
|
|
25
|
-
defineProps({ title: String, status: { type: String, default: '• Editing' }, showBack: Boolean, style: Object })
|
|
26
|
-
const emit = defineEmits(['back'])
|
|
27
|
-
</script>
|
|
28
|
-
<template>
|
|
29
|
-
<div :style="{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 16px', boxSizing: 'border-box', flexShrink: 0, ...style }">
|
|
30
|
-
<div style="display:flex;align-items:center;gap:8px">
|
|
31
|
-
<IconButton v-if="showBack" icon="chevron-left" size="md" @click="emit('back')" />
|
|
32
|
-
<span style="font-family:var(--font-sans);font-weight:var(--weight-semibold);font-size:var(--text-md);line-height:var(--leading-md);letter-spacing:var(--tracking-md);color:var(--text-heading)">{{ title }}</span>
|
|
33
|
-
<span v-if="status" style="font-family:var(--font-sans);font-size:var(--text-sm);line-height:var(--leading-sm);color:var(--text-muted);margin-left:6px">{{ status }}</span>
|
|
34
|
-
</div>
|
|
35
|
-
<div style="display:flex;align-items:center;gap:8px"><slot name="actions" /></div>
|
|
36
|
-
</div>
|
|
37
|
-
</template>
|
|
38
|
-
-->
|
|
39
|
-
|
|
40
|
-
<!-- PageHeading — page heading block: 15px/500 title + 13px muted subtitle. -->
|
|
41
|
-
<!--
|
|
42
|
-
<script setup name="PageHeading">
|
|
43
|
-
defineProps({ title: String, subtitle: String, style: Object })
|
|
44
|
-
</script>
|
|
45
|
-
<template>
|
|
46
|
-
<div :style="{ display: 'flex', flexDirection: 'column', gap: 4, ...style }">
|
|
47
|
-
<span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-lg);line-height:var(--leading-lg);letter-spacing:var(--tracking-lg);color:var(--text-primary)">{{ title }}</span>
|
|
48
|
-
<span v-if="subtitle" 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)">{{ subtitle }}</span>
|
|
49
|
-
</div>
|
|
50
|
-
</template>
|
|
51
|
-
-->
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import Icon from '../icons/Icon.vue'
|
|
4
|
-
import
|
|
4
|
+
import IconTile from '../display/IconTile.vue'
|
|
5
|
+
import StatusBadge from '../display/StatusBadge.vue'
|
|
5
6
|
/**
|
|
6
7
|
* Canvas node card — 220×90, r16, soft shadow. Selected = blue border + blue-100 ring.
|
|
7
8
|
* badge: "error" | "warning" | "clock" overlay on the icon tile.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref } from 'vue'
|
|
3
|
-
import
|
|
3
|
+
import IconTile from '../display/IconTile.vue'
|
|
4
4
|
// Palette grid tile — 40px tinted icon + 10px two-line label.
|
|
5
5
|
defineProps({ icon: String, hue: String, label: String, style: Object })
|
|
6
6
|
defineEmits(['click'])
|