@nxgt/material-vue 0.25.2 → 0.26.1

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 (52) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/dist/{Button-C1aaI0Gw.js → Button-16J7vTpE.js} +1 -1
  4. package/dist/{HoverCardTrigger-JtHn0IFQ.js → HoverCardTrigger-C41002JS.js} +2 -2
  5. package/dist/{camera-BacVyXl0.js → camera-DvKuqT4L.js} +4 -4
  6. package/dist/{components-DRBKkOqk.js → components-CCWod7cJ.js} +10461 -10194
  7. package/dist/components.js +8 -8
  8. package/dist/composables.js +2 -2
  9. package/dist/dnd.js +1 -1
  10. package/dist/{form-scope-VxlJuvFe.js → form-scope-x-7TYn2s.js} +1 -1
  11. package/dist/{i18n-provider-D0oYv0XS.js → i18n-provider-CaaZCmaR.js} +1 -1
  12. package/dist/i18n.js +4 -4
  13. package/dist/{icon-button-B2FQtIHS.js → icon-button-CDdNsZuZ.js} +1 -1
  14. package/dist/lib/components/ui/collections/data-table/data-table-server-footer.vue.d.ts +35 -0
  15. package/dist/lib/components/ui/collections/data-table/data-table.vue.d.ts +18 -30
  16. package/dist/lib/components/ui/collections/data-table/index.d.ts +3 -1
  17. package/dist/lib/components/ui/collections/data-table/partials/data-table-body.vue.d.ts +4 -0
  18. package/dist/lib/components/ui/collections/data-table/partials/data-table-page-control.vue.d.ts +15 -0
  19. package/dist/lib/components/ui/collections/data-table/partials/data-table-row.vue.d.ts +31 -0
  20. package/dist/lib/components/ui/collections/data-table/types.d.ts +66 -0
  21. package/dist/lib/components/ui/collections/data-table/use-data-table.d.ts +13 -3
  22. package/dist/lib/components/ui/fields/contacts/consts.d.ts +1 -1
  23. package/dist/lib/i18n/resources/en/en.d.ts +5 -0
  24. package/dist/lib/i18n/resources/en/material.json.d.ts +6 -1
  25. package/dist/lib/i18n/resources/fr/fr.d.ts +5 -0
  26. package/dist/lib/i18n/resources/fr/material.json.d.ts +6 -1
  27. package/dist/lib/i18n/resources/resources.d.ts +10 -0
  28. package/dist/lib/i18n/translator.d.ts +1 -1
  29. package/dist/lib/nuxt/component-names.d.ts +1 -1
  30. package/dist/lib.js +2 -2
  31. package/dist/main.js +17 -17
  32. package/dist/map.js +4 -4
  33. package/dist/material-vue.css +1 -1
  34. package/dist/{pdf-outline-sidebar-jJfhNjnN.js → pdf-outline-sidebar-D3vq2qfS.js} +2 -2
  35. package/dist/{pdf-page-BW1hXzPu.js → pdf-page-BrJbyjQj.js} +1 -1
  36. package/dist/{pdf-page-list-D277RtWS.js → pdf-page-list-BCgYEszV.js} +2 -2
  37. package/dist/{pdf-reader-BReUTsn_.js → pdf-reader-BAzevSJU.js} +8 -8
  38. package/dist/{pdf-sidebar-Bk6PowY1.js → pdf-sidebar-DSh2Mend.js} +3 -3
  39. package/dist/{pdf-thumbnail-sidebar-BcNDFsks.js → pdf-thumbnail-sidebar-D_EMcbKF.js} +3 -3
  40. package/dist/{pdf-toolbar-CHjaMhH9.js → pdf-toolbar-BcL7HlfS.js} +4 -4
  41. package/dist/pdf.js +2 -2
  42. package/dist/{sortable-provider-C83VBfGl.js → sortable-provider-DMpmM7Ke.js} +2 -2
  43. package/dist/{stored-language-CC_vdcCN.js → stored-language-Bxhsx4iB.js} +12 -2
  44. package/dist/{stx-player-BMQ1D2Li.js → stx-player-MldK_ZAZ.js} +17 -17
  45. package/dist/{translator-B-X_1taI.js → translator-CLI_7Crg.js} +1 -1
  46. package/dist/{use-heading-spy-C_ySuvTl.js → use-heading-spy-VPuFj2Gt.js} +1 -1
  47. package/dist/{use-material-translation-B6sIk0mF.js → use-material-translation-CONqD9wJ.js} +3 -3
  48. package/docs/components/data-table-filter-and-lists.md +173 -5
  49. package/docs/roadmap.md +6 -0
  50. package/docs/troubleshooting.md +82 -0
  51. package/lib/nuxt/component-names.ts +1 -0
  52. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import { n as e } from "./rolldown-runtime-CWhphoD1.js";
2
2
  import { t } from "./utils-CykWQ0iJ.js";
3
- import { Ef as n } from "./components-DRBKkOqk.js";
4
- import { t as r } from "./use-material-translation-B6sIk0mF.js";
3
+ import { Ef as n } from "./components-CCWod7cJ.js";
4
+ import { t as r } from "./use-material-translation-CONqD9wJ.js";
5
5
  import { Fragment as i, computed as a, createBlock as o, createCommentVNode as s, createElementBlock as c, createElementVNode as l, defineComponent as u, normalizeClass as d, normalizeStyle as f, openBlock as p, ref as m, renderList as h, toDisplayString as g, unref as _, withCtx as v } from "vue";
6
6
  //#region lib/components/ui/media/pdf-viewer/partials/pdf-outline-row.vue?vue&type=script&setup=true&lang.ts
7
7
  var y = {
@@ -1,5 +1,5 @@
1
1
  import { t as e } from "./utils-CykWQ0iJ.js";
2
- import { Op as t } from "./components-DRBKkOqk.js";
2
+ import { Op as t } from "./components-CCWod7cJ.js";
3
3
  import { computed as n, createBlock as r, createCommentVNode as i, createElementBlock as a, createElementVNode as o, defineComponent as s, nextTick as c, normalizeClass as l, normalizeStyle as u, onBeforeUnmount as d, openBlock as f, ref as p, shallowRef as m, unref as h, watch as g } from "vue";
4
4
  import { DEFAULT_PAGE_SIZE as _, escapeHtml as v, findTextMatches as y, highlightItemHtml as b, placeholderSize as x, rotatedSize as S } from "@nxgt/material-core/pdf-viewer";
5
5
  //#region lib/components/ui/media/pdf-viewer/pdf-note-icons.ts
@@ -1,6 +1,6 @@
1
1
  import { n as e } from "./rolldown-runtime-CWhphoD1.js";
2
- import { Ef as t } from "./components-DRBKkOqk.js";
3
- import { t as n } from "./pdf-page-BW1hXzPu.js";
2
+ import { Ef as t } from "./components-CCWod7cJ.js";
3
+ import { t as n } from "./pdf-page-BrJbyjQj.js";
4
4
  import { Fragment as r, createBlock as i, createElementBlock as a, createElementVNode as o, defineComponent as s, openBlock as c, renderList as l, withCtx as u } from "vue";
5
5
  //#region lib/components/ui/media/pdf-viewer/partials/pdf-page-list.vue?vue&type=script&setup=true&lang.ts
6
6
  var d = { class: "flex flex-col items-center gap-4 px-4 py-4" }, f = /*@__PURE__*/ s({
@@ -1,12 +1,12 @@
1
1
  import { t as e } from "./utils-CykWQ0iJ.js";
2
- import { On as t } from "./components-DRBKkOqk.js";
3
- import { t as n } from "./use-material-translation-B6sIk0mF.js";
4
- import { t as r } from "./Button-C1aaI0Gw.js";
5
- import { o as i } from "./HoverCardTrigger-JtHn0IFQ.js";
6
- import { n as a } from "./pdf-page-BW1hXzPu.js";
7
- import { t as o } from "./pdf-page-list-D277RtWS.js";
8
- import { t as s } from "./pdf-sidebar-Bk6PowY1.js";
9
- import { t as c } from "./pdf-toolbar-CHjaMhH9.js";
2
+ import { On as t } from "./components-CCWod7cJ.js";
3
+ import { t as n } from "./use-material-translation-CONqD9wJ.js";
4
+ import { t as r } from "./Button-16J7vTpE.js";
5
+ import { o as i } from "./HoverCardTrigger-C41002JS.js";
6
+ import { n as a } from "./pdf-page-BrJbyjQj.js";
7
+ import { t as o } from "./pdf-page-list-BCgYEszV.js";
8
+ import { t as s } from "./pdf-sidebar-DSh2Mend.js";
9
+ import { t as c } from "./pdf-toolbar-BcL7HlfS.js";
10
10
  import { computed as l, createBlock as u, createCommentVNode as d, createElementBlock as f, createElementVNode as p, createTextVNode as m, createVNode as h, defineComponent as g, nextTick as _, normalizeClass as v, onBeforeUnmount as y, onMounted as b, openBlock as x, reactive as S, ref as C, shallowRef as w, toDisplayString as T, toRef as E, unref as D, useId as O, watch as k, withCtx as A } from "vue";
11
11
  import { AlertCircle as j, FileX2 as M, KeyRound as N, Loader2 as P } from "@lucide/vue";
12
12
  import { pdfSourceKey as F, toPdfjsParams as I } from "@nxgt/material-core/pdf-source";
@@ -1,8 +1,8 @@
1
1
  import { n as e } from "./rolldown-runtime-CWhphoD1.js";
2
2
  import { t } from "./utils-CykWQ0iJ.js";
3
- import { t as n } from "./use-material-translation-B6sIk0mF.js";
4
- import { t as r } from "./pdf-outline-sidebar-jJfhNjnN.js";
5
- import { t as i } from "./pdf-thumbnail-sidebar-BcNDFsks.js";
3
+ import { t as n } from "./use-material-translation-CONqD9wJ.js";
4
+ import { t as r } from "./pdf-outline-sidebar-D3vq2qfS.js";
5
+ import { t as i } from "./pdf-thumbnail-sidebar-D_EMcbKF.js";
6
6
  import { createElementBlock as a, createElementVNode as o, createVNode as s, defineComponent as c, normalizeClass as l, openBlock as u, toDisplayString as d, unref as f, useId as p, useTemplateRef as m } from "vue";
7
7
  //#region lib/components/ui/media/pdf-viewer/partials/pdf-sidebar.vue?vue&type=script&setup=true&lang.ts
8
8
  var h = ["aria-label"], g = ["aria-label"], _ = [
@@ -1,8 +1,8 @@
1
1
  import { n as e } from "./rolldown-runtime-CWhphoD1.js";
2
2
  import { t } from "./utils-CykWQ0iJ.js";
3
- import { Ef as n } from "./components-DRBKkOqk.js";
4
- import { t as r } from "./use-material-translation-B6sIk0mF.js";
5
- import { t as i } from "./pdf-page-BW1hXzPu.js";
3
+ import { Ef as n } from "./components-CCWod7cJ.js";
4
+ import { t as r } from "./use-material-translation-CONqD9wJ.js";
5
+ import { t as i } from "./pdf-page-BrJbyjQj.js";
6
6
  import { Fragment as a, createCommentVNode as o, createElementBlock as s, createElementVNode as c, createVNode as l, defineComponent as u, normalizeClass as d, openBlock as f, renderList as p, toDisplayString as m, unref as h, withCtx as g } from "vue";
7
7
  //#region lib/components/ui/media/pdf-viewer/partials/pdf-thumbnail-sidebar.vue?vue&type=script&setup=true&lang.ts
8
8
  var _ = ["aria-label"], v = {
@@ -1,10 +1,10 @@
1
1
  import { n as e } from "./rolldown-runtime-CWhphoD1.js";
2
2
  import { t } from "./utils-CykWQ0iJ.js";
3
- import { t as n } from "./use-material-translation-B6sIk0mF.js";
4
- import { s as r } from "./Button-C1aaI0Gw.js";
5
- import { t as i } from "./icon-button-B2FQtIHS.js";
3
+ import { t as n } from "./use-material-translation-CONqD9wJ.js";
4
+ import { s as r } from "./Button-16J7vTpE.js";
5
+ import { t as i } from "./icon-button-CDdNsZuZ.js";
6
6
  import { a, i as o, n as s, o as c, r as l, t as u } from "./DropdownMenuTrigger-BHAhXGUO.js";
7
- import { o as d } from "./HoverCardTrigger-JtHn0IFQ.js";
7
+ import { o as d } from "./HoverCardTrigger-C41002JS.js";
8
8
  import { Fragment as f, computed as p, createBlock as m, createCommentVNode as h, createElementBlock as g, createElementVNode as _, createTextVNode as v, createVNode as y, defineComponent as b, nextTick as x, normalizeClass as S, openBlock as C, ref as w, renderList as T, toDisplayString as E, unref as D, useTemplateRef as O, watch as k, withCtx as A } from "vue";
9
9
  import { ChevronDown as j, ChevronLeft as M, ChevronRight as N, ChevronUp as P, Download as F, Maximize2 as I, Minimize2 as L, Moon as R, Printer as z, RotateCw as B, Search as V, Sidebar as H, ZoomIn as U, ZoomOut as W } from "@lucide/vue";
10
10
  //#region lib/components/ui/media/pdf-viewer/partials/pdf-toolbar-search.vue?vue&type=script&setup=true&lang.ts
package/dist/pdf.js CHANGED
@@ -1,5 +1,5 @@
1
- import { n as e, t } from "./use-material-translation-B6sIk0mF.js";
2
- import { t as n } from "./Button-C1aaI0Gw.js";
1
+ import { n as e, t } from "./use-material-translation-CONqD9wJ.js";
2
+ import { t as n } from "./Button-16J7vTpE.js";
3
3
  import { computed as r, createBlock as i, createTextVNode as a, defineComponent as o, getCurrentInstance as s, nextTick as c, onMounted as l, onScopeDispose as u, openBlock as d, ref as f, renderSlot as p, shallowRef as m, toDisplayString as ee, toValue as h, unref as g, watch as te, withCtx as ne } from "vue";
4
4
  import { contentDisposition as re, pdfBytesResponse as ie, toWebStream as ae } from "@nxgt/material-core/pdf/http";
5
5
  import { badge as oe, dataTable as se, defaultPdfTheme as ce, definePdfDocument as _, divider as v, header as y, heading as b, image as x, keyValue as S, keyValueOf as C, link as w, metric as T, page as E, pageBreak as D, pageNumber as O, pdfDocument as k, resolvePdfColor as A, resolvePdfTheme as j, row as M, section as N, spacer as P, stack as F, table as I, text as L, totals as R } from "@nxgt/material-core/pdf/model";
@@ -1,6 +1,6 @@
1
1
  import { t as e } from "./utils-CykWQ0iJ.js";
2
- import { t } from "./use-material-translation-B6sIk0mF.js";
3
- import { t as n } from "./icon-button-B2FQtIHS.js";
2
+ import { t } from "./use-material-translation-CONqD9wJ.js";
3
+ import { t as n } from "./icon-button-CDdNsZuZ.js";
4
4
  import { Teleport as r, computed as i, createBlock as a, createCommentVNode as o, createElementBlock as s, createVNode as c, defineComponent as l, inject as u, mergeProps as d, normalizeStyle as f, onUnmounted as p, openBlock as m, provide as h, reactive as g, ref as _, renderSlot as v, toValue as y, unref as b, useAttrs as x, watch as S, watchEffect as C, withCtx as w, withModifiers as T } from "vue";
5
5
  import { GripVertical as E } from "@lucide/vue";
6
6
  import { Primitive as D } from "reka-ui";
@@ -341,7 +341,12 @@ var e = {
341
341
  "go-to-last-page": "Go to last page",
342
342
  "rows-per-page": "Rows per page",
343
343
  "rows-selected": "{count} of {total} {count, plural, =0 {row selected} one {row selected} other {rows selected} }",
344
- "page-of": "Page {page} of {total}"
344
+ "page-of": "Page {page} of {total}",
345
+ label: "Pagination",
346
+ range: "{from}–{to} of {total}",
347
+ "range-open": "{from}–{to}",
348
+ total: "{total, plural, one {# row} other {# rows}}",
349
+ selected: "{count, plural, one {# row selected} other {# rows selected}}"
345
350
  },
346
351
  "delete-confirm": {
347
352
  title: "Delete {count} item(s)?",
@@ -1512,7 +1517,12 @@ var e = {
1512
1517
  "go-to-last-page": "Aller à la dernière page",
1513
1518
  "rows-per-page": "Lignes par page",
1514
1519
  "rows-selected": "{count} sur {total} {count, plural, =0 {ligne sélectionnée} one {ligne sélectionnée} other {lignes sélectionnées} }",
1515
- "page-of": "Page {page} sur {total}"
1520
+ "page-of": "Page {page} sur {total}",
1521
+ label: "Pagination",
1522
+ range: "{from}–{to} sur {total}",
1523
+ "range-open": "{from}–{to}",
1524
+ total: "{total, plural, one {# ligne} other {# lignes}}",
1525
+ selected: "{count, plural, one {# ligne sélectionnée} other {# lignes sélectionnées}}"
1516
1526
  },
1517
1527
  "delete-confirm": {
1518
1528
  title: "Supprimer {count} élément(s) ?",
@@ -1,6 +1,6 @@
1
1
  import { t as e } from "./utils-CykWQ0iJ.js";
2
- import { $t as t, Jt as n, Qt as r, Yt as i, Zt as a, en as o, jp as s, qt as c } from "./components-DRBKkOqk.js";
3
- import { t as l } from "./use-material-translation-B6sIk0mF.js";
2
+ import { $t as t, Jt as n, Mp as r, Qt as i, Yt as a, Zt as o, en as s, qt as c } from "./components-CCWod7cJ.js";
3
+ import { t as l } from "./use-material-translation-CONqD9wJ.js";
4
4
  import { createBlock as u, createCommentVNode as d, createElementBlock as f, createElementVNode as p, createTextVNode as m, createVNode as h, defineComponent as g, normalizeClass as _, openBlock as v, toDisplayString as y, unref as b, withCtx as x } from "vue";
5
5
  //#region lib/components/ui/media/stx-player/partials/stx-player-frame.vue?vue&type=script&setup=true&lang.ts
6
6
  var S = ["data-layout", "aria-label"], C = {
@@ -19,26 +19,26 @@ var S = ["data-layout", "aria-label"], C = {
19
19
  Array
20
20
  ] }
21
21
  },
22
- setup(i) {
23
- let { containerRef: g, state: w } = o();
22
+ setup(a) {
23
+ let { containerRef: g, state: w } = s();
24
24
  function T(e) {
25
25
  g.value = e instanceof HTMLElement ? e : null;
26
26
  }
27
27
  let { t: E } = l();
28
- return (o, l) => (v(), f("section", {
28
+ return (s, l) => (v(), f("section", {
29
29
  ref: T,
30
30
  "data-slot": "stx-player",
31
- "data-layout": i.layout,
31
+ "data-layout": a.layout,
32
32
  "aria-label": b(w).current?.title ?? b(E)("material.stx-player.label", "Media player"),
33
- class: _(b(e)("@container rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50", o.$props.class))
34
- }, [i.layout === "mini" ? (v(), u(a, { key: 0 })) : (v(), f("div", {
33
+ class: _(b(e)("@container rounded outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50", s.$props.class))
34
+ }, [a.layout === "mini" ? (v(), u(o, { key: 0 })) : (v(), f("div", {
35
35
  key: 1,
36
- class: _(b(e)("grid gap-3", i.showQueue && "@min-[768px]:grid-cols-[minmax(0,1fr)_18rem] @min-[768px]:items-stretch"))
36
+ class: _(b(e)("grid gap-3", a.showQueue && "@min-[768px]:grid-cols-[minmax(0,1fr)_18rem] @min-[768px]:items-stretch"))
37
37
  }, [p("div", { class: _(b(e)("bg-card overflow-hidden rounded border", b(w).current?.kind === "audio" && "flex flex-col gap-4 p-6")) }, [
38
- b(w).current?.kind === "audio" ? (v(), f("div", C, [h(s, { variant: "title-medium" }, {
38
+ b(w).current?.kind === "audio" ? (v(), f("div", C, [h(r, { variant: "title-medium" }, {
39
39
  default: x(() => [m(y(b(w).current.title), 1)]),
40
40
  _: 1
41
- }), b(w).current.artist ? (v(), u(s, {
41
+ }), b(w).current.artist ? (v(), u(r, {
42
42
  key: 0,
43
43
  class: "text-muted-foreground"
44
44
  }, {
@@ -50,7 +50,7 @@ var S = ["data-layout", "aria-label"], C = {
50
50
  key: 0,
51
51
  tone: "overlay"
52
52
  }, {
53
- trailing: x(() => [h(r, { tone: "overlay" })]),
53
+ trailing: x(() => [h(i, { tone: "overlay" })]),
54
54
  _: 1
55
55
  }))]),
56
56
  _: 1
@@ -59,10 +59,10 @@ var S = ["data-layout", "aria-label"], C = {
59
59
  key: 1,
60
60
  tone: "surface"
61
61
  }, {
62
- trailing: x(() => [h(r, { tone: "surface" })]),
62
+ trailing: x(() => [h(i, { tone: "surface" })]),
63
63
  _: 1
64
64
  })) : d("", !0)
65
- ], 2), i.showQueue ? (v(), u(n, {
65
+ ], 2), a.showQueue ? (v(), u(n, {
66
66
  key: 0,
67
67
  class: "min-h-72"
68
68
  })) : d("", !0)], 2))], 10, S));
@@ -99,16 +99,16 @@ var S = ["data-layout", "aria-label"], C = {
99
99
  emits: ["trackChange"],
100
100
  setup(e, { emit: t }) {
101
101
  let n = e, r = t;
102
- function a(e, t) {
102
+ function i(e, t) {
103
103
  n.onTrackChange?.(e, t), r("trackChange", e, t);
104
104
  }
105
- return (t, r) => (v(), u(i, {
105
+ return (t, r) => (v(), u(a, {
106
106
  tracks: e.tracks,
107
107
  "current-index": e.currentIndex,
108
108
  "default-index": e.defaultIndex,
109
109
  "auto-play": e.autoPlay,
110
110
  loop: e.loop,
111
- "on-track-change": a
111
+ "on-track-change": i
112
112
  }, {
113
113
  default: x(() => [h(w, {
114
114
  layout: e.layout,
@@ -1,4 +1,4 @@
1
- import { a as e, i as t, r as n, t as r } from "./stored-language-CC_vdcCN.js";
1
+ import { a as e, i as t, r as n, t as r } from "./stored-language-Bxhsx4iB.js";
2
2
  import { t as i } from "./get-CTzJAwfc.js";
3
3
  //#region ../../node_modules/@formatjs/fast-memoize/index.js
4
4
  function a(e, t) {
@@ -1,4 +1,4 @@
1
- import { a as e } from "./i18n-provider-D0oYv0XS.js";
1
+ import { a as e } from "./i18n-provider-CaaZCmaR.js";
2
2
  import { c as t, d as n, f as r, l as i, n as a, u as o } from "./number.utils-Cy1z6qI7.js";
3
3
  import { computed as s, getCurrentInstance as c, onMounted as l, ref as u, toValue as d } from "vue";
4
4
  import { useIntersectionObserver as f, useMediaQuery as p, useSSRWidth as m, useStorage as h } from "@vueuse/core";
@@ -1,6 +1,6 @@
1
- import "./stored-language-CC_vdcCN.js";
2
- import { a as e } from "./i18n-provider-D0oYv0XS.js";
3
- import { i as t, r as n } from "./translator-B-X_1taI.js";
1
+ import "./stored-language-Bxhsx4iB.js";
2
+ import { a as e } from "./i18n-provider-CaaZCmaR.js";
3
+ import { i as t, r as n } from "./translator-CLI_7Crg.js";
4
4
  //#region lib/i18n/translate-material.ts
5
5
  function r(e, r, i, a) {
6
6
  if (e) for (let t of /* @__PURE__ */ new Set([e, e.split("-")[0]])) {
@@ -12,7 +12,7 @@ each other, so either can be used alone.
12
12
 
13
13
  | | Component | What it does |
14
14
  | --- | --- | --- |
15
- | <img src="../previews/data-table-filter-lists-datatable.webp" width="96" alt=""> | [DataTable](#datatable) | TanStack Table v9 with sorting, selection, expansion and pagination |
15
+ | <img src="../previews/data-table-filter-lists-datatable.webp" width="96" alt=""> | [DataTable](#datatable) | TanStack Table v9 with sorting, selection, expansion and pagination, client-side or one server page at a time |
16
16
  | <img src="../previews/data-table-filter-lists-filter.webp" width="96" alt=""> | [Filter](#filter) | Schema-driven filters with chips, presets and persistence |
17
17
  | <img src="../previews/data-table-filter-lists-filter.webp" width="96" alt=""> | [FilterInline, FilterPopover…](#filterinline-filterpopover-filterdialog-filtersheet-filterdrawer) | The filter panel inline or in an overlay |
18
18
  | <img src="../previews/data-table-filter-lists-pagination.webp" width="96" alt=""> | [Pagination](#pagination) | Pagination primitives |
@@ -23,7 +23,7 @@ each other, so either can be used alone.
23
23
 
24
24
  ![DataTable](../previews/data-table-filter-lists-datatable.webp)
25
25
 
26
- TanStack Table v9 with sorting, filtering, selection, expandable rows, pagination or load-more, column visibility, skeleton rows while loading and your own empty state.
26
+ TanStack Table v9 with sorting, filtering, selection, expandable rows, pagination or load-more, column visibility, skeleton rows while loading and your own empty state — or one server page at a time, with controlled sorting and selection, cursor Previous / Next links and row links.
27
27
 
28
28
  ```vue
29
29
  <script setup lang="ts">
@@ -58,8 +58,9 @@ const columns = columnHelper.columns([
58
58
  ```
59
59
 
60
60
  Parts: `DataTable`; `DataTableColumnHeader` (a sortable header with a hide
61
- action); `DataTablePagination` and `DataTableViewOptions` (used by the table,
62
- exported for a table of your own; `DataTablePagination` takes `disabled`);
61
+ action); `DataTablePagination`, `DataTableServerFooter` and
62
+ `DataTableViewOptions` (used by the table, exported for a table or a footer
63
+ of your own; both footers take `disabled`);
63
64
  `useColumnHelpers<T>()` returns a
64
65
  `columnHelper` bound to the table's features and `predefinedColumns()` —
65
66
  `select` (checkboxes) and `expand` (a toggle for the expanded row, named
@@ -82,6 +83,15 @@ table enables, for typing your own column defs.
82
83
  | `loading` | `boolean` | `false` | skeleton rows, or the current rows dimmed; `aria-busy`; controls disabled |
83
84
  | `loadingRows` | `number` | the page size | how many skeleton rows |
84
85
  | `#empty="{ filtered }"` | slot | icon and "No data available" | content of the empty row, spanning every visible column |
86
+ | `manual` | `boolean` | `false` | `data` is one server page: no client filtering, sorting or paging; the footer is the server footer ([server-side data](#server-side-data)) |
87
+ | `v-model:sorting` | `SortingState` | held inside | controlled sorting; `update:sorting` gets the new state |
88
+ | `getRowId` | `(row, index) => string` | the index | the row's id, the key of the row selection |
89
+ | `v-model:row-selection` | `RowSelectionState` | held inside | controlled selection, by id; may hold rows of other pages |
90
+ | `pagination` | `{ totalCount?, offset?, previous?, next? }` | — | the server footer's range ("41–60 of 133") and Previous / Next: an href (a link) or a callback (a button) |
91
+ | `#footer="{ table }"` / `footer` | slot / `false \| (table) => VNode` | the pagination | replaces the footer; `false` removes it |
92
+ | `getRowHref` | `(row) => string \| undefined` | — | the first data cell links to the row's page; a click elsewhere on the row follows it |
93
+ | `@row-click` / `onRowClick` | `(row, event) => void` | — | a click on the row (not on a control); alone (without `getRowHref`), it makes the row focusable and Enter calls it |
94
+ | `linkComponent` | `Component` (given `href`) | `<a>` | the link for `getRowHref` and the page links: wrap your router's |
85
95
 
86
96
  ### Loading and empty states
87
97
 
@@ -132,10 +142,168 @@ default, or the `#empty` slot, told whether rows exist but none matches the
132
142
  filters (`filtered`); content that renders nothing gets the default.
133
143
  `DataTableEmptyState` types the slot props.
134
144
 
145
+ ### Server-side data
146
+
147
+ When the server filters, sorts and pages, hand the table **one page** and
148
+ turn on `manual`: the rows are rendered as given — no client slicing,
149
+ sorting or filtering — and the footer becomes the server footer. Keep the
150
+ query (sorting, cursors, filters) in the URL, so the page renders on the
151
+ server, survives a reload and works with no JavaScript.
152
+
153
+ ```vue
154
+ <!-- pages/students.vue (vue-router; Nuxt's useFetch works the same way) -->
155
+ <script setup lang="ts">
156
+ import type { RowSelectionState, SortingState } from '@tanstack/vue-table';
157
+ import { computed, defineComponent, h, ref, watch } from 'vue';
158
+ import { RouterLink as Link, useRoute, useRouter } from 'vue-router';
159
+ import {
160
+ Button,
161
+ DataTable,
162
+ DataTableColumnHeader,
163
+ useColumnHelpers,
164
+ } from '@nxgt/material-vue/components';
165
+
166
+ type Student = { id: string; name: string; email: string };
167
+ type Page = { nodes: Student[]; endCursor: string; hasNextPage: boolean; totalCount: number };
168
+ const PAGE_SIZE = 20;
169
+
170
+ // RouterLink takes `to`; the table hands its links an `href`.
171
+ const RouterLink = defineComponent({
172
+ props: { href: { type: String, required: true } },
173
+ setup: (props, { slots }) => () => h(Link, { to: props.href }, slots),
174
+ });
175
+
176
+ const route = useRoute();
177
+ const router = useRouter();
178
+ // The cursors of the pages before this one: Previous drops the last.
179
+ const trail = computed(() => [route.query.after ?? []].flat() as string[]);
180
+ const sorting = computed<SortingState>(() => {
181
+ const [id = 'name', dir = 'asc'] = String(route.query.sort ?? '').split('.');
182
+ return [{ id, desc: dir === 'desc' }];
183
+ });
184
+ const page = ref<Page>({ nodes: [], endCursor: '', hasNextPage: false, totalCount: 0 });
185
+ const loading = ref(true);
186
+ watch(
187
+ () => route.fullPath,
188
+ async () => {
189
+ loading.value = true;
190
+ const [sort] = sorting.value;
191
+ page.value = await api.students({
192
+ first: PAGE_SIZE,
193
+ after: trail.value.at(-1),
194
+ sort: sort.id,
195
+ dir: sort.desc ? 'desc' : 'asc',
196
+ });
197
+ loading.value = false;
198
+ },
199
+ { immediate: true },
200
+ );
201
+
202
+ const rowSelection = ref<RowSelectionState>({});
203
+ const selected = computed(() =>
204
+ Object.keys(rowSelection.value).filter((id) => rowSelection.value[id]),
205
+ );
206
+
207
+ const { columnHelper, predefinedColumns } = useColumnHelpers<Student>();
208
+ const columns = columnHelper.columns([
209
+ predefinedColumns().select,
210
+ columnHelper.accessor('name', {
211
+ header: ({ column }) => h(DataTableColumnHeader, { column, title: 'Name' }),
212
+ }),
213
+ columnHelper.accessor('email', { header: 'Email' }),
214
+ ]);
215
+
216
+ const pageHref = (cursors: string[]) =>
217
+ router.resolve({ query: { ...route.query, after: cursors } }).href;
218
+ const pagination = computed(() => ({
219
+ totalCount: page.value.totalCount,
220
+ offset: trail.value.length * PAGE_SIZE,
221
+ previous: trail.value.length ? pageHref(trail.value.slice(0, -1)) : null,
222
+ next: page.value.hasNextPage ? pageHref([...trail.value, page.value.endCursor]) : null,
223
+ }));
224
+ function sortBy([sort]: SortingState) {
225
+ // A new order starts at the first page.
226
+ router.push({
227
+ query: { ...route.query, sort: `${sort?.id ?? 'name'}.${sort?.desc ? 'desc' : 'asc'}`, after: [] },
228
+ });
229
+ }
230
+ async function archive() {
231
+ await api.archiveStudents(selected.value);
232
+ rowSelection.value = {};
233
+ }
234
+ </script>
235
+
236
+ <template>
237
+ <DataTable
238
+ manual
239
+ :data="page.nodes"
240
+ :columns="columns"
241
+ :loading="loading"
242
+ :sorting="sorting"
243
+ @update:sorting="sortBy"
244
+ :get-row-id="(student) => student.id"
245
+ v-model:row-selection="rowSelection"
246
+ :get-row-href="(student) => `/students/${student.id}`"
247
+ :link-component="RouterLink"
248
+ :pagination="pagination"
249
+ >
250
+ <template #actions>
251
+ <Button :disabled="!selected.length" @click="archive">
252
+ Archive {{ selected.length }} selected
253
+ </Button>
254
+ </template>
255
+ </DataTable>
256
+ </template>
257
+ ```
258
+
259
+ - **Paging.** `pagination.previous` and `next` are each an href, rendered as
260
+ a link (`rel="prev"` / `rel="next"`, through `linkComponent`), or a
261
+ callback, rendered as a button; absent or `null`, the button is disabled,
262
+ and so are both while `loading`. `totalCount` and `offset` (the 0-based
263
+ index of the page's first row) make the range: "41–60 of 133"
264
+ (`material.table.pagination.range`), "41–60" without a total, "133 rows"
265
+ without an offset. Cursor paging knows the offset from the trail's length
266
+ times the page size; leave it out if pages can be short.
267
+ - **Sorting** is `v-model:sorting`: the headers show it, and
268
+ `update:sorting` hands you the new `SortingState` to put in the URL.
269
+ Filters work the same way: render them in `#actions` and keep their values
270
+ in the URL; `manual` turns client filtering off.
271
+ - **Selection** is `v-model:row-selection`, keyed by `getRowId`, so it
272
+ survives a page change: ids of other pages stay in it, and the footer
273
+ counts them all ("3 rows selected", `material.table.pagination.selected`).
274
+ `#actions="{ table }"` reads it too (`table.store.state.rowSelection`). To
275
+ keep it across a reload, put it in the URL or `sessionStorage`. Unbound,
276
+ both models are held inside, as before. Bind both halves (`v-model`, or
277
+ the prop with its `update:` listener): Vue keeps a local copy of a model
278
+ whose listener is missing, so `:sorting` alone is not read-only, unlike
279
+ React's `sorting` without `onSortingChange`.
280
+ - **Row links.** With `getRowHref`, the first data column's cell is a real
281
+ link to the row's page — a tab stop, opened by Enter, in a new tab by
282
+ Ctrl/Cmd+click, followed with no JavaScript. A click anywhere else on the
283
+ row follows the same link, except on a control (a checkbox, a button, a
284
+ link, a menu, a field, or anything marked `data-row-click-ignore`) and at
285
+ the end of a text selection. The link wraps what the first data column
286
+ renders, so that column must render plain content, not a link or a
287
+ button of its own. `@row-click="(row, event) => …"` is for what is not a
288
+ navigation (opening a sheet). Alone, it makes the row focusable and Enter
289
+ calls it; with `getRowHref`, the link stays the keyboard target, and
290
+ calling `event.preventDefault()` in it stops a mouse click from following
291
+ the link. A focusable row has no role of its own: for anything a screen
292
+ reader user must reach, prefer `getRowHref` or a button in a cell.
293
+ - **Your own footer.** The `#footer="{ table }"` slot or the `footer` prop
294
+ (`(table) => VNode`) replaces the footer — `DataTableServerFooter` is the
295
+ default one, to wrap or reuse — and `:footer="false"` removes it.
296
+ - **Loading and empty** behave as above: keep `loading` on while the next
297
+ page is fetched, and the current rows stay in place, dimmed.
298
+
299
+ The server footer's strings are `material.table.pagination.*`
300
+ (`previous`, `next`, `label`, `range`, `range-open`, `total`, `selected`),
301
+ in English and French.
302
+
135
303
  For the TanStack API itself (column helpers, features, row models), see
136
304
  [TanStack Table v9](https://tanstack.com/table/latest).
137
305
 
138
- **Storybook:** `Data table, filter & lists/DataTable` · **React:** `DataTable`, `DataTableColumnHeader`, `DataTablePagination`, `DataTableViewOptions`
306
+ **Storybook:** `Data table, filter & lists/DataTable`, `…/DataTable/Server-side data` · **React:** `DataTable`, `DataTableColumnHeader`, `DataTablePagination`, `DataTableServerFooter`, `DataTableViewOptions`
139
307
 
140
308
  ---
141
309
 
package/docs/roadmap.md CHANGED
@@ -92,6 +92,12 @@ Nothing in progress right now; see Next.
92
92
 
93
93
  ## Shipped
94
94
 
95
+ - **DataTable server-side data** — hand the table one page from your loader
96
+ (`manual`): no client paging or sorting, `v-model:sorting` and
97
+ `v-model:row-selection` by row id (`getRowId`), a footer with "41–60 of 133"
98
+ and Previous / Next as links that work with no JavaScript (`pagination`,
99
+ `DataTableServerFooter`, `#footer`), and rows that link to their page
100
+ (`getRowHref`, `@row-click`, `linkComponent`). 0.26.0.
95
101
  - **TreeSelectField** — pick one or several items from a tree inside a form
96
102
  field, as chips when several, with an optional filter that keeps each match
97
103
  under its ancestors (`TreeSelectField`, `TreeSelectFormField`). 0.25.0.
@@ -118,6 +118,10 @@ words in your error.
118
118
  - [A `RichTextEditor` became editable after upgrading](#a-richtexteditor-became-editable-after-upgrading)
119
119
  - [Focus falls to `<body>` when a controlled `Drawer` closes](#focus-falls-to-body-when-a-controlled-drawer-closes)
120
120
  - [A `TimePicker` test expects AM / PM to add 12 hours, or a click on the pressed half to switch to PM](#a-timepicker-test-expects-am--pm-to-add-12-hours-or-a-click-on-the-pressed-half-to-switch-to-pm)
121
+ - [A link or button in the first column of a `DataTable` with `getRowHref`](#a-link-or-button-in-the-first-column-of-a-datatable-with-getrowhref)
122
+ - [A row does not navigate after `@row-click` ran](#a-row-does-not-navigate-after-row-click-ran)
123
+ - [A `DataTable` Previous or Next button is disabled](#a-datatable-previous-or-next-button-is-disabled)
124
+ - [A `DataTable` shows a sorting or a selection the URL never got](#a-datatable-shows-a-sorting-or-a-selection-the-url-never-got)
121
125
 
122
126
  **PDF reader**
123
127
 
@@ -2101,6 +2105,84 @@ expect(onChange).toHaveBeenLastCalledWith(new Date(2026, 0, 1, 21, 30)); // from
2101
2105
  await userEvent.type(canvas.getByRole('spinbutton', { name: 'Hours' }), '10');
2102
2106
  ```
2103
2107
 
2108
+ ### A link or button in the first column of a `DataTable` with `getRowHref`
2109
+
2110
+ **When:** a `DataTable` has `getRowHref`, and the cell of its first data column (the first visible column with an accessor; without one, the first that is not `select` or `expand`) renders a link or a button of its own.
2111
+ **Why:** with `getRowHref` that cell's content is wrapped in the row's link, so a link or button inside it is interactive content nested in a link: a nested `<a>` is invalid HTML, so server and client markup can disagree (a hydration warning in an SSR app), and a click or Enter lands on the inner control or the outer link depending on the target. The row's click handling skips clicks on any control, so the row does not follow its link from there either.
2112
+ **Fix:** keep that column plain, and put the control in another column:
2113
+
2114
+ ```vue
2115
+ <DataTable
2116
+ manual
2117
+ :data="rows"
2118
+ :columns="columns"
2119
+ :get-row-href="(student) => `/students/${student.id}`"
2120
+ :link-component="RouterLink"
2121
+ />
2122
+ ```
2123
+ ```ts
2124
+ // columns: the first accessor column renders plain content
2125
+ columnHelper.accessor('name', { header: 'Name' }),
2126
+ // the control goes in a column of its own, after it
2127
+ columnHelper.display({
2128
+ id: 'open',
2129
+ cell: ({ row }) => h(Button, { onClick: () => openSheet(row.original) }, () => 'Quick view'),
2130
+ }),
2131
+ ```
2132
+
2133
+ Anything else that must not make the row follow its link on a click (a custom widget that is not a button, link or field) can carry `data-row-click-ignore`.
2134
+
2135
+ ### A row does not navigate after `@row-click` ran
2136
+
2137
+ **When:** a `DataTable` has both `getRowHref` and `@row-click`; the handler runs, and a mouse click does not go to the row's page.
2138
+ **Why:** the handler runs first, and the row follows its link only if the click's `defaultPrevented` is still false after it. A handler that calls `event.preventDefault()` therefore stops the navigation by design, which is how a handler cancels it (for a row that opens a sheet instead). Ctrl/Cmd+click on the link, and Enter on it, do not go through the row handler.
2139
+ **Fix:** do not call `preventDefault()` in the handler when the row should still navigate:
2140
+
2141
+ ```vue
2142
+ <DataTable
2143
+ manual
2144
+ :data="rows"
2145
+ :columns="columns"
2146
+ :get-row-href="(row) => `/students/${row.id}`"
2147
+ @row-click="(row, event) => openSheet(row)"
2148
+ />
2149
+ ```
2150
+
2151
+ ### A `DataTable` Previous or Next button is disabled
2152
+
2153
+ **When:** a server-mode `DataTable` (`manual`) renders its footer with Previous or Next greyed out, though the server has more pages.
2154
+ **Why:** each button is enabled only when `pagination.previous` / `pagination.next` is an href string or a function, and the table is not `loading`. An absent or `null` value gives a disabled button, and both buttons are disabled while `loading` is true, so a `loading` that stays on (a flag never reset after the navigation ended) leaves them disabled. A `pagination` that is not passed renders no buttons at all.
2155
+ **Fix:** pass a target on each side that has a page, and turn `loading` off when the page has arrived:
2156
+
2157
+ ```vue
2158
+ <DataTable
2159
+ manual
2160
+ :data="rows"
2161
+ :columns="columns"
2162
+ :loading="navigating"
2163
+ :pagination="{ previous: hasPrevious ? previousHref : null, next: hasNext ? nextHref : null, totalCount, offset }"
2164
+ />
2165
+ ```
2166
+
2167
+ ### A `DataTable` shows a sorting or a selection the URL never got
2168
+
2169
+ **When:** a `DataTable` has `:sorting` (or `:row-selection`) bound to state you derive from the URL, with no `@update:sorting` (or `@update:row-selection`), and clicking a header changes the arrow while the rows and the URL stay as they were.
2170
+ **Why:** `sorting` and `rowSelection` are `defineModel`s. Vue keeps a local copy and applies a change to it unless the parent binds both the prop and its `update:` listener, so a one-way `:sorting` is a starting value the table then overrides, until the prop changes.
2171
+ **Fix:** bind both halves, and write the new value where the data comes from:
2172
+
2173
+ ```vue
2174
+ <DataTable
2175
+ manual
2176
+ :data="rows"
2177
+ :columns="columns"
2178
+ :sorting="sorting"
2179
+ @update:sorting="(next) => router.push({ query: { ...route.query, sort: toQuery(next) } })"
2180
+ v-model:row-selection="rowSelection"
2181
+ />
2182
+ ```
2183
+
2184
+ ---
2185
+
2104
2186
  ---
2105
2187
 
2106
2188
  ## PDF reader
@@ -160,6 +160,7 @@ export const COMPONENT_NAMES = [
160
160
  'DataTable',
161
161
  'DataTableColumnHeader',
162
162
  'DataTablePagination',
163
+ 'DataTableServerFooter',
163
164
  'DataTableViewOptions',
164
165
  'DateField',
165
166
  'DateFormField',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material-vue",
3
- "version": "0.25.2",
3
+ "version": "0.26.1",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"
@@ -119,7 +119,7 @@
119
119
  "docs:previews": "vitest run --project previews"
120
120
  },
121
121
  "dependencies": {
122
- "@nxgt/material-core": "^0.3.2",
122
+ "@nxgt/material-core": "^0.4.0",
123
123
  "@floating-ui/dom": "^1.8.0",
124
124
  "@fortawesome/fontawesome-common-types": "7.3.1",
125
125
  "@lucide/vue": "^1.45.0",