@keyfunc/keybricks-ds 2.5.1 → 2.7.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 +3 -3
- package/DESIGN.md +13 -4
- package/INTEGRATION.md +25 -2
- package/_adherence.oxlintrc.json +57 -11
- package/_ds_bundle.js +2137 -186
- package/_scripts/build-index.mjs +1 -1
- package/_src/icon-index.js +1 -1
- package/assets/icons/icon-index.json +1 -1
- package/changelog.js +15 -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 +5 -2
- package/components/_cards/button-2-states.card.html +5 -5
- package/components/_cards/button-3-sizes-icons.card.html +2 -2
- package/components/_cards/button-4-group.card.html +2 -2
- package/components/_cards/button-5-ai.card.html +34 -0
- package/components/_cards/canvas-zoom-controls.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/charts-99-fullpage.card.html +1 -1
- package/components/_cards/checkbox.card.html +1 -1
- package/components/_cards/codeblock.card.html +1 -1
- package/components/_cards/combobox.card.html +1 -1
- package/components/_cards/context-menu.card.html +1 -1
- package/components/_cards/date-picker.card.html +1 -1
- package/components/_cards/diffvalue.card.html +1 -1
- package/components/_cards/divider.card.html +1 -1
- package/components/_cards/drawer.card.html +1 -1
- package/components/_cards/dropdown-menu.card.html +1 -1
- package/components/_cards/dropzone.card.html +1 -1
- package/components/_cards/empty-state.card.html +1 -1
- package/components/_cards/filter-bar.card.html +1 -1
- package/components/_cards/icon-button.card.html +1 -1
- package/components/_cards/icons-0-usage.card.html +2 -2
- package/components/_cards/icons-1-core.card.html +1 -1
- package/components/_cards/icons-2-feather.card.html +1 -1
- package/components/_cards/icons-2b-lucide.card.html +1 -1
- package/components/_cards/icons-3-unicons.card.html +1 -1
- package/components/_cards/icons-4-datasource.card.html +1 -1
- package/components/_cards/input.card.html +1 -1
- package/components/_cards/kbd.card.html +5 -7
- 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-global.card.html +72 -0
- 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/slider.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 +1 -1
- 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/_vue/index.mjs +2 -0
- package/components/buttons/BUTTON_MIGRATION.md +111 -0
- package/components/buttons/Button.d.ts +14 -5
- package/components/buttons/Button.jsx +45 -8
- package/components/buttons/Button.vue +61 -12
- package/components/buttons/Button.vue.d.ts +47 -0
- package/components/buttons/ai-button.js +54 -0
- package/components/buttons/buttons.prompt.md +7 -2
- package/components/display/Kbd.d.ts +3 -1
- package/components/display/Kbd.jsx +8 -3
- package/components/display/Kbd.vue +7 -3
- package/components/display/display.prompt.md +3 -1
- package/components/feedback/MESSAGE_SERVICES.md +71 -0
- package/components/feedback/Modal.d.ts +7 -0
- package/components/feedback/Modal.jsx +9 -9
- package/components/feedback/Toast.vue +8 -8
- package/components/feedback/_internal/MessageBoxView.vue +237 -0
- package/components/feedback/_internal/MessageView.vue +73 -0
- package/components/feedback/_internal/service-utils.js +46 -0
- package/components/feedback/feedback.prompt.md +2 -0
- package/components/feedback/message-box.d.ts +69 -0
- package/components/feedback/message-box.js +92 -0
- package/components/feedback/message.d.ts +37 -0
- package/components/feedback/message.js +68 -0
- package/components/forms/GlobalSearch.d.ts +83 -0
- package/components/forms/GlobalSearch.jsx +172 -0
- package/components/forms/GlobalSearch.vue +161 -0
- package/components/forms/forms.prompt.md +7 -0
- package/components/forms/global-search.js +161 -0
- package/components/icons/Icon.d.ts +2 -0
- package/components/icons/Icon.jsx +14 -6
- package/components/icons/Icon.vue +11 -4
- package/components/icons/icon-paths.js +4 -2
- package/components/index.d.ts +1 -0
- package/components/index.js +1 -0
- package/components/navigation/TopBar.d.ts +9 -3
- package/components/navigation/TopBar.jsx +5 -7
- package/components/navigation/TopBar.vue +5 -6
- package/covers.js +4 -2
- package/gallery.html +1 -1
- package/guidelines/brand-logo.html +1 -1
- package/guidelines/colors-schemes.html +1 -1
- package/guidelines/colors-transparency.html +1 -1
- package/guidelines/foundations-borders.html +1 -1
- package/guidelines/foundations-chrome.html +1 -1
- package/guidelines/foundations-motion.html +1 -1
- package/guidelines/foundations-size-tiers.html +1 -1
- package/guidelines/foundations-spacing.html +1 -1
- package/guidelines/interaction-states.html +1 -1
- package/guidelines/type-extended.html +1 -1
- package/guidelines/type-mono.html +1 -1
- package/guidelines/type-scale.html +1 -1
- package/guidelines/type-weights.html +1 -1
- package/index.html +8 -8
- package/package.json +10 -2
- package/readme.md +4 -4
- package/readme.zh-CN.md +4 -4
- package/site-nav.js +1 -1
- package/styles.css +1 -0
- package/tokens/ai.css +17 -0
- package/tokens/colors.css +1 -1
- package/tokens/tokens.d.ts +28 -0
- package/tokens/tokens.js +47 -5
- package/tokens/tokens.json +123 -5
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<!-- @dsCard group="Components" viewport="1000x3634" name="Search · Global" subtitle="GlobalSearch — TopBar search with ⌘K / Ctrl K · scopes · grouped results · Prophet AI hand-off · Deep Search" -->
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
|
+
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
5
|
+
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
|
6
|
+
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
|
7
|
+
<script src="../../_ds_bundle.js"></script>
|
|
8
|
+
<script src="../_shared/view-source.js"></script>
|
|
9
|
+
<script src="../_shared/card-kit.js"></script>
|
|
10
|
+
<style>.gs-frame{border:1px solid var(--border-card);border-radius:var(--radius-lg);overflow:hidden;background:var(--bg-app)}.gs-note{font-family:var(--font-sans);font-size:var(--text-xs);line-height:var(--leading-xs);color:var(--text-secondary);padding:10px 24px}</style></head>
|
|
11
|
+
<body><div id="root"></div>
|
|
12
|
+
<script type="text/babel">
|
|
13
|
+
const { GlobalSearch, TopBar, GLOBAL_SEARCH_LABELS } = window.KeybricksDesignSystem_71231a;
|
|
14
|
+
const { KBSec: Sec } = window;
|
|
15
|
+
const ITEMS = [
|
|
16
|
+
{ kind: "feature", title: "Revenue Planning", sub: "Planning · Forecast models", keywords: ["forecast", "budget"] },
|
|
17
|
+
{ kind: "feature", title: "Revenue Recognition Rules", sub: "Rules · IFRS 15" },
|
|
18
|
+
{ kind: "feature", title: "Consolidation Monitor", sub: "Close · Group entities", keywords: ["revenue", "close"] },
|
|
19
|
+
{ kind: "feature", title: "FX Rates", sub: "Data · Currency tables", keywords: ["currency", "revenue"] },
|
|
20
|
+
{ kind: "feature", title: "Revenue Allocation", sub: "Rules · Cost centers" },
|
|
21
|
+
{ kind: "document", title: "Q3 Revenue Bridge", sub: "Finance / Reports · updated 2h ago", meta: "Report", tone: "join" },
|
|
22
|
+
{ kind: "document", title: "Revenue by Entity FY24", sub: "Finance / Sheets · updated yesterday", meta: "Sheet", tone: "source" },
|
|
23
|
+
{ kind: "document", title: "Revenue Forecast Model", sub: "Planning / Models · updated 3d ago", meta: "Model", tone: "transform", icon: "box" },
|
|
24
|
+
{ kind: "document", title: "APAC Revenue Review", sub: "Finance / Reports · updated last week", meta: "Report", tone: "join" },
|
|
25
|
+
{ kind: "document", title: "Revenue Close Checklist", sub: "Close / Docs · updated last week", meta: "Doc", tone: "output" },
|
|
26
|
+
{ kind: "document", title: "Deferred Revenue Schedule", sub: "Finance / Sheets · updated 2 weeks ago", meta: "Sheet", tone: "source" },
|
|
27
|
+
{ kind: "member", title: "Revenue (4000)", sub: "Account · Income statement" },
|
|
28
|
+
{ kind: "member", title: "Other Revenue (4900)", sub: "Account · Income statement" },
|
|
29
|
+
{ kind: "member", title: "Revenue Adjustments", sub: "Scenario · Actuals" },
|
|
30
|
+
{ kind: "member", title: "Net Revenue", sub: "Account · Calculated" },
|
|
31
|
+
{ kind: "action", title: "Run revenue consolidation", sub: "Close · about 4 min", icon: "play" },
|
|
32
|
+
{ kind: "action", title: "Create revenue forecast", sub: "Planning", icon: "plus" },
|
|
33
|
+
];
|
|
34
|
+
const RECENT = [ITEMS[5], ITEMS[6], ITEMS[7]];
|
|
35
|
+
const PROMPTS = ["Which entity had the largest YoY revenue change in Q3?", "Explain how the FX model translates currencies", "Which workflows are waiting for my approval?"];
|
|
36
|
+
const STEPS = ["Understand the question and plan the search", "Search document titles and text", "Read model and dimension data", "Cross-check sources", "Write conclusion with citations"];
|
|
37
|
+
const DONE = { status: "done", elapsed: "42 s", steps: STEPS.map((label, i) => ({ label, state: "done", meta: ["2 s", "11 s", "14 s", "8 s", "7 s"][i] })), conclusion: ["APAC drove most of the Q3 revenue change: +18% YoY, led by Shanghai Ops after the July price update.", "EMEA was flat (+1%); AMER fell 4% on the delayed Northwind renewal."], sources: ["Q3 Revenue Bridge", "Revenue by Entity FY24", "APAC Revenue Review", "FX Rates", "Revenue (4000)"] };
|
|
38
|
+
const slow = (term) => new Promise((r) => setTimeout(() => r(ITEMS.filter((it) => (it.title + " " + (it.keywords || []).join(" ")).toLowerCase().includes(term.toLowerCase()))), 900));
|
|
39
|
+
const never = () => new Promise(() => {});
|
|
40
|
+
function deepSearch(term, update) {
|
|
41
|
+
let i = 0; const steps = STEPS.map((label) => ({ label, state: "pending" }));
|
|
42
|
+
const tick = () => { steps.forEach((s, j) => { s.state = j < i ? "done" : j === i ? "running" : "pending"; }); update({ status: "running", steps: steps.map((s) => ({ ...s })) }); if (i++ < STEPS.length) t = setTimeout(tick, 1100); else update({ ...DONE }); };
|
|
43
|
+
let t = setTimeout(tick, 200); return () => clearTimeout(t);
|
|
44
|
+
}
|
|
45
|
+
const base = { items: ITEMS, recentSearches: ["revenue bridge", "FX rates", "approval workflows"], recent: RECENT, prompts: PROMPTS };
|
|
46
|
+
function Sheet(){
|
|
47
|
+
const [asked, setAsked] = React.useState(null);
|
|
48
|
+
return <div className="kb-sheet">
|
|
49
|
+
<Sec title="Live — in the TopBar" note="Click the field or press ⌘K / Ctrl K. Type “revenue”: a 300ms pause, then a spinner and skeleton rows, then grouped results. ↑↓ select, ↵ open, ⌘↵ / Ctrl Enter asks Prophet AI, Esc closes. Choose Deep Search on a query to watch it run.">
|
|
50
|
+
<div className="gs-frame"><TopBar user={{ name: "Felix Wang", email: "felix@keyfunc.com" }} search={{ ...base, onQuery: slow, onDeepSearch: deepSearch }} onAi={(q) => setAsked(q)} />
|
|
51
|
+
<div className="gs-note">{asked == null ? "Prophet AI hand-off: nothing asked yet." : "Prophet AI panel would open with: " + (asked ? "“" + asked + "”" : "(no question)")}</div></div>
|
|
52
|
+
</Sec>
|
|
53
|
+
<Sec title="Empty — recent searches, recent and Try Asking Prophet AI" note="No scope bar and no pinned rows. Prompts are rows: ↑↓ reaches them and ↵ hands the prompt to Prophet AI. The selected prompt's tile takes the AI hover treatment.">
|
|
54
|
+
<GlobalSearch inline {...base} />
|
|
55
|
+
</Sec>
|
|
56
|
+
<Sec title="Results — scope bar, grouped results, pinned AI rows" note="Under All, groups show Features 4 · Documents 5 · Dimension Members 3 · Actions 2, with View all to switch scope. The matched text is highlighted. Ask Prophet AI and Deep Search stay pinned however many results there are.">
|
|
57
|
+
<GlobalSearch inline {...base} defaultQuery="revenue" onDeepSearch={deepSearch} style={{ maxHeight: 620 }} />
|
|
58
|
+
</Sec>
|
|
59
|
+
<Sec title="Loading" note="After a 300ms typing pause: spinner in place of the search icon, “Searching…”, skeleton rows. The pinned rows already follow the live text.">
|
|
60
|
+
<GlobalSearch inline {...base} defaultQuery="forecast" onQuery={never} onDeepSearch={deepSearch} />
|
|
61
|
+
</Sec>
|
|
62
|
+
<Sec title="Deep Search — done" note="Steps fold into one row (click to expand), then the cited conclusion. Ask Follow-up hands off to Prophet AI; Esc goes back to results.">
|
|
63
|
+
<GlobalSearch inline {...base} defaultQuery="Q3 revenue change by entity" defaultDeep={DONE} onDeepSearch={deepSearch} />
|
|
64
|
+
</Sec>
|
|
65
|
+
<Sec title="中文 — labels={GLOBAL_SEARCH_LABELS.zh}" note="Prophet AI is 方知 in Chinese, with no “AI” suffix. The other strings are drafts.">
|
|
66
|
+
<div lang="zh-CN"><GlobalSearch inline labels={GLOBAL_SEARCH_LABELS.zh} items={ITEMS} defaultQuery="revenue" onDeepSearch={deepSearch} style={{ maxHeight: 560 }} /></div>
|
|
67
|
+
</Sec>
|
|
68
|
+
</div>;
|
|
69
|
+
}
|
|
70
|
+
ReactDOM.createRoot(document.getElementById("root")).render(<Sheet/>);
|
|
71
|
+
if (window.KBViewSource) setTimeout(function(){ KBViewSource.mount("#root", [{"name":"GlobalSearch.jsx","path":"../forms/GlobalSearch.jsx"},{"name":"global-search.js","path":"../forms/global-search.js"}]); }, 0);
|
|
72
|
+
</script></body></html>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x756" name="Search Input" subtitle="Bordered · bare top-bar · with value/clear · disabled" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x1864" name="Segmented Control" subtitle="Mutually exclusive views in one inset track — md/sm · counts · icons · icon-only · disabled option · full width · ViewToggle" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x513" name="Select" subtitle="md / sm · placeholder · disabled · error" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x267" name="Slider" subtitle="Filled track · ring knob · value bubble · min/max labels" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x1454" name="Smart Table (Database Grid)" subtitle="Frozen header and row numbers · default and compact density · typed fields · hue pills · aggregates · + New" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="924x590" name="Spreadsheet" subtitle="Formula bar · frozen headers · range selection · =SUM() · sheet tabs · status bar" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x618" name="Stepper" subtitle="Multi-step flow header — done · current · upcoming" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x11184" name="Table" subtitle="Anatomy · densities · cell types (display · editable · per tier) · two-line name · sorting · selection · frames · column resize · states · composition" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x1118" name="Tabs" subtitle="Underline tabs · panel tabs · pill filter tabs · counts on every kind" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x438" name="Tag" subtitle="Removable filter tags — distinct from Badge/ActionChip" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="924x674" name="Textarea" subtitle="Default · character count · over the limit · disabled" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x373" name="Timeline" subtitle="Vertical activity timeline — toned icon nodes on a hairline spine" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x744" name="Toast" subtitle="Tones · with/without meta · live ToastStack demo" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x2771" name="Toggle Switch" subtitle="Binary settings pill — sizes · semantic red→green · glyph knobs · inline text · captions · async loading" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x499" name="Tooltip" subtitle="Placements — top · bottom · left · right, on icon and text triggers" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x3171" name="Tree" subtitle="Cascading checkboxes · disabled nodes · lines · async load · search · drag · reorder · keyboard" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css"><link rel="stylesheet" href="../../tokens/hover.css"><link rel="stylesheet" href="../_shared/card-kit.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -94,6 +94,7 @@ export { default as Combobox } from "../forms/Combobox.vue";
|
|
|
94
94
|
export { default as DatePicker } from "../forms/DatePicker.vue";
|
|
95
95
|
export { default as Dropzone } from "../forms/Dropzone.vue";
|
|
96
96
|
export { default as Field } from "../forms/Field.vue";
|
|
97
|
+
export { default as GlobalSearch } from "../forms/GlobalSearch.vue";
|
|
97
98
|
export { default as Input } from "../forms/Input.vue";
|
|
98
99
|
export { default as MultiSelect } from "../forms/MultiSelect.vue";
|
|
99
100
|
export { default as NumberStepper } from "../forms/NumberStepper.vue";
|
|
@@ -150,3 +151,4 @@ export { PIPELINE_CATALOG } from "../pipeline/pipeline-catalog.js";
|
|
|
150
151
|
export { VUE_ICON_PATHS } from "../icons/icon-paths.js";
|
|
151
152
|
export { FEATHER_PATHS } from "../icons/feather-paths.js";
|
|
152
153
|
export { LUCIDE_VERSION, loadLucide, setLucideSource } from "../icons/lucide-loader.js";
|
|
154
|
+
export { GLOBAL_SEARCH_LABELS, GS_ICONS, GS_KINDS } from "../forms/global-search.js";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Vue Button 与旧按钮适配
|
|
2
|
+
|
|
3
|
+
2.6.0(未发布)为 Vue Button 补充适配旧按钮所需的通用能力。
|
|
4
|
+
原有图标名、variant、默认插槽用法继续支持;本次没有修改 React Button。
|
|
5
|
+
本次只修改源码与文档,没有执行测试、类型检查、构建或浏览器验证,不能据此宣称已完美替换业务组件。
|
|
6
|
+
|
|
7
|
+
## 新增接口
|
|
8
|
+
|
|
9
|
+
| 接口 | 行为 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `@click` | 传递原生 MouseEvent;disabled 或 loading 时不发出事件 |
|
|
12
|
+
| `nativeType` | `button / submit / reset`,默认仍为 button |
|
|
13
|
+
| `text` | 默认插槽的后备文本,已有插槽优先 |
|
|
14
|
+
| `icon` | 原 DS 图标名之外,额外接受 Vue 组件或 VNode |
|
|
15
|
+
| `trailingIcon` | 独立右侧图标;loading 时继续显示,适合 popup 下拉箭头 |
|
|
16
|
+
| `#icon` | 覆盖主图标,可放旧 svgicon、字体图标或自定义 SVG;随 loading 隐藏 |
|
|
17
|
+
| `#loading` | 覆盖加载图标;默认继续使用 DS spinner |
|
|
18
|
+
| `#trailing` | 覆盖独立尾部内容,不随 loading 隐藏 |
|
|
19
|
+
| `aria-busy` | 与 loading 同步;原生 disabled 仍由 disabled 或 loading 控制 |
|
|
20
|
+
|
|
21
|
+
icon / loading 插槽接收 `{ size, style }`,trailing 额外接收 loading。
|
|
22
|
+
默认继承当前尺寸区;显式 iconSize 优先于尺寸区。自定义插槽中的图标由调用方应用传入的尺寸。
|
|
23
|
+
class、id、title、style、原生表单属性以及其他未声明属性继续透传到同一个原生 button 根节点。
|
|
24
|
+
Vue 专用类型文件为 `Button.vue.d.ts`,`Button.d.ts` 仍是 React 类型。
|
|
25
|
+
|
|
26
|
+
```vue
|
|
27
|
+
<template>
|
|
28
|
+
<KbButton
|
|
29
|
+
:loading="saving"
|
|
30
|
+
:icon="SaveIcon"
|
|
31
|
+
trailing-icon="chevron-down"
|
|
32
|
+
@click="save($event)"
|
|
33
|
+
>
|
|
34
|
+
保存
|
|
35
|
+
</KbButton>
|
|
36
|
+
</template>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## actnplanBtn 的适配边界
|
|
40
|
+
|
|
41
|
+
建议保留 `actnplanBtn.vue` 的路径、props、事件和兼容 class,内部改为 KbButton。
|
|
42
|
+
DS 接收通用接口;`regular / negative / warning / inbody / popup` 以及旧图标名由应用转换。
|
|
43
|
+
|
|
44
|
+
| 原配置 | 应用层转换 |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| regular | variant=primary |
|
|
47
|
+
| regular + inbody | variant=outline |
|
|
48
|
+
| negative | variant=outline;它通常表示取消,不是危险操作 |
|
|
49
|
+
| warning | variant=danger |
|
|
50
|
+
| disabled / loading | 同名属性,loading 时主图标替换为 spinner,文字保留 |
|
|
51
|
+
| popup | trailingIcon=chevron-down,loading 时仍显示箭头 |
|
|
52
|
+
| text | 保留 `<span class="text">` 默认插槽,兼容已有选择器 |
|
|
53
|
+
| click | 继续传出原生事件,不吞掉 `$event`、冒泡或表单默认行为 |
|
|
54
|
+
| class / id / style | 透传;保留 btn、actnplanButton、原 type class 和 inbody class |
|
|
55
|
+
|
|
56
|
+
旧 actnplanBtn 没有指定原生 button type,在表单里通常默认提交;严格兼容时适配层应设置 nativeType=submit,并核对各表单的实际使用。
|
|
57
|
+
不能把旧业务 type 直接绑定到原生 type。外部已经通过原生属性控制 form、name、value 的场景也应保留。
|
|
58
|
+
|
|
59
|
+
## 图标映射
|
|
60
|
+
|
|
61
|
+
映射应独立放在应用的 `actnplanButtonIcons.ts`,不要塞进 DS 的通用图标字典。
|
|
62
|
+
应用先从空格分隔的 class 中提取 glyphicon-*,因此 `glyphicon glyphicon-refresh` 与 `glyphicon-refresh` 进入相同映射。
|
|
63
|
+
|
|
64
|
+
| 旧图标 | 建议的 DS 图标 |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| 1em-ok / glyphicon-ok | check |
|
|
67
|
+
| 1em-cancel / glyphicon-remove | x |
|
|
68
|
+
| 1em-add / glyphicon-plus | plus |
|
|
69
|
+
| glyphicon-floppy-disk | save |
|
|
70
|
+
| 1em-delete / glyphicon-trash | trash-2 |
|
|
71
|
+
| 1em-refresh / glyphicon-refresh | refresh-cw |
|
|
72
|
+
| 1em-clone | copy |
|
|
73
|
+
| glyphicon-search | search |
|
|
74
|
+
| glyphicon-menu-down | chevron-down |
|
|
75
|
+
|
|
76
|
+
已经确认对应关系的图标转换为 DS 名称。业务专用 SVG 或无可靠对应的 Glyphicon 通过 icon 插槽保留;不要把未知旧名称直接传入 DS Icon,否则会显示空图标。
|
|
77
|
+
动态 icon 使用 computed 解析;旧 SVG 使用 watch 加载并忽略过期请求,避免切换按钮状态后显示旧图标。
|
|
78
|
+
旧 fill 样式仅作用于旧 SVG,DS 图标保留 fill=none、stroke=currentColor。
|
|
79
|
+
|
|
80
|
+
```vue
|
|
81
|
+
<template>
|
|
82
|
+
<KbButton
|
|
83
|
+
class="btn actnplanButton"
|
|
84
|
+
:class="[type, { inbody }]"
|
|
85
|
+
:variant="variant"
|
|
86
|
+
:disabled="disabled"
|
|
87
|
+
:loading="loading"
|
|
88
|
+
:trailing-icon="popup ? 'chevron-down' : undefined"
|
|
89
|
+
native-type="submit"
|
|
90
|
+
@click="emit('click', $event)"
|
|
91
|
+
>
|
|
92
|
+
<template v-if="icon" #icon="{ size, style }">
|
|
93
|
+
<AppIcon v-if="mappedIcon" :name="mappedIcon" :size="size" :style="style" />
|
|
94
|
+
<span v-else-if="isGlyph" class="glyphicon" :class="glyphClass" aria-hidden="true" />
|
|
95
|
+
<svgicon v-else-if="svgData" :data="svgData" :fill="false" :style="style" />
|
|
96
|
+
</template>
|
|
97
|
+
<span class="text">{{ text }}</span>
|
|
98
|
+
</KbButton>
|
|
99
|
+
</template>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
以上为应用适配层的模板示意,其中 variant、mappedIcon 等由应用解析。
|
|
103
|
+
图标放在按钮内部即可,不需要 svgContainer,避免把一个按钮拆成多个点击区域。
|
|
104
|
+
|
|
105
|
+
## 接入顺序
|
|
106
|
+
|
|
107
|
+
先发布包含本次改动的 DS,再升级应用安装版本,最后修改 actnplanBtn 适配层并移除旧的按钮视觉样式。
|
|
108
|
+
应用所有版本都需要加载 DS 样式,不能只依赖 V2 的适配器注册入口。
|
|
109
|
+
同时清理 runtime/modal.vue 中临时添加的默认按钮视觉覆盖,让按钮外观由 DS 统一管理;默认按钮位置可继续由 modal 决定。
|
|
110
|
+
客户主题 mixin 和外部 `.actnplanButton` 规则需要逐项处理,不能继续用旧的 disabled / fill / hover 规则覆盖新按钮。
|
|
111
|
+
源码修改不会自动影响应用已安装的旧版本;本轮未切换 actnplanBtn,也未修改应用依赖、lock 文件或生成产物。
|
|
@@ -3,14 +3,16 @@ import * as React from "react";
|
|
|
3
3
|
* Keybricks button — flat, hairline. Height, text, icon and padding come from the size tier of the region it sits in.
|
|
4
4
|
*/
|
|
5
5
|
export interface ButtonProps {
|
|
6
|
-
/** primary (blue #155dfc) | dark (#171717, e.g. "AI
|
|
7
|
-
variant?: "primary" | "dark" | "outline" | "ghost" | "primary-soft" | "danger" | "danger-soft" | "text";
|
|
6
|
+
/** primary (blue #155dfc) | dark (#171717, e.g. "Publish" — never for AI; use ai) | outline | ghost | primary-soft (blue tint toggle, e.g. "Data Preview") | danger (solid red, destructive) | danger-soft (red tint, e.g. "Remove") | text (no background/border ever — underlines on hover, e.g. "View all", "Cancel") | ai (Prophet AI / 方知 entry points ONLY — animated 1px gradient ring, gradient tint + glow on hover; icon defaults to "prophet-sparkle"; at most one per region) */
|
|
7
|
+
variant?: "primary" | "dark" | "outline" | "ghost" | "primary-soft" | "danger" | "danger-soft" | "text" | "ai";
|
|
8
8
|
/** Size tier: sm 28px/12px text (13px in CJK) · md 32px/14px · lg 40px/14px. Omit to inherit the tier of the enclosing region
|
|
9
9
|
* (toolbar, form, table, dialog — or the app root), which is the default and the normal case: controls that share
|
|
10
10
|
* a row must share a tier. Set it only on a control that stands alone. */
|
|
11
11
|
size?: "sm" | "md" | "lg";
|
|
12
|
-
/** Icon glyph name */
|
|
13
|
-
icon?: string;
|
|
12
|
+
/** Icon glyph name, or your own element (Vue: name, component or VNode; #icon slot). variant="ai" takes a glyph name only. */
|
|
13
|
+
icon?: string | React.ReactNode;
|
|
14
|
+
/** Separate trailing mark (e.g. "chevron-down" on a menu button); stays visible while loading. Vue: also #trailing slot. */
|
|
15
|
+
trailingIcon?: string | React.ReactNode;
|
|
14
16
|
/** Overrides the tier's icon size (14 at sm, 16 at md/lg) */
|
|
15
17
|
iconSize?: number;
|
|
16
18
|
/** Where `icon` renders relative to the label (default "left") */
|
|
@@ -26,11 +28,18 @@ export interface ButtonProps {
|
|
|
26
28
|
iconOnly?: boolean;
|
|
27
29
|
/** aria-label, required when iconOnly */
|
|
28
30
|
label?: string;
|
|
31
|
+
/** Fallback label when there are no children */
|
|
32
|
+
text?: string;
|
|
33
|
+
/** Native button type (default "button"); "submit" for legacy forms */
|
|
34
|
+
nativeType?: "button" | "submit" | "reset";
|
|
35
|
+
/** Force the hover styling — for specimens */
|
|
36
|
+
hovered?: boolean;
|
|
29
37
|
/** Force the press (mouse-down) styling — for specimens and controlled toggles */
|
|
30
38
|
pressed?: boolean;
|
|
31
39
|
/** Force the keyboard-focus ring — for specimens */
|
|
32
40
|
focused?: boolean;
|
|
33
|
-
|
|
41
|
+
/** Receives the native event; not called while disabled or loading */
|
|
42
|
+
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
34
43
|
children?: React.ReactNode;
|
|
35
44
|
style?: React.CSSProperties;
|
|
36
45
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Icon } from "../icons/Icon.jsx";
|
|
3
|
+
import { aiStyles, aiSpinDelay, rampAiSpin } from "./ai-button.js";
|
|
3
4
|
|
|
4
5
|
/* Every dimension comes from the size tier the button sits in (tokens/layout.css): height, text, icon and padding
|
|
5
6
|
switch together when a region declares data-size="sm" | "md" | "lg". A `size` prop only stamps that attribute on
|
|
@@ -35,14 +36,50 @@ function Spinner({ size, color }) {
|
|
|
35
36
|
return <span style={{ display: "inline-block", width: size, height: size, ...(size ? null : tierIcon), borderRadius: "var(--radius-full)", border: "2px solid currentColor", borderTopColor: "transparent", opacity: .9, color, animation: "kb-spin .7s linear infinite", flexShrink: 0 }} />;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
39
|
+
/** variant="ai" — Prophet AI (方知) entry points only. Same box as any Button at the tier; see ai-button.js. */
|
|
40
|
+
function AiButton({ size, icon, trailing, nativeType, iconSize, medium, fullWidth, iconOnly, label, busy, loading, hovered, isPressed, isFocused, setHover, setPress, setFocus, children, style, onClick, rest }) {
|
|
41
|
+
if (typeof icon !== "string" || !icon) icon = "prophet-sparkle";
|
|
42
|
+
const ref = React.useRef(null);
|
|
43
|
+
const delay = React.useRef(aiSpinDelay()).current;
|
|
44
|
+
const on = !busy && hovered;
|
|
45
|
+
React.useEffect(() => rampAiSpin(ref.current, on), [on]);
|
|
46
|
+
const a = aiStyles({ on, press: !busy && isPressed, busy, focused: isFocused, medium, fullWidth, iconOnly, delay, style });
|
|
47
|
+
const sz = iconSize || 16, tier = iconSize ? null : tierIcon;
|
|
48
|
+
return (
|
|
49
|
+
<button ref={ref} type={nativeType} data-press="own" data-size={size} style={a.outer} disabled={busy} aria-busy={loading || undefined} aria-label={iconOnly ? label : undefined} onClick={onClick}
|
|
50
|
+
onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}
|
|
51
|
+
onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}
|
|
52
|
+
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") setPress(true); }} onKeyUp={() => setPress(false)}
|
|
53
|
+
onFocus={(e) => { try { setFocus(e.target.matches(":focus-visible")); } catch (err) { setFocus(true); } }} onBlur={() => setFocus(false)} {...rest}>
|
|
54
|
+
<span aria-hidden="true" style={a.glow} />
|
|
55
|
+
<span aria-hidden="true" style={a.ring} />
|
|
56
|
+
<span style={a.surface}>
|
|
57
|
+
<span aria-hidden="true" style={a.tint} />
|
|
58
|
+
{loading ? <span style={a.iconBox}><Spinner size={iconSize} /></span> : icon ? (
|
|
59
|
+
<span style={a.iconBox}>
|
|
60
|
+
<Icon name={icon} size={sz} style={{ ...tier, ...a.iconRest }} />
|
|
61
|
+
<Icon name={icon} size={sz} gradient style={{ ...tier, ...a.iconGrad }} />
|
|
62
|
+
</span>
|
|
63
|
+
) : null}
|
|
64
|
+
{iconOnly ? null : <span style={a.label}>{children}</span>}
|
|
65
|
+
{trailing}
|
|
66
|
+
</span>
|
|
67
|
+
</button>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Keybricks button. variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text | ai; size: sm 28 | md 32 | lg 40 — omit it to inherit the region's tier (the default). Interaction states: hover (one step darker) · press (two steps, set via mouse-down) · keyboard focus ring · loading · disabled. */
|
|
72
|
+
export function Button({ variant = "primary", size, icon, trailingIcon, iconSize, iconPosition = "left", medium, disabled, loading, fullWidth, iconOnly, label, text, nativeType = "button", hovered, pressed, focused, children, style, onClick, ...rest }) {
|
|
40
73
|
const [hover, setHover] = React.useState(false);
|
|
41
74
|
const [press, setPress] = React.useState(false);
|
|
42
75
|
const [focus, setFocus] = React.useState(false);
|
|
43
76
|
const busy = disabled || loading;
|
|
44
77
|
const isPressed = press || pressed;
|
|
45
78
|
const isFocused = focus || focused;
|
|
79
|
+
const glyph = (g) => (typeof g === "string" ? <Icon name={g} size={iconSize || 16} style={iconSize ? null : tierIcon} /> : g || null);
|
|
80
|
+
const tail = trailingIcon ? glyph(trailingIcon) : null;
|
|
81
|
+
const label_ = children ?? text;
|
|
82
|
+
if (variant === "ai") return <AiButton size={size} icon={icon} trailing={tail} nativeType={nativeType} iconSize={iconSize} medium={medium} fullWidth={fullWidth} iconOnly={iconOnly} label={label} busy={busy} loading={loading} hovered={hover || hovered} isPressed={isPressed} isFocused={isFocused} setHover={setHover} setPress={setPress} setFocus={setFocus} style={style} onClick={onClick} rest={rest}>{label_}</AiButton>;
|
|
46
83
|
const s = {
|
|
47
84
|
...btnBase, ...variants[variant],
|
|
48
85
|
...(medium ? { fontWeight: "var(--weight-medium)" } : null),
|
|
@@ -57,23 +94,23 @@ export function Button({ variant = "primary", size, icon, iconSize, iconPosition
|
|
|
57
94
|
if (isPressed) s.opacity = .7;
|
|
58
95
|
} else if (isPressed) {
|
|
59
96
|
s.background = presses[variant];
|
|
60
|
-
} else if (hover) {
|
|
97
|
+
} else if (hover || hovered) {
|
|
61
98
|
s.background = hovers[variant];
|
|
62
99
|
}
|
|
63
100
|
if (isFocused) s.boxShadow = "var(--ring-focus)";
|
|
64
101
|
}
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const trailing = !loading && icon && iconPosition === "right" ? <Icon name={icon} size={iconSize || 16} style={iconStyle} /> : null;
|
|
102
|
+
const leading = loading ? <Spinner size={iconSize} color={s.color} /> : (icon && iconPosition === "left" ? glyph(icon) : null);
|
|
103
|
+
const trailing = !loading && icon && iconPosition === "right" ? glyph(icon) : null;
|
|
68
104
|
return (
|
|
69
|
-
<button type=
|
|
105
|
+
<button type={nativeType} data-press="own" data-size={size} style={s} disabled={busy} aria-busy={loading || undefined} aria-label={iconOnly ? label : undefined} onClick={onClick}
|
|
70
106
|
onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}
|
|
71
107
|
onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}
|
|
72
108
|
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") setPress(true); }} onKeyUp={() => setPress(false)}
|
|
73
109
|
onFocus={(e) => { try { setFocus(e.target.matches(":focus-visible")); } catch (err) { setFocus(true); } }} onBlur={() => setFocus(false)} {...rest}>
|
|
74
110
|
{leading}
|
|
75
|
-
{iconOnly ? null :
|
|
111
|
+
{iconOnly ? null : label_}
|
|
76
112
|
{trailing}
|
|
113
|
+
{tail}
|
|
77
114
|
</button>
|
|
78
115
|
);
|
|
79
116
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { px } from '../px.js'
|
|
3
|
-
import { computed, ref } from 'vue'
|
|
3
|
+
import { cloneVNode, computed, h, isVNode, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
4
4
|
import Icon from '../icons/Icon.vue'
|
|
5
|
+
import { aiStyles, aiSpinDelay, rampAiSpin } from './ai-button.js'
|
|
5
6
|
|
|
6
7
|
// Every dimension comes from the size tier the button sits in (tokens/layout.css): height, text, icon and padding
|
|
7
8
|
// switch together when a region declares data-size="sm" | "md" | "lg". The `size` prop only stamps that attribute on
|
|
@@ -22,14 +23,16 @@ const hovers = { primary: 'var(--action-primary-hover)', dark: 'var(--action-dar
|
|
|
22
23
|
const presses = { primary: 'var(--action-primary-active)', dark: 'var(--action-dark-active)', outline: 'var(--bg-press)', ghost: 'var(--bg-press)', 'primary-soft': 'var(--soft-active)', danger: 'var(--danger-active)', 'danger-soft': 'var(--danger-soft-active)' }
|
|
23
24
|
const colorHoverVariants = { text: 'var(--text-heading)' }
|
|
24
25
|
|
|
25
|
-
// Button — variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm 28 | md 32 | lg 40 — omit it to inherit the region's tier (the default).
|
|
26
|
+
// Button — variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text | ai (Prophet AI entry points only); size: sm 28 | md 32 | lg 40 — omit it to inherit the region's tier (the default).
|
|
26
27
|
const props = defineProps({
|
|
27
28
|
variant: { type: String, default: 'primary' }, size: String,
|
|
28
|
-
icon: String,
|
|
29
|
+
icon: [String, Object, Function], trailingIcon: [String, Object, Function],
|
|
30
|
+
iconSize: Number, iconPosition: { type: String, default: 'left' },
|
|
29
31
|
medium: Boolean, disabled: Boolean, loading: Boolean, fullWidth: Boolean, iconOnly: Boolean,
|
|
30
|
-
label: String, pressed: Boolean, focused: Boolean, style: Object,
|
|
32
|
+
label: String, text: String, hovered: Boolean, pressed: Boolean, focused: Boolean, style: Object,
|
|
33
|
+
nativeType: { type: String, default: 'button', validator: value => ['button', 'submit', 'reset'].includes(value) },
|
|
31
34
|
})
|
|
32
|
-
defineEmits(['click'])
|
|
35
|
+
const emit = defineEmits(['click'])
|
|
33
36
|
const hover = ref(false), press = ref(false), focus = ref(false)
|
|
34
37
|
const busy = computed(() => props.disabled || props.loading)
|
|
35
38
|
const isPressed = computed(() => press.value || props.pressed)
|
|
@@ -45,22 +48,68 @@ const s = computed(() => {
|
|
|
45
48
|
if (hover.value || isPressed.value) { base.color = colorHoverVariants[props.variant]; base.textDecoration = 'underline' }
|
|
46
49
|
if (isPressed.value) base.opacity = .7
|
|
47
50
|
} else if (isPressed.value) base.background = presses[props.variant]
|
|
48
|
-
else if (hover.value) base.background = hovers[props.variant]
|
|
51
|
+
else if (hover.value || props.hovered) base.background = hovers[props.variant]
|
|
49
52
|
if (isFocused.value) base.boxShadow = 'var(--ring-focus)'
|
|
50
53
|
}
|
|
51
54
|
return base
|
|
52
55
|
})
|
|
53
56
|
const iSize = computed(() => props.iconSize || 16)
|
|
54
57
|
const iconStyle = computed(() => props.iconSize ? null : tierIcon)
|
|
58
|
+
|
|
59
|
+
// 旧 SVG、组件和 VNode 都可以由适配层传入,业务图标名称不进入 DS。
|
|
60
|
+
function glyph(value) {
|
|
61
|
+
if (!value) return null
|
|
62
|
+
const attrs = {
|
|
63
|
+
size: iSize.value, width: iSize.value, height: iSize.value, 'aria-hidden': true, focusable: false,
|
|
64
|
+
style: { display: 'block', flexShrink: 0, width: px(iSize.value), height: px(iSize.value), ...iconStyle.value },
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === 'string') return h(Icon, { ...attrs, name: value, size: iSize.value })
|
|
67
|
+
return isVNode(value) ? cloneVNode(value, attrs) : h(value, attrs)
|
|
68
|
+
}
|
|
69
|
+
const LeadingIcon = () => glyph(props.icon)
|
|
70
|
+
const TrailingIcon = () => glyph(props.trailingIcon)
|
|
71
|
+
|
|
72
|
+
function onClick(event) {
|
|
73
|
+
if (busy.value) return
|
|
74
|
+
emit('click', event)
|
|
75
|
+
}
|
|
76
|
+
// variant="ai" — see ai-button.js
|
|
77
|
+
const aiEl = ref(null), aiDelay = aiSpinDelay()
|
|
78
|
+
const aiOn = computed(() => !busy.value && (hover.value || props.hovered))
|
|
79
|
+
const ai = computed(() => aiStyles({ on: aiOn.value, press: !busy.value && isPressed.value, busy: busy.value, focused: isFocused.value, medium: props.medium, fullWidth: props.fullWidth, iconOnly: props.iconOnly, delay: aiDelay, style: props.style }))
|
|
80
|
+
const aiIcon = computed(() => (typeof props.icon === 'string' && props.icon) || 'prophet-sparkle')
|
|
81
|
+
let stopRamp = () => {}
|
|
82
|
+
const ramp = (on) => { stopRamp(); stopRamp = rampAiSpin(aiEl.value, on) }
|
|
83
|
+
watch(aiOn, ramp)
|
|
84
|
+
onMounted(() => { if (aiOn.value) ramp(true) })
|
|
85
|
+
onBeforeUnmount(() => stopRamp())
|
|
55
86
|
</script>
|
|
56
87
|
<template>
|
|
57
|
-
<button type="
|
|
58
|
-
@click="
|
|
88
|
+
<button v-if="variant === 'ai'" ref="aiEl" :type="nativeType" data-press="own" :data-size="size" :style="ai.outer" :disabled="busy" :aria-busy="loading" :aria-label="iconOnly ? label : undefined"
|
|
89
|
+
@click="onClick" @mouseenter="hover = true" @mouseleave="hover = false; press = false"
|
|
90
|
+
@mousedown="press = true" @mouseup="press = false" @keydown.enter="press = true" @keydown.space="press = true" @keyup="press = false"
|
|
91
|
+
@focus="focus = $event.target.matches(':focus-visible')" @blur="focus = false">
|
|
92
|
+
<span aria-hidden="true" :style="ai.glow" />
|
|
93
|
+
<span aria-hidden="true" :style="ai.ring" />
|
|
94
|
+
<span :style="ai.surface">
|
|
95
|
+
<span aria-hidden="true" :style="ai.tint" />
|
|
96
|
+
<span v-if="loading" :style="ai.iconBox"><slot name="loading" :size="iSize" :style="iconStyle"><span aria-hidden="true" :style="{ display: 'inline-block', width: px(iSize), height: px(iSize), ...iconStyle, borderRadius: 'var(--radius-full)', border: '2px solid currentColor', borderTopColor: 'transparent', opacity: .9, animation: 'kb-spin .7s linear infinite', flexShrink: 0 }" /></slot></span>
|
|
97
|
+
<span v-else :style="ai.iconBox">
|
|
98
|
+
<Icon :name="aiIcon" :size="iSize" :style="{ ...iconStyle, ...ai.iconRest }" />
|
|
99
|
+
<Icon :name="aiIcon" :size="iSize" gradient :style="{ ...iconStyle, ...ai.iconGrad }" />
|
|
100
|
+
</span>
|
|
101
|
+
<span v-if="!iconOnly" :style="ai.label"><slot>{{ text }}</slot></span>
|
|
102
|
+
<slot name="trailing" :size="iSize" :style="iconStyle" :loading="loading"><TrailingIcon v-if="trailingIcon" /></slot>
|
|
103
|
+
</span>
|
|
104
|
+
</button>
|
|
105
|
+
<button v-else :type="nativeType" data-press="own" :data-size="size" :style="s" :disabled="busy" :aria-busy="loading" :aria-label="iconOnly ? label : undefined"
|
|
106
|
+
@click="onClick" @mouseenter="hover = true" @mouseleave="hover = false; press = false"
|
|
59
107
|
@mousedown="press = true" @mouseup="press = false" @keydown.enter="press = true" @keydown.space="press = true" @keyup="press = false"
|
|
60
108
|
@focus="focus = $event.target.matches(':focus-visible')" @blur="focus = false">
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
<slot v-if="!iconOnly"
|
|
64
|
-
<
|
|
109
|
+
<slot v-if="loading" name="loading" :size="iSize" :style="iconStyle"><span aria-hidden="true" :style="{ display: 'inline-block', width: px(iSize), height: px(iSize), ...iconStyle, borderRadius: 'var(--radius-full)', border: '2px solid currentColor', borderTopColor: 'transparent', opacity: .9, animation: 'kb-spin .7s linear infinite', flexShrink: 0 }" /></slot>
|
|
110
|
+
<slot v-else-if="iconPosition === 'left'" name="icon" :size="iSize" :style="iconStyle"><LeadingIcon v-if="icon" /></slot>
|
|
111
|
+
<slot v-if="!iconOnly">{{ text }}</slot>
|
|
112
|
+
<slot v-if="!loading && iconPosition === 'right'" name="icon" :size="iSize" :style="iconStyle"><LeadingIcon v-if="icon" /></slot>
|
|
113
|
+
<slot name="trailing" :size="iSize" :style="iconStyle" :loading="loading"><TrailingIcon v-if="trailingIcon" /></slot>
|
|
65
114
|
</button>
|
|
66
115
|
</template>
|