@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.
Files changed (2) hide show
  1. package/README.md +122 -198
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,12 +1,7 @@
1
1
  # @nxgt/material
2
2
 
3
- The shared React component library for the nxgt applications primitives,
4
- form fields, layout shells, the `DataTable`, the drag-and-drop kit, i18n
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 needed to install.
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
- **Required peers** — the app must resolve the same copy as this package, or
20
- hooks throw and design tokens come from two builds:
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 | Why |
19
+ | Peer | Used by |
23
20
  | --- | --- |
24
- | `react`, `react-dom` | `^19.2.8` |
25
- | `react-hook-form` | `^7.85.0` — every `*FormField` |
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` | `^9` `DataTable` |
24
+ | `@tanstack/react-table` `^9` | `DataTable` |
28
25
  | `react-use` | assorted hooks |
29
26
 
30
- There is **no `postinstall` build**. The package publishes `dist/` and not
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
- ## Consuming it
31
+ ### Styles
58
32
 
59
- Three things have to be in place. Two are CSS, and the third is the one people
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
- - **`@import "@nxgt/material/styles.css"`** brings in the design tokens: the
85
- `oklch` colour scale, `--radius`, the `dark` custom variant, the font stacks.
86
- It is `lib/styles.css` shipped raw rather than compiled, because Tailwind
87
- v4 needs the source `@theme` layer to run through *your* Tailwind build.
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
- There is a second, compiled stylesheet at `@nxgt/material` (the package's
96
- `style` condition, `dist/material.css`). None of our apps import it; they take
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
- ### 3. The icon sprites — the consumer provides them
49
+ ### Icons
100
50
 
101
- `Icon` renders a `<use>` pointing at a **root-relative** URL:
51
+ `Icon` points at a **root-relative** URL on *your* origin:
102
52
 
103
53
  ```tsx
104
- <use xlinkHref={`/assets/icons/sprites/${style}.svg#${name}`} />
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
- That URL is resolved by the browser against **your** origin, not against the
108
- package. So the app must serve the sprite sheets itself, from:
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
- public/assets/icons/sprites/<style>.svg
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
- **This package deliberately does not ship them.** They are Font Awesome Pro
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
- **Committing the sheets in a consuming application is a different thing, and it
123
- is fine.** A private application repository holding the sprites it serves is
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
- Each consuming app therefore commits its own sprites into
128
- `public/assets/icons/sprites/`, taken from its own Font Awesome Pro kit. The styles in use across the parc are
129
- the 37 sheets covering `regular`, `light`, `thin`, `solid`, `duotone`,
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
- ## i18n
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
- `@nxgt/material` owns the i18n **mechanism** (`@nxgt/material/i18n`); consuming
139
- apps own their resource **content**.
110
+ ### i18n
140
111
 
141
- This package ships a base resource bundle and reserves exactly **one
142
- top-level namespace per language: `material`**. `mergeResources` is a
143
- *shallow*, per-language merge, so an app-side `material` namespace **silently
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 **only** in `I18nProvider`. Never re-introduce a
173
- per-call-site `useState` / `useLocalStorage` for language that is the bug
174
- that forced `window.location.reload()` in language switchers.
175
-
176
- Components inside this package use `useOptionalI18nContext()` /
177
- `useMaterialTranslation()` and always pass their historical English copy as
178
- the fallback, so they keep rendering in apps that have not mounted
179
- `I18nProvider` yet. Messages are ICU MessageFormat; a missing key is returned
180
- verbatim rather than throwing.
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 { TextFormField, Button } from '@nxgt/material/components';
251
- import { useForm } from 'react-hook-form';
151
+ import { ActivityLayout, ActivityContent } from '@nxgt/material/components';
252
152
 
253
- const form = useForm({ defaultValues: { email: '' } });
153
+ const items = [
154
+ { label: 'Home', url: '/', icon: 'house' },
155
+ { label: 'Settings', url: '/settings', icon: 'gear', footer: true },
156
+ ];
254
157
 
255
- <TextFormField control={form.control} name="email" label="Email" />
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
- ## Working on this package
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 # tsc -b && vite build, then copies lib/styles.css to dist/
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",