@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.
Files changed (90) hide show
  1. package/.claude/settings.local.json +13 -0
  2. package/.lore +83 -0
  3. package/histoire.config.js +43 -0
  4. package/package.json +39 -0
  5. package/postcss.config.js +6 -0
  6. package/src/components/Badge.vue +36 -0
  7. package/src/components/Button.vue +44 -0
  8. package/src/components/Checkbox.vue +51 -0
  9. package/src/components/CheckboxCards.vue +61 -0
  10. package/src/components/CodeEditor.vue +299 -0
  11. package/src/components/Collapsible.vue +69 -0
  12. package/src/components/CollapsibleGroup.vue +38 -0
  13. package/src/components/Combobox.vue +179 -0
  14. package/src/components/ContextMenu.vue +65 -0
  15. package/src/components/ContextMenuPanel.vue +115 -0
  16. package/src/components/DataTable.vue +326 -0
  17. package/src/components/Dropdown.vue +127 -0
  18. package/src/components/GhostInput.vue +29 -0
  19. package/src/components/Input.vue +23 -0
  20. package/src/components/KeyValue.vue +149 -0
  21. package/src/components/LabeledTextarea.vue +64 -0
  22. package/src/components/LabeledTextareaGroup.vue +14 -0
  23. package/src/components/Mention.vue +79 -0
  24. package/src/components/Modal.vue +109 -0
  25. package/src/components/MultiCombobox.vue +209 -0
  26. package/src/components/NavTree.vue +98 -0
  27. package/src/components/NumberInput.vue +128 -0
  28. package/src/components/PopConfirm.vue +94 -0
  29. package/src/components/Popover.vue +53 -0
  30. package/src/components/RadioCards.vue +37 -0
  31. package/src/components/RadioGroup.vue +57 -0
  32. package/src/components/RangeSlider.vue +165 -0
  33. package/src/components/ScrollBox.vue +78 -0
  34. package/src/components/SectionHeader.vue +18 -0
  35. package/src/components/Select.vue +187 -0
  36. package/src/components/Switch.vue +85 -0
  37. package/src/components/Tabs.vue +34 -0
  38. package/src/components/TagInput.vue +80 -0
  39. package/src/components/Textarea.vue +97 -0
  40. package/src/components/ToastContainer.vue +104 -0
  41. package/src/components/ToggleButtons.vue +45 -0
  42. package/src/components/ToggleGroup.vue +30 -0
  43. package/src/components/Tooltip.vue +56 -0
  44. package/src/components/Tree.vue +188 -0
  45. package/src/composables/toast.js +54 -0
  46. package/src/composables/useClickOutside.js +23 -0
  47. package/src/composables/useMention.js +291 -0
  48. package/src/composables/usePointerDrag.js +39 -0
  49. package/src/histoire-setup.js +1 -0
  50. package/src/index.js +43 -0
  51. package/src/style.css +96 -0
  52. package/stories/Badge.story.vue +24 -0
  53. package/stories/Button.story.vue +45 -0
  54. package/stories/Checkbox.story.vue +31 -0
  55. package/stories/CheckboxCards.story.vue +51 -0
  56. package/stories/CodeEditor.story.vue +71 -0
  57. package/stories/Collapsible.story.vue +84 -0
  58. package/stories/Combobox.story.vue +44 -0
  59. package/stories/ContextMenu.story.vue +59 -0
  60. package/stories/DataTable.story.vue +185 -0
  61. package/stories/Dropdown.story.vue +49 -0
  62. package/stories/GhostInput.story.vue +24 -0
  63. package/stories/Input.story.vue +23 -0
  64. package/stories/KeyValue.story.vue +104 -0
  65. package/stories/LabeledTextarea.story.vue +44 -0
  66. package/stories/Mention.story.vue +166 -0
  67. package/stories/Modal.story.vue +86 -0
  68. package/stories/MultiCombobox.story.vue +76 -0
  69. package/stories/NavTree.story.vue +184 -0
  70. package/stories/NumberInput.story.vue +31 -0
  71. package/stories/Overview.story.vue +85 -0
  72. package/stories/PopConfirm.story.vue +39 -0
  73. package/stories/RadioCards.story.vue +66 -0
  74. package/stories/RadioGroup.story.vue +52 -0
  75. package/stories/RangeSlider.story.vue +75 -0
  76. package/stories/ScrollBox.story.vue +54 -0
  77. package/stories/SectionHeader.story.vue +22 -0
  78. package/stories/Select.story.vue +34 -0
  79. package/stories/Switch.story.vue +42 -0
  80. package/stories/Tabs.story.vue +34 -0
  81. package/stories/TagInput.story.vue +54 -0
  82. package/stories/Textarea.story.vue +28 -0
  83. package/stories/Toast.story.vue +28 -0
  84. package/stories/ToggleButtons.story.vue +57 -0
  85. package/stories/ToggleGroup.story.vue +34 -0
  86. package/stories/Tooltip.story.vue +55 -0
  87. package/stories/Tree.story.vue +115 -0
  88. package/tailwind.config.js +9 -0
  89. package/tailwind.preset.js +79 -0
  90. 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"