@revenexx/studio-shared 0.1.1 → 0.2.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 (63) hide show
  1. package/README.md +22 -12
  2. package/dist/module.d.mts +9 -0
  3. package/dist/module.json +9 -0
  4. package/dist/module.mjs +27 -0
  5. package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +12 -0
  6. package/{components → dist/runtime/components}/DataTable/ColumnsMenu.vue +10 -17
  7. package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +12 -0
  8. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +59 -0
  9. package/dist/runtime/components/DataTable/DataTable.vue +453 -0
  10. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +59 -0
  11. package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +12 -0
  12. package/{components → dist/runtime/components}/DataTable/FilterMenu.vue +4 -8
  13. package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +12 -0
  14. package/dist/runtime/components/DataTable/types.d.ts +70 -0
  15. package/dist/runtime/components/DataTable/types.js +0 -0
  16. package/dist/runtime/components/Shared/BuildLogsFlyout.d.vue.ts +30 -0
  17. package/{components → dist/runtime/components}/Shared/BuildLogsFlyout.vue +27 -45
  18. package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +30 -0
  19. package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +17 -0
  20. package/{components → dist/runtime/components}/Shared/GitDeploymentSettings.vue +22 -35
  21. package/dist/runtime/components/Shared/GitDeploymentSettings.vue.d.ts +17 -0
  22. package/dist/runtime/components/Shared/KpiTile.d.vue.ts +13 -0
  23. package/{components → dist/runtime/components}/Shared/KpiTile.vue +11 -19
  24. package/dist/runtime/components/Shared/KpiTile.vue.d.ts +13 -0
  25. package/dist/runtime/components/Shared/PageHeader.d.vue.ts +23 -0
  26. package/{components → dist/runtime/components}/Shared/PageHeader.vue +6 -8
  27. package/dist/runtime/components/Shared/PageHeader.vue.d.ts +23 -0
  28. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +36 -0
  29. package/{components → dist/runtime/components}/Shared/SidebarNav.vue +15 -47
  30. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +36 -0
  31. package/dist/runtime/components/Shared/SortHeader.d.vue.ts +23 -0
  32. package/dist/runtime/components/Shared/SortHeader.vue +35 -0
  33. package/dist/runtime/components/Shared/SortHeader.vue.d.ts +23 -0
  34. package/dist/runtime/components/Shared/StateCard.d.vue.ts +32 -0
  35. package/{components → dist/runtime/components}/Shared/StateCard.vue +21 -36
  36. package/dist/runtime/components/Shared/StateCard.vue.d.ts +32 -0
  37. package/dist/runtime/components/Shared/StudioCard.d.vue.ts +15 -0
  38. package/dist/runtime/components/Shared/StudioCard.vue +19 -0
  39. package/dist/runtime/components/Shared/StudioCard.vue.d.ts +15 -0
  40. package/dist/runtime/components/Shared/TabBar.d.vue.ts +23 -0
  41. package/{components → dist/runtime/components}/Shared/TabBar.vue +7 -16
  42. package/dist/runtime/components/Shared/TabBar.vue.d.ts +23 -0
  43. package/dist/runtime/components/Shared/ViewTab.d.vue.ts +7 -0
  44. package/{components → dist/runtime/components}/Shared/ViewTab.vue +7 -9
  45. package/dist/runtime/components/Shared/ViewTab.vue.d.ts +7 -0
  46. package/dist/runtime/components/Shared/Wizard.d.vue.ts +47 -0
  47. package/{components → dist/runtime/components}/Shared/Wizard.vue +24 -41
  48. package/dist/runtime/components/Shared/Wizard.vue.d.ts +47 -0
  49. package/{composables/usePlatformAuth.ts → dist/runtime/composables/usePlatformAuth.d.ts} +3 -21
  50. package/dist/runtime/composables/usePlatformAuth.js +17 -0
  51. package/dist/runtime/composables/usePlatformTenant.d.ts +14 -0
  52. package/dist/runtime/composables/usePlatformTenant.js +18 -0
  53. package/dist/runtime/types/nav.d.ts +23 -0
  54. package/dist/runtime/types/nav.js +0 -0
  55. package/dist/types.d.mts +15 -0
  56. package/package.json +25 -12
  57. package/components/DataTable/DataTable.vue +0 -518
  58. package/components/DataTable/types.ts +0 -82
  59. package/components/Shared/SortHeader.vue +0 -50
  60. package/components/Shared/StudioCard.vue +0 -24
  61. package/composables/usePlatformTenant.ts +0 -35
  62. package/index.ts +0 -12
  63. package/nuxt.config.ts +0 -23
@@ -1,518 +0,0 @@
1
- <script setup lang="ts" generic="T">
2
- import { AltArrowDown, Filter, Magnifier as Search } from '#solar-icons'
3
- import {
4
- Button,
5
- DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator,
6
- DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,
7
- Input,
8
- } from '@revenexx/studio'
9
- import type { Component } from 'vue'
10
- import SortHeader from '../Shared/SortHeader.vue'
11
- import ColumnsMenu from './ColumnsMenu.vue'
12
- import FilterMenu from './FilterMenu.vue'
13
- import type { DataTableColumn, DataTableFilter, DataTableState, RowKey, SortState } from './types'
14
-
15
- // One DataTable to rule them all. Two operating modes:
16
- //
17
- // Static (default): the table holds search/sort/page state internally
18
- // and filters/sorts/paginates the `data` array in-memory.
19
- //
20
- // Server (`server-mode`): the page owns the data pipeline. The table
21
- // just renders `data` (the current page) and emits `state-change`
22
- // whenever search / sort / page changes — the page should re-fetch.
23
- // Pass `total` so the footer knows the un-paginated count.
24
- //
25
- // Per-row rendering is slot-based: `#cell-<key>` receives `{ row }`.
26
- // Cells fall back to the column's sortGetter value if no slot is given.
27
-
28
- const props = withDefaults(defineProps<{
29
- data: T[]
30
- columns: DataTableColumn<T>[]
31
- rowKey: RowKey<T>
32
-
33
- // Toolbar
34
- search?: boolean | { placeholder?: string }
35
- columnsMenu?: boolean
36
- filter?: boolean
37
- /** Built-in status/dimension filter pills (static mode). One dimension, or
38
- * several (each renders as its own sub-menu). */
39
- filterTabs?: DataTableFilter<T> | DataTableFilter<T>[]
40
-
41
- // Pagination
42
- pagination?: boolean | { pageSize?: number }
43
-
44
- // Sort
45
- defaultSort?: { column: string, direction: 'asc' | 'desc' }
46
-
47
- // Server mode
48
- serverMode?: boolean
49
- total?: number
50
- loading?: boolean
51
-
52
- // Empty state
53
- emptyText?: string
54
- }>(), {
55
- search: true,
56
- columnsMenu: true,
57
- filter: false,
58
- pagination: true,
59
- serverMode: false,
60
- loading: false,
61
- })
62
-
63
- const emit = defineEmits<{
64
- rowClick: [row: T]
65
- stateChange: [state: DataTableState]
66
- }>()
67
-
68
- // ---------------------------------------------------------------------------
69
- // Internal state
70
- // ---------------------------------------------------------------------------
71
- const query = ref('')
72
- // `sort` and `page` are optionally controlled: a parent in server mode
73
- // can `v-model:sort` / `v-model:page` to stay authoritative over the data
74
- // pipeline (e.g. AppRenderer/ListView driving PostgREST). Left unbound,
75
- // they behave as plain local state seeded from `defaultSort`.
76
- const sort = defineModel<SortState>('sort', {
77
- default: () => ({
78
- column: null,
79
- direction: null,
80
- }),
81
- })
82
- const page = defineModel<number>('page', { default: 0 })
83
- if (props.defaultSort && sort.value.column == null) {
84
- sort.value = { column: props.defaultSort.column, direction: props.defaultSort.direction }
85
- }
86
- // Page size is local state (the footer lets the user change it), seeded from
87
- // the `pagination.pageSize` prop. Defaults to 20.
88
- const pageSize = ref(
89
- typeof props.pagination === 'object' && props.pagination?.pageSize
90
- ? props.pagination.pageSize
91
- : 20,
92
- )
93
- const pageSizeOptions = computed(() => Array.from(new Set([10, 20, 50, 100, pageSize.value])).sort((a, b) => a - b))
94
-
95
- // Column visibility — initialised once from column defaults.
96
- const visibleKeys = ref<Set<string>>(
97
- new Set(props.columns.filter(c => c.visible !== false).map(c => c.key)),
98
- )
99
- function toggleColumn(key: string) {
100
- const next = new Set(visibleKeys.value)
101
- if (next.has(key)) next.delete(key)
102
- else next.add(key)
103
- visibleKeys.value = next
104
- }
105
-
106
- const visibleColumns = computed(() =>
107
- props.columns.filter(c => visibleKeys.value.has(c.key)),
108
- )
109
-
110
- // ColumnsMenu is non-generic (DataTableColumn<unknown>[]); the per-row getters
111
- // make DataTableColumn<T> invariant in T, so widen for the menu prop.
112
- const menuColumns = computed(() => props.columns as unknown as DataTableColumn[])
113
-
114
- // ---------------------------------------------------------------------------
115
- // Static-mode data pipeline (search → sort → paginate). Skipped in
116
- // server mode; in server mode the page is expected to do this work and
117
- // pass back the already-prepared `data`.
118
- // ---------------------------------------------------------------------------
119
- function searchOf(col: DataTableColumn<T>, row: T): string {
120
- if (col.searchGetter) return col.searchGetter(row).toLowerCase()
121
- if (col.sortGetter) {
122
- const v = col.sortGetter(row)
123
- return v == null ? '' : String(v).toLowerCase()
124
- }
125
- const raw = (row as Record<string, unknown>)[col.key]
126
- return raw == null ? '' : String(raw).toLowerCase()
127
- }
128
-
129
- // Filter dimensions (static mode) run before search. One or many dimensions,
130
- // each rendered as a sub-menu under the generic "Filter" dropdown.
131
- interface FilterDim { key: string, label: string, getter: (row: T) => string | null | undefined, options: { value: string, label: string }[], allLabel?: string }
132
- const filterDims = computed<FilterDim[]>(() => {
133
- const raw = props.filterTabs
134
- if (!raw) return []
135
- const arr = Array.isArray(raw) ? raw : [raw]
136
- return arr.map((f, i) => ({ key: f.key ?? f.label ?? `dim${i}`, label: f.label ?? 'Status', getter: f.getter, options: f.options, allLabel: f.allLabel }))
137
- })
138
- const activeFilters = ref<Record<string, string | null>>({})
139
- const anyFilterActive = computed(() => Object.values(activeFilters.value).some(Boolean))
140
- function setFilter(key: string, value: string | null) {
141
- activeFilters.value = { ...activeFilters.value, [key]: value }
142
- }
143
- function dimCounts(d: FilterDim): Record<string, number> {
144
- const m: Record<string, number> = {}
145
- for (const row of props.data) { const k = String(d.getter(row) ?? ''); m[k] = (m[k] ?? 0) + 1 }
146
- return m
147
- }
148
- const filtered = computed(() => {
149
- if (props.serverMode || !filterDims.value.length) return props.data
150
- const active = filterDims.value.filter(d => activeFilters.value[d.key])
151
- if (!active.length) return props.data
152
- return props.data.filter(row => active.every(d => String(d.getter(row) ?? '') === activeFilters.value[d.key]))
153
- })
154
-
155
- const searched = computed(() => {
156
- if (props.serverMode) return props.data
157
- const q = query.value.trim().toLowerCase()
158
- if (!q) return filtered.value
159
- return filtered.value.filter(row =>
160
- props.columns.some(col => searchOf(col, row).includes(q)),
161
- )
162
- })
163
-
164
- function sortKey(col: DataTableColumn<T>, row: T) {
165
- if (col.sortGetter) return col.sortGetter(row)
166
- const raw = (row as Record<string, unknown>)[col.key]
167
- return raw as never
168
- }
169
-
170
- const sorted = computed(() => {
171
- if (props.serverMode) return searched.value
172
- const col = sort.value.column
173
- const dir = sort.value.direction
174
- if (!col || !dir) return searched.value
175
- const c = props.columns.find(x => x.key === col)
176
- if (!c) return searched.value
177
- const sign = dir === 'asc' ? 1 : -1
178
- return [...searched.value].sort((a, b) => {
179
- const av = sortKey(c, a)
180
- const bv = sortKey(c, b)
181
- const aNull = av == null || av === ''
182
- const bNull = bv == null || bv === ''
183
- if (aNull && bNull) return 0
184
- if (aNull) return 1
185
- if (bNull) return -1
186
- const aCmp = typeof av === 'boolean' ? (av ? 1 : 0) : av
187
- const bCmp = typeof bv === 'boolean' ? (bv ? 1 : 0) : bv
188
- if (aCmp < bCmp) return -1 * sign
189
- if (aCmp > bCmp) return 1 * sign
190
- return 0
191
- })
192
- })
193
-
194
- const totalCount = computed(() =>
195
- props.serverMode ? (props.total ?? props.data.length) : sorted.value.length,
196
- )
197
-
198
- const paginated = computed(() => {
199
- if (props.serverMode || !props.pagination) return sorted.value
200
- const start = page.value * pageSize.value
201
- return sorted.value.slice(start, start + pageSize.value)
202
- })
203
-
204
- const displayedRows = computed(() => paginated.value)
205
-
206
- // Guarantee unique v-for keys. External data sources (e.g. the mail gateway's
207
- // queued/held messages) can hand back rows with a null or duplicated row-key;
208
- // duplicate keys make Vue's keyed patch crash on the next re-sort/re-filter
209
- // with "Cannot read properties of null (reading 'nextSibling')". Suffixing
210
- // repeats keeps every key distinct without changing what the page passed.
211
- const keyedRows = computed<{ row: T, key: string | number }[]>(() => {
212
- const seen = new Map<string | number, number>()
213
- return displayedRows.value.map((row) => {
214
- const base = rowKeyFor(row) ?? '∅'
215
- const n = seen.get(base) ?? 0
216
- seen.set(base, n + 1)
217
- return { row, key: n === 0 ? base : `${base}#${n}` }
218
- })
219
- })
220
-
221
- // ---------------------------------------------------------------------------
222
- // State-change hook for server mode
223
- // ---------------------------------------------------------------------------
224
- function emitState() {
225
- emit('stateChange', {
226
- search: query.value,
227
- sort: { ...sort.value },
228
- page: page.value,
229
- pageSize: pageSize.value,
230
- })
231
- }
232
- watch([query, sort, page, pageSize], () => {
233
- if (props.serverMode) emitState()
234
- })
235
-
236
- // Reset to first page whenever search / sort / filter / page-size changes.
237
- watch([query, sort, pageSize, activeFilters], () => {
238
- page.value = 0
239
- }, { deep: true })
240
-
241
- // ---------------------------------------------------------------------------
242
- // Header sort
243
- // ---------------------------------------------------------------------------
244
- function toggleSort(column: string) {
245
- if (sort.value.column !== column) {
246
- sort.value = { column, direction: 'asc' }
247
- return
248
- }
249
- if (sort.value.direction === 'asc') {
250
- sort.value = { column, direction: 'desc' }
251
- return
252
- }
253
- sort.value = { column: null, direction: null }
254
- }
255
-
256
- // ---------------------------------------------------------------------------
257
- // Misc
258
- // ---------------------------------------------------------------------------
259
- function rowKeyFor(row: T): string | number {
260
- if (typeof props.rowKey === 'function') return props.rowKey(row)
261
- return row[props.rowKey] as unknown as string | number
262
- }
263
-
264
- const gridTemplate = computed(() => {
265
- const tracks = visibleColumns.value.map(c => c.width ?? 'minmax(0,1fr)')
266
- return `grid-template-columns: ${tracks.join(' ')};`
267
- })
268
-
269
- const placeholder = computed(() =>
270
- typeof props.search === 'object' ? (props.search.placeholder ?? 'Search…') : 'Search…',
271
- )
272
-
273
- const slots = useSlots()
274
- const showToolbar = computed(() =>
275
- props.search || props.columnsMenu || props.filter || !!props.filterTabs || !!slots.actions || !!slots['toolbar-left'],
276
- )
277
-
278
- // Leading icon tile for a (primary) column. `cellIcon` is a component — we must
279
- // NOT call it: Solar icons are functional components (typeof === 'function'), so
280
- // a `typeof ci === 'function'` "is this a getter?" check wrongly invokes the
281
- // icon as `icon(row)` and it crashes destructuring its render context. For a
282
- // per-row icon, use a `#cell-<key>` slot instead.
283
- function cellIconFor(col: DataTableColumn<T>): Component | null {
284
- return col.cellIcon ?? null
285
- }
286
- const showFooter = computed(() => props.pagination !== false)
287
-
288
- // Pagination
289
- const lastPage = computed(() =>
290
- pageSize.value > 0 ? Math.max(0, Math.ceil(totalCount.value / pageSize.value) - 1) : 0,
291
- )
292
- const rangeStart = computed(() => totalCount.value === 0 ? 0 : page.value * pageSize.value + 1)
293
- const rangeEnd = computed(() =>
294
- Math.min((page.value + 1) * pageSize.value, totalCount.value),
295
- )
296
-
297
- function prevPage() {
298
- if (page.value > 0) page.value -= 1
299
- }
300
- function nextPage() {
301
- if (page.value < lastPage.value) page.value += 1
302
- }
303
-
304
- // Default cell value when no slot is provided.
305
- function defaultCell(col: DataTableColumn<T>, row: T): string {
306
- const v = col.sortGetter
307
- ? col.sortGetter(row)
308
- : (row as Record<string, unknown>)[col.key]
309
- if (v == null) return '—'
310
- return String(v)
311
- }
312
- </script>
313
-
314
- <template>
315
- <div class="rounded-lg border border-black/[0.06] overflow-hidden flex flex-col">
316
- <!-- Toolbar ------------------------------------------------------ -->
317
- <!-- Toolbar: LEFT = search · filter · columns · RIGHT = actions.
318
- (Pagination always lives in the footer.) -->
319
- <div
320
- v-if="showToolbar"
321
- class="flex items-center justify-between gap-2 px-4 py-3 bg-white/55 backdrop-blur-md"
322
- >
323
- <div class="flex items-center gap-2 min-w-0">
324
- <div v-if="search" class="relative w-[21rem] max-w-full">
325
- <Search :size="13" class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" />
326
- <Input
327
- v-model="query"
328
- :placeholder="placeholder"
329
- class="h-7 pl-8 text-sm bg-white"
330
- />
331
- </div>
332
- <!-- Generic "Filter" dropdown — each dimension is a labelled sub-menu. -->
333
- <DropdownMenu v-if="filterDims.length">
334
- <DropdownMenuTrigger as-child>
335
- <Button
336
- variant="outline"
337
- size="sm"
338
- class="h-7 gap-1.5 text-sm"
339
- :class="anyFilterActive ? 'border-primary/30 text-primary' : ''"
340
- >
341
- <Filter :size="14" />
342
- Filter
343
- <AltArrowDown :size="12" class="opacity-60" />
344
- </Button>
345
- </DropdownMenuTrigger>
346
- <DropdownMenuContent align="start" class="w-48">
347
- <DropdownMenuSub v-for="d in filterDims" :key="d.key">
348
- <DropdownMenuSubTrigger class="gap-2">
349
- <span class="flex-1">{{ d.label }}</span>
350
- <span v-if="activeFilters[d.key]" class="text-xs text-primary capitalize">{{ activeFilters[d.key] }}</span>
351
- </DropdownMenuSubTrigger>
352
- <DropdownMenuSubContent class="w-48">
353
- <DropdownMenuItem class="gap-2" @select="setFilter(d.key, null)">
354
- <span class="flex-1">{{ d.allLabel ?? 'All' }}</span>
355
- <span class="tabular-nums text-muted-foreground">{{ data.length }}</span>
356
- </DropdownMenuItem>
357
- <DropdownMenuSeparator />
358
- <DropdownMenuItem
359
- v-for="o in d.options"
360
- :key="o.value"
361
- class="gap-2"
362
- @select="setFilter(d.key, o.value)"
363
- >
364
- <span class="flex-1">{{ o.label }}</span>
365
- <span class="tabular-nums text-muted-foreground">{{ dimCounts(d)[o.value] ?? 0 }}</span>
366
- </DropdownMenuItem>
367
- </DropdownMenuSubContent>
368
- </DropdownMenuSub>
369
- </DropdownMenuContent>
370
- </DropdownMenu>
371
- <FilterMenu v-if="filter">
372
- <slot name="filter-menu" />
373
- </FilterMenu>
374
- <ColumnsMenu
375
- v-if="columnsMenu"
376
- :columns="menuColumns"
377
- :visible="visibleKeys"
378
- @toggle="toggleColumn"
379
- />
380
- <slot name="toolbar-left" />
381
- </div>
382
- <div class="flex items-center gap-1.5 ml-auto shrink-0">
383
- <slot name="actions" />
384
- </div>
385
- </div>
386
-
387
- <!-- Header row -------------------------------------------------- -->
388
- <div
389
- class="grid items-center gap-2 border-t rounded-t-lg border-gray-200 bg-gray-100 px-4 py-2.5 text-sm font-medium uppercase tracking-normal text-muted-foreground"
390
- :style="gridTemplate"
391
- >
392
- <div
393
- v-for="col in visibleColumns"
394
- :key="col.key"
395
- :class="col.alignRight ? 'text-right' : ''"
396
- >
397
- <SortHeader
398
- v-if="col.sortable"
399
- :column="col.key"
400
- :active="sort.column"
401
- :direction="sort.direction"
402
- :align-right="col.alignRight"
403
- @toggle="toggleSort"
404
- >
405
- {{ col.label }}
406
- </SortHeader>
407
- <span v-else>{{ col.label }}</span>
408
- </div>
409
- </div>
410
-
411
- <!-- Body --------------------------------------------------------- -->
412
- <div
413
- v-for="{ row, key } in keyedRows"
414
- :key="key"
415
- class="group grid items-center gap-2 border-t border-black/[0.04] px-4 py-3 cursor-pointer bg-gray-50 transition-colors"
416
- :style="gridTemplate"
417
- @click="emit('rowClick', row)"
418
- >
419
- <div
420
- v-for="col in visibleColumns"
421
- :key="col.key"
422
- class="min-w-0"
423
- :class="col.alignRight ? 'text-right' : ''"
424
- >
425
- <slot :name="`cell-${col.key}`" :row="row">
426
- <!-- Primary column: bold record anchor + optional tinted icon tile. -->
427
- <div v-if="col.primary" class="flex items-center gap-2.5 min-w-0">
428
- <span
429
- v-if="cellIconFor(col)"
430
- class="flex size-7 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary"
431
- >
432
- <component :is="cellIconFor(col)" :size="13" />
433
- </span>
434
- <span class="text-sm font-semibold text-[#0B0522] truncate transition-colors group-hover:text-primary">{{ defaultCell(col, row) }}</span>
435
- </div>
436
- <span v-else class="text-sm truncate">{{ defaultCell(col, row) }}</span>
437
- </slot>
438
- </div>
439
- </div>
440
-
441
- <!-- Loading skeleton (only when there are no rows yet) ----------- -->
442
- <template v-if="loading && !displayedRows.length">
443
- <div
444
- v-for="n in 6"
445
- :key="`sk-${n}`"
446
- class="grid items-center gap-2 border-t border-black/[0.04] bg-gray-50 px-4 py-3"
447
- :style="gridTemplate"
448
- >
449
- <div
450
- v-for="col in visibleColumns"
451
- :key="col.key"
452
- :class="col.alignRight ? 'flex justify-end' : ''"
453
- >
454
- <div
455
- class="h-3 rounded bg-black/[0.07] animate-pulse"
456
- :class="col.alignRight ? 'w-10' : 'w-2/3'"
457
- />
458
- </div>
459
- </div>
460
- </template>
461
-
462
- <!-- Empty -------------------------------------------------------- -->
463
- <div
464
- v-if="!displayedRows.length && !loading"
465
- class="px-3 py-8 text-center text-sm text-muted-foreground"
466
- >
467
- <slot name="empty">
468
- {{ emptyText ?? 'No entries.' }}
469
- </slot>
470
- </div>
471
-
472
- <!-- Footer ------------------------------------------------------- -->
473
- <div
474
- v-if="showFooter"
475
- class="flex items-center justify-between gap-2 border-t border-gray-200 bg-white/55 backdrop-blur-md px-4 py-3 text-sm text-muted-foreground/60"
476
- >
477
- <div class="flex items-center gap-2.5">
478
- <span class="tabular-nums">
479
- <span v-if="totalCount === 0">No results</span>
480
- <span v-else>Showing {{ rangeStart }}–{{ rangeEnd }} of {{ totalCount }}</span>
481
- </span>
482
- <span class="text-muted-foreground/40">-</span>
483
- <label class="flex items-center gap-1.5">
484
- <span class="hidden sm:inline">Rows per page</span>
485
- <select
486
- v-model.number="pageSize"
487
- class="h-7 rounded-md border border-input bg-white px-1.5 text-sm tabular-nums focus:outline-none focus:ring-1 focus:ring-ring"
488
- >
489
- <option v-for="s in pageSizeOptions" :key="s" :value="s">{{ s }}</option>
490
- </select>
491
- </label>
492
- </div>
493
- <div v-if="lastPage > 0" class="flex items-center gap-2">
494
- <Button
495
- variant="outline"
496
- size="sm"
497
- class="h-7 text-sm"
498
- :disabled="page === 0 || loading"
499
- @click="prevPage"
500
- >
501
- Previous
502
- </Button>
503
- <span class="tabular-nums">
504
- {{ page + 1 }} / {{ lastPage + 1 }}
505
- </span>
506
- <Button
507
- variant="outline"
508
- size="sm"
509
- class="h-7 text-sm"
510
- :disabled="page >= lastPage || loading"
511
- @click="nextPage"
512
- >
513
- Next
514
- </Button>
515
- </div>
516
- </div>
517
- </div>
518
- </template>
@@ -1,82 +0,0 @@
1
- // Shared types for the cockpit-wide DataTable component.
2
- //
3
- // A page builds a `columns` array (mapping data fields to display +
4
- // behaviour) and passes it to `<DataTable>` together with its source
5
- // rows. Sorting, text search, column visibility and pagination are
6
- // handled inside the table; backend-driven modes can take over by
7
- // setting `server-mode` and reacting to the emitted `state-change`.
8
-
9
- import type { Component } from 'vue'
10
-
11
- export type Comparable = string | number | boolean | Date | null | undefined
12
-
13
- export interface DataTableColumn<T = unknown> {
14
- /** Stable key — matches the slot name `cell-<key>` and is used in the
15
- * sort state. */
16
- key: string
17
- /** Header label. */
18
- label: string
19
- /** CSS grid track. Defaults to `minmax(0,1fr)` for text columns; pass
20
- * a fixed value like `120px` or `minmax(0,140px)` for compact ones. */
21
- width?: string
22
- /** Enable sort affordance on this column. */
23
- sortable?: boolean
24
- /** Returns the sort key for the row. Falls back to `row[key]` if it
25
- * looks like a primitive. ISO date strings sort correctly as-is. */
26
- sortGetter?: (row: T) => Comparable
27
- /** Returns the string a text-search query is matched against. Falls
28
- * back to the stringified sortGetter result. */
29
- searchGetter?: (row: T) => string
30
- /** Initial visibility (default `true`). */
31
- visible?: boolean
32
- /** Hide-able via the column menu (default `true`). */
33
- toggleable?: boolean
34
- /** Right-align cell content (numeric columns). */
35
- alignRight?: boolean
36
- /** Optional icon component for the header. */
37
- icon?: Component
38
- /** Marks the identifying column. When set (and no `#cell-<key>` slot is
39
- * given) the DataTable renders the value as the bold record anchor —
40
- * `font-semibold text-[#0B0522]`, truncated — optionally led by a tinted
41
- * icon tile (see `cellIcon`). One per table. (datatable-ux) */
42
- primary?: boolean
43
- /** Leading tinted icon tile for the cell (typically the `primary` column).
44
- * A static icon component, rendered as the `bg-primary/10` rounded tile the
45
- * scanning rules call for. (For a per-row icon use a `#cell-<key>` slot —
46
- * `cellIcon` must NOT be a getter: Solar icons are functional components, so
47
- * the table can't tell a getter from an icon and would crash invoking it.) */
48
- cellIcon?: Component
49
- }
50
-
51
- /** A built-in filter dimension rendered under the generic "Filter" dropdown as
52
- * a labelled sub-menu (datatable-ux). Static mode only: the DataTable filters
53
- * `data` in-memory and shows a count per option. Pass one, or an array of
54
- * several dimensions — each becomes its own sub-menu. */
55
- export interface DataTableFilter<T = unknown> {
56
- /** Stable id for this dimension (defaults to `label`). */
57
- key?: string
58
- /** Sub-menu title, e.g. "Status" (default `Status`). */
59
- label?: string
60
- /** The row's value for this dimension (e.g. `r => r.status`). */
61
- getter: (row: T) => string | null | undefined
62
- /** The selectable values + their labels. */
63
- options: { value: string, label: string }[]
64
- /** Label for the reset/"all" entry (default `All`). */
65
- allLabel?: string
66
- }
67
-
68
- export type SortDirection = 'asc' | 'desc' | null
69
-
70
- export interface SortState {
71
- column: string | null
72
- direction: SortDirection
73
- }
74
-
75
- export interface DataTableState {
76
- search: string
77
- sort: SortState
78
- page: number
79
- pageSize: number
80
- }
81
-
82
- export type RowKey<T> = keyof T | ((row: T) => string | number)
@@ -1,50 +0,0 @@
1
- <script setup lang="ts">
2
- import { RoundSortVertical, SortFromBottomToTop, SortFromTopToBottom } from '#solar-icons'
3
-
4
- // Clickable column header for data tables (datatable-ux). One Solar sort
5
- // glyph that switches with the sort state — matching cover's tokens:
6
- // idle → round-sort-vertical
7
- // asc → sort-from-bottom-to-top
8
- // desc → sort-from-top-to-bottom
9
- // Outline weight (Linear), never duotone. Active = primary accent;
10
- // inactive = dimmed, revealed on hover.
11
- //
12
- // <SortHeader column="name" :active="sort.column"
13
- // :direction="sort.direction" @toggle="toggle">Name</SortHeader>
14
-
15
- const props = defineProps<{
16
- column: string
17
- active: string | null
18
- direction: 'asc' | 'desc' | null
19
- /** Right-align the indicator for numeric columns. */
20
- alignRight?: boolean
21
- }>()
22
-
23
- const emit = defineEmits<{ toggle: [column: string] }>()
24
-
25
- const isActive = computed(() => props.active === props.column && !!props.direction)
26
- const sortIcon = computed(() => {
27
- if (isActive.value && props.direction === 'asc') return SortFromBottomToTop
28
- if (isActive.value && props.direction === 'desc') return SortFromTopToBottom
29
- return RoundSortVertical
30
- })
31
- </script>
32
-
33
- <template>
34
- <button
35
- type="button"
36
- class="group inline-flex cursor-pointer items-center gap-1.5 -m-1 p-1 rounded-md text-left transition-colors hover:text-primary"
37
- :class="alignRight ? 'flex-row-reverse w-full justify-end' : ''"
38
- @click="emit('toggle', column)"
39
- >
40
- <slot />
41
- <component
42
- :is="sortIcon"
43
- weight="Linear"
44
- :size="14"
45
- class="shrink-0 transition-colors"
46
- :class="isActive ? 'text-primary' : 'text-foreground/30 group-hover:text-primary'"
47
- aria-hidden="true"
48
- />
49
- </button>
50
- </template>
@@ -1,24 +0,0 @@
1
- <script setup lang="ts">
2
- /**
3
- * The standard cockpit card — the 2-layer "glass" used on the dashboard:
4
- * an outer transparent ring (hairline border + white outline) wrapping a solid
5
- * white inner pane clipped to the radius. Use everywhere instead of a single
6
- * bordered div. Optional `to` makes the whole card a link.
7
- */
8
- import { NuxtLink } from '#components'
9
-
10
- defineProps<{ to?: string }>()
11
- </script>
12
-
13
- <template>
14
- <component
15
- :is="to ? NuxtLink : 'div'"
16
- :to="to"
17
- class="block rounded-lg border border-gray-400/10 outline-1 outline-white/30"
18
- :class="to ? 'transition-transform hover:-translate-y-0.5' : ''"
19
- >
20
- <div class="relative h-full overflow-hidden rounded-lg bg-white">
21
- <slot />
22
- </div>
23
- </component>
24
- </template>