@nxgt/material 1.0.0 → 1.0.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.
Files changed (2) hide show
  1. package/README.md +181 -13
  2. package/package.json +7 -2
package/README.md CHANGED
@@ -8,6 +8,52 @@ Built with Tailwind CSS v4 and Base UI. Consumers read `dist/`.
8
8
 
9
9
  ---
10
10
 
11
+ ## Install
12
+
13
+ ```bash
14
+ bun add @nxgt/material
15
+ ```
16
+
17
+ Public on npmjs; no token needed to install.
18
+
19
+ **Required peers** — the app must resolve the same copy as this package, or
20
+ hooks throw and design tokens come from two builds:
21
+
22
+ | Peer | Why |
23
+ | --- | --- |
24
+ | `react`, `react-dom` | `^19.2.8` |
25
+ | `react-hook-form` | `^7.85.0` — every `*FormField` |
26
+ | `react-redux`, `@reduxjs/toolkit` | `DataTable`, filter, gallery, kanban, player |
27
+ | `@tanstack/react-table` | `^9` — `DataTable` |
28
+ | `react-use` | assorted hooks |
29
+
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.
54
+
55
+ ---
56
+
11
57
  ## Consuming it
12
58
 
13
59
  Three things have to be in place. Two are CSS, and the third is the one people
@@ -15,7 +61,7 @@ forget: **the icon sprites are yours to supply.**
15
61
 
16
62
  ### 1. Install
17
63
 
18
- ```jsonc
64
+ ```json
19
65
  // package.json
20
66
  "dependencies": {
21
67
  "@nxgt/material": "…"
@@ -37,10 +83,10 @@ this order:
37
83
 
38
84
  - **`@import "@nxgt/material/styles.css"`** brings in the design tokens: the
39
85
  `oklch` colour scale, `--radius`, the `dark` custom variant, the font stacks.
40
- It is `lib/styles.css` shipped verbatim rather than compiled, because Tailwind
41
- v4 wants the source `@theme` layer to run through *your* Tailwind build.
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.
42
88
  - **`@source "../node_modules/@nxgt/material/"`** is not optional and its
43
- absence does not error. Tailwind v4 generates only the utilities it can see in
89
+ absence does not error. Tailwind v4 generates only the classes it can see in
44
90
  the files it scans, and by default it does not scan inside `node_modules`.
45
91
  Without this line, every utility class used *inside* a `@nxgt/material`
46
92
  component is absent from the output and the components render unstyled — a
@@ -70,7 +116,6 @@ public/assets/icons/sprites/<style>.svg
70
116
  covers using the icons in your own products, not redistributing them inside a
71
117
  library. A published `@nxgt/material` tarball that contained them would be
72
118
  redistribution regardless of whether this repository is private.
73
-
74
119
  `files` therefore carries an explicit `"!dist/assets"`, so a stale local build
75
120
  cannot leak them back into a tarball by accident.
76
121
 
@@ -83,11 +128,135 @@ Each consuming app therefore commits its own sprites into
83
128
  `public/assets/icons/sprites/`, taken from its own Font Awesome Pro kit. The styles in use across the parc are
84
129
  the 37 sheets covering `regular`, `light`, `thin`, `solid`, `duotone`,
85
130
  `sharp-*`, `brands` and the expressive families (`chisel`, `etch`, `jelly`,
86
- `notdog`, `slab-press`, …). An icon whose sheet is missing renders as nothing —
131
+ `slickdog`, `slab-press`, …). An icon whose sheet is missing renders as nothing —
87
132
  no error, no fallback.
88
133
 
89
134
  ---
90
135
 
136
+ ## i18n
137
+
138
+ `@nxgt/material` owns the i18n **mechanism** (`@nxgt/material/i18n`); consuming
139
+ apps own their resource **content**.
140
+
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.
146
+
147
+ ```ts
148
+ // app/i18n/index.ts
149
+ import {
150
+ createTranslator,
151
+ createTypedTranslation,
152
+ mergeResources,
153
+ resources as materialResources,
154
+ type FlatObject,
155
+ } from '@nxgt/material/i18n';
156
+ import { resources as appResources } from './resources';
157
+
158
+ export const resources = mergeResources(materialResources, appResources);
159
+ export type LocaleKey = keyof FlatObject<typeof resources.en, string>;
160
+
161
+ export const translate = createTranslator<LocaleKey>(resources);
162
+ export const useTranslation = createTypedTranslation<LocaleKey>(translate);
163
+ ```
164
+
165
+ ```tsx
166
+ // app root — mount once, above everything that translates
167
+ <I18nProvider defaultLanguage="en">
168
+ <App />
169
+ </I18nProvider>
170
+ ```
171
+
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.
248
+
249
+ ```tsx
250
+ import { TextFormField, Button } from '@nxgt/material/components';
251
+ import { useForm } from 'react-hook-form';
252
+
253
+ const form = useForm({ defaultValues: { email: '' } });
254
+
255
+ <TextFormField control={form.control} name="email" label="Email" />
256
+ ```
257
+
258
+ ---
259
+
91
260
  ## Working on this package
92
261
 
93
262
  ```sh
@@ -97,11 +266,10 @@ bun run storybook
97
266
  bun run test
98
267
  ```
99
268
 
100
- `bun run build` is what consumers read. There is **no `postinstall` build**: the
101
- package publishes `dist/` and not `lib/`, so a build triggered on a consumer's
102
- machine would have no sources to compile and would fail the install outright
103
- under npm. Build here, then publish.
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.
104
272
 
105
- New reusable components arrive here rather than in an app see the
106
- "Reusable UI components belong in `@nxgt/material`" section of the consuming
107
- repositories' `AGENTS.md`.
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.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -62,8 +62,11 @@
62
62
  "postbuild": "cp lib/styles.css dist/",
63
63
  "storybook": "storybook dev -p 6006",
64
64
  "build-storybook": "storybook build",
65
- "publish": "bun publish --access=public",
66
65
  "test": "vitest run",
66
+ "changeset": "changeset",
67
+ "changeset:version": "changeset version",
68
+ "changeset:publish": "bun run build && bun run scripts/publish.ts",
69
+ "changeset:status": "changeset status --since=origin/develop",
67
70
  "shadcn:add": "bunx --bun shadcn@latest add",
68
71
  "tsc": "tsc -b",
69
72
  "check": "biome check --write",
@@ -151,6 +154,8 @@
151
154
  },
152
155
  "devDependencies": {
153
156
  "@biomejs/biome": "2.5.5",
157
+ "@changesets/changelog-github": "^1.0.1",
158
+ "@changesets/cli": "^3.0.1",
154
159
  "@chromatic-com/storybook": "^5.3.1",
155
160
  "@storybook/addon-a11y": "10.6.0",
156
161
  "@storybook/addon-docs": "10.6.0",