@invopop/popui 0.1.35 → 0.1.41

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 (119) hide show
  1. package/dist/BaseButton.svelte +4 -0
  2. package/dist/BaseDropdown.svelte +42 -3
  3. package/dist/BaseDropdown.svelte.d.ts +1 -0
  4. package/dist/BaseTableHeaderOrderBy.svelte +35 -12
  5. package/dist/ButtonSearch.svelte +82 -0
  6. package/dist/ButtonSearch.svelte.d.ts +4 -0
  7. package/dist/ButtonUuidCopy.svelte +1 -0
  8. package/dist/DatePicker.svelte +96 -27
  9. package/dist/DatePicker.svelte.d.ts +5 -1
  10. package/dist/DrawerContext.svelte +443 -34
  11. package/dist/DrawerContextItem.svelte +36 -29
  12. package/dist/DropdownSelect.svelte +68 -18
  13. package/dist/DropdownSelect.svelte.d.ts +4 -1
  14. package/dist/DropdownSelectGroup.svelte +15 -0
  15. package/dist/DropdownSelectGroup.svelte.d.ts +7 -0
  16. package/dist/EmptyState.svelte +6 -2
  17. package/dist/InputSearch.svelte +45 -5
  18. package/dist/InputSelect.svelte +12 -3
  19. package/dist/InputText.svelte +25 -8
  20. package/dist/InputToggle.svelte +23 -6
  21. package/dist/StepIcon.svelte +35 -0
  22. package/dist/StepIcon.svelte.d.ts +4 -0
  23. package/dist/StepIconList.svelte +24 -31
  24. package/dist/TagStatus.svelte +1 -1
  25. package/dist/button/button.svelte +34 -3
  26. package/dist/button/button.svelte.d.ts +29 -0
  27. package/dist/clickOutside.d.ts +5 -2
  28. package/dist/clickOutside.js +9 -3
  29. package/dist/data-table/cells/boolean-cell.svelte +29 -0
  30. package/dist/data-table/cells/boolean-cell.svelte.d.ts +8 -0
  31. package/dist/data-table/cells/cell-skeleton.svelte +35 -0
  32. package/dist/data-table/cells/cell-skeleton.svelte.d.ts +4 -0
  33. package/dist/data-table/cells/currency-cell.svelte +10 -0
  34. package/dist/data-table/cells/currency-cell.svelte.d.ts +8 -0
  35. package/dist/data-table/cells/date-cell.svelte +10 -0
  36. package/dist/data-table/cells/date-cell.svelte.d.ts +8 -0
  37. package/dist/data-table/cells/tag-cell.svelte +12 -0
  38. package/dist/data-table/cells/tag-cell.svelte.d.ts +8 -0
  39. package/dist/data-table/cells/text-cell.svelte +10 -0
  40. package/dist/data-table/cells/text-cell.svelte.d.ts +8 -0
  41. package/dist/data-table/cells/uuid-cell.svelte +17 -0
  42. package/dist/data-table/cells/uuid-cell.svelte.d.ts +8 -0
  43. package/dist/data-table/column-definitions.d.ts +12 -0
  44. package/dist/data-table/column-definitions.js +42 -0
  45. package/dist/data-table/column-sizing-helpers.d.ts +6 -0
  46. package/dist/data-table/column-sizing-helpers.js +24 -0
  47. package/dist/data-table/create-columns.d.ts +3 -0
  48. package/dist/data-table/create-columns.js +67 -0
  49. package/dist/data-table/data-table-cell.svelte +94 -0
  50. package/dist/data-table/data-table-cell.svelte.d.ts +25 -0
  51. package/dist/data-table/data-table-header-cell.svelte +188 -0
  52. package/dist/data-table/data-table-header-cell.svelte.d.ts +25 -0
  53. package/dist/data-table/data-table-helpers.d.ts +10 -0
  54. package/dist/data-table/data-table-helpers.js +124 -0
  55. package/dist/data-table/data-table-pagination.svelte +221 -0
  56. package/dist/data-table/data-table-pagination.svelte.d.ts +4 -0
  57. package/dist/data-table/data-table-row.svelte +57 -0
  58. package/dist/data-table/data-table-row.svelte.d.ts +25 -0
  59. package/dist/data-table/data-table-svelte.svelte.d.ts +40 -0
  60. package/dist/data-table/data-table-svelte.svelte.js +115 -0
  61. package/dist/data-table/data-table-toolbar.svelte +19 -0
  62. package/dist/data-table/data-table-toolbar.svelte.d.ts +32 -0
  63. package/dist/data-table/data-table-types.d.ts +196 -0
  64. package/dist/data-table/data-table-types.js +1 -0
  65. package/dist/data-table/data-table-view-options.svelte +126 -0
  66. package/dist/data-table/data-table-view-options.svelte.d.ts +29 -0
  67. package/dist/data-table/data-table.svelte +437 -0
  68. package/dist/data-table/data-table.svelte.d.ts +25 -0
  69. package/dist/data-table/flex-render.svelte +40 -0
  70. package/dist/data-table/flex-render.svelte.d.ts +33 -0
  71. package/dist/data-table/index.d.ts +13 -0
  72. package/dist/data-table/index.js +13 -0
  73. package/dist/data-table/render-helpers.d.ts +90 -0
  74. package/dist/data-table/render-helpers.js +99 -0
  75. package/dist/data-table/table-setup.d.ts +39 -0
  76. package/dist/data-table/table-setup.js +151 -0
  77. package/dist/data-table/table-styles.d.ts +17 -0
  78. package/dist/data-table/table-styles.js +70 -0
  79. package/dist/drawer-dnd-helpers.d.ts +30 -0
  80. package/dist/drawer-dnd-helpers.js +72 -0
  81. package/dist/helpers.d.ts +1 -0
  82. package/dist/helpers.js +3 -0
  83. package/dist/index.d.ts +15 -3
  84. package/dist/index.js +28 -5
  85. package/dist/skeleton/index.d.ts +5 -0
  86. package/dist/skeleton/index.js +7 -0
  87. package/dist/skeleton/skeleton-avatar.svelte +14 -0
  88. package/dist/skeleton/skeleton-avatar.svelte.d.ts +7 -0
  89. package/dist/skeleton/skeleton-card.svelte +22 -0
  90. package/dist/skeleton/skeleton-card.svelte.d.ts +9 -0
  91. package/dist/skeleton/skeleton-list.svelte +25 -0
  92. package/dist/skeleton/skeleton-list.svelte.d.ts +8 -0
  93. package/dist/skeleton/skeleton.svelte +17 -0
  94. package/dist/skeleton/skeleton.svelte.d.ts +5 -0
  95. package/dist/svg/IconDelivery.svelte +1 -1
  96. package/dist/svg/IconOrder.svelte +1 -1
  97. package/dist/svg/IconPayment.svelte +1 -1
  98. package/dist/table/table-cell.svelte +4 -2
  99. package/dist/table/table-head.svelte +4 -2
  100. package/dist/table/table-header.svelte +1 -1
  101. package/dist/table/table-row.svelte +4 -2
  102. package/dist/table/table.svelte +2 -2
  103. package/dist/tailwind.theme.css +30 -6
  104. package/dist/tooltip/index.d.ts +2 -1
  105. package/dist/tooltip/index.js +3 -2
  106. package/dist/tooltip/tooltip-auto-hide.svelte +31 -0
  107. package/dist/tooltip/tooltip-auto-hide.svelte.d.ts +7 -0
  108. package/dist/types.d.ts +51 -73
  109. package/package.json +14 -8
  110. package/dist/BaseTable.svelte +0 -391
  111. package/dist/BaseTable.svelte.d.ts +0 -4
  112. package/dist/BaseTableCellContent.svelte +0 -58
  113. package/dist/BaseTableCellContent.svelte.d.ts +0 -4
  114. package/dist/BaseTableCheckbox.svelte +0 -33
  115. package/dist/BaseTableCheckbox.svelte.d.ts +0 -4
  116. package/dist/BaseTableHeaderContent.svelte +0 -67
  117. package/dist/BaseTableHeaderContent.svelte.d.ts +0 -4
  118. package/dist/BaseTableRow.svelte +0 -127
  119. package/dist/BaseTableRow.svelte.d.ts +0 -4
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { DrawerContextProps, DrawerOption } from './types.ts'
2
+ import type { DrawerContextProps, DrawerOption, AnyProp } from './types.ts'
3
3
  import DrawerContextItem from './DrawerContextItem.svelte'
4
4
  import DrawerContextSeparator from './DrawerContextSeparator.svelte'
5
5
  import EmptyState from './EmptyState.svelte'
@@ -7,17 +7,46 @@
7
7
  import { Icon } from '@steeze-ui/svelte-icon'
8
8
  import { ChevronRight } from '@steeze-ui/heroicons'
9
9
  import { slide } from 'svelte/transition'
10
+ import { flip } from 'svelte/animate'
11
+ import {
12
+ draggable as makeDraggable,
13
+ dropTargetForElements
14
+ } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'
15
+ import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
16
+ import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'
17
+ import { reorder } from '@atlaskit/pragmatic-drag-and-drop/reorder'
18
+ import {
19
+ attachClosestEdge,
20
+ extractClosestEdge,
21
+ type Edge
22
+ } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'
23
+ import { onMount, onDestroy, untrack } from 'svelte'
24
+ import {
25
+ shouldShowDropIndicator,
26
+ reorderItems,
27
+ moveItemBetweenGroups,
28
+ type DropIndicatorState,
29
+ type DndItem as DndItemType
30
+ } from './drawer-dnd-helpers'
31
+
32
+ const flipDurationMs = 150
10
33
 
11
34
  let {
12
35
  items = $bindable([]),
13
36
  multiple = false,
37
+ draggable = false,
14
38
  widthClass = 'w-60',
39
+ collapsibleGroups = true,
15
40
  onclick,
16
41
  onselect,
42
+ onreorder,
43
+ ondropitem,
17
44
  children,
18
45
  groups
19
46
  }: DrawerContextProps = $props()
20
47
 
48
+ type DndItem = DrawerOption & { id: string }
49
+
21
50
  let selectedItems = $derived(items.filter((i) => i.selected))
22
51
  let hasGroups = $derived(groups && groups.length > 0)
23
52
  let { groupedItems, ungroupedItems } = $derived.by(() => {
@@ -42,7 +71,79 @@
42
71
  })
43
72
 
44
73
  let openGroups = $state<Record<string, boolean>>({})
74
+ let groupDndItems = $state<Record<string, DndItem[]>>({})
75
+ let ungroupedDndItems = $state<DndItem[]>([])
76
+ let mounted = $state(false)
77
+ let itemsCache = $state<DrawerOption[]>([])
78
+ let isDragging = $state(false)
79
+ let draggedItemId = $state<string | null>(null)
80
+ let emitTimeout: number | undefined
81
+ let draggedOverGroup = $state<string | null>(null)
82
+ let dropIndicator = $state<DropIndicatorState>(null)
83
+ let cleanupFunctions: (() => void)[] = []
84
+
85
+ // Build internal DND items from external items
86
+ function buildListIn() {
87
+ if (hasGroups) {
88
+ // Build DND items for each group
89
+ groups!.forEach((group) => {
90
+ const groupItems = groupedItems.get(group.slug) || []
91
+ groupDndItems[group.slug] = groupItems.map((item: DrawerOption, i: number) => ({
92
+ ...item,
93
+ id: `${group.slug}-${item.value}-${i}`
94
+ }))
95
+ })
96
+ }
97
+
98
+ // Build DND items for ungrouped
99
+ ungroupedDndItems = ungroupedItems.map((item, i) => ({
100
+ ...item,
101
+ id: `ungrouped-${item.value}-${i}`
102
+ }))
103
+ }
104
+
105
+ // Build external items from internal DND items
106
+ function buildListOut() {
107
+ const newItems: DrawerOption[] = []
108
+ const used = new Set<AnyProp>()
109
+
110
+ // Add all grouped items
111
+ if (hasGroups) {
112
+ groups!.forEach((group) => {
113
+ const dndItems = groupDndItems[group.slug] || []
114
+ dndItems.forEach((dndItem) => {
115
+ if (!used.has(dndItem.value)) {
116
+ const { id, ...item } = dndItem
117
+ newItems.push({ ...item, groupBy: group.slug })
118
+ used.add(dndItem.value)
119
+ }
120
+ })
121
+ })
122
+ }
123
+
124
+ // Add ungrouped items
125
+ ungroupedDndItems.forEach((dndItem) => {
126
+ if (!used.has(dndItem.value)) {
127
+ const { id, ...item } = dndItem
128
+ newItems.push(item)
129
+ used.add(dndItem.value)
130
+ }
131
+ })
132
+
133
+ return newItems
134
+ }
135
+
136
+ // Sync items when they change from outside
137
+ $effect(() => {
138
+ if (items && mounted && !isDragging) {
139
+ if (JSON.stringify(items) !== JSON.stringify(itemsCache)) {
140
+ buildListIn()
141
+ itemsCache = JSON.parse(JSON.stringify(items))
142
+ }
143
+ }
144
+ })
45
145
 
146
+ // Open group with selected item on mount
46
147
  $effect(() => {
47
148
  if (hasGroups) {
48
149
  const selectedItem = items.find((i) => i.selected)
@@ -52,10 +153,243 @@
52
153
  }
53
154
  })
54
155
 
156
+ // Notify parent of selection changes
55
157
  $effect(() => {
56
158
  onselect?.(selectedItems)
57
159
  })
58
160
 
161
+ onMount(() => {
162
+ itemsCache = JSON.parse(JSON.stringify(items))
163
+ buildListIn()
164
+ mounted = true
165
+
166
+ // Set up auto-scroll
167
+ const autoScrollCleanup = autoScrollForElements({
168
+ element: document.documentElement
169
+ })
170
+ cleanupFunctions.push(autoScrollCleanup)
171
+ })
172
+
173
+ onDestroy(() => {
174
+ cleanupFunctions.forEach((cleanup) => cleanup())
175
+ cleanupFunctions = []
176
+ })
177
+
178
+ function emitGroupDistribution() {
179
+ if (ondropitem && hasGroups) {
180
+ // Clear any pending emit
181
+ if (emitTimeout) {
182
+ clearTimeout(emitTimeout)
183
+ }
184
+
185
+ // Debounce the emit to avoid duplicate calls when dragging between groups
186
+ emitTimeout = window.setTimeout(() => {
187
+ const groupsDistribution: Record<string, DrawerOption[]> = {}
188
+ groups!.forEach((group) => {
189
+ const dndItems = groupDndItems[group.slug] || []
190
+ groupsDistribution[group.slug] = dndItems.map(({ id, ...item }) => item)
191
+ })
192
+ ondropitem(groupsDistribution)
193
+ }, 0)
194
+ }
195
+ }
196
+
197
+ // Setup draggable item (Svelte action)
198
+ function setupDraggableItem(element: HTMLElement, params: [DndItem, string]) {
199
+ const [dndItem, groupSlug] = params
200
+ if (!element || dndItem.locked) return
201
+
202
+ const cleanup = makeDraggable({
203
+ element,
204
+ getInitialData: () => ({ id: dndItem.id, groupSlug, type: 'drawer-item' }),
205
+ onDragStart: () => {
206
+ isDragging = true
207
+ draggedItemId = dndItem.id
208
+ dropIndicator = null
209
+ },
210
+ onDrop: () => {
211
+ isDragging = false
212
+ draggedItemId = null
213
+ dropIndicator = null
214
+ }
215
+ })
216
+
217
+ return {
218
+ destroy() {
219
+ cleanup()
220
+ }
221
+ }
222
+ }
223
+
224
+ // Setup drop zone for a container (group or ungrouped) (Svelte action)
225
+ function setupDropZone(element: HTMLElement, groupSlug: string) {
226
+ if (!element) return
227
+
228
+ const cleanup = dropTargetForElements({
229
+ element,
230
+ getData: () => {
231
+ const items = groupSlug === 'ungrouped' ? ungroupedDndItems : groupDndItems[groupSlug] || []
232
+ return { groupSlug, items: items.map((i) => i.id) }
233
+ },
234
+ canDrop: ({ source }) => source.data.type === 'drawer-item',
235
+ onDragEnter: () => {
236
+ draggedOverGroup = groupSlug
237
+ },
238
+ onDragLeave: () => {
239
+ if (draggedOverGroup === groupSlug) {
240
+ draggedOverGroup = null
241
+ }
242
+ },
243
+ onDrop: ({ source, location }) => {
244
+ draggedOverGroup = null
245
+ dropIndicator = null
246
+ const sourceId = source.data.id as string
247
+ const sourceGroup = source.data.groupSlug as string
248
+ const targetGroup = groupSlug
249
+
250
+ // Get source and target arrays
251
+ const sourceItems =
252
+ sourceGroup === 'ungrouped' ? ungroupedDndItems : groupDndItems[sourceGroup] || []
253
+ const targetItems =
254
+ targetGroup === 'ungrouped' ? ungroupedDndItems : groupDndItems[targetGroup] || []
255
+
256
+ // Find the dragged item
257
+ const sourceIndex = sourceItems.findIndex((item) => item.id === sourceId)
258
+ if (sourceIndex === -1) return
259
+
260
+ const draggedItem = sourceItems[sourceIndex]
261
+
262
+ // If moving within the same group, we need to handle reordering
263
+ if (sourceGroup === targetGroup) {
264
+ // Check if we're dropping on another item
265
+ const dropTargets = location.current.dropTargets
266
+ const itemDropTarget = dropTargets.find((target) => target.data.itemId)
267
+
268
+ if (itemDropTarget) {
269
+ const targetItemId = itemDropTarget.data.itemId as string
270
+ const edge = extractClosestEdge(itemDropTarget.data)
271
+
272
+ if (edge) {
273
+ const newItems = reorderItems(targetItems, sourceId, targetItemId, edge)
274
+
275
+ if (targetGroup === 'ungrouped') {
276
+ ungroupedDndItems = newItems
277
+ } else {
278
+ groupDndItems[targetGroup] = newItems
279
+ }
280
+ }
281
+ }
282
+ } else {
283
+ // Moving between groups
284
+ const dropTargets = location.current.dropTargets
285
+ const itemDropTarget = dropTargets.find((target) => target.data.itemId)
286
+
287
+ let targetItemId: string | undefined
288
+ let edge: Edge | undefined
289
+
290
+ if (itemDropTarget) {
291
+ targetItemId = itemDropTarget.data.itemId as string
292
+ edge = extractClosestEdge(itemDropTarget.data) || undefined
293
+ }
294
+
295
+ const { newSourceItems, newTargetItems } = moveItemBetweenGroups(
296
+ sourceItems,
297
+ targetItems,
298
+ sourceId,
299
+ targetItemId,
300
+ edge
301
+ )
302
+
303
+ if (sourceGroup === 'ungrouped') {
304
+ ungroupedDndItems = newSourceItems
305
+ } else {
306
+ groupDndItems[sourceGroup] = newSourceItems
307
+ }
308
+
309
+ if (targetGroup === 'ungrouped') {
310
+ ungroupedDndItems = newTargetItems
311
+ } else {
312
+ groupDndItems[targetGroup] = newTargetItems
313
+ }
314
+ }
315
+
316
+ // Update items and notify
317
+ const newItems = buildListOut()
318
+ items = newItems
319
+ itemsCache = JSON.parse(JSON.stringify(items))
320
+ onreorder?.(newItems)
321
+ emitGroupDistribution()
322
+ }
323
+ })
324
+
325
+ return {
326
+ destroy() {
327
+ cleanup()
328
+ }
329
+ }
330
+ }
331
+
332
+ // Setup drop zone for individual items (for reordering within same group) (Svelte action)
333
+ function setupItemDropZone(element: HTMLElement, params: [DndItem, string]) {
334
+ const [dndItem, groupSlug] = params
335
+ if (!element) return
336
+
337
+ const cleanup = dropTargetForElements({
338
+ element,
339
+ getData: ({ input }) => {
340
+ const data = { itemId: dndItem.id, groupSlug }
341
+ return attachClosestEdge(data, {
342
+ element,
343
+ input,
344
+ allowedEdges: ['top', 'bottom']
345
+ })
346
+ },
347
+ canDrop: ({ source }) => {
348
+ return source.data.type === 'drawer-item' && source.data.id !== dndItem.id
349
+ },
350
+ onDragEnter: ({ self, source }) => {
351
+ const edge = extractClosestEdge(self.data)
352
+ if (!edge) return
353
+
354
+ const sourceId = source.data.id as string
355
+ const sourceGroup = source.data.groupSlug as string
356
+ const items = groupSlug === 'ungrouped' ? ungroupedDndItems : groupDndItems[groupSlug] || []
357
+
358
+ if (shouldShowDropIndicator(sourceId, dndItem.id, sourceGroup, groupSlug, edge, items)) {
359
+ dropIndicator = { itemId: dndItem.id, edge }
360
+ }
361
+ },
362
+ onDrag: ({ self, source }) => {
363
+ const edge = extractClosestEdge(self.data)
364
+ if (!edge) return
365
+
366
+ const sourceId = source.data.id as string
367
+ const sourceGroup = source.data.groupSlug as string
368
+ const items = groupSlug === 'ungrouped' ? ungroupedDndItems : groupDndItems[groupSlug] || []
369
+
370
+ if (shouldShowDropIndicator(sourceId, dndItem.id, sourceGroup, groupSlug, edge, items)) {
371
+ dropIndicator = { itemId: dndItem.id, edge }
372
+ } else {
373
+ dropIndicator = null
374
+ }
375
+ },
376
+ onDragLeave: () => {
377
+ if (dropIndicator?.itemId === dndItem.id) {
378
+ dropIndicator = null
379
+ }
380
+ },
381
+ onDrop: () => {
382
+ dropIndicator = null
383
+ }
384
+ })
385
+
386
+ return {
387
+ destroy() {
388
+ cleanup()
389
+ }
390
+ }
391
+ }
392
+
59
393
  function updateItem(item: DrawerOption) {
60
394
  items = items.map((i) => {
61
395
  if (i.value === item.value) return item
@@ -72,7 +406,7 @@
72
406
  {#if item.separator}
73
407
  <DrawerContextSeparator />
74
408
  {:else}
75
- <div class:px-1={!item.groupBy}>
409
+ <div class:px-1={!item.groupBy} class:cursor-grab={draggable && !item.locked}>
76
410
  <DrawerContextItem {item} {multiple} {onclick} onchange={updateItem} />
77
411
  </div>
78
412
  {/if}
@@ -83,67 +417,142 @@
83
417
  >
84
418
  {@render children?.()}
85
419
 
86
- {#if hasGroups}
420
+ {#if hasGroups && groups}
87
421
  {#each groups as group, index}
88
422
  {@const groupItems = groupedItems.get(group.slug) || []}
89
423
  {@const isLastGroup = index === groups!.length - 1}
90
- {@const isOpen = openGroups[group.slug]}
91
- {@const hasOpenGroup = Object.values(openGroups).some((v) => v)}
424
+ {@const isOpen = collapsibleGroups ? openGroups[group.slug] : true}
425
+ {@const hasOpenGroup = collapsibleGroups ? Object.values(openGroups).some((v) => v) : true}
92
426
  <div
93
427
  class="px-1"
94
- class:flex-1={isOpen}
428
+ class:flex-1={isOpen && collapsibleGroups}
95
429
  class:flex={isOpen}
96
430
  class:flex-col={isOpen}
97
- class:min-h-0={isOpen}
98
- class:flex-shrink-0={!isOpen && hasOpenGroup}
431
+ class:min-h-0={isOpen && collapsibleGroups}
432
+ class:flex-shrink-0={!isOpen && hasOpenGroup && collapsibleGroups}
99
433
  >
100
- <button
101
- class="cursor-pointer flex items-center justify-between h-8 pl-2.5 pr-2.5 py-2.5 text-base font-medium text-foreground-default-secondary w-full hover:bg-background-default-secondary rounded-lg overflow-clip flex-shrink-0"
102
- onclick={() => toggleGroup(group.slug)}
103
- >
104
- <div class="flex items-center gap-1.5">
434
+ {#if collapsibleGroups}
435
+ <button
436
+ class="cursor-pointer flex items-center justify-between h-8 pl-2.5 pr-2.5 py-2.5 text-base font-medium text-foreground-default-secondary w-full hover:bg-background-default-secondary rounded-lg overflow-clip flex-shrink-0"
437
+ onclick={() => toggleGroup(group.slug)}
438
+ >
439
+ <div class="flex items-center gap-1.5">
440
+ <span>{group.label}</span>
441
+ <Icon
442
+ src={ChevronRight}
443
+ class="size-3 text-icon-default-secondary transition-all transform {isOpen
444
+ ? 'rotate-90'
445
+ : ''}"
446
+ />
447
+ </div>
448
+ {#if groupItems.length && !group.hideCounter}
449
+ <BaseCounter value={groupItems.length} />
450
+ {/if}
451
+ </button>
452
+ {:else}
453
+ <div
454
+ class="flex items-center justify-between h-8 pl-2.5 pr-2.5 py-2.5 text-base font-medium text-foreground-default-secondary w-full overflow-clip flex-shrink-0"
455
+ >
105
456
  <span>{group.label}</span>
106
- <Icon
107
- src={ChevronRight}
108
- class="size-3 text-icon-default-secondary transition-all transform {isOpen
109
- ? 'rotate-90'
110
- : ''}"
111
- />
457
+ {#if groupItems.length && !group.hideCounter}
458
+ <BaseCounter value={groupItems.length} />
459
+ {/if}
112
460
  </div>
113
- {#if groupItems.length}
114
- <BaseCounter value={groupItems.length} />
115
- {/if}
116
- </button>
461
+ {/if}
117
462
 
118
463
  {#if isOpen}
119
- <div class="w-full overflow-y-auto flex-1 min-h-0" transition:slide={{ duration: 200 }}>
120
- {#if !groupItems.length}
464
+ <div
465
+ class="w-full overflow-y-auto {collapsibleGroups ? 'flex-1 min-h-0' : ''}"
466
+ transition:slide={{ duration: collapsibleGroups ? 200 : 0 }}
467
+ >
468
+ {#if draggable}
469
+ <div use:setupDropZone={group.slug} class="min-h-[40px]">
470
+ {#if !groupItems.length}
471
+ <div class="px-1 pt-1 pb-5">
472
+ <EmptyState
473
+ iconSource={group.emptyIcon}
474
+ title={group.emptyTitle}
475
+ description={group.emptyDescription}
476
+ />
477
+ </div>
478
+ {:else}
479
+ {#each groupDndItems[group.slug] || [] as dndItem (dndItem.id)}
480
+ {@const showTopIndicator =
481
+ dropIndicator?.itemId === dndItem.id && dropIndicator?.edge === 'top'}
482
+ {@const showBottomIndicator =
483
+ dropIndicator?.itemId === dndItem.id && dropIndicator?.edge === 'bottom'}
484
+ {@const isBeingDragged = draggedItemId === dndItem.id}
485
+ <div
486
+ animate:flip={{ duration: isDragging ? flipDurationMs : 0 }}
487
+ use:setupDraggableItem={[dndItem, group.slug]}
488
+ use:setupItemDropZone={[dndItem, group.slug]}
489
+ class:border-t-2={showTopIndicator}
490
+ class:border-t-accent-50={showTopIndicator}
491
+ class:border-b-2={showBottomIndicator}
492
+ class:border-b-accent-50={showBottomIndicator}
493
+ class:opacity-40={isBeingDragged}
494
+ class="transition-opacity"
495
+ >
496
+ {@render drawerItem(dndItem)}
497
+ </div>
498
+ {/each}
499
+ {/if}
500
+ </div>
501
+ {:else if !groupItems.length}
121
502
  <div class="px-1 pt-1 pb-5">
122
503
  <EmptyState
123
504
  iconSource={group.emptyIcon}
124
- title={group.emptyTitle || 'No items here'}
125
- description={group.emptyDescription || 'Add items to get started'}
505
+ title={group.emptyTitle}
506
+ description={group.emptyDescription}
126
507
  />
127
508
  </div>
128
509
  {:else}
129
- {#each groupItems as item}
510
+ {#each groupItems as item (item.value)}
130
511
  {@render drawerItem(item)}
131
512
  {/each}
132
513
  {/if}
133
514
  </div>
134
515
  {/if}
135
516
  </div>
136
- {#if !isLastGroup}
517
+ {#if !isLastGroup && collapsibleGroups}
137
518
  <DrawerContextSeparator />
138
519
  {/if}
139
520
  {/each}
140
521
  {/if}
141
522
 
142
523
  {#if ungroupedItems.length}
143
- <div class="flex-shrink-0 overflow-y-auto max-h-[564px]">
144
- {#each ungroupedItems as item}
145
- {@render drawerItem(item)}
146
- {/each}
147
- </div>
524
+ {#if draggable}
525
+ <div
526
+ class="flex-shrink-0 overflow-y-auto max-h-[564px] min-h-[40px]"
527
+ use:setupDropZone={'ungrouped'}
528
+ >
529
+ {#each ungroupedDndItems as dndItem (dndItem.id)}
530
+ {@const showTopIndicator =
531
+ dropIndicator?.itemId === dndItem.id && dropIndicator?.edge === 'top'}
532
+ {@const showBottomIndicator =
533
+ dropIndicator?.itemId === dndItem.id && dropIndicator?.edge === 'bottom'}
534
+ {@const isBeingDragged = draggedItemId === dndItem.id}
535
+ <div
536
+ animate:flip={{ duration: isDragging ? flipDurationMs : 0 }}
537
+ use:setupDraggableItem={[dndItem, 'ungrouped']}
538
+ use:setupItemDropZone={[dndItem, 'ungrouped']}
539
+ class:border-t-2={showTopIndicator}
540
+ class:border-t-accent-50={showTopIndicator}
541
+ class:border-b-2={showBottomIndicator}
542
+ class:border-b-accent-50={showBottomIndicator}
543
+ class:opacity-40={isBeingDragged}
544
+ class="transition-opacity"
545
+ >
546
+ {@render drawerItem(dndItem)}
547
+ </div>
548
+ {/each}
549
+ </div>
550
+ {:else}
551
+ <div class="flex-shrink-0 overflow-y-auto max-h-[564px]">
552
+ {#each ungroupedItems as item (item.value)}
553
+ {@render drawerItem(item)}
554
+ {/each}
555
+ </div>
556
+ {/if}
148
557
  {/if}
149
558
  </div>
@@ -2,7 +2,7 @@
2
2
  import type { DrawerContextItemProps } from './types.ts'
3
3
  import InputCheckbox from './InputCheckbox.svelte'
4
4
  import { Icon } from '@steeze-ui/svelte-icon'
5
- import { onMount } from 'svelte'
5
+ import { onMount, onDestroy } from 'svelte'
6
6
  import { Success } from '@invopop/ui-icons'
7
7
  import clsx from 'clsx'
8
8
  import BaseFlag from './BaseFlag.svelte'
@@ -23,32 +23,33 @@
23
23
  let styles = $derived(
24
24
  clsx(
25
25
  'px-2 py-1.5 space-x-1.5',
26
- { 'bg-background-selected': item.selected && !multiple },
26
+ { 'bg-background-selected': item?.selected && !multiple },
27
27
  {
28
28
  'group-hover:bg-background-default-secondary':
29
- (!item.selected && !item.disabled) || multiple
29
+ (!item?.selected && !item?.disabled) || multiple
30
30
  }
31
31
  )
32
32
  )
33
+
33
34
  let labelStyles = $derived(
34
35
  clsx(
35
- { 'text-foreground-critical': item.destructive },
36
- { 'text-foreground': !item.destructive },
37
- { 'opacity-30': item.locked }
36
+ { 'text-foreground-critical': item?.destructive },
37
+ { 'text-foreground': !item?.destructive },
38
+ { 'opacity-30': item?.locked }
38
39
  )
39
40
  )
40
- let title = $derived(item.label.length > 25 ? item.label : undefined)
41
+ let title = $derived(item?.label && item.label.length > 25 ? item.label : undefined)
41
42
 
42
43
  onMount(() => {
43
- if (!scrollIfSelected) return
44
-
45
- if (item.selected) {
44
+ if (scrollIfSelected && item?.selected) {
46
45
  el?.scrollIntoView()
47
46
  }
48
47
  })
49
48
 
50
49
  function handleClick(event: MouseEvent) {
51
50
  event.stopPropagation()
51
+ if (!item) return
52
+
52
53
  if (multiple) {
53
54
  item.selected = !item.selected
54
55
  onchange?.(item)
@@ -61,49 +62,55 @@
61
62
  <button
62
63
  bind:this={el}
63
64
  class="cursor-pointer w-full disabled:opacity-30 group"
64
- disabled={item.disabled}
65
+ disabled={item?.disabled}
65
66
  onclick={handleClick}
66
67
  >
67
- <div class="{styles} rounded-md pr-2 flex items-center justify-start w-full">
68
- {#if item.useAvatar}
69
- <ProfileAvatar name={item.label} picture={item.picture || ''} variant="sm" />
70
- {:else if item.picture}
71
- <ProfileAvatar name={item.label} picture={item.picture} variant="sm" />
72
- {:else if item.icon}
68
+ <div class="bg-background rounded-md">
69
+ <div class="{styles} rounded-md pr-2 flex items-center justify-start w-full">
70
+ {#if item?.useAvatar}
71
+ <ProfileAvatar name={item?.label || ''} picture={item?.picture || ''} variant="sm" />
72
+ {:else if item?.picture}
73
+ <ProfileAvatar name={item?.label || ''} picture={item?.picture} variant="sm" />
74
+ {:else if item?.icon}
73
75
  <Icon
74
76
  src={item.icon}
75
- class="w-4 h-4 {item.destructive
77
+ class="w-4 h-4 {item?.destructive
76
78
  ? 'text-icon-critical'
77
- : item.iconClass || 'text-icon'} {item.locked ? 'opacity-30' : ''}"
79
+ : item?.iconClass || 'text-icon'} {item?.locked ? 'opacity-30' : ''}"
78
80
  />
79
81
  {/if}
80
82
  <div class="whitespace-nowrap flex-1 text-left flex items-center space-x-1.5 truncate" {title}>
81
- {#if item.color}
83
+ {#if item?.color}
82
84
  <TagStatus status={item.color} dot />
83
85
  {/if}
84
- <span class="{labelStyles} text-base font-medium truncate">{item.label}</span>
86
+ <span class="{labelStyles} text-base font-medium truncate">{item?.label || ''}</span>
85
87
 
86
- {#if item.country}
88
+ {#if item?.country}
87
89
  <BaseFlag country={item.country} />
88
90
  <span class="text-xs font-medium text-foreground-default-secondary uppercase">
89
91
  {item.country}
90
92
  </span>
91
93
  {/if}
92
94
  </div>
93
- {#if item.action}
94
- {@render item.action()}
95
+ {#if item?.action}
96
+ <div class="no-drag !cursor-default">
97
+ {@render item.action(item)}
98
+ </div>
95
99
  {:else if multiple}
96
100
  <InputCheckbox
97
- checked={item.selected ?? false}
101
+ checked={item?.selected ?? false}
98
102
  onchange={(value) => {
99
- item.selected = value
100
- onchange?.(item)
103
+ if (item) {
104
+ item.selected = value
105
+ onchange?.(item)
106
+ }
101
107
  }}
102
108
  />
103
- {:else if item.selected}
109
+ {:else if item?.selected}
104
110
  <Icon src={Success} class="size-4 text-icon-selected" />
105
- {:else if item.rightIcon}
111
+ {:else if item?.rightIcon}
106
112
  <Icon src={item.rightIcon} class="size-4 text-icon-default-secondary" />
107
113
  {/if}
114
+ </div>
108
115
  </div>
109
116
  </button>