@keyfunc/keybricks-ds 2.5.0 → 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 +3 -3
- package/INTEGRATION.md +12 -1
- 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 +7 -0
- 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/Table.jsx +1 -1
- package/components/data/Table.vue +1 -1
- 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/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 +8 -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 +1 -1
- package/package.json +4 -2
- package/tokens/dark.css +1 -1
- package/tokens/interaction.css +4 -0
- package/tokens/layout.css +3 -0
package/changelog.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
Each item: tag (new | improved | fixed | breaking | removed) and the text as [en, zh, ja].
|
|
4
4
|
INTEGRATION.md keeps the full migration notes; this list is the short, reader-facing summary. */
|
|
5
5
|
window.KB_CHANGELOG = [
|
|
6
|
+
{ v: "2.5.1", date: "2026-10-30", items: [
|
|
7
|
+
{ tag: "fixed", t: ["Keyboard support: Checkbox, DatePicker, MultiSelect, Accordion, Dropzone, pipeline nodes and palette, selectable bar lists, and the Sunburst / World map charts can all be used without a mouse.", "键盘支持:复选框、日期选择、多选、折叠面板、上传区、管道节点与节点面板、可点选的条形列表,以及旭日图 / 世界地图,都可以不用鼠标操作。", "キーボード対応:チェックボックス、日付選択、マルチセレクト、アコーディオン、ドロップゾーン、パイプラインのノードとパレット、選択可能なバーリスト、サンバースト / 世界地図をマウスなしで操作可能に。"] },
|
|
8
|
+
{ tag: "fixed", t: ["Long text no longer spills: field values, menu items, segments, tree rows and navigation names truncate; drawer titles wrap.", "长文字不再溢出:输入框的值、菜单项、分段选择、树形列表和导航名称会截断;侧滑面板标题会换行。", "長いテキストがはみ出さないように:入力値、メニュー項目、セグメント、ツリー行、ナビゲーション名は省略表示、ドロワーのタイトルは折り返し。"] },
|
|
9
|
+
{ tag: "fixed", t: ["Vue: Tree right-click menu event and BarList row selection, matching React.", "Vue:树形列表支持右键菜单事件,条形列表支持点选,与 React 一致。", "Vue:Tree の右クリックメニューイベントと BarList の行選択を React と同様に追加。"] },
|
|
10
|
+
{ tag: "fixed", t: ["The documentation pages inside the npm package now open locally — before, every component box stayed empty.", "npm 包里的文档网页现在可以在本地直接打开(之前每个组件框都是空白的)。", "npm パッケージ内のドキュメントページをローカルで開けるように(以前は各コンポーネント枠が空白だった)。"] },
|
|
11
|
+
{ tag: "improved", t: ["Animations and shadows use the system tokens throughout; shadows deepen in dark mode.", "动画和阴影统一使用系统规范值,深色模式下阴影自动加深。", "アニメーションと影をシステムのトークンに統一。ダークモードでは影が自動で濃くなる。"] },
|
|
12
|
+
] },
|
|
6
13
|
{ v: "2.5.0", date: "2026-10-30", items: [
|
|
7
14
|
{ tag: "new", t: ["Every example on this site has a <b>Show code</b> bar: the React and Vue 3 code that builds it, ready to paste.", "网站上每个例子下都有<b>显示代码</b>:给出做出该例子的 React 和 Vue 3 代码,可直接复制使用。", "サイトの各例の下に<b>コードを表示</b>を追加:その例を作る React / Vue 3 のコードをそのまま貼り付けて使える。"] },
|
|
8
15
|
{ tag: "improved", t: ["Vue imports in one line: <code>import { Button } from '@keyfunc/keybricks-ds/vue'</code>.", "Vue 一行引入:<code>import { Button } from '@keyfunc/keybricks-ds/vue'</code>。", "Vue は 1 行でインポート:<code>import { Button } from '@keyfunc/keybricks-ds/vue'</code>。"] },
|
|
@@ -13,7 +13,7 @@ export function BarList({ rows = [], max, width = 360, valueFormat = fmtNum, sho
|
|
|
13
13
|
<div style={{ display: "flex", flexDirection: "column", gap: 10, width, ...style }}>
|
|
14
14
|
{rows.map((r) => (
|
|
15
15
|
<div key={r.label} onMouseEnter={interactive ? () => setHover(r.label) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
16
|
-
onClick={onSelect ? () => onSelect(r) : undefined}
|
|
16
|
+
onClick={onSelect ? () => onSelect(r) : undefined} role={onSelect ? "button" : undefined} tabIndex={onSelect ? 0 : undefined} onKeyDown={onSelect ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSelect(r); } } : undefined}
|
|
17
17
|
style={{ opacity: hover == null || hover === r.label ? 1 : 0.5, cursor: onSelect ? "pointer" : "default", transition: "var(--t-hover)" }}>
|
|
18
18
|
<div style={{ display: "flex", alignItems: "baseline", gap: 8, paddingBottom: 4 }}>
|
|
19
19
|
<span style={{ fontFamily: "var(--font-sans)", fontSize: "var(--text-xs)", color: "var(--text-body)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{r.label}</span>
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, ref } from 'vue'
|
|
2
|
+
import { computed, ref, getCurrentInstance } from 'vue'
|
|
3
3
|
import { fmtNum } from './chart-geometry.js'
|
|
4
4
|
// Horizontal labeled bars — top-N rankings, budget consumption.
|
|
5
5
|
const props = defineProps({ rows:{type:Array,default:()=>[]}, max:Number, width:{type:Number,default:360}, barHeight:{type:Number,default:8}, showTrack:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })
|
|
6
6
|
const top = computed(() => props.max!=null ? props.max : Math.max(1,...props.rows.map(r=>r.value)))
|
|
7
7
|
const total = computed(() => props.rows.reduce((s,r)=>s+r.value,0)||1)
|
|
8
8
|
const hover = ref(null)
|
|
9
|
+
// @select(row) — like React's onSelect: a listener makes each row a button (click, Enter / Space).
|
|
10
|
+
const emit = defineEmits(['select'])
|
|
11
|
+
const inst = getCurrentInstance()
|
|
12
|
+
const selectable = computed(() => !!(inst && inst.vnode.props && inst.vnode.props.onSelect))
|
|
13
|
+
function pick(r) { if (selectable.value) emit('select', r) }
|
|
9
14
|
</script>
|
|
10
15
|
<template>
|
|
11
16
|
<div :style="{display:'flex',flexDirection:'column',gap:'10px',width:width+'px',...style}">
|
|
12
|
-
<div v-for="r in rows" :key="r.label" @mouseenter="interactive && (hover=r.label)" @mouseleave="interactive && (hover=null)"
|
|
13
|
-
:style="{opacity:hover==null||hover===r.label?1:0.5,transition:'var(--t-hover)'}">
|
|
17
|
+
<div v-for="r in rows" :key="r.label" @mouseenter="interactive && (hover=r.label)" @mouseleave="interactive && (hover=null)" :role="selectable ? 'button' : undefined" :tabindex="selectable ? 0 : undefined" @click="pick(r)" @keydown.enter.prevent="pick(r)" @keydown.space.prevent="pick(r)"
|
|
18
|
+
:style="{opacity:hover==null||hover===r.label?1:0.5,transition:'var(--t-hover)',cursor:selectable?'pointer':'default'}">
|
|
14
19
|
<div style="display:flex;align-items:baseline;gap:8px;padding-bottom:4px">
|
|
15
20
|
<span style="font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-body);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ r.label }}</span>
|
|
16
21
|
<span style="margin-left:auto;font-family:var(--font-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);color:var(--text-heading)">{{ valueFormat(r.value) }}</span>
|
|
@@ -8,6 +8,8 @@ export interface Ribbon3DProps {
|
|
|
8
8
|
yaw?: number; pitch?: number;
|
|
9
9
|
/** Band thickness as a fraction of its row, 0–1 */ ribbon?: number;
|
|
10
10
|
interactive?: boolean;
|
|
11
|
+
/** Formats values in the tooltip and axis (default: grouped number). */
|
|
12
|
+
valueFormat?: (v: number) => string;
|
|
11
13
|
style?: React.CSSProperties;
|
|
12
14
|
}
|
|
13
15
|
export declare function Ribbon3D(props: Ribbon3DProps): JSX.Element;
|
|
@@ -7,6 +7,10 @@ export interface Scatter3DProps {
|
|
|
7
7
|
yaw?: number; pitch?: number;
|
|
8
8
|
/** Base dot radius */ r?: number;
|
|
9
9
|
legend?: boolean; interactive?: boolean;
|
|
10
|
+
/** Axis titles (default "x" / "y" / "z"). */
|
|
11
|
+
xLabel?: string; yLabel?: string; zLabel?: string;
|
|
12
|
+
/** Formats values in the tooltip (default: grouped number). */
|
|
13
|
+
valueFormat?: (v: number) => string;
|
|
10
14
|
style?: React.CSSProperties;
|
|
11
15
|
}
|
|
12
16
|
export declare function Scatter3D(props: Scatter3DProps): JSX.Element;
|
|
@@ -33,7 +33,12 @@ export function SunburstChart({ data, size = 320, ringWidth, valueFormat = fmtNu
|
|
|
33
33
|
return (
|
|
34
34
|
<div style={{ ...wrapStyle, ...style }}>
|
|
35
35
|
<div style={{ position: "relative", width: size, height: size }}>
|
|
36
|
-
<svg width={size} height={size} role="img"
|
|
36
|
+
<svg width={size} height={size} role="img" aria-label={interactive ? "Sunburst chart — arrow keys move between segments, Enter zooms in, Escape zooms out" : undefined} tabIndex={interactive ? 0 : undefined} onBlur={() => setHover(null)}
|
|
37
|
+
onKeyDown={interactive ? (e) => { const n = segs.length; if (!n) return; const h = hover;
|
|
38
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") { e.preventDefault(); setHover(h == null ? 0 : (h + 1) % n); }
|
|
39
|
+
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") { e.preventDefault(); setHover(h == null ? n - 1 : (h - 1 + n) % n); }
|
|
40
|
+
else if ((e.key === "Enter" || e.key === " ") && h != null && segs[h] && segs[h].node.children) { e.preventDefault(); setZoom([...zoom, ...segs[h].path]); setHover(null); }
|
|
41
|
+
else if (e.key === "Escape") { if (zoom.length) setZoom(zoom.slice(0, -1)); setHover(null); } } : undefined}>
|
|
37
42
|
{segs.map((s, i) => (
|
|
38
43
|
<path key={i} d={arcPath(size / 2, size / 2, rInner + rw * (s.depth + 1) - 1.5, rInner + rw * s.depth, s.a0 + 0.35, Math.max(s.a0 + 0.5, s.a1 - 0.35))}
|
|
39
44
|
fill={seriesColor(s.colorIdx)} opacity={hover == null ? 0.92 - s.depth * 0.16 : hover === i ? 1 : 0.28}
|
|
@@ -33,11 +33,17 @@ const tip = computed(()=>{
|
|
|
33
33
|
const p = polar(props.size/2, props.size/2, g.value.rInner+g.value.rw*(s.depth+0.5), (s.a0+s.a1)/2)
|
|
34
34
|
return { x:p[0], y:p[1], title:s.node.label, value: props.valueFormat(s.node.value)+' · '+((s.node.value/(g.value.focus.value||1))*100).toFixed(1)+'%' }
|
|
35
35
|
})
|
|
36
|
+
// One Tab stop for the whole chart: arrows walk the segments (same highlight + tooltip as hover), Enter zooms in, Escape zooms out.
|
|
37
|
+
function onKey(e) { if (!props.interactive) return; const segs = g.value.segs, n = segs.length; if (!n) return; const h = hover.value
|
|
38
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); hover.value = h == null ? 0 : (h + 1) % n }
|
|
39
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); hover.value = h == null ? n - 1 : (h - 1 + n) % n }
|
|
40
|
+
else if ((e.key === 'Enter' || e.key === ' ') && h != null && segs[h] && segs[h].node.children) { e.preventDefault(); zoom.value = [...zoom.value, ...segs[h].path]; hover.value = null }
|
|
41
|
+
else if (e.key === 'Escape') { if (zoom.value.length) zoom.value = zoom.value.slice(0, -1); hover.value = null } }
|
|
36
42
|
</script>
|
|
37
43
|
<template>
|
|
38
44
|
<div :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
39
45
|
<div :style="{position:'relative',width:size+'px',height:size+'px'}">
|
|
40
|
-
<svg :width="size" :height="size" role="img">
|
|
46
|
+
<svg :width="size" :height="size" role="img" :aria-label="interactive ? 'Sunburst chart — arrow keys move between segments, Enter zooms in, Escape zooms out' : undefined" :tabindex="interactive ? 0 : undefined" @keydown="onKey" @blur="hover = null">
|
|
41
47
|
<path v-for="(s,i) in g.segs" :key="i" :d="s.d" :fill="s.color"
|
|
42
48
|
:opacity="hover==null ? 0.92-s.depth*0.16 : (hover===i?1:0.28)"
|
|
43
49
|
@mouseenter="interactive && (hover=i)" @mouseleave="interactive && (hover=null)"
|
|
@@ -8,6 +8,8 @@ export interface Surface3DProps {
|
|
|
8
8
|
/** Cell outlines */ wire?: boolean;
|
|
9
9
|
/** Drag-to-orbit */ interactive?: boolean;
|
|
10
10
|
valueFormat?: (v: number) => string;
|
|
11
|
+
/** Axis titles (default "x" / "y"). */
|
|
12
|
+
xLabel?: string; yLabel?: string;
|
|
11
13
|
style?: React.CSSProperties;
|
|
12
14
|
}
|
|
13
15
|
export declare function Surface3D(props: Surface3DProps): JSX.Element;
|
|
@@ -80,7 +80,12 @@ export function WorldMap({ values = {}, width = 660, height = 340, projection =
|
|
|
80
80
|
const h = hover != null ? paths[hover] : null;
|
|
81
81
|
return (
|
|
82
82
|
<div style={{ ...wrapStyle, ...style }}>
|
|
83
|
-
<svg width={width} height={height - (legend ? 22 : 0)} role="img"
|
|
83
|
+
<svg width={width} height={height - (legend ? 22 : 0)} role="img" aria-label={interactive ? "Map — arrow keys move between regions" + (onSelect ? ", Enter selects" : "") : undefined} tabIndex={interactive ? 0 : undefined} onBlur={() => setHover(null)}
|
|
84
|
+
onKeyDown={interactive ? (e) => { const n = paths.length; if (!n) return; const h = hover;
|
|
85
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") { e.preventDefault(); setHover(h == null ? 0 : (h + 1) % n); }
|
|
86
|
+
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") { e.preventDefault(); setHover(h == null ? n - 1 : (h - 1 + n) % n); }
|
|
87
|
+
else if ((e.key === "Enter" || e.key === " ") && h != null && onSelect) { e.preventDefault(); onSelect(paths[h].name, resolved[paths[h].name]); }
|
|
88
|
+
else if (e.key === "Escape") setHover(null); } : undefined}>
|
|
84
89
|
{paths.map((p, i) => (
|
|
85
90
|
<path key={p.name + i} d={p.d} fill={fillOf(p.name)} stroke="var(--bg-surface)" strokeWidth="0.5"
|
|
86
91
|
onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}
|
|
@@ -49,12 +49,18 @@ function fill(name){
|
|
|
49
49
|
const swatches = computed(() => props.scale==='diverging'
|
|
50
50
|
? ['var(--chart-neg)','color-mix(in srgb, var(--chart-neg) 40%, var(--bg-surface))','var(--chart-track)','color-mix(in srgb, var(--chart-pos) 40%, var(--bg-surface))','var(--chart-pos)']
|
|
51
51
|
: HEAT)
|
|
52
|
+
// One Tab stop for the whole map: arrows walk the regions (same highlight + tooltip as hover), Enter emits select, Escape clears.
|
|
53
|
+
function onKey(e) { if (!props.interactive) return; const ps = paths.value, n = ps.length; if (!n) return; const h = hover.value
|
|
54
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { e.preventDefault(); hover.value = h == null ? 0 : (h + 1) % n }
|
|
55
|
+
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); hover.value = h == null ? n - 1 : (h - 1 + n) % n }
|
|
56
|
+
else if ((e.key === 'Enter' || e.key === ' ') && h != null) { e.preventDefault(); emit('select', ps[h].name, resolved.value[ps[h].name]) }
|
|
57
|
+
else if (e.key === 'Escape') hover.value = null }
|
|
52
58
|
</script>
|
|
53
59
|
<template>
|
|
54
60
|
<div v-if="err" :style="{width:width+'px',height:height+'px',display:'flex',alignItems:'center',justifyContent:'center',fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums',fontSize:'var(--text-xs)',color:'var(--text-muted)',background:'var(--bg-inset)',borderRadius:'var(--radius-lg)',...style}">{{ err }}</div>
|
|
55
61
|
<div v-else-if="!paths" :style="{width:width+'px',height:height+'px',display:'flex',alignItems:'center',justifyContent:'center',fontFamily: 'var(--font-sans)', fontVariantNumeric: 'tabular-nums',fontSize:'var(--text-xs)',color:'var(--text-muted)',...style}">loading map…</div>
|
|
56
62
|
<div v-else :style="{position:'relative',display:'inline-block',lineHeight:0,...style}">
|
|
57
|
-
<svg :width="width" :height="plotH" role="img">
|
|
63
|
+
<svg :width="width" :height="plotH" role="img" :aria-label="interactive ? 'Map — arrow keys move between regions, Enter selects' : undefined" :tabindex="interactive ? 0 : undefined" @keydown="onKey" @blur="hover = null">
|
|
58
64
|
<path v-for="(p,i) in paths" :key="p.name+i" :d="p.d" :fill="fill(p.name)" stroke="var(--bg-surface)" stroke-width="0.5"
|
|
59
65
|
@mouseenter="interactive && (hover=i)" @mouseleave="interactive && (hover=null)" @click="emit('select', p.name, resolved[p.name])"
|
|
60
66
|
:style="{opacity:hover==null||hover===i?1:0.75,transition:'opacity var(--dur-fast) var(--ease-out)'}"/>
|
|
@@ -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
|
}
|
|
@@ -188,7 +188,7 @@ function cellPad(i) { return { ...(i === 0 && props.selectable ? { position: 're
|
|
|
188
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>
|
|
189
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)' }">
|
|
190
190
|
<template v-for="{ r, ri } in [item]" :key="'r'">
|
|
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
|
|
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>
|
|
192
192
|
</template>
|
|
193
193
|
</tr>
|
|
194
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')" />
|
|
@@ -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>
|