@nxgt/material 1.0.1 → 1.0.2
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 +122 -198
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
# @nxgt/material
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
helpers and the `Icon` component.
|
|
6
|
-
|
|
7
|
-
Built with Tailwind CSS v4 and Base UI. Consumers read `dist/`.
|
|
8
|
-
|
|
9
|
-
---
|
|
3
|
+
React component library: primitives, form fields, layout shell, `DataTable`,
|
|
4
|
+
drag-and-drop, i18n, and `Icon`. Tailwind CSS v4 + Base UI.
|
|
10
5
|
|
|
11
6
|
## Install
|
|
12
7
|
|
|
@@ -14,139 +9,113 @@ Built with Tailwind CSS v4 and Base UI. Consumers read `dist/`.
|
|
|
14
9
|
bun add @nxgt/material
|
|
15
10
|
```
|
|
16
11
|
|
|
17
|
-
Public on npmjs; no token
|
|
12
|
+
Public on npmjs; no token. **Required peers** — the app must resolve the same
|
|
13
|
+
copy, or hooks throw and tokens come from two builds:
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
```bash
|
|
16
|
+
bun add react react-dom react-hook-form react-redux @reduxjs/toolkit @tanstack/react-table react-use
|
|
17
|
+
```
|
|
21
18
|
|
|
22
|
-
| Peer |
|
|
19
|
+
| Peer | Used by |
|
|
23
20
|
| --- | --- |
|
|
24
|
-
| `react`, `react-dom`
|
|
25
|
-
| `react-hook-form` |
|
|
21
|
+
| `react`, `react-dom` `^19` | everything |
|
|
22
|
+
| `react-hook-form` | every `*FormField` |
|
|
26
23
|
| `react-redux`, `@reduxjs/toolkit` | `DataTable`, filter, gallery, kanban, player |
|
|
27
|
-
| `@tanstack/react-table`
|
|
24
|
+
| `@tanstack/react-table` `^9` | `DataTable` |
|
|
28
25
|
| `react-use` | assorted hooks |
|
|
29
26
|
|
|
30
|
-
There is
|
|
31
|
-
`lib/`, so a build triggered on a consumer's machine would have no sources to
|
|
32
|
-
compile and would fail the install outright under npm. Build here, then
|
|
33
|
-
publish.
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## Subpaths
|
|
38
|
-
|
|
39
|
-
| Specifier | What is in it |
|
|
40
|
-
| --- | --- |
|
|
41
|
-
| `@nxgt/material` | the default barrel: components, hooks, i18n, lib, models, types, and the compiled `style` condition `dist/material.css` |
|
|
42
|
-
| `@nxgt/material/components` | every UI component, including `*FormField` wrappers |
|
|
43
|
-
| `@nxgt/material/hooks` | `useTheme`, `useIsClient`, `useMobile`, `useSearchParam`, … |
|
|
44
|
-
| `@nxgt/material/dnd` | `@dnd-kit` primitives re-exported for sortable lists and kanban |
|
|
45
|
-
| `@nxgt/material/i18n` | `I18nProvider`, `mergeResources`, `createTranslator`, the reserved `material` namespace |
|
|
46
|
-
| `@nxgt/material/models` | shared status types |
|
|
47
|
-
| `@nxgt/material/lib` | `cn`, `clientOnly`, date/number/upload helpers, the redux `useSliceReducer` |
|
|
48
|
-
| `@nxgt/material/types` | shared function types |
|
|
49
|
-
| `@nxgt/material/styles.css` | **source** Tailwind v4 `@theme` tokens — this is the stylesheet apps import |
|
|
50
|
-
|
|
51
|
-
The compiled stylesheet at `@nxgt/material` (the package's `style` condition,
|
|
52
|
-
`dist/material.css`) is not what our apps import; they take the tokens and let
|
|
53
|
-
their own Tailwind build produce the utilities.
|
|
27
|
+
The tarball is `dist/`, not `lib/`. There is no `postinstall` build.
|
|
54
28
|
|
|
55
|
-
|
|
29
|
+
## Setup
|
|
56
30
|
|
|
57
|
-
|
|
31
|
+
### Styles
|
|
58
32
|
|
|
59
|
-
|
|
60
|
-
forget: **the icon sprites are yours to supply.**
|
|
61
|
-
|
|
62
|
-
### 1. Install
|
|
63
|
-
|
|
64
|
-
```json
|
|
65
|
-
// package.json
|
|
66
|
-
"dependencies": {
|
|
67
|
-
"@nxgt/material": "…"
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### 2. The three CSS lines
|
|
72
|
-
|
|
73
|
-
In the app's root stylesheet — `app/app.css` in every consumer we have — in
|
|
74
|
-
this order:
|
|
33
|
+
In the app's root stylesheet, in this order:
|
|
75
34
|
|
|
76
35
|
```css
|
|
77
36
|
@import "tailwindcss";
|
|
78
|
-
|
|
79
37
|
@import "@nxgt/material/styles.css";
|
|
80
|
-
|
|
81
38
|
@source "../node_modules/@nxgt/material/";
|
|
82
39
|
```
|
|
83
40
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
- **`@source "../node_modules/@nxgt/material/"`** is not optional and its
|
|
89
|
-
absence does not error. Tailwind v4 generates only the classes it can see in
|
|
90
|
-
the files it scans, and by default it does not scan inside `node_modules`.
|
|
91
|
-
Without this line, every utility class used *inside* a `@nxgt/material`
|
|
92
|
-
component is absent from the output and the components render unstyled — a
|
|
93
|
-
blank-looking app with no message in any console.
|
|
41
|
+
`styles.css` is the **source** `@theme` layer (colours, `--radius`, `dark`,
|
|
42
|
+
fonts) so it runs through *your* Tailwind build. `@source` is required and
|
|
43
|
+
silent when missing: Tailwind v4 does not scan `node_modules`, so without it
|
|
44
|
+
every utility used *inside* a component is absent and the UI renders unstyled.
|
|
94
45
|
|
|
95
|
-
|
|
96
|
-
`
|
|
97
|
-
the tokens and let their own Tailwind build produce the utilities.
|
|
46
|
+
Do not import the compiled `style` condition on `@nxgt/material`
|
|
47
|
+
(`dist/material.css`). Take the tokens; let the app produce the utilities.
|
|
98
48
|
|
|
99
|
-
###
|
|
49
|
+
### Icons
|
|
100
50
|
|
|
101
|
-
`Icon`
|
|
51
|
+
`Icon` points at a **root-relative** URL on *your* origin:
|
|
102
52
|
|
|
103
53
|
```tsx
|
|
104
|
-
|
|
54
|
+
import { Icon } from '@nxgt/material/components';
|
|
55
|
+
|
|
56
|
+
<Icon name="atom" type="duotone" />
|
|
57
|
+
// → /assets/icons/sprites/duotone.svg#atom
|
|
105
58
|
```
|
|
106
59
|
|
|
107
|
-
|
|
108
|
-
package
|
|
60
|
+
Serve Font Awesome Pro sheets from `public/assets/icons/sprites/<style>.svg`.
|
|
61
|
+
This package does **not** ship them: the FA Pro licence covers use in your
|
|
62
|
+
own products, not redistribution inside a library. `files` includes
|
|
63
|
+
`"!dist/assets"` so a local build cannot leak them into a tarball. A missing
|
|
64
|
+
sheet renders as nothing — no error, no fallback.
|
|
109
65
|
|
|
110
|
-
|
|
111
|
-
|
|
66
|
+
## Subpaths
|
|
67
|
+
|
|
68
|
+
| Specifier | What is in it |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `@nxgt/material` | default barrel + compiled `style` condition (unused; import `styles.css`) |
|
|
71
|
+
| `@nxgt/material/components` | UI components and `*FormField` wrappers |
|
|
72
|
+
| `@nxgt/material/hooks` | `useTheme`, `useIsClient`, `useMobile`, `useSearchParam`, … |
|
|
73
|
+
| `@nxgt/material/dnd` | `@dnd-kit` primitives |
|
|
74
|
+
| `@nxgt/material/i18n` | `I18nProvider`, `mergeResources`, `createTranslator` |
|
|
75
|
+
| `@nxgt/material/lib` | `cn`, `clientOnly`, date/number/upload, `useSliceReducer` |
|
|
76
|
+
| `@nxgt/material/models` | status types |
|
|
77
|
+
| `@nxgt/material/types` | shared function types |
|
|
78
|
+
| `@nxgt/material/styles.css` | source Tailwind v4 tokens — **this** is what the app imports |
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
### Components
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { Button, Icon } from '@nxgt/material/components';
|
|
86
|
+
|
|
87
|
+
<Button variant="filled" color="primary">
|
|
88
|
+
<Icon name="plus" />
|
|
89
|
+
Add
|
|
90
|
+
</Button>
|
|
112
91
|
```
|
|
113
92
|
|
|
114
|
-
|
|
115
|
-
7.3.1, whose commercial licence is perpetual in *duration*, not in *scope*: it
|
|
116
|
-
covers using the icons in your own products, not redistributing them inside a
|
|
117
|
-
library. A published `@nxgt/material` tarball that contained them would be
|
|
118
|
-
redistribution regardless of whether this repository is private.
|
|
119
|
-
`files` therefore carries an explicit `"!dist/assets"`, so a stale local build
|
|
120
|
-
cannot leak them back into a tarball by accident.
|
|
93
|
+
### Form fields
|
|
121
94
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
exactly the "own products" case the licence covers; every consumer in the parc
|
|
125
|
-
does it. The line is redistribution through a package, not storage.
|
|
95
|
+
Every field has a standalone (`TextField`) and a React Hook Form wrapper
|
|
96
|
+
(`TextFormField`) in the same barrel. Wrappers take `control` + `name`.
|
|
126
97
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
`sharp-*`, `brands` and the expressive families (`chisel`, `etch`, `jelly`,
|
|
131
|
-
`slickdog`, `slab-press`, …). An icon whose sheet is missing renders as nothing —
|
|
132
|
-
no error, no fallback.
|
|
98
|
+
```tsx
|
|
99
|
+
import { TextFormField, Button } from '@nxgt/material/components';
|
|
100
|
+
import { useForm } from 'react-hook-form';
|
|
133
101
|
|
|
134
|
-
|
|
102
|
+
const form = useForm({ defaultValues: { email: '' } });
|
|
135
103
|
|
|
136
|
-
|
|
104
|
+
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
105
|
+
<TextFormField control={form.control} name="email" label="Email" />
|
|
106
|
+
<Button type="submit">Save</Button>
|
|
107
|
+
</form>
|
|
108
|
+
```
|
|
137
109
|
|
|
138
|
-
|
|
139
|
-
apps own their resource **content**.
|
|
110
|
+
### i18n
|
|
140
111
|
|
|
141
|
-
This package
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
shadows every string this package ships**. Collisions are reported through
|
|
145
|
-
`console.warn` outside production.
|
|
112
|
+
This package owns the **mechanism**; the app owns resource **content**. The
|
|
113
|
+
reserved namespace is `material` — `mergeResources` is a shallow per-language
|
|
114
|
+
merge, so an app-side `material` key silently shadows every string we ship.
|
|
146
115
|
|
|
147
116
|
```ts
|
|
148
|
-
// app/i18n/index.ts
|
|
149
117
|
import {
|
|
118
|
+
I18nProvider,
|
|
150
119
|
createTranslator,
|
|
151
120
|
createTypedTranslation,
|
|
152
121
|
mergeResources,
|
|
@@ -157,119 +126,74 @@ import { resources as appResources } from './resources';
|
|
|
157
126
|
|
|
158
127
|
export const resources = mergeResources(materialResources, appResources);
|
|
159
128
|
export type LocaleKey = keyof FlatObject<typeof resources.en, string>;
|
|
160
|
-
|
|
161
129
|
export const translate = createTranslator<LocaleKey>(resources);
|
|
162
130
|
export const useTranslation = createTypedTranslation<LocaleKey>(translate);
|
|
163
131
|
```
|
|
164
132
|
|
|
165
133
|
```tsx
|
|
166
|
-
// app root — mount once, above everything that translates
|
|
167
134
|
<I18nProvider defaultLanguage="en">
|
|
168
135
|
<App />
|
|
169
136
|
</I18nProvider>
|
|
170
137
|
```
|
|
171
138
|
|
|
172
|
-
Language state lives
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
the
|
|
179
|
-
`
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
New user-facing strings in this package go into **both** `en/material.json`
|
|
183
|
-
and `fr/material.json`, under `material.<component>.*`. Consuming apps must
|
|
184
|
-
never declare a top-level `material` key in their own resources.
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
## The app shell is parameterised
|
|
189
|
-
|
|
190
|
-
`ActivityLayout`, `ActivityContent`, `AppSidebar`, `AppHeader`,
|
|
191
|
-
`AppBreadcrumb`, `MenuItem`:
|
|
192
|
-
|
|
193
|
-
- **`items` is a prop, not a `useMenuItems()` call.** Each app builds and
|
|
194
|
-
filters its own menu.
|
|
195
|
-
- **`MenuItem.label` is a `ReactNode`, already translated — not a key.** This
|
|
196
|
-
package can only resolve its own reserved `material.*` namespace. The app
|
|
197
|
-
translates; the shell renders.
|
|
198
|
-
- **`MenuItem.footer` is explicit.** `partitionMenu` is that rule.
|
|
199
|
-
- **`avatar` and `headerActions` are props.** The shell holds no session.
|
|
200
|
-
- **`AppBreadcrumb` reads `handle.breadcrumb` off `useMatches()`.** It needs
|
|
201
|
-
a **data router** — a story or test must use `createMemoryRouter` +
|
|
202
|
-
`RouterProvider`, not `MemoryRouter`.
|
|
203
|
-
|
|
204
|
-
The active menu entry is the **longest** match, resolved across the whole
|
|
205
|
-
menu (`findActiveMenuItem`), segment-aware — `/admin` does not cover
|
|
206
|
-
`/administrators`. A per-entry predicate cannot work once a menu is mounted
|
|
207
|
-
anywhere but `/`.
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
## SSR consumers evaluate the barrels at boot
|
|
212
|
-
|
|
213
|
-
The SSR apps in sellix-monorepo (`oauth-ui`, `kratos-ui`) bundle this package
|
|
214
|
-
into their server build (`ssr.noExternal: ['@nxgt/material']`): every module
|
|
215
|
-
the `main` / `components` barrels reach is evaluated in the server process at
|
|
216
|
-
startup, whether or not any page renders the component. Storybook and the SPA
|
|
217
|
-
apps never see this, so a change that breaks it passes every check in this
|
|
218
|
-
repo.
|
|
219
|
-
|
|
220
|
-
- **Nothing browser-only at module scope.** No `window`, `document`,
|
|
221
|
-
`DOMMatrix`, `Worker`, and no third-party module that does it for you.
|
|
222
|
-
`pdfjs-dist` (via `react-pdf`) runs `new DOMMatrix()` at import — which is
|
|
223
|
-
why `pdf-viewer/` reaches `react-pdf` only through `react-pdf.lazy.ts` (a
|
|
224
|
-
dynamic `import()`, `Page`/`Document` as `lazy()`), why each component that
|
|
225
|
-
renders them ends its file with `export const X = clientOnly(X)`, and why
|
|
226
|
-
`pdf-viewer/index.test.ts` fails if a static `react-pdf` import comes back.
|
|
227
|
-
`clientOnly` lives in `@nxgt/material/lib`. Reuse the pair — dynamic import
|
|
228
|
-
for the dependency, `clientOnly` for the component — for the next
|
|
229
|
-
browser-only dependency rather than adding a `typeof window` guard. A guard
|
|
230
|
-
around a *statement* does nothing when the *import* is the side effect;
|
|
231
|
-
only `import()` defers it.
|
|
232
|
-
- **Never name an SVG with `<svg><title>`; use `role="img"` +
|
|
233
|
-
`aria-label`.** React 19 treats `<title>` as hoistable document metadata: on
|
|
234
|
-
the server it lifts the element out and emits `<title></title>`, while the
|
|
235
|
-
client renders the text inline. That is a hydration mismatch, and React
|
|
236
|
-
answers one by discarding the whole server tree and regenerating it on the
|
|
237
|
-
client — as a console warning, with nothing visibly broken. `Icon` did this,
|
|
238
|
-
so every SSR page in every consuming app silently client-rendered itself.
|
|
239
|
-
|
|
240
|
-
---
|
|
241
|
-
|
|
242
|
-
## Form fields
|
|
243
|
-
|
|
244
|
-
Every field has a standalone component (`TextField`, `SelectField`, …) and a
|
|
245
|
-
React Hook Form wrapper in the same barrel (`TextFormField`,
|
|
246
|
-
`SelectFormField`, …). The wrappers take `control` + `name` and wire
|
|
247
|
-
`Controller` for you.
|
|
139
|
+
Language state lives only in `I18nProvider`. Components in this package fall
|
|
140
|
+
back to their English copy if the provider is not mounted. Messages are ICU
|
|
141
|
+
MessageFormat; a missing key is returned verbatim.
|
|
142
|
+
|
|
143
|
+
### App shell
|
|
144
|
+
|
|
145
|
+
`items` is a prop — the shell does not build or filter the menu. `label` is
|
|
146
|
+
already translated (`ReactNode`), not a key. `footer: true` pins an entry;
|
|
147
|
+
do not rely on "last item". `avatar` and `headerActions` are props: the shell
|
|
148
|
+
holds no session.
|
|
248
149
|
|
|
249
150
|
```tsx
|
|
250
|
-
import {
|
|
251
|
-
import { useForm } from 'react-hook-form';
|
|
151
|
+
import { ActivityLayout, ActivityContent } from '@nxgt/material/components';
|
|
252
152
|
|
|
253
|
-
const
|
|
153
|
+
const items = [
|
|
154
|
+
{ label: 'Home', url: '/', icon: 'house' },
|
|
155
|
+
{ label: 'Settings', url: '/settings', icon: 'gear', footer: true },
|
|
156
|
+
];
|
|
254
157
|
|
|
255
|
-
<
|
|
158
|
+
<ActivityLayout items={items} brand={<Logo />} avatar={<Avatar />}>
|
|
159
|
+
<Outlet />
|
|
160
|
+
</ActivityLayout>
|
|
256
161
|
```
|
|
257
162
|
|
|
258
|
-
|
|
163
|
+
The active entry is the **longest** match across the whole menu, segment-aware
|
|
164
|
+
(`/admin` does not cover `/administrators`). `AppBreadcrumb` reads
|
|
165
|
+
`handle.breadcrumb` off `useMatches()` and needs a data router
|
|
166
|
+
(`createMemoryRouter` + `RouterProvider`, not `MemoryRouter`).
|
|
167
|
+
|
|
168
|
+
## SSR
|
|
259
169
|
|
|
260
|
-
|
|
170
|
+
An SSR app that bundles this package (`ssr.noExternal: ['@nxgt/material']`)
|
|
171
|
+
evaluates every module the barrels reach at **boot**, whether or not a page
|
|
172
|
+
renders the component.
|
|
173
|
+
|
|
174
|
+
- **Nothing browser-only at module scope.** `pdfjs-dist` runs `new DOMMatrix()`
|
|
175
|
+
at import. Reach it through `import()` and wrap the component with
|
|
176
|
+
`clientOnly` from `@nxgt/material/lib`. A `typeof window` guard around a
|
|
177
|
+
statement does nothing when the *import* is the side effect.
|
|
178
|
+
- **Never `<svg><title>`.** React 19 hoists `<title>` as document metadata:
|
|
179
|
+
server emits `<title></title>`, client renders the text, hydration discards
|
|
180
|
+
the whole tree. Use `role="img"` + `aria-label` (`Icon` already does).
|
|
181
|
+
|
|
182
|
+
## Traps
|
|
183
|
+
|
|
184
|
+
| Symptom | Cause | Fix |
|
|
185
|
+
| --- | --- | --- |
|
|
186
|
+
| Components render unstyled, no console error | Tailwind did not scan the package | `@source "../node_modules/@nxgt/material/"` |
|
|
187
|
+
| `<Icon>` is blank | sprite sheet not served | `public/assets/icons/sprites/<style>.svg` |
|
|
188
|
+
| Package strings vanish after adding i18n | app declared `material` | never a top-level `material` key in app resources |
|
|
189
|
+
| Hydration mismatch on every page | `<svg><title>` | `role="img"` + `aria-label` |
|
|
190
|
+
| `ReferenceError: DOMMatrix` at server boot | static `react-pdf` import | `import()` + `clientOnly` |
|
|
191
|
+
|
|
192
|
+
## Development
|
|
261
193
|
|
|
262
194
|
```sh
|
|
263
195
|
bun install
|
|
264
|
-
bun run build #
|
|
196
|
+
bun run build # what consumers read
|
|
265
197
|
bun run storybook
|
|
266
198
|
bun run test
|
|
267
199
|
```
|
|
268
|
-
|
|
269
|
-
`bun run build` is what consumers read. New reusable components arrive here
|
|
270
|
-
rather than in an app — see the "Reusable UI components belong in
|
|
271
|
-
`@nxgt/material`" section of the consuming repositories' AGENTS.md.
|
|
272
|
-
|
|
273
|
-
A change that should reach a consumer needs a changeset (`bun changeset`) and
|
|
274
|
-
a merge to `develop`. The README is this package's page on npmjs: a public-API
|
|
275
|
-
change updates it in the same PR.
|