@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
@@ -1,15 +1,12 @@
1
1
  <!-- prettier-ignore-start -->
2
2
  <!-- eslint-disable -->
3
3
  <script setup lang="ts">
4
+ import type { VirtualElement } from '@floating-ui/dom'
4
5
  import type { IconType, ThemeType } from '../index'
5
- import { Btn, useDevice, useEscapeKey } from '../index'
6
+ import type { AlignedPlacement, AnchorPoint, DropdownItem, TriggerEvent } from './Dropdown.types'
7
+ import { Btn, Divider, useDevice, useEscapeKey } from '../index'
6
8
  import { autoPlacement, computePosition, flip, offset, shift } from '@floating-ui/dom'
7
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch } from 'vue'
8
-
9
- export type Side = 'top' | 'right' | 'bottom' | 'left'
10
- export type Alignment = 'start' | 'end'
11
- export type AlignedPlacement = 'auto' | Side | `${Side}-${Alignment}`
12
- export type TriggerEvent = 'click' | 'hover'
9
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, useSlots, watch } from 'vue'
13
10
 
14
11
  defineOptions({ inheritAttrs: false, name: 'DropDown' })
15
12
  const props = withDefaults(defineProps<{
@@ -26,9 +23,7 @@ const props = withDefaults(defineProps<{
26
23
  icon?: IconType
27
24
  color?: ThemeType
28
25
  triggerClass?: string
29
- /** Extra class(es) applied to the teleported popover element. Useful to scope
30
- * a theme onto the popup, which renders outside the trigger's DOM subtree
31
- * (e.g. `popup-class="bgl-dark-mode"` for an always-dark recreation). */
26
+ /** Extra class(es) applied to the teleported popover element. */
32
27
  popupClass?: string
33
28
  iconEnd?: IconType
34
29
  outline?: boolean
@@ -39,18 +34,20 @@ const props = withDefaults(defineProps<{
39
34
  noAutoFocus?: boolean
40
35
  autoHide?: boolean
41
36
  triggers?: TriggerEvent[]
42
- showTriggers?: TriggerEvent[]
43
- hideTriggers?: TriggerEvent[]
44
37
  popperTriggers?: TriggerEvent[]
45
- popperShowTriggers?: TriggerEvent[]
46
- popperHideTriggers?: TriggerEvent[]
47
38
  showGroup?: string
48
39
  card?: boolean
49
40
  /** Remove all popup content padding, whether or not the slot contains a Card. */
50
41
  noPadding?: boolean
51
42
  delay?: number | { show: number; hide: number }
52
- /** Override the anchor element used for positioning. When provided, the #trigger slot is not rendered. */
53
- referenceEl?: Element | null
43
+ /** Declarative menu items. Can be used instead of or alongside the default slot. */
44
+ items?: DropdownItem[]
45
+ /** Alias for `items` for backward compatibility. */
46
+ options?: DropdownItem[]
47
+ /** Override the anchor element (or Floating UI VirtualElement) used for positioning. */
48
+ referenceEl?: Element | VirtualElement | null
49
+ /** Position the dropdown at explicit viewport coordinates `{ x, y, width?, height? }`. */
50
+ anchorPoint?: AnchorPoint | null
54
51
  }>(), {
55
52
  thin: true,
56
53
  flat: true,
@@ -60,26 +57,24 @@ const props = withDefaults(defineProps<{
60
57
  autoHide: true,
61
58
  triggers: () => ['click'],
62
59
  popperTriggers: () => [],
63
- popperShowTriggers: () => [],
64
- popperHideTriggers: () => [],
65
60
  })
66
61
 
67
- const emit = defineEmits<{ show: []; hide: [] }>()
62
+ const emit = defineEmits<{
63
+ show: []
64
+ hide: []
65
+ select: [value: string | number | DropdownItem, item: DropdownItem]
66
+ }>()
68
67
 
68
+ const slots = useSlots()
69
69
  const shown = defineModel('shown', { type: Boolean, default: false })
70
70
  const hasOpened = ref(shown.value)
71
71
 
72
72
  const triggerWrapRef = ref<HTMLElement | null>(null)
73
73
  const popoverRef = ref<HTMLElement | null>(null)
74
+ const runtimeAnchorPoint = ref<AnchorPoint | null>(null)
74
75
 
75
- // Tracks whether the popover was opened via keyboard (Enter/Space/Arrow) so we know
76
- // whether to move focus into the menu on open.
77
76
  let openedViaKeyboard = false
78
77
 
79
- // Unique id for this popover instance, used to track parent–child relationships
80
- // across teleported popovers. `useId()` is SSR-stable and collision-free — the
81
- // old module-level counter reset per SSR render, producing duplicate ids like
82
- // `bgl-dd-1` on multiple dropdowns (an a11y failure: duplicate ARIA IDs).
83
78
  const bglId = `bgl-dd-${useId()}`
84
79
  const POPOVER_ROOT = '#bagelink-popover-root'
85
80
  const teleportTarget = ref<string | Element>(POPOVER_ROOT)
@@ -89,6 +84,11 @@ const { isMobile } = useDevice()
89
84
  const shouldDisablePositioning = computed(() => props.disablePlacement && isMobile.value)
90
85
  const isSmallScreen = isMobile
91
86
 
87
+ const resolvedItems = computed(() => props.items ?? props.options ?? [])
88
+ const effectiveAnchorPoint = computed(() => props.anchorPoint ?? runtimeAnchorPoint.value)
89
+ const hasExternalAnchor = computed(() => Boolean(props.referenceEl || effectiveAnchorPoint.value))
90
+ const shouldRenderTrigger = computed(() => !hasExternalAnchor.value || Boolean(slots.trigger))
91
+
92
92
  const effectiveTriggers = computed(() =>
93
93
  isSmallScreen.value
94
94
  ? props.triggers.map(t => (t === 'hover' ? 'click' : t))
@@ -121,8 +121,38 @@ const resolvedDelay = computed((): { show: number; hide: number } => {
121
121
  // ShowGroup registry — shared across all Dropdown instances on the page
122
122
  const groupRegistry = new Map<string, Set<() => void>>()
123
123
 
124
+ function toVirtualElement(pt: AnchorPoint): VirtualElement {
125
+ const width = pt.width ?? 0
126
+ const height = pt.height ?? 0
127
+ return {
128
+ getBoundingClientRect() {
129
+ return {
130
+ width,
131
+ height,
132
+ x: pt.x,
133
+ y: pt.y,
134
+ top: pt.y,
135
+ left: pt.x,
136
+ right: pt.x + width,
137
+ bottom: pt.y + height,
138
+ toJSON() { return this },
139
+ } as DOMRect
140
+ },
141
+ }
142
+ }
143
+
144
+ function resolveAnchorElement(): Element | VirtualElement | null {
145
+ if (effectiveAnchorPoint.value) {
146
+ return toVirtualElement(effectiveAnchorPoint.value)
147
+ }
148
+ if (props.referenceEl) {
149
+ return props.referenceEl
150
+ }
151
+ return (triggerWrapRef.value?.firstElementChild ?? triggerWrapRef.value) as Element | null
152
+ }
153
+
124
154
  async function updatePosition() {
125
- const triggerEl = (props.referenceEl ?? triggerWrapRef.value?.firstElementChild) as HTMLElement | null
155
+ const triggerEl = resolveAnchorElement()
126
156
  if (!popoverRef.value || !triggerEl || shouldDisablePositioning.value) return
127
157
 
128
158
  const isAuto = props.placement === 'auto'
@@ -147,33 +177,33 @@ function isOpen(): boolean {
147
177
  }
148
178
 
149
179
  async function internalShow() {
150
- if (props.disabled || isOpen()) return
180
+ if (props.disabled) return
181
+ if (isOpen()) {
182
+ await updatePosition()
183
+ return
184
+ }
151
185
 
152
186
  hasOpened.value = true
153
- teleportTarget.value = triggerWrapRef.value?.closest('dialog[open]') ?? POPOVER_ROOT
187
+ const hostEl = props.referenceEl instanceof Element ? props.referenceEl : triggerWrapRef.value
188
+ teleportTarget.value = hostEl?.closest('dialog[open]') ?? POPOVER_ROOT
154
189
 
155
190
  if (props.showGroup && groupRegistry.has(props.showGroup)) {
156
191
  groupRegistry.get(props.showGroup)!.forEach(fn => fn())
157
192
  }
158
193
 
159
- // Wait for Teleport to move the element to the new target before promoting to top layer
160
194
  await nextTick()
161
195
  if (!popoverRef.value) return
162
196
 
163
- // Tag with our own id and, if the trigger lives inside another open popover, tag with that owner's id.
164
- // This lets ancestor Dropdowns skip closing when a click lands inside our (teleported) popover.
165
197
  popoverRef.value.dataset.bglId = bglId
166
- const ownerPopover = triggerWrapRef.value?.closest<HTMLElement>('[data-bgl-id]')
198
+ const ownerPopover = hostEl?.closest<HTMLElement>('[data-bgl-id]')
167
199
  if (ownerPopover?.dataset.bglId) popoverRef.value.dataset.bglOwner = ownerPopover.dataset.bglId
168
200
  else delete popoverRef.value.dataset.bglOwner
169
201
 
170
- // Show hidden first so floating-ui can measure it, then position, then reveal
171
202
  popoverRef.value.style.opacity = '0'
172
203
  popoverRef.value.showPopover()
173
204
  await updatePosition()
174
205
  popoverRef.value.style.opacity = ''
175
206
 
176
- // For keyboard-driven opens, move focus into the menu so arrow keys work immediately.
177
207
  if (openedViaKeyboard) {
178
208
  focusFirstItem()
179
209
  openedViaKeyboard = false
@@ -182,9 +212,6 @@ async function internalShow() {
182
212
 
183
213
  function internalHide(opts?: { restoreFocus?: boolean }) {
184
214
  if (!isOpen()) return
185
- // Only restore focus to the trigger for keyboard-driven closes (Esc/click).
186
- // Hover-driven closes must NOT steal focus back, otherwise the trigger gets a
187
- // stray :focus-visible ring after the pointer has already moved elsewhere.
188
215
  const restoreFocus = opts?.restoreFocus ?? true
189
216
  if (restoreFocus && !props.noAutoFocus) {
190
217
  const focusable = triggerWrapRef.value?.querySelector<HTMLElement>(
@@ -195,6 +222,24 @@ function internalHide(opts?: { restoreFocus?: boolean }) {
195
222
  popoverRef.value?.hidePopover()
196
223
  }
197
224
 
225
+ function openAt(target: AnchorPoint | MouseEvent) {
226
+ if ('clientX' in target && 'clientY' in target) {
227
+ runtimeAnchorPoint.value = { x: target.clientX, y: target.clientY, width: 0, height: 0 }
228
+ } else {
229
+ runtimeAnchorPoint.value = { ...target }
230
+ }
231
+ internalShow()
232
+ }
233
+
234
+ function handleItemClick(item: DropdownItem) {
235
+ if (item.disabled) return
236
+ item.action?.()
237
+ emit('select', item.value ?? item.label ?? item, item)
238
+ if (props.autoHide) {
239
+ internalHide()
240
+ }
241
+ }
242
+
198
243
  // --- Keyboard accessibility helpers -------------------------------------------------
199
244
 
200
245
  const FOCUSABLE_SELECTOR = [
@@ -219,13 +264,11 @@ function focusItemAt(index: number) {
219
264
  items[wrapped]?.focus()
220
265
  }
221
266
 
222
- // Move focus into the menu (first focusable item) — used for keyboard-driven opens.
223
267
  function focusFirstItem() {
224
268
  if (props.noAutoFocus) return
225
269
  focusItemAt(0)
226
270
  }
227
271
 
228
- // Arrow-key navigation while the popover is open.
229
272
  function onPopoverKeydown(e: KeyboardEvent) {
230
273
  const items = getFocusableItems()
231
274
  if (items.length === 0) return
@@ -260,6 +303,20 @@ watch(shown, (val) => {
260
303
  else if (!val && isOpen()) internalHide()
261
304
  })
262
305
 
306
+ // Reposition live when anchorPoint or referenceEl changes while open
307
+ watch(
308
+ () => [
309
+ effectiveAnchorPoint.value?.x,
310
+ effectiveAnchorPoint.value?.y,
311
+ effectiveAnchorPoint.value?.width,
312
+ effectiveAnchorPoint.value?.height,
313
+ props.referenceEl,
314
+ ],
315
+ () => {
316
+ if (isOpen()) updatePosition()
317
+ }
318
+ )
319
+
263
320
  // Sync popover state → shown model
264
321
  function onPopoverToggle(e: Event) {
265
322
  const te = e as ToggleEvent
@@ -280,19 +337,13 @@ function onDocumentPointerDown(e: PointerEvent) {
280
337
  if (!props.autoHide || !isOpen()) return
281
338
  const target = e.target as Node
282
339
  if (popoverRef.value?.contains(target) || triggerWrapRef.value?.contains(target)) return
283
- // External anchor (referenceEl) counts as the trigger — don't close when
284
- // the user interacts with it (e.g. re-clicking the chip that opened us).
285
340
  if (props.referenceEl instanceof Node && props.referenceEl.contains(target)) return
286
- // Don't close if the click landed inside a descendant popover (e.g. a SelectInput inside this
287
- // dropdown that teleports its popup to <body>). We detect this via a data-bgl-owner attribute
288
- // that each Dropdown sets on its popover element to point to the nearest ancestor popover's id.
289
341
  const openPopovers = document.querySelectorAll<HTMLElement>('[popover]:popover-open')
290
342
  const myId = popoverRef.value?.dataset.bglId
291
343
  for (const p of Array.from(openPopovers)) {
292
344
  if (p === popoverRef.value) continue
293
345
  if (p.contains(target) && myId && p.dataset.bglOwner === myId) return
294
346
  }
295
- // Pointer landed elsewhere — let the click move focus naturally, don't yank it back to the trigger.
296
347
  internalHide({ restoreFocus: false })
297
348
  }
298
349
 
@@ -304,11 +355,8 @@ let showTimer: ReturnType<typeof setTimeout> | undefined
304
355
  let hideTimer: ReturnType<typeof setTimeout> | undefined
305
356
  function clearTimers() { clearTimeout(showTimer); clearTimeout(hideTimer) }
306
357
 
307
- // Guard against double toggles within the same tick. The <Btn> trigger handles keydown.enter/space
308
- // itself AND the browser also fires a native click for Enter/Space on a <button>, which would
309
- // otherwise toggle the popover twice (open → immediately close, the "opens for a second" bug).
310
358
  let lastToggleAt = 0
311
- function toggle(viaKeyboard: boolean) {
359
+ function toggle(viaKeyboard = false) {
312
360
  const now = Date.now()
313
361
  if (now - lastToggleAt < 50) return
314
362
  lastToggleAt = now
@@ -318,12 +366,9 @@ function toggle(viaKeyboard: boolean) {
318
366
 
319
367
  function onTriggerClick(e?: MouseEvent) {
320
368
  if (!isClickTrigger.value) return
321
- // A click via keyboard (Enter/Space) reports detail === 0; a real pointer click reports >= 1.
322
369
  toggle(!!e && e.detail === 0)
323
370
  }
324
371
 
325
- // Keyboard activation on the trigger. Works for ALL trigger types (including hover-only) so the
326
- // dropdown is reachable by keyboard users — WCAG requires hover-revealed content to also open on focus.
327
372
  function onTriggerKeydown(e: KeyboardEvent) {
328
373
  const target = e.target as HTMLElement | null
329
374
  if (
@@ -345,8 +390,6 @@ function onTriggerKeydown(e: KeyboardEvent) {
345
390
  case 'ArrowDown':
346
391
  e.preventDefault()
347
392
  openedViaKeyboard = true
348
- // Arrow keys are a deliberate request to navigate, so move focus into the menu
349
- // even when noAutoFocus is set (noAutoFocus only suppresses focus-on-open).
350
393
  if (isOpen()) focusItemAt(0)
351
394
  else internalShow()
352
395
  break
@@ -401,7 +444,6 @@ onMounted(() => {
401
444
  if (!groupRegistry.has(props.showGroup)) groupRegistry.set(props.showGroup, new Set())
402
445
  groupRegistry.get(props.showGroup)!.add(internalHide)
403
446
  }
404
- // Honour an already-true v-model:shown on mount (watch is not immediate).
405
447
  if (shown.value) internalShow()
406
448
  })
407
449
 
@@ -412,14 +454,14 @@ onBeforeUnmount(() => {
412
454
  if (isOpen()) popoverRef.value?.hidePopover()
413
455
  })
414
456
 
415
- defineExpose({ show, hide, shown })
457
+ defineExpose({ show, hide, toggle, openAt, updatePosition, shown })
416
458
  </script>
417
459
 
418
460
  <template>
419
461
  <div style="display: contents">
420
- <!-- Trigger wrapper — omitted when referenceEl is provided externally -->
462
+ <!-- Trigger wrapper — omitted when anchored externally without a #trigger slot -->
421
463
  <div
422
- v-if="!referenceEl"
464
+ v-if="shouldRenderTrigger"
423
465
  ref="triggerWrapRef" v-bind="$attrs"
424
466
  @mouseenter="onTriggerMouseenter" @mouseleave="onTriggerMouseleave" @focus="onTriggerMouseenter"
425
467
  @blur="onTriggerMouseleave" @keydown="onTriggerKeydown"
@@ -456,7 +498,31 @@ defineExpose({ show, hide, shown })
456
498
  'bgl-dropdown__content--no-padding': props.noPadding,
457
499
  }"
458
500
  style="border-radius: calc(var(--bgl-card-border-radius) * 0.7);">
459
- <slot :hide :show :shown />
501
+ <template v-if="resolvedItems.length > 0">
502
+ <div class="bgl-dropdown__items flex flex-column gap-0125">
503
+ <template v-for="(item, idx) in resolvedItems" :key="item.value ?? item.label ?? idx">
504
+ <Divider v-if="item.divider" class="my-025" />
505
+ <div v-else-if="item.header" class="bgl-dropdown__header px-05 py-025 txt11 semi opacity-5 uppercase">
506
+ {{ item.header }}
507
+ </div>
508
+ <Btn
509
+ v-else
510
+ thin
511
+ flat
512
+ full-width
513
+ align-txt="start"
514
+ role="menuitem"
515
+ :icon="item.icon"
516
+ :value="item.label"
517
+ :color="item.danger ? 'red' : item.color"
518
+ :disabled="item.disabled"
519
+ :class="[item.class, { active: item.active, 'bgl-dropdown__item--danger': item.danger }]"
520
+ @click="handleItemClick(item)"
521
+ />
522
+ </template>
523
+ </div>
524
+ </template>
525
+ <slot :hide :show :shown :open-at="openAt" />
460
526
  </div>
461
527
  </div>
462
528
  </Teleport>
@@ -533,15 +599,10 @@ animation: bgl-dropdown-enter 0.18s ease 0.04s both;
533
599
  transform-origin: top left;
534
600
  }
535
601
 
536
- /* Default menu breathing room so items don't touch the rounded top/bottom edges.
537
- Applies to the card content itself so plain menu content (e.g. a list of Btns)
538
- gets the same padding as content wrapped in a .bgl_card — no consumer py-* needed. */
539
602
  .bgl-dropdown__content--card {
540
603
  padding-block: 0.5rem;
541
604
  padding-inline: 0.375rem;
542
605
  }
543
- /* A direct Card is the popup's main surface, so neither wrapper nor Card gets
544
- the default menu padding. */
545
606
  .bgl-dropdown__content--card:has(> .bgl_card) {
546
607
  padding: 0;
547
608
  }
@@ -554,6 +615,10 @@ padding: 0;
554
615
  padding: 0;
555
616
  }
556
617
 
618
+ .bgl-dropdown__items {
619
+ min-width: 150px;
620
+ }
621
+
557
622
  </style>
558
623
  <!-- prettier-ignore-end -->
559
624
  <!-- eslint-enable -->
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { FilterField, QueryOption, OptionsSource } from './FilterQuery.types'
3
- import type { AlignedPlacement } from './Dropdown.vue'
3
+ import type { AlignedPlacement } from './Dropdown.types'
4
4
  export type { FilterField, QueryOption, OptionsSource }
5
5
  </script>
6
6
 
@@ -2,7 +2,7 @@
2
2
  import type { IconType } from '../../index'
3
3
  import { useDevice } from '../../index'
4
4
  import { computed, ref, onMounted, watchEffect } from 'vue'
5
- import { FONT_AWESOME_ICONS, MATERIAL_ICONS, FONT_AWESOME_BRANDS_ICONS } from './constants'
5
+ import { MATERIAL_ICONS, FONT_AWESOME_BRANDS_ICONS } from './constants'
6
6
 
7
7
  type AnimateType = 'spin' | 'pulse' | 'bounce' | 'fade-in' | 'slide-up' | 'rotate-in'
8
8
  type TransitionType = 'slide-up' | 'rotate' | 'scale' | 'fade'
@@ -21,7 +21,6 @@ const props = withDefaults(defineProps<{
21
21
  Uses `color`, and defaults to black when no color is provided. */
22
22
  soft?: boolean
23
23
  weight?: number | string
24
- fontAwesome?: boolean
25
24
  fill?: boolean
26
25
  /** Entrance / looping effect. Pure CSS, opt-in. e.g. `animate="spin"`. */
27
26
  animate?: AnimateType
@@ -50,17 +49,15 @@ const computedSize = computed(() => {
50
49
  })
51
50
 
52
51
  const iconRenderType = computed(() => {
53
- if (props.fontAwesome && (FONT_AWESOME_ICONS.includes(iconRender.value) || FONT_AWESOME_BRANDS_ICONS.includes(iconRender.value))) { return 'font-awesome' }
52
+ // Only brand glyphs use Font Awesome. Regular/solid icons are intentionally
53
+ // unsupported; their names fall back to Material Symbols.
54
+ if (FONT_AWESOME_BRANDS_ICONS.includes(iconRender.value)) { return 'font-awesome-brand' }
54
55
  if (MATERIAL_ICONS.includes(iconRender.value)) { return 'material' }
55
- if (FONT_AWESOME_ICONS.includes(iconRender.value) || FONT_AWESOME_BRANDS_ICONS.includes(iconRender.value)) { return 'font-awesome' }
56
56
  return 'material'
57
57
  })
58
58
 
59
- const isFaBrand = computed(() => FONT_AWESOME_BRANDS_ICONS.includes(iconRender.value))
60
-
61
59
  // Hide until required font is ready to avoid ligature text/fallback flash
62
60
  const isMaterialReady = ref(true)
63
- const isFAFreeReady = ref(true)
64
61
  const isFABrandsReady = ref(true)
65
62
 
66
63
  function getFontFaceSet(): FontFaceSet | null {
@@ -92,11 +89,6 @@ async function loadMaterialIfNeeded() {
92
89
  isMaterialReady.value = ok
93
90
  }
94
91
 
95
- async function loadFAFreeIfNeeded() {
96
- const ok = await ensureFontLoaded('Font Awesome 6 Free')
97
- isFAFreeReady.value = ok
98
- }
99
-
100
92
  async function loadFABrandsIfNeeded() {
101
93
  const ok = await ensureFontLoaded('Font Awesome 6 Brands')
102
94
  isFABrandsReady.value = ok
@@ -108,14 +100,10 @@ onMounted(() => {
108
100
  const fonts = getFontFaceSet()
109
101
  isMaterialReady.value = fonts ? fonts.check('1em "Material Symbols Outlined"') : true
110
102
  void loadMaterialIfNeeded()
111
- } else if (isFaBrand.value) {
103
+ } else {
112
104
  const fonts = getFontFaceSet()
113
105
  isFABrandsReady.value = fonts ? fonts.check('1em "Font Awesome 6 Brands"') : true
114
106
  void loadFABrandsIfNeeded()
115
- } else {
116
- const fonts = getFontFaceSet()
117
- isFAFreeReady.value = fonts ? fonts.check('1em "Font Awesome 6 Free"') : true
118
- void loadFAFreeIfNeeded()
119
107
  }
120
108
  })
121
109
 
@@ -123,16 +111,14 @@ watchEffect(() => {
123
111
  if (!supportsFontLoading()) { return }
124
112
  if (iconRenderType.value === 'material') {
125
113
  void loadMaterialIfNeeded()
126
- } else if (isFaBrand.value) {
127
- void loadFABrandsIfNeeded()
128
114
  } else {
129
- void loadFAFreeIfNeeded()
115
+ void loadFABrandsIfNeeded()
130
116
  }
131
117
  })
132
118
 
133
119
  const isCurrentFontReady = computed(() => {
134
120
  if (iconRenderType.value === 'material') { return isMaterialReady.value }
135
- return isFaBrand.value ? isFABrandsReady.value : isFAFreeReady.value
121
+ return isFABrandsReady.value
136
122
  })
137
123
 
138
124
  // Entrance / loop animation class + timing.
@@ -213,16 +199,13 @@ const iconStyle = computed(() => {
213
199
  {{ iconRender }}
214
200
  </span>
215
201
  <span
216
- v-else-if="iconRenderType === 'font-awesome'" :key="`font-awesome-${iconRender}`" class="fa bgl_icon-font notranslate" :class="[
202
+ v-else-if="iconRenderType === 'font-awesome-brand'" :key="`font-awesome-brand-${iconRender}`" class="fa fa-brands bgl_icon-font notranslate" :class="[
217
203
  `fa-${iconRender}`,
218
204
  animateClass,
219
205
  roundClass,
220
206
  roundPairClass,
221
207
  softClass,
222
208
  {
223
- 'fa-brands': isFaBrand,
224
- 'fa-solid': fill,
225
- 'far': !fill && !isFaBrand,
226
209
  'color-white keep-white': round && !soft && isRawCssColor,
227
210
  },
228
211
  ]" :style="iconStyle" aria-hidden="true"
@@ -239,7 +222,6 @@ const iconStyle = computed(() => {
239
222
  * RANGES shrinks the served font dramatically (the full ~3.8MB font is the
240
223
  * single biggest asset on the page) while keeping weight/fill working. */
241
224
  @import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,100..700,0..1,0');
242
- @import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css');
243
225
 
244
226
  [dir='rtl'] .data-row .bgl_icon-font,
245
227
  [dir='rtl'] .embedded-field .bgl_icon-font,
@@ -264,10 +246,6 @@ const iconStyle = computed(() => {
264
246
  box-sizing: border-box;
265
247
  }
266
248
 
267
- .bgl_icon-font.fa {
268
- font-family: 'Font Awesome 6 Free', serif !important;
269
- }
270
-
271
249
  .bgl_icon-font.fa-brands {
272
250
  font-family: 'Font Awesome 6 Brands', serif !important;
273
251
  }
@@ -1,169 +1,3 @@
1
- export const FONT_AWESOME_ICONS = [
2
- 'trash-can',
3
- 'message',
4
- 'file-lines',
5
- 'calendar-days',
6
- 'hand-point-right',
7
- 'face-smile-beam',
8
- 'face-grin-stars',
9
- 'address-book',
10
- 'comments',
11
- 'paste',
12
- 'face-grin-tongue-squint',
13
- 'face-flushed',
14
- 'square-caret-right',
15
- 'square-minus',
16
- 'compass',
17
- 'square-caret-down',
18
- 'face-kiss-beam',
19
- 'lightbulb',
20
- 'flag',
21
- 'square-check',
22
- 'circle-dot',
23
- 'face-dizzy',
24
- 'futbol',
25
- 'pen-to-square',
26
- 'hourglass-half',
27
- 'eye-slash',
28
- 'hand',
29
- 'hand-spock',
30
- 'face-kiss',
31
- 'face-grin-tongue',
32
- 'chess-bishop',
33
- 'face-grin-wink',
34
- 'face-grin-wide',
35
- 'face-frown-open',
36
- 'hand-point-up',
37
- 'bookmark',
38
- 'hand-point-down',
39
- 'folder',
40
- 'user',
41
- 'square-caret-left',
42
- 'star',
43
- 'chess-knight',
44
- 'face-laugh-squint',
45
- 'face-laugh',
46
- 'folder-open',
47
- 'clipboard',
48
- 'chess-queen',
49
- 'hand-back-fist',
50
- 'square-caret-up',
51
- 'chart-bar',
52
- 'window-restore',
53
- 'square-plus',
54
- 'image',
55
- 'folder-closed',
56
- 'lemon',
57
- 'handshake',
58
- 'gem',
59
- 'circle-play',
60
- 'circle-check',
61
- 'circle-stop',
62
- 'id-badge',
63
- 'face-laugh-beam',
64
- 'registered',
65
- 'address-card',
66
- 'face-tired',
67
- 'font-awesome',
68
- 'face-smile-wink',
69
- 'file-word',
70
- 'file-powerpoint',
71
- 'envelope-open',
72
- 'file-zipper',
73
- 'square',
74
- 'snowflake',
75
- 'newspaper',
76
- 'face-kiss-wink-heart',
77
- 'star-half-stroke',
78
- 'file-excel',
79
- 'face-grin-beam',
80
- 'object-ungroup',
81
- 'circle-right',
82
- 'face-rolling-eyes',
83
- 'object-group',
84
- 'heart',
85
- 'face-surprise',
86
- 'circle-pause',
87
- 'circle',
88
- 'circle-up',
89
- 'file-audio',
90
- 'file-image',
91
- 'circle-question',
92
- 'face-meh-blank',
93
- 'eye',
94
- 'face-sad-cry',
95
- 'file-code',
96
- 'window-maximize',
97
- 'face-frown',
98
- 'floppy-disk',
99
- 'comment-dots',
100
- 'face-grin-squint',
101
- 'hand-pointer',
102
- 'hand-scissors',
103
- 'face-grin-tears',
104
- 'calendar-xmark',
105
- 'file-video',
106
- 'file-pdf',
107
- 'comment',
108
- 'envelope',
109
- 'hourglass',
110
- 'calendar-check',
111
- 'hard-drive',
112
- 'face-grin-squint-tears',
113
- 'rectangle-list',
114
- 'calendar-plus',
115
- 'circle-left',
116
- 'money-bill-1',
117
- 'clock',
118
- 'keyboard',
119
- 'closed-captioning',
120
- 'images',
121
- 'face-grin',
122
- 'face-meh',
123
- 'id-card',
124
- 'sun',
125
- 'face-laugh-wink',
126
- 'circle-down',
127
- 'thumbs-down',
128
- 'chess-pawn',
129
- 'credit-card',
130
- 'bell',
131
- 'file',
132
- 'hospital',
133
- 'chess-rook',
134
- 'star-half',
135
- 'chess-king',
136
- 'circle-user',
137
- 'copy',
138
- 'share-from-square',
139
- 'copyright',
140
- 'map',
141
- 'bell-slash',
142
- 'hand-lizard',
143
- 'face-smile',
144
- 'hand-peace',
145
- 'face-grin-hearts',
146
- 'building',
147
- 'face-grin-beam-sweat',
148
- 'moon',
149
- 'calendar',
150
- 'face-grin-tongue-wink',
151
- 'clone',
152
- 'face-angry',
153
- 'rectangle-xmark',
154
- 'paper-plane',
155
- 'life-ring',
156
- 'face-grimace',
157
- 'calendar-minus',
158
- 'circle-xmark',
159
- 'thumbs-up',
160
- 'window-minimize',
161
- 'square-full',
162
- 'note-sticky',
163
- 'face-sad-tear',
164
- 'hand-point-left'
165
- ]
166
-
167
1
  export const FONT_AWESOME_BRANDS_ICONS = [
168
2
  'monero',
169
3
  'hooli',