@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.
Files changed (164) hide show
  1. package/DEPLOY.md +3 -3
  2. package/DESIGN.md +13 -4
  3. package/INTEGRATION.md +25 -2
  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 +15 -0
  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_MIGRATION.md +111 -0
  103. package/components/buttons/Button.d.ts +14 -5
  104. package/components/buttons/Button.jsx +45 -8
  105. package/components/buttons/Button.vue +61 -12
  106. package/components/buttons/Button.vue.d.ts +47 -0
  107. package/components/buttons/ai-button.js +54 -0
  108. package/components/buttons/buttons.prompt.md +7 -2
  109. package/components/display/Kbd.d.ts +3 -1
  110. package/components/display/Kbd.jsx +8 -3
  111. package/components/display/Kbd.vue +7 -3
  112. package/components/display/display.prompt.md +3 -1
  113. package/components/feedback/MESSAGE_SERVICES.md +71 -0
  114. package/components/feedback/Modal.d.ts +7 -0
  115. package/components/feedback/Modal.jsx +9 -9
  116. package/components/feedback/Toast.vue +8 -8
  117. package/components/feedback/_internal/MessageBoxView.vue +237 -0
  118. package/components/feedback/_internal/MessageView.vue +73 -0
  119. package/components/feedback/_internal/service-utils.js +46 -0
  120. package/components/feedback/feedback.prompt.md +2 -0
  121. package/components/feedback/message-box.d.ts +69 -0
  122. package/components/feedback/message-box.js +92 -0
  123. package/components/feedback/message.d.ts +37 -0
  124. package/components/feedback/message.js +68 -0
  125. package/components/forms/GlobalSearch.d.ts +83 -0
  126. package/components/forms/GlobalSearch.jsx +172 -0
  127. package/components/forms/GlobalSearch.vue +161 -0
  128. package/components/forms/forms.prompt.md +7 -0
  129. package/components/forms/global-search.js +161 -0
  130. package/components/icons/Icon.d.ts +2 -0
  131. package/components/icons/Icon.jsx +14 -6
  132. package/components/icons/Icon.vue +11 -4
  133. package/components/icons/icon-paths.js +4 -2
  134. package/components/index.d.ts +1 -0
  135. package/components/index.js +1 -0
  136. package/components/navigation/TopBar.d.ts +9 -3
  137. package/components/navigation/TopBar.jsx +5 -7
  138. package/components/navigation/TopBar.vue +5 -6
  139. package/covers.js +4 -2
  140. package/gallery.html +1 -1
  141. package/guidelines/brand-logo.html +1 -1
  142. package/guidelines/colors-schemes.html +1 -1
  143. package/guidelines/colors-transparency.html +1 -1
  144. package/guidelines/foundations-borders.html +1 -1
  145. package/guidelines/foundations-chrome.html +1 -1
  146. package/guidelines/foundations-motion.html +1 -1
  147. package/guidelines/foundations-size-tiers.html +1 -1
  148. package/guidelines/foundations-spacing.html +1 -1
  149. package/guidelines/interaction-states.html +1 -1
  150. package/guidelines/type-extended.html +1 -1
  151. package/guidelines/type-mono.html +1 -1
  152. package/guidelines/type-scale.html +1 -1
  153. package/guidelines/type-weights.html +1 -1
  154. package/index.html +8 -8
  155. package/package.json +10 -2
  156. package/readme.md +4 -4
  157. package/readme.zh-CN.md +4 -4
  158. package/site-nav.js +1 -1
  159. package/styles.css +1 -0
  160. package/tokens/ai.css +17 -0
  161. package/tokens/colors.css +1 -1
  162. package/tokens/tokens.d.ts +28 -0
  163. package/tokens/tokens.js +47 -5
  164. package/tokens/tokens.json +123 -5
@@ -0,0 +1,73 @@
1
+ <script setup>
2
+ import { computed, h, isVNode, onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
+ import Toast from '../Toast.vue'
4
+ import Icon from '../../icons/Icon.vue'
5
+ import { contentNode } from './service-utils.js'
6
+
7
+ const props = defineProps({ options: { type: Object, required: true } })
8
+ const emit = defineEmits(['close', 'height'])
9
+ const root = ref(null)
10
+ const tone = computed(() => props.options.type === 'error' ? 'danger' : props.options.type)
11
+ const Content = () => contentNode(props.options.message, props.options.dangerouslyUseHTMLString)
12
+ const CustomIcon = () => typeof props.options.icon === 'string'
13
+ ? h(Icon, { name: props.options.icon, size: 16 })
14
+ : isVNode(props.options.icon) ? props.options.icon : h(props.options.icon)
15
+ let timer
16
+ let observer
17
+ let paused = false
18
+ function stop() { clearTimeout(timer) }
19
+ function start() {
20
+ stop()
21
+ if (!paused && props.options.duration > 0) timer = setTimeout(() => emit('close'), props.options.duration)
22
+ }
23
+ function onEscape(event) {
24
+ if (event.key === 'Escape') emit('close')
25
+ }
26
+ watch(() => [props.options.restart, props.options.duration], start)
27
+ onMounted(() => {
28
+ start()
29
+ const measure = () => emit('height', root.value.getBoundingClientRect().height)
30
+ measure()
31
+ if (typeof ResizeObserver !== 'undefined') {
32
+ observer = new ResizeObserver(measure)
33
+ observer.observe(root.value)
34
+ }
35
+ document.addEventListener('keydown', onEscape)
36
+ })
37
+ onBeforeUnmount(() => {
38
+ stop()
39
+ observer?.disconnect()
40
+ document.removeEventListener('keydown', onEscape)
41
+ })
42
+ </script>
43
+
44
+ <template>
45
+ <div
46
+ :id="options.id"
47
+ ref="root"
48
+ :class="['kb-message', options.customClass, { 'kb-message--center': options.center, 'kb-message--plain': options.plain }]"
49
+ :role="options.type === 'error' || options.type === 'warning' ? 'alert' : 'status'"
50
+ :style="{ position: 'fixed', top: `${options.top}px`, left: '50%', transform: 'translateX(-50%)', zIndex: options.zIndex ?? 'var(--z-toast)', maxWidth: '92vw' }"
51
+ @mouseenter="paused = true; stop()"
52
+ @mouseleave="paused = false; start()"
53
+ @focusin="paused = true; stop()"
54
+ @focusout="paused = false; start()"
55
+ >
56
+ <Toast
57
+ :tone="tone"
58
+ :dismissible="options.showClose"
59
+ :close-label="options.closeLabel"
60
+ :style="{ minWidth: 'min(280px, 92vw)', maxWidth: '100%', boxSizing: 'border-box', ...(options.plain ? { boxShadow: 'none' } : {}) }"
61
+ @close="emit('close')"
62
+ >
63
+ <template v-if="options.icon" #icon><CustomIcon /></template>
64
+ <Content />
65
+ <span v-if="options.repeatNum > 1" class="kb-message__count"> × {{ options.repeatNum }}</span>
66
+ </Toast>
67
+ </div>
68
+ </template>
69
+
70
+ <style scoped>
71
+ .kb-message--center { text-align: center; }
72
+ .kb-message__count { color: var(--text-secondary); font-variant-numeric: tabular-nums; }
73
+ </style>
@@ -0,0 +1,46 @@
1
+ import { h, isVNode, render } from 'vue'
2
+
3
+ // 默认按文本渲染,只有调用方显式开启时才渲染可信 HTML。
4
+ export function contentNode(message, html = false) {
5
+ const value = typeof message === 'function' ? message() : message
6
+ if (isVNode(value) || Array.isArray(value)) return value
7
+ return html ? h('span', { innerHTML: String(value ?? '') }) : String(value ?? '')
8
+ }
9
+
10
+ export function resolveTarget(options = {}) {
11
+ const target = typeof options.appendTo === 'string'
12
+ ? document.querySelector(options.appendTo) : options.appendTo || document.body
13
+ if (!target) throw new Error('Keybricks: appendTo 未找到挂载目标')
14
+ return target
15
+ }
16
+
17
+ export function mountService(component, props, options = {}, appContext) {
18
+ const target = resolveTarget(options)
19
+ const container = document.createElement('div')
20
+ const vnode = h(component, props)
21
+ vnode.appContext = appContext || null
22
+ target.appendChild(container)
23
+ try {
24
+ render(vnode, container)
25
+ } catch (error) {
26
+ render(null, container)
27
+ container.remove()
28
+ throw error
29
+ }
30
+ return () => {
31
+ render(null, container)
32
+ container.remove()
33
+ }
34
+ }
35
+
36
+ let locks = 0
37
+ let previousOverflow
38
+ export function lockBody() {
39
+ if (locks++ === 0) {
40
+ previousOverflow = document.body.style.overflow
41
+ document.body.style.overflow = 'hidden'
42
+ }
43
+ return () => {
44
+ if (--locks === 0) document.body.style.overflow = previousOverflow
45
+ }
46
+ }
@@ -33,3 +33,5 @@ Additions (not in the source file): `Tooltip`, `Modal`, `Toast`/`ToastStack`, `A
33
33
  Failed tasks retry up to 3 times with exponential backoff.
34
34
  </Accordion>
35
35
  ```
36
+
37
+ Vue only: imperative `Message` (`@keyfunc/keybricks-ds/vue/message`) and `MessageBox` (`@keyfunc/keybricks-ds/vue/message-box`) services, drop-in for most `ElMessage` / `ElMessageBox` calls. Message renders the Keybricks `Toast`; MessageBox renders a DS dialog with DS Buttons. Limits and Element Plus mapping: `MESSAGE_SERVICES.md`.
@@ -0,0 +1,69 @@
1
+ import type { AppContext, Component, VNode } from 'vue';
2
+ import type { MessageContent, MessageType } from './message.js';
3
+
4
+ export type MessageBoxAction = 'confirm' | 'cancel' | 'close';
5
+ export interface MessageBoxInputResult { value: string; action: MessageBoxAction; }
6
+ export type MessageBoxResult = MessageBoxAction | MessageBoxInputResult;
7
+ export interface MessageBoxOptions {
8
+ title?: MessageContent;
9
+ message?: MessageContent;
10
+ type?: MessageType | '';
11
+ icon?: string | Component | VNode;
12
+ width?: string | number;
13
+ size?: 'sm' | 'md' | 'lg';
14
+ customClass?: string;
15
+ modalClass?: string;
16
+ zIndex?: number;
17
+ appendTo?: string | HTMLElement;
18
+ modal?: boolean;
19
+ lockScroll?: boolean;
20
+ autofocus?: boolean;
21
+ center?: boolean;
22
+ roundButton?: boolean;
23
+ showClose?: boolean;
24
+ closeLabel?: string;
25
+ closeOnClickModal?: boolean;
26
+ closeOnPressEscape?: boolean;
27
+ distinguishCancelAndClose?: boolean;
28
+ dangerouslyUseHTMLString?: boolean;
29
+ showConfirmButton?: boolean;
30
+ showCancelButton?: boolean;
31
+ confirmButtonText?: string;
32
+ cancelButtonText?: string;
33
+ confirmButtonClass?: string;
34
+ cancelButtonClass?: string;
35
+ confirmButtonLoading?: boolean;
36
+ cancelButtonLoading?: boolean;
37
+ confirmButtonDisabled?: boolean;
38
+ cancelButtonDisabled?: boolean;
39
+ showInput?: boolean;
40
+ inputValue?: string;
41
+ inputType?: string;
42
+ inputPlaceholder?: string;
43
+ inputPattern?: RegExp;
44
+ inputErrorMessage?: string;
45
+ inputValidator?: (value: string) => boolean | string | Promise<boolean | string>;
46
+ beforeClose?: (action: MessageBoxAction, instance: MessageBoxState, done: () => void) => void;
47
+ callback?: (result: MessageBoxResult, instance: MessageBoxState) => void;
48
+ }
49
+ export interface MessageBoxState extends MessageBoxOptions {
50
+ inputValue: string;
51
+ confirmButtonLoading: boolean;
52
+ cancelButtonLoading: boolean;
53
+ action?: MessageBoxAction;
54
+ }
55
+ export interface MessageBoxShortcut<Result = MessageBoxResult> {
56
+ (message: MessageContent, title?: MessageContent, options?: MessageBoxOptions, appContext?: AppContext | null): Promise<Result>;
57
+ (message: MessageContent, options?: MessageBoxOptions, appContext?: AppContext | null): Promise<Result>;
58
+ }
59
+ export interface MessageBoxService {
60
+ (options?: MessageBoxOptions | MessageContent, appContext?: AppContext | null): Promise<MessageBoxResult>;
61
+ alert: MessageBoxShortcut;
62
+ confirm: MessageBoxShortcut;
63
+ prompt: MessageBoxShortcut<MessageBoxInputResult>;
64
+ /** 清空队列,不结算已有 Promise,与 ElMessageBox.close 相同。 */
65
+ close(): void;
66
+ defaults: Partial<MessageBoxOptions>;
67
+ }
68
+ export declare const MessageBox: MessageBoxService;
69
+ export default MessageBox;
@@ -0,0 +1,92 @@
1
+ import { getCurrentInstance, isVNode, reactive } from 'vue'
2
+ import MessageBoxView from './_internal/MessageBoxView.vue'
3
+ import { mountService } from './_internal/service-utils.js'
4
+
5
+ const queue = []
6
+ let active
7
+ let seed = 0
8
+
9
+ function next() {
10
+ if (active || !queue.length) return
11
+ const entry = active = queue.shift()
12
+ try {
13
+ entry.dispose = mountService(MessageBoxView, {
14
+ options: entry.state,
15
+ onAction: action => requestAction(entry, action),
16
+ }, entry.state, entry.context)
17
+ } catch (error) {
18
+ active = undefined
19
+ entry.reject(error)
20
+ next()
21
+ }
22
+ }
23
+
24
+ function finish(entry, action) {
25
+ if (active !== entry || entry.closed) return
26
+ entry.closed = true
27
+ entry.dispose?.()
28
+ active = undefined
29
+ const result = entry.state.showInput ? { value: entry.state.inputValue, action } : action
30
+ try {
31
+ if (entry.state.callback) entry.state.callback(result, entry.state)
32
+ else if (action === 'confirm') entry.resolve(result)
33
+ else entry.reject(action === 'close' && !entry.state.distinguishCancelAndClose ? 'cancel' : action)
34
+ } finally {
35
+ next()
36
+ }
37
+ }
38
+
39
+ function requestAction(entry, action) {
40
+ if (entry.closed || active !== entry) return
41
+ entry.state.action = action
42
+ const done = () => finish(entry, action)
43
+ if (entry.state.beforeClose) entry.state.beforeClose(action, entry.state, done)
44
+ else done()
45
+ }
46
+
47
+ /** Vue 弹窗服务;队列内同一时刻只显示一个确认框。 */
48
+ export function MessageBox(value = {}, appContext) {
49
+ if (typeof document === 'undefined') return Promise.reject(new Error('Keybricks MessageBox 需要浏览器环境'))
50
+ const options = typeof value === 'string' || typeof value === 'function' || isVNode(value) ? { message: value } : value
51
+ const state = reactive({
52
+ id: `kb-message-box-${++seed}`, title: '', message: '', type: '',
53
+ showClose: true, showConfirmButton: true, showCancelButton: false,
54
+ closeOnClickModal: true, closeOnPressEscape: true, lockScroll: true,
55
+ autofocus: true, inputValue: '', inputType: 'text', inputErrorMessage: '',
56
+ confirmButtonLoading: false, cancelButtonLoading: false,
57
+ confirmButtonDisabled: false, cancelButtonDisabled: false,
58
+ ...MessageBox.defaults, ...options,
59
+ })
60
+ return new Promise((resolve, reject) => {
61
+ queue.push({ state, resolve, reject, context: appContext || getCurrentInstance()?.appContext })
62
+ next()
63
+ })
64
+ }
65
+
66
+ for (const kind of ['alert', 'confirm', 'prompt']) {
67
+ MessageBox[kind] = (message, title, options, appContext) => {
68
+ if (title && typeof title === 'object' && !isVNode(title)) {
69
+ appContext = options
70
+ options = title
71
+ title = undefined
72
+ }
73
+ return MessageBox({
74
+ boxType: kind, showCancelButton: kind !== 'alert', showInput: kind === 'prompt',
75
+ closeOnClickModal: kind !== 'alert', closeOnPressEscape: kind !== 'alert',
76
+ ...options, message, ...(title === undefined ? {} : { title }),
77
+ }, appContext)
78
+ }
79
+ }
80
+
81
+ // 与 ElMessageBox.close 一致:清空所有弹窗,不触发用户操作回调或结算 Promise。
82
+ MessageBox.close = () => {
83
+ queue.length = 0
84
+ if (active) {
85
+ const entry = active
86
+ active = undefined
87
+ entry.closed = true
88
+ entry.dispose?.()
89
+ }
90
+ }
91
+ export default MessageBox
92
+ MessageBox.defaults = {}
@@ -0,0 +1,37 @@
1
+ import type { AppContext, Component, VNode } from 'vue';
2
+
3
+ export type MessageType = 'success' | 'info' | 'warning' | 'error';
4
+ export type MessageContent = string | VNode | (() => VNode);
5
+ export interface MessageHandle { close(): void; }
6
+ export interface MessageOptions {
7
+ message?: MessageContent;
8
+ type?: MessageType;
9
+ icon?: string | Component | VNode;
10
+ /** 毫秒;0 表示不自动关闭,默认 3000。 */
11
+ duration?: number;
12
+ showClose?: boolean;
13
+ closeLabel?: string;
14
+ offset?: number;
15
+ zIndex?: number;
16
+ customClass?: string;
17
+ center?: boolean;
18
+ plain?: boolean;
19
+ grouping?: boolean;
20
+ /** 仅允许可信且已由应用净化的 HTML。 */
21
+ dangerouslyUseHTMLString?: boolean;
22
+ appendTo?: string | HTMLElement;
23
+ onClose?: (handle: MessageHandle) => void;
24
+ }
25
+ export type MessageParams = MessageContent | MessageOptions;
26
+ export interface MessageService {
27
+ (options?: MessageParams, appContext?: AppContext | null): MessageHandle;
28
+ success(options: MessageParams, appContext?: AppContext | null): MessageHandle;
29
+ info(options: MessageParams, appContext?: AppContext | null): MessageHandle;
30
+ warning(options: MessageParams, appContext?: AppContext | null): MessageHandle;
31
+ error(options: MessageParams, appContext?: AppContext | null): MessageHandle;
32
+ closeAll(type?: MessageType): void;
33
+ /** 在调用时读取;可随应用语言切换更新。 */
34
+ defaults: Partial<MessageOptions>;
35
+ }
36
+ export declare const Message: MessageService;
37
+ export default Message;
@@ -0,0 +1,68 @@
1
+ import { getCurrentInstance, isVNode, reactive } from 'vue'
2
+ import MessageView from './_internal/MessageView.vue'
3
+ import { mountService, resolveTarget } from './_internal/service-utils.js'
4
+
5
+ const instances = []
6
+ let seed = 0
7
+ const normalize = value => typeof value === 'string' || typeof value === 'function' || isVNode(value)
8
+ ? { message: value } : { ...value }
9
+
10
+ /** Vue 消息服务;样式由 Toast 提供,业务错误脱敏仍由应用处理。 */
11
+ export function Message(value = {}, appContext) {
12
+ if (typeof document === 'undefined') return { close() {} }
13
+ const options = { type: 'info', duration: 3000, offset: 16, ...Message.defaults, ...normalize(value) }
14
+ const target = resolveTarget(options)
15
+ if (options.grouping) {
16
+ const existing = instances.find(item => item.target === target && item.options.message === options.message)
17
+ if (existing) {
18
+ existing.options.repeatNum++
19
+ existing.options.type = options.type
20
+ existing.options.restart++
21
+ return existing.handle
22
+ }
23
+ }
24
+ const state = reactive({ ...options, id: `kb-message-${++seed}`, repeatNum: 1, restart: 0, height: 0, top: options.offset })
25
+ let dispose
26
+ let closed = false
27
+ const handle = { close() {
28
+ if (closed) return
29
+ closed = true
30
+ instances.splice(instances.indexOf(entry), 1)
31
+ dispose?.()
32
+ reflow()
33
+ options.onClose?.(handle)
34
+ } }
35
+ const entry = { options: state, handle, target }
36
+ function reflow() {
37
+ const tops = new Map()
38
+ for (const item of instances) {
39
+ const target = item.target
40
+ item.options.top = Math.max(Number(item.options.offset) || 0, tops.get(target) || 0)
41
+ tops.set(target, item.options.top + item.options.height + 16)
42
+ }
43
+ }
44
+ instances.push(entry)
45
+ try {
46
+ dispose = mountService(MessageView, {
47
+ options: state, onClose: handle.close,
48
+ onHeight: height => { state.height = height; reflow() },
49
+ }, options, appContext || getCurrentInstance()?.appContext)
50
+ } catch (error) {
51
+ instances.splice(instances.indexOf(entry), 1)
52
+ reflow()
53
+ throw error
54
+ }
55
+ reflow()
56
+ return handle
57
+ }
58
+
59
+ for (const type of ['success', 'info', 'warning', 'error']) {
60
+ Message[type] = (value, appContext) => Message({ ...normalize(value), type }, appContext)
61
+ }
62
+ Message.closeAll = type => {
63
+ for (const entry of [...instances]) {
64
+ if (!type || entry.options.type === type) entry.handle.close()
65
+ }
66
+ }
67
+ export default Message
68
+ Message.defaults = {}
@@ -0,0 +1,83 @@
1
+ import * as React from "react";
2
+ /** One searchable thing. kind groups it (Features 4 · Documents 5 · Dimension Members 3 · Actions 2 under All). */
3
+ export interface GlobalSearchItem {
4
+ kind?: "feature" | "document" | "member" | "action";
5
+ title: string;
6
+ /** Second line, e.g. "Finance / Reports · updated 2h ago" */
7
+ sub?: string;
8
+ /** Right-hand meta, e.g. the document type */
9
+ meta?: string;
10
+ /** Glyph; defaults by kind (layout-grid · file-text · database · zap) */
11
+ icon?: string;
12
+ /** Documents only: category hue of the tile (--cat-<tone>-bg / --cat-<tone>) */
13
+ tone?: "source" | "join" | "transform" | "merge" | "output";
14
+ /** Matched after the title (path, kind, owner …), not highlighted */
15
+ keywords?: string[];
16
+ onSelect?: () => void;
17
+ }
18
+ export interface GlobalSearchStep { label: string; meta?: string; state: "done" | "running" | "pending" }
19
+ /** Deep Search progress, pushed through the update callback of onDeepSearch. */
20
+ export interface GlobalSearchDeepState {
21
+ status?: "running" | "done";
22
+ steps?: GlobalSearchStep[];
23
+ /** e.g. "42 s" */
24
+ elapsed?: string;
25
+ /** Paragraphs of the cited conclusion */
26
+ conclusion?: string | string[];
27
+ /** Source chip labels; their count is the citation count */
28
+ sources?: string[];
29
+ onOpenReport?: () => void;
30
+ }
31
+ /** Every string in the card. GLOBAL_SEARCH_LABELS.en is the default; .zh is the drafted Chinese (Prophet AI = 方知). {n} {q} {t} {i} are filled in. */
32
+ export interface GlobalSearchLabels {
33
+ field?: string; input?: string; ask?: string; clear?: string;
34
+ /** All · Features · Documents · Data · Actions — in that order */
35
+ scopes?: string[];
36
+ groups?: { feature?: string; document?: string; member?: string; action?: string };
37
+ recentSearches?: string; recent?: string; prompts?: string; viewAll?: string;
38
+ results?: string; searching?: string; noResults?: string;
39
+ askRow?: string; askSub?: string; deepRow?: string; deepSub?: string;
40
+ deepTitle?: string; running?: string; doneStatus?: string; stepsDone?: string; stepOf?: string;
41
+ conclusion?: string; openReport?: string; followUp?: string;
42
+ select?: string; close?: string; privacy?: string;
43
+ }
44
+ /**
45
+ * Global Search — the TopBar instance of Search. Collapsed: a field styled like SearchInput (min 400px, grows to fit its placeholder, never truncated)
46
+ * with a ⌘K / Ctrl K hint. Exception to SearchInput: placeholder --text-secondary (≥4.5:1) and hover fills --bg-inset as well as darkening the border. Click or ⌘K / Ctrl K opens a 720px card over the top bar with a scrim. Keys: ↑↓ select (wraps) · ↵ open ·
47
+ * ⌘↵ / Ctrl Enter ask Prophet AI · Esc back (Deep Search) / close. It never renders AI answers: every AI entry closes the card and
48
+ * calls onAsk(question) so the separate Prophet AI panel can open.
49
+ */
50
+ export interface GlobalSearchProps {
51
+ /** Collapsed field text (default labels.field) */
52
+ placeholder?: string;
53
+ /** Input placeholder inside the open card (default labels.input) */
54
+ inputPlaceholder?: string;
55
+ /** Local data, matched case-insensitively on title, then keywords */
56
+ items?: GlobalSearchItem[];
57
+ /** Remote search instead of items: runs after a 300ms typing pause; the card shows a spinner and skeleton rows until it resolves */
58
+ onQuery?: (term: string) => GlobalSearchItem[] | Promise<GlobalSearchItem[]>;
59
+ /** Chips in the empty state; clicking one searches immediately */
60
+ recentSearches?: string[];
61
+ /** Recently opened items in the empty state (3) */
62
+ recent?: GlobalSearchItem[];
63
+ /** "Try Asking Prophet AI" prompts in the empty state (3) */
64
+ prompts?: string[];
65
+ /** A result or recent item was chosen (the card closes) */
66
+ onSelect?: (item: GlobalSearchItem) => void;
67
+ /** Hand-off to the Prophet AI panel with the question ("" for an empty query). The card has already closed. */
68
+ onAsk?: (question: string) => void;
69
+ /** Shows the pinned Deep Search row. Called when it's chosen; push progress through update; return a cancel function (called on Esc / typing / close). */
70
+ onDeepSearch?: (term: string, update: (state: GlobalSearchDeepState) => void) => void | (() => void);
71
+ /** Receives the query as the user types */
72
+ onInput?: (value: string) => void;
73
+ labels?: GlobalSearchLabels;
74
+ /** Renders the open card in place (no field, no scrim) — for docs and specimens */
75
+ inline?: boolean;
76
+ defaultOpen?: boolean;
77
+ defaultQuery?: string;
78
+ /** Start in Deep Search mode with this state (specimens) */
79
+ defaultDeep?: GlobalSearchDeepState;
80
+ style?: React.CSSProperties;
81
+ }
82
+ export declare function GlobalSearch(props: GlobalSearchProps): JSX.Element;
83
+ export declare const GLOBAL_SEARCH_LABELS: { en: Required<GlobalSearchLabels>; zh: Required<GlobalSearchLabels> };
@@ -0,0 +1,172 @@
1
+ import React from "react";
2
+ import { Icon } from "../icons/Icon.jsx";
3
+ import { IconButton } from "../buttons/IconButton.jsx";
4
+ import { Button } from "../buttons/Button.jsx";
5
+ import { Kbd } from "../display/Kbd.jsx";
6
+ import { SegmentedControl } from "../tabs/SegmentedControl.jsx";
7
+ import { Spinner, Skeleton } from "../feedback/Alert.jsx";
8
+ import { GS_ICONS, gsLabels, gsFill, gsIsMac, gsMod, gsFilter, gsSplit, gsRows, gsPos, gsReveal, gsStyles as S, gsAiTile } from "./global-search.js";
9
+
10
+ /** Default copy: .en (default) and .zh (drafted; Prophet AI = 方知). Pass one, or your own, as labels. */
11
+ export { GLOBAL_SEARCH_LABELS } from "./global-search.js";
12
+
13
+ function AiTile({ on }) {
14
+ const t = gsAiTile(on);
15
+ return <span style={t.outer}><span style={t.inner}><span style={t.tint} /><Icon name="prophet-sparkle" size={16} gradient={on} style={t.icon} /></span></span>;
16
+ }
17
+
18
+ /** Global Search (TopBar instance of Search). Collapsed: a quiet field with a ⌘K / Ctrl K hint. Open (click, ⌘K / Ctrl K): a 720px card over the
19
+ * top bar with a scrim — input row, scope bar, grouped results, pinned Ask Prophet AI / Deep Search rows, keyboard footer.
20
+ * It never shows AI answers: every AI entry closes the card and calls onAsk(question) so the Prophet AI panel can open.
21
+ * items: local data matched by title / keywords; or onQuery(term) → items | Promise<items> (debounced 300ms).
22
+ * onDeepSearch(term, update) runs Deep Search: push { status, steps, elapsed, conclusion, sources, onOpenReport } through update; return a cancel fn. */
23
+ export function GlobalSearch({ placeholder, inputPlaceholder, items = [], onQuery, recentSearches = [], recent = [], prompts = [], onSelect, onAsk, onDeepSearch, onInput, labels, inline, defaultOpen, defaultQuery = "", defaultDeep, style }) {
24
+ const L = gsLabels(labels);
25
+ const [open, setOpen] = React.useState(!!(inline || defaultOpen));
26
+ const [q, setQ] = React.useState(defaultQuery);
27
+ const [scope, setScope] = React.useState(0);
28
+ const [found, setFound] = React.useState(null);
29
+ const [loading, setLoading] = React.useState(false);
30
+ const [sel, setSel] = React.useState(0);
31
+ const [deep, setDeep] = React.useState(defaultDeep ? { query: defaultQuery, ...defaultDeep } : null);
32
+ const [stepsOpen, setStepsOpen] = React.useState(false);
33
+ const [hover, setHover] = React.useState(false);
34
+ const [pos, setPos] = React.useState({ left: 8, width: 720, maxH: 640 });
35
+ const fieldRef = React.useRef(null), inputRef = React.useRef(null), bodyRef = React.useRef(null);
36
+ const cancelDeep = React.useRef(null), now = React.useRef(!!defaultQuery);
37
+ const mac = React.useMemo(gsIsMac, []);
38
+ const term = q.trim();
39
+
40
+ const stopDeep = () => { if (cancelDeep.current) cancelDeep.current(); cancelDeep.current = null; };
41
+ const openCard = () => { if (inline) return; setPos(gsPos(fieldRef.current)); setOpen(true); };
42
+ const close = () => { if (inline) return; stopDeep(); setDeep(null); setOpen(false); if (fieldRef.current) fieldRef.current.focus(); };
43
+ const ask = (text) => { close(); if (onAsk) onAsk(text || ""); };
44
+ const startDeep = () => {
45
+ stopDeep(); setStepsOpen(false);
46
+ setDeep({ query: term, status: "running", steps: [] });
47
+ const c = onDeepSearch && onDeepSearch(term, (s) => setDeep((d) => (d ? { ...d, ...s } : d)));
48
+ cancelDeep.current = typeof c === "function" ? c : null;
49
+ };
50
+ const exitDeep = () => { stopDeep(); setDeep(null); };
51
+
52
+ React.useEffect(() => {
53
+ if (defaultOpen && !inline) setPos(gsPos(fieldRef.current));
54
+ if (inline) return;
55
+ const h = (e) => { if (gsMod(e, mac) && e.key && e.key.toLowerCase() === "k") { e.preventDefault(); setPos(gsPos(fieldRef.current)); setOpen(true); } };
56
+ document.addEventListener("keydown", h);
57
+ return () => { document.removeEventListener("keydown", h); stopDeep(); };
58
+ }, [inline, mac]);
59
+ React.useEffect(() => { if (open && !inline && inputRef.current) inputRef.current.focus(); }, [open]);
60
+ React.useEffect(() => {
61
+ if (!term) { setFound(null); setLoading(false); return; }
62
+ let live = true;
63
+ const run = () => {
64
+ setLoading(true);
65
+ Promise.resolve(onQuery ? onQuery(term) : gsFilter(items, term)).then((r) => { if (live) { setFound(r || []); setLoading(false); } }, () => { if (live) { setFound([]); setLoading(false); } });
66
+ };
67
+ let t = null;
68
+ if (now.current) { now.current = false; run(); } else t = setTimeout(run, 300);
69
+ return () => { live = false; if (t) clearTimeout(t); };
70
+ }, [term]);
71
+ React.useEffect(() => { setSel(0); }, [found, scope]);
72
+
73
+ const { rows, total } = gsRows({ term, found, loading, scope, recent, prompts, deep, hasDeep: !!onDeepSearch, L });
74
+ const cur = Math.max(0, Math.min(sel, rows.length - 1));
75
+ React.useEffect(() => { gsReveal(bodyRef.current, cur); }, [cur]);
76
+
77
+ const activate = (r) => {
78
+ if (!r) return;
79
+ if (r.kind === "result" || r.kind === "recent") { close(); if (onSelect) onSelect(r.item); if (r.item.onSelect) r.item.onSelect(); }
80
+ else if (r.kind === "prompt") ask(r.text);
81
+ else if (r.kind === "ask") ask(term);
82
+ else if (r.kind === "deep") startDeep();
83
+ };
84
+ const change = (v) => { if (deep) exitDeep(); setQ(v); setSel(0); if (onInput) onInput(v); };
85
+ const pickChip = (v) => { now.current = true; change(v); };
86
+ const key = (e) => {
87
+ if (e.nativeEvent && e.nativeEvent.isComposing) return;
88
+ const n = rows.length;
89
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") { if (!n) return; e.preventDefault(); setSel((cur + (e.key === "ArrowDown" ? 1 : -1) + n) % n); }
90
+ else if (e.key === "Enter") { e.preventDefault(); if (gsMod(e, mac)) ask(term); else activate(rows[cur]); }
91
+ else if (e.key === "Escape") { e.preventDefault(); if (deep) exitDeep(); else close(); }
92
+ };
93
+
94
+ const rowProps = (r) => ({ key: r.i, "data-gs-row": r.i, role: "option", "aria-selected": r.i === cur, style: S.row(r.i === cur), onMouseEnter: () => setSel(r.i), onMouseDown: (e) => e.preventDefault(), onClick: () => activate(r) });
95
+ const head = (r) => r.head ? <div key={"h" + r.i} style={S.head}><span>{r.head.label}</span>{r.head.more ? <button type="button" style={S.more} onMouseDown={(e) => e.preventDefault()} onClick={() => setScope(r.head.more.scope)}>{gsFill(L.viewAll, { n: r.head.more.n })}</button> : null}</div> : null;
96
+ const enter = (r) => (r.i === cur ? <Kbd keys={["enter"]} /> : null);
97
+ const itemRow = (r, mark) => {
98
+ const it = r.item, k = it.kind || "feature", parts = mark ? gsSplit(it.title, term) : [it.title, "", ""];
99
+ return <React.Fragment key={r.i}>{head(r)}<div {...rowProps(r)}>
100
+ <span style={S.tile(k === "document" ? it.tone : null)}><Icon name={it.icon || GS_ICONS[k]} size={16} /></span>
101
+ <span style={S.text}><span style={S.title}>{parts[0]}{parts[1] ? <span style={S.mark}>{parts[1]}</span> : null}{parts[2]}</span>{it.sub ? <span style={S.sub}>{it.sub}</span> : null}</span>
102
+ {it.meta ? <span style={S.meta}>{it.meta}</span> : null}{enter(r)}
103
+ </div></React.Fragment>;
104
+ };
105
+
106
+ let body;
107
+ if (deep) {
108
+ const steps = deep.steps || [], n = steps.length, ri = steps.findIndex((s) => s.state === "running"), run = steps[ri];
109
+ body = <div style={{ ...S.body, padding: 0 }}><div style={S.deep}>
110
+ <div style={S.deepHead}><span style={S.tile()}><Icon name="text-search" size={16} /></span><span style={S.text}><span style={S.deepTitle}>{gsFill(L.deepTitle, { q: deep.query })}</span><span style={S.sub}>{deep.status === "done" ? gsFill(L.doneStatus, { t: deep.elapsed || "", n: (deep.sources || []).length }) : L.running}</span></span></div>
111
+ {n ? <div style={S.steps}>
112
+ <button type="button" data-wash="" aria-expanded={stepsOpen} style={S.toggle} onClick={() => setStepsOpen(!stepsOpen)}>
113
+ {deep.status === "done" || !run
114
+ ? <><span style={{ ...S.stepIcon, color: "var(--success)" }}><Icon name="circle-check" size={14} /></span><span>{gsFill(L.stepsDone, { n })}</span><span style={S.muted}>{deep.elapsed}</span></>
115
+ : <><span style={S.stepIcon}><Spinner size={12} color="var(--text-secondary)" /></span><span>{run.label}…</span><span style={S.muted}>{gsFill(L.stepOf, { i: ri + 1, n })}</span></>}
116
+ <span style={S.grow} /><span style={{ ...S.stepIcon, color: "var(--text-muted)", transform: stepsOpen ? "rotate(180deg)" : "none" }}><Icon name="chevron-down" size={14} /></span>
117
+ </button>
118
+ {stepsOpen ? steps.map((s, i) => <div key={i} style={S.stepLine}>
119
+ <span style={{ ...S.stepIcon, color: "var(--success)" }}>{s.state === "done" ? <Icon name="circle-check" size={14} /> : s.state === "running" ? <Spinner size={12} color="var(--text-secondary)" /> : <span style={S.pending} />}</span>
120
+ <span style={{ color: s.state === "pending" ? "var(--text-muted)" : "var(--text-body)" }}>{s.label}</span>{s.meta ? <span style={S.muted}>{s.meta}</span> : null}
121
+ </div>) : null}
122
+ </div> : null}
123
+ {deep.conclusion ? <div style={S.conclusion}>
124
+ <span style={S.concLabel}>{L.conclusion}</span>
125
+ <div style={S.concText}>{[].concat(deep.conclusion).map((p, i) => <span key={i}>{p}</span>)}</div>
126
+ {deep.sources && deep.sources.length ? <div style={S.sources}>{deep.sources.map((s, i) => <span key={i} style={S.source}><Icon name="file-text" size={12} />{s}</span>)}</div> : null}
127
+ <div data-size="sm" style={S.actions}><Button variant="primary-soft" icon="file-text" onClick={deep.onOpenReport}>{L.openReport}</Button><Button variant="ghost" icon="prophet-sparkle" onClick={() => ask(deep.query)}>{L.followUp}</Button></div>
128
+ </div> : null}
129
+ </div></div>;
130
+ } else if (!term) {
131
+ body = <div ref={bodyRef} role="listbox" style={S.body}>
132
+ {recentSearches.length ? <><div style={S.head}><span>{L.recentSearches}</span></div><div style={S.chips}>{recentSearches.map((s) => <button key={s} type="button" style={S.chip} onMouseDown={(e) => e.preventDefault()} onClick={() => pickChip(s)}><span style={S.chipIcon}><Icon name="history" size={12} /></span>{s}</button>)}</div></> : null}
133
+ {rows.map((r) => r.kind === "recent" ? itemRow(r, false) : <React.Fragment key={r.i}>{head(r)}<div {...rowProps(r)}><AiTile on={r.i === cur} /><span style={S.text}><span style={S.title}>{r.text}</span></span>{enter(r)}</div></React.Fragment>)}
134
+ </div>;
135
+ } else {
136
+ const res = rows.filter((r) => r.kind === "result");
137
+ body = <div ref={bodyRef} role="listbox" style={S.body}>
138
+ {loading ? <><div style={S.head}><Skeleton width={64} height={10} /></div>{[0, 1, 2, 3].map((i) => <div key={i} style={S.skelRow}><Skeleton width={32} height={32} radius="var(--radius-md)" /><span style={S.skelText}><Skeleton width="46%" height={12} /><Skeleton width="28%" height={10} /></span></div>)}</>
139
+ : found && !res.length ? <div style={S.empty}>{L.noResults}</div> : res.map((r) => itemRow(r, true))}
140
+ </div>;
141
+ }
142
+
143
+ const pinned = !deep && term ? <div style={S.pinned}>{rows.filter((r) => r.kind === "ask" || r.kind === "deep").map((r) => r.kind === "ask"
144
+ ? <div {...rowProps(r)}><AiTile on={r.i === cur} /><span style={S.text}><span style={S.title}>{gsFill(L.askRow, { q: term })}</span><span style={S.sub}>{L.askSub}</span></span><Kbd keys={["mod", "enter"]} /></div>
145
+ : <div {...rowProps(r)}><span style={S.tile()}><Icon name="text-search" size={16} /></span><span style={S.text}><span style={S.title}>{gsFill(L.deepRow, { q: term })}</span><span style={S.sub}>{L.deepSub}</span></span>{enter(r)}</div>)}</div> : null;
146
+
147
+ const card = (
148
+ <div role="dialog" aria-modal={inline ? undefined : "true"} aria-label={L.input} data-size="md" onKeyDown={key} style={{ ...S.card, ...(inline ? null : S.cardFixed(pos)), ...(inline ? style : null) }}>
149
+ <div style={S.inputRow}>
150
+ <span style={S.lead}>{loading ? <Spinner size={14} color="var(--text-secondary)" /> : <Icon name={deep ? "text-search" : "search"} size={16} />}</span>
151
+ <input ref={inputRef} value={q} onChange={(e) => change(e.target.value)} placeholder={inputPlaceholder || L.input} aria-label={inputPlaceholder || L.input} style={S.input} />
152
+ <span style={S.trail}>{q ? <IconButton icon="x" muted label={L.clear} title={L.clear} onClick={() => { change(""); if (inputRef.current) inputRef.current.focus(); }} /> : null}<Button variant="ai" onClick={() => ask(term)}>{L.ask}</Button></span>
153
+ </div>
154
+ {term && !deep ? <div style={S.scope}><SegmentedControl size="sm" options={L.scopes} value={L.scopes[scope]} onChange={(v) => setScope(Math.max(0, L.scopes.indexOf(v)))} /><span style={S.count}>{loading ? L.searching : found ? gsFill(L.results, { n: total }) : ""}</span></div> : null}
155
+ {body}
156
+ {pinned}
157
+ <div style={S.footer}>
158
+ <span style={S.hints}><span style={S.hint}><Kbd keys={["↑", "↓"]} />{L.select}</span><span style={S.hint}><Kbd keys={["mod", "enter"]} />{L.ask}</span><span style={S.hint}><Kbd keys={["esc"]} />{L.close}</span></span>
159
+ <span style={S.hint}><Icon name="lock" size={12} />{L.privacy}</span>
160
+ </div>
161
+ </div>
162
+ );
163
+ if (inline) return card;
164
+ return (
165
+ <>
166
+ <button ref={fieldRef} type="button" data-field="" aria-haspopup="dialog" aria-expanded={open} onClick={openCard} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{ ...S.field(hover), ...style }}>
167
+ <span style={S.fieldIcon}><Icon name="search" size={16} /></span><span style={S.fieldLabel}>{placeholder || L.field}</span><Kbd keys={["mod", "K"]} />
168
+ </button>
169
+ {open ? <><div style={S.scrim} onMouseDown={close} />{card}</> : null}
170
+ </>
171
+ );
172
+ }