@revenexx/studio-shared 0.3.2 → 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 +36 -3
  44. package/dist/runtime/components/DataTable/DataTable.vue +252 -27
  45. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +36 -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 +3 -2
@@ -19,7 +19,10 @@ import {
19
19
  import { orderColumnsBy } from "../../lib/columnOrder";
20
20
  import { initiallyHidden, visibleColumnsOf } from "../../lib/columnVisibility";
21
21
  import { countByValue, matchesFilter } from "../../lib/dataTableFilters";
22
+ import { gridTemplateColumns } from "../../lib/gridTracks";
22
23
  import SortHeader from "../Shared/SortHeader.vue";
24
+ import FacetChips from "./FacetChips.vue";
25
+ import FacetPanel from "./FacetPanel.vue";
23
26
  import FilterMenu from "./FilterMenu.vue";
24
27
  import SavedViewsMenu from "./SavedViewsMenu.vue";
25
28
  import SortMenu from "./SortMenu.vue";
@@ -31,10 +34,12 @@ const props = defineProps({
31
34
  columnsMenu: { type: Boolean, required: false, default: true },
32
35
  filter: { type: [Boolean, Object], required: false, default: false },
33
36
  filterTabs: { type: [Object, Array], required: false },
37
+ facets: { type: Array, required: false, default: () => [] },
38
+ chipFacetLabels: { type: Boolean, required: false, default: false },
34
39
  savedViews: { type: [Object, null], required: false, default: null },
35
40
  viewModes: { type: [Array, null], required: false, default: null },
36
41
  rail: { type: [Boolean, Object], required: false, default: false },
37
- scrollBody: { type: [Boolean, Object], required: false },
42
+ scrollBody: { type: [Boolean, String, Object], required: false },
38
43
  pagination: { type: [Boolean, Object], required: false, default: true },
39
44
  defaultSort: { type: Object, required: false },
40
45
  serverMode: { type: Boolean, required: false, default: false },
@@ -56,6 +61,8 @@ const sort = defineModel("sort", { type: Object, ...{
56
61
  const page = defineModel("page", { type: Number, ...{ default: 0 } });
57
62
  const viewMode = defineModel("viewMode", { type: String, ...{ default: "list" } });
58
63
  const railOpen = defineModel("railOpen", { type: Boolean, ...{ default: true } });
64
+ const facetSelection = defineModel("facetSelection", { type: Object, ...{ default: () => ({}) } });
65
+ const panelFacets = computed(() => props.facets);
59
66
  if (props.defaultSort && sort.value.column == null) {
60
67
  sort.value = { column: props.defaultSort.column, direction: props.defaultSort.direction };
61
68
  }
@@ -233,10 +240,8 @@ function rowKeyFor(row) {
233
240
  if (typeof props.rowKey === "function") return props.rowKey(row);
234
241
  return row[props.rowKey];
235
242
  }
236
- const gridTemplate = computed(() => {
237
- const tracks = visibleColumns.value.map((c) => c.width ?? "minmax(0,1fr)");
238
- return `grid-template-columns: ${tracks.join(" ")};`;
239
- });
243
+ const gridTemplate = computed(() => `grid-template-columns: ${gridTemplateColumns(visibleColumns.value.map((c) => c.width))};`);
244
+ const SUBGRID_ROW = "grid-column: 1 / -1; grid-template-columns: subgrid;";
240
245
  const placeholder = computed(
241
246
  () => typeof props.search === "object" ? props.search.placeholder ?? t("shared.datatable.toolbar.search.placeholder.default") : t("shared.datatable.toolbar.search.placeholder.default")
242
247
  );
@@ -249,7 +254,7 @@ const showToolbar = computed(
249
254
  () => props.search || props.columnsMenu || props.filter || !!props.filterTabs || !!props.savedViews || !!props.viewModes?.length || !!props.rail || !!slots.actions || !!slots.scope || !!slots["toolbar-left"] || !!slots["toolbar-right"]
250
255
  );
251
256
  const railLabel = computed(
252
- () => (typeof props.rail === "object" ? props.rail.label : void 0) ?? t("shared.datatable.toolbar.rail.label.default")
257
+ () => (typeof props.rail === "object" ? props.rail.label : void 0) ?? null
253
258
  );
254
259
  const showTable = computed(() => !props.viewModes?.length || viewMode.value === "list");
255
260
  const sortableColumns = computed(() => props.columns.filter((c) => c.sortable));
@@ -257,8 +262,121 @@ const showSortMenu = computed(() => !showTable.value && sortableColumns.value.le
257
262
  function cellIconFor(col) {
258
263
  return col.cellIcon ?? null;
259
264
  }
265
+ const scrollEl = ref(null);
266
+ const footerEl = ref(null);
267
+ const fitHeight = ref(null);
268
+ const MIN_FIT_PX = 220;
269
+ const BOTTOM_GAP_PX = 16;
270
+ function scrollParent(el) {
271
+ let node = el.parentElement;
272
+ while (node && node !== document.body) {
273
+ const overflow = getComputedStyle(node).overflowY;
274
+ if (overflow === "auto" || overflow === "scroll") return node;
275
+ node = node.parentElement;
276
+ }
277
+ return document.documentElement;
278
+ }
279
+ function measureFit() {
280
+ if (props.scrollBody !== "fit" || !import.meta.client) return;
281
+ const el = scrollEl.value;
282
+ if (!el) return;
283
+ const container = scrollParent(el);
284
+ const offset = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
285
+ const reserve = (footerEl.value?.offsetHeight ?? 0) + BOTTOM_GAP_PX;
286
+ const overflow = Math.max(container.scrollHeight - container.clientHeight, 0);
287
+ const available = Math.max(
288
+ Math.floor(container.clientHeight - offset - reserve - overflow) - 1,
289
+ MIN_FIT_PX
290
+ );
291
+ const current = fitHeight.value ? Number.parseInt(fitHeight.value, 10) : null;
292
+ if (current !== null && Math.abs(current - available) < 8) return;
293
+ fitHeight.value = `${available}px`;
294
+ }
295
+ if (import.meta.client) {
296
+ let frame = 0;
297
+ let timers = [];
298
+ const schedule = () => {
299
+ if (frame) return;
300
+ frame = requestAnimationFrame(() => {
301
+ frame = 0;
302
+ measureFit();
303
+ });
304
+ };
305
+ onMounted(() => {
306
+ measureFit();
307
+ window.addEventListener("resize", schedule);
308
+ timers = [setTimeout(schedule, 250), setTimeout(schedule, 1200)];
309
+ });
310
+ onBeforeUnmount(() => {
311
+ window.removeEventListener("resize", schedule);
312
+ for (const timer of timers) clearTimeout(timer);
313
+ timers = [];
314
+ if (frame) cancelAnimationFrame(frame);
315
+ });
316
+ watch(() => [props.data.length, props.loading], () => nextTick(() => {
317
+ schedule();
318
+ syncThumb();
319
+ }));
320
+ onMounted(() => {
321
+ nextTick(syncThumb);
322
+ window.addEventListener("resize", syncThumb);
323
+ });
324
+ onBeforeUnmount(() => window.removeEventListener("resize", syncThumb));
325
+ watch(fitHeight, () => nextTick(syncThumb));
326
+ }
327
+ const headerRowEl = ref(null);
328
+ const headerHeight = ref(0);
329
+ const rowsOverflow = ref(false);
330
+ const thumbHeight = ref(0);
331
+ const thumbOffset = ref(0);
332
+ const MIN_THUMB_PX = 32;
333
+ const drawnBarSupported = import.meta.client && typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("selector(::-webkit-scrollbar)");
334
+ function syncThumb() {
335
+ const el = scrollEl.value;
336
+ if (!props.scrollBody || !el) return;
337
+ headerHeight.value = headerRowEl.value?.offsetHeight ?? 0;
338
+ const trackHeight = el.clientHeight - headerHeight.value;
339
+ const scrollable = el.scrollHeight - el.clientHeight;
340
+ rowsOverflow.value = drawnBarSupported && scrollable > 1 && trackHeight > MIN_THUMB_PX;
341
+ if (!rowsOverflow.value) return;
342
+ const ratio = el.clientHeight / el.scrollHeight;
343
+ const height = Math.max(Math.round(trackHeight * ratio), MIN_THUMB_PX);
344
+ thumbHeight.value = height;
345
+ thumbOffset.value = Math.round(el.scrollTop / scrollable * (trackHeight - height));
346
+ }
347
+ function onRowsScroll() {
348
+ syncThumb();
349
+ }
350
+ function startThumbDrag(event) {
351
+ const el = scrollEl.value;
352
+ if (!el) return;
353
+ event.preventDefault();
354
+ const startY = event.clientY;
355
+ const startScroll = el.scrollTop;
356
+ const trackHeight = el.clientHeight - headerHeight.value;
357
+ const scrollable = el.scrollHeight - el.clientHeight;
358
+ const travel = trackHeight - thumbHeight.value;
359
+ if (travel <= 0) return;
360
+ const target = event.target;
361
+ target.setPointerCapture(event.pointerId);
362
+ const move = (e) => {
363
+ el.scrollTop = startScroll + (e.clientY - startY) / travel * scrollable;
364
+ };
365
+ const up = (e) => {
366
+ target.releasePointerCapture(e.pointerId);
367
+ target.removeEventListener("pointermove", move);
368
+ target.removeEventListener("pointerup", up);
369
+ target.removeEventListener("pointercancel", up);
370
+ };
371
+ target.addEventListener("pointermove", move);
372
+ target.addEventListener("pointerup", up);
373
+ target.addEventListener("pointercancel", up);
374
+ }
260
375
  const scrollStyle = computed(() => {
261
376
  if (!props.scrollBody) return void 0;
377
+ if (props.scrollBody === "fit") {
378
+ return fitHeight.value ? { maxHeight: fitHeight.value } : void 0;
379
+ }
262
380
  const configured = typeof props.scrollBody === "object" ? props.scrollBody.maxHeight : void 0;
263
381
  return { maxHeight: configured ?? "calc(100vh - 22rem)" };
264
382
  });
@@ -329,13 +447,16 @@ function defaultCell(col, row) {
329
447
  variant="outline"
330
448
  size="sm"
331
449
  class="h-7 shrink-0 gap-1.5 text-sm"
332
- :class="railOpen ? 'border-primary/30 text-primary' : ''"
450
+ :class="[
451
+ railOpen ? 'border-primary/30 text-primary' : '',
452
+ railLabel ? '' : 'w-7 px-0 justify-center'
453
+ ]"
333
454
  :aria-pressed="railOpen"
334
- :aria-label="railOpen ? t('shared.datatable.toolbar.rail.aria.hide', { label: railLabel }) : t('shared.datatable.toolbar.rail.aria.show', { label: railLabel })"
455
+ :aria-label="railLabel ? railOpen ? t('shared.datatable.toolbar.rail.aria.hide', { label: railLabel }) : t('shared.datatable.toolbar.rail.aria.show', { label: railLabel }) : railOpen ? t('shared.datatable.toolbar.rail.aria.hideUnnamed') : t('shared.datatable.toolbar.rail.aria.showUnnamed')"
335
456
  @click="railOpen = !railOpen"
336
457
  >
337
458
  <SidebarMinimalistic :size="14" />
338
- {{ railLabel }}
459
+ <template v-if="railLabel">{{ railLabel }}</template>
339
460
  </Button>
340
461
  <!-- Shrinks (never wraps the row) once the toolbar carries scope /
341
462
  view-mode / saved-view controls next to it.
@@ -384,8 +505,20 @@ function defaultCell(col, row) {
384
505
  <Filter :size="14" weight="BoldDuotone" />
385
506
  </template>
386
507
  </FilterPanel>
508
+ <!--
509
+ The one filter control, for a surface that owns its own query.
510
+
511
+ Drawn in the same slot as the other two so a surface never shows two
512
+ filter triggers: `facets` and `filter` are alternatives, and
513
+ `filterTabs` is the in-memory path this replaces.
514
+ -->
515
+ <FacetPanel
516
+ v-if="panelFacets.length"
517
+ v-model="facetSelection"
518
+ :facets="facets"
519
+ />
387
520
  <FilterMenu
388
- v-if="filter"
521
+ v-if="filter && !panelFacets.length"
389
522
  :width="typeof filter === 'object' ? filter.width : void 0"
390
523
  :active="typeof filter === 'object' ? filter.active : void 0"
391
524
  :count="typeof filter === 'object' ? filter.count : void 0"
@@ -422,23 +555,51 @@ function defaultCell(col, row) {
422
555
  <EyeClosed :size="16" weight="Linear" />
423
556
  </template>
424
557
  </ColumnPicker>
558
+ <!--
559
+ Saved views, beside the columns picker rather than out past the market
560
+ scope and the view-mode switch.
561
+
562
+ It belongs with what it saves. Filters, sort and columns are the three
563
+ things a view is made of, so the control that names one sits at the end
564
+ of that group — not after the market scope, which is not part of a
565
+ view, or the view-mode switch, which is about what you are looking at.
566
+ -->
567
+ <SavedViewsMenu v-if="savedViews" :config="savedViews" />
425
568
  <!-- Market scope — the surface passes the shared MarketScopeToggle in;
426
569
  the row position is fixed here. -->
427
570
  <slot name="scope" />
428
571
  <!-- The editor's device switch, which is the right shape wherever the
429
572
  choice changes what you are LOOKING AT rather than which rows. -->
430
573
  <SegmentedToggle v-if="viewModes?.length" v-model="viewMode" variant="inset">
574
+ <!--
575
+ Violet on hover, and violet while it is the one in effect — the
576
+ same language the filter trigger, the column picker and the saved
577
+ views speak two controls to the left. The track's own variant only
578
+ lifts the active segment onto a white pill, which says WHICH is
579
+ picked but not that the row is a set of live controls.
580
+
581
+ The hover is written against `data-[state=off]` and marked
582
+ important because the design system's own toggle already ships
583
+ `data-[state=off]:hover:text-foreground`. A plain `hover:` loses to
584
+ it on specificity, and matching the specificity alone would leave
585
+ the winner to whatever order Tailwind happened to emit the two
586
+ utilities in — a coin toss that reads as a bug the day it lands the
587
+ other way. Delete the `!` if the toggle's own hover ever goes violet.
588
+
589
+ `aria-label` because these are icon-only: the mode still has to
590
+ announce itself.
591
+ -->
431
592
  <SegmentedToggleItem
432
593
  v-for="m in viewModes"
433
594
  :key="m.value"
434
595
  :value="m.value"
435
596
  :aria-label="m.label"
597
+ class="transition-colors data-[state=on]:text-primary data-[state=off]:hover:text-primary!"
436
598
  >
437
599
  <component :is="m.icon" v-if="m.icon" :size="14" weight="Bold" />
438
600
  <template v-if="!m.icon">{{ m.label }}</template>
439
601
  </SegmentedToggleItem>
440
602
  </SegmentedToggle>
441
- <SavedViewsMenu v-if="savedViews" :config="savedViews" />
442
603
  <slot name="toolbar-left" />
443
604
  </div>
444
605
  <div class="flex items-center gap-2 ml-auto shrink-0">
@@ -447,15 +608,40 @@ function defaultCell(col, row) {
447
608
  </div>
448
609
  </div>
449
610
 
611
+ <!--
612
+ What is filtering right now. Not optional beside a paned panel: with it
613
+ closed, a count on an icon says that something narrows the list and never
614
+ what.
615
+ -->
616
+ <FacetChips
617
+ v-if="facets.length"
618
+ v-model="facetSelection"
619
+ :facets="facets"
620
+ :show-facet="chipFacetLabels"
621
+ />
622
+
450
623
  <!-- Alternate view mode (gallery, board, …) — same toolbar, same footer. -->
451
624
  <div v-if="!showTable" class="border-t border-black/[0.04] bg-white/55">
452
625
  <slot name="view" :rows="displayedRows" :loading="loading" />
453
626
  </div>
454
627
 
455
- <!-- The header and the rows share one box so their columns cannot drift.
456
- `display: contents` when scrolling is off makes this element vanish
457
- from layout entirely, so every table that has not asked for it renders
458
- exactly as it did.
628
+ <!-- Two boxes: one that scrolls, and the table itself.
629
+
630
+ The table is ONE GRID and the rows are subgrids of it (`SUBGRID_ROW`),
631
+ which is what keeps a heading over its own column. It used to be a grid
632
+ per row, and a content-sized track then measured itself separately in
633
+ each one — see `SUBGRID_ROW` for what that did to every list with row
634
+ actions.
635
+
636
+ The scrolling is the outer box's job, and it has to be, because the
637
+ grid carries `min-w-min`: a box that grows to its own content cannot
638
+ also be the thing that scrolls it. The panel around them clips
639
+ (`overflow-hidden`, for the rounded corners), so without this a table
640
+ whose declared widths outgrow the panel simply lost the excess.
641
+
642
+ The inner element used to be `display: contents` when scrolling was
643
+ off, which made it vanish from layout — and a box that is not in the
644
+ layout can be neither a grid nor a scroller.
459
645
 
460
646
  IT IS ALSO THE TABLE. The box that holds the header and the rows and
461
647
  nothing else is the only element in this component that can carry the
@@ -470,19 +656,27 @@ function defaultCell(col, row) {
470
656
  that. A static table read with a screen reader's own table commands is
471
657
  what these lists actually are, and both roles announce the column
472
658
  heading with the cell, which is the thing that was missing. -->
659
+ <div :class="scrollBody ? 'relative flex min-h-0 flex-col' : ''">
473
660
  <div
474
- :role="showTable ? 'table' : void 0"
475
- :aria-busy="showTable && loading ? 'true' : void 0"
476
- :class="scrollBody ? 'min-h-0 overflow-y-auto' : 'contents'"
661
+ ref="scrollEl"
662
+ :class="scrollBody ? 'rx-rows min-h-0 overflow-auto' : 'overflow-x-auto'"
477
663
  :style="scrollStyle"
664
+ @scroll="onRowsScroll"
478
665
  >
666
+ <div
667
+ :role="showTable ? 'table' : void 0"
668
+ :aria-busy="showTable && loading ? 'true' : void 0"
669
+ :class="showTable ? 'grid min-w-min gap-x-2' : ''"
670
+ :style="showTable ? gridTemplate : void 0"
671
+ >
479
672
  <!-- Header row -------------------------------------------------- -->
480
673
  <div
481
674
  v-if="showTable"
675
+ ref="headerRowEl"
482
676
  role="row"
483
- 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"
677
+ class="grid items-center gap-x-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"
484
678
  :class="scrollBody ? 'sticky top-0 z-10' : ''"
485
- :style="gridTemplate"
679
+ :style="SUBGRID_ROW"
486
680
  >
487
681
  <!-- `columnheader` is what names every cell underneath it, so this is
488
682
  the half of the fix that does the work. `aria-sort` goes with it:
@@ -494,6 +688,8 @@ function defaultCell(col, row) {
494
688
  :key="col.key"
495
689
  role="columnheader"
496
690
  :aria-sort="ariaSortFor(col)"
691
+ :title="col.label || void 0"
692
+ class="min-w-0"
497
693
  :class="col.alignRight ? 'text-right' : ''"
498
694
  >
499
695
  <!-- `#header-<key>` lets a page take over a header cell (e.g. a
@@ -514,7 +710,7 @@ function defaultCell(col, row) {
514
710
  the row's uppercase. Without it a column reads "LAST CHECK"
515
711
  beside "Channel" and "Provider" for no reason a reader can see —
516
712
  the header shouts wherever a column happens not to be sortable. -->
517
- <span v-else class="normal-case">{{ col.label }}</span>
713
+ <span v-else class="block truncate normal-case">{{ col.label }}</span>
518
714
  </slot>
519
715
  </div>
520
716
  </div>
@@ -527,8 +723,11 @@ function defaultCell(col, row) {
527
723
  <div
528
724
  v-if="entry.kind === 'header'"
529
725
  role="row"
530
- class="border-t border-black/[0.04] bg-gray-100/70 px-4 py-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground"
726
+ class="col-[1/-1] border-t border-black/[0.04] bg-gray-100/70 px-4 py-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground"
531
727
  >
728
+ <!-- Spans every column like the rows do, so a table wide enough to
729
+ scroll does not show this band stopping short of the rows it
730
+ groups. No subgrid: it holds one cell, not a set of columns. -->
532
731
  <div role="cell" :aria-colspan="visibleColumns.length" class="flex items-center gap-2">
533
732
  {{ entry.label }}
534
733
  <span class="tabular-nums normal-case text-muted-foreground/70">{{ entry.count }}</span>
@@ -537,8 +736,8 @@ function defaultCell(col, row) {
537
736
  <div
538
737
  v-else
539
738
  role="row"
540
- class="group grid items-center gap-2 border-t border-black/[0.04] px-4 py-3 cursor-pointer bg-gray-50 transition-colors"
541
- :style="gridTemplate"
739
+ class="group grid items-center gap-x-2 border-t border-black/[0.04] px-4 py-3 cursor-pointer bg-gray-50 transition-colors"
740
+ :style="SUBGRID_ROW"
542
741
  @click="emit('rowClick', entry.row)"
543
742
  >
544
743
  <div
@@ -578,8 +777,8 @@ function defaultCell(col, row) {
578
777
  v-for="n in 6"
579
778
  :key="`sk-${n}`"
580
779
  aria-hidden="true"
581
- class="grid items-center gap-2 border-t border-black/[0.04] bg-gray-50 px-4 py-3"
582
- :style="gridTemplate"
780
+ class="grid items-center gap-x-2 border-t border-black/[0.04] bg-gray-50 px-4 py-3"
781
+ :style="SUBGRID_ROW"
583
782
  >
584
783
  <div
585
784
  v-for="col in visibleColumns"
@@ -598,7 +797,7 @@ function defaultCell(col, row) {
598
797
  <div
599
798
  v-if="showTable && !displayedRows.length && !loading"
600
799
  role="row"
601
- class="px-3 py-8 text-center text-sm text-muted-foreground"
800
+ class="col-[1/-1] px-3 py-8 text-center text-sm text-muted-foreground"
602
801
  >
603
802
  <div role="cell" :aria-colspan="visibleColumns.length">
604
803
  <slot name="empty">
@@ -607,11 +806,33 @@ function defaultCell(col, row) {
607
806
  </div>
608
807
  </div>
609
808
 
809
+ </div>
810
+ </div>
811
+
812
+ <!-- The rows' scrollbar ---------------------------------------- -->
813
+ <!-- Drawn, not borrowed. A browser's own bar belongs to the scrolling
814
+ box, and the column head is inside that box — it has to be, because
815
+ the head and the rows are one grid and a `max-content` track resolves
816
+ per grid (PE-610). So the native bar runs beside the head, which is
817
+ not where a bar over the ROWS belongs. This one starts under the
818
+ head, ends at the footer, and drives the same native scrolling. -->
819
+ <div
820
+ v-if="scrollBody && rowsOverflow"
821
+ class="pointer-events-none absolute right-0 z-20 w-3.5"
822
+ :style="{ top: `${headerHeight}px`, bottom: '0px' }"
823
+ >
824
+ <div
825
+ class="rx-thumb pointer-events-auto absolute right-[3px] w-2 cursor-default rounded-full bg-[rgb(11_5_34_/_0.18)] transition-colors hover:bg-[rgb(11_5_34_/_0.32)]"
826
+ :style="{ height: `${thumbHeight}px`, transform: `translateY(${thumbOffset}px)` }"
827
+ @pointerdown="startThumbDrag"
828
+ />
829
+ </div>
610
830
  </div>
611
831
 
612
832
  <!-- Footer ------------------------------------------------------- -->
613
833
  <div
614
834
  v-if="showFooter"
835
+ ref="footerEl"
615
836
  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"
616
837
  >
617
838
  <div class="flex items-center gap-2.5">
@@ -656,3 +877,7 @@ function defaultCell(col, row) {
656
877
  </div>
657
878
  </div>
658
879
  </template>
880
+
881
+ <style scoped>
882
+ .rx-rows::-webkit-scrollbar{height:10px;width:0}.rx-rows::-webkit-scrollbar-track{background:transparent}.rx-rows::-webkit-scrollbar-thumb{background:rgba(11,5,34,.18);background-clip:padding-box;border:3px solid transparent;border-radius:9999px}.rx-thumb{min-height:32px}
883
+ </style>
@@ -1,3 +1,4 @@
1
+ import type { Facet, FacetSelection } from '../../lib/facets.js';
1
2
  import type { DataTableColumn, DataTableFilter, DataTableRail, DataTableSavedViews, DataTableState, DataTableViewMode, RowKey, SortState } from './types.js';
2
3
  declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
3
4
  props: __VLS_PrettifyLocal<Pick<Partial<{}> & Omit<{
@@ -5,9 +6,10 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
5
6
  readonly "onUpdate:page"?: ((value: number) => any) | undefined;
6
7
  readonly "onUpdate:viewMode"?: ((value: string) => any) | undefined;
7
8
  readonly "onUpdate:railOpen"?: ((value: boolean) => any) | undefined;
9
+ readonly "onUpdate:facetSelection"?: ((value: FacetSelection) => any) | undefined;
8
10
  readonly onRowClick?: ((row: T) => any) | undefined;
9
11
  readonly onStateChange?: ((state: DataTableState) => any) | undefined;
10
- } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:sort" | "onUpdate:page" | "onUpdate:viewMode" | "onUpdate:railOpen" | "onRowClick" | "onStateChange"> & ({
12
+ } & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>, "onUpdate:sort" | "onUpdate:page" | "onUpdate:viewMode" | "onUpdate:railOpen" | "onUpdate:facetSelection" | "onRowClick" | "onStateChange"> & ({
11
13
  data: T[];
12
14
  columns: DataTableColumn<T>[];
13
15
  rowKey: RowKey<T>;
@@ -28,6 +30,25 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
28
30
  /** Built-in status/dimension filter pills (static mode). One dimension, or
29
31
  * several (each renders as its own sub-menu). */
30
32
  filterTabs?: DataTableFilter<T> | DataTableFilter<T>[];
33
+ /**
34
+ * The surface's declared dimensions, as one facet model (PE-6xx).
35
+ *
36
+ * The THIRD filter path, and the one the others are being folded into. It
37
+ * differs from `filterTabs` in who does the work: those are filtered in
38
+ * memory here, which a server-paged list cannot use — a facet count taken
39
+ * over one page of fifty rows out of twenty-three thousand is a number
40
+ * nobody can reconcile with the table under it. Here the surface owns the
41
+ * query and the counts, and this component only draws the control and hands
42
+ * back a committed selection (`v-model:facet-selection`).
43
+ *
44
+ * EVERY declared dimension lives in this one panel, committed together, and
45
+ * reads back in one chip strip under the toolbar. There is deliberately no
46
+ * second placement: a tree pinned in a left column beside a panel is two
47
+ * places to look and two commit rules for one question.
48
+ */
49
+ facets?: Facet[];
50
+ /** Say which facet each chip belongs to (see `FacetChips.showFacet`). */
51
+ chipFacetLabels?: boolean;
31
52
  /** Saved views control — the surface supplies snapshot/apply, the table
32
53
  * owns the control, the storage and the dialogs. */
33
54
  savedViews?: DataTableSavedViews | null;
@@ -52,8 +73,13 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
52
73
  * sitting above it. That is not a detail: a scrollbar takes width from the
53
74
  * scrolling box, so a header outside it stays wider than the rows and every
54
75
  * column drifts by however wide the scrollbar is.
76
+ *
77
+ * `'fit'` measures instead of guessing: the rows get whatever height is left
78
+ * in the surrounding scroll container, so the table ends where the page ends
79
+ * and nothing below it is pushed out of sight. A short list still ends after
80
+ * its last row — the measurement is a MAXIMUM, not a height.
55
81
  */
56
- scrollBody?: boolean | {
82
+ scrollBody?: boolean | "fit" | {
57
83
  maxHeight?: string;
58
84
  };
59
85
  pagination?: boolean | {
@@ -74,6 +100,13 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
74
100
  page?: number;
75
101
  viewMode?: string;
76
102
  railOpen?: boolean;
103
+ /**
104
+ * The committed facet selection. Two-way so a chip removed under the toolbar,
105
+ * a facet picked in the surface's pinned rail and the panel's own Apply all
106
+ * write to one place — and so the surface can seed it from a saved view or a
107
+ * URL without this component holding a second copy to drift from.
108
+ */
109
+ facetSelection?: FacetSelection;
77
110
  }) & Partial<{}>> & import("vue").PublicProps;
78
111
  expose(exposed: import("vue").ShallowUnwrapRef<{}>): void;
79
112
  attrs: any;
@@ -101,7 +134,7 @@ declare const _default: <T>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>
101
134
  } & {
102
135
  empty?: (props: {}) => any;
103
136
  };
104
- emit: (((evt: "rowClick", row: T) => void) & ((evt: "stateChange", state: DataTableState) => void)) & (((evt: "update:sort", value: SortState) => void) & ((evt: "update:page", value: number) => void) & ((evt: "update:viewMode", value: string) => void) & ((evt: "update:railOpen", value: boolean) => void));
137
+ emit: (((evt: "rowClick", row: T) => void) & ((evt: "stateChange", state: DataTableState) => void)) & (((evt: "update:sort", value: SortState) => void) & ((evt: "update:page", value: number) => void) & ((evt: "update:viewMode", value: string) => void) & ((evt: "update:railOpen", value: boolean) => void) & ((evt: "update:facetSelection", value: FacetSelection) => void));
105
138
  }>) => import("vue").VNode & {
106
139
  __ctx?: Awaited<typeof __VLS_setup>;
107
140
  };
@@ -0,0 +1,16 @@
1
+ import type { Facet, FacetSelection } from '../../lib/facets.js';
2
+ type __VLS_Props = {
3
+ facets: Facet[];
4
+ modelValue: FacetSelection;
5
+ /**
6
+ * Say which facet each chip belongs to. On where "Draft" could be a status or
7
+ * a lifecycle; off where the values speak for themselves.
8
+ */
9
+ showFacet?: boolean;
10
+ };
11
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
+ "update:modelValue": (value: FacetSelection) => any;
13
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
14
+ "onUpdate:modelValue"?: ((value: FacetSelection) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ export default _default;
@@ -0,0 +1,72 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { CloseCircle } from "#solar-icons";
4
+ import { dateTimeFormat } from "../../lib/formatBridge";
5
+ import { appliedFacets, clearFacetValue } from "../../lib/facets";
6
+ const props = defineProps({
7
+ facets: { type: Array, required: true },
8
+ modelValue: { type: Object, required: true },
9
+ showFacet: { type: Boolean, required: false }
10
+ });
11
+ const emit = defineEmits(["update:modelValue"]);
12
+ const t = useT();
13
+ function formatRange(range) {
14
+ const fmt = dateTimeFormat({ dateStyle: "medium" });
15
+ const one = (value) => {
16
+ if (!value) return null;
17
+ const date = new Date(value);
18
+ return Number.isNaN(date.getTime()) ? value : fmt.format(date);
19
+ };
20
+ const from = one(range.from);
21
+ const to = one(range.to);
22
+ if (from && to) return `${from} \u2013 ${to}`;
23
+ if (from) return t("shared.facetchips.range.since", { date: from });
24
+ return t("shared.facetchips.range.until", { date: to ?? "" });
25
+ }
26
+ const chips = computed(() => appliedFacets(props.facets, props.modelValue, {
27
+ range: formatRange,
28
+ text: (value) => `\u201C${value}\u201D`
29
+ }));
30
+ function remove(chip) {
31
+ emit("update:modelValue", clearFacetValue(props.modelValue, chip.facetKey, chip.value));
32
+ }
33
+ function clearAll() {
34
+ emit("update:modelValue", {});
35
+ }
36
+ </script>
37
+
38
+ <template>
39
+ <div
40
+ v-if="chips.length"
41
+ class="flex flex-wrap items-center gap-1.5 border-t border-black/[0.04] bg-white px-4 py-2.5"
42
+ role="group"
43
+ :aria-label="t('shared.facetchips.aria')"
44
+ >
45
+ <span
46
+ v-for="chip in chips"
47
+ :key="chip.id"
48
+ class="inline-flex items-center gap-1.5 rounded-full bg-primary/10 py-0.5 pr-1 pl-2.5 text-xs font-medium text-primary"
49
+ >
50
+ <span v-if="showFacet" class="font-normal opacity-60">{{ chip.facetLabel }}</span>
51
+ <span class="max-w-60 truncate">{{ chip.label }}</span>
52
+ <button
53
+ type="button"
54
+ class="cursor-pointer rounded-full opacity-60 transition-opacity hover:opacity-100"
55
+ :aria-label="t('shared.facetchips.remove', { facet: chip.facetLabel, value: chip.label })"
56
+ @click="remove(chip)"
57
+ >
58
+ <CloseCircle :size="13" weight="Bold" />
59
+ </button>
60
+ </span>
61
+ <!-- Only past two. With one chip beside it, "clear all" is a second control
62
+ for what the cross next to it already does. -->
63
+ <button
64
+ v-if="chips.length > 1"
65
+ type="button"
66
+ class="ml-1 cursor-pointer text-xs text-muted-foreground underline decoration-from-font underline-offset-2 hover:text-foreground"
67
+ @click="clearAll"
68
+ >
69
+ {{ t("shared.facetchips.clearAll") }}
70
+ </button>
71
+ </div>
72
+ </template>
@@ -0,0 +1,16 @@
1
+ import type { Facet, FacetSelection } from '../../lib/facets.js';
2
+ type __VLS_Props = {
3
+ facets: Facet[];
4
+ modelValue: FacetSelection;
5
+ /**
6
+ * Say which facet each chip belongs to. On where "Draft" could be a status or
7
+ * a lifecycle; off where the values speak for themselves.
8
+ */
9
+ showFacet?: boolean;
10
+ };
11
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
12
+ "update:modelValue": (value: FacetSelection) => any;
13
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
14
+ "onUpdate:modelValue"?: ((value: FacetSelection) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ export default _default;
@@ -0,0 +1,20 @@
1
+ import type { Facet, FacetSelection } from '../../lib/facets.js';
2
+ type __VLS_Props = {
3
+ /** Every dimension this list can be narrowed by. All of them live here. */
4
+ facets: Facet[];
5
+ /** What is applied right now. */
6
+ modelValue: FacetSelection;
7
+ size?: 'sm' | 'default';
8
+ };
9
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
10
+ apply: (value: FacetSelection) => any;
11
+ "update:modelValue": (value: FacetSelection) => any;
12
+ reset: () => any;
13
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
14
+ onApply?: ((value: FacetSelection) => any) | undefined;
15
+ "onUpdate:modelValue"?: ((value: FacetSelection) => any) | undefined;
16
+ onReset?: (() => any) | undefined;
17
+ }>, {
18
+ size: "sm" | "default";
19
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
20
+ export default _default;