@keyfunc/keybricks-ds 2.4.12 → 2.5.0
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 +23 -6
- package/_adherence.oxlintrc.json +12 -4
- package/changelog.js +14 -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 +28 -5
- 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/data/ListRow.d.ts +6 -3
- package/components/data/ListRow.jsx +21 -13
- package/components/data/ListRow.vue +25 -15
- package/components/data/Table.vue +6 -2
- package/components/feedback/ContextMenu.d.ts +3 -1
- package/components/feedback/ContextMenu.jsx +46 -8
- package/components/feedback/ContextMenu.vue +47 -7
- package/components/feedback/Menu.d.ts +5 -1
- package/components/feedback/Menu.jsx +6 -4
- package/components/feedback/Menu.vue +5 -3
- package/components/feedback/Timeline.vue +4 -1
- package/components/navigation/Tree.vue +6 -0
- package/index.html +6 -56
- package/package.json +1 -1
- 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>
|
|
@@ -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 .15s ease' }" /></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)"
|
|
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 .15s ease' }" /></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>
|
|
@@ -2,8 +2,10 @@ import * as React from "react";
|
|
|
2
2
|
/** Floating context menu — r10, soft shadow, control-height rows at the region's tier (32px / 14px at md), red danger row.
|
|
3
3
|
* Action menus keep rows flush; a single-choice menu (any item `selected`) spaces rows 2px apart. Dividers keep 4px above and below. */
|
|
4
4
|
export interface ContextMenuProps {
|
|
5
|
-
items?: { label?: string; icon?: string; danger?: boolean; divider?: boolean; /** Current choice of a single-choice menu: --bg-hover fill, --weight-medium --text-heading, no checkmark. Any selected item spaces the rows 2px apart. */ selected?: boolean }[];
|
|
5
|
+
items?: { label?: string; icon?: string; /** Colour for this item's icon — any CSS colour, e.g. "var(--cat-join)". Omit for a single-colour icon in the row's text colour. */ iconColor?: string; /** Shown in a 300px panel beside the menu while the item is hovered or focused: the item's icon and label, then this text. */ description?: string; danger?: boolean; divider?: boolean; /** Current choice of a single-choice menu: --bg-hover fill, --weight-medium --text-heading, no checkmark. Any selected item spaces the rows 2px apart. */ selected?: boolean }[];
|
|
6
6
|
onSelect?: (label: string) => void;
|
|
7
|
+
/** Side the description panel opens on (default "right"); it flips to the other side when the viewport has no room. */
|
|
8
|
+
descriptionSide?: "left" | "right";
|
|
7
9
|
/** Size tier of the rows. Omit to inherit the tier of the control that opened the menu (the default). */
|
|
8
10
|
size?: "sm" | "md" | "lg";
|
|
9
11
|
style?: React.CSSProperties;
|
|
@@ -1,22 +1,60 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Icon } from "../icons/Icon.jsx";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const HINT_W = 300, HINT_GAP = 8;
|
|
5
|
+
|
|
6
|
+
/** Right-click context menu (Preview / Duplicate / Delete). items: [{label, icon, iconColor, description, danger, divider, selected}]. Rows are control-height at the region's tier (a menu inherits the tier of the control that opened it); `size` overrides.
|
|
5
7
|
* selected: true marks the current choice of a single-choice menu (section switcher, language picker): --bg-hover fill, --weight-medium
|
|
6
8
|
* --text-heading, no checkmark. A menu with any selected item spaces its rows 2px apart (like Sidebar / Tree / DocTree) so a hovered
|
|
7
|
-
* fill never touches the selected one; an action menu (nothing selected) keeps its rows flush. Dividers keep 4px above and below.
|
|
8
|
-
|
|
9
|
+
* fill never touches the selected one; an action menu (nothing selected) keeps its rows flush. Dividers keep 4px above and below.
|
|
10
|
+
* iconColor: any CSS colour for that item's icon (e.g. "var(--cat-join)"); without it the icon takes the row's text colour.
|
|
11
|
+
* description: hovering or focusing the item shows a 300px panel beside the menu — the item's icon and label, then the description.
|
|
12
|
+
* It opens on descriptionSide ("right" by default) and flips to the other side when the viewport has no room. */
|
|
13
|
+
export function ContextMenu({ items = [], onSelect, size, descriptionSide = "right", style }) {
|
|
9
14
|
const [hover, setHover] = React.useState(-1);
|
|
15
|
+
const [hint, setHint] = React.useState(-1);
|
|
16
|
+
const [side, setSide] = React.useState(descriptionSide);
|
|
17
|
+
const wrap = React.useRef(null);
|
|
18
|
+
const hintId = React.useId();
|
|
10
19
|
const choice = items.some((it) => it && it.selected);
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
const hasHint = items.some((it) => it && it.description);
|
|
21
|
+
const show = (i) => {
|
|
22
|
+
setHover(i);
|
|
23
|
+
if (!hasHint) return;
|
|
24
|
+
const it = items[i];
|
|
25
|
+
if (!it || !it.description) { setHint(-1); return; }
|
|
26
|
+
const r = wrap.current && wrap.current.getBoundingClientRect();
|
|
27
|
+
let s = descriptionSide;
|
|
28
|
+
if (r) { if (s === "right" && r.right + HINT_GAP + HINT_W > window.innerWidth && r.left - HINT_GAP - HINT_W >= 0) s = "left"; else if (s === "left" && r.left - HINT_GAP - HINT_W < 0 && r.right + HINT_GAP + HINT_W <= window.innerWidth) s = "right"; }
|
|
29
|
+
setSide(s); setHint(i);
|
|
30
|
+
};
|
|
31
|
+
const onBlur = (e) => { setHover(-1); if (hasHint && !(wrap.current && wrap.current.contains(e.relatedTarget))) setHint(-1); };
|
|
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 }}>
|
|
13
34
|
{items.map((it, i) => it.divider ? <div key={i} role="separator" style={{ height: 1, flexShrink: 0, background: "var(--border-subtle)", margin: "4px 0" }} /> : (
|
|
14
35
|
<button key={i} type="button" onClick={() => onSelect && onSelect(it.label)}
|
|
15
|
-
onMouseEnter={() =>
|
|
16
|
-
|
|
17
|
-
{
|
|
36
|
+
onMouseEnter={() => show(i)} onMouseLeave={() => setHover(-1)} onFocus={() => show(i)} onBlur={onBlur}
|
|
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}
|
|
18
40
|
</button>
|
|
19
41
|
))}
|
|
20
42
|
</div>
|
|
21
43
|
);
|
|
44
|
+
if (!hasHint) return menu;
|
|
45
|
+
const h = items[hint];
|
|
46
|
+
return (
|
|
47
|
+
<div ref={wrap} data-size={size} onMouseLeave={() => setHint(-1)} style={{ position: "relative", display: "inline-flex" }}>
|
|
48
|
+
{menu}
|
|
49
|
+
{h && h.description ? (
|
|
50
|
+
<div id={hintId} role="tooltip" style={{ position: "absolute", top: 0, [side === "left" ? "right" : "left"]: `calc(100% + ${HINT_GAP}px)`, width: HINT_W, boxSizing: "border-box", padding: "12px 14px", display: "flex", flexDirection: "column", gap: 6, 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)" }}>
|
|
51
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
52
|
+
{h.icon ? <Icon name={h.icon} size={16} style={{ flexShrink: 0, color: h.iconColor || (h.danger ? "var(--danger)" : "var(--text-heading)") }} /> : null}
|
|
53
|
+
<span style={{ fontSize: "var(--text-base)", lineHeight: "var(--leading-base)", letterSpacing: "var(--tracking-base)", fontWeight: "var(--weight-medium)", color: "var(--text-heading)" }}>{h.label}</span>
|
|
54
|
+
</div>
|
|
55
|
+
<span style={{ fontSize: "var(--text-xs)", lineHeight: "var(--leading-xs)", letterSpacing: "var(--tracking-xs)", color: "var(--text-secondary)", textWrap: "pretty" }}>{h.description}</span>
|
|
56
|
+
</div>
|
|
57
|
+
) : null}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
22
60
|
}
|
|
@@ -1,21 +1,61 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref, computed } from 'vue'
|
|
3
3
|
import Icon from '../icons/Icon.vue'
|
|
4
|
-
// Right-click context menu (Preview / Duplicate / Delete). items: [{label, icon, danger, divider, selected}]. Rows are control-height at the region's tier (a menu inherits the tier of the control that opened it); `size` overrides.
|
|
4
|
+
// Right-click context menu (Preview / Duplicate / Delete). items: [{label, icon, iconColor, description, danger, divider, selected}]. Rows are control-height at the region's tier (a menu inherits the tier of the control that opened it); `size` overrides.
|
|
5
5
|
// selected: true marks the current choice of a single-choice menu: --bg-hover fill, --weight-medium --text-heading, no checkmark. A menu with any
|
|
6
6
|
// selected item spaces its rows 2px apart (like Sidebar / Tree / DocTree); an action menu keeps its rows flush. Dividers keep 4px above and below.
|
|
7
|
-
|
|
7
|
+
// iconColor: any CSS colour for that item's icon; without it the icon takes the row's text colour.
|
|
8
|
+
// description: hovering or focusing the item shows a 300px panel beside the menu (icon + label, then the description), on descriptionSide
|
|
9
|
+
// ("right" by default), flipping to the other side when the viewport has no room.
|
|
10
|
+
const props = defineProps({ items: { type: Array, default: () => [] }, size: String, descriptionSide: { type: String, default: 'right' }, style: Object })
|
|
8
11
|
const choice = computed(() => props.items.some(it => it && it.selected))
|
|
12
|
+
const hasHint = computed(() => props.items.some(it => it && it.description))
|
|
9
13
|
const emit = defineEmits(['select'])
|
|
10
|
-
const hover = ref(-1)
|
|
14
|
+
const hover = ref(-1), hint = ref(-1), side = ref(props.descriptionSide), wrap = ref(null)
|
|
15
|
+
const HINT_W = 300, HINT_GAP = 8
|
|
16
|
+
const hintId = 'kb-cm-hint-' + Math.random().toString(36).slice(2, 8)
|
|
17
|
+
function show(i) {
|
|
18
|
+
hover.value = i
|
|
19
|
+
if (!hasHint.value) return
|
|
20
|
+
const it = props.items[i]
|
|
21
|
+
if (!it || !it.description) { hint.value = -1; return }
|
|
22
|
+
const r = wrap.value && wrap.value.getBoundingClientRect()
|
|
23
|
+
let s = props.descriptionSide
|
|
24
|
+
if (r) { if (s === 'right' && r.right + HINT_GAP + HINT_W > window.innerWidth && r.left - HINT_GAP - HINT_W >= 0) s = 'left'; else if (s === 'left' && r.left - HINT_GAP - HINT_W < 0 && r.right + HINT_GAP + HINT_W <= window.innerWidth) s = 'right' }
|
|
25
|
+
side.value = s; hint.value = i
|
|
26
|
+
}
|
|
27
|
+
function onBlur(e) { hover.value = -1; if (hasHint.value && !(wrap.value && wrap.value.contains(e.relatedTarget))) hint.value = -1 }
|
|
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' })
|
|
31
|
+
const iconStyle = (it) => ({ width: 'var(--control-icon)', height: 'var(--control-icon)', flexShrink: 0, ...(it.iconColor ? { color: it.iconColor } : null) })
|
|
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)' }))
|
|
11
33
|
</script>
|
|
12
34
|
<template>
|
|
13
|
-
<div
|
|
35
|
+
<div v-if="hasHint" ref="wrap" :data-size="size" @mouseleave="hint = -1" :style="{ position: 'relative', display: 'inline-flex' }">
|
|
36
|
+
<div role="menu" :style="menuStyle">
|
|
37
|
+
<template v-for="(it, i) in items" :key="i">
|
|
38
|
+
<div v-if="it.divider" role="separator" :style="{ height: '1px', flexShrink: 0, background: 'var(--border-subtle)', margin: '4px 0' }" />
|
|
39
|
+
<button v-else type="button" @click="emit('select', it.label)" @mouseenter="show(i)" @mouseleave="hover = -1" @focus="show(i)" @blur="onBlur"
|
|
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)" />{{ it.label }}
|
|
42
|
+
</button>
|
|
43
|
+
</template>
|
|
44
|
+
</div>
|
|
45
|
+
<div v-if="cur && cur.description" :id="hintId" role="tooltip" :style="hintStyle">
|
|
46
|
+
<div :style="{ display: 'flex', alignItems: 'center', gap: '8px' }">
|
|
47
|
+
<Icon v-if="cur.icon" :name="cur.icon" :size="16" :style="{ flexShrink: 0, color: cur.iconColor || (cur.danger ? 'var(--danger)' : 'var(--text-heading)') }" />
|
|
48
|
+
<span :style="{ fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', fontWeight: 'var(--weight-medium)', color: 'var(--text-heading)' }">{{ cur.label }}</span>
|
|
49
|
+
</div>
|
|
50
|
+
<span :style="{ fontSize: 'var(--text-xs)', lineHeight: 'var(--leading-xs)', letterSpacing: 'var(--tracking-xs)', color: 'var(--text-secondary)', textWrap: 'pretty' }">{{ cur.description }}</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div v-else role="menu" :data-size="size" :style="menuStyle">
|
|
14
54
|
<template v-for="(it, i) in items" :key="i">
|
|
15
55
|
<div v-if="it.divider" role="separator" :style="{ height: '1px', flexShrink: 0, background: 'var(--border-subtle)', margin: '4px 0' }" />
|
|
16
|
-
<button v-else type="button" @click="emit('select', it.label)" @mouseenter="hover = i" @mouseleave="hover = -1"
|
|
17
|
-
:role="choice ? 'menuitemradio' : 'menuitem'" :aria-checked="choice ? !!it.selected : undefined" :style="
|
|
18
|
-
<Icon v-if="it.icon" :name="it.icon" :size="16" :style="
|
|
56
|
+
<button v-else type="button" @click="emit('select', it.label)" @mouseenter="hover = i" @mouseleave="hover = -1" @focus="hover = i" @blur="hover = -1"
|
|
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)" />{{ it.label }}
|
|
19
59
|
</button>
|
|
20
60
|
</template>
|
|
21
61
|
</div>
|
|
@@ -2,9 +2,13 @@ import * as React from "react";
|
|
|
2
2
|
/** Trigger-anchored dropdown menu (reuses ContextMenu's look). Addition — for "Show ⌄"-style buttons and user menus. */
|
|
3
3
|
export interface MenuProps {
|
|
4
4
|
trigger: React.ReactNode;
|
|
5
|
-
items?: { label?: string; icon?: string; danger?: boolean; divider?: boolean; /** Current choice of a single-choice menu: --bg-hover fill, --weight-medium --text-heading, no checkmark. Any selected item spaces the rows 2px apart. */ selected?: boolean }[];
|
|
5
|
+
items?: { label?: string; icon?: string; /** Colour for this item's icon — any CSS colour, e.g. "var(--cat-join)". Omit for a single-colour icon in the row's text colour. */ iconColor?: string; /** Shown in a 300px panel beside the menu while the item is hovered or focused: the item's icon and label, then this text. */ description?: string; danger?: boolean; divider?: boolean; /** Current choice of a single-choice menu: --bg-hover fill, --weight-medium --text-heading, no checkmark. Any selected item spaces the rows 2px apart. */ selected?: boolean }[];
|
|
6
6
|
onSelect?: (label: string) => void;
|
|
7
7
|
align?: "start" | "end";
|
|
8
|
+
/** Side for item descriptions. Default: away from the trigger's edge — "left" when align="end", "right" when align="start". Flips if the viewport has no room. */
|
|
9
|
+
descriptionSide?: "left" | "right";
|
|
10
|
+
/** Start open (docs and demos). */
|
|
11
|
+
defaultOpen?: boolean;
|
|
8
12
|
style?: React.CSSProperties;
|
|
9
13
|
}
|
|
10
14
|
export declare function Menu(props: MenuProps): JSX.Element;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { ContextMenu } from "./ContextMenu.jsx";
|
|
3
3
|
|
|
4
|
-
/** Click-to-open dropdown menu anchored under its trigger (vs. ContextMenu, which opens at a click point). Closes on outside click.
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
/** Click-to-open dropdown menu anchored under its trigger (vs. ContextMenu, which opens at a click point). Closes on outside click.
|
|
5
|
+
* Items take ContextMenu's fields (icon, iconColor, description, …). Item descriptions open on the side away from the trigger's edge:
|
|
6
|
+
* align="end" (trigger at the right) → left, align="start" (trigger at the left) → right; descriptionSide overrides. */
|
|
7
|
+
export function Menu({ trigger, items = [], onSelect, align = "start", descriptionSide, defaultOpen = false, style }) {
|
|
8
|
+
const [open, setOpen] = React.useState(defaultOpen);
|
|
7
9
|
const ref = React.useRef(null);
|
|
8
10
|
React.useEffect(() => {
|
|
9
11
|
if (!open) return;
|
|
@@ -16,7 +18,7 @@ export function Menu({ trigger, items = [], onSelect, align = "start", style })
|
|
|
16
18
|
<span onClick={() => setOpen((o) => !o)}>{trigger}</span>
|
|
17
19
|
{open ? (
|
|
18
20
|
<div style={{ position: "absolute", top: "calc(100% + 6px)", [align === "end" ? "right" : "left"]: 0, zIndex: "var(--z-menu)" }}>
|
|
19
|
-
<ContextMenu items={items} onSelect={(l) => { setOpen(false); onSelect && onSelect(l); }} />
|
|
21
|
+
<ContextMenu items={items} descriptionSide={descriptionSide || (align === "end" ? "left" : "right")} onSelect={(l) => { setOpen(false); onSelect && onSelect(l); }} />
|
|
20
22
|
</div>
|
|
21
23
|
) : null}
|
|
22
24
|
</div>
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import ContextMenu from './ContextMenu.vue'
|
|
4
4
|
// Click-to-open dropdown menu anchored under its trigger (vs. ContextMenu, which opens at a click point). Closes on outside click.
|
|
5
|
-
|
|
5
|
+
// Items take ContextMenu's fields (icon, iconColor, description, …). Descriptions open on the side away from the trigger's edge:
|
|
6
|
+
// align="end" → left, align="start" → right; descriptionSide overrides.
|
|
7
|
+
const props = defineProps({ items: { type: Array, default: () => [] }, align: { type: String, default: 'start' }, descriptionSide: String, defaultOpen: Boolean, style: Object })
|
|
6
8
|
const emit = defineEmits(['select'])
|
|
7
|
-
const open = ref(
|
|
9
|
+
const open = ref(props.defaultOpen)
|
|
8
10
|
const root = ref(null)
|
|
9
11
|
function onDoc(e) { if (root.value && !root.value.contains(e.target)) open.value = false }
|
|
10
12
|
onMounted(() => document.addEventListener('mousedown', onDoc))
|
|
@@ -15,7 +17,7 @@ function select(l) { open.value = false; emit('select', l) }
|
|
|
15
17
|
<div ref="root" :style="{ position: 'relative', display: 'inline-flex', ...style }">
|
|
16
18
|
<span @click="open = !open"><slot name="trigger" /></span>
|
|
17
19
|
<div v-if="open" :style="{ position: 'absolute', top: 'calc(100% + 6px)', [align === 'end' ? 'right' : 'left']: 0, zIndex: 'var(--z-menu)' }">
|
|
18
|
-
<ContextMenu :items="items" @select="select" />
|
|
20
|
+
<ContextMenu :items="items" :description-side="descriptionSide || (align === 'end' ? 'left' : 'right')" @select="select" />
|
|
19
21
|
</div>
|
|
20
22
|
</div>
|
|
21
23
|
</template>
|
|
@@ -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>
|
|
@@ -34,6 +34,8 @@ const props = defineProps({
|
|
|
34
34
|
loadData: Function, rowActions: Function, style: Object,
|
|
35
35
|
})
|
|
36
36
|
const emit = defineEmits(['select', 'check', 'expand', 'drop'])
|
|
37
|
+
const Cell = (p) => p.v
|
|
38
|
+
Cell.props = ['v']
|
|
37
39
|
|
|
38
40
|
const data = ref(JSON.parse(JSON.stringify(props.treeData || props.items)))
|
|
39
41
|
// Compare by value: re-seeding on identity alone would wipe async-loaded children.
|
|
@@ -263,6 +265,8 @@ const dropAt = k => (drop.value && drop.value.key === k ? drop.value.pos : null)
|
|
|
263
265
|
</span>
|
|
264
266
|
<span v-if="row.n.tag" style="font-family:var(--font-sans);font-size:var(--text-xs);line-height:1;padding:2px 5px;border-radius:var(--radius-sm);background:var(--bg-inset);color:var(--text-secondary);flex-shrink:0">{{ row.n.tag }}</span>
|
|
265
267
|
<span v-if="row.n.count != null" style="margin-left:auto;font-family:var(--font-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);color:var(--text-muted);padding-left:8px">{{ row.n.count }}</span>
|
|
268
|
+
<!-- row actions: #row-actions="{ node }" slot, or rowActions(node) returning a VNode; shown on hover / focus -->
|
|
269
|
+
<span v-if="(rowActions || $slots['row-actions']) && !row.n.disabled" class="kb-tree-actions" @click.stop :style="{ marginLeft: row.n.count != null ? '6px' : 'auto', display: 'flex', gap: '2px', flexShrink: 0 }"><slot name="row-actions" :node="row.n"><Cell v-if="rowActions" :v="rowActions(row.n)" /></slot></span>
|
|
266
270
|
</div>
|
|
267
271
|
<div v-if="!rows.length" style="padding:10px 8px;font-family:var(--font-sans);font-size:var(--text-base);color:var(--text-muted)">No matching items</div>
|
|
268
272
|
</div>
|
|
@@ -270,6 +274,8 @@ const dropAt = k => (drop.value && drop.value.key === k ? drop.value.pos : null)
|
|
|
270
274
|
<style scoped>
|
|
271
275
|
.kb-tree-row:not(.is-disabled):hover{background:var(--bg-hover)}
|
|
272
276
|
.kb-tree-row:focus-visible{outline:2px solid var(--focus-border);outline-offset:-2px}
|
|
277
|
+
.kb-tree-actions{opacity:0;transition:var(--t-hover)}
|
|
278
|
+
.kb-tree-row:hover .kb-tree-actions,.kb-tree-row:focus-within .kb-tree-actions{opacity:1}
|
|
273
279
|
.kb-tree-spin{animation:kb-tree-spin 900ms linear infinite}
|
|
274
280
|
@keyframes kb-tree-spin{to{transform:rotate(360deg)}}
|
|
275
281
|
</style>
|
package/index.html
CHANGED
|
@@ -59,22 +59,6 @@ section{margin-bottom:52px;scroll-margin-top:78px}
|
|
|
59
59
|
.frame-wrap{position:relative;border:1px solid var(--border-card);border-radius:var(--radius-lg);background:var(--bg-surface);overflow:hidden}
|
|
60
60
|
.frame-wrap iframe{border:0;display:block;transform-origin:top left;background:var(--bg-surface)}
|
|
61
61
|
.frame-ph{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-family:var(--font-sans);font-variant-numeric:tabular-nums;font-size:var(--text-xs);color:var(--text-muted);background:var(--bg-inset);letter-spacing:var(--tracking-xs)}
|
|
62
|
-
.code-area{margin-top:14px}
|
|
63
|
-
.code-btn{display:inline-flex;align-items:center;gap:6px;height:28px;box-sizing:border-box;padding:0 8px;line-height:var(--leading-xs);border-radius:var(--radius-md);border:1px solid var(--border-control);background:var(--bg-surface);color:var(--text-secondary);font-size:var(--text-xs);font-family:var(--font-sans);cursor:pointer;transition:var(--t-hover);letter-spacing:var(--tracking-xs)}
|
|
64
|
-
.code-btn:hover{background:var(--bg-hover);color:var(--text-heading)}
|
|
65
|
-
.code-panel{display:none;margin-top:10px;border:1px solid var(--border-card);border-radius:var(--radius-lg);overflow:hidden;background:var(--bg-inset)}
|
|
66
|
-
.code-panel.open{display:block}
|
|
67
|
-
.code-head{display:flex;align-items:center;justify-content:space-between;background:var(--bg-surface);border-bottom:1px solid var(--border-subtle)}
|
|
68
|
-
.code-tabs{display:flex;flex:1;overflow-x:auto;scrollbar-width:none}
|
|
69
|
-
.code-tabs::-webkit-scrollbar{display:none}
|
|
70
|
-
.code-tab{padding:8px 12px;border:none;background:transparent;font-size:var(--text-xs);font-family:var(--font-sans);color:var(--text-secondary);cursor:pointer;white-space:nowrap;border-bottom:2px solid transparent;margin-bottom:-1px;letter-spacing:var(--tracking-xs)}
|
|
71
|
-
.code-tab.on{color:var(--text-heading);border-bottom-color:var(--action-primary)}
|
|
72
|
-
.code-fw{display:flex;gap:3px;padding:6px 8px;flex-shrink:0}
|
|
73
|
-
.code-fw{display:flex;align-items:center;flex-shrink:0;height:28px;box-sizing:border-box;gap:2px;padding:2px;margin:0 8px;border-radius:var(--radius-md);background:var(--bg-inset);border:1px solid var(--border-control)}.fw-btn{display:inline-flex;align-items:center;height:22px;padding:0 6px;font-size:var(--text-xs);line-height:var(--leading-xs);border-radius:var(--radius-sm);border:none;background:transparent;color:var(--text-secondary);font-family:var(--font-sans);font-weight:var(--weight-medium);white-space:nowrap;cursor:pointer;transition:var(--t-hover);letter-spacing:var(--tracking-xs)}.fw-btn:not(.on):hover{color:var(--text-heading)}
|
|
74
|
-
.fw-btn.on{background:var(--bg-surface);color:var(--text-heading);box-shadow:0 1px 2px rgba(0,0,0,.08)}
|
|
75
|
-
.code-body{position:relative}
|
|
76
|
-
.code-body pre{margin:0;padding:14px 70px 14px 16px;overflow:auto;max-height:420px;font-family:var(--font-mono);font-size:var(--text-code);line-height:var(--leading-code);color:var(--text-body);white-space:pre}
|
|
77
|
-
.copy-btn{position:absolute;top:8px;right:10px;display:inline-flex;align-items:center;height:28px;box-sizing:border-box;padding:0 8px;border-radius:var(--radius-md);border:1px solid var(--border-control);background:var(--bg-surface);color:var(--text-secondary);font-size:var(--text-xs);line-height:var(--leading-xs);font-family:var(--font-sans);cursor:pointer;letter-spacing:var(--tracking-xs)}
|
|
78
62
|
|
|
79
63
|
/* overview */
|
|
80
64
|
#overview{margin-bottom:64px}
|
|
@@ -192,7 +176,7 @@ section{margin-bottom:52px;scroll-margin-top:78px}
|
|
|
192
176
|
const GROUPS=[["Brand","品牌","ブランド"],["Colors","颜色","カラー"],["Type","字体排印","タイポグラフィ"],["Foundations","基础规范","基盤仕様"],["Components","组件","コンポーネント"],["Charts","图表","チャート"],["Icons","图标库","アイコン"]];
|
|
193
177
|
|
|
194
178
|
const S={
|
|
195
|
-
brandsub:["Design System · v2.
|
|
179
|
+
brandsub:["Design System · v2.5.0","设计系统 · v2.5.0","デザインシステム · v2.5.0"],
|
|
196
180
|
doc_readme:["Readme","说明文档","ドキュメント"],
|
|
197
181
|
doc_design:["DESIGN.md","下载 DESIGN.md","DESIGN.md"],
|
|
198
182
|
doc_integration:["Integration","接入指南","導入ガイド"],
|
|
@@ -258,6 +242,7 @@ function setLang(l){
|
|
|
258
242
|
document.getElementById("filter").placeholder=pick(S.filter_ph);
|
|
259
243
|
document.querySelectorAll('a[data-readme]').forEach(a=>{a.href=(l==="zh"||l==="both")?"readme.zh-CN.md":"readme.md";});
|
|
260
244
|
if(typeof fitChangelogTags==="function")fitChangelogTags();
|
|
245
|
+
syncFrames();
|
|
261
246
|
}
|
|
262
247
|
|
|
263
248
|
/* ---------- overview ---------- */
|
|
@@ -451,6 +436,8 @@ function renderNav(){
|
|
|
451
436
|
let followSystem=true;
|
|
452
437
|
const themeOf=()=>html.getAttribute("data-theme")||"light";
|
|
453
438
|
function pushTheme(f){
|
|
439
|
+
/* Cards also need the site language: the "Show code" bars under each example follow it. */
|
|
440
|
+
try{f.contentWindow&&f.contentWindow.postMessage({kb:"lang",lang},"*");}catch(e){}
|
|
454
441
|
if(f.dataset.alwaysdark==="1"||f.dataset.selfmanaged==="1")return;
|
|
455
442
|
const t=themeOf();
|
|
456
443
|
try{f.contentWindow&&f.contentWindow.postMessage({kb:"theme",theme:t},"*");}catch(e){}
|
|
@@ -473,49 +460,12 @@ if(mq&&mq.addEventListener)mq.addEventListener("change",e=>{if(followSystem)setT
|
|
|
473
460
|
document.getElementById("themeToggle").addEventListener("click",()=>setTheme(themeOf()==="dark"?"light":"dark",true));
|
|
474
461
|
|
|
475
462
|
/* ---------- frames: lazy load, fit to width, grow with content ---------- */
|
|
476
|
-
/* Cards
|
|
477
|
-
cannot read the frame's DOM); the View Code UI is rendered here, below the box. */
|
|
478
|
-
const CODE_ICON='<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 16 4-4-4-4"/><path d="m6 8-4 4 4 4"/><path d="m14.5 4-5 16"/></svg>';
|
|
479
|
-
function buildCode(f,files){
|
|
480
|
-
if(f.__code||!files||!files.length)return;
|
|
481
|
-
const area=document.createElement("div");area.className="code-area";
|
|
482
|
-
const btn=document.createElement("button");btn.className="code-btn";
|
|
483
|
-
btn.innerHTML=CODE_ICON+'<span>View Code</span>';
|
|
484
|
-
const panel=document.createElement("div");panel.className="code-panel";
|
|
485
|
-
const head=document.createElement("div");head.className="code-head";
|
|
486
|
-
const tabs=document.createElement("div");tabs.className="code-tabs";
|
|
487
|
-
const fwb=document.createElement("div");fwb.className="code-fw";
|
|
488
|
-
head.appendChild(tabs);head.appendChild(fwb);
|
|
489
|
-
const body=document.createElement("div");body.className="code-body";
|
|
490
|
-
const pre=document.createElement("pre");
|
|
491
|
-
const copy=document.createElement("button");copy.className="copy-btn";copy.textContent="Copy";
|
|
492
|
-
body.appendChild(pre);body.appendChild(copy);
|
|
493
|
-
panel.appendChild(head);panel.appendChild(body);
|
|
494
|
-
area.appendChild(btn);area.appendChild(panel);
|
|
495
|
-
f.__section.appendChild(area);
|
|
496
|
-
let framework="react",active=0,open=false;const cache={};
|
|
497
|
-
files.forEach((t,i)=>{const b=document.createElement("button");b.className="code-tab";b.textContent=t.name;b.onclick=()=>{active=i;render();};tabs.appendChild(b);});
|
|
498
|
-
[["react","React"],["vue","Vue 3"]].forEach(([k,lab])=>{const b=document.createElement("button");b.className="fw-btn";b.textContent=lab;b.onclick=()=>{framework=k;render();};fwb.appendChild(b);});
|
|
499
|
-
const hidden=(t)=>(framework==="vue"&&t.reactOnly)||(framework==="react"&&t.vueOnly);
|
|
500
|
-
function render(){
|
|
501
|
-
if(hidden(files[active])){const k=files.findIndex(t=>!hidden(t));if(k>=0)active=k;}
|
|
502
|
-
[...tabs.children].forEach((el,i)=>{el.classList.toggle("on",i===active);el.style.display=hidden(files[i])?"none":"";el.textContent=framework==="vue"?(files[i].vueName||files[i].name.replace(/\.jsx$/,".vue")):files[i].name;});
|
|
503
|
-
[...fwb.children].forEach((el,i)=>el.classList.toggle("on",i===(framework==="react"?0:1)));
|
|
504
|
-
const key=framework+"|"+files[active].path;
|
|
505
|
-
if(cache[key]!=null)pre.textContent=cache[key];
|
|
506
|
-
else{pre.textContent="Loading…";try{f.contentWindow.postMessage({kb:"src-get",path:files[active].path,framework},"*");}catch(e){}}
|
|
507
|
-
}
|
|
508
|
-
copy.onclick=()=>{const t=pre.textContent;try{navigator.clipboard.writeText(t);}catch(e){}copy.textContent="Copied";setTimeout(()=>{copy.textContent="Copy";},1200);};
|
|
509
|
-
btn.onclick=()=>{open=!open;panel.classList.toggle("open",open);btn.querySelector("span").textContent=open?"Hide Code":"View Code";if(open)render();};
|
|
510
|
-
f.__code={receive(m){cache[m.framework+"|"+m.path]=m.text;if(open&&files[active].path===m.path&&framework===m.framework)pre.textContent=m.text;}};
|
|
511
|
-
}
|
|
463
|
+
/* Cards ask for the site theme over postMessage (works from file:// too). Usage code ("Show code") lives inside each card. */
|
|
512
464
|
addEventListener("message",e=>{
|
|
513
465
|
const m=e.data;if(!m||!m.kb)return;
|
|
514
466
|
const f=[...document.querySelectorAll("iframe")].find(x=>{try{return x.contentWindow===e.source;}catch(err){return false;}});
|
|
515
467
|
if(!f)return;
|
|
516
|
-
if(m.kb==="
|
|
517
|
-
else if(m.kb==="src-text"&&f.__code)f.__code.receive(m);
|
|
518
|
-
else if(m.kb==="theme-ask")pushTheme(f);
|
|
468
|
+
if(m.kb==="theme-ask")pushTheme(f);
|
|
519
469
|
});
|
|
520
470
|
function fit(f){
|
|
521
471
|
const wrap=f.__wrap,avail=wrap.clientWidth;
|
package/package.json
CHANGED
package/tokens/typography.css
CHANGED
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
[lang|="zh"]{--font-sans:'Inter','PingFang SC','Hiragino Sans GB','Microsoft YaHei','Noto Sans SC',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
40
40
|
[lang|="ja"]{--font-sans:'Inter','Hiragino Kaku Gothic ProN','Hiragino Sans','Yu Gothic UI','Meiryo','Noto Sans JP',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
41
41
|
[lang|="ko"]{--font-sans:'Inter','Apple SD Gothic Neo','Malgun Gothic','Noto Sans KR',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
42
|
-
|
|
42
|
+
/* Set the font only where a language is declared; plain text inherits it, so descendant icon and code fonts keep their own family (e.g. AG Grid selection icons). */
|
|
43
|
+
:where([lang|="zh"],[lang|="ja"],[lang|="ko"]){font-family:var(--font-sans)}
|
|
43
44
|
[lang|="zh"],[lang|="ja"],[lang|="ko"]{--weight-medium:500;--weight-semibold:500;
|
|
44
45
|
--text-glyph-min:12px;--text-2xs:12px;--text-xs:13px;--leading-xs:18px;--text-sm:13px;--leading-sm:18px;
|
|
45
46
|
--tracking-2xs:0;--tracking-xs:0;--tracking-sm:0;--tracking-base:0;--tracking-md:0;--tracking-lg:0;--tracking-xl:0;--tracking-num:0;--tracking-2xl:0;--tracking-3xl:0;--tracking-4xl:0;--tracking-display:0}
|