@keyfunc/keybricks-ds 2.4.13 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPLOY.md +4 -4
- package/INTEGRATION.md +28 -6
- package/_adherence.oxlintrc.json +10 -4
- package/_ds_bundle.js +20559 -0
- package/_src/icon-index.js +1 -0
- package/_src/unicons-index.js +1 -0
- package/_src/usage-code.js +737 -0
- package/changelog.js +17 -0
- package/components/_cards/accordion.card.html +1 -1
- package/components/_cards/action-chip.card.html +1 -1
- package/components/_cards/activity.card.html +1 -1
- package/components/_cards/alert.card.html +1 -1
- package/components/_cards/app-chrome.card.html +1 -1
- package/components/_cards/banner.card.html +1 -1
- package/components/_cards/breadcrumb.card.html +1 -1
- package/components/_cards/button-1-base.card.html +1 -1
- package/components/_cards/button-2-states.card.html +1 -1
- package/components/_cards/button-3-sizes-icons.card.html +1 -1
- package/components/_cards/button-4-group.card.html +1 -1
- package/components/_cards/cards-indicators.card.html +1 -1
- package/components/_cards/charts-1-sparkline.card.html +1 -1
- package/components/_cards/charts-10-gauge.card.html +1 -1
- package/components/_cards/charts-11-funnel.card.html +1 -1
- package/components/_cards/charts-12-treemap.card.html +1 -1
- package/components/_cards/charts-14-radar.card.html +1 -1
- package/components/_cards/charts-15-distribution.card.html +1 -1
- package/components/_cards/charts-16-pareto.card.html +1 -1
- package/components/_cards/charts-17-candlestick.card.html +1 -1
- package/components/_cards/charts-18-sankey.card.html +1 -1
- package/components/_cards/charts-19-chord.card.html +1 -1
- package/components/_cards/charts-2-line.card.html +1 -1
- package/components/_cards/charts-20-sunburst.card.html +1 -1
- package/components/_cards/charts-21-worldmap.card.html +1 -1
- package/components/_cards/charts-22-gantt.card.html +1 -1
- package/components/_cards/charts-23-bar3d.card.html +1 -1
- package/components/_cards/charts-24-pie3d.card.html +1 -1
- package/components/_cards/charts-25-surface3d.card.html +1 -1
- package/components/_cards/charts-26-scatter3d.card.html +1 -1
- package/components/_cards/charts-27-ribbon3d.card.html +1 -1
- package/components/_cards/charts-3-bar.card.html +1 -1
- package/components/_cards/charts-4-barlist.card.html +1 -1
- package/components/_cards/charts-5-donut.card.html +1 -1
- package/components/_cards/charts-6-scatter.card.html +1 -1
- package/components/_cards/charts-7-heatmap.card.html +1 -1
- package/components/_cards/charts-8-waterfall.card.html +1 -1
- package/components/_cards/charts-9-bullet.card.html +1 -1
- package/components/_cards/checkbox.card.html +1 -1
- package/components/_cards/codeblock.card.html +1 -1
- package/components/_cards/combobox.card.html +1 -1
- package/components/_cards/context-menu.card.html +1 -1
- package/components/_cards/date-picker.card.html +1 -1
- package/components/_cards/diffvalue.card.html +1 -1
- package/components/_cards/divider.card.html +1 -1
- package/components/_cards/drawer.card.html +1 -1
- package/components/_cards/dropdown-menu.card.html +1 -1
- package/components/_cards/dropzone.card.html +1 -1
- package/components/_cards/empty-state.card.html +1 -1
- package/components/_cards/icon-button.card.html +1 -1
- package/components/_cards/input.card.html +1 -1
- package/components/_cards/kbd.card.html +1 -1
- package/components/_cards/keyvalue.card.html +1 -1
- package/components/_cards/listrow.card.html +1 -1
- package/components/_cards/loading.card.html +1 -1
- package/components/_cards/log-viewer.card.html +1 -1
- package/components/_cards/multi-select.card.html +1 -1
- package/components/_cards/number-stepper.card.html +1 -1
- package/components/_cards/pagination.card.html +1 -1
- package/components/_cards/people.card.html +1 -1
- package/components/_cards/pipeline-node.card.html +1 -1
- package/components/_cards/pipeline-palette.card.html +1 -1
- package/components/_cards/radio.card.html +1 -1
- package/components/_cards/relative-time-picker.card.html +1 -1
- package/components/_cards/search.card.html +1 -1
- package/components/_cards/segmented-control.card.html +1 -1
- package/components/_cards/select.card.html +1 -1
- package/components/_cards/smart-table.card.html +1 -1
- package/components/_cards/spreadsheet.card.html +1 -1
- package/components/_cards/stepper.card.html +1 -1
- package/components/_cards/table.card.html +7 -2
- package/components/_cards/tabs.card.html +1 -1
- package/components/_cards/tag.card.html +1 -1
- package/components/_cards/textarea.card.html +1 -1
- package/components/_cards/timeline.card.html +1 -1
- package/components/_cards/toast.card.html +1 -1
- package/components/_cards/toggles.card.html +1 -1
- package/components/_cards/tooltip.card.html +1 -1
- package/components/_cards/tree.card.html +1 -1
- package/components/_shared/view-source.js +15 -185
- package/components/charts/BarList.jsx +1 -1
- package/components/charts/BarList.vue +8 -3
- package/components/charts/Ribbon3D.d.ts +2 -0
- package/components/charts/Scatter3D.d.ts +4 -0
- package/components/charts/SunburstChart.jsx +6 -1
- package/components/charts/SunburstChart.vue +7 -1
- package/components/charts/Surface3D.d.ts +2 -0
- package/components/charts/WorldMap.jsx +6 -1
- package/components/charts/WorldMap.vue +7 -1
- package/components/data/ListRow.d.ts +6 -3
- package/components/data/ListRow.jsx +21 -13
- package/components/data/ListRow.vue +25 -15
- package/components/data/Table.jsx +1 -1
- package/components/data/Table.vue +6 -2
- package/components/display/ProgressBar.jsx +1 -1
- package/components/display/ProgressBar.vue +1 -1
- package/components/feedback/Accordion.jsx +1 -1
- package/components/feedback/Accordion.vue +1 -1
- package/components/feedback/ContextMenu.jsx +3 -3
- package/components/feedback/ContextMenu.vue +4 -4
- package/components/feedback/Drawer.jsx +3 -3
- package/components/feedback/Drawer.vue +3 -3
- package/components/feedback/Timeline.vue +4 -1
- package/components/forms/Checkbox.d.ts +2 -0
- package/components/forms/Checkbox.jsx +9 -4
- package/components/forms/Checkbox.vue +6 -2
- package/components/forms/DatePicker.jsx +3 -1
- package/components/forms/DatePicker.vue +2 -1
- package/components/forms/Dropzone.jsx +1 -1
- package/components/forms/Dropzone.vue +1 -1
- package/components/forms/MultiSelect.jsx +5 -3
- package/components/forms/MultiSelect.vue +4 -3
- package/components/forms/Slider.jsx +1 -1
- package/components/forms/Slider.vue +1 -1
- package/components/forms/Switch.jsx +4 -4
- package/components/forms/Switch.vue +4 -4
- package/components/navigation/Sidebar.jsx +2 -2
- package/components/navigation/Sidebar.vue +2 -2
- package/components/navigation/TopBar.jsx +1 -1
- package/components/navigation/TopBar.vue +1 -1
- package/components/navigation/Tree.jsx +2 -2
- package/components/navigation/Tree.vue +14 -5
- package/components/pipeline/DataPreview.jsx +1 -1
- package/components/pipeline/DataPreview.vue +1 -1
- package/components/pipeline/NodeCard.jsx +1 -1
- package/components/pipeline/NodeCard.vue +1 -1
- package/components/pipeline/PaletteItem.vue +1 -1
- package/components/pipeline/PalettePanel.jsx +3 -3
- package/components/pipeline/PalettePanel.vue +2 -2
- package/components/tabs/SegmentedControl.jsx +2 -2
- package/components/tabs/SegmentedControl.vue +2 -2
- package/index.html +6 -56
- package/package.json +4 -2
- package/tokens/dark.css +1 -1
- package/tokens/interaction.css +4 -0
- package/tokens/layout.css +3 -0
- package/tokens/typography.css +2 -1
|
@@ -2,24 +2,32 @@ import React from "react";
|
|
|
2
2
|
import { Icon } from "../icons/Icon.jsx";
|
|
3
3
|
import { IconTile } from "../display/Badge.jsx";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const T_TITLE = { fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", color: "var(--text-heading)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" };
|
|
6
|
+
const T_META = { fontFamily: "var(--font-sans)", fontWeight: "var(--weight-regular)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", letterSpacing: "var(--tracking-xs)", color: "var(--text-secondary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" };
|
|
7
|
+
const T_TRAIL = { fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", fontVariantNumeric: "tabular-nums", color: "var(--text-secondary)", whiteSpace: "nowrap", flexShrink: 0 };
|
|
8
|
+
|
|
9
|
+
/** Expandable workflow list row: chevron + 32px hairline icon tile + title / meta + right count.
|
|
10
|
+
* The chevron shows when the row has content to expand (children); the row is a button only when it has onClick —
|
|
11
|
+
* focusable, Enter / Space, aria-expanded, hover wash from tokens/hover.css [data-row]. A row with neither is static.
|
|
12
|
+
* Rows without a chevron keep its 16px slot, so tiles line up in a mixed list. Expanded content starts under the title. */
|
|
6
13
|
export function ListRow({ icon = "file-text", title, meta, trailing, expanded, onClick, children, style }) {
|
|
7
|
-
const
|
|
14
|
+
const hasContent = children != null && children !== false;
|
|
15
|
+
const clickable = typeof onClick === "function";
|
|
16
|
+
const onKey = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick(e); } };
|
|
8
17
|
return (
|
|
9
18
|
<div style={{ borderBottom: "1px solid var(--border-card)", ...style }}>
|
|
10
|
-
<div
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
</div>
|
|
19
|
+
<div data-row={clickable ? "" : undefined} role={clickable ? "button" : undefined} tabIndex={clickable ? 0 : undefined}
|
|
20
|
+
aria-expanded={clickable && hasContent ? !!expanded : undefined} onClick={clickable ? onClick : undefined} onKeyDown={clickable ? onKey : undefined}
|
|
21
|
+
style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px var(--edge-pad)", cursor: clickable ? "pointer" : "default", background: "var(--row-wash, transparent)", outlineOffset: -2, transition: "var(--t-hover)" }}>
|
|
22
|
+
<span aria-hidden="true" style={{ width: 16, flexShrink: 0, display: "inline-flex", color: "var(--text-muted)", transform: expanded ? "rotate(90deg)" : "none", transition: "transform var(--dur-fast) var(--ease-out)" }}>{hasContent ? <Icon name="chevron-right" size={16} /> : null}</span>
|
|
23
|
+
<IconTile icon={icon} kind="row" />
|
|
24
|
+
<div style={{ display: "flex", flexDirection: "column", minWidth: 0, flex: 1 }}>
|
|
25
|
+
<span style={T_TITLE}>{title}</span>
|
|
26
|
+
{meta ? <span style={T_META}>{meta}</span> : null}
|
|
19
27
|
</div>
|
|
20
|
-
{trailing
|
|
28
|
+
{trailing != null && trailing !== "" ? <span style={T_TRAIL}>{trailing}</span> : null}
|
|
21
29
|
</div>
|
|
22
|
-
{expanded &&
|
|
30
|
+
{expanded && hasContent ? <div style={{ padding: "0 var(--edge-pad) 12px calc(var(--edge-pad) + 72px)" }}>{children}</div> : null}
|
|
23
31
|
</div>
|
|
24
32
|
);
|
|
25
33
|
}
|
|
@@ -1,25 +1,35 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import {
|
|
2
|
+
import { computed, getCurrentInstance, useSlots } from 'vue'
|
|
3
3
|
import Icon from '../icons/Icon.vue'
|
|
4
4
|
import IconTile from '../display/IconTile.vue'
|
|
5
|
-
// Expandable workflow list row: chevron +
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// Expandable workflow list row: chevron + 32px hairline icon tile + title / meta + right count.
|
|
6
|
+
// The chevron shows when the row has content to expand (default slot); the row is a button only when it has a @click
|
|
7
|
+
// listener — focusable, Enter / Space, aria-expanded, hover wash from tokens/hover.css [data-row]. A row with neither is static.
|
|
8
|
+
// Rows without a chevron keep its 16px slot, so tiles line up in a mixed list. Expanded content starts under the title.
|
|
9
|
+
const props = defineProps({ icon: { type: String, default: 'file-text' }, title: String, meta: String, trailing: [String, Number], expanded: Boolean, style: Object })
|
|
10
|
+
const emit = defineEmits(['click'])
|
|
11
|
+
const slots = useSlots()
|
|
12
|
+
const inst = getCurrentInstance()
|
|
13
|
+
const hasContent = computed(() => !!slots.default)
|
|
14
|
+
const clickable = computed(() => !!(inst && inst.vnode.props && inst.vnode.props.onClick))
|
|
15
|
+
function onKey(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); emit('click', e) } }
|
|
16
|
+
const T_TITLE = { fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', color: 'var(--text-heading)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }
|
|
17
|
+
const T_META = { fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-regular)', fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', color: 'var(--text-secondary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }
|
|
18
|
+
const T_TRAIL = { fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', fontVariantNumeric: 'tabular-nums', color: 'var(--text-secondary)', whiteSpace: 'nowrap', flexShrink: 0 }
|
|
9
19
|
</script>
|
|
10
20
|
<template>
|
|
11
21
|
<div :style="{ borderBottom: '1px solid var(--border-card)', ...style }">
|
|
12
|
-
<div
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</
|
|
22
|
+
<div :data-row="clickable ? '' : undefined" :role="clickable ? 'button' : undefined" :tabindex="clickable ? 0 : undefined"
|
|
23
|
+
:aria-expanded="clickable && hasContent ? !!expanded : undefined" @click="clickable && emit('click', $event)" @keydown="clickable && onKey($event)"
|
|
24
|
+
:style="{ display: 'flex', alignItems: 'center', gap: '12px', padding: '12px var(--edge-pad)', cursor: clickable ? 'pointer' : 'default', background: 'var(--row-wash, transparent)', outlineOffset: '-2px', transition: 'var(--t-hover)' }">
|
|
25
|
+
<span aria-hidden="true" :style="{ width: '16px', flexShrink: 0, display: 'inline-flex', color: 'var(--text-muted)', transform: expanded ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }"><Icon v-if="hasContent" name="chevron-right" :size="16" /></span>
|
|
26
|
+
<IconTile :icon="icon" kind="row" />
|
|
27
|
+
<div style="display:flex;flex-direction:column;min-width:0;flex:1">
|
|
28
|
+
<span :style="T_TITLE">{{ title }}</span>
|
|
29
|
+
<span v-if="meta" :style="T_META">{{ meta }}</span>
|
|
20
30
|
</div>
|
|
21
|
-
<span v-if="trailing" style="
|
|
31
|
+
<span v-if="trailing != null && trailing !== ''" :style="T_TRAIL">{{ trailing }}</span>
|
|
22
32
|
</div>
|
|
23
|
-
<div v-if="expanded" style="padding:0
|
|
33
|
+
<div v-if="expanded && hasContent" style="padding:0 var(--edge-pad) 12px calc(var(--edge-pad) + 72px)"><slot /></div>
|
|
24
34
|
</div>
|
|
25
35
|
</template>
|
|
@@ -89,7 +89,7 @@ function TreeToggle({ open, label, onClick }) {
|
|
|
89
89
|
return (
|
|
90
90
|
<button type="button" aria-expanded={open} aria-label={label} title={label} onClick={(e) => { e.stopPropagation(); onClick(); }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
91
91
|
style={{ width: TREE_TOGGLE, height: TREE_TOGGLE, padding: 0, border: "none", borderRadius: "var(--radius-sm)", display: "inline-flex", alignItems: "center", justifyContent: "center", cursor: "pointer", background: hover ? "var(--overlay-hover)" : "transparent", color: hover ? "var(--text-body)" : "var(--text-muted)", transition: "var(--t-hover)" }}>
|
|
92
|
-
<Icon name="chevron-right" size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)", transform: open ? "rotate(90deg)" : "none", transition: "transform
|
|
92
|
+
<Icon name="chevron-right" size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)", transform: open ? "rotate(90deg)" : "none", transition: "transform var(--dur-fast) var(--ease-out)" }} />
|
|
93
93
|
</button>
|
|
94
94
|
);
|
|
95
95
|
}
|
|
@@ -5,7 +5,11 @@ import Checkbox from '../forms/Checkbox.vue'
|
|
|
5
5
|
import IconTile from '../display/IconTile.vue'
|
|
6
6
|
import IconButton from '../buttons/IconButton.vue'
|
|
7
7
|
import Icon from '../icons/Icon.vue'
|
|
8
|
-
//
|
|
8
|
+
// A cell: a #cell-<key> slot ({ value, row, index }) when the page gives one; else column.render's result — text or a VNode from h() — or the raw value.
|
|
9
|
+
const Cell = (p) => p.v
|
|
10
|
+
Cell.props = ['v']
|
|
11
|
+
// Keybricks table. columns: [{key,label,width,minWidth,mono,code,twoLine,align,render}]. Custom cells: <template #cell-status="{ value, row, index }">…</template>,
|
|
12
|
+
// or render(value, row, index) returning text or a VNode (h()).
|
|
9
13
|
// twoLine — the name column of a document list: icon tile, title over a description, optional mark ("favorite" = filled star), trailing ⋯
|
|
10
14
|
// (@row-more(row, index, event)); @row-open(row, index, event) makes the tile and the title open the row (hand cursor, Enter / Space on the title).
|
|
11
15
|
// tree — rows nest through `children` (or `hasChildren` for rows you load on demand); the twoLine column indents 24px per level with an
|
|
@@ -184,7 +188,7 @@ function cellPad(i) { return { ...(i === 0 && props.selectable ? { position: 're
|
|
|
184
188
|
<tr v-else-if="pinInfo && pinInfo.style === 'labels' && pinInfo.grouped && pos === pinInfo.unStart"><td :colspan="columns.length || 1" :style="GROUP_CELL">{{ L.unavailable }} · {{ pinInfo.un }}</td></tr>
|
|
185
189
|
<tr data-row="" :aria-level="tree ? item.level + 1 : undefined" :aria-expanded="item.kids ? item.open : undefined" :style="{ ...(isDis(item.r, item.ri) ? { '--row-wash': 'initial' } : null), height: 'var(--bar-h)', background: pinInfo && pinInfo.sel.has(item.ri) ? 'var(--row-wash, var(--bg-inset))' : 'var(--row-wash, transparent)', transition: 'var(--t-hover)', borderBottom: pinInfo && ((pinInfo.rest > 0 && pos === pinInfo.rest - 1) || (pinInfo.unStart > 0 && pos === pinInfo.unStart - 1)) ? '1px solid var(--border-control)' : bordered && pos === view.length - 1 ? 'none' : '1px solid var(--border-subtle)' }">
|
|
186
190
|
<template v-for="{ r, ri } in [item]" :key="'r'">
|
|
187
|
-
<td v-for="(c, i) in columns" :key="c.key" :style="{ ...cellPad(i), ...(c.twoLine ? { paddingTop: 'var(--title-cell-py)', paddingBottom: 'var(--title-cell-py)', maxWidth: 0 } : null), textAlign: c.align || 'left', whiteSpace: 'nowrap', ...(resizable ? { overflow: 'hidden', textOverflow: 'ellipsis' } : null), ...cellFont(c) }"><span v-if="selectable && i === 0" :style="checkSlot" :title="isDis(r, ri) && disabledReason ? disabledReason(r, ri) : undefined"><Checkbox :checked="selected.includes(ri)" :disabled="isDis(r, ri)" @change="v => emit('toggle', ri, v)" /></span><div v-if="c.twoLine" :style="{ display: 'flex', alignItems: 'center', minWidth: px(c.minWidth ?? 180), ...(treeCol === c ? { paddingLeft: (item.level * TREE_INDENT) + 'px' } : null) }"><span v-if="treeCol === c" :style="{ width: TREE_TOGGLE + 'px', flexShrink: 0, marginRight: (TREE_INDENT - TREE_TOGGLE) + 'px', display: 'inline-flex' }"><button v-if="item.kids" type="button" :aria-expanded="item.open" :aria-label="item.open ? L.collapse : L.expand" :title="item.open ? L.collapse : L.expand" @click.stop="toggleRow(item)" @mouseenter="hotToggle = item.key" @mouseleave="hotToggle = null" :style="{ width: TREE_TOGGLE + 'px', height: TREE_TOGGLE + 'px', padding: 0, border: 'none', borderRadius: 'var(--radius-sm)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', background: hotToggle === item.key ? 'var(--overlay-hover)' : 'transparent', color: hotToggle === item.key ? 'var(--text-body)' : 'var(--text-muted)', transition: 'var(--t-hover)' }"><Icon name="chevron-right" :size="16" :style="{ width: 'var(--control-icon)', height: 'var(--control-icon)', transform: item.open ? 'rotate(90deg)' : 'none', transition: 'transform
|
|
191
|
+
<td v-for="(c, i) in columns" :key="c.key" :style="{ ...cellPad(i), ...(c.twoLine ? { paddingTop: 'var(--title-cell-py)', paddingBottom: 'var(--title-cell-py)', maxWidth: 0 } : null), textAlign: c.align || 'left', whiteSpace: 'nowrap', ...(resizable ? { overflow: 'hidden', textOverflow: 'ellipsis' } : null), ...cellFont(c) }"><span v-if="selectable && i === 0" :style="checkSlot" :title="isDis(r, ri) && disabledReason ? disabledReason(r, ri) : undefined"><Checkbox :checked="selected.includes(ri)" :disabled="isDis(r, ri)" @change="v => emit('toggle', ri, v)" /></span><div v-if="c.twoLine" :style="{ display: 'flex', alignItems: 'center', minWidth: px(c.minWidth ?? 180), ...(treeCol === c ? { paddingLeft: (item.level * TREE_INDENT) + 'px' } : null) }"><span v-if="treeCol === c" :style="{ width: TREE_TOGGLE + 'px', flexShrink: 0, marginRight: (TREE_INDENT - TREE_TOGGLE) + 'px', display: 'inline-flex' }"><button v-if="item.kids" type="button" :aria-expanded="item.open" :aria-label="item.open ? L.collapse : L.expand" :title="item.open ? L.collapse : L.expand" @click.stop="toggleRow(item)" @mouseenter="hotToggle = item.key" @mouseleave="hotToggle = null" :style="{ width: TREE_TOGGLE + 'px', height: TREE_TOGGLE + 'px', padding: 0, border: 'none', borderRadius: 'var(--radius-sm)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', background: hotToggle === item.key ? 'var(--overlay-hover)' : 'transparent', color: hotToggle === item.key ? 'var(--text-body)' : 'var(--text-muted)', transition: 'var(--t-hover)' }"><Icon name="chevron-right" :size="16" :style="{ width: 'var(--control-icon)', height: 'var(--control-icon)', transform: item.open ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }" /></button></span><div style="display:flex;align-items:center;gap:var(--title-cell-tile-gap);flex:1;min-width:0"><template v-for="t in [tl(c, r, ri)]" :key="0"><span v-if="t.icon" :style="{ display: 'inline-flex', flexShrink: 0, cursor: canOpen ? 'pointer' : undefined }" @click="e => canOpen && openRow(r, ri, e)"><IconTile :icon="t.icon" :hue="t.hue || 'neutral'" kind="row" :style="T_TILE" :icon-style="T_ICON" /></span><div style="display:flex;flex-direction:column;gap:var(--title-cell-gap);flex:1;min-width:0"><div style="display:flex;align-items:center;gap:4px;min-width:0"><span v-if="canOpen" role="button" tabindex="0" :style="{ ...T_TITLE, cursor: 'pointer' }" @click="e => openRow(r, ri, e)" @keydown="e => openKey(r, ri, e)"><slot :name="'cell-' + c.key" :value="r[c.key]" :row="r" :index="ri"><Cell :v="c.render ? c.render(r[c.key], r, ri) : r[c.key]" /></slot></span><span v-else :style="T_TITLE"><slot :name="'cell-' + c.key" :value="r[c.key]" :row="r" :index="ri"><Cell :v="c.render ? c.render(r[c.key], r, ri) : r[c.key]" /></slot></span><svg v-if="t.mark === 'favorite'" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round" role="img" aria-label="Favorite" style="display:block;flex-shrink:0;color:var(--warning)"><path :d="STAR_FILLED" /></svg><span v-else-if="t.mark" role="img" :aria-label="t.mark.label" :style="{ display: 'inline-flex', flexShrink: 0, color: t.mark.color || 'var(--text-muted)' }"><Icon :name="t.mark.icon" :size="14" /></span></div><span v-if="t.desc" :style="T_DESC">{{ t.desc }}</span></div><IconButton v-if="t.more" icon="ellipsis" size="sm" muted :label="L.more" :title="L.more" @click="e => more(r, ri, e)" /></template></div></div><code v-else-if="c.code && (typeof r[c.key] === 'string' || typeof r[c.key] === 'number') && r[c.key] !== ''" :style="codeChip"><slot :name="'cell-' + c.key" :value="r[c.key]" :row="r" :index="ri"><Cell :v="c.render ? c.render(r[c.key], r, ri) : r[c.key]" /></slot></code><template v-else><slot :name="'cell-' + c.key" :value="r[c.key]" :row="r" :index="ri"><Cell :v="c.render ? c.render(r[c.key], r, ri) : r[c.key]" /></slot></template></td>
|
|
188
192
|
</template>
|
|
189
193
|
</tr>
|
|
190
194
|
</template>
|
|
@@ -11,7 +11,7 @@ export function ProgressBar({ label, value = 50, color = "var(--chart-blue)", sh
|
|
|
11
11
|
</div>
|
|
12
12
|
) : null}
|
|
13
13
|
<div style={{ height: 6, borderRadius: "var(--radius-full)", background: "var(--track)", overflow: "hidden" }}>
|
|
14
|
-
<div style={{ width: `${value}%`, height: "100%", borderRadius: "var(--radius-full)", background: color, transition: "width
|
|
14
|
+
<div style={{ width: `${value}%`, height: "100%", borderRadius: "var(--radius-full)", background: color, transition: "width var(--dur-slow) var(--ease-out)" }} />
|
|
15
15
|
</div>
|
|
16
16
|
</div>
|
|
17
17
|
);
|
|
@@ -9,7 +9,7 @@ defineProps({ label: String, value: { type: Number, default: 50 }, color: { type
|
|
|
9
9
|
<span v-if="showValue" style="color:var(--text-heading)">{{ value }}%</span>
|
|
10
10
|
</div>
|
|
11
11
|
<div style="height:6px;border-radius:var(--radius-full);background:var(--track);overflow:hidden">
|
|
12
|
-
<div :style="{ width: value + '%', height: '100%', borderRadius: 'var(--radius-full)', background: color, transition: 'width
|
|
12
|
+
<div :style="{ width: value + '%', height: '100%', borderRadius: 'var(--radius-full)', background: color, transition: 'width var(--dur-slow) var(--ease-out)' }" />
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
15
15
|
</template>
|
|
@@ -8,7 +8,7 @@ export function Accordion({ title, subtitle, defaultOpen, open: openProp, onTogg
|
|
|
8
8
|
const toggle = () => (onToggle ? onToggle(!open) : setOpenState(!open));
|
|
9
9
|
return (
|
|
10
10
|
<div style={{ borderBottom: "1px solid var(--border-card)", ...style }}>
|
|
11
|
-
<div onClick={toggle} data-wash="" style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 8px", margin: "0 -4px", borderRadius: "var(--radius-md)", background: "var(--wash, transparent)", transition: "var(--t-hover)", cursor: "pointer" }}>
|
|
11
|
+
<div role="button" tabIndex={0} aria-expanded={!!open} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); } }} onClick={toggle} data-wash="" style={{ display: "flex", alignItems: "center", gap: 10, padding: "14px 8px", margin: "0 -4px", borderRadius: "var(--radius-md)", background: "var(--wash, transparent)", transition: "var(--t-hover)", cursor: "pointer" }}>
|
|
12
12
|
<span style={{ color: "var(--text-muted)", display: "inline-flex", transform: open ? "rotate(90deg)" : "none", transition: "transform var(--dur-fast) var(--ease-out)" }}><Icon name="chevron-right" size={14} /></span>
|
|
13
13
|
<div style={{ display: "flex", flexDirection: "column", flex: 1 }}>
|
|
14
14
|
<span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-medium)", fontSize: "var(--text-base)", letterSpacing: "var(--tracking-base)", color: "var(--text-heading)" }}>{title}</span>
|
|
@@ -10,7 +10,7 @@ function toggle() { if (props.open != null) emit('toggle', !open.value); else op
|
|
|
10
10
|
</script>
|
|
11
11
|
<template>
|
|
12
12
|
<div :style="{ borderBottom: '1px solid var(--border-card)', ...style }">
|
|
13
|
-
<div @click="toggle" data-wash="" style="display:flex;align-items:center;gap:10px;padding:14px 8px;margin:0 -4px;border-radius:var(--radius-md);background:var(--wash, transparent);transition:var(--t-hover);cursor:pointer">
|
|
13
|
+
<div role="button" tabindex="0" :aria-expanded="!!open" @keydown.enter.prevent="toggle" @keydown.space.prevent="toggle" @click="toggle" data-wash="" style="display:flex;align-items:center;gap:10px;padding:14px 8px;margin:0 -4px;border-radius:var(--radius-md);background:var(--wash, transparent);transition:var(--t-hover);cursor:pointer">
|
|
14
14
|
<span :style="{ color: 'var(--text-muted)', display: 'inline-flex', transform: open ? 'rotate(90deg)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)' }"><Icon name="chevron-right" :size="14" /></span>
|
|
15
15
|
<div style="display:flex;flex-direction:column;flex:1">
|
|
16
16
|
<span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);letter-spacing:var(--tracking-base);color:var(--text-heading)">{{ title }}</span>
|
|
@@ -30,13 +30,13 @@ export function ContextMenu({ items = [], onSelect, size, descriptionSide = "rig
|
|
|
30
30
|
};
|
|
31
31
|
const onBlur = (e) => { setHover(-1); if (hasHint && !(wrap.current && wrap.current.contains(e.relatedTarget))) setHint(-1); };
|
|
32
32
|
const menu = (
|
|
33
|
-
<div role="menu" data-size={hasHint ? undefined : size} style={{ background: "var(--bg-surface)", borderRadius: "var(--radius-lg)", border: "1px solid var(--border-card)", boxShadow: "var(--shadow-menu)", padding: 4, minWidth: 148, display: "inline-flex", flexDirection: "column", gap: choice ? 2 : 0, ...style }}>
|
|
33
|
+
<div role="menu" data-size={hasHint ? undefined : size} style={{ background: "var(--bg-surface)", borderRadius: "var(--radius-lg)", border: "1px solid var(--border-card)", boxShadow: "var(--shadow-menu)", padding: 4, minWidth: 148, maxWidth: 320, display: "inline-flex", flexDirection: "column", gap: choice ? 2 : 0, ...style }}>
|
|
34
34
|
{items.map((it, i) => it.divider ? <div key={i} role="separator" style={{ height: 1, flexShrink: 0, background: "var(--border-subtle)", margin: "4px 0" }} /> : (
|
|
35
35
|
<button key={i} type="button" onClick={() => onSelect && onSelect(it.label)}
|
|
36
36
|
onMouseEnter={() => show(i)} onMouseLeave={() => setHover(-1)} onFocus={() => show(i)} onBlur={onBlur}
|
|
37
37
|
aria-describedby={hint === i ? hintId : undefined}
|
|
38
|
-
role={choice ? "menuitemradio" : "menuitem"} aria-checked={choice ? !!it.selected : undefined} style={{ display: "flex", alignItems: "center", gap: 10, height: "var(--control-h)", flexShrink: 0, padding: "0 10px", boxSizing: "border-box", borderRadius: "var(--radius-md)", border: "none", cursor: "pointer", background: it.selected || hover === i || hint === i ? "var(--bg-hover)" : "transparent", fontFamily: "var(--font-sans)", fontWeight: it.selected ? "var(--weight-medium)" : "var(--weight-regular)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: it.danger ? "var(--danger)" : "var(--text-heading)", textAlign: "left", whiteSpace: "nowrap" }}>
|
|
39
|
-
{it.icon ? <Icon name={it.icon} size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)", flexShrink: 0, ...(it.iconColor ? { color: it.iconColor } : null) }} /> : null}{it.label}
|
|
38
|
+
role={choice ? "menuitemradio" : "menuitem"} aria-checked={choice ? !!it.selected : undefined} style={{ display: "flex", alignItems: "center", gap: 10, height: "var(--control-h)", flexShrink: 0, padding: "0 10px", boxSizing: "border-box", borderRadius: "var(--radius-md)", border: "none", cursor: "pointer", background: it.selected || hover === i || hint === i ? "var(--bg-hover)" : "transparent", fontFamily: "var(--font-sans)", fontWeight: it.selected ? "var(--weight-medium)" : "var(--weight-regular)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: it.danger ? "var(--danger)" : "var(--text-heading)", textAlign: "left", whiteSpace: "nowrap", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }} title={it.label}>
|
|
39
|
+
{it.icon ? <Icon name={it.icon} size={16} style={{ width: "var(--control-icon)", height: "var(--control-icon)", flexShrink: 0, ...(it.iconColor ? { color: it.iconColor } : null) }} /> : null}<span style={{ minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>{it.label}</span>
|
|
40
40
|
</button>
|
|
41
41
|
))}
|
|
42
42
|
</div>
|
|
@@ -26,8 +26,8 @@ function show(i) {
|
|
|
26
26
|
}
|
|
27
27
|
function onBlur(e) { hover.value = -1; if (hasHint.value && !(wrap.value && wrap.value.contains(e.relatedTarget))) hint.value = -1 }
|
|
28
28
|
const cur = computed(() => props.items[hint.value])
|
|
29
|
-
const menuStyle = computed(() => ({ background: 'var(--bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-menu)', padding: '4px', minWidth: '148px', display: 'inline-flex', flexDirection: 'column', gap: choice.value ? '2px' : 0, ...props.style }))
|
|
30
|
-
const rowStyle = (it, i) => ({ display: 'flex', alignItems: 'center', gap: '10px', height: 'var(--control-h)', flexShrink: 0, padding: '0 10px', boxSizing: 'border-box', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', background: it.selected || hover.value === i || hint.value === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontWeight: it.selected ? 'var(--weight-medium)' : 'var(--weight-regular)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: it.danger ? 'var(--danger)' : 'var(--text-heading)', textAlign: 'left', whiteSpace: 'nowrap' })
|
|
29
|
+
const menuStyle = computed(() => ({ background: 'var(--bg-surface)', borderRadius: 'var(--radius-lg)', border: '1px solid var(--border-card)', boxShadow: 'var(--shadow-menu)', padding: '4px', minWidth: '148px', maxWidth: '320px', display: 'inline-flex', flexDirection: 'column', gap: choice.value ? '2px' : 0, ...props.style }))
|
|
30
|
+
const rowStyle = (it, i) => ({ display: 'flex', alignItems: 'center', gap: '10px', height: 'var(--control-h)', flexShrink: 0, padding: '0 10px', boxSizing: 'border-box', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', background: it.selected || hover.value === i || hint.value === i ? 'var(--bg-hover)' : 'transparent', fontFamily: 'var(--font-sans)', fontWeight: it.selected ? 'var(--weight-medium)' : 'var(--weight-regular)', fontSize: 'var(--control-text)', lineHeight: 'var(--control-leading)', letterSpacing: 'var(--control-tracking)', color: it.danger ? 'var(--danger)' : 'var(--text-heading)', textAlign: 'left', whiteSpace: 'nowrap', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' })
|
|
31
31
|
const iconStyle = (it) => ({ width: 'var(--control-icon)', height: 'var(--control-icon)', flexShrink: 0, ...(it.iconColor ? { color: it.iconColor } : null) })
|
|
32
32
|
const hintStyle = computed(() => ({ position: 'absolute', top: 0, [side.value === 'left' ? 'right' : 'left']: `calc(100% + ${HINT_GAP}px)`, width: HINT_W + 'px', boxSizing: 'border-box', padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: '6px', background: 'var(--bg-surface)', border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-menu)', fontFamily: 'var(--font-sans)', animation: 'kb-fade-in var(--dur) var(--ease-out)' }))
|
|
33
33
|
</script>
|
|
@@ -38,7 +38,7 @@ const hintStyle = computed(() => ({ position: 'absolute', top: 0, [side.value ==
|
|
|
38
38
|
<div v-if="it.divider" role="separator" :style="{ height: '1px', flexShrink: 0, background: 'var(--border-subtle)', margin: '4px 0' }" />
|
|
39
39
|
<button v-else type="button" @click="emit('select', it.label)" @mouseenter="show(i)" @mouseleave="hover = -1" @focus="show(i)" @blur="onBlur"
|
|
40
40
|
:aria-describedby="hint === i ? hintId : undefined" :role="choice ? 'menuitemradio' : 'menuitem'" :aria-checked="choice ? !!it.selected : undefined" :style="rowStyle(it, i)">
|
|
41
|
-
<Icon v-if="it.icon" :name="it.icon" :size="16" :style="iconStyle(it)"
|
|
41
|
+
<Icon v-if="it.icon" :name="it.icon" :size="16" :style="iconStyle(it)" /><span style="min-width:0;overflow:hidden;text-overflow:ellipsis">{{ it.label }}</span>
|
|
42
42
|
</button>
|
|
43
43
|
</template>
|
|
44
44
|
</div>
|
|
@@ -55,7 +55,7 @@ const hintStyle = computed(() => ({ position: 'absolute', top: 0, [side.value ==
|
|
|
55
55
|
<div v-if="it.divider" role="separator" :style="{ height: '1px', flexShrink: 0, background: 'var(--border-subtle)', margin: '4px 0' }" />
|
|
56
56
|
<button v-else type="button" @click="emit('select', it.label)" @mouseenter="hover = i" @mouseleave="hover = -1" @focus="hover = i" @blur="hover = -1"
|
|
57
57
|
:role="choice ? 'menuitemradio' : 'menuitem'" :aria-checked="choice ? !!it.selected : undefined" :style="rowStyle(it, i)">
|
|
58
|
-
<Icon v-if="it.icon" :name="it.icon" :size="16" :style="iconStyle(it)"
|
|
58
|
+
<Icon v-if="it.icon" :name="it.icon" :size="16" :style="iconStyle(it)" /><span style="min-width:0;overflow:hidden;text-overflow:ellipsis">{{ it.label }}</span>
|
|
59
59
|
</button>
|
|
60
60
|
</template>
|
|
61
61
|
</div>
|
|
@@ -5,10 +5,10 @@ import { IconButton } from "../buttons/IconButton.jsx";
|
|
|
5
5
|
export function Drawer({ open = true, title, subtitle, width = 400, inline, height, size, footer, onClose, children, style }) {
|
|
6
6
|
if (!open) return null;
|
|
7
7
|
const panel = (
|
|
8
|
-
<div role="dialog" aria-label={title} data-size={size} style={{ width, maxWidth: "100%", height: inline ? (height || 420) : "100%", background: "var(--bg-raised)", borderLeft: "1px solid var(--border-card)", boxShadow: inline ? "var(--shadow-menu)" : "-
|
|
8
|
+
<div role="dialog" aria-label={title} data-size={size} style={{ width, maxWidth: "100%", height: inline ? (height || 420) : "100%", background: "var(--bg-raised)", borderLeft: "1px solid var(--border-card)", boxShadow: inline ? "var(--shadow-menu)" : "var(--shadow-drawer)", display: "flex", flexDirection: "column", boxSizing: "border-box", ...(inline ? { border: "1px solid var(--border-card)", borderRadius: "var(--radius-lg)" } : null), ...style }}>
|
|
9
9
|
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", padding: "16px 16px 12px", borderBottom: "1px solid var(--border-card)", flexShrink: 0 }}>
|
|
10
|
-
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
|
11
|
-
<span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-semibold)", fontSize: "var(--text-md)", lineHeight: "var(--leading-md)", letterSpacing: "var(--tracking-md)", color: "var(--text-heading)" }}>{title}</span>
|
|
10
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 3, minWidth: 0 }}>
|
|
11
|
+
<span style={{ fontFamily: "var(--font-sans)", fontWeight: "var(--weight-semibold)", fontSize: "var(--text-md)", lineHeight: "var(--leading-md)", letterSpacing: "var(--tracking-md)", color: "var(--text-heading)", minWidth: 0, overflowWrap: "anywhere" }}>{title}</span>
|
|
12
12
|
{subtitle && <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", color: "var(--text-secondary)" }}>{subtitle}</span>}
|
|
13
13
|
</div>
|
|
14
14
|
<IconButton icon="x" label="Close" onClick={onClose} style={{ margin: "calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0" }} />
|
|
@@ -14,10 +14,10 @@ const contents = { display: 'contents' }
|
|
|
14
14
|
:style="inline ? contents : { position: 'fixed', inset: 0, zIndex: 'var(--z-modal)', background: 'var(--bg-scrim)', display: 'flex', justifyContent: 'flex-end', animation: 'kb-fade-in var(--dur-fast) var(--ease-out)' }">
|
|
15
15
|
<div @click.stop :style="inline ? contents : { height: '100%', animation: 'kb-slide-in var(--dur) var(--ease-out)' }">
|
|
16
16
|
<div role="dialog" :aria-label="title" :data-size="size"
|
|
17
|
-
:style="{ width: px(width), maxWidth: '100%', height: inline ? px(height) : '100%', background: 'var(--bg-raised)', borderLeft: '1px solid var(--border-card)', boxShadow: inline ? 'var(--shadow-menu)' : '-
|
|
17
|
+
:style="{ width: px(width), maxWidth: '100%', height: inline ? px(height) : '100%', background: 'var(--bg-raised)', borderLeft: '1px solid var(--border-card)', boxShadow: inline ? 'var(--shadow-menu)' : 'var(--shadow-drawer)', display: 'flex', flexDirection: 'column', boxSizing: 'border-box', ...(inline ? { border: '1px solid var(--border-card)', borderRadius: 'var(--radius-lg)' } : null), ...style }">
|
|
18
18
|
<div style="display:flex;align-items:flex-start;justify-content:space-between;padding:16px 16px 12px;border-bottom:1px solid var(--border-card);flex-shrink:0">
|
|
19
|
-
<div style="display:flex;flex-direction:column;gap:3px">
|
|
20
|
-
<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>
|
|
19
|
+
<div style="display:flex;flex-direction:column;gap:3px;min-width:0">
|
|
20
|
+
<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);min-width:0;overflow-wrap:anywhere">{{ title }}</span>
|
|
21
21
|
<span v-if="subtitle" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary)">{{ subtitle }}</span>
|
|
22
22
|
</div>
|
|
23
23
|
<IconButton icon="x" label="Close" :style="{ margin: 'calc((var(--leading-md) - var(--control-h)) / 2) -6px 0 0' }" @click="emit('close')" />
|
|
@@ -3,6 +3,9 @@ import Icon from '../icons/Icon.vue'
|
|
|
3
3
|
const tlTones = { success: 'var(--success)', danger: 'var(--danger)', warning: 'var(--warning)', info: 'var(--action-primary)', neutral: 'var(--dot-neutral)' }
|
|
4
4
|
// Vertical activity timeline: outlined icon nodes on a hairline spine. items: [{icon,tone,title,meta,body}].
|
|
5
5
|
defineProps({ items: { type: Array, default: () => [] }, style: Object })
|
|
6
|
+
// body may be text or a VNode (h(...)); a #body="{ item, index }" slot overrides it.
|
|
7
|
+
const Cell = (p) => p.v
|
|
8
|
+
Cell.props = ['v']
|
|
6
9
|
</script>
|
|
7
10
|
<template>
|
|
8
11
|
<div :style="{ display: 'flex', flexDirection: 'column', ...style }">
|
|
@@ -14,7 +17,7 @@ defineProps({ items: { type: Array, default: () => [] }, style: Object })
|
|
|
14
17
|
<div :style="{ display: 'flex', flexDirection: 'column', gap: '2px', paddingBottom: i < items.length - 1 ? '18px' : 0, minWidth: 0, paddingTop: '3px' }">
|
|
15
18
|
<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)">{{ it.title }}</span>
|
|
16
19
|
<span v-if="it.meta" 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)">{{ it.meta }}</span>
|
|
17
|
-
<span v-if="it.body" style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body);padding-top:4px"
|
|
20
|
+
<span v-if="it.body" style="font-family:var(--font-sans);font-size:var(--text-base);line-height:var(--leading-base);color:var(--text-body);padding-top:4px"><slot name="body" :item="it" :index="i"><Cell :v="it.body" /></slot></span>
|
|
18
21
|
</div>
|
|
19
22
|
</div>
|
|
20
23
|
</div>
|
|
@@ -9,6 +9,8 @@ export interface CheckboxProps {
|
|
|
9
9
|
/** Size tier. Omit to inherit the enclosing region's tier (the default). */
|
|
10
10
|
size?: "sm" | "md" | "lg";
|
|
11
11
|
disabled?: boolean;
|
|
12
|
+
/** Tab order. Default 0; pass -1 inside a control that owns focus (Tree row, MultiSelect option). */
|
|
13
|
+
tabIndex?: number;
|
|
12
14
|
style?: React.CSSProperties;
|
|
13
15
|
}
|
|
14
16
|
export declare function Checkbox(props: CheckboxProps): JSX.Element;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Icon } from "../icons/Icon.jsx";
|
|
3
3
|
|
|
4
|
-
/** Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text.
|
|
5
|
-
|
|
4
|
+
/** Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text.
|
|
5
|
+
* Keyboard: the label is role="checkbox" (aria-checked true / false / "mixed"), a Tab stop, Space toggles. Inside a control that owns focus
|
|
6
|
+
* (a Tree row, a MultiSelect option) pass tabIndex={-1} so the row stays the single stop. */
|
|
7
|
+
export function Checkbox({ checked, indeterminate, onChange, label, size, disabled, tabIndex, style }) {
|
|
8
|
+
const flip = () => { if (!disabled && onChange) onChange(!checked); };
|
|
6
9
|
const box = "var(--check-size)", glyph = { width: "calc(var(--check-size) - 4px)", height: "calc(var(--check-size) - 4px)" };
|
|
7
10
|
const filled = checked || indeterminate;
|
|
8
11
|
return (
|
|
9
|
-
<label data-size={size} data-check={disabled ? undefined : ""}
|
|
10
|
-
|
|
12
|
+
<label data-size={size} data-check={disabled ? undefined : ""} role="checkbox" aria-checked={indeterminate && !checked ? "mixed" : !!checked} aria-disabled={disabled || undefined}
|
|
13
|
+
tabIndex={disabled ? -1 : tabIndex ?? 0} onKeyDown={(e) => { if (e.key === " ") { e.preventDefault(); e.stopPropagation(); flip(); } }}
|
|
14
|
+
style={{ outlineOffset: 2, display: "inline-flex", alignItems: "center", verticalAlign: "middle", lineHeight: "var(--control-leading)", gap: "var(--control-gap)", cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, ...style }}
|
|
15
|
+
onClick={flip}>
|
|
11
16
|
<span style={{ width: box, height: box, borderRadius: 4, boxSizing: "border-box", border: filled ? "none" : "1.5px solid var(--check-border, var(--checkbox-border))", background: filled ? "var(--action-primary)" : "var(--bg-surface)", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "var(--on-accent)", flexShrink: 0, transition: "var(--t-hover)" }}>
|
|
12
17
|
{checked ? <Icon name="check" size={12} strokeWidth={3} style={glyph} /> : indeterminate ? <Icon name="minus" size={12} strokeWidth={3} style={glyph} /> : null}
|
|
13
18
|
</span>
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import Icon from '../icons/Icon.vue'
|
|
4
4
|
// Checkbox — box at the region's tier (sm 14 · md/lg 16, tokens/layout.css --check-size), 1.5px border, r4; checked = blue fill + white check; indeterminate = blue fill + dash; disabled = 50% opacity. The label is the click target and uses the tier's control text.
|
|
5
|
-
|
|
5
|
+
// Keyboard: the label is role="checkbox" (aria-checked true / false / "mixed"), a Tab stop, Space toggles. Inside a control that owns focus
|
|
6
|
+
// (a Tree row, a MultiSelect option) pass :tabindex="-1" so the row stays the single stop.
|
|
7
|
+
const props = defineProps({ checked: Boolean, indeterminate: Boolean, label: String, size: String, disabled: Boolean, tabindex: { type: [Number, String], default: 0 }, style: Object })
|
|
6
8
|
const emit = defineEmits(['change'])
|
|
7
9
|
const box = 'var(--check-size)', glyph = { width: 'calc(var(--check-size) - 4px)', height: 'calc(var(--check-size) - 4px)' }
|
|
8
10
|
const filled = computed(() => props.checked || props.indeterminate)
|
|
11
|
+
function flip() { if (!props.disabled) emit('change', !props.checked) }
|
|
12
|
+
function onKey(e) { if (e.key === ' ') { e.preventDefault(); e.stopPropagation(); flip() } }
|
|
9
13
|
</script>
|
|
10
14
|
<template>
|
|
11
|
-
<label :data-size="size" :data-check="disabled ? undefined : ''" :style="{ display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--control-leading)', gap: 'var(--control-gap)', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, ...style }" @click="
|
|
15
|
+
<label :data-size="size" :data-check="disabled ? undefined : ''" role="checkbox" :aria-checked="indeterminate && !checked ? 'mixed' : !!checked" :aria-disabled="disabled || undefined" :tabindex="disabled ? -1 : tabindex" @keydown="onKey" :style="{ outlineOffset: '2px', display: 'inline-flex', alignItems: 'center', verticalAlign: 'middle', lineHeight: 'var(--control-leading)', gap: 'var(--control-gap)', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, ...style }" @click="flip">
|
|
12
16
|
<span :style="{ width: box, height: box, borderRadius: '4px', boxSizing: 'border-box', border: filled ? 'none' : '1.5px solid var(--check-border, var(--checkbox-border))', background: filled ? 'var(--action-primary)' : 'var(--bg-surface)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: 'var(--on-accent)', flexShrink: 0, transition: 'var(--t-hover)' }">
|
|
13
17
|
<Icon v-if="checked" name="check" :size="12" :stroke-width="3" :style="glyph" />
|
|
14
18
|
<Icon v-else-if="indeterminate" name="minus" :size="12" :stroke-width="3" :style="glyph" />
|
|
@@ -26,7 +26,9 @@ export function DatePicker({ value = "", placeholder = "Select date", disabled,
|
|
|
26
26
|
const monthTitle = fmt(locale, { year: "numeric", month: "long" }).format(new Date(view.y, view.m, 1));
|
|
27
27
|
return (
|
|
28
28
|
<div ref={root} data-size={size} style={{ position: "relative", width, ...style }}>
|
|
29
|
-
<div data-field={disabled ? undefined : ""}
|
|
29
|
+
<div data-field={disabled ? undefined : ""} role="combobox" aria-haspopup="dialog" aria-expanded={open} aria-disabled={disabled || undefined} tabIndex={disabled ? -1 : 0}
|
|
30
|
+
onKeyDown={(e) => { if (disabled) return; if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") { e.preventDefault(); setOpen(true); } else if (e.key === "Escape") setOpen(false); }}
|
|
31
|
+
onClick={() => !disabled && setOpen(!open)} style={{ ...inputBase, display: "flex", alignItems: "center", justifyContent: "space-between", cursor: disabled ? "not-allowed" : "pointer", borderColor: error ? "var(--danger)" : open ? "var(--focus-border)" : "var(--field-border, var(--border-control))", boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", opacity: disabled ? .5 : 1, color: label ? "var(--text-primary)" : "var(--text-muted)" }}>
|
|
30
32
|
{label || placeholder}<span style={{ color: "var(--text-muted)", display: "inline-flex" }}><Icon name="calendar" size={16} style={tierIcon} /></span>
|
|
31
33
|
</div>
|
|
32
34
|
{open && <div style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, zIndex: "var(--z-menu)", width: 252, background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-menu)", padding: 12, boxSizing: "border-box" }}>
|
|
@@ -26,10 +26,11 @@ function pick(i) { emit('update:modelValue', dayIso(i)); open.value = false }
|
|
|
26
26
|
function onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }
|
|
27
27
|
onMounted(() => document.addEventListener('mousedown', onDoc))
|
|
28
28
|
onUnmounted(() => document.removeEventListener('mousedown', onDoc))
|
|
29
|
+
function onFieldKey(e) { if (props.disabled) return; if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') { e.preventDefault(); open.value = true } else if (e.key === 'Escape') open.value = false }
|
|
29
30
|
</script>
|
|
30
31
|
<template>
|
|
31
32
|
<div ref="root" :data-size="size" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
|
|
32
|
-
<div :data-field="disabled ? undefined : ''" @click="!disabled && (open = !open)" :style="{ ...inputBase, display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: disabled ? 'not-allowed' : 'pointer', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1, color: label ? 'var(--text-primary)' : 'var(--text-muted)' }">
|
|
33
|
+
<div :data-field="disabled ? undefined : ''" role="combobox" aria-haspopup="dialog" :aria-expanded="open" :aria-disabled="disabled || undefined" :tabindex="disabled ? -1 : 0" @keydown="onFieldKey" @click="!disabled && (open = !open)" :style="{ ...inputBase, display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: disabled ? 'not-allowed' : 'pointer', borderColor: error ? 'var(--danger)' : open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))', boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', opacity: disabled ? .5 : 1, color: label ? 'var(--text-primary)' : 'var(--text-muted)' }">
|
|
33
34
|
{{ label || placeholder }}<span style="color:var(--text-muted);display:inline-flex"><Icon name="calendar" :size="16" :style="tierIcon" /></span>
|
|
34
35
|
</div>
|
|
35
36
|
<div v-if="open" style="position:absolute;top:calc(100% + 4px);left:0;z-index:var(--z-menu);width:252px;background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-menu);padding:12px;box-sizing:border-box">
|
|
@@ -22,7 +22,7 @@ export function Dropzone({ title = "Click to upload or drag and drop", hint = "J
|
|
|
22
22
|
const tone = disabled ? "default" : error ? "error" : dragActive ? "drag" : hover ? "hover" : "default";
|
|
23
23
|
const stop = (e) => { e.preventDefault(); e.stopPropagation(); };
|
|
24
24
|
return (
|
|
25
|
-
<div onClick={disabled ? undefined : onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
25
|
+
<div role="button" tabIndex={disabled ? -1 : 0} aria-disabled={disabled || undefined} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (!disabled && onClick) onClick(e); } }} onClick={disabled ? undefined : onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
26
26
|
onDragEnter={disabled ? undefined : (e) => { stop(e); setDragging(true); }}
|
|
27
27
|
onDragOver={disabled ? undefined : stop}
|
|
28
28
|
onDragLeave={disabled ? undefined : (e) => { stop(e); setDragging(false); }}
|
|
@@ -17,7 +17,7 @@ function onDrop(e) { stop(e); dragging.value = false; emit('dropFiles', e.dataTr
|
|
|
17
17
|
<span style="font-family:var(--font-sans);font-weight:var(--weight-medium);font-size:var(--text-base);color:var(--text-heading)">{{ file }}</span>
|
|
18
18
|
<button type="button" data-link="" @click="$emit('remove')" style="display:flex;align-items:center;gap:4px;border:none;background:transparent;cursor:pointer;color:var(--danger);font-family:var(--font-sans);font-size:var(--text-base);padding:0;text-decoration-line:var(--link-line, none);text-underline-offset:2px"><Icon name="trash-2" :size="13" />Remove</button>
|
|
19
19
|
</div>
|
|
20
|
-
<div v-else @click="!disabled && $emit('click')" @mouseenter="hover = true" @mouseleave="hover = false"
|
|
20
|
+
<div v-else role="button" :tabindex="disabled ? -1 : 0" :aria-disabled="disabled || undefined" @keydown.enter.prevent="!disabled && $emit('click')" @keydown.space.prevent="!disabled && $emit('click')" @click="!disabled && $emit('click')" @mouseenter="hover = true" @mouseleave="hover = false"
|
|
21
21
|
@dragenter="!disabled && ((e) => { stop(e); dragging = true })($event)" @dragover="!disabled && stop($event)"
|
|
22
22
|
@dragleave="!disabled && ((e) => { stop(e); dragging = false })($event)" @drop="!disabled && onDrop($event)"
|
|
23
23
|
:style="{ height, borderRadius: 'var(--radius-lg)', border: `1px dashed ${toneColor[tone]}`, background: error ? 'var(--danger-bg)' : dragActive ? 'var(--soft-bg)' : 'transparent', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', boxSizing: 'border-box', opacity: disabled ? .5 : 1, transition: 'var(--t-hover)', ...style }">
|
|
@@ -14,7 +14,9 @@ export function MultiSelect({ options = [], value = [], placeholder = "Select...
|
|
|
14
14
|
const shown = value.slice(0, maxTags), extra = value.length - shown.length;
|
|
15
15
|
return (
|
|
16
16
|
<div ref={root} data-size={size} style={{ position: "relative", width, ...style }}>
|
|
17
|
-
<div data-field={disabled ? undefined : ""}
|
|
17
|
+
<div data-field={disabled ? undefined : ""} role="combobox" aria-haspopup="listbox" aria-expanded={open} aria-disabled={disabled || undefined} tabIndex={disabled ? -1 : 0}
|
|
18
|
+
onKeyDown={(e) => { if (disabled) return; if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") { e.preventDefault(); setOpen(true); } else if (e.key === "Escape") setOpen(false); }}
|
|
19
|
+
onClick={() => !disabled && setOpen(!open)} style={{ minHeight: "var(--control-h)", borderRadius: "var(--radius-md)", border: `1px solid ${open ? "var(--focus-border)" : "var(--field-border, var(--border-control))"}`, boxShadow: open ? "0 0 0 2px var(--focus-ring)" : "none", background: "var(--bg-surface)", padding: "calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)", boxSizing: "border-box", display: "flex", alignItems: "center", flexWrap: "wrap", gap: 4, cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? .5 : 1, position: "relative" }}>
|
|
18
20
|
{value.length === 0 && <span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-muted)", paddingLeft: 4 }}>{placeholder}</span>}
|
|
19
21
|
{shown.map(v => <Tag key={v} removable onRemove={() => toggle(v)}>{v}</Tag>)}
|
|
20
22
|
{extra > 0 && <Tag>+{extra}</Tag>}
|
|
@@ -24,9 +26,9 @@ export function MultiSelect({ options = [], value = [], placeholder = "Select...
|
|
|
24
26
|
</span>
|
|
25
27
|
</div>
|
|
26
28
|
{open && <div style={{ position: "absolute", top: "calc(100% + 4px)", left: 0, right: 0, zIndex: "var(--z-menu)", background: "var(--bg-surface)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-md)", boxShadow: "var(--shadow-menu)", padding: 4, maxHeight: 208, overflowY: "auto" }}>
|
|
27
|
-
{options.map(o => <div key={o} role="option" aria-selected={value.includes(o)} tabIndex={0} onClick={() => toggle(o)} style={{ display: "flex", alignItems: "center", gap: 8, height: "var(--control-h)", padding: "0 10px", boxSizing: "border-box", borderRadius: "var(--radius-sm)", cursor: "pointer", background: "transparent" }}
|
|
29
|
+
{options.map(o => <div key={o} role="option" aria-selected={value.includes(o)} tabIndex={0} onClick={() => toggle(o)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(o); } else if (e.key === "Escape") setOpen(false); }} style={{ display: "flex", alignItems: "center", gap: 8, height: "var(--control-h)", padding: "0 10px", boxSizing: "border-box", borderRadius: "var(--radius-sm)", cursor: "pointer", background: "transparent" }}
|
|
28
30
|
onMouseEnter={(e) => e.currentTarget.style.background = "var(--bg-hover)"} onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}>
|
|
29
|
-
<Checkbox checked={value.includes(o)}
|
|
31
|
+
<Checkbox checked={value.includes(o)} tabIndex={-1} />
|
|
30
32
|
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--control-text)", letterSpacing: "var(--control-tracking)", color: "var(--text-heading)" }}>{o}</span>
|
|
31
33
|
</div>)}
|
|
32
34
|
</div>}
|
|
@@ -14,10 +14,11 @@ const extra = computed(() => props.modelValue.length - shown.value.length)
|
|
|
14
14
|
function onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }
|
|
15
15
|
onMounted(() => document.addEventListener('mousedown', onDoc))
|
|
16
16
|
onUnmounted(() => document.removeEventListener('mousedown', onDoc))
|
|
17
|
+
function onFieldKey(e) { if (props.disabled) return; if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') { e.preventDefault(); open.value = true } else if (e.key === 'Escape') open.value = false }
|
|
17
18
|
</script>
|
|
18
19
|
<template>
|
|
19
20
|
<div ref="root" :data-size="size" :style="{ position: 'relative', width: typeof width === 'number' ? width + 'px' : width, ...style }">
|
|
20
|
-
<div :data-field="disabled ? undefined : ''" @click="!disabled && (open = !open)" :style="{ minHeight: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: `1px solid ${open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))'}`, boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', background: 'var(--bg-surface)', padding: 'calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)', boxSizing: 'border-box', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, position: 'relative' }">
|
|
21
|
+
<div :data-field="disabled ? undefined : ''" role="combobox" aria-haspopup="listbox" :aria-expanded="open" :aria-disabled="disabled || undefined" :tabindex="disabled ? -1 : 0" @keydown="onFieldKey" @click="!disabled && (open = !open)" :style="{ minHeight: 'var(--control-h)', borderRadius: 'var(--radius-md)', border: `1px solid ${open ? 'var(--focus-border)' : 'var(--field-border, var(--border-control))'}`, boxShadow: open ? '0 0 0 2px var(--focus-ring)' : 'none', background: 'var(--bg-surface)', padding: 'calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) + var(--control-icon) * 2 + 8px) calc((var(--control-h) - var(--tag-h) - 2px) / 2) calc(var(--control-px) - 4px)', boxSizing: 'border-box', display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '4px', cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? .5 : 1, position: 'relative' }">
|
|
21
22
|
<span v-if="modelValue.length === 0" style="font-family:var(--font-sans);font-size:var(--control-text);line-height:var(--control-leading);letter-spacing:var(--control-tracking);color:var(--text-muted);padding-left:4px">{{ placeholder }}</span>
|
|
22
23
|
<Tag v-for="v in shown" :key="v" removable @remove="toggle(v)">{{ v }}</Tag>
|
|
23
24
|
<Tag v-if="extra > 0">+{{ extra }}</Tag>
|
|
@@ -27,8 +28,8 @@ onUnmounted(() => document.removeEventListener('mousedown', onDoc))
|
|
|
27
28
|
</span>
|
|
28
29
|
</div>
|
|
29
30
|
<div v-if="open" style="position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:var(--z-menu);background:var(--bg-surface);border:1px solid var(--border-card);border-radius:var(--radius-md);box-shadow:var(--shadow-menu);padding:4px;max-height:208px;overflow-y:auto">
|
|
30
|
-
<div v-for="o in options" :key="o" role="option" :aria-selected="modelValue.includes(o)" tabindex="0" @click="toggle(o)" class="kb-ms-row" style="display:flex;align-items:center;gap:8px;height:var(--control-h);padding:0 10px;box-sizing:border-box;border-radius:var(--radius-sm);cursor:pointer">
|
|
31
|
-
<Checkbox :checked="modelValue.includes(o)"
|
|
31
|
+
<div v-for="o in options" :key="o" role="option" :aria-selected="modelValue.includes(o)" tabindex="0" @click="toggle(o)" @keydown.enter.prevent="toggle(o)" @keydown.space.prevent="toggle(o)" @keydown.esc="open = false" class="kb-ms-row" style="display:flex;align-items:center;gap:8px;height:var(--control-h);padding:0 10px;box-sizing:border-box;border-radius:var(--radius-sm);cursor:pointer">
|
|
32
|
+
<Checkbox :checked="modelValue.includes(o)" :tabindex="-1" />
|
|
32
33
|
<span style="font-family:var(--font-sans);font-size:var(--control-text);letter-spacing:var(--control-tracking);color:var(--text-heading)">{{ o }}</span>
|
|
33
34
|
</div>
|
|
34
35
|
</div>
|
|
@@ -12,7 +12,7 @@ export function Slider({ value = 50, min = 0, max = 100, step = 1, labels, showV
|
|
|
12
12
|
<div ref={track} data-thumb={disabled ? undefined : ""} onMouseDown={disabled ? undefined : (e) => { setDrag(true); set(e.clientX); }} style={{ position: "relative", height: 20, display: "flex", alignItems: "center", cursor: disabled ? "not-allowed" : "pointer" }}>
|
|
13
13
|
<div style={{ position: "absolute", left: 0, right: 0, height: 6, borderRadius: "var(--radius-full)", background: "var(--track)" }}></div>
|
|
14
14
|
<div style={{ position: "absolute", left: 0, width: pct + "%", height: 6, borderRadius: "var(--radius-full)", background: "var(--action-primary)" }}></div>
|
|
15
|
-
<span style={{ position: "absolute", left: pct + "%", transform: "translateX(-50%)", width: 16, height: 16, borderRadius: "var(--radius-full)", background: "var(--bg-surface)", border: "2px solid var(--action-primary)", boxSizing: "border-box", boxShadow: drag ? "0 0 0 4px var(--focus-ring),
|
|
15
|
+
<span style={{ position: "absolute", left: pct + "%", transform: "translateX(-50%)", width: 16, height: 16, borderRadius: "var(--radius-full)", background: "var(--bg-surface)", border: "2px solid var(--action-primary)", boxSizing: "border-box", boxShadow: drag ? "0 0 0 4px var(--focus-ring), var(--shadow-thumb)" : "var(--thumb-ring, var(--shadow-thumb))", transition: "box-shadow var(--dur-fast) var(--ease-out)" }}></span>
|
|
16
16
|
{(drag || showValue) && <span style={{ position: "absolute", left: pct + "%", transform: "translateX(-50%)", bottom: 22, background: "var(--tooltip-bg)", color: "var(--tooltip-fg)", borderRadius: "var(--radius-sm)", padding: "2px 6px", fontFamily: "var(--font-sans)", fontVariantNumeric: "tabular-nums", fontSize: "var(--text-xs)", whiteSpace: "nowrap", pointerEvents: "none" }}>{value}{unit}</span>}
|
|
17
17
|
</div>
|
|
18
18
|
{labels && <div style={{ display: "flex", justifyContent: "space-between", paddingTop: 4, fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", color: "var(--text-muted)" }}><span>{min}{unit}</span><span>{max}{unit}</span></div>}
|
|
@@ -15,7 +15,7 @@ onUnmounted(up)
|
|
|
15
15
|
<div ref="track" :data-thumb="disabled ? undefined : ''" @mousedown="!disabled && (drag = true, set($event.clientX))" :style="{ position: 'relative', height: '20px', display: 'flex', alignItems: 'center', cursor: disabled ? 'not-allowed' : 'pointer' }">
|
|
16
16
|
<div style="position:absolute;left:0;right:0;height:6px;border-radius:var(--radius-full);background:var(--track)" />
|
|
17
17
|
<div :style="{ position: 'absolute', left: 0, width: pct + '%', height: '6px', borderRadius: 'var(--radius-full)', background: 'var(--action-primary)' }" />
|
|
18
|
-
<span :style="{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', width: '16px', height: '16px', borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', border: '2px solid var(--action-primary)', boxSizing: 'border-box', boxShadow: drag ? '0 0 0 4px var(--focus-ring),
|
|
18
|
+
<span :style="{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', width: '16px', height: '16px', borderRadius: 'var(--radius-full)', background: 'var(--bg-surface)', border: '2px solid var(--action-primary)', boxSizing: 'border-box', boxShadow: drag ? '0 0 0 4px var(--focus-ring), var(--shadow-thumb)' : 'var(--thumb-ring, var(--shadow-thumb))', transition: 'box-shadow var(--dur-fast) var(--ease-out)' }" />
|
|
19
19
|
<span v-if="drag || showValue" :style="{ position: 'absolute', left: pct + '%', transform: 'translateX(-50%)', bottom: '22px', background: 'var(--tooltip-bg)', color: 'var(--tooltip-fg)', borderRadius: 'var(--radius-sm)', padding: '2px 6px', fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums', fontSize: 'var(--text-xs)', whiteSpace: 'nowrap', pointerEvents: 'none' }">{{ modelValue }}{{ unit }}</span>
|
|
20
20
|
</div>
|
|
21
21
|
<div v-if="labels" style="display:flex;justify-content:space-between;padding-top:4px;font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-muted)"><span>{{ min }}{{ unit }}</span><span>{{ max }}{{ unit }}</span></div>
|