@morze/ui 0.1.0 → 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.
package/README.md CHANGED
@@ -17,7 +17,9 @@ box-shadow: 0 3px 10px -4px rgba(tone,.4),
17
17
 
18
18
  That recipe is applied to **every active element**: buttons, toggles,
19
19
  checkboxes, radios, switches, sliders, tabs, the select trigger, badges and the
20
- active navigation item.
20
+ active navigation item. The label itself stays flat — the embossed
21
+ `text-shadow` is off (`--mz-convex-text-shadow: none`), as on the landing,
22
+ because on a 34px control it only made the text shout.
21
23
 
22
24
  ## Install
23
25
 
@@ -38,7 +40,7 @@ import { Button, Switch, MorzeThemeProvider } from '@morze/ui'
38
40
  export default function App() {
39
41
  return (
40
42
  <MorzeThemeProvider defaultTheme="dark">
41
- <Button dot>Submit request</Button>
43
+ <Button>Submit request</Button>
42
44
  <Switch defaultChecked />
43
45
  </MorzeThemeProvider>
44
46
  )
@@ -47,6 +49,23 @@ export default function App() {
47
49
 
48
50
  Tokens only, without components: `import '@morze/ui/tokens.css'`.
49
51
 
52
+ **In a Tailwind v4 host, import the kit into a layer.** The kit ships plain,
53
+ unlayered CSS, and unlayered CSS beats layered CSS regardless of source order —
54
+ imported naively, `.mz-btn--primary` outranks every utility passed through
55
+ `className`:
56
+
57
+ ```css
58
+ /* Layer order first — @layer statements may precede @import. */
59
+ @layer theme, base, components, morze-ui, utilities;
60
+
61
+ @import 'tailwindcss';
62
+ @import '@morze/ui/styles.css' layer(morze-ui);
63
+ ```
64
+
65
+ The kit then wins over the host's preflight, and a `className` on a component
66
+ still wins over the kit. Note that `cn` is plain `clsx`, not `tailwind-merge`:
67
+ conflicting classes are not de-duplicated, they are resolved by that cascade.
68
+
50
69
  ## Themes
51
70
 
52
71
  Dark is the default, light is opt-in through an attribute (or the
@@ -82,7 +101,7 @@ and the focus ring:
82
101
  ```tsx
83
102
  <Button tone="accent">Save</Button>
84
103
  <Switch tone="success" defaultChecked />
85
- <Badge tone="danger" dot>Live</Badge>
104
+ <Badge tone="danger">Live</Badge>
86
105
  <Progress value={40} tone="warning" />
87
106
  ```
88
107
 
@@ -101,17 +120,43 @@ larger one for surfaces.
101
120
  | `--mz-radius-sm` | `6px` | items inside containers: menu items, tabs, checkbox |
102
121
  | `--mz-radius` | `8px` | buttons, inputs, select, toggles, badges, alerts |
103
122
  | `--mz-radius-lg` | `12px` | cards, dialogs, popovers, menus |
104
- | `--mz-radius-pill` | `999px` | only what is round by meaning: radio, indicator dots |
123
+ | `--mz-radius-pill` | `999px` | only what is round by meaning: the radio and its mark |
105
124
 
106
125
  Fully square corners: `--mz-radius: 0; --mz-radius-sm: 0; --mz-radius-lg: 0`.
107
126
 
108
- Fonts are **system stacks** nothing is downloaded and the kit inherits the
109
- host application's look. Control labels are plain sentence case.
127
+ **The kit inherits your application's font.** Every `mz-` element is
128
+ `font-family: inherit`, so a button, a table cell and a menu item read in the
129
+ same face as the text around them — the kit never renders a label in a font the
130
+ page does not use. Control labels are plain sentence case.
131
+
132
+ `--mz-font-sans` is the fallback, applied to `.mz-root`: `'Golos Text'` (the
133
+ face morze.tech uses) with a system stack behind it. Nothing is downloaded —
134
+ load the webfont in the host if you want it:
135
+
136
+ ```html
137
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
138
+ <link href="https://fonts.googleapis.com/css2?family=Golos+Text:wght@400..700&display=swap"
139
+ rel="stylesheet" />
140
+ ```
141
+
142
+ Set the font on `body`, not only inside the app shell: dialogs, menus and
143
+ tooltips are portalled to `<body>` and inherit from there.
144
+
145
+ The **leading** is the kit's own, though: `--mz-line` (1.45) is applied at zero
146
+ specificity to every `mz-` element, so a landing-style `line-height: 1.7` on
147
+ your body does not inflate a dialog description or a field hint. For the same
148
+ reason the kit zeroes the UA margins on the headings, paragraphs and lists it
149
+ renders itself — a host with no CSS reset used to get a dialog title pushed
150
+ 14px away from its description. Your own markup inside a `Card` or `Dialog` is
151
+ untouched.
110
152
 
111
153
  ```css
154
+ body { font-family: var(--mz-font-sans); }
155
+
112
156
  :root {
113
157
  --mz-primary-rgb: 120, 90, 250; /* brand colour */
114
- --mz-font-sans: 'Inter', sans-serif;
158
+ --mz-font-sans: 'Inter', sans-serif; /* or your own face */
159
+ --mz-font-display: 'Unbounded', sans-serif; /* headings only */
115
160
  --mz-radius: 4px; /* tighter */
116
161
  }
117
162
  ```
@@ -183,7 +228,7 @@ What this kit adds on top:
183
228
  - `Button` — `variant`: `primary` (default) `secondary` `outline` `ghost`
184
229
  `destructive` `link`; `size`: `xs` (28px) `sm` (34) `md` (40, default)
185
230
  `lg` (48) plus square `icon-xs` `icon-sm` `icon` `icon-lg`; `loading`,
186
- `dot` (a pulsing morse dot), `tone`. The `sm/md/lg` heights (34/40/48px) are
231
+ `tone`. The `sm/md/lg` heights (34/40/48px) are
187
232
  shared by buttons, inputs and toggles, so controls line up in a row. With
188
233
  `asChild` the loading indicator is grafted inside the child element and the
189
234
  blocked state is expressed through `aria-disabled`, since a link has no
@@ -199,7 +244,7 @@ What this kit adds on top:
199
244
  also gets `role="button"`, `tabIndex` and Enter/Space activation.
200
245
  - `Spinner` — `label` (default `Loading`) is announced by screen readers;
201
246
  `label={null}` makes it purely decorative.
202
- - `Badge` — `variant`: `solid` `soft` `outline`, plus `dot`.
247
+ - `Badge` — `variant`: `solid` `soft` `outline`.
203
248
  - `Field` / `FieldHint` / `FieldError` — form scaffolding.
204
249
 
205
250
  ```tsx
@@ -342,7 +387,23 @@ What it does:
342
387
  - **Header filters** — `text`, `select`, `number-range`, `date-range`,
343
388
  `boolean`. A value is staged in the popover and committed on Apply: otherwise
344
389
  every keystroke would be a request. Active filters are echoed as chips above
345
- the table.
390
+ the table. `type: 'custom'` renders a widget of your own in the same popover —
391
+ an async multiselect, a range slider — and it takes part in `query.filters`
392
+ like the built-in ones:
393
+
394
+ ```tsx
395
+ filter: {
396
+ type: 'custom',
397
+ render: ({ value, commit }) => (
398
+ <ClientPicker selected={value as string[]} onPick={(ids, names) => commit(ids, names)} />
399
+ ),
400
+ describe: (value) => `${(value as string[]).length} selected`,
401
+ }
402
+ ```
403
+
404
+ The value reaches the backend untouched; `commit(value, label)` applies and
405
+ closes, `onChange` only stages. `actions: false` drops the Apply/Reset footer
406
+ for a widget that commits itself.
346
407
  - **Default widths** — columns stretch to fill the container, so there is no
347
408
  dead space at the right edge. The maths runs off the declared `width` values
348
409
  (they act as proportions), re-runs when the container resizes and always
@@ -371,11 +432,29 @@ What it does:
371
432
  - **States** — skeletons on the first load, a thin progress line when refetching
372
433
  over data already on screen, an empty result and an error with a retry.
373
434
  - **Density** `compact | normal | relaxed`.
435
+ - **Row styling** — `rowClassName` tints a row by record state (a soft-deleted
436
+ row painted red); `rowProps` adds `data-*`, `title` or a handler of your own.
437
+ The table's own attributes win, so neither can break selection or expansion.
438
+ - **Load more instead of paging** — pass `onLoadMore` and the pager is replaced
439
+ by a footer inside the table's own scroller, so an endless scroll works from
440
+ the inside (a host cannot bolt a sentinel onto a scroller it does not own).
441
+ `hasMore` defaults to `data.length < total`, `autoLoadMore={false}` waits for
442
+ a click, and `pagination` brings the pager back if you want both. The table
443
+ asks once per batch of rows: a host that answers with nothing new is not
444
+ asked again.
445
+ - **The pager adapts** — `pageSizeOptions={false}`, or a single option, hides
446
+ the rows-per-page select for a backend that fixes the page size.
374
447
 
375
448
  Helpers for your own UI: `useTableQuery`, `useSavedViews`, `useColumnLayout`,
376
449
  `useRowSelection`, plus the pure functions `toggleSort`, `setFilter`,
377
450
  `serializeSort` / `parseSort`.
378
451
 
452
+ Cells already truncate: `.mz-dt__cell` is `overflow: hidden; text-overflow:
453
+ ellipsis; white-space: nowrap`. Carrying a `className="block truncate"` from a
454
+ shadcn table onto cell content is redundant — and a block-level child that
455
+ reflows during a render is exactly what used to feed the scroll observer into a
456
+ render loop (see `FIXES.md`).
457
+
379
458
  A table stays a table: on narrow screens it scrolls horizontally with the first
380
459
  column pinned, it does not reflow into cards.
381
460
 
@@ -408,6 +487,23 @@ const de: Partial<DataTableLabels> = {
408
487
  The complete set of keys is `DataTableLabels`, with defaults in
409
488
  `defaultDataTableLabels`.
410
489
 
490
+ Ready-made bundles ship behind their own entry points, so a non-English host
491
+ does not hand-carry ~50 strings — and does not silently drift as the kit adds
492
+ them:
493
+
494
+ ```tsx
495
+ import { dataTable as ru, common } from '@morze/ui/locales/ru'
496
+
497
+ <DataTable labels={ru} locale="ru-RU" … />
498
+ <Dialog><DialogContent closeLabel={common.close}>…</DialogContent></Dialog>
499
+ ```
500
+
501
+ `common` carries the strings the rest of the kit takes as individual props
502
+ (`close`, `loading`, `sidebarNavigation`, `sidebarSections`, `toggleSidebar`).
503
+ A bundle is plain data — no React, no styles — so it is safe to import from a
504
+ server component. `@morze/ui/locales/en` is the same shape for English, and
505
+ `MorzeLocale` types a language of your own.
506
+
411
507
  ## Accessibility and behaviour
412
508
 
413
509
  - Behaviour, focus management, keyboard and ARIA come from Radix.
@@ -432,8 +528,14 @@ npm run dev # playground with every component (http://localhost:5250)
432
528
  npm run typecheck
433
529
  npm run test # vitest: component behaviour plus CSS contracts
434
530
  npm run build # dist: ESM + CJS + .d.ts + morze-ui.css
531
+ npm run shot # redraws docs/screenshot.png from playground/shot.html
435
532
  ```
436
533
 
534
+ The still at the top of this file is generated, not composed by hand: `npm run
535
+ shot` renders `playground/shot.html` in headless Chrome at 2x and trims the
536
+ frame to its content, so the picture can follow the kit whenever the look
537
+ changes.
538
+
437
539
  `tests/smoke.test.tsx` covers component behaviour and accessibility,
438
540
  `tests/css-contract.test.ts` covers the visual layer's contracts (cascade order
439
541
  for tones, hover states, isolation from foreign markup, geometry). The second
package/dist/index.cjs CHANGED
@@ -239,7 +239,6 @@ function Button({
239
239
  asChild = false,
240
240
  tone,
241
241
  loading = false,
242
- dot = false,
243
242
  disabled,
244
243
  children,
245
244
  ...props
@@ -247,7 +246,6 @@ function Button({
247
246
  const Comp = asChild ? radixUi.Slot.Root : "button";
248
247
  const isDisabled = disabled || loading;
249
248
  const decorate = (label) => /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
250
- dot ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mz-btn__dot", "aria-hidden": "true" }) : null,
251
249
  label,
252
250
  loading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mz-btn__spinner", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx(SpinnerIcon, {}) }) : null
253
251
  ] });
@@ -291,7 +289,6 @@ function Badge({
291
289
  className,
292
290
  variant = "solid",
293
291
  tone,
294
- dot = false,
295
292
  asChild = false,
296
293
  children,
297
294
  ...props
@@ -305,10 +302,7 @@ function Badge({
305
302
  "data-tone": tone,
306
303
  className: cn(badgeVariants({ variant }), className),
307
304
  ...props,
308
- children: asChild ? children : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
309
- dot ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mz-badge__dot", "aria-hidden": "true" }) : null,
310
- children
311
- ] })
305
+ children
312
306
  }
313
307
  );
314
308
  }
@@ -1846,6 +1840,8 @@ var defaultDataTableLabels = {
1846
1840
  previousPage: "Previous page",
1847
1841
  nextPage: "Next page",
1848
1842
  lastPage: "Last page",
1843
+ loadMore: "Load more",
1844
+ loadingMore: "Loading\u2026",
1849
1845
  selectedCount: (count) => `Selected: ${count}`,
1850
1846
  allMatchingSuffix: " (all matching)",
1851
1847
  selectAllMatching: (total) => `Select all ${total}`,
@@ -1870,6 +1866,8 @@ function isFilterActive(value) {
1870
1866
  return Boolean(value.from || value.to);
1871
1867
  case "boolean":
1872
1868
  return true;
1869
+ case "custom":
1870
+ return !(value.value === void 0 || value.value === null || value.value === "" || Array.isArray(value.value) && value.value.length === 0);
1873
1871
  }
1874
1872
  }
1875
1873
  function activeFilters(filters) {
@@ -1940,10 +1938,11 @@ function ColumnFilter({ columnLabel, def, value, onApply, labels: labelsProp })
1940
1938
  draft,
1941
1939
  setDraft,
1942
1940
  labels,
1943
- onSubmit: () => commit(draft)
1941
+ onSubmit: () => commit(draft),
1942
+ onCommit: commit
1944
1943
  }
1945
1944
  ),
1946
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mz-dt__filter-actions", children: [
1945
+ def.type === "custom" && def.actions === false ? null : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mz-dt__filter-actions", children: [
1947
1946
  /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", variant: "ghost", onClick: () => commit(void 0), children: labels.reset }),
1948
1947
  /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", onClick: () => commit(draft), children: labels.apply })
1949
1948
  ] })
@@ -1955,7 +1954,8 @@ function FilterBody({
1955
1954
  draft,
1956
1955
  setDraft,
1957
1956
  labels,
1958
- onSubmit
1957
+ onSubmit,
1958
+ onCommit
1959
1959
  }) {
1960
1960
  const submitOnEnter = (event) => {
1961
1961
  if (event.key === "Enter") {
@@ -2063,6 +2063,17 @@ function FilterBody({
2063
2063
  )
2064
2064
  ] });
2065
2065
  }
2066
+ if (def.type === "custom") {
2067
+ const staged = draft?.type === "custom" ? draft : void 0;
2068
+ const wrap = (value, label) => value === void 0 ? void 0 : { type: "custom", value, label };
2069
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: def.render({
2070
+ value: staged?.value,
2071
+ label: staged?.label,
2072
+ onChange: (value, label) => setDraft(wrap(value, label)),
2073
+ commit: (value, label) => onCommit(wrap(value, label)),
2074
+ labels
2075
+ }) });
2076
+ }
2066
2077
  const current = draft?.type === "boolean" ? draft.value : void 0;
2067
2078
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mz-dt__filter-options", role: "radiogroup", children: [
2068
2079
  { value: true, label: def.trueLabel ?? labels.yes },
@@ -2205,6 +2216,7 @@ function DataTablePagination({
2205
2216
  onQueryChange
2206
2217
  }) {
2207
2218
  const labels = resolveLabels(labelsProp);
2219
+ const sizes = pageSizeOptions === false ? [] : pageSizeOptions;
2208
2220
  const pages = pageCount(total, query.pageSize);
2209
2221
  const from = total === 0 ? 0 : (query.page - 1) * query.pageSize + 1;
2210
2222
  const to = total === void 0 ? from + rowsOnPage - 1 : Math.min(query.page * query.pageSize, total);
@@ -2221,7 +2233,7 @@ function DataTablePagination({
2221
2233
  total.toLocaleString(locale)
2222
2234
  ] }) : null
2223
2235
  ] }) }),
2224
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mz-dt__pagination-size", children: [
2236
+ sizes.length > 1 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mz-dt__pagination-size", children: [
2225
2237
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.rowsPerPage }),
2226
2238
  /* @__PURE__ */ jsxRuntime.jsxs(
2227
2239
  Select,
@@ -2230,11 +2242,11 @@ function DataTablePagination({
2230
2242
  onValueChange: (value) => onQueryChange({ ...query, pageSize: Number(value), page: 1 }),
2231
2243
  children: [
2232
2244
  /* @__PURE__ */ jsxRuntime.jsx(SelectTrigger, { size: "sm", className: "mz-dt__pagination-select", children: /* @__PURE__ */ jsxRuntime.jsx(SelectValue, {}) }),
2233
- /* @__PURE__ */ jsxRuntime.jsx(SelectContent, { children: pageSizeOptions.map((size) => /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: String(size), children: size }, size)) })
2245
+ /* @__PURE__ */ jsxRuntime.jsx(SelectContent, { children: sizes.map((size) => /* @__PURE__ */ jsxRuntime.jsx(SelectItem, { value: String(size), children: size }, size)) })
2234
2246
  ]
2235
2247
  }
2236
2248
  )
2237
- ] }),
2249
+ ] }) : null,
2238
2250
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mz-dt__pagination-nav", children: [
2239
2251
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mz-dt__pagination-page", children: [
2240
2252
  query.page,
@@ -2620,6 +2632,8 @@ function DataTable({
2620
2632
  bulkActions,
2621
2633
  renderExpanded,
2622
2634
  onRowClick,
2635
+ rowClassName,
2636
+ rowProps,
2623
2637
  layout: controlledLayout,
2624
2638
  onLayoutChange,
2625
2639
  persistKey = null,
@@ -2629,6 +2643,10 @@ function DataTable({
2629
2643
  stickyHeader = true,
2630
2644
  emptyState,
2631
2645
  pageSizeOptions,
2646
+ pagination,
2647
+ onLoadMore,
2648
+ hasMore,
2649
+ autoLoadMore = true,
2632
2650
  labels: labelsProp,
2633
2651
  locale,
2634
2652
  caption,
@@ -2683,7 +2701,11 @@ function DataTable({
2683
2701
  if (!scroller) return;
2684
2702
  const update = () => {
2685
2703
  const max = scroller.scrollWidth - scroller.clientWidth;
2686
- setScrolled({ left: scroller.scrollLeft > 1, right: scroller.scrollLeft < max - 1 });
2704
+ const left = scroller.scrollLeft > 1;
2705
+ const right = scroller.scrollLeft < max - 1;
2706
+ setScrolled(
2707
+ (current) => current.left === left && current.right === right ? current : { left, right }
2708
+ );
2687
2709
  };
2688
2710
  update();
2689
2711
  scroller.addEventListener("scroll", update, { passive: true });
@@ -2694,6 +2716,34 @@ function DataTable({
2694
2716
  observer.disconnect();
2695
2717
  };
2696
2718
  }, [visible.length, data.length]);
2719
+ const moreRef = React39__namespace.useRef(null);
2720
+ const onLoadMoreRef = React39__namespace.useRef(onLoadMore);
2721
+ React39__namespace.useEffect(() => {
2722
+ onLoadMoreRef.current = onLoadMore;
2723
+ });
2724
+ const askedAt = React39__namespace.useRef(-1);
2725
+ const canLoadMore = Boolean(onLoadMore) && (hasMore ?? (total === void 0 || data.length < total));
2726
+ const showLoadMore = canLoadMore && !error && data.length > 0;
2727
+ const showPager = pagination ?? !onLoadMore;
2728
+ const askForMore = () => {
2729
+ if (askedAt.current === data.length) return;
2730
+ askedAt.current = data.length;
2731
+ onLoadMoreRef.current?.();
2732
+ };
2733
+ React39__namespace.useEffect(() => {
2734
+ const sentinel = moreRef.current;
2735
+ const scroller = scrollerRef.current;
2736
+ if (!sentinel || !scroller || !showLoadMore || !autoLoadMore || loading) return;
2737
+ if (typeof IntersectionObserver === "undefined") return;
2738
+ const observer = new IntersectionObserver(
2739
+ (entries) => {
2740
+ if (entries.some((entry) => entry.isIntersecting)) askForMore();
2741
+ },
2742
+ { root: scroller, rootMargin: "120px" }
2743
+ );
2744
+ observer.observe(sentinel);
2745
+ return () => observer.disconnect();
2746
+ }, [showLoadMore, autoLoadMore, loading, data.length]);
2697
2747
  const offsets = React39__namespace.useMemo(() => {
2698
2748
  const result = /* @__PURE__ */ new Map();
2699
2749
  let leftParts = selectable ? [`${SELECT_WIDTH}px`] : [];
@@ -2945,14 +2995,21 @@ function DataTable({
2945
2995
  /* @__PURE__ */ jsxRuntime.jsx("tbody", { children: loading && data.length === 0 ? Array.from({ length: Math.min(query.pageSize, 8) }, (_, index) => /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "mz-dt__row", children: /* @__PURE__ */ jsxRuntime.jsx("td", { className: "mz-dt__td", colSpan, children: /* @__PURE__ */ jsxRuntime.jsx(Skeleton, { style: { height: 14, width: `${60 + index * 13 % 35}%` } }) }) }, `skeleton-${index}`)) : data.map((row, rowIndex) => {
2946
2996
  const key = rowKey(row);
2947
2997
  const isOpen = expanded.has(key);
2998
+ const context = { rowIndex };
2999
+ const hostClass = rowClassName?.(row, context);
3000
+ const hostProps = rowProps?.(row, context);
2948
3001
  return /* @__PURE__ */ jsxRuntime.jsxs(React39__namespace.Fragment, { children: [
2949
3002
  /* @__PURE__ */ jsxRuntime.jsxs(
2950
3003
  "tr",
2951
3004
  {
2952
- className: "mz-dt__row",
3005
+ ...hostProps,
3006
+ className: cn("mz-dt__row", hostClass, hostProps?.className),
2953
3007
  "data-selected": rows.selected.has(key) || rows.allMatching || void 0,
2954
3008
  "data-clickable": onRowClick ? true : void 0,
2955
- onClick: onRowClick ? () => onRowClick(row) : void 0,
3009
+ onClick: onRowClick || hostProps?.onClick ? (event) => {
3010
+ hostProps?.onClick?.(event);
3011
+ onRowClick?.(row);
3012
+ } : void 0,
2956
3013
  children: [
2957
3014
  selectable ? /* @__PURE__ */ jsxRuntime.jsx(
2958
3015
  "td",
@@ -3001,7 +3058,11 @@ function DataTable({
3001
3058
  ]
3002
3059
  }
3003
3060
  ),
3004
- isOpen && renderExpanded ? /* @__PURE__ */ jsxRuntime.jsx("tr", { className: "mz-dt__row mz-dt__row--expanded", children: /* @__PURE__ */ jsxRuntime.jsx("td", { className: "mz-dt__td mz-dt__td--expanded", colSpan, children: renderExpanded(row) }) }) : null
3061
+ isOpen && renderExpanded ? (
3062
+ // The detail panel belongs to the same record, so a row
3063
+ // tint carries onto it; the attributes do not.
3064
+ /* @__PURE__ */ jsxRuntime.jsx("tr", { className: cn("mz-dt__row mz-dt__row--expanded", hostClass), children: /* @__PURE__ */ jsxRuntime.jsx("td", { className: "mz-dt__td mz-dt__td--expanded", colSpan, children: renderExpanded(row) }) })
3065
+ ) : null
3005
3066
  ] }, key);
3006
3067
  }) })
3007
3068
  ] }),
@@ -3023,11 +3084,12 @@ function DataTable({
3023
3084
  /* @__PURE__ */ jsxRuntime.jsx("p", { children: error }),
3024
3085
  onRetry ? /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", variant: "secondary", onClick: onRetry, children: labels.retry }) : null
3025
3086
  ] }) : null,
3087
+ showLoadMore ? /* @__PURE__ */ jsxRuntime.jsx("div", { ref: moreRef, className: "mz-dt__more", "data-slot": "data-table-load-more", children: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "secondary", size: "sm", disabled: loading, onClick: askForMore, children: loading ? labels.loadingMore : labels.loadMore }) }) : null,
3026
3088
  loading && data.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mz-dt__loading-bar", "aria-hidden": "true" }) : null
3027
3089
  ]
3028
3090
  }
3029
3091
  ),
3030
- /* @__PURE__ */ jsxRuntime.jsx(
3092
+ showPager ? /* @__PURE__ */ jsxRuntime.jsx(
3031
3093
  DataTablePagination,
3032
3094
  {
3033
3095
  query,
@@ -3038,7 +3100,7 @@ function DataTable({
3038
3100
  locale,
3039
3101
  onQueryChange
3040
3102
  }
3041
- ),
3103
+ ) : null,
3042
3104
  selectable && (rows.count ?? 0) > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mz-dt__bulkbar", role: "region", "aria-label": labels.bulkActions, children: [
3043
3105
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mz-dt__bulkbar-count", children: [
3044
3106
  labels.selectedCount(
@@ -3073,6 +3135,10 @@ function describeFilter(value, def, labels) {
3073
3135
  return `: ${value.from ?? "\u2026"} \u2013 ${value.to ?? "\u2026"}`;
3074
3136
  case "boolean":
3075
3137
  return `: ${value.value ? labels.yes.toLowerCase() : labels.no.toLowerCase()}`;
3138
+ case "custom": {
3139
+ const text = (def?.type === "custom" ? def.describe?.(value.value) : void 0) ?? value.label;
3140
+ return text ? `: ${text}` : "";
3141
+ }
3076
3142
  }
3077
3143
  }
3078
3144