@morze/ui 0.1.0 → 0.2.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.
- package/README.md +112 -10
- package/dist/index.cjs +114 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -63
- package/dist/index.d.ts +78 -63
- package/dist/index.js +114 -34
- package/dist/index.js.map +1 -1
- package/dist/locales/en.cjs +73 -0
- package/dist/locales/en.cjs.map +1 -0
- package/dist/locales/en.d.cts +7 -0
- package/dist/locales/en.d.ts +7 -0
- package/dist/locales/en.js +66 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/locales/ru.cjs +70 -0
- package/dist/locales/ru.cjs.map +1 -0
- package/dist/locales/ru.d.cts +16 -0
- package/dist/locales/ru.d.ts +16 -0
- package/dist/locales/ru.js +63 -0
- package/dist/locales/ru.js.map +1 -0
- package/dist/morze-ui-tokens.css +1 -1
- package/dist/morze-ui.css +1 -1
- package/dist/types-B8jz2FF9.d.cts +75 -0
- package/dist/types-B8jz2FF9.d.ts +75 -0
- package/package.json +13 -2
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
|
|
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"
|
|
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
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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:
|
|
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,
|
|
@@ -2408,20 +2420,27 @@ function useColumnLayout({
|
|
|
2408
2420
|
() => controlled ? merge(baseline, controlled, baseline) : internal,
|
|
2409
2421
|
[controlled, internal, baseline]
|
|
2410
2422
|
);
|
|
2423
|
+
const onLayoutChangeRef = React39__namespace.useRef(onLayoutChange);
|
|
2424
|
+
React39__namespace.useEffect(() => {
|
|
2425
|
+
onLayoutChangeRef.current = onLayoutChange;
|
|
2426
|
+
});
|
|
2427
|
+
const internalRef = React39__namespace.useRef(internal);
|
|
2428
|
+
React39__namespace.useEffect(() => {
|
|
2429
|
+
internalRef.current = internal;
|
|
2430
|
+
}, [internal]);
|
|
2411
2431
|
const update = React39__namespace.useCallback(
|
|
2412
2432
|
(patch, options) => {
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
});
|
|
2433
|
+
const base = controlled ? merge(baseline, controlled, baseline) : internalRef.current;
|
|
2434
|
+
const next = patch(base);
|
|
2435
|
+
if (next === base) return;
|
|
2436
|
+
internalRef.current = next;
|
|
2437
|
+
setInternal(next);
|
|
2438
|
+
if (options?.persist !== false) {
|
|
2439
|
+
writeStored2(persistKey, next);
|
|
2440
|
+
onLayoutChangeRef.current?.(next);
|
|
2441
|
+
}
|
|
2423
2442
|
},
|
|
2424
|
-
[controlled, baseline, persistKey
|
|
2443
|
+
[controlled, baseline, persistKey]
|
|
2425
2444
|
);
|
|
2426
2445
|
const byId = React39__namespace.useMemo(
|
|
2427
2446
|
() => new Map(columns.map((c) => [c.id, c])),
|
|
@@ -2605,6 +2624,7 @@ function useRowSelection(options) {
|
|
|
2605
2624
|
}
|
|
2606
2625
|
var SELECT_WIDTH = 44;
|
|
2607
2626
|
var EXPAND_WIDTH = 40;
|
|
2627
|
+
var FIT_CYCLE_MS = 250;
|
|
2608
2628
|
function DataTable({
|
|
2609
2629
|
columns,
|
|
2610
2630
|
data,
|
|
@@ -2620,6 +2640,8 @@ function DataTable({
|
|
|
2620
2640
|
bulkActions,
|
|
2621
2641
|
renderExpanded,
|
|
2622
2642
|
onRowClick,
|
|
2643
|
+
rowClassName,
|
|
2644
|
+
rowProps,
|
|
2623
2645
|
layout: controlledLayout,
|
|
2624
2646
|
onLayoutChange,
|
|
2625
2647
|
persistKey = null,
|
|
@@ -2629,6 +2651,10 @@ function DataTable({
|
|
|
2629
2651
|
stickyHeader = true,
|
|
2630
2652
|
emptyState,
|
|
2631
2653
|
pageSizeOptions,
|
|
2654
|
+
pagination,
|
|
2655
|
+
onLoadMore,
|
|
2656
|
+
hasMore,
|
|
2657
|
+
autoLoadMore = true,
|
|
2632
2658
|
labels: labelsProp,
|
|
2633
2659
|
locale,
|
|
2634
2660
|
caption,
|
|
@@ -2662,28 +2688,38 @@ function DataTable({
|
|
|
2662
2688
|
}),
|
|
2663
2689
|
pageKeys
|
|
2664
2690
|
});
|
|
2665
|
-
const
|
|
2691
|
+
const recentFits = React39__namespace.useRef([]);
|
|
2692
|
+
const hiddenKey = layout.hidden.join("\0");
|
|
2693
|
+
const orderKey = layout.order.join("\0");
|
|
2666
2694
|
React39__namespace.useEffect(() => {
|
|
2667
2695
|
const scroller = scrollerRef.current;
|
|
2668
2696
|
if (!scroller || !autoFit) return;
|
|
2669
2697
|
const controls = (selectable ? SELECT_WIDTH : 0) + (renderExpanded ? EXPAND_WIDTH : 0);
|
|
2698
|
+
recentFits.current = [];
|
|
2670
2699
|
const run = () => {
|
|
2671
2700
|
const available = scroller.clientWidth - controls - 2;
|
|
2672
|
-
|
|
2673
|
-
|
|
2701
|
+
const now = Date.now();
|
|
2702
|
+
const recent = recentFits.current.filter((entry) => now - entry.at < FIT_CYCLE_MS);
|
|
2703
|
+
recentFits.current = recent;
|
|
2704
|
+
if (recent.some((entry) => Math.abs(available - entry.width) < 1)) return;
|
|
2705
|
+
recent.push({ width: available, at: now });
|
|
2674
2706
|
fitTo(available);
|
|
2675
2707
|
};
|
|
2676
2708
|
run();
|
|
2677
2709
|
const observer = new ResizeObserver(run);
|
|
2678
2710
|
observer.observe(scroller);
|
|
2679
2711
|
return () => observer.disconnect();
|
|
2680
|
-
}, [autoFit, fitTo, selectable, renderExpanded,
|
|
2712
|
+
}, [autoFit, fitTo, selectable, renderExpanded, hiddenKey, orderKey]);
|
|
2681
2713
|
React39__namespace.useEffect(() => {
|
|
2682
2714
|
const scroller = scrollerRef.current;
|
|
2683
2715
|
if (!scroller) return;
|
|
2684
2716
|
const update = () => {
|
|
2685
2717
|
const max = scroller.scrollWidth - scroller.clientWidth;
|
|
2686
|
-
|
|
2718
|
+
const left = scroller.scrollLeft > 1;
|
|
2719
|
+
const right = scroller.scrollLeft < max - 1;
|
|
2720
|
+
setScrolled(
|
|
2721
|
+
(current) => current.left === left && current.right === right ? current : { left, right }
|
|
2722
|
+
);
|
|
2687
2723
|
};
|
|
2688
2724
|
update();
|
|
2689
2725
|
scroller.addEventListener("scroll", update, { passive: true });
|
|
@@ -2694,6 +2730,34 @@ function DataTable({
|
|
|
2694
2730
|
observer.disconnect();
|
|
2695
2731
|
};
|
|
2696
2732
|
}, [visible.length, data.length]);
|
|
2733
|
+
const moreRef = React39__namespace.useRef(null);
|
|
2734
|
+
const onLoadMoreRef = React39__namespace.useRef(onLoadMore);
|
|
2735
|
+
React39__namespace.useEffect(() => {
|
|
2736
|
+
onLoadMoreRef.current = onLoadMore;
|
|
2737
|
+
});
|
|
2738
|
+
const askedAt = React39__namespace.useRef(-1);
|
|
2739
|
+
const canLoadMore = Boolean(onLoadMore) && (hasMore ?? (total === void 0 || data.length < total));
|
|
2740
|
+
const showLoadMore = canLoadMore && !error && data.length > 0;
|
|
2741
|
+
const showPager = pagination ?? !onLoadMore;
|
|
2742
|
+
const askForMore = () => {
|
|
2743
|
+
if (askedAt.current === data.length) return;
|
|
2744
|
+
askedAt.current = data.length;
|
|
2745
|
+
onLoadMoreRef.current?.();
|
|
2746
|
+
};
|
|
2747
|
+
React39__namespace.useEffect(() => {
|
|
2748
|
+
const sentinel = moreRef.current;
|
|
2749
|
+
const scroller = scrollerRef.current;
|
|
2750
|
+
if (!sentinel || !scroller || !showLoadMore || !autoLoadMore || loading) return;
|
|
2751
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
2752
|
+
const observer = new IntersectionObserver(
|
|
2753
|
+
(entries) => {
|
|
2754
|
+
if (entries.some((entry) => entry.isIntersecting)) askForMore();
|
|
2755
|
+
},
|
|
2756
|
+
{ root: scroller, rootMargin: "120px" }
|
|
2757
|
+
);
|
|
2758
|
+
observer.observe(sentinel);
|
|
2759
|
+
return () => observer.disconnect();
|
|
2760
|
+
}, [showLoadMore, autoLoadMore, loading, data.length]);
|
|
2697
2761
|
const offsets = React39__namespace.useMemo(() => {
|
|
2698
2762
|
const result = /* @__PURE__ */ new Map();
|
|
2699
2763
|
let leftParts = selectable ? [`${SELECT_WIDTH}px`] : [];
|
|
@@ -2945,14 +3009,21 @@ function DataTable({
|
|
|
2945
3009
|
/* @__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
3010
|
const key = rowKey(row);
|
|
2947
3011
|
const isOpen = expanded.has(key);
|
|
3012
|
+
const context = { rowIndex };
|
|
3013
|
+
const hostClass = rowClassName?.(row, context);
|
|
3014
|
+
const hostProps = rowProps?.(row, context);
|
|
2948
3015
|
return /* @__PURE__ */ jsxRuntime.jsxs(React39__namespace.Fragment, { children: [
|
|
2949
3016
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2950
3017
|
"tr",
|
|
2951
3018
|
{
|
|
2952
|
-
|
|
3019
|
+
...hostProps,
|
|
3020
|
+
className: cn("mz-dt__row", hostClass, hostProps?.className),
|
|
2953
3021
|
"data-selected": rows.selected.has(key) || rows.allMatching || void 0,
|
|
2954
3022
|
"data-clickable": onRowClick ? true : void 0,
|
|
2955
|
-
onClick: onRowClick ? () =>
|
|
3023
|
+
onClick: onRowClick || hostProps?.onClick ? (event) => {
|
|
3024
|
+
hostProps?.onClick?.(event);
|
|
3025
|
+
onRowClick?.(row);
|
|
3026
|
+
} : void 0,
|
|
2956
3027
|
children: [
|
|
2957
3028
|
selectable ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2958
3029
|
"td",
|
|
@@ -3001,7 +3072,11 @@ function DataTable({
|
|
|
3001
3072
|
]
|
|
3002
3073
|
}
|
|
3003
3074
|
),
|
|
3004
|
-
isOpen && renderExpanded ?
|
|
3075
|
+
isOpen && renderExpanded ? (
|
|
3076
|
+
// The detail panel belongs to the same record, so a row
|
|
3077
|
+
// tint carries onto it; the attributes do not.
|
|
3078
|
+
/* @__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) }) })
|
|
3079
|
+
) : null
|
|
3005
3080
|
] }, key);
|
|
3006
3081
|
}) })
|
|
3007
3082
|
] }),
|
|
@@ -3023,11 +3098,12 @@ function DataTable({
|
|
|
3023
3098
|
/* @__PURE__ */ jsxRuntime.jsx("p", { children: error }),
|
|
3024
3099
|
onRetry ? /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", variant: "secondary", onClick: onRetry, children: labels.retry }) : null
|
|
3025
3100
|
] }) : null,
|
|
3101
|
+
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
3102
|
loading && data.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mz-dt__loading-bar", "aria-hidden": "true" }) : null
|
|
3027
3103
|
]
|
|
3028
3104
|
}
|
|
3029
3105
|
),
|
|
3030
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3106
|
+
showPager ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3031
3107
|
DataTablePagination,
|
|
3032
3108
|
{
|
|
3033
3109
|
query,
|
|
@@ -3038,7 +3114,7 @@ function DataTable({
|
|
|
3038
3114
|
locale,
|
|
3039
3115
|
onQueryChange
|
|
3040
3116
|
}
|
|
3041
|
-
),
|
|
3117
|
+
) : null,
|
|
3042
3118
|
selectable && (rows.count ?? 0) > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mz-dt__bulkbar", role: "region", "aria-label": labels.bulkActions, children: [
|
|
3043
3119
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mz-dt__bulkbar-count", children: [
|
|
3044
3120
|
labels.selectedCount(
|
|
@@ -3073,6 +3149,10 @@ function describeFilter(value, def, labels) {
|
|
|
3073
3149
|
return `: ${value.from ?? "\u2026"} \u2013 ${value.to ?? "\u2026"}`;
|
|
3074
3150
|
case "boolean":
|
|
3075
3151
|
return `: ${value.value ? labels.yes.toLowerCase() : labels.no.toLowerCase()}`;
|
|
3152
|
+
case "custom": {
|
|
3153
|
+
const text = (def?.type === "custom" ? def.describe?.(value.value) : void 0) ?? value.label;
|
|
3154
|
+
return text ? `: ${text}` : "";
|
|
3155
|
+
}
|
|
3076
3156
|
}
|
|
3077
3157
|
}
|
|
3078
3158
|
|