picocrank 1.25.1 → 1.26.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 (44) hide show
  1. package/eslint.config.js +68 -0
  2. package/package.json +13 -3
  3. package/router.js +74 -0
  4. package/styles/dark-theme.css +21 -9
  5. package/styles/picocrank-extensions.css +131 -2
  6. package/supplemental-themes/catppuccin-latte-frappe/theme.css +12 -22
  7. package/supplemental-themes/dracula-alucard/theme.css +12 -22
  8. package/supplemental-themes/gruvbox-dark-light/theme.css +12 -22
  9. package/supplemental-themes/waffles/theme.css +2 -12
  10. package/vue/components/Breadcrumbs.vue +192 -45
  11. package/vue/components/Calendar.vue +93 -180
  12. package/vue/components/DangerZone.vue +186 -0
  13. package/vue/components/Header.vue +22 -6
  14. package/vue/components/Login.vue +6 -7
  15. package/vue/components/NavigationGrid.vue +26 -204
  16. package/vue/components/NotificationBlock.vue +120 -0
  17. package/vue/components/NotificationPopup.vue +44 -41
  18. package/vue/components/QuickSearch.vue +21 -47
  19. package/vue/components/RadioGroup.vue +4 -4
  20. package/vue/components/ReadOnlyTextArea.vue +1 -1
  21. package/vue/components/SidebarNavLink.vue +3 -3
  22. package/vue/components/Table.vue +109 -81
  23. package/vue/components/TableColumnFilterPopover.vue +7 -17
  24. package/vue/components/TableColumnOptionsPopover.vue +22 -27
  25. package/vue/components/Tabs.vue +366 -82
  26. package/vue/components/TopBar.vue +2 -2
  27. package/vue/composables/tableColumnPriorities.js +33 -0
  28. package/vue/composables/tableFilters.js +1 -1
  29. package/vue/data/navSectionHubs.js +196 -0
  30. package/vue/data/routeBreadcrumbs.js +90 -0
  31. package/vue/examples/Admin.vue +12 -12
  32. package/vue/examples/App.vue +27 -36
  33. package/vue/examples/CalendarExample.vue +32 -22
  34. package/vue/examples/ExampleHeaderThemeSwitcher.vue +1 -1
  35. package/vue/examples/Login.vue +0 -3
  36. package/vue/examples/LoginExample.vue +2 -14
  37. package/vue/examples/NavSectionHub.vue +36 -0
  38. package/vue/examples/PageStructure.vue +6 -3
  39. package/vue/examples/StatusExample.vue +248 -129
  40. package/vue/examples/TableExample.vue +68 -67
  41. package/vue/examples/TabsExample.vue +200 -255
  42. package/vue/examples/ThemePreview.vue +21 -23
  43. package/vue/examples/ViewItem.vue +32 -5
  44. package/vue/examples/Welcome.vue +3 -112
@@ -1,9 +1,34 @@
1
1
  <template>
2
- <div
3
- class="notification-popup notification show"
4
- :class="popup.class"
5
- role="status"
6
- >
2
+ <div class="notification-popup" role="status">
3
+ <NotificationBlock
4
+ :type="notificationType"
5
+ :label="popup.label || ''"
6
+ >
7
+ <span class="notification-popup-content">
8
+ <span class="notification-popup-message">{{ popup.message }}</span>
9
+
10
+ <RouterLink
11
+ v-if="popup.linkTo && !isExternalLink"
12
+ :to="popup.linkTo"
13
+ class="notification-popup-link"
14
+ @click="emit('dismiss', popup.id)"
15
+ >
16
+ {{ popup.linkLabel || 'View' }}
17
+ </RouterLink>
18
+
19
+ <a
20
+ v-else-if="popup.linkTo && isExternalLink"
21
+ :href="popup.linkTo"
22
+ class="notification-popup-link"
23
+ target="_blank"
24
+ rel="noopener noreferrer"
25
+ @click="emit('dismiss', popup.id)"
26
+ >
27
+ {{ popup.linkLabel || 'View' }}
28
+ </a>
29
+ </span>
30
+ </NotificationBlock>
31
+
7
32
  <button
8
33
  type="button"
9
34
  class="notification-popup-dismiss"
@@ -12,33 +37,6 @@
12
37
  >
13
38
  <HugeiconsIcon :icon="Cancel01Icon" width="1em" height="1em" />
14
39
  </button>
15
-
16
- <div class="notification-popup-body">
17
- <p class="notification-popup-message">
18
- <strong v-if="popup.label">{{ popup.label }}:</strong>
19
- {{ popup.message }}
20
- </p>
21
-
22
- <RouterLink
23
- v-if="popup.linkTo && !isExternalLink"
24
- :to="popup.linkTo"
25
- class="notification-popup-link"
26
- @click="emit('dismiss', popup.id)"
27
- >
28
- {{ popup.linkLabel || 'View' }}
29
- </RouterLink>
30
-
31
- <a
32
- v-else-if="popup.linkTo && isExternalLink"
33
- :href="popup.linkTo"
34
- class="notification-popup-link"
35
- target="_blank"
36
- rel="noopener noreferrer"
37
- @click="emit('dismiss', popup.id)"
38
- >
39
- {{ popup.linkLabel || 'View' }}
40
- </a>
41
- </div>
42
40
  </div>
43
41
  </template>
44
42
 
@@ -47,6 +45,7 @@ import { computed } from 'vue'
47
45
  import { RouterLink } from 'vue-router'
48
46
  import { HugeiconsIcon } from '@hugeicons/vue'
49
47
  import { Cancel01Icon } from '@hugeicons/core-free-icons'
48
+ import NotificationBlock from './NotificationBlock.vue'
50
49
 
51
50
  const props = defineProps({
52
51
  popup: {
@@ -57,6 +56,8 @@ const props = defineProps({
57
56
 
58
57
  const emit = defineEmits(['dismiss'])
59
58
 
59
+ const notificationType = computed(() => props.popup.class || 'info')
60
+
60
61
  const isExternalLink = computed(() =>
61
62
  typeof props.popup.linkTo === 'string' && /^https?:\/\//.test(props.popup.linkTo)
62
63
  )
@@ -67,30 +68,32 @@ const isExternalLink = computed(() =>
67
68
  position: relative;
68
69
  width: min(22rem, calc(100vw - 2rem));
69
70
  margin: 0;
70
- padding: 0.75rem 2.25rem 0.75rem 0.75rem;
71
- cursor: default;
72
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
71
+ box-shadow: 0 4px 12px color-mix(in srgb, var(--shadow-color) 60%, transparent);
73
72
  }
74
73
 
75
- .notification-popup-message {
74
+ .notification-popup :deep(.notification) {
76
75
  margin: 0;
77
76
  }
78
77
 
79
- .notification-popup-body {
78
+ .notification-popup-content {
80
79
  display: flex;
81
80
  flex-direction: column;
82
81
  gap: 0.5rem;
83
82
  }
84
83
 
84
+ .notification-popup-message {
85
+ display: block;
86
+ }
87
+
85
88
  .notification-popup-link {
86
- color: inherit;
89
+ color: var(--link-color);
87
90
  font-weight: bold;
88
91
  text-decoration: underline;
89
92
  align-self: flex-start;
90
93
  }
91
94
 
92
95
  .notification-popup-link:hover {
93
- opacity: 0.85;
96
+ text-decoration-thickness: 2px;
94
97
  }
95
98
 
96
99
  .notification-popup-dismiss {
@@ -106,13 +109,13 @@ const isExternalLink = computed(() =>
106
109
  border: 0;
107
110
  border-radius: 0.35rem;
108
111
  background: transparent;
109
- color: inherit;
112
+ color: var(--text-color);
110
113
  cursor: pointer;
111
114
  opacity: 0.75;
112
115
  }
113
116
 
114
117
  .notification-popup-dismiss:hover {
115
118
  opacity: 1;
116
- background: rgba(0, 0, 0, 0.08);
119
+ background: var(--hover-background-color);
117
120
  }
118
121
  </style>
@@ -547,14 +547,14 @@ defineExpose({
547
547
  width: min(32rem, calc(100vw - 2rem));
548
548
  max-width: 32rem;
549
549
  padding: 0;
550
- border: 1px solid var(--border-color, #d7d7d7);
551
- background: var(--standout-bg-color, #fff);
550
+ border: 1px solid var(--quick-search-border);
551
+ background: var(--quick-search-dialog-bg);
552
552
  /* Dialog is rendered under <header>, so do not inherit header's white text */
553
- color: var(--text-color, #334155);
553
+ color: var(--quick-search-dialog-fg);
554
554
  }
555
555
 
556
556
  .quick-search-dialog::backdrop {
557
- background-color: rgba(0, 0, 0, 0.5);
557
+ background-color: var(--quick-search-backdrop);
558
558
  }
559
559
 
560
560
  .dialog-body {
@@ -573,23 +573,22 @@ defineExpose({
573
573
  .search-input {
574
574
  width: 100%;
575
575
  padding: 0.75em 2.25em 0.75em 0.75em;
576
- border: 1px solid var(--border-color, #d7d7d7);
576
+ border: 1px solid var(--quick-search-border);
577
577
  border-radius: 0.4em;
578
578
  font-size: 1rem;
579
- outline: none;
580
- background-color: #fff;
581
- color: var(--text-color, #334155);
579
+ background-color: var(--quick-search-input-bg);
580
+ color: var(--quick-search-input-fg);
582
581
  }
583
582
 
584
- .search-input:focus {
585
- border-color: #3b82f6;
586
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
583
+ .search-input:focus-visible {
584
+ outline: 2px solid var(--quick-search-focus-outline);
585
+ outline-offset: 2px;
587
586
  }
588
587
 
589
588
  .search-icon {
590
589
  position: absolute;
591
590
  right: 0.75rem;
592
- color: #6b7280;
591
+ color: var(--quick-search-muted-fg);
593
592
  pointer-events: none;
594
593
  display: inline-flex;
595
594
  }
@@ -606,12 +605,13 @@ defineExpose({
606
605
 
607
606
  .search-status {
608
607
  margin: 0;
608
+ color: var(--quick-search-muted-fg);
609
609
  }
610
610
 
611
611
  .search-results {
612
612
  max-height: min(20rem, 50vh);
613
613
  overflow-y: auto;
614
- border: 1px solid var(--border-color, #e1e5e9);
614
+ border: 1px solid var(--quick-search-border);
615
615
  border-radius: 0.4em;
616
616
  }
617
617
 
@@ -620,7 +620,7 @@ defineExpose({
620
620
  align-items: center;
621
621
  padding: 0.75rem 1rem;
622
622
  cursor: pointer;
623
- border-bottom: 1px solid var(--border-color, #f3f4f6);
623
+ border-bottom: 1px solid var(--quick-search-border);
624
624
  }
625
625
 
626
626
  .search-result-item:last-child {
@@ -629,8 +629,8 @@ defineExpose({
629
629
 
630
630
  .search-result-item:hover,
631
631
  .search-result-item.active {
632
- background-color: var(--hover-background-color);
633
- color: var(--hover-text-color);
632
+ background-color: var(--quick-search-hover-bg);
633
+ color: var(--quick-search-hover-fg);
634
634
  }
635
635
 
636
636
  .result-content {
@@ -645,20 +645,20 @@ defineExpose({
645
645
 
646
646
  .result-description {
647
647
  font-size: 0.875rem;
648
- color: #6b7280;
648
+ color: var(--quick-search-muted-fg);
649
649
  margin-bottom: 0.25rem;
650
650
  }
651
651
 
652
652
  .result-category {
653
653
  font-size: 0.75rem;
654
- color: #9ca3af;
654
+ color: var(--quick-search-subtle-fg);
655
655
  text-transform: uppercase;
656
656
  letter-spacing: 0.05em;
657
657
  }
658
658
 
659
659
  .result-icon {
660
660
  margin-right: 0.75rem;
661
- color: #6b7280;
661
+ color: var(--quick-search-muted-fg);
662
662
  flex-shrink: 0;
663
663
  display: inline-flex;
664
664
  }
@@ -668,7 +668,7 @@ defineExpose({
668
668
  flex-direction: column;
669
669
  align-items: center;
670
670
  padding: 1.5rem 1rem;
671
- color: #6b7280;
671
+ color: var(--quick-search-muted-fg);
672
672
  text-align: center;
673
673
  }
674
674
 
@@ -682,35 +682,9 @@ defineExpose({
682
682
  }
683
683
 
684
684
  mark {
685
- background-color: var(--karma-important, #fef3c7);
685
+ background-color: var(--quick-search-highlight-bg);
686
686
  color: inherit;
687
687
  padding: 0;
688
688
  border-radius: 2px;
689
689
  }
690
-
691
- html[data-theme="dark"] {
692
- .quick-search-dialog {
693
- color: #f9fafb;
694
- }
695
-
696
- .search-input {
697
- background-color: #1f2937;
698
- border-color: #374151;
699
- color: #f9fafb;
700
- }
701
-
702
- .search-icon,
703
- .result-description,
704
- .result-category,
705
- .result-icon,
706
- .no-results-content,
707
- .search-status {
708
- color: #9ca3af;
709
- }
710
-
711
- .search-input:focus {
712
- border-color: #60a5fa;
713
- box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.1);
714
- }
715
- }
716
690
  </style>
@@ -147,7 +147,7 @@ function select(option) {
147
147
  }
148
148
 
149
149
  .radio-group label:has(input:focus-visible) {
150
- outline: 2px solid black;
150
+ outline: 2px solid var(--radio-group-focus-outline);
151
151
  outline-offset: 2px;
152
152
  z-index: 2;
153
153
  }
@@ -155,8 +155,8 @@ function select(option) {
155
155
  /* Beat Femtocrank's gray .radio-group focus/hover when the option is selected. */
156
156
  .radio-group label:has(input:checked):hover,
157
157
  .radio-group label:has(input:checked:focus-visible) {
158
- background-color: #3a6f3a;
159
- color: white;
158
+ background-color: var(--radio-group-checked-hover-bg);
159
+ color: var(--radio-group-checked-hover-fg);
160
160
  }
161
161
 
162
162
  .radio-group.disabled {
@@ -185,7 +185,7 @@ function select(option) {
185
185
 
186
186
  .radio-group label.disabled:has(input:focus-visible),
187
187
  .radio-group.disabled label:has(input:focus-visible) {
188
- outline: 2px solid black;
188
+ outline: 2px solid var(--radio-group-focus-outline);
189
189
  outline-offset: 2px;
190
190
  }
191
191
  </style>
@@ -144,7 +144,7 @@ async function copy() {
144
144
  await navigator.clipboard.writeText(text)
145
145
  markCopied()
146
146
  emit('copy', text)
147
- } catch (error) {
147
+ } catch (_error) {
148
148
  try {
149
149
  textareaRef.value?.select()
150
150
  document.execCommand('copy')
@@ -121,7 +121,7 @@ a.active {
121
121
  width: 0.5rem;
122
122
  height: 0.5rem;
123
123
  border-radius: 50%;
124
- background: var(--indicator-color, #dc3545);
124
+ background: var(--karma-bad-tint);
125
125
  flex-shrink: 0;
126
126
  }
127
127
 
@@ -129,8 +129,8 @@ a.active {
129
129
  min-width: 1.25rem;
130
130
  padding: 0.1rem 0.4rem;
131
131
  border-radius: 999px;
132
- background: var(--indicator-color, #dc3545);
133
- color: #fff;
132
+ background: var(--karma-bad-tint);
133
+ color: var(--text-color);
134
134
  font-size: 0.75em;
135
135
  font-weight: 600;
136
136
  line-height: 1.2;
@@ -37,27 +37,24 @@
37
37
  v-if="showColumnOptionsButton"
38
38
  class="actions table-column-options-header"
39
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>
40
+ <span class="actions-menu-trigger">
41
+ <button
42
+ ref="columnOptionsButtonRef"
43
+ type="button"
44
+ aria-haspopup="dialog"
45
+ aria-label="Column options"
46
+ :aria-expanded="columnOptionsOpen ? 'true' : 'false'"
47
+ @click.stop="openColumnOptions"
48
+ >
49
+ <HugeiconsIcon
50
+ :icon="LayoutGridIcon"
51
+ width="0.95em"
52
+ height="0.95em"
53
+ :strokeWidth="2"
54
+ aria-hidden="true"
55
+ />
56
+ </button>
57
+ </span>
61
58
  </th>
62
59
  </tr>
63
60
  </thead>
@@ -71,7 +68,12 @@
71
68
  <td
72
69
  v-for="(header, cellIndex) in visibleHeaders"
73
70
  :key="header.key || cellIndex"
74
- :class="[cellClasses(header), header.class]"
71
+ :colspan="bodyColumnColspan(cellIndex) || undefined"
72
+ :class="[
73
+ cellClasses(header),
74
+ header.class,
75
+ isLastColumnWithOptions(cellIndex) ? 'table-last-column-with-options' : null,
76
+ ]"
75
77
  >
76
78
  <slot
77
79
  v-if="slots[`cell-${header.key}`]"
@@ -89,11 +91,6 @@
89
91
  {{ row[header.key] }}
90
92
  </span>
91
93
  </td>
92
- <td
93
- v-if="showColumnOptionsButton"
94
- class="actions table-column-options-cell"
95
- aria-hidden="true"
96
- />
97
94
  </tr>
98
95
  </tbody>
99
96
  </table>
@@ -135,6 +132,7 @@
135
132
  :column-keys="resolvedColumnOrder"
136
133
  :visible-keys="visibleColumnKeys"
137
134
  :column-priorities="resolvedColumnPriorities"
135
+ :default-column-priorities="defaultColumnPriorities"
138
136
  :filters="activeFilters"
139
137
  :rows="props.data"
140
138
  :table-id="props.tableId"
@@ -161,11 +159,9 @@ import TableColumnFilterPopover from './TableColumnFilterPopover.vue'
161
159
  import TableColumnOptionsPopover from './TableColumnOptionsPopover.vue'
162
160
  import { useLongPress } from '../composables/useLongPress.js'
163
161
  import {
164
- applyFilters,
165
162
  buildSelectOptions,
166
163
  cloneColumnFilterEntries,
167
164
  cloneFilters,
168
- countActiveFilters,
169
165
  isColumnFiltered,
170
166
  isFilterEntryActive,
171
167
  normalizeColumnFilterEntries,
@@ -186,6 +182,10 @@ import {
186
182
  sanitizePresetState,
187
183
  layoutStatesEqual,
188
184
  } from '../composables/tableColumnPresets.js'
185
+ import {
186
+ defaultColumnPriorityForIndex,
187
+ isValidColPriority,
188
+ } from '../composables/tableColumnPriorities.js'
189
189
 
190
190
  const DEFAULT_PAGE_SIZE = 10
191
191
 
@@ -231,6 +231,10 @@ const props = defineProps({
231
231
  type: Boolean,
232
232
  default: false,
233
233
  },
234
+ responsiveColumns: {
235
+ type: Boolean,
236
+ default: true,
237
+ },
234
238
  stickyCols: {
235
239
  type: Number,
236
240
  default: null,
@@ -286,6 +290,8 @@ const emit = defineEmits([
286
290
  'column-order-change',
287
291
  'update:columnPriorities',
288
292
  'column-priorities-change',
293
+ 'update:layoutLabel',
294
+ 'layout-label-change',
289
295
  ])
290
296
 
291
297
  const attrs = useAttrs()
@@ -368,6 +374,18 @@ const showColumnOptionsButton = computed(() =>
368
374
  props.columnOptions && manageableColumnKeys.value.length > 0,
369
375
  )
370
376
 
377
+ function isLastVisibleColumn(index) {
378
+ return index === visibleHeaders.value.length - 1
379
+ }
380
+
381
+ function isLastColumnWithOptions(index) {
382
+ return showColumnOptionsButton.value && isLastVisibleColumn(index)
383
+ }
384
+
385
+ function bodyColumnColspan(index) {
386
+ return isLastColumnWithOptions(index) ? 2 : null
387
+ }
388
+
371
389
  function isHeaderHidden(header) {
372
390
  if (header.hidden) {
373
391
  return true
@@ -386,9 +404,13 @@ const visibleColumnKeys = computed(() =>
386
404
  .map((header) => header.key),
387
405
  )
388
406
 
389
- function defaultColumnPriority(header) {
390
- return isValidColPriority(header?.colPriority) ? header.colPriority : null
391
- }
407
+ const hasHorizontalScroll = computed(() =>
408
+ props.horizontalScroll || (props.stickyCols >= 1 && props.stickyCols <= 3),
409
+ )
410
+
411
+ const useResponsiveColumns = computed(() =>
412
+ props.responsiveColumns && !hasHorizontalScroll.value,
413
+ )
392
414
 
393
415
  function resolvedColumnPriority(header) {
394
416
  if (!header?.key) {
@@ -404,9 +426,35 @@ function resolvedColumnPriority(header) {
404
426
  return isValidColPriority(override) ? override : null
405
427
  }
406
428
 
407
- return defaultColumnPriority(header)
429
+ if (!useResponsiveColumns.value) {
430
+ return null
431
+ }
432
+
433
+ const index = visibleHeaders.value.findIndex((item) => item.key === header.key)
434
+ if (index < 0) {
435
+ return null
436
+ }
437
+
438
+ return defaultColumnPriorityForIndex(header, index, useResponsiveColumns.value)
408
439
  }
409
440
 
441
+ const defaultColumnPriorities = computed(() => {
442
+ if (!useResponsiveColumns.value) {
443
+ return Object.fromEntries(
444
+ visibleHeaders.value
445
+ .filter((header) => isValidColPriority(header?.colPriority))
446
+ .map((header) => [header.key, header.colPriority]),
447
+ )
448
+ }
449
+
450
+ return Object.fromEntries(
451
+ visibleHeaders.value.flatMap((header, index) => {
452
+ const priority = defaultColumnPriorityForIndex(header, index, true)
453
+ return isValidColPriority(priority) ? [[header.key, priority]] : []
454
+ }),
455
+ )
456
+ })
457
+
410
458
  const resolvedColumnPriorities = computed(() =>
411
459
  Object.fromEntries(
412
460
  manageableColumnKeys.value.map((key) => {
@@ -420,10 +468,6 @@ const hasColPriorities = computed(() =>
420
468
  orderedHeaders.value.some((header) => isValidColPriority(resolvedColumnPriority(header))),
421
469
  )
422
470
 
423
- const hasHorizontalScroll = computed(() =>
424
- props.horizontalScroll || (props.stickyCols >= 1 && props.stickyCols <= 3),
425
- )
426
-
427
471
  const needsTableWrapper = computed(() => hasColPriorities.value || hasHorizontalScroll.value)
428
472
 
429
473
  const tableWrapperClasses = computed(() => {
@@ -528,10 +572,6 @@ const queryParams = computed(() => {
528
572
  return params
529
573
  })
530
574
 
531
- function isValidColPriority(priority) {
532
- return Number.isInteger(priority) && priority >= 1 && priority <= 5
533
- }
534
-
535
575
  function colPriorityClass(priority) {
536
576
  return isValidColPriority(priority) ? `col-priority-${priority}` : null
537
577
  }
@@ -1105,6 +1145,11 @@ const activeLayoutLabel = computed(() => {
1105
1145
  return 'Custom'
1106
1146
  })
1107
1147
 
1148
+ function emitLayoutLabelChange(label) {
1149
+ emit('update:layoutLabel', label)
1150
+ emit('layout-label-change', label)
1151
+ }
1152
+
1108
1153
  function loadDeveloperDefaults() {
1109
1154
  setFilters({})
1110
1155
  sortBy.value = null
@@ -1313,6 +1358,10 @@ watch(
1313
1358
  { deep: true },
1314
1359
  )
1315
1360
 
1361
+ watch(activeLayoutLabel, (label) => {
1362
+ emitLayoutLabelChange(label)
1363
+ }, { immediate: true })
1364
+
1316
1365
  watch(sortedItems, () => {
1317
1366
  if (!isRemote.value) {
1318
1367
  page.value = 1
@@ -1356,6 +1405,11 @@ onBeforeUnmount(() => {
1356
1405
  clearTimeout(fetchDebounceTimer.value)
1357
1406
  }
1358
1407
  })
1408
+
1409
+ defineExpose({
1410
+ layoutLabel: activeLayoutLabel,
1411
+ loadDeveloperDefaults,
1412
+ })
1359
1413
  </script>
1360
1414
 
1361
1415
  <style scoped>
@@ -1414,7 +1468,7 @@ tbody tr.row-clickable {
1414
1468
  min-height: 6rem;
1415
1469
  padding: 2rem 1rem;
1416
1470
  text-align: center;
1417
- color: var(--text-muted, #666);
1471
+ color: var(--table-muted-fg);
1418
1472
  }
1419
1473
 
1420
1474
  .table-empty-state.loading {
@@ -1436,48 +1490,26 @@ tbody tr.row-clickable {
1436
1490
  gap: 0.75rem;
1437
1491
  padding: 2rem 1rem;
1438
1492
  text-align: center;
1439
- color: var(--text-muted, #666);
1493
+ color: var(--table-muted-fg);
1440
1494
  }
1441
1495
 
1442
1496
  .table-filtered-empty-message {
1443
1497
  margin: 0;
1444
1498
  }
1445
1499
 
1446
- th.table-column-options-header,
1447
- td.table-column-options-cell {
1500
+ th.table-column-options-header {
1448
1501
  padding-right: 1rem;
1449
1502
  text-align: right;
1450
1503
  vertical-align: middle;
1451
- }
1452
-
1453
- th.table-column-options-header {
1454
1504
  width: auto;
1455
1505
  min-width: 2.5rem;
1456
1506
  max-width: none;
1457
1507
  white-space: nowrap;
1458
1508
  }
1459
1509
 
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;
1510
+ td.table-last-column-with-options {
1511
+ padding-right: 1rem;
1512
+ vertical-align: middle;
1481
1513
  }
1482
1514
 
1483
1515
  th.table-column-options-header .actions-menu-trigger {
@@ -1486,24 +1518,20 @@ th.table-column-options-header .actions-menu-trigger {
1486
1518
  flex: 0 0 auto;
1487
1519
  }
1488
1520
 
1489
- .table-scroll th.table-column-options-header,
1490
- .table-scroll td.table-column-options-cell {
1521
+ .table-scroll {
1522
+ min-width: 0;
1523
+ max-width: 100%;
1524
+ contain: inline-size;
1525
+ }
1526
+
1527
+ .table-scroll th.table-column-options-header {
1491
1528
  position: sticky;
1492
1529
  right: 0;
1493
- z-index: 1;
1530
+ z-index: 2;
1494
1531
  background-color: var(--table-sticky-bg);
1495
1532
  box-shadow: -1px 0 0 var(--border-color);
1496
1533
  }
1497
1534
 
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
1535
  th.actions .actions-menu-trigger button {
1508
1536
  display: inline-flex;
1509
1537
  align-items: center;