@revenexx/studio-shared 0.3.1 → 0.3.3

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 (85) hide show
  1. package/dist/module.d.mts +4 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +9 -0
  4. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.d.vue.ts +15 -0
  5. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +39 -0
  6. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue.d.ts +15 -0
  7. package/dist/runtime/components/Dashboard/DashboardControls.d.vue.ts +57 -0
  8. package/dist/runtime/components/Dashboard/DashboardControls.vue +209 -0
  9. package/dist/runtime/components/Dashboard/DashboardControls.vue.d.ts +57 -0
  10. package/dist/runtime/components/Dashboard/DashboardGrid.d.vue.ts +16 -0
  11. package/dist/runtime/components/Dashboard/DashboardGrid.vue +87 -0
  12. package/dist/runtime/components/Dashboard/DashboardGrid.vue.d.ts +16 -0
  13. package/dist/runtime/components/Dashboard/DashboardNameDialog.d.vue.ts +18 -0
  14. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +61 -0
  15. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue.d.ts +18 -0
  16. package/dist/runtime/components/Dashboard/DashboardPickerDialog.d.vue.ts +14 -0
  17. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +100 -0
  18. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue.d.ts +14 -0
  19. package/dist/runtime/components/Dashboard/DashboardShareDialog.d.vue.ts +27 -0
  20. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +155 -0
  21. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue.d.ts +27 -0
  22. package/dist/runtime/components/Dashboard/DashboardTopBar.d.vue.ts +59 -0
  23. package/dist/runtime/components/Dashboard/DashboardTopBar.vue +79 -0
  24. package/dist/runtime/components/Dashboard/DashboardTopBar.vue.d.ts +59 -0
  25. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.d.vue.ts +42 -0
  26. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +153 -0
  27. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue.d.ts +42 -0
  28. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +27 -0
  29. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +80 -0
  30. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +27 -0
  31. package/dist/runtime/components/Dashboard/DashboardWidgetHost.d.vue.ts +6 -0
  32. package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue +30 -0
  33. package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue.d.ts +6 -0
  34. package/dist/runtime/components/Dashboard/DashboardWidgetIcon.d.vue.ts +8 -0
  35. package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue +13 -0
  36. package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue.d.ts +8 -0
  37. package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.d.vue.ts +23 -0
  38. package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue +140 -0
  39. package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue.d.ts +23 -0
  40. package/dist/runtime/components/Dashboard/DashboardWidgetPreview.d.vue.ts +22 -0
  41. package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue +124 -0
  42. package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue.d.ts +22 -0
  43. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +39 -3
  44. package/dist/runtime/components/DataTable/DataTable.vue +306 -32
  45. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +39 -3
  46. package/dist/runtime/components/DataTable/FacetChips.d.vue.ts +16 -0
  47. package/dist/runtime/components/DataTable/FacetChips.vue +72 -0
  48. package/dist/runtime/components/DataTable/FacetChips.vue.d.ts +16 -0
  49. package/dist/runtime/components/DataTable/FacetPanel.d.vue.ts +20 -0
  50. package/dist/runtime/components/DataTable/FacetPanel.vue +359 -0
  51. package/dist/runtime/components/DataTable/FacetPanel.vue.d.ts +20 -0
  52. package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +5 -1
  53. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +83 -63
  54. package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +5 -1
  55. package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +5 -1
  56. package/dist/runtime/components/DataTable/SortMenu.vue +40 -12
  57. package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +5 -1
  58. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +2 -2
  59. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +2 -2
  60. package/dist/runtime/components/Media/MediaUploader.d.vue.ts +2 -2
  61. package/dist/runtime/components/Media/MediaUploader.vue.d.ts +2 -2
  62. package/dist/runtime/components/Shared/SortHeader.vue +6 -2
  63. package/dist/runtime/components/Shared/TabBar.d.vue.ts +23 -7
  64. package/dist/runtime/components/Shared/TabBar.vue +3 -2
  65. package/dist/runtime/components/Shared/TabBar.vue.d.ts +23 -7
  66. package/dist/runtime/composables/useDashboards.d.ts +27 -0
  67. package/dist/runtime/composables/useDashboards.js +143 -0
  68. package/dist/runtime/composables/useSavedViews.d.ts +44 -0
  69. package/dist/runtime/composables/useSavedViews.js +44 -0
  70. package/dist/runtime/composables/useStickyListView.d.ts +60 -0
  71. package/dist/runtime/composables/useStickyListView.js +32 -0
  72. package/dist/runtime/lib/canonicalJson.d.ts +43 -0
  73. package/dist/runtime/lib/canonicalJson.js +38 -0
  74. package/dist/runtime/lib/dashboardIcons.d.ts +4 -0
  75. package/dist/runtime/lib/dashboardIcons.js +50 -0
  76. package/dist/runtime/lib/facets.d.ts +200 -0
  77. package/dist/runtime/lib/facets.js +171 -0
  78. package/dist/runtime/lib/gridTracks.d.ts +55 -0
  79. package/dist/runtime/lib/gridTracks.js +29 -0
  80. package/dist/runtime/lib/widgetRegistry.d.ts +124 -0
  81. package/dist/runtime/lib/widgetRegistry.js +66 -0
  82. package/dist/runtime/types/dashboard.d.ts +95 -0
  83. package/dist/runtime/types/dashboard.js +2 -0
  84. package/dist/types.d.mts +8 -0
  85. package/package.json +4 -3
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { AltArrowDown, Bookmark, TrashBinMinimalistic } from "#solar-icons";
2
+ import { Bookmark, CheckCircle, Diskette, TrashBinMinimalistic } from "#solar-icons";
3
3
  import {
4
4
  Button,
5
5
  Dialog,
@@ -11,51 +11,33 @@ import {
11
11
  DropdownMenu,
12
12
  DropdownMenuContent,
13
13
  DropdownMenuItem,
14
+ DropdownMenuLabel,
14
15
  DropdownMenuSeparator,
15
16
  DropdownMenuTrigger,
16
17
  Input
17
18
  } from "@revenexx/studio";
19
+ import { sameJson } from "../../lib/canonicalJson";
18
20
  const props = defineProps({
19
- config: { type: Object, required: true }
21
+ config: { type: Object, required: true },
22
+ size: { type: String, required: false, default: "sm" }
20
23
  });
21
24
  const t = useT();
25
+ const store = useSavedViews();
22
26
  const views = ref([]);
23
- function read() {
24
- try {
25
- if (typeof window === "undefined") return [];
26
- const raw = window.localStorage.getItem(props.config.storageKey);
27
- if (!raw) return [];
28
- const parsed = JSON.parse(raw);
29
- if (!Array.isArray(parsed)) return [];
30
- return parsed.filter((v) => !!v && typeof v === "object" && typeof v.name === "string");
31
- } catch {
32
- return [];
33
- }
34
- }
35
- function persist() {
36
- try {
37
- if (typeof window === "undefined") return;
38
- window.localStorage.setItem(props.config.storageKey, JSON.stringify(views.value));
39
- } catch {
40
- }
27
+ async function load() {
28
+ views.value = await store.list(props.config.storageKey);
41
29
  }
42
- onMounted(() => {
43
- views.value = read();
44
- });
45
- watch(() => props.config.storageKey, () => {
46
- views.value = read();
47
- });
30
+ onMounted(load);
31
+ watch(() => props.config.storageKey, load);
48
32
  const allLabel = computed(() => props.config.allLabel ?? t("shared.datatable.savedviews.all.default"));
49
- function same(a, b) {
50
- return JSON.stringify(a) === JSON.stringify(b);
51
- }
52
- const isDefault = computed(() => same(props.config.snapshot(), props.config.defaultSnapshot()));
33
+ const isDefault = computed(() => sameJson(props.config.snapshot(), props.config.defaultSnapshot()));
53
34
  function isActive(v) {
54
- return same(v.snapshot, props.config.snapshot());
35
+ return sameJson(v.snapshot, props.config.snapshot());
55
36
  }
37
+ const appliedView = computed(() => views.value.find(isActive) ?? null);
38
+ const hasAppliedView = computed(() => appliedView.value !== null);
56
39
  const activeLabel = computed(() => {
57
- const hit = views.value.find(isActive);
58
- if (hit) return hit.name;
40
+ if (appliedView.value) return appliedView.value.name;
59
41
  return isDefault.value ? allLabel.value : t("shared.datatable.savedviews.custom");
60
42
  });
61
43
  function applyDefault() {
@@ -66,25 +48,31 @@ function applyView(v) {
66
48
  }
67
49
  const saveOpen = ref(false);
68
50
  const saveName = ref("");
51
+ const saving = ref(false);
69
52
  function openSave() {
70
- saveName.value = "";
53
+ saveName.value = appliedView.value?.name ?? "";
71
54
  saveOpen.value = true;
72
55
  }
73
- function save() {
56
+ async function save() {
74
57
  const name = saveName.value.trim();
75
- if (!name) return;
76
- const entry = { name, snapshot: props.config.snapshot() };
77
- const idx = views.value.findIndex((v) => v.name === name);
78
- if (idx >= 0) views.value.splice(idx, 1, entry);
79
- else views.value.push(entry);
80
- persist();
81
- saveOpen.value = false;
58
+ if (!name || saving.value) return;
59
+ saving.value = true;
60
+ try {
61
+ const entry = await store.save(props.config.storageKey, name, props.config.snapshot());
62
+ const at = views.value.findIndex((v) => v.name === name);
63
+ if (at >= 0) views.value.splice(at, 1, entry);
64
+ else views.value = [...views.value, entry].sort((a, b) => a.name.localeCompare(b.name));
65
+ saveOpen.value = false;
66
+ } finally {
67
+ saving.value = false;
68
+ }
82
69
  }
83
70
  const deleteTarget = ref(null);
84
- function remove() {
85
- if (!deleteTarget.value) return;
86
- views.value = views.value.filter((v) => v.name !== deleteTarget.value.name);
87
- persist();
71
+ async function remove() {
72
+ const target = deleteTarget.value;
73
+ if (!target) return;
74
+ await store.remove(props.config.storageKey, target.id);
75
+ views.value = views.value.filter((v) => v.id !== target.id);
88
76
  deleteTarget.value = null;
89
77
  }
90
78
  </script>
@@ -93,34 +81,57 @@ function remove() {
93
81
  <DropdownMenu>
94
82
  <DropdownMenuTrigger as-child>
95
83
  <Button
96
- variant="outline"
97
- size="sm"
98
- class="h-7 max-w-48 gap-1.5 text-sm"
99
- :class="isDefault ? '' : 'border-primary/30 text-primary'"
100
- :aria-label="t('shared.datatable.savedviews.aria')"
84
+ :variant="hasAppliedView ? 'default' : 'outline'"
85
+ :size="size === 'sm' ? 'icon-sm' : 'icon'"
86
+ :data-active="hasAppliedView"
87
+ :aria-label="t('shared.datatable.savedviews.trigger.aria', { name: activeLabel })"
88
+ :title="t('shared.datatable.savedviews.trigger.aria', { name: activeLabel })"
101
89
  >
102
- <Bookmark :size="13" />
103
- <span class="truncate">{{ activeLabel }}</span>
104
- <AltArrowDown :size="12" class="opacity-60" />
90
+ <!--
91
+ Violet like the filter trigger and the column picker beside it, and
92
+ filled the same way once a stored view is in effect — the button
93
+ carries that state rather than a second mark inside it. On the filled
94
+ face the icon inherits the button's own foreground, or it would be
95
+ violet on violet.
96
+ -->
97
+ <span class="flex shrink-0 items-center" :class="hasAppliedView ? '' : 'text-primary'">
98
+ <Bookmark :class="size === 'sm' ? 'size-3.5' : 'size-4'" weight="BoldDuotone" />
99
+ </span>
105
100
  </Button>
106
101
  </DropdownMenuTrigger>
107
- <DropdownMenuContent align="end" class="w-56">
102
+ <!--
103
+ `align="start"`, like every other menu in this row. It was `end`, so this
104
+ was the one control whose menu grew to the LEFT of its trigger while the
105
+ market switcher's and the column picker's grew right — a menu that opens
106
+ the other way is one a reader has to re-find every time.
107
+ -->
108
+ <DropdownMenuContent align="start" class="w-60">
109
+ <!-- Named, like the market switcher's "Showing" and the column picker's
110
+ "Columns": a menu of bare rows does not say what it is a menu OF. -->
111
+ <DropdownMenuLabel>{{ t("shared.datatable.savedviews.heading") }}</DropdownMenuLabel>
112
+ <!--
113
+ A tick, not just violet text. The mark says WHICH row is in effect, and
114
+ a reader who cannot separate the two hues gets nothing from a colour —
115
+ the same reason the market switcher puts a check on its active option.
116
+ -->
108
117
  <DropdownMenuItem class="gap-2" @select="applyDefault">
109
- <span class="flex-1 truncate">{{ allLabel }}</span>
110
- <span v-if="isDefault" class="text-xs text-primary">{{ t("shared.datatable.savedviews.active.badge") }}</span>
118
+ <span class="flex-1 truncate" :class="isDefault ? 'font-medium text-primary' : ''">{{ allLabel }}</span>
119
+ <CheckCircle v-if="isDefault" :size="14" weight="Bold" class="shrink-0 text-primary" />
111
120
  </DropdownMenuItem>
112
121
  <template v-if="views.length">
113
122
  <DropdownMenuSeparator />
114
123
  <DropdownMenuItem
115
124
  v-for="v in views"
116
- :key="v.name"
125
+ :key="v.id"
117
126
  class="gap-2"
127
+ :aria-current="isActive(v) ? 'true' : void 0"
118
128
  @select="applyView(v)"
119
129
  >
120
- <span class="flex-1 truncate">{{ v.name }}</span>
130
+ <span class="min-w-0 flex-1 truncate" :class="isActive(v) ? 'font-medium text-primary' : ''">{{ v.name }}</span>
131
+ <CheckCircle v-if="isActive(v)" :size="14" weight="Bold" class="shrink-0 text-primary" />
121
132
  <span
122
133
  role="button"
123
- class="text-muted-foreground transition-colors hover:text-rose-600"
134
+ class="shrink-0 text-muted-foreground transition-colors hover:text-rose-600"
124
135
  :aria-label="t('shared.datatable.savedviews.delete.aria', { name: v.name })"
125
136
  @click.stop="deleteTarget = v"
126
137
  @pointerdown.stop.prevent
@@ -130,9 +141,18 @@ function remove() {
130
141
  </DropdownMenuItem>
131
142
  </template>
132
143
  <DropdownMenuSeparator />
133
- <DropdownMenuItem @select="openSave">
134
- {{ t("shared.datatable.savedviews.action.savecurrent") }}
144
+ <DropdownMenuItem class="gap-2" @select="openSave">
145
+ <Diskette :size="14" class="shrink-0 text-muted-foreground" />
146
+ <span class="flex-1">{{ t("shared.datatable.savedviews.action.savecurrent") }}</span>
135
147
  </DropdownMenuItem>
148
+ <!--
149
+ Where the last read or write actually landed. One line, and only when
150
+ the answer is the surprising one: a control that claims nothing is
151
+ better than one that claims a sync it did not get.
152
+ -->
153
+ <p v-if="!store.durable" class="px-2 pt-1.5 pb-1 text-xs text-muted-foreground">
154
+ {{ t("shared.datatable.savedviews.local.note") }}
155
+ </p>
136
156
  </DropdownMenuContent>
137
157
  </DropdownMenu>
138
158
 
@@ -158,7 +178,7 @@ function remove() {
158
178
  <Button variant="outline" @click="saveOpen = false">
159
179
  {{ t("shared.datatable.savedviews.savedialog.action.cancel") }}
160
180
  </Button>
161
- <Button :disabled="!saveName.trim()" @click="save">
181
+ <Button :disabled="!saveName.trim() || saving" @click="save">
162
182
  {{ t("shared.datatable.savedviews.savedialog.action.save") }}
163
183
  </Button>
164
184
  </DialogFooter>
@@ -1,6 +1,10 @@
1
1
  import type { DataTableSavedViews } from './types.js';
2
2
  type __VLS_Props = {
3
3
  config: DataTableSavedViews;
4
+ /** Matches the row it sits in; `sm` is the 32px square its neighbours are. */
5
+ size?: 'sm' | 'default';
4
6
  };
5
- declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
7
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
8
+ size: "sm" | "default";
9
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
10
  export default _default;
@@ -1,6 +1,8 @@
1
1
  import type { DataTableColumn, SortState } from './types.js';
2
2
  type __VLS_Props = {
3
3
  columns: DataTableColumn[];
4
+ /** Matches the row it sits in; `sm` is the 32px square its neighbours are. */
5
+ size?: 'sm' | 'default';
4
6
  };
5
7
  type __VLS_PublicProps = __VLS_Props & {
6
8
  modelValue: SortState;
@@ -9,5 +11,7 @@ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {},
9
11
  "update:modelValue": (value: SortState) => any;
10
12
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
11
13
  "onUpdate:modelValue"?: ((value: SortState) => any) | undefined;
12
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ }>, {
15
+ size: "sm" | "default";
16
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
17
  export default _default;
@@ -1,22 +1,32 @@
1
1
  <script setup>
2
- import { AltArrowDown, SortVertical } from "#solar-icons";
2
+ import { CheckCircle, RoundSortVertical, SortFromBottomToTop, SortFromTopToBottom } from "#solar-icons";
3
3
  import {
4
4
  Button,
5
5
  DropdownMenu,
6
6
  DropdownMenuContent,
7
7
  DropdownMenuItem,
8
+ DropdownMenuLabel,
8
9
  DropdownMenuTrigger
9
10
  } from "@revenexx/studio";
10
11
  const props = defineProps({
11
- columns: { type: Array, required: true }
12
+ columns: { type: Array, required: true },
13
+ size: { type: String, required: false, default: "sm" }
12
14
  });
13
15
  const sort = defineModel({ type: Object, ...{ required: true } });
14
16
  const t = useT();
17
+ const sorted = computed(() => Boolean(sort.value.column && sort.value.direction));
18
+ const icon = computed(() => {
19
+ if (!sorted.value) return RoundSortVertical;
20
+ return sort.value.direction === "asc" ? SortFromBottomToTop : SortFromTopToBottom;
21
+ });
15
22
  const label = computed(() => {
16
23
  const col = props.columns.find((c) => c.key === sort.value.column);
17
- if (!col || !sort.value.direction) return t("shared.datatable.sortmenu.label.default");
24
+ if (!col || !sorted.value) return t("shared.datatable.sortmenu.label.default");
18
25
  return `${col.label} ${sort.value.direction === "asc" ? "\u2191" : "\u2193"}`;
19
26
  });
27
+ function isActive(key, direction) {
28
+ return sort.value.column === key && sort.value.direction === direction;
29
+ }
20
30
  function pick(key, direction) {
21
31
  sort.value = { column: key, direction };
22
32
  }
@@ -25,21 +35,39 @@ function pick(key, direction) {
25
35
  <template>
26
36
  <DropdownMenu>
27
37
  <DropdownMenuTrigger as-child>
28
- <Button variant="outline" size="sm" class="h-7 gap-1.5 text-sm" :aria-label="t('shared.datatable.sortmenu.aria')">
29
- <SortVertical :size="14" />
30
- {{ label }}
31
- <AltArrowDown :size="12" class="opacity-60" />
38
+ <Button
39
+ :variant="sorted ? 'default' : 'outline'"
40
+ :size="size === 'sm' ? 'icon-sm' : 'icon'"
41
+ :data-active="sorted"
42
+ :aria-label="t('shared.datatable.sortmenu.aria', { sort: label })"
43
+ :title="t('shared.datatable.sortmenu.aria', { sort: label })"
44
+ >
45
+ <!-- Violet while idle like the filter trigger and the column picker
46
+ beside it; on the filled face the icon takes the button's own
47
+ foreground, or it would be violet on violet. -->
48
+ <span class="flex shrink-0 items-center" :class="sorted ? '' : 'text-primary'">
49
+ <component :is="icon" weight="Linear" :class="size === 'sm' ? 'size-3.5' : 'size-4'" />
50
+ </span>
32
51
  </Button>
33
52
  </DropdownMenuTrigger>
34
- <DropdownMenuContent align="start" class="w-52">
53
+ <DropdownMenuContent align="start" class="w-56">
54
+ <DropdownMenuLabel>{{ t("shared.datatable.sortmenu.heading") }}</DropdownMenuLabel>
35
55
  <template v-for="col in columns" :key="col.key">
36
56
  <DropdownMenuItem class="gap-2" @select="pick(col.key, 'asc')">
37
- <span class="flex-1 truncate">{{ col.label }} ↑</span>
38
- <span v-if="sort.column === col.key && sort.direction === 'asc'" class="text-xs text-primary">{{ t("shared.datatable.sortmenu.active.badge") }}</span>
57
+ <SortFromBottomToTop weight="Linear" :size="13" class="shrink-0 text-muted-foreground" />
58
+ <span
59
+ class="min-w-0 flex-1 truncate"
60
+ :class="isActive(col.key, 'asc') ? 'font-medium text-primary' : ''"
61
+ >{{ col.label }}</span>
62
+ <CheckCircle v-if="isActive(col.key, 'asc')" :size="14" weight="Bold" class="shrink-0 text-primary" />
39
63
  </DropdownMenuItem>
40
64
  <DropdownMenuItem class="gap-2" @select="pick(col.key, 'desc')">
41
- <span class="flex-1 truncate">{{ col.label }} ↓</span>
42
- <span v-if="sort.column === col.key && sort.direction === 'desc'" class="text-xs text-primary">{{ t("shared.datatable.sortmenu.active.badge") }}</span>
65
+ <SortFromTopToBottom weight="Linear" :size="13" class="shrink-0 text-muted-foreground" />
66
+ <span
67
+ class="min-w-0 flex-1 truncate"
68
+ :class="isActive(col.key, 'desc') ? 'font-medium text-primary' : ''"
69
+ >{{ col.label }}</span>
70
+ <CheckCircle v-if="isActive(col.key, 'desc')" :size="14" weight="Bold" class="shrink-0 text-primary" />
43
71
  </DropdownMenuItem>
44
72
  </template>
45
73
  </DropdownMenuContent>
@@ -1,6 +1,8 @@
1
1
  import type { DataTableColumn, SortState } from './types.js';
2
2
  type __VLS_Props = {
3
3
  columns: DataTableColumn[];
4
+ /** Matches the row it sits in; `sm` is the 32px square its neighbours are. */
5
+ size?: 'sm' | 'default';
4
6
  };
5
7
  type __VLS_PublicProps = __VLS_Props & {
6
8
  modelValue: SortState;
@@ -9,5 +11,7 @@ declare const _default: import("vue").DefineComponent<__VLS_PublicProps, {}, {},
9
11
  "update:modelValue": (value: SortState) => any;
10
12
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
11
13
  "onUpdate:modelValue"?: ((value: SortState) => any) | undefined;
12
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ }>, {
15
+ size: "sm" | "default";
16
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
17
  export default _default;
@@ -6,11 +6,11 @@ type __VLS_Props = {
6
6
  kinds?: string[];
7
7
  };
8
8
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
- select: (assets: StorageAsset[]) => any;
10
9
  "update:open": (value: boolean) => any;
10
+ select: (assets: StorageAsset[]) => any;
11
11
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
- onSelect?: ((assets: StorageAsset[]) => any) | undefined;
13
12
  "onUpdate:open"?: ((value: boolean) => any) | undefined;
13
+ onSelect?: ((assets: StorageAsset[]) => any) | undefined;
14
14
  }>, {
15
15
  multiple: boolean;
16
16
  kinds: string[];
@@ -6,11 +6,11 @@ type __VLS_Props = {
6
6
  kinds?: string[];
7
7
  };
8
8
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
- select: (assets: StorageAsset[]) => any;
10
9
  "update:open": (value: boolean) => any;
10
+ select: (assets: StorageAsset[]) => any;
11
11
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
12
- onSelect?: ((assets: StorageAsset[]) => any) | undefined;
13
12
  "onUpdate:open"?: ((value: boolean) => any) | undefined;
13
+ onSelect?: ((assets: StorageAsset[]) => any) | undefined;
14
14
  }>, {
15
15
  multiple: boolean;
16
16
  kinds: string[];
@@ -14,16 +14,16 @@ type __VLS_Props = {
14
14
  folderName: string | null;
15
15
  };
16
16
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
17
+ "update:open": (value: boolean) => any;
17
18
  done: (summary: {
18
19
  created: number;
19
20
  failed: number;
20
21
  }) => any;
21
- "update:open": (value: boolean) => any;
22
22
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
23
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
23
24
  onDone?: ((summary: {
24
25
  created: number;
25
26
  failed: number;
26
27
  }) => any) | undefined;
27
- "onUpdate:open"?: ((value: boolean) => any) | undefined;
28
28
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
29
29
  export default _default;
@@ -14,16 +14,16 @@ type __VLS_Props = {
14
14
  folderName: string | null;
15
15
  };
16
16
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
17
+ "update:open": (value: boolean) => any;
17
18
  done: (summary: {
18
19
  created: number;
19
20
  failed: number;
20
21
  }) => any;
21
- "update:open": (value: boolean) => any;
22
22
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
23
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
23
24
  onDone?: ((summary: {
24
25
  created: number;
25
26
  failed: number;
26
27
  }) => any) | undefined;
27
- "onUpdate:open"?: ((value: boolean) => any) | undefined;
28
28
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
29
29
  export default _default;
@@ -18,11 +18,15 @@ const sortIcon = computed(() => {
18
18
  <template>
19
19
  <button
20
20
  type="button"
21
- class="group inline-flex cursor-pointer items-center gap-1.5 -m-1 p-1 rounded-md text-left transition-colors hover:text-primary"
21
+ class="group inline-flex min-w-0 max-w-full cursor-pointer items-center gap-1.5 -m-1 p-1 rounded-md text-left transition-colors hover:text-primary"
22
22
  :class="alignRight ? 'flex-row-reverse w-full justify-end' : ''"
23
23
  @click="emit('toggle', column)"
24
24
  >
25
- <slot />
25
+ <!-- The label truncates. Without this the button is as wide as its words
26
+ and a heading longer than its column simply prints over the next one —
27
+ "SortierungGeändert" on a category list, with no hint that two columns
28
+ are involved. The cell carries the full text as its `title`. -->
29
+ <span class="min-w-0 truncate"><slot /></span>
26
30
  <component
27
31
  :is="sortIcon"
28
32
  weight="Linear"
@@ -1,10 +1,22 @@
1
1
  /**
2
- * Underline tab bar — the standard cockpit detail-page tabs. A full-width
3
- * hairline sits behind the row; the active/hover indicator is a 2px bar that
4
- * spans exactly the label width (inset-x-3 = the px-3 padding), sitting on the
5
- * baseline over the hairline. Active = primary; hover = soft primary. Tabs
6
- * scroll horizontally on overflow. To bleed the line to the panel edges, pass
7
- * e.g. `class="-mx-4 px-4"` from a padded page.
2
+ * Underline tab bar — the standard cockpit tabs. A full-width hairline sits
3
+ * behind the row; the active/hover indicator is a 2px bar spanning exactly the
4
+ * label width (inset-x-2.5 = the button's px-2.5), sitting on the baseline over
5
+ * the hairline. Active = primary; hover = soft primary. Tabs scroll
6
+ * horizontally on overflow. To bleed the line to the panel edges, pass e.g.
7
+ * `class="-mx-4 px-4"` from a padded page.
8
+ *
9
+ * ## `sticky`
10
+ *
11
+ * On by default, because the original job was a detail page: the row glues
12
+ * itself to the bottom of the sticky `PageHeader` (hence the magic `57px`) and
13
+ * frosts its background so content scrolls through.
14
+ *
15
+ * Off for a row INSIDE a panel — the queue tabs between a DataTable's toolbar
16
+ * and its header, say. There the offset is wrong (there is no page header above
17
+ * it) and the frosted layer is worse than nothing, because the panel already
18
+ * has a background of its own and the blur only muddies the rows scrolling past
19
+ * underneath. Same tabs, same indicator, same hairline; just not pinned.
8
20
  */
9
21
  export interface TabItem {
10
22
  key: string;
@@ -14,10 +26,14 @@ export interface TabItem {
14
26
  type __VLS_Props = {
15
27
  tabs: TabItem[];
16
28
  modelValue: string;
29
+ /** Pin to the page header. Off for a bar inside a panel. */
30
+ sticky?: boolean;
17
31
  };
18
32
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
33
  "update:modelValue": (key: string) => any;
20
34
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
21
35
  "onUpdate:modelValue"?: ((key: string) => any) | undefined;
22
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
+ }>, {
37
+ sticky: boolean;
38
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
39
  export default _default;
@@ -1,7 +1,8 @@
1
1
  <script setup>
2
2
  defineProps({
3
3
  tabs: { type: Array, required: true },
4
- modelValue: { type: String, required: true }
4
+ modelValue: { type: String, required: true },
5
+ sticky: { type: Boolean, required: false, default: true }
5
6
  });
6
7
  defineEmits(["update:modelValue"]);
7
8
  </script>
@@ -11,7 +12,7 @@ defineEmits(["update:modelValue"]);
11
12
  pt-4 pb-3 + text-xl line + 1px border) so header and tabs stay glued as
12
13
  one unit while the panel scrolls. Glassy frosted bg (blur + low opacity)
13
14
  lets content scroll through. -->
14
- <div class="sticky top-[57px] z-10 bg-white/40 backdrop-blur-xl">
15
+ <div :class="sticky ? 'sticky top-[57px] z-10 bg-white/40 backdrop-blur-xl' : 'relative'">
15
16
  <!-- Pull the row out by the tab padding so the first/last label sits flush
16
17
  with the surrounding content (headline) edges. -->
17
18
  <div class="relative z-10 -mx-2.5 flex gap-1 overflow-x-auto">
@@ -1,10 +1,22 @@
1
1
  /**
2
- * Underline tab bar — the standard cockpit detail-page tabs. A full-width
3
- * hairline sits behind the row; the active/hover indicator is a 2px bar that
4
- * spans exactly the label width (inset-x-3 = the px-3 padding), sitting on the
5
- * baseline over the hairline. Active = primary; hover = soft primary. Tabs
6
- * scroll horizontally on overflow. To bleed the line to the panel edges, pass
7
- * e.g. `class="-mx-4 px-4"` from a padded page.
2
+ * Underline tab bar — the standard cockpit tabs. A full-width hairline sits
3
+ * behind the row; the active/hover indicator is a 2px bar spanning exactly the
4
+ * label width (inset-x-2.5 = the button's px-2.5), sitting on the baseline over
5
+ * the hairline. Active = primary; hover = soft primary. Tabs scroll
6
+ * horizontally on overflow. To bleed the line to the panel edges, pass e.g.
7
+ * `class="-mx-4 px-4"` from a padded page.
8
+ *
9
+ * ## `sticky`
10
+ *
11
+ * On by default, because the original job was a detail page: the row glues
12
+ * itself to the bottom of the sticky `PageHeader` (hence the magic `57px`) and
13
+ * frosts its background so content scrolls through.
14
+ *
15
+ * Off for a row INSIDE a panel — the queue tabs between a DataTable's toolbar
16
+ * and its header, say. There the offset is wrong (there is no page header above
17
+ * it) and the frosted layer is worse than nothing, because the panel already
18
+ * has a background of its own and the blur only muddies the rows scrolling past
19
+ * underneath. Same tabs, same indicator, same hairline; just not pinned.
8
20
  */
9
21
  export interface TabItem {
10
22
  key: string;
@@ -14,10 +26,14 @@ export interface TabItem {
14
26
  type __VLS_Props = {
15
27
  tabs: TabItem[];
16
28
  modelValue: string;
29
+ /** Pin to the page header. Off for a bar inside a panel. */
30
+ sticky?: boolean;
17
31
  };
18
32
  declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
19
33
  "update:modelValue": (key: string) => any;
20
34
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
21
35
  "onUpdate:modelValue"?: ((key: string) => any) | undefined;
22
- }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
36
+ }>, {
37
+ sticky: boolean;
38
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
39
  export default _default;
@@ -0,0 +1,27 @@
1
+ import type { ComputedRef } from 'vue';
2
+ import type { Dashboard, DashboardScope, DashboardWritePayload } from '../types/dashboard.js';
3
+ export declare class DashboardApiError extends Error {
4
+ status: number;
5
+ constructor(status: number, message: string);
6
+ }
7
+ export interface UseDashboardsOptions {
8
+ /** Where the dashboard routes live. The cockpit serves them from Nitro. */
9
+ base?: string;
10
+ }
11
+ export declare function useDashboards(scope: DashboardScope, options?: UseDashboardsOptions): {
12
+ scope: DashboardScope;
13
+ dashboards: ComputedRef<Dashboard[]>;
14
+ defaultId: ComputedRef<string | null>;
15
+ loading: ComputedRef<boolean>;
16
+ saving: ComputedRef<boolean>;
17
+ error: ComputedRef<string | null>;
18
+ load: (force?: boolean) => Promise<Dashboard[]>;
19
+ resolve: (slug: string | null) => Dashboard | null;
20
+ create: (payload: DashboardWritePayload & {
21
+ name: string;
22
+ }) => Promise<Dashboard>;
23
+ save: (id: string, payload: DashboardWritePayload) => Promise<Dashboard>;
24
+ remove: (id: string) => Promise<void>;
25
+ duplicate: (id: string) => Promise<Dashboard>;
26
+ setDefault: (id: string | null) => Promise<void>;
27
+ };