picocrank 1.33.0 → 1.34.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 (35) hide show
  1. package/docs/filter-query-v1.md +5 -1
  2. package/package.json +1 -1
  3. package/router.js +33 -0
  4. package/styles/picocrank-extensions.css +23 -2
  5. package/vue/components/Accordion.spec.js +48 -0
  6. package/vue/components/Accordion.vue +46 -0
  7. package/vue/components/AccordionItem.vue +103 -0
  8. package/vue/components/FieldValue.spec.js +88 -0
  9. package/vue/components/FieldValue.vue +222 -0
  10. package/vue/components/FormField.spec.js +1 -0
  11. package/vue/components/FormField.vue +38 -10
  12. package/vue/components/Header.vue +24 -2
  13. package/vue/components/NotificationBlock.vue +25 -0
  14. package/vue/components/ReorderList.spec.js +318 -0
  15. package/vue/components/ReorderList.vue +614 -0
  16. package/vue/components/Table.spec.js +27 -0
  17. package/vue/components/Table.vue +458 -112
  18. package/vue/components/TableColumnFilterPopover.vue +63 -19
  19. package/vue/components/TableColumnOptionsPopover.vue +248 -273
  20. package/vue/components/TopBar.vue +6 -2
  21. package/vue/components/accordionContext.js +1 -0
  22. package/vue/composables/tableColumnPresets.js +33 -0
  23. package/vue/composables/tableGroupBy.js +173 -0
  24. package/vue/composables/tableGroupBy.spec.js +69 -0
  25. package/vue/composables/tableViewSettings.js +34 -0
  26. package/vue/composables/tableViewSettings.spec.js +21 -0
  27. package/vue/composables/useHeaderTopBarFit.js +91 -0
  28. package/vue/composables/useResponsiveNav.js +5 -3
  29. package/vue/data/navSectionHubs.js +16 -1
  30. package/vue/examples/AccordionExample.vue +33 -0
  31. package/vue/examples/FieldValueExample.vue +143 -0
  32. package/vue/examples/FormExample.vue +11 -6
  33. package/vue/examples/ReorderListExample.vue +236 -0
  34. package/vue/examples/TableExample.vue +5 -1
  35. package/vue/examples/TableRemoteExample.vue +28 -4
@@ -17,108 +17,75 @@
17
17
  <Tabs
18
18
  class="column-options-tabs"
19
19
  :tabs="optionTabs"
20
- default-tab="custom"
20
+ default-tab="current"
21
21
  @tab-change="onOptionTabChange"
22
22
  >
23
- <template #tab-custom>
23
+ <template #tab-current>
24
24
  <p class="subtle column-options-help">
25
- Drag rows or use the arrows to reorder. Toggle checkboxes to show or hide columns. Set responsive priority (1 hides first) when using column priorities layout.
25
+ Drag the handle to reorder columns. Toggle checkboxes to show or hide columns. Set responsive priority (1 hides first) when using column priorities layout.
26
26
  </p>
27
27
 
28
- <div ref="listScrollRef" class="column-options-scroll" :style="listScrollStyle">
29
- <ul class="column-options-list">
30
- <li
31
- v-for="key in draftOrder"
32
- :key="key"
33
- class="column-options-item"
34
- :class="{
35
- 'is-dragging': draggingKey === key,
36
- 'is-drag-over': dragOverKey === key && draggingKey !== key,
37
- 'is-fixed': !isOrderable(key),
38
- }"
39
- :draggable="isOrderable(key)"
40
- @dragstart="onDragStart(key, $event)"
41
- @dragover.prevent="onDragOver(key)"
42
- @drop.prevent="onDrop(key)"
43
- @dragend="onDragEnd"
28
+ <label v-if="groupableColumnOptions.length > 0" class="column-options-group-by">
29
+ <span class="field-label">Group by</span>
30
+ <select
31
+ :value="draftGroupBy ?? ''"
32
+ aria-label="Group table rows by column"
33
+ @change="setGroupBy($event.target.value)"
34
+ >
35
+ <option value="">None</option>
36
+ <option
37
+ v-for="header in groupableColumnOptions"
38
+ :key="header.key"
39
+ :value="header.key"
44
40
  >
45
- <label class="column-options-label">
46
- <input
47
- type="checkbox"
48
- :checked="isVisible(key)"
49
- :disabled="!isHideable(key) || (isVisible(key) && draftVisibleKeys.length <= 1)"
50
- @change="toggleVisible(key)"
51
- />
52
- <span>{{ columnDisplayLabel(key) }}</span>
53
- </label>
54
-
55
- <select
56
- v-if="isPriorityConfigurable(key)"
57
- class="column-options-priority"
58
- :value="prioritySelectValue(key)"
59
- :aria-label="`Priority for ${columnLabel(key)} column`"
60
- @change="setPriority(key, $event.target.value)"
61
- >
62
- <option value="">Never hide</option>
63
- <option
64
- v-for="level in priorityLevels"
65
- :key="level"
66
- :value="level"
67
- >
68
- {{ priorityOptionLabel(level) }}
69
- </option>
70
- </select>
71
- <span v-else class="column-options-priority-spacer" aria-hidden="true" />
41
+ {{ header.label || header.key }}
42
+ </option>
43
+ </select>
44
+ </label>
72
45
 
73
- <div class="column-options-reorder">
74
- <div class="column-options-move" role="group" :aria-label="`Move ${columnLabel(key)} column`">
75
- <button
76
- type="button"
77
- class="neutral column-options-move-button"
78
- :disabled="!canMove(key, -1)"
79
- :aria-label="`Move ${columnLabel(key)} up`"
80
- @click="moveColumn(key, -1)"
81
- >
82
- <HugeiconsIcon
83
- :icon="ArrowUp01Icon"
84
- width="0.9em"
85
- height="0.9em"
86
- :strokeWidth="2"
87
- aria-hidden="true"
88
- />
89
- </button>
90
- <button
91
- type="button"
92
- class="neutral column-options-move-button"
93
- :disabled="!canMove(key, 1)"
94
- :aria-label="`Move ${columnLabel(key)} down`"
95
- @click="moveColumn(key, 1)"
96
- >
97
- <HugeiconsIcon
98
- :icon="ArrowDown01Icon"
99
- width="0.9em"
100
- height="0.9em"
101
- :strokeWidth="2"
102
- aria-hidden="true"
103
- />
104
- </button>
105
- </div>
106
-
107
- <span
108
- class="column-options-drag-handle"
109
- :class="{ disabled: !isOrderable(key) }"
110
- aria-hidden="true"
111
- >
112
- <HugeiconsIcon
113
- :icon="DragDropVerticalIcon"
114
- width="0.95em"
115
- height="0.95em"
116
- :strokeWidth="2"
46
+ <div ref="listScrollRef" class="column-options-scroll" :style="listScrollStyle">
47
+ <ReorderList
48
+ :model-value="draftOrder"
49
+ :item-key="columnKeyIdentity"
50
+ :item-label="columnLabel"
51
+ :is-item-reorder-disabled="(key) => !isOrderable(key)"
52
+ aria-label="Column order"
53
+ :show-move-buttons="false"
54
+ :show-drag-handles="true"
55
+ @update:model-value="onDraftOrderUpdate"
56
+ >
57
+ <template #item="{ item: key }">
58
+ <div class="column-options-row-body">
59
+ <label class="column-options-label">
60
+ <input
61
+ type="checkbox"
62
+ :checked="isVisible(key)"
63
+ :disabled="!isHideable(key) || (isVisible(key) && draftVisibleKeys.length <= 1)"
64
+ @change="toggleVisible(key)"
117
65
  />
118
- </span>
66
+ <span>{{ columnDisplayLabel(key) }}</span>
67
+ </label>
68
+
69
+ <select
70
+ v-if="isPriorityConfigurable(key)"
71
+ class="column-options-priority"
72
+ :value="prioritySelectValue(key)"
73
+ :aria-label="`Priority for ${columnLabel(key)} column`"
74
+ @change="setPriority(key, $event.target.value)"
75
+ >
76
+ <option value="">Never hide</option>
77
+ <option
78
+ v-for="level in priorityLevels"
79
+ :key="level"
80
+ :value="level"
81
+ >
82
+ {{ priorityOptionLabel(level) }}
83
+ </option>
84
+ </select>
85
+ <span v-else class="column-options-priority-spacer" aria-hidden="true" />
119
86
  </div>
120
- </li>
121
- </ul>
87
+ </template>
88
+ </ReorderList>
122
89
  </div>
123
90
  </template>
124
91
 
@@ -173,13 +140,15 @@
173
140
  <p v-else class="subtle column-options-tab-placeholder">No saved layouts yet.</p>
174
141
  </template>
175
142
  <p v-else class="subtle column-options-tab-placeholder">
176
- Provide a <code>table-id</code> or remote <code>layout-presets</code> callbacks to enable saved layouts.
143
+ Set <code>view-control-mode="localStorageControlled"</code> with a
144
+ <code>table-id</code>, or <code>view-control-mode="callbackControlled"</code>
145
+ with <code>view-settings</code> callbacks, to enable saved layouts.
177
146
  </p>
178
147
 
179
148
  <p v-if="loadError" class="column-options-tab-error">{{ loadError }}</p>
180
149
 
181
- <div class="column-options-load-default">
182
- <button type="button" class="neutral" @click="submitLoadDeveloperDefaults">
150
+ <div v-if="defaultPresetId" class="column-options-load-default">
151
+ <button type="button" class="neutral" @click="submitLoadPreset(defaultPresetId)">
183
152
  Load default
184
153
  </button>
185
154
  </div>
@@ -188,35 +157,76 @@
188
157
 
189
158
  <template #tab-save>
190
159
  <div v-if="!layoutPresetsEnabled" class="column-options-tab-placeholder">
191
- <p class="subtle">Provide a <code>table-id</code> or remote <code>layout-presets</code> callbacks to enable saved layouts.</p>
160
+ <p class="subtle">
161
+ Set <code>view-control-mode="localStorageControlled"</code> with a
162
+ <code>table-id</code>, or <code>view-control-mode="callbackControlled"</code>
163
+ with <code>view-settings</code> callbacks, to enable saved layouts.
164
+ </p>
192
165
  </div>
193
166
  <div v-else class="column-options-save-panel">
194
- <p class="subtle column-options-save-help">
195
- Saves the current table view (filters, page size, column order, visibility, and priorities).
196
- </p>
197
- <label class="column-options-save-field">
198
- <span class="field-label">Layout name</span>
199
- <input
200
- v-model="savePresetName"
201
- type="text"
202
- placeholder="e.g. Managers view"
203
- @keydown.enter.prevent="submitSavePreset"
204
- />
205
- </label>
206
- <label class="column-options-save-default">
207
- <input v-model="saveAsDefault" type="checkbox" />
208
- <span>Set as default</span>
209
- </label>
210
- <p v-if="saveError" class="column-options-tab-error">{{ saveError }}</p>
211
- <p v-if="saveSuccess" class="column-options-tab-success">{{ saveSuccess }}</p>
212
- <button type="button" @click="submitSavePreset">Save layout</button>
167
+ <div class="column-options-save-overwrite">
168
+ <p class="field-label">Saved views</p>
169
+ <ul v-if="presets.length > 0" class="column-options-preset-list">
170
+ <li
171
+ v-for="preset in presets"
172
+ :key="preset.id"
173
+ class="column-options-preset-item"
174
+ >
175
+ <div class="column-options-preset-meta">
176
+ <strong>{{ preset.name }}</strong>
177
+ <span v-if="preset.id === defaultPresetId" class="tag">Default</span>
178
+ <p class="subtle column-options-preset-date">{{ formatPresetDate(preset.savedAt) }}</p>
179
+ </div>
180
+ <div
181
+ class="column-options-preset-actions"
182
+ role="group"
183
+ :aria-label="`Overwrite ${preset.name}`"
184
+ >
185
+ <button
186
+ type="button"
187
+ class="neutral"
188
+ @click="submitOverwritePreset(preset.id, preset.name)"
189
+ >
190
+ Overwrite
191
+ </button>
192
+ </div>
193
+ </li>
194
+ </ul>
195
+ <p v-else class="subtle column-options-tab-placeholder">No saved layouts yet.</p>
196
+ </div>
197
+
198
+ <div class="column-options-save-as">
199
+ <p class="field-label">Save as</p>
200
+ <p class="subtle column-options-save-help">
201
+ Saves the current table view (filters, group by, page size, column order, visibility, and priorities).
202
+ </p>
203
+ <label class="column-options-save-field">
204
+ <span class="field-label">Layout name</span>
205
+ <input
206
+ v-model="savePresetName"
207
+ type="text"
208
+ placeholder="e.g. Managers view"
209
+ @keydown.enter.prevent="submitSavePreset"
210
+ />
211
+ </label>
212
+ <label class="column-options-save-default">
213
+ <input v-model="saveAsDefault" type="checkbox" />
214
+ <span>Set as default</span>
215
+ </label>
216
+ <p v-if="saveError" class="column-options-tab-error">{{ saveError }}</p>
217
+ <button type="button" @click="submitSavePreset">Save as new layout</button>
218
+ </div>
213
219
  </div>
214
220
  </template>
215
221
  </Tabs>
216
222
  </div>
217
223
 
218
- <div ref="actionsRef" role="toolbar" class="popover-actions">
219
- <button type="button" class="neutral" @click="close">Close</button>
224
+ <div
225
+ v-if="activeOptionTab === 'current'"
226
+ ref="actionsRef"
227
+ role="toolbar"
228
+ class="popover-actions"
229
+ >
220
230
  <button type="button" class="neutral" @click="showAll">Show all</button>
221
231
  </div>
222
232
  </div>
@@ -225,24 +235,25 @@
225
235
 
226
236
  <script setup>
227
237
  import { computed, nextTick, onBeforeUnmount, ref, useId, watch } from 'vue'
228
- import { HugeiconsIcon } from '@hugeicons/vue'
229
- import {
230
- ArrowDown01Icon,
231
- ArrowUp01Icon,
232
- DragDropVerticalIcon,
233
- } from '@hugeicons/core-free-icons'
234
238
  import {
235
239
  isFilterEntryActive,
236
240
  normalizeColumnFilterEntries,
237
241
  } from '../composables/tableFilters.js'
242
+ import { groupableHeaders } from '../composables/tableGroupBy.js'
243
+ import { useNotificationPopups } from '../composables/useNotificationPopups.js'
244
+ import ReorderList from './ReorderList.vue'
238
245
  import Tabs from './Tabs.vue'
239
246
 
247
+ const { show: showNotificationPopup } = useNotificationPopups()
248
+
240
249
  const optionTabs = [
241
- { id: 'custom', label: 'Custom' },
250
+ { id: 'current', label: 'Current' },
242
251
  { id: 'load', label: 'Load' },
243
252
  { id: 'save', label: 'Save' },
244
253
  ]
245
254
 
255
+ const activeOptionTab = ref('current')
256
+
246
257
  const props = defineProps({
247
258
  open: {
248
259
  type: Boolean,
@@ -300,6 +311,10 @@ const props = defineProps({
300
311
  type: Function,
301
312
  default: null,
302
313
  },
314
+ overwriteLayoutPreset: {
315
+ type: Function,
316
+ default: null,
317
+ },
303
318
  loadLayoutPreset: {
304
319
  type: Function,
305
320
  default: null,
@@ -316,6 +331,14 @@ const props = defineProps({
316
331
  type: Function,
317
332
  default: null,
318
333
  },
334
+ groupBy: {
335
+ type: String,
336
+ default: null,
337
+ },
338
+ groupable: {
339
+ type: Boolean,
340
+ default: true,
341
+ },
319
342
  })
320
343
 
321
344
  const emit = defineEmits(['update:open', 'apply', 'cancel'])
@@ -335,12 +358,10 @@ const priorityLevels = [1, 2, 3, 4, 5]
335
358
  const draftOrder = ref([])
336
359
  const draftVisibleKeys = ref([])
337
360
  const draftPriorities = ref({})
338
- const draggingKey = ref(null)
339
- const dragOverKey = ref(null)
361
+ const draftGroupBy = ref(null)
340
362
  const savePresetName = ref('')
341
363
  const saveAsDefault = ref(false)
342
364
  const saveError = ref('')
343
- const saveSuccess = ref('')
344
365
  const loadError = ref('')
345
366
 
346
367
  const headerByKey = computed(() => new Map(
@@ -349,10 +370,16 @@ const headerByKey = computed(() => new Map(
349
370
  .map((header) => [header.key, header]),
350
371
  ))
351
372
 
373
+ const groupableColumnOptions = computed(() => groupableHeaders(props.headers, props.groupable))
374
+
352
375
  function columnLabel(key) {
353
376
  return headerByKey.value.get(key)?.label || key
354
377
  }
355
378
 
379
+ function columnKeyIdentity(key) {
380
+ return key
381
+ }
382
+
356
383
  function columnFilterCount(key) {
357
384
  const header = headerByKey.value.get(key)
358
385
  if (!header || header.filterable === false) {
@@ -474,7 +501,7 @@ function positionPopover() {
474
501
  function updateListScrollLimit() {
475
502
  listScrollStyle.value = {}
476
503
 
477
- if (!popoverRef.value || !listScrollRef.value) {
504
+ if (activeOptionTab.value !== 'current' || !popoverRef.value || !listScrollRef.value) {
478
505
  return
479
506
  }
480
507
 
@@ -483,7 +510,9 @@ function updateListScrollLimit() {
483
510
  const popoverEl = popoverRef.value
484
511
  const titleHeight = titleRef.value?.offsetHeight ?? 0
485
512
  const tabsHeaderHeight = popoverEl.querySelector('.column-options-tabs .tabs-header')?.offsetHeight ?? 0
486
- const helpHeight = popoverEl.querySelector('.column-options-help')?.offsetHeight ?? 0
513
+ const activePanel = popoverEl.querySelector('.column-options-tabs .tab-panel.active')
514
+ const helpHeight = activePanel?.querySelector('.column-options-help')?.offsetHeight ?? 0
515
+ const groupByHeight = activePanel?.querySelector('.column-options-group-by')?.offsetHeight ?? 0
487
516
  const actionsHeight = actionsRef.value?.offsetHeight ?? 0
488
517
  const popoverStyles = window.getComputedStyle(popoverEl)
489
518
  const paddingTop = Number.parseFloat(popoverStyles.paddingTop) || 0
@@ -497,10 +526,10 @@ function updateListScrollLimit() {
497
526
  availablePopoverHeight = Math.max(spaceBelow, spaceAbove)
498
527
  }
499
528
 
500
- const reservedHeight = paddingTop + paddingBottom + titleHeight + tabsHeaderHeight + helpHeight + actionsHeight + bodyGap
529
+ const reservedHeight = paddingTop + paddingBottom + titleHeight + tabsHeaderHeight + helpHeight + groupByHeight + actionsHeight + bodyGap
501
530
  const optionsAreaBudget = Math.max(0, availablePopoverHeight - reservedHeight)
502
531
 
503
- const firstItem = listScrollRef.value.querySelector('.column-options-item')
532
+ const firstItem = listScrollRef.value.querySelector('.reorder-list-row')
504
533
  const rowHeight = firstItem?.offsetHeight || 40
505
534
  const optionCount = draftOrder.value.length
506
535
 
@@ -529,7 +558,6 @@ function resetPresetForm() {
529
558
  savePresetName.value = ''
530
559
  saveAsDefault.value = false
531
560
  saveError.value = ''
532
- saveSuccess.value = ''
533
561
  loadError.value = ''
534
562
  }
535
563
 
@@ -546,7 +574,6 @@ function formatPresetDate(savedAt) {
546
574
 
547
575
  async function submitSavePreset() {
548
576
  saveError.value = ''
549
- saveSuccess.value = ''
550
577
 
551
578
  if (!props.saveLayoutPreset) {
552
579
  saveError.value = 'Save is unavailable.'
@@ -563,11 +590,40 @@ async function submitSavePreset() {
563
590
  return
564
591
  }
565
592
 
566
- saveSuccess.value = `Saved “${result.preset.name}”.`
593
+ showNotificationPopup({
594
+ label: 'Saved',
595
+ class: 'success',
596
+ message: `Saved “${result.preset.name}”.`,
597
+ })
567
598
  savePresetName.value = ''
568
599
  saveAsDefault.value = false
569
600
  }
570
601
 
602
+ async function submitOverwritePreset(presetId, presetName) {
603
+ saveError.value = ''
604
+
605
+ if (!props.overwriteLayoutPreset) {
606
+ saveError.value = 'Overwrite is unavailable.'
607
+ return
608
+ }
609
+
610
+ if (!globalThis.confirm?.(`Overwrite saved layout “${presetName}” with the current table view?`)) {
611
+ return
612
+ }
613
+
614
+ const result = await Promise.resolve(props.overwriteLayoutPreset({ presetId }))
615
+ if (!result?.ok) {
616
+ saveError.value = result?.error || 'Unable to overwrite layout.'
617
+ return
618
+ }
619
+
620
+ showNotificationPopup({
621
+ label: 'Saved',
622
+ class: 'success',
623
+ message: `Overwrote “${presetName}”.`,
624
+ })
625
+ }
626
+
571
627
  async function submitLoadPreset(presetId) {
572
628
  loadError.value = ''
573
629
  const result = await Promise.resolve(props.loadLayoutPreset?.(presetId))
@@ -612,6 +668,12 @@ function resetDraft() {
612
668
  draftOrder.value = [...props.columnKeys]
613
669
  draftVisibleKeys.value = [...props.visibleKeys]
614
670
  draftPriorities.value = { ...props.columnPriorities }
671
+ draftGroupBy.value = props.groupBy || null
672
+ }
673
+
674
+ function setGroupBy(rawValue) {
675
+ draftGroupBy.value = rawValue || null
676
+ emitLiveChange()
615
677
  }
616
678
 
617
679
  function toggleVisible(key) {
@@ -632,90 +694,22 @@ function toggleVisible(key) {
632
694
  emitLiveChange()
633
695
  }
634
696
 
635
- function canMove(key, direction) {
636
- if (!isOrderable(key)) {
637
- return false
638
- }
639
-
640
- let index = draftOrder.value.indexOf(key)
641
- while (index >= 0 && index < draftOrder.value.length) {
642
- index += direction
643
- if (index < 0 || index >= draftOrder.value.length) {
644
- return false
645
- }
646
- if (isOrderable(draftOrder.value[index])) {
647
- return true
648
- }
649
- }
650
-
651
- return false
652
- }
653
-
654
- function moveColumn(key, direction) {
655
- if (!canMove(key, direction)) {
697
+ function onDraftOrderUpdate(next) {
698
+ if (!Array.isArray(next) || next.length !== draftOrder.value.length) {
656
699
  return
657
700
  }
658
701
 
659
- const order = [...draftOrder.value]
660
- let index = order.indexOf(key)
661
-
662
- while (true) {
663
- const targetIndex = index + direction
664
- if (targetIndex < 0 || targetIndex >= order.length) {
665
- return
666
- }
667
- if (isOrderable(order[targetIndex])) {
668
- const swapKey = order[targetIndex]
669
- order[index] = swapKey
670
- order[targetIndex] = key
671
- draftOrder.value = order
672
- emitLiveChange()
702
+ for (let index = 0; index < next.length; index += 1) {
703
+ const previousKey = draftOrder.value[index]
704
+ if (!isOrderable(previousKey) && next[index] !== previousKey) {
673
705
  return
674
706
  }
675
- index = targetIndex
676
707
  }
677
- }
678
708
 
679
- function onDragStart(key, event) {
680
- if (!isOrderable(key)) {
681
- event.preventDefault()
682
- return
683
- }
684
- draggingKey.value = key
685
- dragOverKey.value = key
686
- event.dataTransfer.effectAllowed = 'move'
687
- event.dataTransfer.setData('text/plain', key)
688
- }
689
-
690
- function onDragOver(key) {
691
- if (!draggingKey.value || draggingKey.value === key) {
692
- return
693
- }
694
- dragOverKey.value = key
695
- }
696
-
697
- function onDrop(targetKey) {
698
- const sourceKey = draggingKey.value
699
- if (!sourceKey || sourceKey === targetKey) {
700
- return
701
- }
702
-
703
- const order = draftOrder.value.filter((key) => key !== sourceKey)
704
- const targetIndex = order.indexOf(targetKey)
705
- if (targetIndex === -1) {
706
- return
707
- }
708
-
709
- order.splice(targetIndex, 0, sourceKey)
710
- draftOrder.value = order
709
+ draftOrder.value = next
711
710
  emitLiveChange()
712
711
  }
713
712
 
714
- function onDragEnd() {
715
- draggingKey.value = null
716
- dragOverKey.value = null
717
- }
718
-
719
713
  function onDocumentPointerDown(event) {
720
714
  if (!props.open || !popoverRef.value) {
721
715
  return
@@ -772,6 +766,7 @@ function emitLiveChange() {
772
766
  order: [...draftOrder.value],
773
767
  visibleKeys,
774
768
  priorities: buildPriorityOverrides(),
769
+ groupBy: draftGroupBy.value || null,
775
770
  })
776
771
  }
777
772
 
@@ -780,7 +775,8 @@ function showAll() {
780
775
  emitLiveChange()
781
776
  }
782
777
 
783
- async function onOptionTabChange() {
778
+ async function onOptionTabChange(_tab, tabKey) {
779
+ activeOptionTab.value = tabKey ?? 'current'
784
780
  await nextTick()
785
781
  updateListScrollLimit()
786
782
  await nextTick()
@@ -793,6 +789,7 @@ function close() {
793
789
  }
794
790
 
795
791
  async function initializePopover() {
792
+ activeOptionTab.value = 'current'
796
793
  resetDraft()
797
794
  resetPresetForm()
798
795
  await nextTick()
@@ -807,8 +804,6 @@ watch(
807
804
  async (isOpen, wasOpen) => {
808
805
  if (!isOpen) {
809
806
  listScrollStyle.value = {}
810
- draggingKey.value = null
811
- dragOverKey.value = null
812
807
  document.removeEventListener('pointerdown', onDocumentPointerDown)
813
808
  document.removeEventListener('keydown', onDocumentKeyDown)
814
809
  return
@@ -875,6 +870,18 @@ onBeforeUnmount(() => {
875
870
  flex-shrink: 0;
876
871
  }
877
872
 
873
+ .column-options-group-by {
874
+ display: flex;
875
+ flex-direction: column;
876
+ gap: 0.35rem;
877
+ margin: 0 0 0.75rem;
878
+ flex-shrink: 0;
879
+ }
880
+
881
+ .column-options-group-by select {
882
+ width: 100%;
883
+ }
884
+
878
885
  .column-options-tabs {
879
886
  flex: 1 1 auto;
880
887
  min-height: 0;
@@ -889,7 +896,7 @@ onBeforeUnmount(() => {
889
896
  min-height: 0;
890
897
  }
891
898
 
892
- .column-options-tabs :deep(.tab-panel) {
899
+ .column-options-tabs :deep(.tab-panel:not([hidden])) {
893
900
  padding-block: 0.75rem;
894
901
  padding-inline: 0;
895
902
  min-height: 0;
@@ -897,6 +904,10 @@ onBeforeUnmount(() => {
897
904
  flex-direction: column;
898
905
  }
899
906
 
907
+ .column-options-tabs :deep(.tab-panel[hidden]) {
908
+ display: none;
909
+ }
910
+
900
911
  .column-options-tabs :deep(.tab-button) {
901
912
  padding: 0.45rem 0.85rem;
902
913
  font-size: 0.9rem;
@@ -930,6 +941,21 @@ onBeforeUnmount(() => {
930
941
  gap: 0.45rem;
931
942
  }
932
943
 
944
+ .column-options-save-overwrite {
945
+ display: flex;
946
+ flex-direction: column;
947
+ gap: 0.5rem;
948
+ }
949
+
950
+ .column-options-save-as {
951
+ display: flex;
952
+ flex-direction: column;
953
+ gap: 0.75rem;
954
+ margin-top: 1.25rem;
955
+ padding-top: 1rem;
956
+ border-top: 1px solid var(--border-color);
957
+ }
958
+
933
959
  .field-label {
934
960
  font-size: 0.85rem;
935
961
  color: var(--table-popover-muted-fg);
@@ -941,12 +967,6 @@ onBeforeUnmount(() => {
941
967
  color: var(--table-popover-error-fg);
942
968
  }
943
969
 
944
- .column-options-tab-success {
945
- margin: 0;
946
- font-size: 0.85rem;
947
- color: var(--table-popover-success-fg);
948
- }
949
-
950
970
  .column-options-load-panel {
951
971
  display: flex;
952
972
  flex-direction: column;
@@ -1008,49 +1028,23 @@ onBeforeUnmount(() => {
1008
1028
  overscroll-behavior: contain;
1009
1029
  }
1010
1030
 
1011
- .column-options-list {
1012
- list-style: none;
1013
- margin: 0;
1014
- padding: 0;
1015
- display: flex;
1016
- flex-direction: column;
1031
+ .column-options-scroll :deep(.reorder-list) {
1017
1032
  gap: 0.35rem;
1018
1033
  }
1019
1034
 
1020
- .column-options-item {
1021
- display: grid;
1022
- grid-template-columns: minmax(0, 1fr) 5.5rem auto;
1023
- align-items: center;
1024
- gap: 0.5rem;
1035
+ .column-options-scroll :deep(.reorder-list-row) {
1025
1036
  padding: 0.25rem 0.45rem;
1026
- border: 1px solid var(--table-popover-border);
1027
- border-radius: 0.35rem;
1037
+ gap: 0.5rem;
1038
+ border-color: var(--table-popover-border);
1028
1039
  background: var(--table-popover-item-bg);
1029
1040
  }
1030
1041
 
1031
- .column-options-item.is-dragging {
1032
- opacity: 0.55;
1033
- }
1034
-
1035
- .column-options-item.is-drag-over {
1036
- border-color: var(--table-popover-item-drag-border);
1037
- }
1038
-
1039
- .column-options-item.is-fixed {
1040
- opacity: 0.85;
1041
- }
1042
-
1043
- .column-options-drag-handle {
1044
- display: inline-flex;
1042
+ .column-options-row-body {
1043
+ display: grid;
1044
+ grid-template-columns: minmax(0, 1fr) 5.5rem;
1045
1045
  align-items: center;
1046
- color: var(--table-popover-fg, inherit);
1047
- opacity: 0.65;
1048
- cursor: grab;
1049
- }
1050
-
1051
- .column-options-item.is-fixed .column-options-drag-handle.disabled {
1052
- opacity: 0.35;
1053
- cursor: default;
1046
+ gap: 0.5rem;
1047
+ min-width: 0;
1054
1048
  }
1055
1049
 
1056
1050
  .column-options-label {
@@ -1079,25 +1073,6 @@ onBeforeUnmount(() => {
1079
1073
  display: block;
1080
1074
  }
1081
1075
 
1082
- .column-options-reorder {
1083
- display: inline-flex;
1084
- align-items: center;
1085
- gap: 0.25rem;
1086
- }
1087
-
1088
- .column-options-move {
1089
- display: inline-flex;
1090
- gap: 0.2rem;
1091
- }
1092
-
1093
- .column-options-move-button {
1094
- display: inline-flex;
1095
- align-items: center;
1096
- justify-content: center;
1097
- min-width: 1.75rem;
1098
- padding: 0.2rem 0.35rem;
1099
- }
1100
-
1101
1076
  .popover-actions {
1102
1077
  display: flex;
1103
1078
  flex-shrink: 0;