@keyfunc/keybricks-ds 2.6.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPLOY.md +3 -3
- package/DESIGN.md +21 -5
- package/INTEGRATION.md +35 -4
- package/_adherence.oxlintrc.json +56 -11
- package/_ds_bundle.js +2153 -216
- package/_scripts/build-index.mjs +1 -1
- package/_src/icon-index.js +1 -1
- package/{components/feedback → _vue-services}/MESSAGE_SERVICES.md +2 -2
- package/_vue-services/_internal/MessageBoxView.vue +150 -0
- package/{components/feedback → _vue-services}/_internal/service-utils.js +0 -12
- package/_vue-services/message.js +87 -0
- package/assets/icons/icon-index.json +1 -1
- package/changelog.js +21 -2
- package/components/_cards/accordion.card.html +1 -1
- package/components/_cards/action-chip.card.html +1 -1
- package/components/_cards/activity.card.html +1 -1
- package/components/_cards/alert.card.html +1 -1
- package/components/_cards/app-chrome.card.html +1 -1
- package/components/_cards/banner.card.html +1 -1
- package/components/_cards/breadcrumb.card.html +1 -1
- package/components/_cards/button-1-base.card.html +5 -2
- package/components/_cards/button-2-states.card.html +5 -5
- package/components/_cards/button-3-sizes-icons.card.html +2 -2
- package/components/_cards/button-4-group.card.html +2 -2
- package/components/_cards/button-5-ai.card.html +34 -0
- package/components/_cards/canvas-zoom-controls.card.html +1 -1
- package/components/_cards/cards-indicators.card.html +1 -1
- package/components/_cards/charts-1-sparkline.card.html +1 -1
- package/components/_cards/charts-10-gauge.card.html +1 -1
- package/components/_cards/charts-11-funnel.card.html +1 -1
- package/components/_cards/charts-12-treemap.card.html +1 -1
- package/components/_cards/charts-14-radar.card.html +1 -1
- package/components/_cards/charts-15-distribution.card.html +1 -1
- package/components/_cards/charts-16-pareto.card.html +1 -1
- package/components/_cards/charts-17-candlestick.card.html +1 -1
- package/components/_cards/charts-18-sankey.card.html +1 -1
- package/components/_cards/charts-19-chord.card.html +1 -1
- package/components/_cards/charts-2-line.card.html +1 -1
- package/components/_cards/charts-20-sunburst.card.html +1 -1
- package/components/_cards/charts-21-worldmap.card.html +1 -1
- package/components/_cards/charts-22-gantt.card.html +1 -1
- package/components/_cards/charts-23-bar3d.card.html +1 -1
- package/components/_cards/charts-24-pie3d.card.html +1 -1
- package/components/_cards/charts-25-surface3d.card.html +1 -1
- package/components/_cards/charts-26-scatter3d.card.html +1 -1
- package/components/_cards/charts-27-ribbon3d.card.html +1 -1
- package/components/_cards/charts-3-bar.card.html +1 -1
- package/components/_cards/charts-4-barlist.card.html +1 -1
- package/components/_cards/charts-5-donut.card.html +1 -1
- package/components/_cards/charts-6-scatter.card.html +1 -1
- package/components/_cards/charts-7-heatmap.card.html +1 -1
- package/components/_cards/charts-8-waterfall.card.html +1 -1
- package/components/_cards/charts-9-bullet.card.html +1 -1
- package/components/_cards/charts-99-fullpage.card.html +1 -1
- package/components/_cards/checkbox.card.html +1 -1
- package/components/_cards/codeblock.card.html +1 -1
- package/components/_cards/combobox.card.html +1 -1
- package/components/_cards/context-menu.card.html +1 -1
- package/components/_cards/date-picker.card.html +1 -1
- package/components/_cards/diffvalue.card.html +1 -1
- package/components/_cards/divider.card.html +1 -1
- package/components/_cards/drawer.card.html +1 -1
- package/components/_cards/dropdown-menu.card.html +1 -1
- package/components/_cards/dropzone.card.html +1 -1
- package/components/_cards/empty-state.card.html +1 -1
- package/components/_cards/filter-bar.card.html +1 -1
- package/components/_cards/icon-button.card.html +1 -1
- package/components/_cards/icons-0-usage.card.html +2 -2
- package/components/_cards/icons-1-core.card.html +1 -1
- package/components/_cards/icons-2-feather.card.html +1 -1
- package/components/_cards/icons-2b-lucide.card.html +1 -1
- package/components/_cards/icons-3-unicons.card.html +1 -1
- package/components/_cards/icons-4-datasource.card.html +1 -1
- package/components/_cards/input.card.html +1 -1
- package/components/_cards/kbd.card.html +5 -7
- package/components/_cards/keyvalue.card.html +1 -1
- package/components/_cards/listrow.card.html +1 -1
- package/components/_cards/loading.card.html +1 -1
- package/components/_cards/log-viewer.card.html +1 -1
- package/components/_cards/modal.card.html +8 -2
- package/components/_cards/multi-select.card.html +1 -1
- package/components/_cards/number-stepper.card.html +1 -1
- package/components/_cards/pagination.card.html +1 -1
- package/components/_cards/people.card.html +1 -1
- package/components/_cards/pipeline-node.card.html +1 -1
- package/components/_cards/pipeline-palette.card.html +1 -1
- package/components/_cards/radio.card.html +1 -1
- package/components/_cards/relative-time-picker.card.html +1 -1
- package/components/_cards/search-global.card.html +72 -0
- package/components/_cards/search.card.html +1 -1
- package/components/_cards/segmented-control.card.html +1 -1
- package/components/_cards/select.card.html +1 -1
- package/components/_cards/slider.card.html +1 -1
- package/components/_cards/smart-table.card.html +1 -1
- package/components/_cards/spreadsheet.card.html +1 -1
- package/components/_cards/stepper.card.html +1 -1
- package/components/_cards/table.card.html +10 -1
- package/components/_cards/tabs.card.html +1 -1
- package/components/_cards/tag.card.html +1 -1
- package/components/_cards/textarea.card.html +1 -1
- package/components/_cards/timeline.card.html +1 -1
- package/components/_cards/toast.card.html +11 -1
- package/components/_cards/toggles.card.html +1 -1
- package/components/_cards/tooltip.card.html +1 -1
- package/components/_cards/tree.card.html +22 -1
- package/components/_vue/index.mjs +2 -0
- package/components/buttons/Button.d.ts +14 -5
- package/components/buttons/Button.jsx +45 -8
- package/components/buttons/Button.vue +40 -38
- package/components/buttons/Button.vue.d.ts +4 -1
- package/components/buttons/ai-button.js +54 -0
- package/components/buttons/buttons.prompt.md +7 -2
- package/components/data/Table.d.ts +5 -0
- package/components/data/Table.jsx +10 -5
- package/components/data/Table.vue +9 -3
- package/components/data/data.prompt.md +2 -0
- package/components/data/row-open.js +13 -0
- package/components/display/Kbd.d.ts +3 -1
- package/components/display/Kbd.jsx +8 -3
- package/components/display/Kbd.vue +7 -3
- package/components/display/display.prompt.md +3 -1
- package/components/feedback/Modal.d.ts +65 -7
- package/components/feedback/Modal.jsx +52 -22
- package/components/feedback/Modal.vue +24 -6
- package/components/feedback/Toast.vue +7 -7
- package/components/feedback/ToastStack.vue +37 -4
- package/components/feedback/feedback.prompt.md +6 -0
- package/components/feedback/modal-behavior.js +50 -0
- package/components/forms/GlobalSearch.d.ts +83 -0
- package/components/forms/GlobalSearch.jsx +172 -0
- package/components/forms/GlobalSearch.vue +161 -0
- package/components/forms/forms.prompt.md +7 -0
- package/components/forms/global-search.js +161 -0
- package/components/icons/Icon.d.ts +2 -0
- package/components/icons/Icon.jsx +14 -6
- package/components/icons/Icon.vue +11 -4
- package/components/icons/icon-paths.js +4 -2
- package/components/index.d.ts +1 -0
- package/components/index.js +1 -0
- package/components/navigation/TopBar.d.ts +9 -3
- package/components/navigation/TopBar.jsx +5 -7
- package/components/navigation/TopBar.vue +5 -6
- package/components/navigation/Tree.d.ts +5 -1
- package/components/navigation/Tree.jsx +9 -7
- package/components/navigation/Tree.vue +7 -6
- package/components/navigation/navigation.prompt.md +2 -0
- package/covers.js +4 -2
- package/gallery.html +1 -1
- package/guidelines/brand-logo.html +1 -1
- package/guidelines/colors-schemes.html +1 -1
- package/guidelines/colors-transparency.html +1 -1
- package/guidelines/foundations-borders.html +1 -1
- package/guidelines/foundations-chrome.html +1 -1
- package/guidelines/foundations-motion.html +1 -1
- package/guidelines/foundations-size-tiers.html +1 -1
- package/guidelines/foundations-spacing.html +1 -1
- package/guidelines/interaction-states.html +1 -1
- package/guidelines/type-extended.html +1 -1
- package/guidelines/type-mono.html +1 -1
- package/guidelines/type-scale.html +1 -1
- package/guidelines/type-weights.html +1 -1
- package/index.html +10 -10
- package/package.json +7 -6
- package/readme.md +4 -4
- package/readme.zh-CN.md +4 -4
- package/site-nav.js +1 -1
- package/styles.css +1 -0
- package/tokens/ai.css +17 -0
- package/tokens/colors.css +1 -1
- package/tokens/tokens.d.ts +28 -0
- package/tokens/tokens.js +47 -5
- package/tokens/tokens.json +123 -5
- package/components/feedback/_internal/MessageBoxView.vue +0 -237
- package/components/feedback/_internal/MessageView.vue +0 -73
- package/components/feedback/message.js +0 -68
- /package/{components/feedback → _vue-services}/message-box.d.ts +0 -0
- /package/{components/feedback → _vue-services}/message-box.js +0 -0
- /package/{components/feedback → _vue-services}/message.d.ts +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
// Global Search (TopBar instance of Search) — same props and behaviour as GlobalSearch.jsx; shared logic in ./global-search.js.
|
|
3
|
+
// Collapsed: a quiet field with a ⌘K / Ctrl K hint. Open: a 720px card over the top bar with a scrim. Keys: ↑↓ select · ↵ open ·
|
|
4
|
+
// ⌘↵ / Ctrl Enter ask Prophet AI · Esc back / close. It never shows AI answers: AI entries close the card and emit ask(question).
|
|
5
|
+
// items (local, matched on title / keywords) or query(term) → items | Promise (300ms debounce). deepSearch(term, update) → cancel fn.
|
|
6
|
+
// Events: select(item) · ask(question) · input(value).
|
|
7
|
+
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
|
8
|
+
import Icon from '../icons/Icon.vue'
|
|
9
|
+
import IconButton from '../buttons/IconButton.vue'
|
|
10
|
+
import Button from '../buttons/Button.vue'
|
|
11
|
+
import Kbd from '../display/Kbd.vue'
|
|
12
|
+
import SegmentedControl from '../tabs/SegmentedControl.vue'
|
|
13
|
+
import Spinner from '../feedback/Spinner.vue'
|
|
14
|
+
import Skeleton from '../feedback/Skeleton.vue'
|
|
15
|
+
import { GS_ICONS, gsLabels, gsFill, gsIsMac, gsMod, gsFilter, gsSplit, gsRows, gsPos, gsReveal, gsStyles as S, gsAiTile } from './global-search.js'
|
|
16
|
+
|
|
17
|
+
const props = defineProps({
|
|
18
|
+
placeholder: String, inputPlaceholder: String, items: { type: Array, default: () => [] }, query: Function,
|
|
19
|
+
recentSearches: { type: Array, default: () => [] }, recent: { type: Array, default: () => [] }, prompts: { type: Array, default: () => [] },
|
|
20
|
+
deepSearch: Function, labels: Object, inline: Boolean, defaultOpen: Boolean, defaultQuery: { type: String, default: '' }, defaultDeep: Object, style: Object,
|
|
21
|
+
})
|
|
22
|
+
const emit = defineEmits(['select', 'ask', 'input'])
|
|
23
|
+
const L = computed(() => gsLabels(props.labels))
|
|
24
|
+
const open = ref(!!(props.inline || props.defaultOpen)), q = ref(props.defaultQuery), scope = ref(0)
|
|
25
|
+
const found = ref(null), loading = ref(false), sel = ref(0), hover = ref(false), stepsOpen = ref(false)
|
|
26
|
+
const deep = ref(props.defaultDeep ? { query: props.defaultQuery, ...props.defaultDeep } : null)
|
|
27
|
+
const pos = ref({ left: 8, width: 720, maxH: 640 })
|
|
28
|
+
const fieldEl = ref(null), inputEl = ref(null), bodyEl = ref(null)
|
|
29
|
+
const mac = gsIsMac()
|
|
30
|
+
let cancelDeep = null, now = !!props.defaultQuery, timer = null, token = 0
|
|
31
|
+
const term = computed(() => q.value.trim())
|
|
32
|
+
|
|
33
|
+
const stopDeep = () => { if (cancelDeep) cancelDeep(); cancelDeep = null }
|
|
34
|
+
const openCard = () => { if (props.inline) return; pos.value = gsPos(fieldEl.value); open.value = true }
|
|
35
|
+
const close = () => { if (props.inline) return; stopDeep(); deep.value = null; open.value = false; if (fieldEl.value) fieldEl.value.focus() }
|
|
36
|
+
const ask = (text) => { close(); emit('ask', text || '') }
|
|
37
|
+
const startDeep = () => {
|
|
38
|
+
stopDeep(); stepsOpen.value = false
|
|
39
|
+
deep.value = { query: term.value, status: 'running', steps: [] }
|
|
40
|
+
const c = props.deepSearch && props.deepSearch(term.value, (s) => { if (deep.value) deep.value = { ...deep.value, ...s } })
|
|
41
|
+
cancelDeep = typeof c === 'function' ? c : null
|
|
42
|
+
}
|
|
43
|
+
const exitDeep = () => { stopDeep(); deep.value = null }
|
|
44
|
+
|
|
45
|
+
const onKeyK = (e) => { if (gsMod(e, mac) && e.key && e.key.toLowerCase() === 'k') { e.preventDefault(); openCard() } }
|
|
46
|
+
onMounted(() => { if (!props.inline) { document.addEventListener('keydown', onKeyK); if (props.defaultOpen) pos.value = gsPos(fieldEl.value) } })
|
|
47
|
+
onBeforeUnmount(() => { document.removeEventListener('keydown', onKeyK); stopDeep(); clearTimeout(timer) })
|
|
48
|
+
watch(open, (v) => { if (v && !props.inline) nextTick(() => inputEl.value && inputEl.value.focus()) })
|
|
49
|
+
|
|
50
|
+
const search = () => {
|
|
51
|
+
clearTimeout(timer); const t = term.value, my = ++token
|
|
52
|
+
if (!t) { found.value = null; loading.value = false; return }
|
|
53
|
+
const run = () => {
|
|
54
|
+
loading.value = true
|
|
55
|
+
Promise.resolve(props.query ? props.query(t) : gsFilter(props.items, t)).then((r) => { if (my === token) { found.value = r || []; loading.value = false } }, () => { if (my === token) { found.value = []; loading.value = false } })
|
|
56
|
+
}
|
|
57
|
+
if (now) { now = false; run() } else timer = setTimeout(run, 300)
|
|
58
|
+
}
|
|
59
|
+
watch(term, search, { immediate: true })
|
|
60
|
+
watch([found, scope], () => { sel.value = 0 })
|
|
61
|
+
|
|
62
|
+
const model = computed(() => gsRows({ term: term.value, found: found.value, loading: loading.value, scope: scope.value, recent: props.recent, prompts: props.prompts, deep: deep.value, hasDeep: !!props.deepSearch, L: L.value }))
|
|
63
|
+
const rows = computed(() => model.value.rows)
|
|
64
|
+
const cur = computed(() => Math.max(0, Math.min(sel.value, rows.value.length - 1)))
|
|
65
|
+
const bodyRows = computed(() => rows.value.filter((r) => r.kind !== 'ask' && r.kind !== 'deep'))
|
|
66
|
+
const pinRows = computed(() => rows.value.filter((r) => r.kind === 'ask' || r.kind === 'deep'))
|
|
67
|
+
watch(cur, (i) => nextTick(() => gsReveal(bodyEl.value, i)))
|
|
68
|
+
|
|
69
|
+
const activate = (r) => {
|
|
70
|
+
if (!r) return
|
|
71
|
+
if (r.kind === 'result' || r.kind === 'recent') { close(); emit('select', r.item); if (r.item.onSelect) r.item.onSelect() }
|
|
72
|
+
else if (r.kind === 'prompt') ask(r.text)
|
|
73
|
+
else if (r.kind === 'ask') ask(term.value)
|
|
74
|
+
else if (r.kind === 'deep') startDeep()
|
|
75
|
+
}
|
|
76
|
+
const change = (v) => { if (deep.value) exitDeep(); q.value = v; sel.value = 0; emit('input', v) }
|
|
77
|
+
const pickChip = (v) => { now = true; change(v) }
|
|
78
|
+
const clear = () => { change(''); if (inputEl.value) inputEl.value.focus() }
|
|
79
|
+
const key = (e) => {
|
|
80
|
+
if (e.isComposing) return
|
|
81
|
+
const n = rows.value.length
|
|
82
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { if (!n) return; e.preventDefault(); sel.value = (cur.value + (e.key === 'ArrowDown' ? 1 : -1) + n) % n }
|
|
83
|
+
else if (e.key === 'Enter') { e.preventDefault(); if (gsMod(e, mac)) ask(term.value); else activate(rows.value[cur.value]) }
|
|
84
|
+
else if (e.key === 'Escape') { e.preventDefault(); if (deep.value) exitDeep(); else close() }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const parts = (r, mark) => (mark ? gsSplit(r.item.title, term.value) : [r.item.title, '', ''])
|
|
88
|
+
const kindOf = (it) => it.kind || 'feature'
|
|
89
|
+
const steps = computed(() => (deep.value && deep.value.steps) || [])
|
|
90
|
+
const runIdx = computed(() => steps.value.findIndex((s) => s.state === 'running'))
|
|
91
|
+
const deepDone = computed(() => !!deep.value && (deep.value.status === 'done' || runIdx.value < 0))
|
|
92
|
+
const concl = computed(() => (deep.value && deep.value.conclusion ? [].concat(deep.value.conclusion) : null))
|
|
93
|
+
const cardStyle = computed(() => ({ ...S.card, ...(props.inline ? props.style : S.cardFixed(pos.value)) }))
|
|
94
|
+
const fieldStyle = computed(() => ({ ...S.field(hover.value), ...props.style }))
|
|
95
|
+
const ai = (on) => gsAiTile(on)
|
|
96
|
+
const scopeLabel = computed({ get: () => L.value.scopes[scope.value], set: (v) => { scope.value = Math.max(0, L.value.scopes.indexOf(v)) } })
|
|
97
|
+
</script>
|
|
98
|
+
<template>
|
|
99
|
+
<button v-if="!inline" ref="fieldEl" type="button" data-field="" aria-haspopup="dialog" :aria-expanded="open" @click="openCard" @mouseenter="hover = true" @mouseleave="hover = false" :style="fieldStyle">
|
|
100
|
+
<span :style="S.fieldIcon"><Icon name="search" :size="16" /></span><span :style="S.fieldLabel">{{ placeholder || L.field }}</span><Kbd :keys="['mod', 'K']" />
|
|
101
|
+
</button>
|
|
102
|
+
<div v-if="open && !inline" :style="S.scrim" @mousedown="close" />
|
|
103
|
+
<div v-if="open" role="dialog" :aria-modal="inline ? undefined : 'true'" :aria-label="L.input" data-size="md" @keydown="key" :style="cardStyle">
|
|
104
|
+
<div :style="S.inputRow">
|
|
105
|
+
<span :style="S.lead"><Spinner v-if="loading" :size="14" color="var(--text-secondary)" /><Icon v-else :name="deep ? 'text-search' : 'search'" :size="16" /></span>
|
|
106
|
+
<input ref="inputEl" :value="q" @input="change($event.target.value)" :placeholder="inputPlaceholder || L.input" :aria-label="inputPlaceholder || L.input" :style="S.input" />
|
|
107
|
+
<span :style="S.trail"><IconButton v-if="q" icon="x" muted :label="L.clear" :title="L.clear" @click="clear" /><Button variant="ai" @click="ask(term)">{{ L.ask }}</Button></span>
|
|
108
|
+
</div>
|
|
109
|
+
<div v-if="term && !deep" :style="S.scope"><SegmentedControl size="sm" :options="L.scopes" v-model="scopeLabel" /><span :style="S.count">{{ loading ? L.searching : found ? gsFill(L.results, { n: model.total }) : '' }}</span></div>
|
|
110
|
+
|
|
111
|
+
<div v-if="deep" :style="{ ...S.body, padding: '0' }"><div :style="S.deep">
|
|
112
|
+
<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>
|
|
113
|
+
<div v-if="steps.length" :style="S.steps">
|
|
114
|
+
<button type="button" data-wash="" :aria-expanded="stepsOpen" :style="S.toggle" @click="stepsOpen = !stepsOpen">
|
|
115
|
+
<template v-if="deepDone"><span :style="{ ...S.stepIcon, color: 'var(--success)' }"><Icon name="circle-check" :size="14" /></span><span>{{ gsFill(L.stepsDone, { n: steps.length }) }}</span><span :style="S.muted">{{ deep.elapsed }}</span></template>
|
|
116
|
+
<template v-else><span :style="S.stepIcon"><Spinner :size="12" color="var(--text-secondary)" /></span><span>{{ steps[runIdx].label }}…</span><span :style="S.muted">{{ gsFill(L.stepOf, { i: runIdx + 1, n: steps.length }) }}</span></template>
|
|
117
|
+
<span :style="S.grow" /><span :style="{ ...S.stepIcon, color: 'var(--text-muted)', transform: stepsOpen ? 'rotate(180deg)' : 'none' }"><Icon name="chevron-down" :size="14" /></span>
|
|
118
|
+
</button>
|
|
119
|
+
<template v-if="stepsOpen"><div v-for="(s, i) in steps" :key="i" :style="S.stepLine">
|
|
120
|
+
<span :style="{ ...S.stepIcon, color: 'var(--success)' }"><Icon v-if="s.state === 'done'" name="circle-check" :size="14" /><Spinner v-else-if="s.state === 'running'" :size="12" color="var(--text-secondary)" /><span v-else :style="S.pending" /></span>
|
|
121
|
+
<span :style="{ color: s.state === 'pending' ? 'var(--text-muted)' : 'var(--text-body)' }">{{ s.label }}</span><span v-if="s.meta" :style="S.muted">{{ s.meta }}</span>
|
|
122
|
+
</div></template>
|
|
123
|
+
</div>
|
|
124
|
+
<div v-if="concl" :style="S.conclusion">
|
|
125
|
+
<span :style="S.concLabel">{{ L.conclusion }}</span>
|
|
126
|
+
<div :style="S.concText"><span v-for="(p, i) in concl" :key="i">{{ p }}</span></div>
|
|
127
|
+
<div v-if="deep.sources && deep.sources.length" :style="S.sources"><span v-for="(s, i) in deep.sources" :key="i" :style="S.source"><Icon name="file-text" :size="12" />{{ s }}</span></div>
|
|
128
|
+
<div data-size="sm" :style="S.actions"><Button variant="primary-soft" icon="file-text" @click="deep.onOpenReport && deep.onOpenReport()">{{ L.openReport }}</Button><Button variant="ghost" icon="prophet-sparkle" @click="ask(deep.query)">{{ L.followUp }}</Button></div>
|
|
129
|
+
</div>
|
|
130
|
+
</div></div>
|
|
131
|
+
|
|
132
|
+
<div v-else ref="bodyEl" role="listbox" :style="S.body">
|
|
133
|
+
<template v-if="!term && recentSearches.length"><div :style="S.head"><span>{{ L.recentSearches }}</span></div><div :style="S.chips"><button v-for="s in recentSearches" :key="s" type="button" :style="S.chip" @mousedown.prevent @click="pickChip(s)"><span :style="S.chipIcon"><Icon name="history" :size="12" /></span>{{ s }}</button></div></template>
|
|
134
|
+
<template v-if="term && loading"><div :style="S.head"><Skeleton :width="'64px'" :height="'10px'" /></div><div v-for="i in 4" :key="i" :style="S.skelRow"><Skeleton :width="'32px'" :height="'32px'" radius="var(--radius-md)" /><span :style="S.skelText"><Skeleton width="46%" :height="'12px'" /><Skeleton width="28%" :height="'10px'" /></span></div></template>
|
|
135
|
+
<div v-else-if="term && found && !bodyRows.length" :style="S.empty">{{ L.noResults }}</div>
|
|
136
|
+
<template v-else><template v-for="r in bodyRows" :key="r.i">
|
|
137
|
+
<div v-if="r.head" :style="S.head"><span>{{ r.head.label }}</span><button v-if="r.head.more" type="button" :style="S.more" @mousedown.prevent @click="scope = r.head.more.scope">{{ gsFill(L.viewAll, { n: r.head.more.n }) }}</button></div>
|
|
138
|
+
<div :data-gs-row="r.i" role="option" :aria-selected="r.i === cur" :style="S.row(r.i === cur)" @mouseenter="sel = r.i" @mousedown.prevent @click="activate(r)">
|
|
139
|
+
<template v-if="r.kind === 'prompt'"><span :style="ai(r.i === cur).outer"><span :style="ai(r.i === cur).inner"><span :style="ai(r.i === cur).tint" /><Icon name="prophet-sparkle" :size="16" :gradient="r.i === cur" :style="ai(r.i === cur).icon" /></span></span><span :style="S.text"><span :style="S.title">{{ r.text }}</span></span></template>
|
|
140
|
+
<template v-else>
|
|
141
|
+
<span :style="S.tile(kindOf(r.item) === 'document' ? r.item.tone : null)"><Icon :name="r.item.icon || GS_ICONS[kindOf(r.item)]" :size="16" /></span>
|
|
142
|
+
<span :style="S.text"><span :style="S.title">{{ parts(r, r.kind === 'result')[0] }}<span v-if="parts(r, r.kind === 'result')[1]" :style="S.mark">{{ parts(r, r.kind === 'result')[1] }}</span>{{ parts(r, r.kind === 'result')[2] }}</span><span v-if="r.item.sub" :style="S.sub">{{ r.item.sub }}</span></span>
|
|
143
|
+
<span v-if="r.item.meta" :style="S.meta">{{ r.item.meta }}</span>
|
|
144
|
+
</template>
|
|
145
|
+
<Kbd v-if="r.i === cur" :keys="['enter']" />
|
|
146
|
+
</div>
|
|
147
|
+
</template></template>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div v-if="!deep && term" :style="S.pinned">
|
|
151
|
+
<div v-for="r in pinRows" :key="r.i" :data-gs-row="r.i" role="option" :aria-selected="r.i === cur" :style="S.row(r.i === cur)" @mouseenter="sel = r.i" @mousedown.prevent @click="activate(r)">
|
|
152
|
+
<template v-if="r.kind === 'ask'"><span :style="ai(r.i === cur).outer"><span :style="ai(r.i === cur).inner"><span :style="ai(r.i === cur).tint" /><Icon name="prophet-sparkle" :size="16" :gradient="r.i === cur" :style="ai(r.i === cur).icon" /></span></span><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']" /></template>
|
|
153
|
+
<template v-else><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><Kbd v-if="r.i === cur" :keys="['enter']" /></template>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<div :style="S.footer">
|
|
157
|
+
<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>
|
|
158
|
+
<span :style="S.hint"><Icon name="lock" :size="12" />{{ L.privacy }}</span>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</template>
|
|
@@ -27,3 +27,10 @@ Everything is 12px text in 36px controls; readOnly inputs go #fafafa; disabled =
|
|
|
27
27
|
|
|
28
28
|
## RelativeTimePicker
|
|
29
29
|
Filter by "how long ago" (logs, monitoring, inbox): `<RelativeTimePicker value={{amount:1,unit:"Hour"}} onChange={(v, date) => …}/>` — a field-shaped trigger at the tier height; the dialog has an amount input and a unit select (`allowUnlimited` adds "Any time", which resolves to `null`). `labels` localises every string (unit labels: one string, or `{ one, other }` for plural languages); `formatValue` / `formatDate` override the summary and the preview. Vue: `v-model` + `@change(value, date)`.
|
|
30
|
+
|
|
31
|
+
`GlobalSearch` — the TopBar search (TopBar renders it; pass props through `search`). Collapsed it's a field styled like SearchInput (surface fill, `--border-control` hairline, r8) with a ⌘K / Ctrl K hint — **GlobalSearch exception:** its placeholder is `--text-secondary` (≥4.5:1, the platform's main entry prompt) and hover fills `--bg-inset` as well as darkening the border to `--border-strong`. Plain `SearchInput` keeps its muted placeholder and border-only hover; open it's a 720px card: input row with `Button variant="ai"`, scope bar, grouped results (Features 4 · Documents 5 · Dimension Members 3 · Actions 2 under All), pinned "Ask Prophet AI" and "Deep Search" rows, keyboard footer. Keys: ↑↓ · ↵ open · ⌘↵ / Ctrl Enter ask Prophet AI · Esc back / close (Tab keeps normal focus order). It never shows AI answers — every AI entry closes the card and calls `onAsk(question)`.
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
<GlobalSearch items={items} recent={recent} prompts={prompts} onSelect={open} onAsk={openProphetPanel}/>
|
|
35
|
+
<GlobalSearch onQuery={(term) => api.search(term)} onDeepSearch={(term, update) => runDeep(term, update)} labels={GLOBAL_SEARCH_LABELS.zh}/>
|
|
36
|
+
```
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* GlobalSearch — shared by GlobalSearch.jsx and GlobalSearch.vue: copy, matching, grouping, row model and styles.
|
|
2
|
+
Lengths are strings so the same style objects work in React and Vue. */
|
|
3
|
+
import "../buttons/ai-button.js"; // registers --kb-ai-angle + kb-ai-spin for the Prophet AI tile
|
|
4
|
+
|
|
5
|
+
export const GLOBAL_SEARCH_LABELS = {
|
|
6
|
+
en: {
|
|
7
|
+
field: "Search features, documents, or ask Prophet AI", input: "Search features, documents, data, or ask a question", ask: "Ask Prophet AI", clear: "Clear",
|
|
8
|
+
scopes: ["All", "Features", "Documents", "Data", "Actions"],
|
|
9
|
+
groups: { feature: "Features", document: "Documents", member: "Dimension Members", action: "Actions" },
|
|
10
|
+
recentSearches: "Recent Searches", recent: "Recent", prompts: "Try Asking Prophet AI", viewAll: "View all {n}",
|
|
11
|
+
results: "{n} results", searching: "Searching…", noResults: "No matches. Ask Prophet AI or try Deep Search.",
|
|
12
|
+
askRow: "Ask Prophet AI: “{q}”", askSub: "Answers from documents, models and data you can access",
|
|
13
|
+
deepRow: "Deep Search “{q}”", deepSub: "Multi-step search across document text, model data and logs you can access · about 1 min",
|
|
14
|
+
deepTitle: "Deep Search: {q}", running: "Running · about 1 min", doneStatus: "Done · {t} · {n} citations",
|
|
15
|
+
stepsDone: "Completed {n} steps", stepOf: "Step {i} of {n}", conclusion: "Conclusion", openReport: "Open Full Report", followUp: "Ask Follow-up",
|
|
16
|
+
select: "Select", close: "Close / Back", privacy: "Reads only content you can access",
|
|
17
|
+
},
|
|
18
|
+
zh: {
|
|
19
|
+
field: "搜索功能、文档,或问方知", input: "搜索功能、文档、数据,或直接提问", ask: "问方知", clear: "清除",
|
|
20
|
+
scopes: ["全部", "功能", "文档", "数据", "操作"],
|
|
21
|
+
groups: { feature: "功能", document: "文档", member: "维度成员", action: "操作" },
|
|
22
|
+
recentSearches: "最近搜索", recent: "最近访问", prompts: "试试问方知", viewAll: "查看全部 {n}",
|
|
23
|
+
results: "{n} 个结果", searching: "正在搜索…", noResults: "没有匹配的结果,可以问方知或使用深度搜索。",
|
|
24
|
+
askRow: "问方知:“{q}”", askSub: "基于你有权限的文档、模型和数据作答",
|
|
25
|
+
deepRow: "深度搜索“{q}”", deepSub: "在你有权限的文档正文、模型数据和日志中多步检索 · 约 1 分钟",
|
|
26
|
+
deepTitle: "深度搜索:{q}", running: "进行中 · 约 1 分钟", doneStatus: "已完成 · {t} · {n} 条引用",
|
|
27
|
+
stepsDone: "已完成 {n} 个步骤", stepOf: "第 {i} / {n} 步", conclusion: "结论", openReport: "打开完整报告", followUp: "就此追问",
|
|
28
|
+
select: "选择", close: "关闭 / 返回", privacy: "仅读取你有权限的内容",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const GS_KINDS = ["feature", "document", "member", "action"];
|
|
33
|
+
const LIMITS = { feature: 4, document: 5, member: 3, action: 2 };
|
|
34
|
+
export const GS_ICONS = { feature: "layout-grid", document: "file-text", member: "database", action: "zap" };
|
|
35
|
+
|
|
36
|
+
export const gsLabels = (labels) => ({ ...GLOBAL_SEARCH_LABELS.en, ...labels, groups: { ...GLOBAL_SEARCH_LABELS.en.groups, ...(labels && labels.groups) } });
|
|
37
|
+
export const gsFill = (s, vars) => String(s).replace(/\{(\w+)\}/g, (_, k) => (vars[k] != null ? vars[k] : ""));
|
|
38
|
+
export const gsIsMac = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test((navigator.userAgentData && navigator.userAgentData.platform) || navigator.platform || navigator.userAgent || "");
|
|
39
|
+
/** The OS's command modifier: ⌘ on macOS, Ctrl elsewhere. */
|
|
40
|
+
export const gsMod = (e, mac) => (mac ? e.metaKey : e.ctrlKey) && !e.altKey;
|
|
41
|
+
|
|
42
|
+
/** Case-insensitive substring on the title, falling back to keywords (path, kind, owner …). */
|
|
43
|
+
export function gsFilter(items, term) {
|
|
44
|
+
const n = term.trim().toLowerCase();
|
|
45
|
+
if (!n) return [];
|
|
46
|
+
return items.filter((it) => String(it.title).toLowerCase().includes(n) || (it.keywords || []).some((k) => String(k).toLowerCase().includes(n)));
|
|
47
|
+
}
|
|
48
|
+
/** [before, match, after] for highlighting the matched substring of a title. */
|
|
49
|
+
export function gsSplit(title, term) {
|
|
50
|
+
const t = String(title), n = (term || "").trim().toLowerCase(), i = n ? t.toLowerCase().indexOf(n) : -1;
|
|
51
|
+
return i < 0 ? [t, "", ""] : [t.slice(0, i), t.slice(i, i + n.length), t.slice(i + n.length)];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Every selectable row in visual order (↑↓ walks this list), with group headers attached to the first row of each group. */
|
|
55
|
+
export function gsRows({ term, found, loading, scope, recent, prompts, deep, hasDeep, L }) {
|
|
56
|
+
const rows = [];
|
|
57
|
+
let total = 0;
|
|
58
|
+
if (deep) return { rows, total };
|
|
59
|
+
if (!term) {
|
|
60
|
+
recent.forEach((item, j) => rows.push({ kind: "recent", item, head: j === 0 ? { label: L.recent } : null }));
|
|
61
|
+
prompts.forEach((text, j) => rows.push({ kind: "prompt", text, head: j === 0 ? { label: L.prompts } : null }));
|
|
62
|
+
} else {
|
|
63
|
+
if (found && !loading) {
|
|
64
|
+
const kinds = scope ? [GS_KINDS[scope - 1]] : GS_KINDS;
|
|
65
|
+
kinds.forEach((k) => {
|
|
66
|
+
const all = found.filter((it) => (it.kind || "feature") === k);
|
|
67
|
+
total += all.length;
|
|
68
|
+
const shown = scope ? all : all.slice(0, LIMITS[k]);
|
|
69
|
+
shown.forEach((item, j) => rows.push({ kind: "result", item, head: j === 0 ? { label: L.groups[k], more: !scope && all.length > shown.length ? { n: all.length, scope: GS_KINDS.indexOf(k) + 1 } : null } : null }));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
rows.push({ kind: "ask" });
|
|
73
|
+
if (hasDeep) rows.push({ kind: "deep" });
|
|
74
|
+
}
|
|
75
|
+
rows.forEach((r, i) => { r.i = i; });
|
|
76
|
+
return { rows, total };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Where the open card goes: 8px left of the collapsed field, 8px from the top, over the TopBar. */
|
|
80
|
+
export function gsPos(el) {
|
|
81
|
+
const vw = window.innerWidth, vh = window.innerHeight, r = el ? el.getBoundingClientRect() : { left: 16 };
|
|
82
|
+
const width = Math.min(720, vw - 16);
|
|
83
|
+
return { left: Math.max(8, Math.min(r.left - 8, vw - width - 8)), width, maxH: vh - 16 };
|
|
84
|
+
}
|
|
85
|
+
/** Keep the selected row visible inside the scrolling body (no scrollIntoView). */
|
|
86
|
+
export function gsReveal(body, i) {
|
|
87
|
+
if (!body) return;
|
|
88
|
+
const el = body.querySelector('[data-gs-row="' + i + '"]');
|
|
89
|
+
if (!el) return;
|
|
90
|
+
const top = el.offsetTop - body.offsetTop, bottom = top + el.offsetHeight;
|
|
91
|
+
if (top < body.scrollTop) body.scrollTop = top - 6;
|
|
92
|
+
else if (bottom > body.scrollTop + body.clientHeight) body.scrollTop = bottom - body.clientHeight + 6;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const T = (s) => ({ fontFamily: "var(--font-sans)", fontSize: "var(--text-" + s + ")", lineHeight: "var(--leading-" + s + ")", letterSpacing: "var(--tracking-" + s + ")" });
|
|
96
|
+
const ONE = { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" };
|
|
97
|
+
const FADE = "opacity 300ms var(--ease-out)";
|
|
98
|
+
const tileBase = { width: "32px", height: "32px", flexShrink: "0", display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "var(--radius-md)", border: "1px solid var(--border-card)", background: "var(--bg-surface)", color: "var(--text-secondary)", boxSizing: "border-box" };
|
|
99
|
+
|
|
100
|
+
export const gsStyles = {
|
|
101
|
+
/* Collapsed field: SearchInput's box (surface fill, --border-control hairline, r8). GlobalSearch exception: the placeholder is
|
|
102
|
+
--text-secondary (≥4.5:1, it is the platform's main entry prompt) and hover also fills --bg-inset; border darkens via [data-field]. */
|
|
103
|
+
field: (hover) => ({ display: "inline-flex", alignItems: "center", gap: "8px", minWidth: "400px", width: "max-content", maxWidth: "100%", height: "32px", padding: "0 6px 0 10px", boxSizing: "border-box", borderRadius: "var(--radius-md)", background: hover ? "var(--bg-inset)" : "var(--bg-surface)", border: "1px solid var(--field-border, var(--border-control))", cursor: "pointer", transition: "var(--t-hover)", ...T("base"), color: "var(--text-secondary)", textAlign: "left", flexShrink: "1" }),
|
|
104
|
+
fieldIcon: { display: "inline-flex", color: "var(--text-muted)", flexShrink: "0" },
|
|
105
|
+
fieldLabel: { flex: "1 0 auto", paddingRight: "8px", whiteSpace: "nowrap" },
|
|
106
|
+
scrim: { position: "fixed", inset: "0", background: "var(--bg-scrim)", zIndex: "var(--z-modal)", animation: "kb-fade-in var(--dur) var(--ease-out)" },
|
|
107
|
+
card: { display: "flex", flexDirection: "column", width: "720px", maxWidth: "100%", background: "var(--bg-raised)", border: "1px solid var(--border-card)", borderRadius: "var(--radius-lg)", boxShadow: "var(--shadow-modal)", overflow: "hidden", boxSizing: "border-box", color: "var(--text-body)", textAlign: "left" },
|
|
108
|
+
cardFixed: (p) => ({ position: "fixed", top: "8px", left: p.left + "px", width: p.width + "px", maxHeight: p.maxH + "px", zIndex: "var(--z-modal)", animation: "kb-rise var(--dur-slow) var(--ease-out)" }),
|
|
109
|
+
inputRow: { display: "flex", alignItems: "center", gap: "10px", height: "56px", padding: "0 16px", borderBottom: "1px solid var(--border-subtle)", flexShrink: "0", boxSizing: "border-box" },
|
|
110
|
+
lead: { display: "inline-flex", width: "16px", height: "16px", alignItems: "center", justifyContent: "center", color: "var(--text-secondary)", flexShrink: "0" },
|
|
111
|
+
input: { flex: "1 1 auto", minWidth: "0", border: "none", outline: "none", background: "transparent", padding: "0", ...T("md"), color: "var(--text-heading)" },
|
|
112
|
+
trail: { display: "flex", alignItems: "center", gap: "8px", flexShrink: "0" },
|
|
113
|
+
scope: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px", padding: "10px 16px", borderBottom: "1px solid var(--border-subtle)", flexShrink: "0" },
|
|
114
|
+
count: { ...T("xs"), color: "var(--text-muted)", whiteSpace: "nowrap" },
|
|
115
|
+
body: { flex: "1 1 auto", minHeight: "0", overflowY: "auto", padding: "6px", position: "relative" },
|
|
116
|
+
head: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "8px", padding: "8px 10px 4px", ...T("xs"), fontWeight: "var(--weight-medium)", color: "var(--text-muted)" },
|
|
117
|
+
more: { border: "none", background: "transparent", padding: "0", cursor: "pointer", ...T("xs"), color: "var(--link)" },
|
|
118
|
+
row: (on) => ({ display: "flex", alignItems: "center", gap: "10px", minHeight: "44px", padding: "6px 10px", boxSizing: "border-box", borderRadius: "var(--radius-md)", background: on ? "var(--bg-hover)" : "transparent", cursor: "pointer" }),
|
|
119
|
+
tile: (tone) => ({ ...tileBase, ...(tone ? { background: "var(--cat-" + tone + "-bg)", color: "var(--cat-" + tone + ")" } : null) }),
|
|
120
|
+
text: { display: "flex", flexDirection: "column", minWidth: "0", flex: "1 1 auto" },
|
|
121
|
+
title: { ...T("base"), ...ONE, color: "var(--text-heading)" },
|
|
122
|
+
mark: { color: "var(--action-primary)", fontWeight: "var(--weight-medium)" },
|
|
123
|
+
sub: { ...T("xs"), ...ONE, color: "var(--text-secondary)" },
|
|
124
|
+
meta: { ...T("xs"), color: "var(--text-muted)", whiteSpace: "nowrap", flexShrink: "0" },
|
|
125
|
+
chips: { display: "flex", flexWrap: "wrap", gap: "8px", padding: "4px 10px 8px" },
|
|
126
|
+
chip: { display: "inline-flex", alignItems: "center", gap: "6px", height: "28px", padding: "0 10px", borderRadius: "var(--radius-md)", border: "1px solid var(--border-control)", background: "var(--bg-surface)", cursor: "pointer", ...T("xs"), color: "var(--text-body)", boxSizing: "border-box" },
|
|
127
|
+
chipIcon: { display: "inline-flex", color: "var(--text-muted)" },
|
|
128
|
+
empty: { padding: "12px 10px", ...T("base"), color: "var(--text-secondary)" },
|
|
129
|
+
skelRow: { display: "flex", alignItems: "center", gap: "10px", minHeight: "44px", padding: "6px 10px", boxSizing: "border-box" },
|
|
130
|
+
skelText: { display: "flex", flexDirection: "column", gap: "6px", flex: "1 1 auto" },
|
|
131
|
+
pinned: { borderTop: "1px solid var(--border-subtle)", padding: "6px", flexShrink: "0" },
|
|
132
|
+
footer: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "16px", height: "36px", padding: "0 16px", borderTop: "1px solid var(--border-subtle)", background: "var(--bg-surface)", flexShrink: "0", boxSizing: "border-box", ...T("xs"), color: "var(--text-secondary)", whiteSpace: "nowrap" },
|
|
133
|
+
hints: { display: "flex", alignItems: "center", gap: "16px" },
|
|
134
|
+
hint: { display: "inline-flex", alignItems: "center", gap: "6px" },
|
|
135
|
+
deep: { display: "flex", flexDirection: "column", gap: "12px", padding: "12px 16px" },
|
|
136
|
+
deepHead: { display: "flex", alignItems: "center", gap: "10px" },
|
|
137
|
+
deepTitle: { ...T("base"), ...ONE, fontWeight: "var(--weight-medium)", color: "var(--text-heading)" },
|
|
138
|
+
steps: { display: "flex", flexDirection: "column", gap: "2px" },
|
|
139
|
+
toggle: { display: "flex", alignItems: "center", gap: "8px", width: "100%", minHeight: "28px", padding: "0 6px", borderRadius: "var(--radius-md)", border: "none", background: "var(--wash, transparent)", cursor: "pointer", ...T("xs"), color: "var(--text-body)", textAlign: "left", boxSizing: "border-box" },
|
|
140
|
+
stepLine: { display: "flex", alignItems: "center", gap: "8px", minHeight: "24px", padding: "0 6px", ...T("xs") },
|
|
141
|
+
stepIcon: { width: "14px", height: "14px", display: "inline-flex", alignItems: "center", justifyContent: "center", flexShrink: "0" },
|
|
142
|
+
pending: { width: "8px", height: "8px", borderRadius: "var(--radius-full)", border: "1.5px solid var(--border-strong)", boxSizing: "border-box" },
|
|
143
|
+
muted: { color: "var(--text-muted)" },
|
|
144
|
+
grow: { flex: "1 1 auto" },
|
|
145
|
+
conclusion: { display: "flex", flexDirection: "column", gap: "6px", padding: "12px 14px", borderRadius: "var(--radius-lg)", background: "var(--bg-inset)", border: "1px solid var(--border-card)" },
|
|
146
|
+
concLabel: { ...T("xs"), fontWeight: "var(--weight-medium)", color: "var(--text-secondary)" },
|
|
147
|
+
concText: { display: "flex", flexDirection: "column", gap: "4px", ...T("base"), color: "var(--text-body)", textWrap: "pretty" },
|
|
148
|
+
sources: { display: "flex", flexWrap: "wrap", gap: "6px" },
|
|
149
|
+
source: { display: "inline-flex", alignItems: "center", gap: "4px", height: "24px", padding: "0 8px", borderRadius: "var(--radius-sm)", border: "1px solid var(--border-control)", background: "var(--bg-surface)", ...T("xs"), color: "var(--text-body)", boxSizing: "border-box", whiteSpace: "nowrap" },
|
|
150
|
+
actions: { display: "flex", gap: "8px", marginTop: "6px" },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** The 32px Prophet AI tile: hairline tile at rest; selected, it takes the AI hover language (tint, gradient ring at hover speed, gradient glyph), never a glow. */
|
|
154
|
+
export function gsAiTile(on) {
|
|
155
|
+
return {
|
|
156
|
+
outer: { width: "32px", height: "32px", flexShrink: "0", display: "flex", padding: "1px", boxSizing: "border-box", borderRadius: "var(--radius-md)", background: on ? "conic-gradient(from var(--kb-ai-angle), var(--ai-gradient-stops))" : "var(--border-card)", animation: on ? "kb-ai-spin var(--ai-spin-hover) linear infinite" : "none" },
|
|
157
|
+
inner: { position: "relative", flex: "1 1 auto", display: "flex", alignItems: "center", justifyContent: "center", borderRadius: "calc(var(--radius-md) - 1px)", background: "var(--bg-surface)", overflow: "hidden", color: "var(--text-secondary)" },
|
|
158
|
+
tint: { position: "absolute", inset: "0", pointerEvents: "none", background: "linear-gradient(90deg, color-mix(in srgb, var(--ai-gradient-start) var(--ai-tint), transparent), color-mix(in srgb, var(--ai-gradient-end) var(--ai-tint), transparent))", opacity: on ? "1" : "0", transition: FADE },
|
|
159
|
+
icon: { position: "relative" },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -11,6 +11,8 @@ export interface IconProps {
|
|
|
11
11
|
strokeWidth?: number;
|
|
12
12
|
/** Overrides currentColor */
|
|
13
13
|
color?: string;
|
|
14
|
+
/** Strokes the glyph with the AI gradient (--ai-gradient-start → --ai-gradient-end). Prophet AI entry points only; pass [from, to] to override. */
|
|
15
|
+
gradient?: boolean | [string, string];
|
|
14
16
|
style?: React.CSSProperties;
|
|
15
17
|
className?: string;
|
|
16
18
|
}
|
|
@@ -88,8 +88,8 @@ export const ICON_PATHS = {
|
|
|
88
88
|
"time-future": '<path d="M20 6A10 10 0 1 0 22 12"/><path d="M12 7v5h5"/><path d="M21 2v5h-5"/>',
|
|
89
89
|
"time-restatement": '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
|
|
90
90
|
// AI glyphs (2.3.0) — traced from the two reference glyphs, redrawn to core rules (stroke 2, no fill, integer endpoints, >=4 between separate elements)
|
|
91
|
-
ai: '<path d="M9
|
|
92
|
-
"ai-sparkles": '<path d="M9
|
|
91
|
+
ai: '<path d="M9 3H7a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h10a4 4 0 0 0 4-4v-2"/><path d="M16 2c.72 3.6 2.4 5.28 6 6c-3.6.72-5.28 2.4-6 6c-.72-3.6-2.4-5.28-6-6c3.6-.72 5.28-2.4 6-6Z"/><path d="M9 12c.6 1.5 1.5 2.4 3 3c-1.5.6-2.4 1.5-3 3c-.6-1.5-1.5-2.4-3-3c1.5-.6 2.4-1.5 3-3Z"/>',
|
|
92
|
+
"ai-sparkles": '<path d="M9 2c.84 4.2 2.8 6.16 7 7c-4.2.84-6.16 2.8-7 7c-.84-4.2-2.8-6.16-7-7c4.2-.84 6.16-2.8 7-7Z"/><path d="M17 13c.8 2 2 3.2 4 4c-2 .8-3.2 2-4 4c-.8-2-2-3.2-4-4c2-.8 3.2-2 4-4Z"/>',
|
|
93
93
|
// Keybricks business glyphs (2.4.6) — dimensions, modules, rules-*, task-*; drawn to core rules or taken from Lucide
|
|
94
94
|
"dimension-account": '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/><path d="M5 8h3"/><path d="M5 11h3"/><path d="M5 14h3"/><path d="M17 6.5v1"/><path d="M17 12.5v1"/><path d="M18.5 7.5h-2a1.25 1.25 0 0 0 0 2.5h1a1.25 1.25 0 0 1 0 2.5h-2"/>',
|
|
95
95
|
"dimension-audit": '<path d="M11.1 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.589 3.588A2.4 2.4 0 0 1 20 8v3.25"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="m21 22-2.88-2.88"/><circle cx="16" cy="17" r="3"/>',
|
|
@@ -167,16 +167,24 @@ export const ICON_PATHS = {
|
|
|
167
167
|
"task-shared": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><g transform="translate(-.5 4)"><circle cx="15.3" cy="6.8" r=".75"/><circle cx="9.7" cy="10" r=".75"/><circle cx="15.3" cy="13.2" r=".75"/><path d="m11.35 9.06 2.3-1.32"/><path d="m11.35 10.94 2.3 1.32"/></g>',
|
|
168
168
|
"task-intercompany": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><g transform="translate(-.5 4)"><path d="M8.5 14V6.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V14"/><path d="M13.5 9.5H16a1 1 0 0 1 1 1V14"/><path d="M7.5 14h10"/><path d="M11 8.5h.01"/><path d="M11 11h.01"/></g>',
|
|
169
169
|
ownership: '<path d="M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17"/><path d="m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"/><path d="m2 16 6 6"/><circle cx="9" cy="5" r="3"/><path d="M12 5h9"/><path d="M18 5v2.5"/><path d="M21 5v2"/>',
|
|
170
|
+
prophet: '<path d="M5.66 13.5A7.5 7.5 0 1 1 18.34 13.5"/><path d="M8.5 10a3.5 3.5 0 0 1 7 0v1.5c0 3.2-1.5 5.6-3.5 7.5c-2-1.9-3.5-4.3-3.5-7.5Z"/><path d="M8.71 13.5c1.05.95 2.15 1.4 3.29 1.4s2.24-.45 3.29-1.4"/><path d="M3.5 22a5.5 5.5 0 0 1 4-5.3"/><path d="M20.5 22a5.5 5.5 0 0 0-4-5.3"/>',
|
|
171
|
+
"prophet-sparkle": '<path d="M5.66 13.5A7.5 7.5 0 0 1 13.24 2.1"/><path d="M19 2c.36 1.8 1.2 2.64 3 3c-1.8.36-2.64 1.2-3 3c-.36-1.8-1.2-2.64-3-3c1.8-.36 2.64-1.2 3-3Z"/><path d="M8.5 10a3.5 3.5 0 0 1 7 0v1.5c0 3.2-1.5 5.6-3.5 7.5c-2-1.9-3.5-4.3-3.5-7.5Z"/><path d="M8.71 13.5c1.05.95 2.15 1.4 3.29 1.4s2.24-.45 3.29-1.4"/><path d="M3.5 22a5.5 5.5 0 0 1 4-5.3"/><path d="M20.5 22a5.5 5.5 0 0 0-4-5.3"/>',
|
|
170
172
|
};
|
|
171
173
|
|
|
172
174
|
import { FEATHER_PATHS } from "./feather-paths.js";
|
|
173
175
|
|
|
174
|
-
/**
|
|
175
|
-
export
|
|
176
|
-
|
|
176
|
+
/** Stroke gradient for the AI glyph treatment — bottom-left start to top-right end, in glyph space so every path shares it. */
|
|
177
|
+
export const iconGradientDefs = (id, from = "var(--ai-gradient-start)", to = "var(--ai-gradient-end)") =>
|
|
178
|
+
'<defs><linearGradient id="' + id + '" gradientUnits="userSpaceOnUse" x1="2" y1="22" x2="22" y2="2"><stop offset="0" style="stop-color:' + from + '"/><stop offset="1" style="stop-color:' + to + '"/></linearGradient></defs>';
|
|
179
|
+
|
|
180
|
+
/** Lucide-style stroke icon. name: key of ICON_PATHS (core set) or any Feather icon name (feather-paths.js fallback); size in px; strokeWidth defaults to 2 (24-grid). gradient: true strokes it with the AI gradient (Prophet AI entry points only), or pass [from, to]. Browse both sets with keywords in the Icon Gallery card. */
|
|
181
|
+
export function Icon({ name, size = 16, strokeWidth = 2, color, gradient, style, className }) {
|
|
182
|
+
const gid = "kb-ig" + React.useId().replace(/[^a-zA-Z0-9]/g, "");
|
|
183
|
+
let body = ICON_PATHS[name] || FEATHER_PATHS[name] || "";
|
|
184
|
+
if (gradient) body = (Array.isArray(gradient) ? iconGradientDefs(gid, gradient[0], gradient[1]) : iconGradientDefs(gid)) + body;
|
|
177
185
|
return (
|
|
178
186
|
<svg
|
|
179
|
-
width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
187
|
+
width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={gradient ? "url(#" + gid + ")" : "currentColor"}
|
|
180
188
|
strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round"
|
|
181
189
|
className={className}
|
|
182
190
|
style={{ display: "block", flexShrink: 0, color, ...style }}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let kbIconSeq = 0
|
|
3
|
+
</script>
|
|
1
4
|
<script setup>
|
|
2
5
|
// Keybricks icon set — Lucide stroke glyphs (24-grid, stroke 2, round caps/joins, currentColor), rendered at 14/16px.
|
|
3
6
|
// Full path dictionary mirrors icons/Icon.jsx's ICON_PATHS (81 entries) — see that file for the complete list.
|
|
7
|
+
// gradient: true strokes the glyph with the AI gradient (Prophet AI entry points only), or pass [from, to].
|
|
8
|
+
import { computed } from 'vue'
|
|
4
9
|
import { VUE_ICON_PATHS } from './icon-paths.js'
|
|
5
10
|
import { FEATHER_PATHS } from './feather-paths.js'
|
|
6
|
-
const props = defineProps({ name: String, size: { type: Number, default: 16 }, strokeWidth: { type: Number, default: 2 }, color: String, style: Object })
|
|
7
|
-
const
|
|
11
|
+
const props = defineProps({ name: String, size: { type: Number, default: 16 }, strokeWidth: { type: Number, default: 2 }, color: String, gradient: [Boolean, Array], style: Object })
|
|
12
|
+
const gid = 'kb-ig-v' + (++kbIconSeq)
|
|
13
|
+
const defs = (from = 'var(--ai-gradient-start)', to = 'var(--ai-gradient-end)') => '<defs><linearGradient id="' + gid + '" gradientUnits="userSpaceOnUse" x1="2" y1="22" x2="22" y2="2"><stop offset="0" style="stop-color:' + from + '"/><stop offset="1" style="stop-color:' + to + '"/></linearGradient></defs>'
|
|
14
|
+
const body = computed(() => { const b = VUE_ICON_PATHS[props.name] || FEATHER_PATHS[props.name] || ''; return props.gradient ? (Array.isArray(props.gradient) ? defs(props.gradient[0], props.gradient[1]) : defs()) + b : b })
|
|
8
15
|
</script>
|
|
9
16
|
<template>
|
|
10
|
-
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" stroke="currentColor" :stroke-width="strokeWidth" stroke-linecap="round" stroke-linejoin="round"
|
|
11
|
-
:style="{ display: 'block', flexShrink: 0, color, ...style }" v-html="body
|
|
17
|
+
<svg :width="size" :height="size" viewBox="0 0 24 24" fill="none" :stroke="gradient ? 'url(#' + gid + ')' : 'currentColor'" :stroke-width="strokeWidth" stroke-linecap="round" stroke-linejoin="round"
|
|
18
|
+
:style="{ display: 'block', flexShrink: 0, color, ...style }" v-html="body" />
|
|
12
19
|
</template>
|
|
@@ -83,8 +83,8 @@ export const VUE_ICON_PATHS = {
|
|
|
83
83
|
"time-future": '<path d="M20 6A10 10 0 1 0 22 12"/><path d="M12 7v5h5"/><path d="M21 2v5h-5"/>',
|
|
84
84
|
"time-restatement": '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
|
|
85
85
|
// AI glyphs (2.3.0) — traced from the two reference glyphs, redrawn to core rules (stroke 2, no fill, integer endpoints, >=4 between separate elements)
|
|
86
|
-
ai: '<path d="M9
|
|
87
|
-
"ai-sparkles": '<path d="M9
|
|
86
|
+
ai: '<path d="M9 3H7a4 4 0 0 0-4 4v10a4 4 0 0 0 4 4h10a4 4 0 0 0 4-4v-2"/><path d="M16 2c.72 3.6 2.4 5.28 6 6c-3.6.72-5.28 2.4-6 6c-.72-3.6-2.4-5.28-6-6c3.6-.72 5.28-2.4 6-6Z"/><path d="M9 12c.6 1.5 1.5 2.4 3 3c-1.5.6-2.4 1.5-3 3c-.6-1.5-1.5-2.4-3-3c1.5-.6 2.4-1.5 3-3Z"/>',
|
|
87
|
+
"ai-sparkles": '<path d="M9 2c.84 4.2 2.8 6.16 7 7c-4.2.84-6.16 2.8-7 7c-.84-4.2-2.8-6.16-7-7c4.2-.84 6.16-2.8 7-7Z"/><path d="M17 13c.8 2 2 3.2 4 4c-2 .8-3.2 2-4 4c-.8-2-2-3.2-4-4c2-.8 3.2-2 4-4Z"/>',
|
|
88
88
|
// Keybricks business glyphs (2.4.6) — dimensions, modules, rules-*, task-*; drawn to core rules or taken from Lucide
|
|
89
89
|
"dimension-account": '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/><path d="M5 8h3"/><path d="M5 11h3"/><path d="M5 14h3"/><path d="M17 6.5v1"/><path d="M17 12.5v1"/><path d="M18.5 7.5h-2a1.25 1.25 0 0 0 0 2.5h1a1.25 1.25 0 0 1 0 2.5h-2"/>',
|
|
90
90
|
"dimension-audit": '<path d="M11.1 22H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.706.706l3.589 3.588A2.4 2.4 0 0 1 20 8v3.25"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="m21 22-2.88-2.88"/><circle cx="16" cy="17" r="3"/>',
|
|
@@ -162,4 +162,6 @@ export const VUE_ICON_PATHS = {
|
|
|
162
162
|
"task-shared": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><g transform="translate(-.5 4)"><circle cx="15.3" cy="6.8" r=".75"/><circle cx="9.7" cy="10" r=".75"/><circle cx="15.3" cy="13.2" r=".75"/><path d="m11.35 9.06 2.3-1.32"/><path d="m11.35 10.94 2.3 1.32"/></g>',
|
|
163
163
|
"task-intercompany": '<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><g transform="translate(-.5 4)"><path d="M8.5 14V6.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V14"/><path d="M13.5 9.5H16a1 1 0 0 1 1 1V14"/><path d="M7.5 14h10"/><path d="M11 8.5h.01"/><path d="M11 11h.01"/></g>',
|
|
164
164
|
ownership: '<path d="M11 15h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 17"/><path d="m7 21 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"/><path d="m2 16 6 6"/><circle cx="9" cy="5" r="3"/><path d="M12 5h9"/><path d="M18 5v2.5"/><path d="M21 5v2"/>',
|
|
165
|
+
prophet: '<path d="M5.66 13.5A7.5 7.5 0 1 1 18.34 13.5"/><path d="M8.5 10a3.5 3.5 0 0 1 7 0v1.5c0 3.2-1.5 5.6-3.5 7.5c-2-1.9-3.5-4.3-3.5-7.5Z"/><path d="M8.71 13.5c1.05.95 2.15 1.4 3.29 1.4s2.24-.45 3.29-1.4"/><path d="M3.5 22a5.5 5.5 0 0 1 4-5.3"/><path d="M20.5 22a5.5 5.5 0 0 0-4-5.3"/>',
|
|
166
|
+
"prophet-sparkle": '<path d="M5.66 13.5A7.5 7.5 0 0 1 13.24 2.1"/><path d="M19 2c.36 1.8 1.2 2.64 3 3c-1.8.36-2.64 1.2-3 3c-.36-1.8-1.2-2.64-3-3c1.8-.36 2.64-1.2 3-3Z"/><path d="M8.5 10a3.5 3.5 0 0 1 7 0v1.5c0 3.2-1.5 5.6-3.5 7.5c-2-1.9-3.5-4.3-3.5-7.5Z"/><path d="M8.71 13.5c1.05.95 2.15 1.4 3.29 1.4s2.24-.45 3.29-1.4"/><path d="M3.5 22a5.5 5.5 0 0 1 4-5.3"/><path d="M20.5 22a5.5 5.5 0 0 0-4-5.3"/>',
|
|
165
167
|
};
|
package/components/index.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ export * from "./forms/Combobox";
|
|
|
61
61
|
export * from "./forms/DatePicker";
|
|
62
62
|
export * from "./forms/Dropzone";
|
|
63
63
|
export * from "./forms/Field";
|
|
64
|
+
export * from "./forms/GlobalSearch";
|
|
64
65
|
export * from "./forms/Input";
|
|
65
66
|
export * from "./forms/MultiSelect";
|
|
66
67
|
export * from "./forms/NumberStepper";
|
package/components/index.js
CHANGED
|
@@ -76,6 +76,7 @@ export { Combobox } from "./forms/Combobox.jsx";
|
|
|
76
76
|
export { DatePicker } from "./forms/DatePicker.jsx";
|
|
77
77
|
export { Dropzone } from "./forms/Dropzone.jsx";
|
|
78
78
|
export { Field } from "./forms/Field.jsx";
|
|
79
|
+
export { GLOBAL_SEARCH_LABELS, GlobalSearch } from "./forms/GlobalSearch.jsx";
|
|
79
80
|
export { Input, Textarea } from "./forms/Input.jsx";
|
|
80
81
|
export { MultiSelect } from "./forms/MultiSelect.jsx";
|
|
81
82
|
export { NumberStepper } from "./forms/NumberStepper.jsx";
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
|
|
2
|
+
import type { GlobalSearchProps } from "../forms/GlobalSearch";
|
|
3
|
+
/** 56px top bar: GlobalSearch left (⌘K / Ctrl K); notifications, help and the user trigger right. The trigger opens a UserMenu. Prophet AI is reached through the search card (Button variant="ai"), not a separate pill. */
|
|
3
4
|
export interface TopBarProps {
|
|
5
|
+
/** Collapsed search field text (default GlobalSearch labels.field) */
|
|
4
6
|
placeholder?: string;
|
|
5
|
-
/** Receives the
|
|
7
|
+
/** Receives the query as the user types */
|
|
6
8
|
onSearch?: (value: string) => void;
|
|
9
|
+
/** Props for the GlobalSearch (items / onQuery, recent, prompts, onSelect, onDeepSearch, labels …) */
|
|
10
|
+
search?: GlobalSearchProps;
|
|
7
11
|
/** Replaces the whole right-hand group */
|
|
8
12
|
actions?: React.ReactNode;
|
|
13
|
+
/** Overrides the "Ask Prophet AI" label (same as search.labels.ask) */
|
|
9
14
|
aiLabel?: string;
|
|
10
|
-
|
|
15
|
+
/** Prophet AI hand-off: called with the question when any AI entry in the search card is used */
|
|
16
|
+
onAi?: (question: string) => void;
|
|
11
17
|
onNotifications?: () => void;
|
|
12
18
|
onHelp?: () => void;
|
|
13
19
|
notificationsLabel?: string;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Icon } from "../icons/Icon.jsx";
|
|
3
3
|
import { IconButton } from "../buttons/IconButton.jsx";
|
|
4
|
-
import {
|
|
5
|
-
import { SearchInput } from "../forms/SearchInput.jsx";
|
|
4
|
+
import { GlobalSearch } from "../forms/GlobalSearch.jsx";
|
|
6
5
|
import { Avatar } from "../display/Avatar.jsx";
|
|
7
6
|
import { SegmentedControl } from "../tabs/SegmentedControl.jsx";
|
|
8
7
|
import { ContextMenu } from "../feedback/ContextMenu.jsx";
|
|
@@ -33,9 +32,9 @@ export function UserMenu({ name, email, theme, onThemeChange, themeOptions = THE
|
|
|
33
32
|
);
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
/** 56px top bar:
|
|
37
|
-
* The user trigger opens a UserMenu below it (closes on outside click). actions replaces the right-hand group entirely. */
|
|
38
|
-
export function TopBar({ placeholder
|
|
35
|
+
/** 56px top bar: GlobalSearch on the left (⌘K / Ctrl K, Prophet AI hand-off via onAi); notifications, help and the user trigger on the right.
|
|
36
|
+
* The user trigger opens a UserMenu below it (closes on outside click). actions replaces the right-hand group entirely. search passes props to GlobalSearch. */
|
|
37
|
+
export function TopBar({ placeholder, onSearch, search, actions, aiLabel, onAi, onNotifications, onHelp, notificationsLabel = "Notifications", helpLabel = "Help", user, userMenu, style }) {
|
|
39
38
|
const [open, setOpen] = React.useState(false);
|
|
40
39
|
const ref = React.useRef(null);
|
|
41
40
|
React.useEffect(() => {
|
|
@@ -48,10 +47,9 @@ export function TopBar({ placeholder = "Search documents, workflows, data...", o
|
|
|
48
47
|
const [hover, setHover] = React.useState(false);
|
|
49
48
|
return (
|
|
50
49
|
<header style={{ height: "var(--topbar-h)", background: "var(--bg-surface)", borderBottom: "1px solid var(--border-card)", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, padding: "0 24px", boxSizing: "border-box", flexShrink: 0, minWidth: 0, position: "relative", ...style }}>
|
|
51
|
-
<
|
|
50
|
+
<GlobalSearch placeholder={placeholder} onInput={onSearch} onAsk={onAi} {...search} labels={aiLabel ? { ...(search && search.labels), ask: aiLabel } : search && search.labels} />
|
|
52
51
|
<div ref={ref} style={{ display: "flex", alignItems: "center", gap: 8, flexShrink: 0 }}>
|
|
53
52
|
{actions || (<>
|
|
54
|
-
<Button variant="dark" icon="ai" onClick={onAi}>{aiLabel}</Button>
|
|
55
53
|
<IconButton icon="bell" label={notificationsLabel} title={notificationsLabel} onClick={onNotifications} />
|
|
56
54
|
<IconButton icon="circle-help" label={helpLabel} title={helpLabel} onClick={onHelp} />
|
|
57
55
|
{user ? <div role="button" tabIndex={0} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(!open)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen(!open); } }} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import Icon from '../icons/Icon.vue'
|
|
4
4
|
import IconButton from '../buttons/IconButton.vue'
|
|
5
|
-
import
|
|
6
|
-
import SearchInput from '../forms/SearchInput.vue'
|
|
5
|
+
import GlobalSearch from '../forms/GlobalSearch.vue'
|
|
7
6
|
import UserMenu from './UserMenu.vue'
|
|
8
|
-
// 56px top bar:
|
|
7
|
+
// 56px top bar: GlobalSearch on the left (⌘K / Ctrl K; its ask event is re-emitted as ai(question)); notifications, help and the user trigger on the right.
|
|
8
|
+
// search passes props to GlobalSearch (items, query, recent, prompts, deepSearch, labels …).
|
|
9
9
|
// The user trigger opens a UserMenu below it (closes on outside click / Escape); #user-menu slot replaces its content.
|
|
10
10
|
// #actions slot replaces the whole right-hand group.
|
|
11
|
-
const props = defineProps({ placeholder:
|
|
11
|
+
const props = defineProps({ placeholder: String, search: Object, aiLabel: String, notificationsLabel: { type: String, default: 'Notifications' }, helpLabel: { type: String, default: 'Help' }, user: Object, style: Object })
|
|
12
12
|
const emit = defineEmits(['search', 'ai', 'notifications', 'help'])
|
|
13
13
|
const open = ref(false), hover = ref(false), root = ref(null)
|
|
14
14
|
const off = (e) => { if (open.value && root.value && !root.value.contains(e.target)) open.value = false }
|
|
@@ -19,10 +19,9 @@ function key(e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault();
|
|
|
19
19
|
</script>
|
|
20
20
|
<template>
|
|
21
21
|
<header :style="{ height: 'var(--topbar-h)', background: 'var(--bg-surface)', borderBottom: '1px solid var(--border-card)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '16px', padding: '0 24px', boxSizing: 'border-box', flexShrink: 0, minWidth: 0, position: 'relative', ...style }">
|
|
22
|
-
<
|
|
22
|
+
<GlobalSearch :placeholder="placeholder" v-bind="search" :labels="aiLabel ? { ...(search && search.labels), ask: aiLabel } : search && search.labels" @input="v => emit('search', v)" @ask="q => emit('ai', q)" />
|
|
23
23
|
<div ref="root" style="display:flex;align-items:center;gap:8px;flex-shrink:0">
|
|
24
24
|
<slot name="actions">
|
|
25
|
-
<Button variant="dark" icon="ai" @click="emit('ai')">{{ aiLabel }}</Button>
|
|
26
25
|
<IconButton icon="bell" :label="notificationsLabel" :title="notificationsLabel" @click="emit('notifications')" />
|
|
27
26
|
<IconButton icon="circle-help" :label="helpLabel" :title="helpLabel" @click="emit('help')" />
|
|
28
27
|
<div v-if="user" role="button" tabindex="0" aria-haspopup="menu" :aria-expanded="open" @click="open = !open" @keydown="key" @mouseenter="hover = true" @mouseleave="hover = false"
|