@morze/ui 0.1.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/LICENSE +21 -0
- package/README.md +455 -0
- package/dist/index.cjs +3331 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +692 -0
- package/dist/index.d.ts +692 -0
- package/dist/index.js +3161 -0
- package/dist/index.js.map +1 -0
- package/dist/morze-ui-tokens.css +1 -0
- package/dist/morze-ui.css +1 -0
- package/package.json +89 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Morze Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# @morze/ui
|
|
2
|
+
|
|
3
|
+
Morze UI — a React component kit shaped like **shadcn/ui (latest)** on **Radix**
|
|
4
|
+
primitives, wearing the convex look from the Morze landing: a 135° gradient, a
|
|
5
|
+
hairline rim, an inset highlight on top and a tone glow underneath.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
background-color: rgb(tone);
|
|
11
|
+
background-image: linear-gradient(135deg, rgba(255,255,255,.07), rgba(0,0,0,.14));
|
|
12
|
+
border: 1px solid rgba(255,255,255,.18);
|
|
13
|
+
box-shadow: 0 3px 10px -4px rgba(tone,.4),
|
|
14
|
+
inset 0 1px 0 rgba(255,255,255,.35),
|
|
15
|
+
inset 0 -1px 0 rgba(0,0,0,.2);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That recipe is applied to **every active element**: buttons, toggles,
|
|
19
|
+
checkboxes, radios, switches, sliders, tabs, the select trigger, badges and the
|
|
20
|
+
active navigation item.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm i @morze/ui
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`react` and `react-dom` **19** are peer dependencies: like shadcn after its
|
|
29
|
+
React 19 rewrite, components take `ref` as a plain prop and use no `forwardRef`.
|
|
30
|
+
Tailwind is **not required** — the package ships compiled CSS.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// once, at the app root
|
|
34
|
+
import '@morze/ui/styles.css'
|
|
35
|
+
|
|
36
|
+
import { Button, Switch, MorzeThemeProvider } from '@morze/ui'
|
|
37
|
+
|
|
38
|
+
export default function App() {
|
|
39
|
+
return (
|
|
40
|
+
<MorzeThemeProvider defaultTheme="dark">
|
|
41
|
+
<Button dot>Submit request</Button>
|
|
42
|
+
<Switch defaultChecked />
|
|
43
|
+
</MorzeThemeProvider>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Tokens only, without components: `import '@morze/ui/tokens.css'`.
|
|
49
|
+
|
|
50
|
+
## Themes
|
|
51
|
+
|
|
52
|
+
Dark is the default, light is opt-in through an attribute (or the
|
|
53
|
+
`.mz-theme-light` / `.mz-theme-dark` classes if attributes are inconvenient):
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<html data-mz-theme="light">
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`MorzeThemeProvider` does that for you, remembers the choice in `localStorage`
|
|
60
|
+
and understands `system`:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
const { resolvedTheme, setTheme } = useMorzeTheme()
|
|
64
|
+
setTheme('light') // 'dark' | 'light' | 'system'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The first render is always `defaultTheme`; the stored preference is applied in
|
|
68
|
+
an effect after mount, so server and client markup agree and hydration does not
|
|
69
|
+
break. Storage access is wrapped in try/catch — with site data blocked the
|
|
70
|
+
theme simply does not persist.
|
|
71
|
+
|
|
72
|
+
The provider renders a wrapper with the `mz-root` class (background and text
|
|
73
|
+
colour from the tokens). By default `data-mz-theme` is written to `<html>`, so
|
|
74
|
+
portalled dialogs and menus follow the theme too. `target="element"` scopes the
|
|
75
|
+
theme to the wrapper instead.
|
|
76
|
+
|
|
77
|
+
## Tones
|
|
78
|
+
|
|
79
|
+
Every active component accepts `tone`, which re-points the gradient, the glow
|
|
80
|
+
and the focus ring:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<Button tone="accent">Save</Button>
|
|
84
|
+
<Switch tone="success" defaultChecked />
|
|
85
|
+
<Badge tone="danger" dot>Live</Badge>
|
|
86
|
+
<Progress value={40} tone="warning" />
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`primary` (violet) · `accent`/`success` (emerald) · `danger` · `warning` ·
|
|
90
|
+
`info`. A tone is the `--mz-tone-rgb` custom property, so a one-off value works
|
|
91
|
+
too: `style={{ '--mz-tone-rgb': '255, 100, 130' }}` (channels separated by
|
|
92
|
+
commas — the value is substituted into `rgb()`/`rgba()`).
|
|
93
|
+
|
|
94
|
+
## Shape and type
|
|
95
|
+
|
|
96
|
+
The default shape is **rectangular**: one small radius for controls, a slightly
|
|
97
|
+
larger one for surfaces.
|
|
98
|
+
|
|
99
|
+
| Token | Value | Used by |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `--mz-radius-sm` | `6px` | items inside containers: menu items, tabs, checkbox |
|
|
102
|
+
| `--mz-radius` | `8px` | buttons, inputs, select, toggles, badges, alerts |
|
|
103
|
+
| `--mz-radius-lg` | `12px` | cards, dialogs, popovers, menus |
|
|
104
|
+
| `--mz-radius-pill` | `999px` | only what is round by meaning: radio, indicator dots |
|
|
105
|
+
|
|
106
|
+
Fully square corners: `--mz-radius: 0; --mz-radius-sm: 0; --mz-radius-lg: 0`.
|
|
107
|
+
|
|
108
|
+
Fonts are **system stacks** — nothing is downloaded and the kit inherits the
|
|
109
|
+
host application's look. Control labels are plain sentence case.
|
|
110
|
+
|
|
111
|
+
```css
|
|
112
|
+
:root {
|
|
113
|
+
--mz-primary-rgb: 120, 90, 250; /* brand colour */
|
|
114
|
+
--mz-font-sans: 'Inter', sans-serif;
|
|
115
|
+
--mz-radius: 4px; /* tighter */
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
To bring back the landing's own look (pills plus mono caps):
|
|
120
|
+
|
|
121
|
+
```css
|
|
122
|
+
:root {
|
|
123
|
+
--mz-radius-control: var(--mz-radius-pill);
|
|
124
|
+
--mz-label-font: 'JetBrains Mono', ui-monospace, monospace;
|
|
125
|
+
--mz-label-transform: uppercase;
|
|
126
|
+
--mz-label-tracking: 0.08em;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## States
|
|
131
|
+
|
|
132
|
+
Hover **darkens the colour** and grows the element slightly **in place** —
|
|
133
|
+
nothing shifts position, and there is no shine sweep. Press darkens further and
|
|
134
|
+
shrinks a little.
|
|
135
|
+
|
|
136
|
+
```css
|
|
137
|
+
:root {
|
|
138
|
+
--mz-hover-scale: 1.02; /* growth on hover */
|
|
139
|
+
--mz-press-scale: 0.98; /* dip on press */
|
|
140
|
+
--mz-hover-mix: 88%; /* fill colour: 88% tone + 12% black */
|
|
141
|
+
--mz-press-mix: 80%;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The tone fill is stored as a flat colour (`--mz-fill`) under a fixed sheen
|
|
146
|
+
gradient (`--mz-fill-sheen`), so `background-color` animates smoothly —
|
|
147
|
+
browsers do not interpolate gradients. Darkening goes through `color-mix()`;
|
|
148
|
+
where that is unsupported the element simply does not darken and everything
|
|
149
|
+
else still works.
|
|
150
|
+
|
|
151
|
+
Only **filled** surfaces darken: `primary`, `destructive`, a pressed toggle, the
|
|
152
|
+
active tab, a checked checkbox, radio or switch. Neutral variants (`secondary`,
|
|
153
|
+
`outline`, `ghost`) and unselected controls lighten instead — darkening would
|
|
154
|
+
sink them into the page.
|
|
155
|
+
|
|
156
|
+
## Customisation
|
|
157
|
+
|
|
158
|
+
Everything is driven by custom properties; override them after importing the
|
|
159
|
+
styles. The main groups: surfaces (`--mz-bg`, `--mz-surface`, `--mz-elevated`),
|
|
160
|
+
borders, text, tones, the convex recipe (`--mz-convex-*`, `--mz-face*`,
|
|
161
|
+
`--mz-well*`), geometry (`--mz-radius*`), type (`--mz-font-*`, `--mz-label-*`),
|
|
162
|
+
motion and states (`--mz-ease`, `--mz-duration`, `--mz-hover-scale`,
|
|
163
|
+
`--mz-press-scale`, `--mz-hover-mix`, `--mz-press-mix`), focus (`--mz-ring-*`).
|
|
164
|
+
|
|
165
|
+
The full list lives in `dist/morze-ui-tokens.css`.
|
|
166
|
+
|
|
167
|
+
## Components
|
|
168
|
+
|
|
169
|
+
| Input and actions | Navigation and output | Overlays |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| `Button` `Toggle` `ToggleGroup` | `Tabs` `Accordion` | `Dialog` |
|
|
172
|
+
| `Checkbox` `RadioGroup` `Switch` | `Card` `Alert` `Badge` | `DropdownMenu` |
|
|
173
|
+
| `Slider` `Input` `Textarea` | `Progress` `Avatar` | `Popover` |
|
|
174
|
+
| `Label` `Field` `Select` | `Separator` `Skeleton` `Spinner` | `Tooltip` |
|
|
175
|
+
| `DataTable` | `Sidebar` | `Sheet` |
|
|
176
|
+
|
|
177
|
+
The API mirrors shadcn/ui: same names, same sub-component composition,
|
|
178
|
+
`asChild`, a `data-slot` on every part — so examples from the shadcn docs work
|
|
179
|
+
with barely any edits.
|
|
180
|
+
|
|
181
|
+
What this kit adds on top:
|
|
182
|
+
|
|
183
|
+
- `Button` — `variant`: `primary` (default) `secondary` `outline` `ghost`
|
|
184
|
+
`destructive` `link`; `size`: `xs` (28px) `sm` (34) `md` (40, default)
|
|
185
|
+
`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
|
|
187
|
+
shared by buttons, inputs and toggles, so controls line up in a row. With
|
|
188
|
+
`asChild` the loading indicator is grafted inside the child element and the
|
|
189
|
+
blocked state is expressed through `aria-disabled`, since a link has no
|
|
190
|
+
`disabled`.
|
|
191
|
+
- `ToggleGroup` — `appearance`: `segmented` (a sunken well with the active item
|
|
192
|
+
raised), `joined` (one continuous bar), `spaced`.
|
|
193
|
+
- `TabsList` — `variant`: `default` (well) or `line` (underline).
|
|
194
|
+
- `Checkbox` / `Switch` / `Avatar` — `size`: `sm` `md` `lg`.
|
|
195
|
+
- `Input` — the size prop is `inputSize` (`sm` `md` `lg`); the name differs from
|
|
196
|
+
shadcn to avoid clashing with the native `size` attribute on `<input>`.
|
|
197
|
+
- `SelectTrigger` — `size`: `sm` `md` (no `lg`).
|
|
198
|
+
- `Card` — `interactive` adds the hover state; combined with `onClick` the card
|
|
199
|
+
also gets `role="button"`, `tabIndex` and Enter/Space activation.
|
|
200
|
+
- `Spinner` — `label` (default `Loading`) is announced by screen readers;
|
|
201
|
+
`label={null}` makes it purely decorative.
|
|
202
|
+
- `Badge` — `variant`: `solid` `soft` `outline`, plus `dot`.
|
|
203
|
+
- `Field` / `FieldHint` / `FieldError` — form scaffolding.
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
<ToggleGroup type="single" defaultValue="week" appearance="segmented">
|
|
207
|
+
<ToggleGroupItem value="day">Day</ToggleGroupItem>
|
|
208
|
+
<ToggleGroupItem value="week">Week</ToggleGroupItem>
|
|
209
|
+
</ToggleGroup>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Sidebar
|
|
213
|
+
|
|
214
|
+
A navigation column: it collapses into an icon rail, turns into a sliding sheet
|
|
215
|
+
on narrow screens and remembers its state between sessions.
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
<SidebarProvider>
|
|
219
|
+
<Sidebar collapsible="icon">
|
|
220
|
+
<SidebarHeader>
|
|
221
|
+
<Logo className="mz-sidebar-hide-collapsed" />
|
|
222
|
+
<b className="mz-sidebar-hide-collapsed">Morze ERP</b>
|
|
223
|
+
<SidebarTrigger className="mz-sidebar-push" />
|
|
224
|
+
</SidebarHeader>
|
|
225
|
+
<SidebarContent>
|
|
226
|
+
<SidebarGroup>
|
|
227
|
+
<SidebarGroupLabel>Work</SidebarGroupLabel>
|
|
228
|
+
<SidebarMenu>
|
|
229
|
+
<SidebarMenuItem>
|
|
230
|
+
<SidebarMenuButton isActive tooltip="Orders">
|
|
231
|
+
<OrdersIcon />
|
|
232
|
+
<span>Orders</span>
|
|
233
|
+
</SidebarMenuButton>
|
|
234
|
+
<SidebarMenuBadge>12</SidebarMenuBadge>
|
|
235
|
+
</SidebarMenuItem>
|
|
236
|
+
</SidebarMenu>
|
|
237
|
+
</SidebarGroup>
|
|
238
|
+
</SidebarContent>
|
|
239
|
+
<SidebarFooter>…</SidebarFooter>
|
|
240
|
+
</Sidebar>
|
|
241
|
+
|
|
242
|
+
<SidebarInset>
|
|
243
|
+
{/* the top bar stays on the page background, next to the navigation */}
|
|
244
|
+
<header>…</header>
|
|
245
|
+
<SidebarPanel>{children}</SidebarPanel>
|
|
246
|
+
</SidebarInset>
|
|
247
|
+
</SidebarProvider>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
- **Collapsing** — `collapsible`: `icon` (an icon rail, the default),
|
|
251
|
+
`offcanvas` (slides away entirely), `none`. Toggled by `SidebarTrigger` or
|
|
252
|
+
**⌘/Ctrl + B**. There is also an optional `SidebarRail` — an invisible strip
|
|
253
|
+
along the panel edge that toggles it on click; it is not rendered by default,
|
|
254
|
+
add it if you want that gesture.
|
|
255
|
+
- **The rail** hides labels; items with a `tooltip` prop show theirs on the
|
|
256
|
+
right instead. Anything that only makes sense at full width (a wordmark, a
|
|
257
|
+
user name) is marked with `mz-sidebar-hide-collapsed`, otherwise the rail
|
|
258
|
+
clips it. That class is declared with `!important` — it has to beat an inline
|
|
259
|
+
`display` on a logo or an avatar. To push an element to the far end of the
|
|
260
|
+
header use `mz-sidebar-push` rather than an inline `margin-left: auto`: in the
|
|
261
|
+
rail it switches off, otherwise the button drifts off the axis the menu items
|
|
262
|
+
below sit on.
|
|
263
|
+
- **Mobile** — below `mobileBreakpoint` (768px) the panel renders as a `Sheet`
|
|
264
|
+
over the content; a rail would eat what little width is left.
|
|
265
|
+
- **State** lives in `localStorage` (`storageKey`, `null` disables it). The first
|
|
266
|
+
render is always `defaultOpen` and the stored value lands in an effect, so SSR
|
|
267
|
+
and hydration agree. It can also be driven from outside via `open` /
|
|
268
|
+
`onOpenChange`.
|
|
269
|
+
- **Variants** — `variant`:
|
|
270
|
+
- `inset` (default) — the navigation sits directly on the page background and
|
|
271
|
+
the raised surface is the content inside `SidebarPanel`: only the corner
|
|
272
|
+
facing the navigation is rounded, the other three sides are flush with the
|
|
273
|
+
window, and the shadow is cast back towards the sidebar. Put the top bar and
|
|
274
|
+
breadcrumbs in `SidebarInset` **above** the panel, not inside it.
|
|
275
|
+
- `sidebar` — the navigation gets its own surface and border.
|
|
276
|
+
- `floating` — the navigation lifts off the edge as a separate card.
|
|
277
|
+
|
|
278
|
+
The side is `side="left" | "right"` (the panel's rounded corner mirrors
|
|
279
|
+
itself). The panel's radius and padding are `--mz-sidebar-panel-radius` and
|
|
280
|
+
`--mz-sidebar-panel-pad`. The `mz-sidebar-layout--grid` class on
|
|
281
|
+
`SidebarProvider` adds a 48px grid to the background.
|
|
282
|
+
|
|
283
|
+
`SidebarTrigger` lives in the panel's own header: with `collapsible="icon"`
|
|
284
|
+
(the default) the sidebar never disappears completely — the rail stays, and so
|
|
285
|
+
does the button.
|
|
286
|
+
|
|
287
|
+
The exception is **mobile and `offcanvas`**, where the panel leaves together
|
|
288
|
+
with everything inside it, so a second trigger is needed outside. To avoid two
|
|
289
|
+
buttons on desktop, render it conditionally:
|
|
290
|
+
|
|
291
|
+
```tsx
|
|
292
|
+
function MobileTrigger() {
|
|
293
|
+
const { isMobile } = useSidebar()
|
|
294
|
+
return isMobile ? <SidebarTrigger /> : null
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
The default height is `100dvh`. When the sidebar lives inside a panel rather
|
|
299
|
+
than owning the page, set `--mz-sidebar-h: 100%` on `SidebarProvider`.
|
|
300
|
+
|
|
301
|
+
**`Sheet`** is exported separately — a dialog anchored to an edge of the screen
|
|
302
|
+
(`side`: `top | right | bottom | left`), and what the mobile sidebar uses.
|
|
303
|
+
|
|
304
|
+
## DataTable
|
|
305
|
+
|
|
306
|
+
A table for server-driven data: it never sorts or filters anything itself, it
|
|
307
|
+
collects state and hands it over as a single object. One user action, one
|
|
308
|
+
request.
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
const { query, setQuery } = useTableQuery({
|
|
312
|
+
initial: { sort: [{ id: 'date', dir: 'desc' }], pageSize: 25 },
|
|
313
|
+
urlKey: 'orders.', // filters and sorting in the query string
|
|
314
|
+
})
|
|
315
|
+
const { rows, total, loading, error } = useOrders(query)
|
|
316
|
+
|
|
317
|
+
const columns: DataTableColumn<Order>[] = [
|
|
318
|
+
{ id: 'number', header: '#', width: 120, sortable: true, pinned: 'left',
|
|
319
|
+
cell: (row) => <b>{row.number}</b> },
|
|
320
|
+
{ id: 'client', header: 'Client', sortable: true,
|
|
321
|
+
filter: { type: 'select', options: clients } },
|
|
322
|
+
{ id: 'sum', header: 'Total', align: 'right', sortable: true,
|
|
323
|
+
accessor: (row) => formatMoney(row.sum),
|
|
324
|
+
filter: { type: 'number-range', step: 10_000 } },
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
<DataTable
|
|
328
|
+
columns={columns} data={rows} total={total} rowKey={(row) => row.id}
|
|
329
|
+
loading={loading} error={error}
|
|
330
|
+
query={query} onQueryChange={setQuery}
|
|
331
|
+
persistKey="orders" // column widths, order, visibility
|
|
332
|
+
selection={selection} onSelectionChange={setSelection}
|
|
333
|
+
bulkActions={() => <Button size="sm">Export</Button>}
|
|
334
|
+
renderExpanded={(row) => <OrderDetails id={row.id} />}
|
|
335
|
+
/>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
What it does:
|
|
339
|
+
|
|
340
|
+
- **Sorting** — a header click cycles `asc → desc → off`, Shift adds the column
|
|
341
|
+
to a composite sort (an ordered list reaches the backend).
|
|
342
|
+
- **Header filters** — `text`, `select`, `number-range`, `date-range`,
|
|
343
|
+
`boolean`. A value is staged in the popover and committed on Apply: otherwise
|
|
344
|
+
every keystroke would be a request. Active filters are echoed as chips above
|
|
345
|
+
the table.
|
|
346
|
+
- **Default widths** — columns stretch to fill the container, so there is no
|
|
347
|
+
dead space at the right edge. The maths runs off the declared `width` values
|
|
348
|
+
(they act as proportions), re-runs when the container resizes and always
|
|
349
|
+
produces the same result for the same width — resizing the window back and
|
|
350
|
+
forth does not drift the layout. When even the declared widths do not fit,
|
|
351
|
+
they are used as-is and horizontal scrolling kicks in. Turn it off with
|
|
352
|
+
`autoFit={false}`, or per column with `flex: false` (handy for narrow icon or
|
|
353
|
+
action columns). `minWidth` and `maxWidth` are honoured and the surplus is
|
|
354
|
+
redistributed across the rest.
|
|
355
|
+
- **Mouse resize — spreadsheet style**: the column follows the cursor one to
|
|
356
|
+
one, its neighbours do not move, spare space stays at the right, and the
|
|
357
|
+
scrollbar appears when there is none left. Double-clicking the handle fits the
|
|
358
|
+
column to its content; arrows on the focused handle resize from the keyboard.
|
|
359
|
+
During a drag the width is written to a CSS custom property rather than to
|
|
360
|
+
state — not a single row re-render per pixel of travel. A column touched by
|
|
361
|
+
hand is excluded from auto-fit for good.
|
|
362
|
+
- **Pinned columns** left and right, a sticky header, and a hairline on the seam
|
|
363
|
+
between the pinned and scrolling parts.
|
|
364
|
+
- **Row selection** — Shift selects a range, the header checkbox takes the page,
|
|
365
|
+
and the floating bar can escalate to "all N matching" (in that mode a bulk
|
|
366
|
+
action must travel with the query, not with a list of ids).
|
|
367
|
+
- **Columns** — visibility, order (drag and drop plus arrows for the keyboard)
|
|
368
|
+
and pinning; all of it saved to `localStorage` under `persistKey`.
|
|
369
|
+
- **Expandable rows** and **inline cell editing** on double click, with
|
|
370
|
+
optimistic saving and a rollback on failure.
|
|
371
|
+
- **States** — skeletons on the first load, a thin progress line when refetching
|
|
372
|
+
over data already on screen, an empty result and an error with a retry.
|
|
373
|
+
- **Density** `compact | normal | relaxed`.
|
|
374
|
+
|
|
375
|
+
Helpers for your own UI: `useTableQuery`, `useSavedViews`, `useColumnLayout`,
|
|
376
|
+
`useRowSelection`, plus the pure functions `toggleSort`, `setFilter`,
|
|
377
|
+
`serializeSort` / `parseSort`.
|
|
378
|
+
|
|
379
|
+
A table stays a table: on narrow screens it scrolls horizontally with the first
|
|
380
|
+
column pinned, it does not reflow into cards.
|
|
381
|
+
|
|
382
|
+
## Localisation
|
|
383
|
+
|
|
384
|
+
Component strings default to English and every one of them can be replaced.
|
|
385
|
+
`Dialog`/`Sheet` take `closeLabel`, `Spinner` takes `label`, `Sidebar` takes
|
|
386
|
+
`mobileTitle` / `mobileDescription` and `SidebarTrigger` takes `label`.
|
|
387
|
+
|
|
388
|
+
`DataTable` has more strings than the rest, so it takes a partial `labels`
|
|
389
|
+
object — anything omitted falls back to the English default:
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
import { DataTable, type DataTableLabels } from '@morze/ui'
|
|
393
|
+
|
|
394
|
+
const de: Partial<DataTableLabels> = {
|
|
395
|
+
columns: 'Spalten',
|
|
396
|
+
apply: 'Anwenden',
|
|
397
|
+
reset: 'Zuruecksetzen',
|
|
398
|
+
empty: 'Nichts gefunden',
|
|
399
|
+
rowsPerPage: 'Zeilen pro Seite',
|
|
400
|
+
selectedCount: (count) => `Ausgewaehlt: ${count}`,
|
|
401
|
+
sortBy: (column) => `Nach "${column}" sortieren`,
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
<DataTable labels={de} locale="de-DE" … />
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`locale` controls number formatting; without it the browser's locale is used.
|
|
408
|
+
The complete set of keys is `DataTableLabels`, with defaults in
|
|
409
|
+
`defaultDataTableLabels`.
|
|
410
|
+
|
|
411
|
+
## Accessibility and behaviour
|
|
412
|
+
|
|
413
|
+
- Behaviour, focus management, keyboard and ARIA come from Radix.
|
|
414
|
+
- Focus is drawn with an `outline` rather than a shadow, so it never fights the
|
|
415
|
+
convex layers; width and offset are `--mz-ring-width` / `--mz-ring-offset`.
|
|
416
|
+
- `@media (prefers-reduced-motion: reduce)` disables animation and the growth.
|
|
417
|
+
- Classes are prefixed with `mz-` and variables with `--mz-`, so the kit does
|
|
418
|
+
not collide with application styles (Tailwind included). The base layer has no
|
|
419
|
+
descendant selectors: your own markup inside a `Card` or `Dialog` keeps its
|
|
420
|
+
box model and typography.
|
|
421
|
+
|
|
422
|
+
## Next.js
|
|
423
|
+
|
|
424
|
+
The bundle is marked `"use client"` — import components into client parts and
|
|
425
|
+
add the CSS in `app/layout.tsx`.
|
|
426
|
+
|
|
427
|
+
## Development
|
|
428
|
+
|
|
429
|
+
```bash
|
|
430
|
+
npm install
|
|
431
|
+
npm run dev # playground with every component (http://localhost:5250)
|
|
432
|
+
npm run typecheck
|
|
433
|
+
npm run test # vitest: component behaviour plus CSS contracts
|
|
434
|
+
npm run build # dist: ESM + CJS + .d.ts + morze-ui.css
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
`tests/smoke.test.tsx` covers component behaviour and accessibility,
|
|
438
|
+
`tests/css-contract.test.ts` covers the visual layer's contracts (cascade order
|
|
439
|
+
for tones, hover states, isolation from foreign markup, geometry). The second
|
|
440
|
+
file exists because regressions here are cascade regressions: a rule that lands
|
|
441
|
+
later in the bundle and quietly outranks an earlier one.
|
|
442
|
+
|
|
443
|
+
Layout: `src/components/*.tsx` for components (markup and API),
|
|
444
|
+
`src/styles/**` for the visual layer (`tokens.css` → `base.css` →
|
|
445
|
+
`components/*` → `tones.css`). JS is built with tsup, CSS with lightningcss
|
|
446
|
+
(`scripts/build-css.mjs`).
|
|
447
|
+
|
|
448
|
+
## Publishing
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
npm publish --access public
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
The `@morze` scope has to exist in the npm organisation (or point
|
|
455
|
+
`publishConfig` at a private registry).
|