@bagelink/vue 1.17.2 → 1.17.6

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 (95) hide show
  1. package/dist/assetTransport.d.ts +9 -31
  2. package/dist/assetTransport.d.ts.map +1 -1
  3. package/dist/assets.d.ts +39 -20
  4. package/dist/assets.d.ts.map +1 -1
  5. package/dist/components/AddressSearch.vue.d.ts +40 -36
  6. package/dist/components/AddressSearch.vue.d.ts.map +1 -1
  7. package/dist/components/ContextMenu.vue.d.ts +518 -0
  8. package/dist/components/ContextMenu.vue.d.ts.map +1 -0
  9. package/dist/components/Dropdown.types.d.ts +25 -0
  10. package/dist/components/Dropdown.types.d.ts.map +1 -0
  11. package/dist/components/Dropdown.vue.d.ts +20 -15
  12. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  13. package/dist/components/FilterQuery.vue.d.ts +1 -1
  14. package/dist/components/FilterQuery.vue.d.ts.map +1 -1
  15. package/dist/components/Icon/Icon.vue.d.ts +0 -1
  16. package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
  17. package/dist/components/Icon/constants.d.ts +0 -1
  18. package/dist/components/Icon/constants.d.ts.map +1 -1
  19. package/dist/components/Icon/types.d.ts +2 -3
  20. package/dist/components/Icon/types.d.ts.map +1 -1
  21. package/dist/components/Swiper.vue.d.ts +1 -1
  22. package/dist/components/draggable/useDraggable.d.ts.map +1 -1
  23. package/dist/components/form/inputs/IconPickerInput.vue.d.ts +48 -37
  24. package/dist/components/form/inputs/IconPickerInput.vue.d.ts.map +1 -1
  25. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
  26. package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
  27. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +3 -0
  28. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
  29. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +1 -1
  30. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
  31. package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
  32. package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
  33. package/dist/components/form/inputs/RichText/utils/selection.d.ts +2 -0
  34. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  35. package/dist/components/form/inputs/RichText/utils/table.d.ts +34 -8
  36. package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
  37. package/dist/components/form/inputs/SelectInput.vue.d.ts +71 -63
  38. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  39. package/dist/components/form/inputs/TelInput.vue.d.ts +2 -2
  40. package/dist/components/index.d.ts +3 -1
  41. package/dist/components/index.d.ts.map +1 -1
  42. package/dist/composables/index.d.ts +1 -1
  43. package/dist/composables/index.d.ts.map +1 -1
  44. package/dist/composables/useBoundedRectDrag.d.ts.map +1 -1
  45. package/dist/composables/usePointerSession.d.ts +14 -3
  46. package/dist/composables/usePointerSession.d.ts.map +1 -1
  47. package/dist/core.css +3 -3
  48. package/dist/directives/vResize.d.ts.map +1 -1
  49. package/dist/form-flow/form-flow.d.ts +4 -2
  50. package/dist/form-flow/form-flow.d.ts.map +1 -1
  51. package/dist/form-flow/schema-fields.d.ts +4 -1
  52. package/dist/form-flow/schema-fields.d.ts.map +1 -1
  53. package/dist/index.cjs +240 -153
  54. package/dist/index.css +1 -1
  55. package/dist/index.mjs +18459 -18281
  56. package/dist/style.css +3 -3
  57. package/dist/types/BagelForm.d.ts +2 -0
  58. package/dist/types/BagelForm.d.ts.map +1 -1
  59. package/package.json +1 -1
  60. package/src/assetTransport.test.ts +33 -83
  61. package/src/assetTransport.ts +24 -90
  62. package/src/assets.test.ts +58 -99
  63. package/src/assets.ts +78 -161
  64. package/src/components/ContextMenu.vue +113 -0
  65. package/src/components/Dropdown.types.ts +27 -0
  66. package/src/components/Dropdown.vue +129 -64
  67. package/src/components/FilterQuery.vue +1 -1
  68. package/src/components/Icon/Icon.vue +8 -30
  69. package/src/components/Icon/constants.ts +0 -166
  70. package/src/components/Icon/types.ts +2 -3
  71. package/src/components/draggable/useDraggable.ts +67 -100
  72. package/src/components/form/inputs/IconPickerInput.vue +14 -9
  73. package/src/components/form/inputs/RichText/components/EditorToolbar.vue +43 -111
  74. package/src/components/form/inputs/RichText/composables/useCommands.ts +32 -54
  75. package/src/components/form/inputs/RichText/composables/useEditor.ts +45 -171
  76. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +90 -14
  77. package/src/components/form/inputs/RichText/index.vue +1195 -1210
  78. package/src/components/form/inputs/RichText/utils/commands.ts +689 -1028
  79. package/src/components/form/inputs/RichText/utils/selection.ts +66 -41
  80. package/src/components/form/inputs/RichText/utils/table.ts +798 -136
  81. package/src/components/form/inputs/SelectInput.vue +1 -1
  82. package/src/components/form/inputs/Upload/AssetUploadInput.vue +2 -2
  83. package/src/components/index.ts +3 -1
  84. package/src/composables/index.ts +1 -1
  85. package/src/composables/useBoundedRectDrag.ts +9 -12
  86. package/src/composables/usePointerSession.ts +57 -12
  87. package/src/directives/vResize.ts +6 -7
  88. package/src/form-flow/form-flow.ts +8 -3
  89. package/src/form-flow/schema-fields.ts +9 -4
  90. package/src/styles/core.css +1 -18
  91. package/src/types/BagelForm.ts +2 -0
  92. package/dist/components/form/inputs/RichText/utils/formatting.d.ts +0 -10
  93. package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +0 -1
  94. package/src/components/form/inputs/RichText/components/gridBox.vue +0 -58
  95. package/src/components/form/inputs/RichText/utils/formatting.ts +0 -331
package/src/assets.ts CHANGED
@@ -1,132 +1,75 @@
1
1
  import { inject, onScopeDispose, ref, toValue, watch, type InjectionKey, type MaybeRefOrGetter } from 'vue'
2
2
 
3
- /** Persist this reference, never an AssetDelivery. Version pins are immutable. */
3
+ /**
4
+ * Persist this reference, never a delivery URL. Assets are public-by-default;
5
+ * the id alone addresses the bytes. ``version_id`` is accepted for backward
6
+ * compatibility with callers that still emit it, but it is ignored: it no
7
+ * longer selects a version and never appears in a URL.
8
+ */
4
9
  export interface AssetReference {
5
10
  asset_id: string
6
11
  version_id?: string
7
12
  }
8
13
 
9
- export interface AssetDelivery {
10
- url: string
11
- expires_at: string
12
- version_id: string
14
+ export type AssetVisibility = 'protected' | 'public'
15
+
16
+ export interface AssetUploadOptions {
17
+ /** Protected is the safe default. Website/media callers must opt into public. */
18
+ visibility?: AssetVisibility
13
19
  }
14
20
 
21
+ /** Display purpose → on-demand transform query params (mirrors common/blox). */
15
22
  export type AssetPurpose = 'original' | 'preview' | 'thumbnail' | 'website' | 'social'
16
23
 
24
+ const PURPOSE_PARAMS: Record<AssetPurpose, string> = {
25
+ original: '',
26
+ preview: 'fmt=webp',
27
+ thumbnail: 'fmt=webp&w=256',
28
+ website: 'fmt=webp',
29
+ social: 'w=1200&h=630&fmt=jpeg&fit=cover',
30
+ }
31
+
32
+ /** A display intent: a pinned asset plus an optional purpose/preset. */
17
33
  export interface AssetDeliveryRequest extends AssetReference {
18
- /** Backend-approved preset, e.g. an image preview; never arbitrary transforms. */
19
34
  representation?: AssetPurpose
20
35
  }
21
36
 
22
- /** A separate facade: generated SDK namespaces remain unchanged. */
37
+ /** Separate facade: generated SDK namespaces remain unchanged. */
23
38
  export interface AssetTransport {
24
- /** Return deliveries in request order; adapt the SDK's `{items}` envelope here. */
25
- resolve(references: readonly AssetDeliveryRequest[], signal: AbortSignal): Promise<AssetDelivery[]>
26
- upload(file: File, signal: AbortSignal): Promise<AssetReference>
39
+ upload(file: File, signal: AbortSignal, options?: AssetUploadOptions): Promise<AssetReference>
27
40
  }
28
41
 
29
- interface Pending {
30
- reference: AssetDeliveryRequest
31
- signal?: AbortSignal
32
- resolve: (delivery: AssetDelivery) => void
33
- reject: (reason: unknown) => void
42
+ export interface AssetClient {
43
+ /** Upload original bytes; protected unless the caller explicitly opts into public. */
44
+ upload(file: File, signal?: AbortSignal, options?: AssetUploadOptions): Promise<AssetReference>
45
+ /** Build the public CDN URL for a reference + optional purpose. */
46
+ assetUrl(reference: AssetReference, purpose?: AssetPurpose): string
47
+ /** Abort in-flight uploads (logout / tenant / account switch). */
48
+ clearSession(): void
49
+ /** Monotonic counter bumped on clearSession; watchers can subscribe. */
50
+ revision: number
34
51
  }
35
52
 
36
53
  const abortError = () => new DOMException('Asset request cancelled', 'AbortError')
37
54
 
38
- /** App/session scoped, with no persistent storage or delivery cache. */
39
- export function createAssetClient(transport: AssetTransport) {
40
- const revision = ref(0)
55
+ /** App/session scoped; no persistent storage or delivery cache (URLs are public). */
56
+ export function createAssetClient(
57
+ transport: AssetTransport,
58
+ deliveryBaseUrl: string,
59
+ ): AssetClient {
41
60
  const controllers = new Set<AbortController>()
42
- let pending: Pending[] = []
43
- let scheduled = false
44
- const inFlight = new Map<string, { controller: AbortController; promise: Promise<AssetDelivery>; users: number }>()
45
-
46
- async function flush() {
47
- scheduled = false
48
- const batch = pending
49
- pending = []
50
- const active = batch.filter(item => !item.signal?.aborted)
51
- for (const item of batch) if (item.signal?.aborted) item.reject(abortError())
52
- if (!active.length) return
53
- const controller = new AbortController()
54
- controllers.add(controller)
55
- const cancelIfUnused = () => {
56
- for (const item of active) if (item.signal?.aborted) item.reject(abortError())
57
- if (active.every(item => item.signal?.aborted)) controller.abort()
58
- }
59
- const rejectAborted = () => {
60
- for (const item of active) item.reject(abortError())
61
- }
62
- controller.signal.addEventListener('abort', rejectAborted, { once: true })
63
- for (const item of active) item.signal?.addEventListener('abort', cancelIfUnused)
64
- try {
65
- const deliveries = await transport.resolve(active.map(item => item.reference), controller.signal)
66
- if (controller.signal.aborted) throw abortError()
67
- if (deliveries.length !== active.length) throw new Error('Incomplete asset delivery batch')
68
- active.forEach((item, index) => {
69
- const delivery = deliveries[index]!
70
- if (item.signal?.aborted) item.reject(abortError())
71
- else if (!Number.isFinite(Date.parse(delivery.expires_at))
72
- || Date.parse(delivery.expires_at) <= Date.now()
73
- || !/^https?:\/\//.test(delivery.url)
74
- || !delivery.version_id
75
- || (item.reference.version_id && item.reference.version_id !== delivery.version_id)) {
76
- item.reject(new Error('Invalid asset delivery'))
77
- } else item.resolve(delivery)
78
- })
79
- } catch (error) {
80
- for (const item of active) item.reject(error)
81
- } finally {
82
- controllers.delete(controller)
83
- controller.signal.removeEventListener('abort', rejectAborted)
84
- for (const item of active) item.signal?.removeEventListener('abort', cancelIfUnused)
85
- }
61
+ let revision = 0
62
+
63
+ function assetUrl(reference: AssetReference, purpose: AssetPurpose = 'original'): string {
64
+ const query = PURPOSE_PARAMS[purpose] ?? ''
65
+ const base = deliveryBaseUrl.replace(/\/+$/, '')
66
+ return query ? `${base}/${reference.asset_id}?${query}` : `${base}/${reference.asset_id}`
86
67
  }
87
68
 
88
69
  return {
89
- revision,
90
- resolve(reference: AssetDeliveryRequest, signal?: AbortSignal): Promise<AssetDelivery> {
91
- if (signal?.aborted) return Promise.reject(abortError())
92
- const key = JSON.stringify([reference.asset_id, reference.version_id ?? null, reference.representation ?? 'original'])
93
- let entry = inFlight.get(key)
94
- if (!entry) {
95
- const controller = new AbortController()
96
- const promise = new Promise<AssetDelivery>((resolve, reject) => {
97
- pending.push({ reference: { ...reference }, signal: controller.signal, resolve, reject })
98
- if (!scheduled) { scheduled = true; queueMicrotask(() => { void flush() }) }
99
- })
100
- entry = { controller, promise, users: 0 }
101
- inFlight.set(key, entry)
102
- const current = entry
103
- void promise.then(() => {
104
- if (inFlight.get(key) === current) inFlight.delete(key)
105
- }, () => {
106
- if (inFlight.get(key) === current) inFlight.delete(key)
107
- })
108
- }
109
- const shared = entry
110
- shared.users++
111
- return new Promise((resolve, reject) => {
112
- let settled = false
113
- const finish = () => {
114
- if (settled) return false
115
- settled = true
116
- signal?.removeEventListener('abort', abort)
117
- shared.users--
118
- if (!shared.users) {
119
- if (inFlight.get(key) === shared) inFlight.delete(key)
120
- shared.controller.abort()
121
- }
122
- return true
123
- }
124
- const abort = () => { if (finish()) reject(abortError()) }
125
- signal?.addEventListener('abort', abort, { once: true })
126
- void shared.promise.then(value => { if (finish()) resolve(value) }, error => { if (finish()) reject(error) })
127
- })
128
- },
129
- async upload(file: File, signal?: AbortSignal): Promise<AssetReference> {
70
+ get revision() { return revision },
71
+ assetUrl,
72
+ async upload(file: File, signal?: AbortSignal, options?: AssetUploadOptions): Promise<AssetReference> {
130
73
  const controller = new AbortController()
131
74
  const abort = () => controller.abort()
132
75
  signal?.addEventListener('abort', abort, { once: true })
@@ -134,26 +77,22 @@ export function createAssetClient(transport: AssetTransport) {
134
77
  try {
135
78
  if (signal?.aborted) controller.abort()
136
79
  if (controller.signal.aborted) throw abortError()
137
- const reference = await transport.upload(file, controller.signal)
80
+ const reference = await transport.upload(file, controller.signal, options)
138
81
  if (controller.signal.aborted) throw abortError()
139
- return { asset_id: reference.asset_id, version_id: reference.version_id }
82
+ return { asset_id: reference.asset_id }
140
83
  } finally {
141
84
  controllers.delete(controller)
142
85
  signal?.removeEventListener('abort', abort)
143
86
  }
144
87
  },
145
- /** Call synchronously before logout/tenant/account transitions. */
146
88
  clearSession() {
147
- inFlight.clear()
148
89
  for (const controller of controllers) controller.abort()
149
- for (const item of pending) item.reject(abortError())
150
- pending = []
151
- revision.value++
90
+ controllers.clear()
91
+ revision++
152
92
  },
153
93
  }
154
94
  }
155
95
 
156
- export type AssetClient = ReturnType<typeof createAssetClient>
157
96
  export const ASSET_CLIENT_KEY: InjectionKey<AssetClient> = Symbol('bagel:assets')
158
97
 
159
98
  /** Opt-in upload state; original bytes are handed unchanged to the transport. */
@@ -169,14 +108,14 @@ export function useAssetUpload() {
169
108
  uploading.value = false
170
109
  reference.value = undefined
171
110
  }
172
- async function upload(file: File): Promise<AssetReference> {
111
+ async function upload(file: File, options?: AssetUploadOptions): Promise<AssetReference> {
173
112
  cancel()
174
113
  if (!client) throw new Error('Asset client is not configured')
175
114
  const current = generation
176
115
  controller = new AbortController()
177
116
  uploading.value = true
178
117
  try {
179
- const result = await client.upload(file, controller.signal)
118
+ const result = await client.upload(file, controller.signal, options)
180
119
  if (current !== generation) throw abortError()
181
120
  reference.value = result
182
121
  return result
@@ -184,62 +123,40 @@ export function useAssetUpload() {
184
123
  if (current === generation) uploading.value = false
185
124
  }
186
125
  }
187
- const stop = client ? watch(client.revision, cancel, { flush: 'sync' }) : undefined
126
+ const stop = client ? watch(() => client.revision, cancel, { flush: 'sync' }) : undefined
188
127
  onScopeDispose(() => { cancel(); stop?.() })
189
128
  return { upload, cancel, uploading, reference }
190
129
  }
191
130
 
192
- /** Private resolution stays inside display primitives, not persisted domain data. */
193
- export function useAssetDelivery(source: MaybeRefOrGetter<AssetDeliveryRequest | undefined>) {
131
+ /**
132
+ * Resolve an asset reference to a public display URL. Public-by-default means
133
+ * no server round-trip and no expiry: the URL is deterministic from the asset id.
134
+ */
135
+ export function useAssetUrl(source: MaybeRefOrGetter<AssetDeliveryRequest | undefined>) {
194
136
  const client = inject(ASSET_CLIENT_KEY, undefined)
195
137
  const url = ref<string>()
196
138
  const error = ref(false)
197
- let controller: AbortController | undefined
198
- let timer: ReturnType<typeof setTimeout> | undefined
199
- let generation = 0
200
- let retried = false
201
- let invalidated = false
202
139
 
203
- function cancel(keepUrl = false) {
204
- generation++
205
- controller?.abort()
206
- clearTimeout(timer)
207
- if (!keepUrl) url.value = undefined
208
- }
209
- async function load(refresh = false) {
210
- cancel(refresh)
211
- error.value = false
212
- if (invalidated) return
140
+ watch(() => {
213
141
  const reference = toValue(source)
214
- if (!reference) return
215
- if (!client) { error.value = true; return }
216
- const current = generation
217
- controller = new AbortController()
218
- try {
219
- const delivery = await client.resolve(reference, controller.signal)
220
- if (current !== generation) return
221
- url.value = delivery.url
222
- const remaining = Date.parse(delivery.expires_at) - Date.now()
223
- timer = setTimeout(() => { void load(true) }, Math.min(2_147_483_647, Math.max(1, remaining * 0.8)))
224
- } catch {
225
- if (current === generation) { error.value = true; url.value = undefined }
226
- }
227
- }
228
- function onError() {
229
- if (retried) { cancel(); error.value = true; return }
230
- retried = true
231
- void load()
232
- }
233
- watch(() => [toValue(source)?.asset_id, toValue(source)?.version_id, toValue(source)?.representation], () => {
234
- retried = false
235
- invalidated = false
236
- void load()
142
+ return reference ? `${reference.asset_id}:${reference.representation ?? 'original'}` : ''
143
+ }, () => {
144
+ const reference = toValue(source)
145
+ if (!reference) { url.value = undefined; error.value = false; return }
146
+ if (!client) { url.value = undefined; error.value = true; return }
147
+ url.value = client.assetUrl(reference, reference.representation)
148
+ error.value = false
237
149
  }, { immediate: true })
238
- // Never automatically reauthorize old references under a different session.
239
- const stop = client ? watch(client.revision, () => {
240
- invalidated = true
241
- cancel()
242
- }, { flush: 'sync' }) : undefined
243
- onScopeDispose(() => { cancel(); stop?.() })
244
- return { url, error, onError }
150
+
151
+ return { url, error }
152
+ }
153
+
154
+ /**
155
+ * @deprecated Backward-compatible alias for ``useAssetUrl``. Public assets have
156
+ * no expiry, so the returned URL never needs refreshing; ``onError`` is a no-op
157
+ * retry hook kept for callers that expected the old grant-refresh contract.
158
+ */
159
+ export function useAssetDelivery(source: MaybeRefOrGetter<AssetDeliveryRequest | undefined>) {
160
+ const { url, error } = useAssetUrl(source)
161
+ return { url, error, onError: () => { error.value = true } }
245
162
  }
@@ -0,0 +1,113 @@
1
+ <script setup lang="ts">
2
+ import type { VirtualElement } from '@floating-ui/dom'
3
+ import type { AlignedPlacement, AnchorPoint, DropdownItem } from './Dropdown.types'
4
+ import { computed, ref, useSlots } from 'vue'
5
+ import Dropdown from './Dropdown.vue'
6
+
7
+ defineOptions({ inheritAttrs: false, name: 'BglContextMenu' })
8
+
9
+ const props = withDefaults(defineProps<{
10
+ /** Declarative menu items. */
11
+ items?: DropdownItem[]
12
+ /** Optional header title shown at the top of the context menu. */
13
+ title?: string
14
+ /** Explicit viewport anchor `{ x, y, width?, height? }` for controlled mode. */
15
+ anchorPoint?: AnchorPoint | null
16
+ /** External reference element or VirtualElement. */
17
+ referenceEl?: Element | VirtualElement | null
18
+ placement?: AlignedPlacement
19
+ popupClass?: string
20
+ autoHide?: boolean
21
+ disabled?: boolean
22
+ }>(), {
23
+ placement: 'bottom-start',
24
+ autoHide: true,
25
+ disabled: false,
26
+ })
27
+
28
+ const emit = defineEmits<{
29
+ show: []
30
+ hide: []
31
+ select: [value: string | number | DropdownItem, item: DropdownItem]
32
+ }>()
33
+
34
+ const shown = defineModel('shown', { type: Boolean, default: false })
35
+ const slots = useSlots()
36
+ const dropdownRef = ref<InstanceType<typeof Dropdown> | null>(null)
37
+ const internalPoint = ref<AnchorPoint>({ x: 0, y: 0 })
38
+
39
+ const hasTargetSlot = computed(() => Boolean(slots.target))
40
+ const effectiveAnchorPoint = computed<AnchorPoint | null>(() => {
41
+ if (props.anchorPoint) return props.anchorPoint
42
+ if (props.referenceEl) return null
43
+ return internalPoint.value
44
+ })
45
+
46
+ const effectiveItems = computed<DropdownItem[]>(() => {
47
+ if (!props.items?.length) return []
48
+ if (props.title) {
49
+ return [{ header: props.title }, ...props.items]
50
+ }
51
+ return props.items
52
+ })
53
+
54
+ function openAt(target: AnchorPoint | MouseEvent) {
55
+ if (props.disabled) return
56
+ if ('clientX' in target && 'clientY' in target) {
57
+ internalPoint.value = { x: target.clientX, y: target.clientY, width: 0, height: 0 }
58
+ } else {
59
+ internalPoint.value = { ...target }
60
+ }
61
+ dropdownRef.value?.openAt(internalPoint.value)
62
+ }
63
+
64
+ function onTargetContextMenu(e: MouseEvent) {
65
+ if (props.disabled) return
66
+ e.preventDefault()
67
+ openAt(e)
68
+ }
69
+
70
+ function show() {
71
+ dropdownRef.value?.show()
72
+ }
73
+
74
+ function hide() {
75
+ dropdownRef.value?.hide()
76
+ }
77
+
78
+ defineExpose({ openAt, show, hide, shown })
79
+ </script>
80
+
81
+ <template>
82
+ <div
83
+ v-if="hasTargetSlot"
84
+ v-bind="$attrs"
85
+ style="display: contents"
86
+ @contextmenu="onTargetContextMenu"
87
+ >
88
+ <slot name="target" :open-at="openAt" :hide="hide" :shown="shown" />
89
+ </div>
90
+
91
+ <Dropdown
92
+ ref="dropdownRef"
93
+ v-model:shown="shown"
94
+ :anchor-point="effectiveAnchorPoint"
95
+ :reference-el="referenceEl"
96
+ :placement="placement"
97
+ :items="effectiveItems"
98
+ :popup-class="popupClass"
99
+ :auto-hide="autoHide"
100
+ :disabled="disabled"
101
+ :triggers="[]"
102
+ @show="emit('show')"
103
+ @hide="emit('hide')"
104
+ @select="(val, item) => emit('select', val, item)"
105
+ >
106
+ <template #default="slotProps">
107
+ <div v-if="title && !effectiveItems.length" class="px-05 py-025 txt11 semi opacity-5 uppercase">
108
+ {{ title }}
109
+ </div>
110
+ <slot v-bind="slotProps" />
111
+ </template>
112
+ </Dropdown>
113
+ </template>
@@ -0,0 +1,27 @@
1
+ import type { IconType, ThemeType } from '../types'
2
+
3
+ export type Side = 'top' | 'right' | 'bottom' | 'left'
4
+ export type Alignment = 'start' | 'end'
5
+ export type AlignedPlacement = 'auto' | Side | `${Side}-${Alignment}`
6
+ export type TriggerEvent = 'click' | 'hover'
7
+
8
+ export interface AnchorPoint {
9
+ x: number
10
+ y: number
11
+ width?: number
12
+ height?: number
13
+ }
14
+
15
+ export interface DropdownItem {
16
+ label?: string
17
+ value?: string | number
18
+ icon?: IconType
19
+ color?: ThemeType
20
+ danger?: boolean
21
+ disabled?: boolean
22
+ active?: boolean
23
+ divider?: boolean
24
+ header?: string
25
+ class?: string
26
+ action?: () => void
27
+ }