@keyfunc/keybricks-ds 2.6.0 → 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.
Files changed (155) hide show
  1. package/DEPLOY.md +3 -3
  2. package/DESIGN.md +13 -4
  3. package/INTEGRATION.md +18 -3
  4. package/_adherence.oxlintrc.json +57 -11
  5. package/_ds_bundle.js +2137 -186
  6. package/_scripts/build-index.mjs +1 -1
  7. package/_src/icon-index.js +1 -1
  8. package/assets/icons/icon-index.json +1 -1
  9. package/changelog.js +11 -1
  10. package/components/_cards/accordion.card.html +1 -1
  11. package/components/_cards/action-chip.card.html +1 -1
  12. package/components/_cards/activity.card.html +1 -1
  13. package/components/_cards/alert.card.html +1 -1
  14. package/components/_cards/app-chrome.card.html +1 -1
  15. package/components/_cards/banner.card.html +1 -1
  16. package/components/_cards/breadcrumb.card.html +1 -1
  17. package/components/_cards/button-1-base.card.html +5 -2
  18. package/components/_cards/button-2-states.card.html +5 -5
  19. package/components/_cards/button-3-sizes-icons.card.html +2 -2
  20. package/components/_cards/button-4-group.card.html +2 -2
  21. package/components/_cards/button-5-ai.card.html +34 -0
  22. package/components/_cards/canvas-zoom-controls.card.html +1 -1
  23. package/components/_cards/cards-indicators.card.html +1 -1
  24. package/components/_cards/charts-1-sparkline.card.html +1 -1
  25. package/components/_cards/charts-10-gauge.card.html +1 -1
  26. package/components/_cards/charts-11-funnel.card.html +1 -1
  27. package/components/_cards/charts-12-treemap.card.html +1 -1
  28. package/components/_cards/charts-14-radar.card.html +1 -1
  29. package/components/_cards/charts-15-distribution.card.html +1 -1
  30. package/components/_cards/charts-16-pareto.card.html +1 -1
  31. package/components/_cards/charts-17-candlestick.card.html +1 -1
  32. package/components/_cards/charts-18-sankey.card.html +1 -1
  33. package/components/_cards/charts-19-chord.card.html +1 -1
  34. package/components/_cards/charts-2-line.card.html +1 -1
  35. package/components/_cards/charts-20-sunburst.card.html +1 -1
  36. package/components/_cards/charts-21-worldmap.card.html +1 -1
  37. package/components/_cards/charts-22-gantt.card.html +1 -1
  38. package/components/_cards/charts-23-bar3d.card.html +1 -1
  39. package/components/_cards/charts-24-pie3d.card.html +1 -1
  40. package/components/_cards/charts-25-surface3d.card.html +1 -1
  41. package/components/_cards/charts-26-scatter3d.card.html +1 -1
  42. package/components/_cards/charts-27-ribbon3d.card.html +1 -1
  43. package/components/_cards/charts-3-bar.card.html +1 -1
  44. package/components/_cards/charts-4-barlist.card.html +1 -1
  45. package/components/_cards/charts-5-donut.card.html +1 -1
  46. package/components/_cards/charts-6-scatter.card.html +1 -1
  47. package/components/_cards/charts-7-heatmap.card.html +1 -1
  48. package/components/_cards/charts-8-waterfall.card.html +1 -1
  49. package/components/_cards/charts-9-bullet.card.html +1 -1
  50. package/components/_cards/charts-99-fullpage.card.html +1 -1
  51. package/components/_cards/checkbox.card.html +1 -1
  52. package/components/_cards/codeblock.card.html +1 -1
  53. package/components/_cards/combobox.card.html +1 -1
  54. package/components/_cards/context-menu.card.html +1 -1
  55. package/components/_cards/date-picker.card.html +1 -1
  56. package/components/_cards/diffvalue.card.html +1 -1
  57. package/components/_cards/divider.card.html +1 -1
  58. package/components/_cards/drawer.card.html +1 -1
  59. package/components/_cards/dropdown-menu.card.html +1 -1
  60. package/components/_cards/dropzone.card.html +1 -1
  61. package/components/_cards/empty-state.card.html +1 -1
  62. package/components/_cards/filter-bar.card.html +1 -1
  63. package/components/_cards/icon-button.card.html +1 -1
  64. package/components/_cards/icons-0-usage.card.html +2 -2
  65. package/components/_cards/icons-1-core.card.html +1 -1
  66. package/components/_cards/icons-2-feather.card.html +1 -1
  67. package/components/_cards/icons-2b-lucide.card.html +1 -1
  68. package/components/_cards/icons-3-unicons.card.html +1 -1
  69. package/components/_cards/icons-4-datasource.card.html +1 -1
  70. package/components/_cards/input.card.html +1 -1
  71. package/components/_cards/kbd.card.html +5 -7
  72. package/components/_cards/keyvalue.card.html +1 -1
  73. package/components/_cards/listrow.card.html +1 -1
  74. package/components/_cards/loading.card.html +1 -1
  75. package/components/_cards/log-viewer.card.html +1 -1
  76. package/components/_cards/multi-select.card.html +1 -1
  77. package/components/_cards/number-stepper.card.html +1 -1
  78. package/components/_cards/pagination.card.html +1 -1
  79. package/components/_cards/people.card.html +1 -1
  80. package/components/_cards/pipeline-node.card.html +1 -1
  81. package/components/_cards/pipeline-palette.card.html +1 -1
  82. package/components/_cards/radio.card.html +1 -1
  83. package/components/_cards/relative-time-picker.card.html +1 -1
  84. package/components/_cards/search-global.card.html +72 -0
  85. package/components/_cards/search.card.html +1 -1
  86. package/components/_cards/segmented-control.card.html +1 -1
  87. package/components/_cards/select.card.html +1 -1
  88. package/components/_cards/slider.card.html +1 -1
  89. package/components/_cards/smart-table.card.html +1 -1
  90. package/components/_cards/spreadsheet.card.html +1 -1
  91. package/components/_cards/stepper.card.html +1 -1
  92. package/components/_cards/table.card.html +1 -1
  93. package/components/_cards/tabs.card.html +1 -1
  94. package/components/_cards/tag.card.html +1 -1
  95. package/components/_cards/textarea.card.html +1 -1
  96. package/components/_cards/timeline.card.html +1 -1
  97. package/components/_cards/toast.card.html +1 -1
  98. package/components/_cards/toggles.card.html +1 -1
  99. package/components/_cards/tooltip.card.html +1 -1
  100. package/components/_cards/tree.card.html +1 -1
  101. package/components/_vue/index.mjs +2 -0
  102. package/components/buttons/Button.d.ts +14 -5
  103. package/components/buttons/Button.jsx +45 -8
  104. package/components/buttons/Button.vue +40 -38
  105. package/components/buttons/Button.vue.d.ts +4 -1
  106. package/components/buttons/ai-button.js +54 -0
  107. package/components/buttons/buttons.prompt.md +7 -2
  108. package/components/display/Kbd.d.ts +3 -1
  109. package/components/display/Kbd.jsx +8 -3
  110. package/components/display/Kbd.vue +7 -3
  111. package/components/display/display.prompt.md +3 -1
  112. package/components/feedback/Modal.d.ts +7 -0
  113. package/components/feedback/Modal.jsx +9 -9
  114. package/components/feedback/Toast.vue +6 -6
  115. package/components/feedback/feedback.prompt.md +2 -0
  116. package/components/forms/GlobalSearch.d.ts +83 -0
  117. package/components/forms/GlobalSearch.jsx +172 -0
  118. package/components/forms/GlobalSearch.vue +161 -0
  119. package/components/forms/forms.prompt.md +7 -0
  120. package/components/forms/global-search.js +161 -0
  121. package/components/icons/Icon.d.ts +2 -0
  122. package/components/icons/Icon.jsx +14 -6
  123. package/components/icons/Icon.vue +11 -4
  124. package/components/icons/icon-paths.js +4 -2
  125. package/components/index.d.ts +1 -0
  126. package/components/index.js +1 -0
  127. package/components/navigation/TopBar.d.ts +9 -3
  128. package/components/navigation/TopBar.jsx +5 -7
  129. package/components/navigation/TopBar.vue +5 -6
  130. package/covers.js +4 -2
  131. package/gallery.html +1 -1
  132. package/guidelines/brand-logo.html +1 -1
  133. package/guidelines/colors-schemes.html +1 -1
  134. package/guidelines/colors-transparency.html +1 -1
  135. package/guidelines/foundations-borders.html +1 -1
  136. package/guidelines/foundations-chrome.html +1 -1
  137. package/guidelines/foundations-motion.html +1 -1
  138. package/guidelines/foundations-size-tiers.html +1 -1
  139. package/guidelines/foundations-spacing.html +1 -1
  140. package/guidelines/interaction-states.html +1 -1
  141. package/guidelines/type-extended.html +1 -1
  142. package/guidelines/type-mono.html +1 -1
  143. package/guidelines/type-scale.html +1 -1
  144. package/guidelines/type-weights.html +1 -1
  145. package/index.html +8 -8
  146. package/package.json +2 -2
  147. package/readme.md +4 -4
  148. package/readme.zh-CN.md +4 -4
  149. package/site-nav.js +1 -1
  150. package/styles.css +1 -0
  151. package/tokens/ai.css +17 -0
  152. package/tokens/colors.css +1 -1
  153. package/tokens/tokens.d.ts +28 -0
  154. package/tokens/tokens.js +47 -5
  155. 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="700x770" name="Search Input" subtitle="Bordered · bare top-bar · with value/clear · disabled" -->
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="700x1880" name="Segmented Control" subtitle="Mutually exclusive views in one inset track — md/sm · counts · icons · icon-only · disabled option · full width · ViewToggle" -->
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="700x550" name="Select" subtitle="md / sm · placeholder · disabled · error" -->
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="700x300" name="Slider" subtitle="Filled track · ring knob · value bubble · min/max labels" -->
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="700x1470" name="Smart Table (Database Grid)" subtitle="Frozen header and row numbers · default and compact density · typed fields · hue pills · aggregates · + New" -->
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="924x600" name="Spreadsheet" subtitle="Formula bar · frozen headers · range selection · =SUM() · sheet tabs · status bar" -->
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="700x630" name="Stepper" subtitle="Multi-step flow header — done · current · upcoming" -->
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="700x11200" name="Table" subtitle="Anatomy · densities · cell types (display · editable · per tier) · two-line name · sorting · selection · frames · column resize · states · composition" -->
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="700x1130" name="Tabs" subtitle="Underline tabs · panel tabs · pill filter tabs · counts on every kind" -->
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="700x450" name="Tag" subtitle="Removable filter tags — distinct from Badge/ActionChip" -->
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="924x680" name="Textarea" subtitle="Default · character count · over the limit · disabled" -->
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="700x390" name="Timeline" subtitle="Vertical activity timeline — toned icon nodes on a hairline spine" -->
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="700x800" name="Toast" subtitle="Tones · with/without meta · live ToastStack demo" -->
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="700x2780" name="Toggle Switch" subtitle="Binary settings pill — sizes · semantic red→green · glyph knobs · inline text · captions · async loading" -->
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="700x510" name="Tooltip" subtitle="Placements — top · bottom · left · right, on icon and text triggers" -->
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="700x3170" name="Tree" subtitle="Cascading checkboxes · disabled nodes · lines · async load · search · drag · reorder · keyboard" -->
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";
@@ -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 Assistant") | 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") */
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
- onClick?: () => void;
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
- /** Keybricks 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). Interaction states: hover (one step darker) · press (two steps, set via mouse-down) · keyboard focus ring · loading · disabled. */
39
- export function Button({ variant = "primary", size, icon, iconSize, iconPosition = "left", medium, disabled, loading, fullWidth, iconOnly, label, pressed, focused, children, style, onClick, ...rest }) {
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 iconStyle = iconSize ? null : tierIcon;
66
- const leading = loading ? <Spinner size={iconSize} color={s.color} /> : (icon && iconPosition === "left" ? <Icon name={icon} size={iconSize || 16} style={iconStyle} /> : null);
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="button" data-press="own" data-size={size} style={s} disabled={busy} aria-label={iconOnly ? label : undefined} onClick={onClick}
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 : children}
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 { cloneVNode, computed, h, isVNode, 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,13 +23,13 @@ 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
29
  icon: [String, Object, Function], trailingIcon: [String, Object, Function],
29
30
  iconSize: Number, iconPosition: { type: String, default: 'left' },
30
31
  medium: Boolean, disabled: Boolean, loading: Boolean, fullWidth: Boolean, iconOnly: Boolean,
31
- label: String, text: String, pressed: Boolean, focused: Boolean, style: Object,
32
+ label: String, text: String, hovered: Boolean, pressed: Boolean, focused: Boolean, style: Object,
32
33
  nativeType: { type: String, default: 'button', validator: value => ['button', 'submit', 'reset'].includes(value) },
33
34
  })
34
35
  const emit = defineEmits(['click'])
@@ -47,7 +48,7 @@ const s = computed(() => {
47
48
  if (hover.value || isPressed.value) { base.color = colorHoverVariants[props.variant]; base.textDecoration = 'underline' }
48
49
  if (isPressed.value) base.opacity = .7
49
50
  } else if (isPressed.value) base.background = presses[props.variant]
50
- else if (hover.value) base.background = hovers[props.variant]
51
+ else if (hover.value || props.hovered) base.background = hovers[props.variant]
51
52
  if (isFocused.value) base.boxShadow = 'var(--ring-focus)'
52
53
  }
53
54
  return base
@@ -72,42 +73,43 @@ function onClick(event) {
72
73
  if (busy.value) return
73
74
  emit('click', event)
74
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())
75
86
  </script>
76
87
  <template>
77
- <button
78
- :type="nativeType"
79
- data-press="own"
80
- :data-size="size"
81
- :style="s"
82
- :disabled="busy"
83
- :aria-busy="loading"
84
- :aria-label="iconOnly ? label : undefined"
85
- @click="onClick"
86
- @mouseenter="hover = true"
87
- @mouseleave="hover = false; press = false"
88
- @mousedown="press = true"
89
- @mouseup="press = false"
90
- @keydown.enter="press = true"
91
- @keydown.space="press = true"
92
- @keyup="press = false"
93
- @focus="focus = $event.target.matches(':focus-visible')"
94
- @blur="focus = false"
95
- >
96
- <slot v-if="loading" name="loading" :size="iSize" :style="iconStyle">
97
- <span
98
- aria-hidden="true"
99
- :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 }"
100
- />
101
- </slot>
102
- <slot v-else-if="iconPosition === 'left'" name="icon" :size="iSize" :style="iconStyle">
103
- <LeadingIcon v-if="icon" />
104
- </slot>
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"
107
+ @mousedown="press = true" @mouseup="press = false" @keydown.enter="press = true" @keydown.space="press = true" @keyup="press = false"
108
+ @focus="focus = $event.target.matches(':focus-visible')" @blur="focus = false">
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>
105
111
  <slot v-if="!iconOnly">{{ text }}</slot>
106
- <slot v-if="!loading && iconPosition === 'right'" name="icon" :size="iSize" :style="iconStyle">
107
- <LeadingIcon v-if="icon" />
108
- </slot>
109
- <slot name="trailing" :size="iSize" :style="iconStyle" :loading="loading">
110
- <TrailingIcon v-if="trailingIcon" />
111
- </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>
112
114
  </button>
113
115
  </template>
@@ -2,7 +2,8 @@ import type { Component, ComponentOptionsMixin, CSSProperties, DefineComponent,
2
2
 
3
3
  /** Vue 专用声明;同目录 Button.d.ts 继续描述 React 接口。 */
4
4
  export interface VueButtonProps {
5
- variant?: 'primary' | 'dark' | 'outline' | 'ghost' | 'primary-soft' | 'danger' | 'danger-soft' | 'text';
5
+ /** ai:仅用于 Prophet AI(方知)入口;它的 icon 只接受 DS 图标名,默认 prophet-sparkle。 */
6
+ variant?: 'primary' | 'dark' | 'outline' | 'ghost' | 'primary-soft' | 'danger' | 'danger-soft' | 'text' | 'ai';
6
7
  size?: 'sm' | 'md' | 'lg';
7
8
  icon?: string | Component | VNode;
8
9
  /** 不随 loading 隐藏,适用于下拉箭头等独立的尾部标记。 */
@@ -17,6 +18,8 @@ export interface VueButtonProps {
17
18
  label?: string;
18
19
  /** 默认插槽的后备文本。 */
19
20
  text?: string;
21
+ /** 强制悬停样式,用于规范展示。 */
22
+ hovered?: boolean;
20
23
  pressed?: boolean;
21
24
  focused?: boolean;
22
25
  style?: CSSProperties;
@@ -0,0 +1,54 @@
1
+ /* Button variant="ai" — the Prophet AI (方知) entry treatment, shared by Button.jsx and Button.vue.
2
+ Same outer box as every Button at the tier (height, radius, padding); the 1px border is replaced by a 1px ring:
3
+ at rest a gradient comet travels round a neutral track; on hover the full ring, an 8% gradient tint, a soft glow and a
4
+ gradient icon fade in and the ring speeds up. Tokens: tokens/ai.css. One CSS animation (kb-ai-spin) drives the angle;
5
+ hover speed is ramped with Animation.playbackRate, so the angle never jumps. */
6
+ /* The animated angle needs a registered <angle> property and a keyframe on it — injected once, so the token files stay plain. */
7
+ if (typeof document !== "undefined" && !document.querySelector("style[data-kb-ai]")) {
8
+ const el = document.createElement("style");
9
+ el.setAttribute("data-kb-ai", "");
10
+ el.textContent = "@property --kb-ai-angle{syntax:'<angle>';inherits:true;initial-value:0deg}@keyframes kb-ai-spin{to{--kb-ai-angle:360deg}}";
11
+ document.head.appendChild(el);
12
+ }
13
+ const FADE = "opacity 300ms var(--ease-out)";
14
+ const FULL = "conic-gradient(from var(--kb-ai-angle), var(--ai-gradient-stops))";
15
+ const COMET = "conic-gradient(from var(--kb-ai-angle), var(--ai-ring-rest-track) 0 50%, var(--ai-gradient-start) 78%, var(--ai-gradient-end) 100%)";
16
+ const layer = { position: "absolute", inset: "0", borderRadius: "inherit", pointerEvents: "none", transition: FADE };
17
+
18
+ /** A random phase per instance, so neighbouring rings never move in lockstep. */
19
+ export const aiSpinDelay = () => "-" + (Math.random() * 5).toFixed(2) + "s";
20
+
21
+ /** on = hovered (and not busy). Lengths are strings so the same objects work in React and Vue. */
22
+ export function aiStyles({ on, press, busy, focused, medium, fullWidth, iconOnly, delay, style }) {
23
+ const tint = press ? "var(--ai-tint-press)" : "var(--ai-tint)";
24
+ return {
25
+ outer: { position: "relative", isolation: "isolate", display: fullWidth ? "flex" : "inline-flex", alignItems: "stretch", width: fullWidth ? "100%" : iconOnly ? "var(--control-h)" : undefined, height: "var(--control-h)", padding: "var(--ai-ring-width)", border: "none", borderRadius: "var(--radius-md)", boxSizing: "border-box", background: COMET, animation: "kb-ai-spin var(--ai-spin-rest) linear " + delay + " infinite", cursor: busy ? "default" : "pointer", opacity: busy ? .55 : undefined, boxShadow: focused && !busy ? "var(--ring-focus)" : undefined, fontFamily: "var(--font-sans)", fontWeight: medium ? "var(--weight-medium)" : "var(--weight-regular)", fontSize: "var(--control-text)", lineHeight: "var(--control-leading)", letterSpacing: "var(--control-tracking)", color: "var(--text-body)", whiteSpace: "nowrap", verticalAlign: "middle", ...style },
26
+ glow: { ...layer, zIndex: "-1", background: FULL, filter: "blur(var(--ai-glow-blur))", opacity: on ? "var(--ai-glow-opacity)" : "0" },
27
+ ring: { ...layer, background: FULL, opacity: on ? "1" : "0" },
28
+ surface: { position: "relative", flex: "1 1 auto", display: "flex", alignItems: "center", justifyContent: "center", gap: iconOnly ? "0" : "6px", padding: iconOnly ? "0" : "0 calc(var(--control-px) - var(--ai-ring-width))", borderRadius: "calc(var(--radius-md) - var(--ai-ring-width))", background: "var(--bg-surface)", overflow: "hidden" },
29
+ tint: { ...layer, background: "linear-gradient(90deg, color-mix(in srgb, var(--ai-gradient-start) " + tint + ", transparent), color-mix(in srgb, var(--ai-gradient-end) " + tint + ", transparent))", opacity: on || press ? "1" : "0" },
30
+ iconBox: { position: "relative", display: "inline-flex", flexShrink: "0" },
31
+ iconRest: { transition: FADE, opacity: on ? "0" : "1" },
32
+ iconGrad: { position: "absolute", inset: "0", transition: FADE, opacity: on ? "1" : "0" },
33
+ label: { position: "relative" },
34
+ };
35
+ }
36
+
37
+ const secs = (v, d) => { v = (v || "").trim(); const n = parseFloat(v); return isNaN(n) ? d : /ms$/.test(v) ? n / 1000 : n; };
38
+ /** Ease the ring's speed toward rest (fast=false) or hover (fast=true). Returns a cancel function. */
39
+ export function rampAiSpin(el, fast) {
40
+ if (!el || !el.getAnimations || (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches)) return () => {};
41
+ const anim = el.getAnimations().find((a) => a.animationName === "kb-ai-spin");
42
+ if (!anim) return () => {};
43
+ const cs = getComputedStyle(el);
44
+ const target = fast ? secs(cs.getPropertyValue("--ai-spin-rest"), 5) / secs(cs.getPropertyValue("--ai-spin-hover"), 1.2) : 1;
45
+ let raf = 0, last = performance.now();
46
+ const step = (t) => {
47
+ const dt = Math.min(0.05, (t - last) / 1000); last = t;
48
+ const r = anim.playbackRate + (target - anim.playbackRate) * Math.min(1, dt * 5);
49
+ if (Math.abs(target - r) < 0.01) { anim.playbackRate = target; return; }
50
+ anim.playbackRate = r; raf = requestAnimationFrame(step);
51
+ };
52
+ raf = requestAnimationFrame(step);
53
+ return () => cancelAnimationFrame(raf);
54
+ }
@@ -2,7 +2,8 @@ Buttons: `Button` (primary/dark/outline/ghost/primary-soft), `IconButton` (squar
2
2
 
3
3
  ```jsx
4
4
  <Button icon="send">Submit</Button>
5
- <Button variant="dark" icon="ai-sparkles">AI Assistant</Button>
5
+ <Button variant="dark" icon="upload-cloud">Publish</Button>
6
+ <Button variant="ai">Ask Prophet AI</Button> // 中文:问方知 — icon defaults to prophet-sparkle
6
7
  <Button variant="ghost" icon="play">Run Test</Button>
7
8
  <Button variant="text">Cancel</Button>
8
9
  <Button variant="primary-soft" icon="eye">Data Preview</Button>
@@ -17,4 +18,8 @@ Buttons: `Button` (primary/dark/outline/ghost/primary-soft), `IconButton` (squar
17
18
  <ActionChip tone="success">Mark as New</ActionChip>
18
19
  ```
19
20
 
20
- Primary = the single blue CTA per view. Dark = brand-level actions (AI Assistant, Show). Ghost = no box at rest, grey wash on hover — the default for actions in card and section headers, table toolbars and panel chrome ("Export", "Filter", "Run Test", "Back"); same height as outline so it lines up. `text` = bare label, underline on hover — links that happen to be buttons ("View all", "Clear filters"), never beside a boxed button and never a toolbar action. Emphasis is always the variant, never the size: a quieter action gets `ghost` or `text`, not `size="sm"`. Danger/danger-soft = destructive actions. lg (36px) comes from an lg region — dialogs and wizard footers; `Modal` sets it for you. A `size` prop on a single button is only for a control that stands alone, never to adjust one button inside a bar. `loading` swaps the icon for a spinner and disables the button without changing its size; `iconOnly` needs a `label` for accessibility; `ButtonGroup` fuses adjacent buttons into one segmented control or a split button (main action + attached icon-only).
21
+ Primary = the single blue CTA per view. Dark = brand-level actions that should outweigh the page but not compete with the blue CTA (Publish, Show). Never for AI — Prophet AI entries use `ai`. Ghost = no box at rest, grey wash on hover — the default for actions in card and section headers, table toolbars and panel chrome ("Export", "Filter", "Run Test", "Back"); same height as outline so it lines up. `text` = bare label, underline on hover — links that happen to be buttons ("View all", "Clear filters"), never beside a boxed button and never a toolbar action. Emphasis is always the variant, never the size: a quieter action gets `ghost` or `text`, not `size="sm"`. Danger/danger-soft = destructive actions. lg (36px) comes from an lg region — dialogs and wizard footers; `Modal` sets it for you. A `size` prop on a single button is only for a control that stands alone, never to adjust one button inside a bar. `loading` swaps the icon for a spinner and disables the button without changing its size; `iconOnly` needs a `label` for accessibility; `ButtonGroup` fuses adjacent buttons into one segmented control or a split button (main action + attached icon-only).
22
+
23
+ `ai` = Prophet AI (方知) entry points only — never a generic primary action, at most one per region. Same box, tiers and generic states (focus, disabled, loading) as every Button; only the surface differs: a 1px travelling gradient ring at rest, and on hover the full ring, an 8% gradient tint, a soft glow and a gradient icon, with the ring speeding up. It only triggers (opens the Prophet AI panel); it never shows AI content in place. Tokens live in `tokens/ai.css` (`--ai-*`); `<Icon gradient/>` renders the gradient glyph.
24
+
25
+ Legacy adapters (both frameworks): `trailingIcon` (e.g. "chevron-down" on a menu button, stays visible while loading), `nativeType` ("submit" for old forms), `text` (fallback label), element / component icons, `aria-busy`. Vue adds `#icon` / `#loading` / `#trailing` slots and has its own types in `Button.vue.d.ts`. Migrating the old actnplanBtn: `BUTTON_MIGRATION.md`.