@prsm/mono-components 0.1.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/.claude/settings.local.json +13 -0
- package/.lore +83 -0
- package/histoire.config.js +43 -0
- package/package.json +39 -0
- package/postcss.config.js +6 -0
- package/src/components/Badge.vue +36 -0
- package/src/components/Button.vue +44 -0
- package/src/components/Checkbox.vue +51 -0
- package/src/components/CheckboxCards.vue +61 -0
- package/src/components/CodeEditor.vue +299 -0
- package/src/components/Collapsible.vue +69 -0
- package/src/components/CollapsibleGroup.vue +38 -0
- package/src/components/Combobox.vue +179 -0
- package/src/components/ContextMenu.vue +65 -0
- package/src/components/ContextMenuPanel.vue +115 -0
- package/src/components/DataTable.vue +326 -0
- package/src/components/Dropdown.vue +127 -0
- package/src/components/GhostInput.vue +29 -0
- package/src/components/Input.vue +23 -0
- package/src/components/KeyValue.vue +149 -0
- package/src/components/LabeledTextarea.vue +64 -0
- package/src/components/LabeledTextareaGroup.vue +14 -0
- package/src/components/Mention.vue +79 -0
- package/src/components/Modal.vue +109 -0
- package/src/components/MultiCombobox.vue +209 -0
- package/src/components/NavTree.vue +98 -0
- package/src/components/NumberInput.vue +128 -0
- package/src/components/PopConfirm.vue +94 -0
- package/src/components/Popover.vue +53 -0
- package/src/components/RadioCards.vue +37 -0
- package/src/components/RadioGroup.vue +57 -0
- package/src/components/RangeSlider.vue +165 -0
- package/src/components/ScrollBox.vue +78 -0
- package/src/components/SectionHeader.vue +18 -0
- package/src/components/Select.vue +187 -0
- package/src/components/Switch.vue +85 -0
- package/src/components/Tabs.vue +34 -0
- package/src/components/TagInput.vue +80 -0
- package/src/components/Textarea.vue +97 -0
- package/src/components/ToastContainer.vue +104 -0
- package/src/components/ToggleButtons.vue +45 -0
- package/src/components/ToggleGroup.vue +30 -0
- package/src/components/Tooltip.vue +56 -0
- package/src/components/Tree.vue +188 -0
- package/src/composables/toast.js +54 -0
- package/src/composables/useClickOutside.js +23 -0
- package/src/composables/useMention.js +291 -0
- package/src/composables/usePointerDrag.js +39 -0
- package/src/histoire-setup.js +1 -0
- package/src/index.js +43 -0
- package/src/style.css +96 -0
- package/stories/Badge.story.vue +24 -0
- package/stories/Button.story.vue +45 -0
- package/stories/Checkbox.story.vue +31 -0
- package/stories/CheckboxCards.story.vue +51 -0
- package/stories/CodeEditor.story.vue +71 -0
- package/stories/Collapsible.story.vue +84 -0
- package/stories/Combobox.story.vue +44 -0
- package/stories/ContextMenu.story.vue +59 -0
- package/stories/DataTable.story.vue +185 -0
- package/stories/Dropdown.story.vue +49 -0
- package/stories/GhostInput.story.vue +24 -0
- package/stories/Input.story.vue +23 -0
- package/stories/KeyValue.story.vue +104 -0
- package/stories/LabeledTextarea.story.vue +44 -0
- package/stories/Mention.story.vue +166 -0
- package/stories/Modal.story.vue +86 -0
- package/stories/MultiCombobox.story.vue +76 -0
- package/stories/NavTree.story.vue +184 -0
- package/stories/NumberInput.story.vue +31 -0
- package/stories/Overview.story.vue +85 -0
- package/stories/PopConfirm.story.vue +39 -0
- package/stories/RadioCards.story.vue +66 -0
- package/stories/RadioGroup.story.vue +52 -0
- package/stories/RangeSlider.story.vue +75 -0
- package/stories/ScrollBox.story.vue +54 -0
- package/stories/SectionHeader.story.vue +22 -0
- package/stories/Select.story.vue +34 -0
- package/stories/Switch.story.vue +42 -0
- package/stories/Tabs.story.vue +34 -0
- package/stories/TagInput.story.vue +54 -0
- package/stories/Textarea.story.vue +28 -0
- package/stories/Toast.story.vue +28 -0
- package/stories/ToggleButtons.story.vue +57 -0
- package/stories/ToggleGroup.story.vue +34 -0
- package/stories/Tooltip.story.vue +55 -0
- package/stories/Tree.story.vue +115 -0
- package/tailwind.config.js +9 -0
- package/tailwind.preset.js +79 -0
- package/vite.config.js +6 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, reactive, nextTick } from "vue"
|
|
3
|
+
import { Icon } from "@iconify/vue"
|
|
4
|
+
import ScrollBox from "./ScrollBox.vue"
|
|
5
|
+
|
|
6
|
+
const props = defineProps({
|
|
7
|
+
items: { type: Array, required: true },
|
|
8
|
+
modelValue: { type: [String, Number, null], default: null },
|
|
9
|
+
filterable: { type: Boolean, default: false },
|
|
10
|
+
filterPlaceholder: { type: String, default: "search..." },
|
|
11
|
+
defaultExpanded: { type: [Array, Boolean], default: () => [] }
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits(["update:modelValue"])
|
|
15
|
+
|
|
16
|
+
const query = ref("")
|
|
17
|
+
const focusedKey = ref(null)
|
|
18
|
+
const containerRef = ref(null)
|
|
19
|
+
const hasFocus = ref(false)
|
|
20
|
+
|
|
21
|
+
const expandedKeys = reactive(new Set(
|
|
22
|
+
props.defaultExpanded === true ? collectKeys(props.items) : props.defaultExpanded
|
|
23
|
+
))
|
|
24
|
+
|
|
25
|
+
function collectKeys(nodes) {
|
|
26
|
+
const keys = []
|
|
27
|
+
for (const n of nodes) {
|
|
28
|
+
if (n.children?.length) {
|
|
29
|
+
keys.push(n.key)
|
|
30
|
+
keys.push(...collectKeys(n.children))
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return keys
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasMatch(node, q) {
|
|
37
|
+
if (node.label.toLowerCase().includes(q)) return true
|
|
38
|
+
if (node.children) return node.children.some(c => hasMatch(c, q))
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const flatVisible = computed(() => {
|
|
43
|
+
const q = query.value.toLowerCase()
|
|
44
|
+
const result = []
|
|
45
|
+
|
|
46
|
+
function walk(nodes, depth) {
|
|
47
|
+
for (const node of nodes) {
|
|
48
|
+
const hasChildren = node.children?.length > 0
|
|
49
|
+
|
|
50
|
+
if (q && !hasMatch(node, q)) continue
|
|
51
|
+
|
|
52
|
+
const expanded = q ? hasChildren : expandedKeys.has(node.key)
|
|
53
|
+
result.push({ node, depth, hasChildren, expanded })
|
|
54
|
+
|
|
55
|
+
if (hasChildren && expanded) walk(node.children, depth + 1)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
walk(props.items, 0)
|
|
60
|
+
return result
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
function select(key) {
|
|
64
|
+
emit("update:modelValue", key)
|
|
65
|
+
focusedKey.value = key
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toggle(key) {
|
|
69
|
+
if (expandedKeys.has(key)) expandedKeys.delete(key)
|
|
70
|
+
else expandedKeys.add(key)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function scrollToFocused() {
|
|
74
|
+
nextTick(() => {
|
|
75
|
+
const el = containerRef.value?.querySelector?.("[data-focused]")
|
|
76
|
+
?? containerRef.value?.$el?.querySelector("[data-focused]")
|
|
77
|
+
if (el) el.scrollIntoView({ block: "nearest" })
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleKeydown(e) {
|
|
82
|
+
const flat = flatVisible.value
|
|
83
|
+
const idx = flat.findIndex(f => f.node.key === focusedKey.value)
|
|
84
|
+
|
|
85
|
+
if (e.key === "ArrowDown") {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
const next = idx < flat.length - 1 ? idx + 1 : 0
|
|
88
|
+
focusedKey.value = flat[next]?.node.key
|
|
89
|
+
scrollToFocused()
|
|
90
|
+
} else if (e.key === "ArrowUp") {
|
|
91
|
+
e.preventDefault()
|
|
92
|
+
const prev = idx > 0 ? idx - 1 : flat.length - 1
|
|
93
|
+
focusedKey.value = flat[prev]?.node.key
|
|
94
|
+
scrollToFocused()
|
|
95
|
+
} else if (e.key === "ArrowRight") {
|
|
96
|
+
e.preventDefault()
|
|
97
|
+
const current = flat[idx]
|
|
98
|
+
if (!current) return
|
|
99
|
+
if (current.hasChildren && !expandedKeys.has(current.node.key)) {
|
|
100
|
+
expandedKeys.add(current.node.key)
|
|
101
|
+
} else if (current.hasChildren && current.node.children.length) {
|
|
102
|
+
focusedKey.value = current.node.children[0].key
|
|
103
|
+
scrollToFocused()
|
|
104
|
+
}
|
|
105
|
+
} else if (e.key === "ArrowLeft") {
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
const current = flat[idx]
|
|
108
|
+
if (!current) return
|
|
109
|
+
if (current.hasChildren && expandedKeys.has(current.node.key)) {
|
|
110
|
+
expandedKeys.delete(current.node.key)
|
|
111
|
+
} else {
|
|
112
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
113
|
+
if (flat[i].depth < current.depth) {
|
|
114
|
+
focusedKey.value = flat[i].node.key
|
|
115
|
+
scrollToFocused()
|
|
116
|
+
break
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} else if (e.key === "Enter") {
|
|
121
|
+
e.preventDefault()
|
|
122
|
+
const current = flat[idx]
|
|
123
|
+
if (!current) return
|
|
124
|
+
select(current.node.key)
|
|
125
|
+
if (current.hasChildren) toggle(current.node.key)
|
|
126
|
+
} else if (e.key === "Home") {
|
|
127
|
+
e.preventDefault()
|
|
128
|
+
focusedKey.value = flat[0]?.node.key
|
|
129
|
+
scrollToFocused()
|
|
130
|
+
} else if (e.key === "End") {
|
|
131
|
+
e.preventDefault()
|
|
132
|
+
focusedKey.value = flat[flat.length - 1]?.node.key
|
|
133
|
+
scrollToFocused()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
139
|
+
<div
|
|
140
|
+
class="flex flex-col"
|
|
141
|
+
@keydown="handleKeydown"
|
|
142
|
+
@focusin="hasFocus = true"
|
|
143
|
+
@focusout="hasFocus = false"
|
|
144
|
+
>
|
|
145
|
+
<div v-if="filterable" class="px-1 py-1 border-b border-line-subtle">
|
|
146
|
+
<input
|
|
147
|
+
v-model="query"
|
|
148
|
+
type="text"
|
|
149
|
+
class="w-full px-2 py-1 text-base bg-0 border border-line rounded-sm font-mono text-fg-0 outline-none focus:border-accent"
|
|
150
|
+
:placeholder="filterPlaceholder"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
<ScrollBox ref="containerRef" class="flex-1">
|
|
154
|
+
<div
|
|
155
|
+
v-for="{ node, depth, hasChildren, expanded } in flatVisible"
|
|
156
|
+
:key="node.key"
|
|
157
|
+
class="flex items-center gap-0.5 py-0.5 pr-2 cursor-pointer font-mono text-base select-none"
|
|
158
|
+
:class="[
|
|
159
|
+
node.key === modelValue ? 'bg-3 text-fg-0' : 'text-fg-1 hover:bg-2 hover:text-fg-0',
|
|
160
|
+
node.key === focusedKey && hasFocus ? 'outline outline-1 outline-accent -outline-offset-1' : ''
|
|
161
|
+
]"
|
|
162
|
+
:style="{ paddingLeft: (depth * 16 + 4) + 'px' }"
|
|
163
|
+
:data-focused="node.key === focusedKey ? '' : undefined"
|
|
164
|
+
tabindex="-1"
|
|
165
|
+
@click="select(node.key); hasChildren && toggle(node.key)"
|
|
166
|
+
>
|
|
167
|
+
<span class="w-[16px] h-[16px] shrink-0 flex items-center justify-center">
|
|
168
|
+
<Icon
|
|
169
|
+
v-if="hasChildren"
|
|
170
|
+
icon="material-symbols:chevron-right"
|
|
171
|
+
class="text-sm text-fg-2 transition-transform duration-150"
|
|
172
|
+
:class="expanded ? 'rotate-90' : ''"
|
|
173
|
+
/>
|
|
174
|
+
</span>
|
|
175
|
+
<Icon
|
|
176
|
+
v-if="node.icon"
|
|
177
|
+
:icon="node.icon"
|
|
178
|
+
class="text-base shrink-0"
|
|
179
|
+
:class="node.key === modelValue ? 'text-accent' : 'text-fg-2'"
|
|
180
|
+
/>
|
|
181
|
+
<span class="truncate">{{ node.label }}</span>
|
|
182
|
+
</div>
|
|
183
|
+
<div v-if="query && flatVisible.length === 0" class="px-3 py-2 text-base text-fg-3">
|
|
184
|
+
no matches
|
|
185
|
+
</div>
|
|
186
|
+
</ScrollBox>
|
|
187
|
+
</div>
|
|
188
|
+
</template>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { reactive } from "vue"
|
|
2
|
+
|
|
3
|
+
let nextId = 0
|
|
4
|
+
export const toasts = reactive([])
|
|
5
|
+
const timers = new Map()
|
|
6
|
+
|
|
7
|
+
function scheduleRemove(id, duration) {
|
|
8
|
+
if (duration <= 0) return
|
|
9
|
+
const timer = setTimeout(() => remove(id), duration)
|
|
10
|
+
timers.set(id, { timer, duration, started: Date.now() })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function add(message, variant = "neutral", duration = 3000) {
|
|
14
|
+
const id = ++nextId
|
|
15
|
+
toasts.push({ id, message, variant, duration })
|
|
16
|
+
scheduleRemove(id, duration)
|
|
17
|
+
return id
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function remove(id) {
|
|
21
|
+
const idx = toasts.findIndex(t => t.id === id)
|
|
22
|
+
if (idx > -1) toasts.splice(idx, 1)
|
|
23
|
+
const entry = timers.get(id)
|
|
24
|
+
if (entry) {
|
|
25
|
+
clearTimeout(entry.timer)
|
|
26
|
+
timers.delete(id)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pause(id) {
|
|
31
|
+
const entry = timers.get(id)
|
|
32
|
+
if (!entry) return
|
|
33
|
+
clearTimeout(entry.timer)
|
|
34
|
+
entry.remaining = entry.duration - (Date.now() - entry.started)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resume(id) {
|
|
38
|
+
const entry = timers.get(id)
|
|
39
|
+
if (!entry || entry.remaining == null) return
|
|
40
|
+
const timer = setTimeout(() => remove(id), entry.remaining)
|
|
41
|
+
timers.set(id, { timer, duration: entry.remaining, started: Date.now() })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function toast(message, options = {}) {
|
|
45
|
+
return add(message, options.variant || "neutral", options.duration ?? 3000)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
toast.success = (msg, opts) => add(msg, "success", opts?.duration ?? 3000)
|
|
49
|
+
toast.warning = (msg, opts) => add(msg, "warning", opts?.duration ?? 3000)
|
|
50
|
+
toast.error = (msg, opts) => add(msg, "error", opts?.duration ?? 3000)
|
|
51
|
+
toast.info = (msg, opts) => add(msg, "info", opts?.duration ?? 3000)
|
|
52
|
+
toast.remove = remove
|
|
53
|
+
toast.pause = pause
|
|
54
|
+
toast.resume = resume
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { onMounted, onUnmounted } from "vue"
|
|
2
|
+
|
|
3
|
+
export function useClickOutside(refs, onClose) {
|
|
4
|
+
function handleClick(e) {
|
|
5
|
+
const elements = refs.map(r => r.value?.$el ?? r.value).filter(Boolean)
|
|
6
|
+
if (elements.some(el => el.contains(e.target))) return
|
|
7
|
+
onClose()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function handleKeydown(e) {
|
|
11
|
+
if (e.key === "Escape") onClose()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
onMounted(() => {
|
|
15
|
+
document.addEventListener("click", handleClick, true)
|
|
16
|
+
document.addEventListener("keydown", handleKeydown)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
onUnmounted(() => {
|
|
20
|
+
document.removeEventListener("click", handleClick, true)
|
|
21
|
+
document.removeEventListener("keydown", handleKeydown)
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { ref, computed, markRaw } from "vue"
|
|
2
|
+
|
|
3
|
+
export function useMention(triggers) {
|
|
4
|
+
const open = ref(false)
|
|
5
|
+
const activeTrigger = ref(null)
|
|
6
|
+
const query = ref("")
|
|
7
|
+
const activeIndex = ref(0)
|
|
8
|
+
const items = ref([])
|
|
9
|
+
const loading = ref(false)
|
|
10
|
+
const inputEl = ref(null)
|
|
11
|
+
const cursorRect = ref(null)
|
|
12
|
+
|
|
13
|
+
const virtualAnchor = computed(() => {
|
|
14
|
+
const r = cursorRect.value
|
|
15
|
+
if (!r) return markRaw({ getBoundingClientRect: () => new DOMRect() })
|
|
16
|
+
return markRaw({ getBoundingClientRect: () => r })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const triggerMap = new Map()
|
|
20
|
+
for (const t of triggers) {
|
|
21
|
+
triggerMap.set(t.key, t)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeItems(raw) {
|
|
25
|
+
if (!raw) return []
|
|
26
|
+
return raw.map(item => {
|
|
27
|
+
if (typeof item === "object" && item !== null) {
|
|
28
|
+
const normalized = { value: item.value, label: item.label ?? String(item.value) }
|
|
29
|
+
if (item.action) normalized.action = item.action
|
|
30
|
+
return normalized
|
|
31
|
+
}
|
|
32
|
+
return { value: item, label: String(item) }
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const filtered = computed(() => {
|
|
37
|
+
if (!query.value) return items.value
|
|
38
|
+
const q = query.value.toLowerCase()
|
|
39
|
+
return items.value.filter(i => i.label.toLowerCase().includes(q))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
function getCursorRect(el) {
|
|
43
|
+
if (el.tagName === "TEXTAREA" || el.tagName === "INPUT") {
|
|
44
|
+
return getInputCursorRect(el)
|
|
45
|
+
}
|
|
46
|
+
const sel = window.getSelection()
|
|
47
|
+
if (!sel || sel.rangeCount === 0) return null
|
|
48
|
+
const range = sel.getRangeAt(0)
|
|
49
|
+
return range.getBoundingClientRect()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getInputCursorRect(el) {
|
|
53
|
+
const div = document.createElement("div")
|
|
54
|
+
const style = getComputedStyle(el)
|
|
55
|
+
for (const prop of style) {
|
|
56
|
+
div.style.setProperty(prop, style.getPropertyValue(prop))
|
|
57
|
+
}
|
|
58
|
+
div.style.position = "fixed"
|
|
59
|
+
div.style.visibility = "hidden"
|
|
60
|
+
div.style.whiteSpace = "pre-wrap"
|
|
61
|
+
div.style.wordWrap = "break-word"
|
|
62
|
+
div.style.overflow = "hidden"
|
|
63
|
+
|
|
64
|
+
if (el.tagName === "INPUT") {
|
|
65
|
+
div.style.whiteSpace = "pre"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const text = el.value.substring(0, el.selectionStart)
|
|
69
|
+
div.textContent = text
|
|
70
|
+
|
|
71
|
+
const span = document.createElement("span")
|
|
72
|
+
span.textContent = "|"
|
|
73
|
+
div.appendChild(span)
|
|
74
|
+
|
|
75
|
+
document.body.appendChild(div)
|
|
76
|
+
const elRect = el.getBoundingClientRect()
|
|
77
|
+
const spanRect = span.getBoundingClientRect()
|
|
78
|
+
|
|
79
|
+
const rect = new DOMRect(
|
|
80
|
+
elRect.left + (spanRect.left - div.getBoundingClientRect().left),
|
|
81
|
+
elRect.top + (spanRect.top - div.getBoundingClientRect().top),
|
|
82
|
+
0,
|
|
83
|
+
spanRect.height
|
|
84
|
+
)
|
|
85
|
+
document.body.removeChild(div)
|
|
86
|
+
return rect
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let revealController = null
|
|
90
|
+
|
|
91
|
+
async function reveal(trigger, el) {
|
|
92
|
+
if (blurTimer) { clearTimeout(blurTimer); blurTimer = null }
|
|
93
|
+
activeTrigger.value = trigger
|
|
94
|
+
query.value = ""
|
|
95
|
+
activeIndex.value = 0
|
|
96
|
+
inputEl.value = el
|
|
97
|
+
cursorRect.value = getCursorRect(el)
|
|
98
|
+
items.value = normalizeItems(trigger.items)
|
|
99
|
+
open.value = true
|
|
100
|
+
|
|
101
|
+
if (trigger.onReveal) {
|
|
102
|
+
if (revealController) revealController.abort()
|
|
103
|
+
revealController = new AbortController()
|
|
104
|
+
loading.value = true
|
|
105
|
+
try {
|
|
106
|
+
const result = await trigger.onReveal()
|
|
107
|
+
if (!revealController.signal.aborted) {
|
|
108
|
+
items.value = normalizeItems(result)
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
loading.value = false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function close() {
|
|
117
|
+
open.value = false
|
|
118
|
+
activeTrigger.value = null
|
|
119
|
+
query.value = ""
|
|
120
|
+
items.value = []
|
|
121
|
+
if (revealController) {
|
|
122
|
+
revealController.abort()
|
|
123
|
+
revealController = null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let confirming = false
|
|
128
|
+
|
|
129
|
+
function confirm(item) {
|
|
130
|
+
if (!activeTrigger.value || !inputEl.value) return
|
|
131
|
+
|
|
132
|
+
const el = inputEl.value
|
|
133
|
+
const trigger = activeTrigger.value
|
|
134
|
+
const val = el.value
|
|
135
|
+
const pos = el.selectionStart
|
|
136
|
+
|
|
137
|
+
const before = val.substring(0, pos)
|
|
138
|
+
const triggerIdx = before.lastIndexOf(trigger.key)
|
|
139
|
+
if (triggerIdx === -1) { close(); return }
|
|
140
|
+
|
|
141
|
+
const prefix = val.substring(0, triggerIdx)
|
|
142
|
+
const suffix = val.substring(pos)
|
|
143
|
+
|
|
144
|
+
if (trigger.action) {
|
|
145
|
+
el.value = prefix + suffix
|
|
146
|
+
el.selectionStart = prefix.length
|
|
147
|
+
el.selectionEnd = prefix.length
|
|
148
|
+
confirming = true
|
|
149
|
+
el.dispatchEvent(new Event("input", { bubbles: true }))
|
|
150
|
+
confirming = false
|
|
151
|
+
el.focus()
|
|
152
|
+
close()
|
|
153
|
+
if (item.action) item.action(item)
|
|
154
|
+
} else {
|
|
155
|
+
const insert = trigger.key + item.label + " "
|
|
156
|
+
el.value = prefix + insert + suffix
|
|
157
|
+
el.selectionStart = prefix.length + insert.length
|
|
158
|
+
el.selectionEnd = prefix.length + insert.length
|
|
159
|
+
confirming = true
|
|
160
|
+
el.dispatchEvent(new Event("input", { bubbles: true }))
|
|
161
|
+
confirming = false
|
|
162
|
+
el.focus()
|
|
163
|
+
close()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function tryOpenTrigger(el) {
|
|
168
|
+
const val = el.value
|
|
169
|
+
const pos = el.selectionStart
|
|
170
|
+
const before = val.substring(0, pos)
|
|
171
|
+
|
|
172
|
+
for (const [key, trigger] of triggerMap) {
|
|
173
|
+
const idx = before.lastIndexOf(key)
|
|
174
|
+
if (idx === -1) continue
|
|
175
|
+
const charBeforeTrigger = idx > 0 ? before[idx - 1] : ""
|
|
176
|
+
const isWordBoundary = idx === 0 || /\s/.test(charBeforeTrigger)
|
|
177
|
+
if (!isWordBoundary) continue
|
|
178
|
+
|
|
179
|
+
const textAfter = before.substring(idx + key.length)
|
|
180
|
+
if (textAfter === "") {
|
|
181
|
+
reveal(trigger, el)
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let filterController = null
|
|
188
|
+
|
|
189
|
+
async function handleInput(e) {
|
|
190
|
+
if (confirming) return
|
|
191
|
+
const el = e.target
|
|
192
|
+
const val = el.value
|
|
193
|
+
const pos = el.selectionStart
|
|
194
|
+
|
|
195
|
+
if (!open.value) {
|
|
196
|
+
tryOpenTrigger(el)
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const before = val.substring(0, pos)
|
|
201
|
+
const triggerKey = activeTrigger.value?.key
|
|
202
|
+
if (!triggerKey) return
|
|
203
|
+
|
|
204
|
+
const triggerIdx = before.lastIndexOf(triggerKey)
|
|
205
|
+
if (triggerIdx === -1) { close(); return }
|
|
206
|
+
|
|
207
|
+
const textBetween = before.substring(triggerIdx + triggerKey.length)
|
|
208
|
+
if (/\s{2,}/.test(textBetween)) { close(); return }
|
|
209
|
+
|
|
210
|
+
query.value = textBetween
|
|
211
|
+
activeIndex.value = 0
|
|
212
|
+
cursorRect.value = getCursorRect(el)
|
|
213
|
+
|
|
214
|
+
const trigger = activeTrigger.value
|
|
215
|
+
if (trigger.onFilter) {
|
|
216
|
+
if (filterController) filterController.abort()
|
|
217
|
+
filterController = new AbortController()
|
|
218
|
+
loading.value = true
|
|
219
|
+
try {
|
|
220
|
+
const result = await trigger.onFilter(textBetween)
|
|
221
|
+
if (!filterController.signal.aborted) {
|
|
222
|
+
items.value = normalizeItems(result)
|
|
223
|
+
}
|
|
224
|
+
} finally {
|
|
225
|
+
loading.value = false
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function handleKeydown(e) {
|
|
231
|
+
if (!open.value) return
|
|
232
|
+
|
|
233
|
+
const isDown = e.key === "ArrowDown" || (e.ctrlKey && e.key === "n")
|
|
234
|
+
const isUp = e.key === "ArrowUp" || (e.ctrlKey && e.key === "p")
|
|
235
|
+
const isConfirm = e.key === "Enter" || (e.ctrlKey && e.key === "y")
|
|
236
|
+
|
|
237
|
+
if (isDown) {
|
|
238
|
+
e.preventDefault()
|
|
239
|
+
activeIndex.value = Math.min(activeIndex.value + 1, filtered.value.length - 1)
|
|
240
|
+
} else if (isUp) {
|
|
241
|
+
e.preventDefault()
|
|
242
|
+
activeIndex.value = Math.max(activeIndex.value - 1, 0)
|
|
243
|
+
} else if (isConfirm && filtered.value.length > 0) {
|
|
244
|
+
e.preventDefault()
|
|
245
|
+
confirm(filtered.value[activeIndex.value])
|
|
246
|
+
} else if (e.key === "Escape") {
|
|
247
|
+
e.preventDefault()
|
|
248
|
+
close()
|
|
249
|
+
} else if (e.key === "Backspace") {
|
|
250
|
+
const el = e.target
|
|
251
|
+
const val = el.value
|
|
252
|
+
const pos = el.selectionStart
|
|
253
|
+
const before = val.substring(0, pos)
|
|
254
|
+
const triggerKey = activeTrigger.value?.key
|
|
255
|
+
const triggerIdx = before.lastIndexOf(triggerKey)
|
|
256
|
+
if (pos <= triggerIdx + triggerKey.length && query.value === "") {
|
|
257
|
+
close()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let blurTimer = null
|
|
263
|
+
|
|
264
|
+
function handleBlur() {
|
|
265
|
+
blurTimer = setTimeout(close, 150)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const handlers = {
|
|
269
|
+
keydown: handleKeydown,
|
|
270
|
+
input: handleInput,
|
|
271
|
+
blur: handleBlur
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function setActiveIndex(i) {
|
|
275
|
+
activeIndex.value = i
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
open,
|
|
280
|
+
filtered,
|
|
281
|
+
activeIndex,
|
|
282
|
+
loading,
|
|
283
|
+
query,
|
|
284
|
+
virtualAnchor,
|
|
285
|
+
activeTrigger,
|
|
286
|
+
handlers,
|
|
287
|
+
confirm,
|
|
288
|
+
close,
|
|
289
|
+
setActiveIndex
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { onUnmounted } from "vue"
|
|
2
|
+
|
|
3
|
+
export function usePointerDrag({ onDrag, onStart, onEnd }) {
|
|
4
|
+
let active = false
|
|
5
|
+
|
|
6
|
+
function handleMouseMove(e) {
|
|
7
|
+
if (!active) return
|
|
8
|
+
onDrag(e.movementX)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function handleMouseUp() {
|
|
12
|
+
if (!active) return
|
|
13
|
+
active = false
|
|
14
|
+
document.exitPointerLock()
|
|
15
|
+
document.removeEventListener("mousemove", handleMouseMove)
|
|
16
|
+
document.removeEventListener("mouseup", handleMouseUp)
|
|
17
|
+
onEnd?.()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function start(e) {
|
|
21
|
+
e.preventDefault()
|
|
22
|
+
active = true
|
|
23
|
+
e.target.requestPointerLock()
|
|
24
|
+
document.addEventListener("mousemove", handleMouseMove)
|
|
25
|
+
document.addEventListener("mouseup", handleMouseUp)
|
|
26
|
+
onStart?.()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onUnmounted(() => {
|
|
30
|
+
if (active) {
|
|
31
|
+
active = false
|
|
32
|
+
document.exitPointerLock()
|
|
33
|
+
document.removeEventListener("mousemove", handleMouseMove)
|
|
34
|
+
document.removeEventListener("mouseup", handleMouseUp)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return { start }
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./style.css"
|
package/src/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { default as Button } from "./components/Button.vue"
|
|
2
|
+
export { default as Input } from "./components/Input.vue"
|
|
3
|
+
export { default as NumberInput } from "./components/NumberInput.vue"
|
|
4
|
+
export { default as Select } from "./components/Select.vue"
|
|
5
|
+
export { default as Dropdown } from "./components/Dropdown.vue"
|
|
6
|
+
export { default as Checkbox } from "./components/Checkbox.vue"
|
|
7
|
+
export { default as GhostInput } from "./components/GhostInput.vue"
|
|
8
|
+
export { default as Tabs } from "./components/Tabs.vue"
|
|
9
|
+
export { default as ToggleGroup } from "./components/ToggleGroup.vue"
|
|
10
|
+
export { default as Badge } from "./components/Badge.vue"
|
|
11
|
+
export { default as Popover } from "./components/Popover.vue"
|
|
12
|
+
export { default as RadioGroup } from "./components/RadioGroup.vue"
|
|
13
|
+
export { default as PopConfirm } from "./components/PopConfirm.vue"
|
|
14
|
+
export { default as Modal } from "./components/Modal.vue"
|
|
15
|
+
export { default as Tooltip } from "./components/Tooltip.vue"
|
|
16
|
+
export { default as Combobox } from "./components/Combobox.vue"
|
|
17
|
+
export { default as ContextMenu } from "./components/ContextMenu.vue"
|
|
18
|
+
export { default as Collapsible } from "./components/Collapsible.vue"
|
|
19
|
+
export { default as Tree } from "./components/Tree.vue"
|
|
20
|
+
export { default as MultiCombobox } from "./components/MultiCombobox.vue"
|
|
21
|
+
export { default as RadioCards } from "./components/RadioCards.vue"
|
|
22
|
+
export { default as CheckboxCards } from "./components/CheckboxCards.vue"
|
|
23
|
+
export { default as RangeSlider } from "./components/RangeSlider.vue"
|
|
24
|
+
export { default as ToggleButtons } from "./components/ToggleButtons.vue"
|
|
25
|
+
export { default as NavTree } from "./components/NavTree.vue"
|
|
26
|
+
export { default as DataTable } from "./components/DataTable.vue"
|
|
27
|
+
export { default as KeyValue } from "./components/KeyValue.vue"
|
|
28
|
+
export { default as CollapsibleGroup } from "./components/CollapsibleGroup.vue"
|
|
29
|
+
export { default as ScrollBox } from "./components/ScrollBox.vue"
|
|
30
|
+
export { default as TagInput } from "./components/TagInput.vue"
|
|
31
|
+
export { default as SectionHeader } from "./components/SectionHeader.vue"
|
|
32
|
+
export { default as Switch } from "./components/Switch.vue"
|
|
33
|
+
export { default as Textarea } from "./components/Textarea.vue"
|
|
34
|
+
export { default as LabeledTextarea } from "./components/LabeledTextarea.vue"
|
|
35
|
+
export { default as LabeledTextareaGroup } from "./components/LabeledTextareaGroup.vue"
|
|
36
|
+
export { default as ToastContainer } from "./components/ToastContainer.vue"
|
|
37
|
+
export { default as Mention } from "./components/Mention.vue"
|
|
38
|
+
export { default as CodeEditor } from "./components/CodeEditor.vue"
|
|
39
|
+
|
|
40
|
+
export { useClickOutside } from "./composables/useClickOutside.js"
|
|
41
|
+
export { usePointerDrag } from "./composables/usePointerDrag.js"
|
|
42
|
+
export { toast } from "./composables/toast.js"
|
|
43
|
+
export { useMention } from "./composables/useMention.js"
|