picocrank 1.24.0 → 1.25.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 (36) hide show
  1. package/AGENTS.md +2 -0
  2. package/docs/filter-query-v1.md +180 -0
  3. package/logo.svg +7 -7
  4. package/package.json +7 -5
  5. package/public/themes/README.md +19 -2
  6. package/public/themes/index.json +1 -5
  7. package/styles/dark-theme.css +16 -1
  8. package/supplemental-themes/README.md +9 -0
  9. package/supplemental-themes/catppuccin-latte-frappe/theme.css +276 -0
  10. package/supplemental-themes/dracula-alucard/theme.css +258 -0
  11. package/supplemental-themes/gruvbox-dark-light/theme.css +264 -0
  12. package/supplemental-themes/index.json +17 -0
  13. package/{public/themes → supplemental-themes}/waffles/theme.css +99 -0
  14. package/vite.config.js +91 -0
  15. package/vue/components/Header.vue +3 -3
  16. package/vue/components/Login.vue +1 -3
  17. package/vue/components/Section.vue +45 -14
  18. package/vue/components/Sidebar.vue +2 -1
  19. package/vue/components/SidebarNavLink.vue +6 -0
  20. package/vue/components/Table.vue +1113 -73
  21. package/vue/components/TableColumnFilterPopover.vue +263 -44
  22. package/vue/components/TableColumnOptionsPopover.vue +1116 -0
  23. package/vue/components/ThemeSwitcher.vue +119 -0
  24. package/vue/components/TopBar.vue +2 -2
  25. package/vue/composables/tableColumnPresets.js +296 -0
  26. package/vue/composables/tableFilterQuery.js +202 -0
  27. package/vue/composables/tableFilters.js +167 -3
  28. package/vue/composables/useCustomTheme.js +167 -24
  29. package/vue/data/examplePeople.js +2 -6
  30. package/vue/examples/App.vue +1 -1
  31. package/vue/examples/LoginExample.vue +1 -3
  32. package/vue/examples/TableExample.vue +329 -11
  33. package/vue/examples/TableRemoteExample.vue +112 -4
  34. package/vue/examples/ThemePreview.vue +17 -31
  35. package/public/themes/catppuccin/theme.css +0 -195
  36. package/public/themes/dracula/theme.css +0 -181
@@ -1,55 +1,117 @@
1
1
  <template>
2
- <table class="row-hover" :class="{ loading: isLoading }">
3
- <thead>
4
- <th
5
- v-for="(header, index) in visibleHeaders"
6
- :key="header.key || index"
7
- :class="headerClasses(header)"
8
- :style="{ width: header.width || 'auto' }"
9
- @click="onHeaderClick(header, $event)"
10
- @contextmenu.prevent="openFilter(header, $event)"
11
- @pointerdown="onHeaderPointerDown(header, $event)"
12
- @pointermove="longPress.onPointerMove"
13
- @pointerup="longPress.onPointerUp"
14
- @pointercancel="longPress.onPointerCancel"
15
- @pointerleave="longPress.onPointerLeave"
16
- >
17
- <span class="header-label">{{ header.label || header.key }}</span>
18
-
19
- <span v-if="filterable && isHeaderFiltered(header)" class="filter-indicator" title="Filtered" aria-hidden="true">⧩</span>
20
-
21
- <span v-if="header.sortable" class="sort-indicator">
22
- <span v-if="sortBy === header.key">
23
- <span v-if="sortDir === 'asc'">▲</span>
24
- <span v-else-if="sortDir === 'desc'">▼</span>
25
- </span>
26
- </span>
27
- </th>
28
- </thead>
29
- <tbody>
30
- <tr v-if="displayRows.length === 0">
31
- <td :colspan="visibleHeaders.length">
32
- <span v-if="isLoading">Loading…</span>
33
- <span v-else>No items found</span>
34
- </td>
35
- </tr>
36
- <tr v-else v-for="(row, rowIndex) in displayRows" :key="rowIndex">
37
- <td v-for="(header, cellIndex) in visibleHeaders" :key="header.key || cellIndex" :class="{ hidden: header.hidden }">
38
- <component
39
- v-if="slotFor(header.key)"
40
- :is="slotFor(header.key)"
41
- :class="{ hidden: header.hidden }"
42
- :row="row"
43
- :value="row[header.key]"
2
+ <div
3
+ v-if="showTableChrome"
4
+ :class="needsTableWrapper ? tableWrapperClasses : undefined"
5
+ :style="needsTableWrapper ? undefined : { display: 'contents' }"
6
+ >
7
+ <table class="row-hover" :class="{ loading: isLoading }">
8
+ <thead>
9
+ <tr>
10
+ <th
11
+ v-for="(header, index) in visibleHeaders"
12
+ :key="header.key || index"
13
+ :class="[headerClasses(header), header.class]"
14
+ :style="{ width: header.width || 'auto' }"
15
+ @click="onHeaderClick(header, $event)"
16
+ @contextmenu.prevent="openFilter(header, $event)"
17
+ @pointerdown="onHeaderPointerDown(header, $event)"
18
+ @pointermove="longPress.onPointerMove"
19
+ @pointerup="longPress.onPointerUp"
20
+ @pointercancel="longPress.onPointerCancel"
21
+ @pointerleave="longPress.onPointerLeave"
22
+ >
23
+ <span class="header-label">
24
+ {{ header.label || header.key }}<template v-if="headerFilterCount(header)"> ({{ headerFilterCount(header) }})</template>
25
+ </span>
26
+
27
+ <span v-if="filterable && isHeaderFiltered(header)" class="filter-indicator" title="Filtered" aria-hidden="true">⧩</span>
28
+
29
+ <span v-if="header.sortable" class="sort-indicator">
30
+ <span v-if="sortBy === header.key">
31
+ <span v-if="sortDir === 'asc'">▲</span>
32
+ <span v-else-if="sortDir === 'desc'">▼</span>
33
+ </span>
34
+ </span>
35
+ </th>
36
+ <th
37
+ v-if="showColumnOptionsButton"
38
+ class="actions table-column-options-header"
39
+ >
40
+ <div class="table-column-options-header-content">
41
+ <span v-if="activeLayoutLabel" class="table-layout-label">{{ activeLayoutLabel }}</span>
42
+ <span class="actions-menu-trigger">
43
+ <button
44
+ ref="columnOptionsButtonRef"
45
+ type="button"
46
+ aria-haspopup="dialog"
47
+ aria-label="Column options"
48
+ :aria-expanded="columnOptionsOpen ? 'true' : 'false'"
49
+ @click="openColumnOptions"
50
+ >
51
+ <HugeiconsIcon
52
+ :icon="LayoutGridIcon"
53
+ width="0.95em"
54
+ height="0.95em"
55
+ :strokeWidth="2"
56
+ aria-hidden="true"
57
+ />
58
+ </button>
59
+ </span>
60
+ </div>
61
+ </th>
62
+ </tr>
63
+ </thead>
64
+ <tbody v-if="hasRows">
65
+ <tr
66
+ v-for="(row, rowIndex) in displayRows"
67
+ :key="resolveRowKey(row, rowIndex)"
68
+ :class="{ 'row-clickable': rowClickable }"
69
+ @click="onRowClick(row, rowIndex, $event)"
70
+ >
71
+ <td
72
+ v-for="(header, cellIndex) in visibleHeaders"
73
+ :key="header.key || cellIndex"
74
+ :class="[cellClasses(header), header.class]"
75
+ >
76
+ <slot
77
+ v-if="slots[`cell-${header.key}`]"
78
+ :name="`cell-${header.key}`"
79
+ :row="row"
80
+ :value="row[header.key]"
81
+ />
82
+ <slot
83
+ v-else-if="slots.cell"
84
+ name="cell"
85
+ :row="row"
86
+ :value="row[header.key]"
87
+ />
88
+ <span v-else>
89
+ {{ row[header.key] }}
90
+ </span>
91
+ </td>
92
+ <td
93
+ v-if="showColumnOptionsButton"
94
+ class="actions table-column-options-cell"
95
+ aria-hidden="true"
44
96
  />
45
- <span v-else>
46
- {{ row[header.key] }}
47
- </span>
48
- </td>
49
- </tr>
50
- </tbody>
51
- </table>
52
- <div v-if="showPagination" class="padding">
97
+ </tr>
98
+ </tbody>
99
+ </table>
100
+
101
+ <div v-if="isFilteredEmpty" class="table-filtered-empty-state">
102
+ <slot name="filtered-empty" :clear-filters="clearAllFilters">
103
+ <p class="table-filtered-empty-message">No rows match the current filters.</p>
104
+ <button type="button" class="neutral" @click="clearAllFilters">Clear filters</button>
105
+ </slot>
106
+ </div>
107
+ </div>
108
+ <div v-else class="table-empty-state" :class="{ loading: isLoading }">
109
+ <span v-if="isLoading">Loading…</span>
110
+ <slot v-else name="empty">
111
+ <div class="table-empty">There are 0 items to show</div>
112
+ </slot>
113
+ </div>
114
+ <div v-if="showPagination && showTableChrome" class="padding">
53
115
  <Pagination :total="totalCount" v-model:page="page" v-model:page-size="pageSize" />
54
116
  </div>
55
117
 
@@ -65,25 +127,72 @@
65
127
  @clear="onFilterClear"
66
128
  @cancel="onFilterCancel"
67
129
  />
130
+
131
+ <TableColumnOptionsPopover
132
+ v-if="showColumnOptionsButton"
133
+ v-model:open="columnOptionsOpen"
134
+ :headers="props.headers"
135
+ :column-keys="resolvedColumnOrder"
136
+ :visible-keys="visibleColumnKeys"
137
+ :column-priorities="resolvedColumnPriorities"
138
+ :filters="activeFilters"
139
+ :rows="props.data"
140
+ :table-id="props.tableId"
141
+ :layout-presets-enabled="layoutPresetsAvailable"
142
+ :presets="savedPresets"
143
+ :default-preset-id="defaultPresetId"
144
+ :save-layout-preset="onSavePreset"
145
+ :load-layout-preset="onLoadPreset"
146
+ :delete-layout-preset="onDeletePreset"
147
+ :set-default-layout-preset="onSetDefaultPreset"
148
+ :load-developer-defaults="loadDeveloperDefaults"
149
+ :anchor-el="columnOptionsButtonRef"
150
+ @apply="onColumnOptionsApply"
151
+ @cancel="onColumnOptionsCancel"
152
+ />
68
153
  </template>
69
154
 
70
155
  <script setup>
71
- import { ref, computed, watch, onMounted, onBeforeUnmount, useSlots } from 'vue'
156
+ import { ref, computed, watch, onMounted, onBeforeUnmount, useSlots, useAttrs } from 'vue'
157
+ import { HugeiconsIcon } from '@hugeicons/vue'
158
+ import { LayoutGridIcon } from '@hugeicons/core-free-icons'
72
159
  import Pagination from './Pagination.vue'
73
160
  import TableColumnFilterPopover from './TableColumnFilterPopover.vue'
161
+ import TableColumnOptionsPopover from './TableColumnOptionsPopover.vue'
74
162
  import { useLongPress } from '../composables/useLongPress.js'
75
163
  import {
76
164
  applyFilters,
77
165
  buildSelectOptions,
166
+ cloneColumnFilterEntries,
78
167
  cloneFilters,
79
- isFilterActive,
168
+ countActiveFilters,
169
+ isColumnFiltered,
170
+ isFilterEntryActive,
171
+ normalizeColumnFilterEntries,
80
172
  sortRows,
81
173
  } from '../composables/tableFilters.js'
174
+ import {
175
+ applyFilterQuery,
176
+ buildFilterQuery,
177
+ isFilterQueryActive,
178
+ } from '../composables/tableFilterQuery.js'
179
+ import {
180
+ getDefaultPreset,
181
+ getPresetById,
182
+ listPresets,
183
+ savePreset,
184
+ deletePreset,
185
+ setDefaultPreset,
186
+ sanitizePresetState,
187
+ layoutStatesEqual,
188
+ } from '../composables/tableColumnPresets.js'
189
+
190
+ const DEFAULT_PAGE_SIZE = 10
82
191
 
83
192
  const sortBy = ref(null)
84
193
  const sortDir = ref('asc')
85
194
  const page = ref(1)
86
- const pageSize = ref(10)
195
+ const pageSize = ref(DEFAULT_PAGE_SIZE)
87
196
 
88
197
  const props = defineProps({
89
198
  headers: {
@@ -118,6 +227,51 @@ const props = defineProps({
118
227
  type: Number,
119
228
  default: 300,
120
229
  },
230
+ horizontalScroll: {
231
+ type: Boolean,
232
+ default: false,
233
+ },
234
+ stickyCols: {
235
+ type: Number,
236
+ default: null,
237
+ validator: (value) => value === null || (value >= 1 && value <= 3),
238
+ },
239
+ columnOptions: {
240
+ type: Boolean,
241
+ default: true,
242
+ },
243
+ columnVisibility: {
244
+ type: Object,
245
+ default: undefined,
246
+ },
247
+ columnOrder: {
248
+ type: Array,
249
+ default: undefined,
250
+ },
251
+ columnPriorities: {
252
+ type: Object,
253
+ default: undefined,
254
+ },
255
+ tableId: {
256
+ type: String,
257
+ default: '',
258
+ },
259
+ loadSavedLayout: {
260
+ type: Boolean,
261
+ default: true,
262
+ },
263
+ defaultColumnVisibility: {
264
+ type: Object,
265
+ default: undefined,
266
+ },
267
+ rowKey: {
268
+ type: [String, Function],
269
+ default: 'id',
270
+ },
271
+ layoutPresets: {
272
+ type: Object,
273
+ default: null,
274
+ },
121
275
  })
122
276
 
123
277
  const emit = defineEmits([
@@ -125,9 +279,19 @@ const emit = defineEmits([
125
279
  'filter-change',
126
280
  'query-change',
127
281
  'fetch-error',
282
+ 'row-click',
283
+ 'update:columnVisibility',
284
+ 'column-visibility-change',
285
+ 'update:columnOrder',
286
+ 'column-order-change',
287
+ 'update:columnPriorities',
288
+ 'column-priorities-change',
128
289
  ])
129
290
 
291
+ const attrs = useAttrs()
130
292
  const slots = useSlots()
293
+
294
+ const rowClickable = computed(() => Boolean(attrs.onRowClick))
131
295
  const internalFilters = ref({})
132
296
  const remoteRows = ref([])
133
297
  const remoteTotal = ref(0)
@@ -137,12 +301,27 @@ const fetchAbortController = ref(null)
137
301
  const fetchDebounceTimer = ref(null)
138
302
 
139
303
  const filterPopoverOpen = ref(false)
304
+ const columnOptionsOpen = ref(false)
305
+ const columnOptionsButtonRef = ref(null)
140
306
  const activeFilterHeader = ref(null)
141
307
  const activeFilterAnchor = ref(null)
142
- const activeFilterValue = ref(null)
308
+ const activeFilterValue = ref([])
309
+ const internalHiddenColumnKeys = ref([])
310
+ const internalColumnOrder = ref([])
311
+ const internalColumnPriorities = ref({})
312
+ const savedPresets = ref([])
313
+ const defaultPresetId = ref(null)
314
+ const activeLayoutPresetId = ref(null)
143
315
 
144
316
  const isRemote = computed(() => typeof props.fetchRows === 'function')
145
317
 
318
+ const layoutPresetsAvailable = computed(() => {
319
+ if (isRemote.value) {
320
+ return props.layoutPresets != null
321
+ }
322
+ return Boolean(props.tableId)
323
+ })
324
+
146
325
  const activeFilters = computed(() => {
147
326
  if (!props.filterable) {
148
327
  return {}
@@ -150,15 +329,122 @@ const activeFilters = computed(() => {
150
329
  return props.filters !== undefined ? props.filters : internalFilters.value
151
330
  })
152
331
 
332
+ const activeFilterQuery = computed(() =>
333
+ buildFilterQuery(activeFilters.value, props.headers, props.data),
334
+ )
335
+
153
336
  const isLoading = computed(() => props.loading || internalLoading.value)
154
337
 
155
- const visibleHeaders = computed(() => props.headers.filter((header) => !header.hidden))
338
+ const configurableHeaders = computed(() =>
339
+ props.headers.filter((header) => header?.key && header.hideable !== false && !header.hidden),
340
+ )
341
+
342
+ const manageableColumnKeys = computed(() =>
343
+ props.headers
344
+ .filter((header) => header?.key && !header.hidden)
345
+ .map((header) => header.key),
346
+ )
347
+
348
+ const resolvedColumnOrder = computed(() => {
349
+ const keys = manageableColumnKeys.value
350
+ const order = props.columnOrder !== undefined ? props.columnOrder : internalColumnOrder.value
351
+ const fromOrder = order.filter((key) => keys.includes(key))
352
+ const missing = keys.filter((key) => !fromOrder.includes(key))
353
+ return [...fromOrder, ...missing]
354
+ })
355
+
356
+ const orderedHeaders = computed(() => {
357
+ const headerByKey = new Map(
358
+ props.headers
359
+ .filter((header) => header?.key)
360
+ .map((header) => [header.key, header]),
361
+ )
362
+ return resolvedColumnOrder.value
363
+ .map((key) => headerByKey.get(key))
364
+ .filter(Boolean)
365
+ })
366
+
367
+ const showColumnOptionsButton = computed(() =>
368
+ props.columnOptions && manageableColumnKeys.value.length > 0,
369
+ )
370
+
371
+ function isHeaderHidden(header) {
372
+ if (header.hidden) {
373
+ return true
374
+ }
375
+ if (props.columnVisibility !== undefined) {
376
+ return props.columnVisibility[header.key] === false
377
+ }
378
+ return internalHiddenColumnKeys.value.includes(header.key)
379
+ }
380
+
381
+ const visibleHeaders = computed(() => orderedHeaders.value.filter((header) => !isHeaderHidden(header)))
382
+
383
+ const visibleColumnKeys = computed(() =>
384
+ orderedHeaders.value
385
+ .filter((header) => !isHeaderHidden(header))
386
+ .map((header) => header.key),
387
+ )
388
+
389
+ function defaultColumnPriority(header) {
390
+ return isValidColPriority(header?.colPriority) ? header.colPriority : null
391
+ }
392
+
393
+ function resolvedColumnPriority(header) {
394
+ if (!header?.key) {
395
+ return null
396
+ }
397
+
398
+ const overrides = props.columnPriorities !== undefined
399
+ ? props.columnPriorities
400
+ : internalColumnPriorities.value
401
+
402
+ if (Object.prototype.hasOwnProperty.call(overrides, header.key)) {
403
+ const override = overrides[header.key]
404
+ return isValidColPriority(override) ? override : null
405
+ }
406
+
407
+ return defaultColumnPriority(header)
408
+ }
409
+
410
+ const resolvedColumnPriorities = computed(() =>
411
+ Object.fromEntries(
412
+ manageableColumnKeys.value.map((key) => {
413
+ const header = props.headers.find((item) => item.key === key)
414
+ return [key, resolvedColumnPriority(header)]
415
+ }),
416
+ ),
417
+ )
418
+
419
+ const hasColPriorities = computed(() =>
420
+ orderedHeaders.value.some((header) => isValidColPriority(resolvedColumnPriority(header))),
421
+ )
422
+
423
+ const hasHorizontalScroll = computed(() =>
424
+ props.horizontalScroll || (props.stickyCols >= 1 && props.stickyCols <= 3),
425
+ )
426
+
427
+ const needsTableWrapper = computed(() => hasColPriorities.value || hasHorizontalScroll.value)
428
+
429
+ const tableWrapperClasses = computed(() => {
430
+ const classes = []
431
+ if (hasColPriorities.value) {
432
+ classes.push('responsive-cols')
433
+ }
434
+ if (hasHorizontalScroll.value) {
435
+ classes.push('table-scroll')
436
+ if (props.stickyCols >= 1 && props.stickyCols <= 3) {
437
+ classes.push(`sticky-cols-${props.stickyCols}`)
438
+ }
439
+ }
440
+ return classes
441
+ })
156
442
 
157
443
  const filteredItems = computed(() => {
158
444
  if (isRemote.value) {
159
445
  return remoteRows.value
160
446
  }
161
- return applyFilters(props.data, props.headers, activeFilters.value)
447
+ return applyFilterQuery(props.data, props.headers, activeFilterQuery.value)
162
448
  })
163
449
 
164
450
  const sortedItems = computed(() => {
@@ -177,6 +463,40 @@ const displayRows = computed(() => {
177
463
  return sortedItems.value.slice(start, start + pageSize.value)
178
464
  })
179
465
 
466
+ const hasRows = computed(() => displayRows.value.length > 0)
467
+
468
+ const hasActiveFilters = computed(() =>
469
+ isFilterQueryActive(activeFilterQuery.value, props.headers, props.data),
470
+ )
471
+
472
+ const hasSourceData = computed(() => {
473
+ if (isRemote.value) {
474
+ return remoteTotal.value > 0 || hasActiveFilters.value
475
+ }
476
+ return props.data.length > 0
477
+ })
478
+
479
+ const isFilteredEmpty = computed(() => {
480
+ if (isLoading.value || hasRows.value || !hasActiveFilters.value) {
481
+ return false
482
+ }
483
+ return hasSourceData.value
484
+ })
485
+
486
+ const showTableChrome = computed(() => hasRows.value || isFilteredEmpty.value)
487
+
488
+ watch(showTableChrome, (visible) => {
489
+ if (visible) {
490
+ return
491
+ }
492
+
493
+ filterPopoverOpen.value = false
494
+ columnOptionsOpen.value = false
495
+ activeFilterHeader.value = null
496
+ activeFilterAnchor.value = null
497
+ activeFilterValue.value = []
498
+ })
499
+
180
500
  const totalCount = computed(() => {
181
501
  if (isRemote.value) {
182
502
  return remoteTotal.value
@@ -191,17 +511,39 @@ const activeSelectOptions = computed(() => {
191
511
  return buildSelectOptions(props.data, activeFilterHeader.value)
192
512
  })
193
513
 
194
- const queryParams = computed(() => ({
195
- page: page.value,
196
- pageSize: pageSize.value,
197
- sortBy: sortBy.value,
198
- sortDir: sortDir.value,
199
- filters: cloneFilters(activeFilters.value),
200
- }))
514
+ const queryParams = computed(() => {
515
+ const params = {
516
+ page: page.value,
517
+ pageSize: pageSize.value,
518
+ sortBy: sortBy.value,
519
+ sortDir: sortDir.value,
520
+ filterQuery: activeFilterQuery.value,
521
+ }
522
+
523
+ // Remote tables use filterQuery only; see docs/filter-query-v1.md.
524
+ if (!isRemote.value) {
525
+ params.filters = cloneFilters(activeFilters.value)
526
+ }
527
+
528
+ return params
529
+ })
530
+
531
+ function isValidColPriority(priority) {
532
+ return Number.isInteger(priority) && priority >= 1 && priority <= 5
533
+ }
534
+
535
+ function colPriorityClass(priority) {
536
+ return isValidColPriority(priority) ? `col-priority-${priority}` : null
537
+ }
538
+
539
+ function headerFilterCount(header) {
540
+ if (!isHeaderFilterable(header)) {
541
+ return 0
542
+ }
201
543
 
202
- function slotFor(key) {
203
- const named = slots[`cell-${key}`]
204
- return named || slots.cell || null
544
+ return normalizeColumnFilterEntries(activeFilters.value?.[header.key], header, props.data)
545
+ .filter((entry) => isFilterEntryActive(entry))
546
+ .length
205
547
  }
206
548
 
207
549
  function isHeaderFilterable(header) {
@@ -209,15 +551,45 @@ function isHeaderFilterable(header) {
209
551
  }
210
552
 
211
553
  function isHeaderFiltered(header) {
212
- return isFilterActive(activeFilters.value?.[header.key])
554
+ return isColumnFiltered(activeFilters.value?.[header.key], header, props.data)
213
555
  }
214
556
 
215
557
  function headerClasses(header) {
558
+ const priority = resolvedColumnPriority(header)
216
559
  return {
217
560
  sortable: header.sortable,
218
561
  filterable: isHeaderFilterable(header),
219
562
  filtered: isHeaderFiltered(header),
563
+ 'menu-open': filterPopoverOpen.value && activeFilterHeader.value?.key === header.key,
564
+ [colPriorityClass(priority)]: isValidColPriority(priority),
565
+ }
566
+ }
567
+
568
+ function cellClasses(header) {
569
+ const priority = resolvedColumnPriority(header)
570
+ return {
571
+ hidden: header.hidden,
572
+ [colPriorityClass(priority)]: isValidColPriority(priority),
573
+ }
574
+ }
575
+
576
+ function resolveRowKey(row, index) {
577
+ if (typeof props.rowKey === 'function') {
578
+ const key = props.rowKey(row, index)
579
+ if (key !== null && key !== undefined && key !== '') {
580
+ return key
581
+ }
582
+ return index
583
+ }
584
+
585
+ if (typeof props.rowKey === 'string' && props.rowKey) {
586
+ const key = row?.[props.rowKey]
587
+ if (key !== null && key !== undefined && key !== '') {
588
+ return key
589
+ }
220
590
  }
591
+
592
+ return index
221
593
  }
222
594
 
223
595
  function setFilters(nextFilters) {
@@ -229,6 +601,15 @@ function setFilters(nextFilters) {
229
601
  emit('filter-change', cloned)
230
602
  }
231
603
 
604
+ function clearAllFilters() {
605
+ setFilters({})
606
+ page.value = 1
607
+ if (isRemote.value) {
608
+ scheduleFetchRows()
609
+ }
610
+ emitQueryChange()
611
+ }
612
+
232
613
  function emitQueryChange() {
233
614
  emit('query-change', queryParams.value)
234
615
  }
@@ -254,6 +635,20 @@ function onHeaderClick(header, event) {
254
635
  toggleSort(header)
255
636
  }
256
637
 
638
+ function filteredRowIndex(displayIndex) {
639
+ if (props.showPagination) {
640
+ return (page.value - 1) * pageSize.value + displayIndex
641
+ }
642
+ return displayIndex
643
+ }
644
+
645
+ function onRowClick(row, displayIndex, event) {
646
+ if (event.target.closest('a, button, input, select, textarea, [data-row-click-ignore]')) {
647
+ return
648
+ }
649
+ emit('row-click', { row, index: filteredRowIndex(displayIndex) })
650
+ }
651
+
257
652
  function openFilter(header, event) {
258
653
  if (!isHeaderFilterable(header)) {
259
654
  return
@@ -261,7 +656,11 @@ function openFilter(header, event) {
261
656
 
262
657
  activeFilterHeader.value = header
263
658
  activeFilterAnchor.value = event.currentTarget
264
- activeFilterValue.value = activeFilters.value?.[header.key] ?? null
659
+ activeFilterValue.value = cloneColumnFilterEntries(
660
+ activeFilters.value?.[header.key],
661
+ header,
662
+ props.data,
663
+ )
265
664
  filterPopoverOpen.value = true
266
665
  }
267
666
 
@@ -282,14 +681,14 @@ function onHeaderPointerDown(header, event) {
282
681
  longPress.onPointerDown(event)
283
682
  }
284
683
 
285
- function onFilterApply(filter) {
684
+ function onFilterApply(filterEntries) {
286
685
  if (!activeFilterHeader.value) {
287
686
  return
288
687
  }
289
688
 
290
689
  const nextFilters = cloneFilters(activeFilters.value)
291
- if (filter) {
292
- nextFilters[activeFilterHeader.value.key] = filter
690
+ if (filterEntries?.length) {
691
+ nextFilters[activeFilterHeader.value.key] = cloneColumnFilterEntries(filterEntries)
293
692
  } else {
294
693
  delete nextFilters[activeFilterHeader.value.key]
295
694
  }
@@ -314,9 +713,431 @@ function onFilterClear() {
314
713
  function onFilterCancel() {
315
714
  activeFilterHeader.value = null
316
715
  activeFilterAnchor.value = null
317
- activeFilterValue.value = null
716
+ activeFilterValue.value = []
717
+ }
718
+
719
+ function buildColumnVisibility(visibleKeys) {
720
+ const visibleKeySet = new Set(visibleKeys)
721
+ return Object.fromEntries(
722
+ configurableHeaders.value.map((header) => [header.key, visibleKeySet.has(header.key)]),
723
+ )
724
+ }
725
+
726
+ function setColumnVisibility(visibleKeys) {
727
+ const visibleKeySet = new Set(visibleKeys)
728
+ const hiddenKeys = configurableHeaders.value
729
+ .map((header) => header.key)
730
+ .filter((key) => !visibleKeySet.has(key))
731
+
732
+ if (props.columnVisibility === undefined) {
733
+ internalHiddenColumnKeys.value = hiddenKeys
734
+ }
735
+
736
+ const nextVisibility = buildColumnVisibility(visibleKeys)
737
+ emit('update:columnVisibility', nextVisibility)
738
+ emit('column-visibility-change', nextVisibility)
739
+ }
740
+
741
+ function syncInternalColumnOrder() {
742
+ if (props.columnOrder !== undefined) {
743
+ return
744
+ }
745
+
746
+ const keys = manageableColumnKeys.value
747
+ const merged = [
748
+ ...internalColumnOrder.value.filter((key) => keys.includes(key)),
749
+ ...keys.filter((key) => !internalColumnOrder.value.includes(key)),
750
+ ]
751
+ internalColumnOrder.value = merged
752
+ }
753
+
754
+ function setColumnOrder(order) {
755
+ const keys = manageableColumnKeys.value
756
+ const nextOrder = [
757
+ ...order.filter((key) => keys.includes(key)),
758
+ ...keys.filter((key) => !order.includes(key)),
759
+ ]
760
+
761
+ if (props.columnOrder === undefined) {
762
+ internalColumnOrder.value = nextOrder
763
+ }
764
+
765
+ emit('update:columnOrder', nextOrder)
766
+ emit('column-order-change', nextOrder)
767
+ }
768
+
769
+ function setColumnPriorities(overrides) {
770
+ const cloned = { ...overrides }
771
+
772
+ if (props.columnPriorities === undefined) {
773
+ internalColumnPriorities.value = cloned
774
+ }
775
+
776
+ emit('update:columnPriorities', cloned)
777
+ emit('column-priorities-change', cloned)
778
+ }
779
+
780
+ function openColumnOptions() {
781
+ columnOptionsOpen.value = true
782
+ void refreshSavedPresets()
783
+ }
784
+
785
+ function onColumnOptionsApply({ order, visibleKeys, priorities }) {
786
+ setColumnOrder(order)
787
+ setColumnVisibility(visibleKeys)
788
+ setColumnPriorities(priorities)
789
+ updateActiveLayoutLabel()
790
+ }
791
+
792
+ function onColumnOptionsCancel() {
793
+ columnOptionsOpen.value = false
794
+ }
795
+
796
+ async function refreshSavedPresets() {
797
+ if (isRemote.value) {
798
+ if (!props.layoutPresets || typeof props.layoutPresets.list !== 'function') {
799
+ savedPresets.value = []
800
+ defaultPresetId.value = null
801
+ return
802
+ }
803
+
804
+ try {
805
+ const result = await props.layoutPresets.list()
806
+ savedPresets.value = Array.isArray(result?.presets) ? result.presets : []
807
+ defaultPresetId.value = result?.defaultPresetId ?? null
808
+ } catch {
809
+ savedPresets.value = []
810
+ defaultPresetId.value = null
811
+ }
812
+ return
813
+ }
814
+
815
+ if (!props.tableId) {
816
+ savedPresets.value = []
817
+ defaultPresetId.value = null
818
+ return
819
+ }
820
+
821
+ const { presets, defaultPresetId: nextDefaultPresetId } = listPresets(props.tableId)
822
+ savedPresets.value = presets
823
+ defaultPresetId.value = nextDefaultPresetId
824
+ }
825
+
826
+ function findSavedPreset(presetId) {
827
+ return savedPresets.value.find((preset) => preset.id === presetId)
828
+ ?? (props.tableId ? getPresetById(props.tableId, presetId) : null)
829
+ }
830
+
831
+ function getCurrentLayoutState() {
832
+ const priorityOverrides = props.columnPriorities !== undefined
833
+ ? { ...props.columnPriorities }
834
+ : { ...internalColumnPriorities.value }
835
+
836
+ return {
837
+ columnOrder: [...resolvedColumnOrder.value],
838
+ columnVisibility: buildColumnVisibility(visibleColumnKeys.value),
839
+ columnPriorities: priorityOverrides,
840
+ filters: cloneFilters(activeFilters.value),
841
+ sortBy: sortBy.value,
842
+ sortDir: sortDir.value,
843
+ pageSize: pageSize.value,
844
+ }
845
+ }
846
+
847
+ function applyPresetState(state) {
848
+ const sanitized = sanitizePresetState(state, props.headers, props.data)
849
+ if (!sanitized) {
850
+ return
851
+ }
852
+
853
+ if (sanitized.columnOrder?.length) {
854
+ setColumnOrder(sanitized.columnOrder)
855
+ }
856
+
857
+ if (sanitized.columnVisibility) {
858
+ const visibleKeys = manageableColumnKeys.value.filter(
859
+ (key) => sanitized.columnVisibility[key] !== false,
860
+ )
861
+ const fallbackKeys = configurableHeaders.value
862
+ .filter((header) => header.hideable === false)
863
+ .map((header) => header.key)
864
+ const nextVisibleKeys = visibleKeys.length > 0
865
+ ? visibleKeys
866
+ : (fallbackKeys.length > 0 ? fallbackKeys : manageableColumnKeys.value.slice(0, 1))
867
+
868
+ setColumnVisibility(nextVisibleKeys)
869
+ }
870
+
871
+ setColumnPriorities(sanitized.columnPriorities ?? {})
872
+ setFilters(sanitized.filters ?? {})
873
+
874
+ if (sanitized.sortBy) {
875
+ sortBy.value = sanitized.sortBy
876
+ sortDir.value = sanitized.sortDir === 'desc' ? 'desc' : 'asc'
877
+ } else {
878
+ sortBy.value = null
879
+ sortDir.value = 'asc'
880
+ }
881
+
882
+ if (sanitized.pageSize) {
883
+ pageSize.value = sanitized.pageSize
884
+ }
885
+
886
+ page.value = 1
887
+ if (isRemote.value) {
888
+ scheduleFetchRows()
889
+ }
890
+ emitQueryChange()
891
+ updateActiveLayoutLabel()
892
+ }
893
+
894
+ async function onSavePreset({ name, setAsDefault }) {
895
+ const state = getCurrentLayoutState()
896
+
897
+ if (isRemote.value) {
898
+ if (!props.layoutPresets || typeof props.layoutPresets.save !== 'function') {
899
+ return { ok: false, error: 'Remote layout save is unavailable.' }
900
+ }
901
+
902
+ try {
903
+ const result = await props.layoutPresets.save({ name, state, setAsDefault })
904
+ if (result?.ok) {
905
+ await refreshSavedPresets()
906
+ if (result.preset?.id) {
907
+ activeLayoutPresetId.value = result.preset.id
908
+ }
909
+ updateActiveLayoutLabel()
910
+ }
911
+ return result ?? { ok: false, error: 'Unable to save layout.' }
912
+ } catch {
913
+ return { ok: false, error: 'Unable to save layout.' }
914
+ }
915
+ }
916
+
917
+ if (!props.tableId) {
918
+ return { ok: false, error: 'Table identity is required.' }
919
+ }
920
+
921
+ const result = savePreset(props.tableId, {
922
+ name,
923
+ state,
924
+ setAsDefault,
925
+ })
926
+ if (result.ok) {
927
+ refreshSavedPresets()
928
+ if (result.preset?.id) {
929
+ activeLayoutPresetId.value = result.preset.id
930
+ }
931
+ updateActiveLayoutLabel()
932
+ }
933
+ return result
934
+ }
935
+
936
+ async function onLoadPreset(presetId) {
937
+ if (isRemote.value) {
938
+ if (!props.layoutPresets || typeof props.layoutPresets.load !== 'function') {
939
+ return { ok: false, error: 'Remote layout load is unavailable.' }
940
+ }
941
+
942
+ try {
943
+ const result = await props.layoutPresets.load(presetId)
944
+ if (!result?.ok || !result.preset?.state) {
945
+ return result ?? { ok: false, error: 'Preset not found.' }
946
+ }
947
+
948
+ applyPresetState(result.preset.state)
949
+ activeLayoutPresetId.value = presetId
950
+ updateActiveLayoutLabel()
951
+ return { ok: true }
952
+ } catch {
953
+ return { ok: false, error: 'Unable to load layout.' }
954
+ }
955
+ }
956
+
957
+ if (!props.tableId) {
958
+ return { ok: false, error: 'Table identity is required.' }
959
+ }
960
+
961
+ const preset = getPresetById(props.tableId, presetId)
962
+ if (!preset) {
963
+ return { ok: false, error: 'Preset not found.' }
964
+ }
965
+
966
+ applyPresetState(preset.state)
967
+ activeLayoutPresetId.value = presetId
968
+ updateActiveLayoutLabel()
969
+ return { ok: true }
970
+ }
971
+
972
+ async function onDeletePreset(presetId) {
973
+ if (isRemote.value) {
974
+ if (!props.layoutPresets || typeof props.layoutPresets.delete !== 'function') {
975
+ return { ok: false, error: 'Remote layout delete is unavailable.' }
976
+ }
977
+
978
+ try {
979
+ const result = await props.layoutPresets.delete(presetId)
980
+ if (result?.ok) {
981
+ await refreshSavedPresets()
982
+ if (activeLayoutPresetId.value === presetId) {
983
+ activeLayoutPresetId.value = null
984
+ }
985
+ updateActiveLayoutLabel()
986
+ }
987
+ return result ?? { ok: false, error: 'Unable to delete layout.' }
988
+ } catch {
989
+ return { ok: false, error: 'Unable to delete layout.' }
990
+ }
991
+ }
992
+
993
+ if (!props.tableId) {
994
+ return { ok: false, error: 'Table identity is required.' }
995
+ }
996
+
997
+ const result = deletePreset(props.tableId, presetId)
998
+ if (result.ok) {
999
+ refreshSavedPresets()
1000
+ if (activeLayoutPresetId.value === presetId) {
1001
+ activeLayoutPresetId.value = null
1002
+ }
1003
+ updateActiveLayoutLabel()
1004
+ }
1005
+ return result
1006
+ }
1007
+
1008
+ async function onSetDefaultPreset(presetId) {
1009
+ if (isRemote.value) {
1010
+ if (!props.layoutPresets || typeof props.layoutPresets.setDefault !== 'function') {
1011
+ return { ok: false, error: 'Unable to update default layout.' }
1012
+ }
1013
+
1014
+ try {
1015
+ const result = await props.layoutPresets.setDefault(presetId)
1016
+ if (result?.ok) {
1017
+ await refreshSavedPresets()
1018
+ }
1019
+ return result ?? { ok: false, error: 'Unable to update default layout.' }
1020
+ } catch {
1021
+ return { ok: false, error: 'Unable to update default layout.' }
1022
+ }
1023
+ }
1024
+
1025
+ if (!props.tableId) {
1026
+ return { ok: false, error: 'Table identity is required.' }
1027
+ }
1028
+
1029
+ const result = setDefaultPreset(props.tableId, presetId)
1030
+ if (result.ok) {
1031
+ refreshSavedPresets()
1032
+ }
1033
+ return result
1034
+ }
1035
+
1036
+ function getDeveloperDefaultColumnOrder() {
1037
+ return props.headers
1038
+ .filter((header) => header?.key && !header.hidden)
1039
+ .map((header) => header.key)
1040
+ }
1041
+
1042
+ function getDeveloperDefaultVisibleKeys() {
1043
+ const keys = manageableColumnKeys.value
1044
+ if (props.defaultColumnVisibility !== undefined) {
1045
+ return keys.filter((key) => props.defaultColumnVisibility[key] !== false)
1046
+ }
1047
+ return [...keys]
1048
+ }
1049
+
1050
+ function getDeveloperDefaultLayoutState() {
1051
+ return {
1052
+ columnOrder: getDeveloperDefaultColumnOrder(),
1053
+ columnVisibility: buildColumnVisibility(getDeveloperDefaultVisibleKeys()),
1054
+ columnPriorities: {},
1055
+ filters: {},
1056
+ sortBy: null,
1057
+ sortDir: 'asc',
1058
+ pageSize: DEFAULT_PAGE_SIZE,
1059
+ }
1060
+ }
1061
+
1062
+ function updateActiveLayoutLabel() {
1063
+ const current = getCurrentLayoutState()
1064
+ const defaults = getDeveloperDefaultLayoutState()
1065
+
1066
+ if (layoutStatesEqual(current, defaults, props.headers, props.data)) {
1067
+ activeLayoutPresetId.value = null
1068
+ return
1069
+ }
1070
+
1071
+ if (activeLayoutPresetId.value && layoutPresetsAvailable.value) {
1072
+ const preset = findSavedPreset(activeLayoutPresetId.value)
1073
+ if (preset && layoutStatesEqual(current, preset.state, props.headers, props.data)) {
1074
+ return
1075
+ }
1076
+ }
1077
+
1078
+ if (layoutPresetsAvailable.value) {
1079
+ const matchingPreset = savedPresets.value.find((preset) =>
1080
+ layoutStatesEqual(current, preset.state, props.headers, props.data))
1081
+ if (matchingPreset) {
1082
+ activeLayoutPresetId.value = matchingPreset.id
1083
+ return
1084
+ }
1085
+ }
1086
+
1087
+ activeLayoutPresetId.value = null
1088
+ }
1089
+
1090
+ const activeLayoutLabel = computed(() => {
1091
+ const current = getCurrentLayoutState()
1092
+ const defaults = getDeveloperDefaultLayoutState()
1093
+
1094
+ if (layoutStatesEqual(current, defaults, props.headers, props.data)) {
1095
+ return ''
1096
+ }
1097
+
1098
+ if (activeLayoutPresetId.value && layoutPresetsAvailable.value) {
1099
+ const preset = findSavedPreset(activeLayoutPresetId.value)
1100
+ if (preset?.name) {
1101
+ return preset.name
1102
+ }
1103
+ }
1104
+
1105
+ return 'Custom'
1106
+ })
1107
+
1108
+ function loadDeveloperDefaults() {
1109
+ setFilters({})
1110
+ sortBy.value = null
1111
+ sortDir.value = 'asc'
1112
+ setColumnOrder(getDeveloperDefaultColumnOrder())
1113
+ setColumnPriorities({})
1114
+
1115
+ const visibleKeys = getDeveloperDefaultVisibleKeys()
1116
+ const fixedVisibleKeys = configurableHeaders.value
1117
+ .filter((header) => header.hideable === false)
1118
+ .map((header) => header.key)
1119
+ const nextVisibleKeys = visibleKeys.length > 0
1120
+ ? visibleKeys
1121
+ : (fixedVisibleKeys.length > 0 ? fixedVisibleKeys : manageableColumnKeys.value.slice(0, 1))
1122
+
1123
+ setColumnVisibility(nextVisibleKeys)
1124
+ activeLayoutPresetId.value = null
1125
+ pageSize.value = DEFAULT_PAGE_SIZE
1126
+ page.value = 1
1127
+ if (isRemote.value) {
1128
+ scheduleFetchRows()
1129
+ }
1130
+ emitQueryChange()
1131
+ updateActiveLayoutLabel()
1132
+ return { ok: true }
318
1133
  }
319
1134
 
1135
+ watch(() => props.columnOptions, (enabled) => {
1136
+ if (!enabled) {
1137
+ columnOptionsOpen.value = false
1138
+ }
1139
+ })
1140
+
320
1141
  async function runFetchRows() {
321
1142
  if (!isRemote.value) {
322
1143
  return
@@ -421,13 +1242,106 @@ watch(
421
1242
  },
422
1243
  )
423
1244
 
1245
+ watch(
1246
+ () => props.columnVisibility,
1247
+ (nextVisibility) => {
1248
+ if (nextVisibility === undefined) {
1249
+ return
1250
+ }
1251
+ internalHiddenColumnKeys.value = configurableHeaders.value
1252
+ .filter((header) => nextVisibility[header.key] === false)
1253
+ .map((header) => header.key)
1254
+ },
1255
+ { deep: true },
1256
+ )
1257
+
1258
+ watch(
1259
+ () => props.columnOrder,
1260
+ (nextOrder) => {
1261
+ if (nextOrder === undefined) {
1262
+ return
1263
+ }
1264
+ internalColumnOrder.value = [...nextOrder]
1265
+ },
1266
+ { deep: true },
1267
+ )
1268
+
1269
+ watch(
1270
+ () => props.columnPriorities,
1271
+ (nextPriorities) => {
1272
+ if (nextPriorities === undefined) {
1273
+ return
1274
+ }
1275
+ internalColumnPriorities.value = { ...nextPriorities }
1276
+ },
1277
+ { deep: true },
1278
+ )
1279
+
1280
+ watch(
1281
+ manageableColumnKeys,
1282
+ () => {
1283
+ syncInternalColumnOrder()
1284
+ },
1285
+ { immediate: true },
1286
+ )
1287
+
1288
+ watch(
1289
+ () => [props.tableId, props.layoutPresets],
1290
+ () => {
1291
+ void refreshSavedPresets()
1292
+ updateActiveLayoutLabel()
1293
+ },
1294
+ { immediate: true },
1295
+ )
1296
+
1297
+ watch(
1298
+ [
1299
+ () => resolvedColumnOrder.value,
1300
+ () => visibleColumnKeys.value,
1301
+ () => props.columnPriorities,
1302
+ internalColumnPriorities,
1303
+ activeFilters,
1304
+ sortBy,
1305
+ sortDir,
1306
+ pageSize,
1307
+ () => props.headers,
1308
+ savedPresets,
1309
+ ],
1310
+ () => {
1311
+ updateActiveLayoutLabel()
1312
+ },
1313
+ { deep: true },
1314
+ )
1315
+
424
1316
  watch(sortedItems, () => {
425
1317
  if (!isRemote.value) {
426
1318
  page.value = 1
427
1319
  }
428
1320
  })
429
1321
 
430
- onMounted(() => {
1322
+ onMounted(async () => {
1323
+ if (props.loadSavedLayout) {
1324
+ if (isRemote.value && props.layoutPresets && typeof props.layoutPresets.getDefault === 'function') {
1325
+ try {
1326
+ const result = await props.layoutPresets.getDefault()
1327
+ if (result?.preset?.state) {
1328
+ applyPresetState(result.preset.state)
1329
+ activeLayoutPresetId.value = result.preset.id
1330
+ }
1331
+ } catch {
1332
+ // Ignore default layout load failures in remote mode.
1333
+ }
1334
+ } else if (!isRemote.value && props.tableId) {
1335
+ const preset = getDefaultPreset(props.tableId)
1336
+ if (preset?.state) {
1337
+ applyPresetState(preset.state)
1338
+ activeLayoutPresetId.value = preset.id
1339
+ }
1340
+ }
1341
+ }
1342
+
1343
+ await refreshSavedPresets()
1344
+ updateActiveLayoutLabel()
431
1345
  emitQueryChange()
432
1346
  if (isRemote.value) {
433
1347
  runFetchRows()
@@ -458,6 +1372,11 @@ table thead th.filtered {
458
1372
  color: #0366d6;
459
1373
  }
460
1374
 
1375
+ table thead th.menu-open {
1376
+ background-color: var(--hover-background-color, rgba(3, 102, 214, 0.08));
1377
+ color: var(--text-color, inherit);
1378
+ }
1379
+
461
1380
  table.loading tbody {
462
1381
  opacity: 0.65;
463
1382
  }
@@ -483,4 +1402,125 @@ td:first-child,
483
1402
  th:first-child {
484
1403
  padding-left: 1rem;
485
1404
  }
1405
+
1406
+ tbody tr.row-clickable {
1407
+ cursor: pointer;
1408
+ }
1409
+
1410
+ .table-empty-state {
1411
+ display: flex;
1412
+ align-items: center;
1413
+ justify-content: center;
1414
+ min-height: 6rem;
1415
+ padding: 2rem 1rem;
1416
+ text-align: center;
1417
+ color: var(--text-muted, #666);
1418
+ }
1419
+
1420
+ .table-empty-state.loading {
1421
+ opacity: 0.65;
1422
+ }
1423
+
1424
+ .table-empty {
1425
+ display: flex;
1426
+ align-items: center;
1427
+ justify-content: center;
1428
+ width: 100%;
1429
+ }
1430
+
1431
+ .table-filtered-empty-state {
1432
+ display: flex;
1433
+ flex-direction: column;
1434
+ align-items: center;
1435
+ justify-content: center;
1436
+ gap: 0.75rem;
1437
+ padding: 2rem 1rem;
1438
+ text-align: center;
1439
+ color: var(--text-muted, #666);
1440
+ }
1441
+
1442
+ .table-filtered-empty-message {
1443
+ margin: 0;
1444
+ }
1445
+
1446
+ th.table-column-options-header,
1447
+ td.table-column-options-cell {
1448
+ padding-right: 1rem;
1449
+ text-align: right;
1450
+ vertical-align: middle;
1451
+ }
1452
+
1453
+ th.table-column-options-header {
1454
+ width: auto;
1455
+ min-width: 2.5rem;
1456
+ max-width: none;
1457
+ white-space: nowrap;
1458
+ }
1459
+
1460
+ td.table-column-options-cell {
1461
+ width: 2.5rem;
1462
+ min-width: 2.5rem;
1463
+ max-width: 2.5rem;
1464
+ }
1465
+
1466
+ .table-column-options-header-content {
1467
+ display: inline-flex;
1468
+ align-items: center;
1469
+ justify-content: flex-end;
1470
+ gap: 0.5rem;
1471
+ width: 100%;
1472
+ }
1473
+
1474
+ .table-layout-label {
1475
+ font-size: 0.85rem;
1476
+ color: var(--text-muted, #666);
1477
+ max-width: 10rem;
1478
+ overflow: hidden;
1479
+ text-overflow: ellipsis;
1480
+ white-space: nowrap;
1481
+ }
1482
+
1483
+ th.table-column-options-header .actions-menu-trigger {
1484
+ display: inline-flex;
1485
+ justify-content: flex-end;
1486
+ flex: 0 0 auto;
1487
+ }
1488
+
1489
+ .table-scroll th.table-column-options-header,
1490
+ .table-scroll td.table-column-options-cell {
1491
+ position: sticky;
1492
+ right: 0;
1493
+ z-index: 1;
1494
+ background-color: var(--table-sticky-bg);
1495
+ box-shadow: -1px 0 0 var(--border-color);
1496
+ }
1497
+
1498
+ .table-scroll thead th.table-column-options-header {
1499
+ z-index: 2;
1500
+ }
1501
+
1502
+ .table-scroll table.row-hover tbody tr:hover td.table-column-options-cell,
1503
+ .table-scroll table.row-hover tbody tr:focus-within td.table-column-options-cell {
1504
+ background-color: var(--table-sticky-hover-bg);
1505
+ }
1506
+
1507
+ th.actions .actions-menu-trigger button {
1508
+ display: inline-flex;
1509
+ align-items: center;
1510
+ justify-content: center;
1511
+ font-weight: normal;
1512
+ padding: 0.2em 0.5em;
1513
+ line-height: 1;
1514
+ vertical-align: middle;
1515
+ background: transparent;
1516
+ border: 0;
1517
+ color: inherit;
1518
+ cursor: pointer;
1519
+ }
1520
+
1521
+ th.actions .actions-menu-trigger button:hover,
1522
+ th.actions .actions-menu-trigger button:focus-visible {
1523
+ background-color: var(--hover-background-color);
1524
+ color: var(--text-color, inherit);
1525
+ }
486
1526
  </style>