@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.
- package/README.md +181 -13
- 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
|
-
```
|
|
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
|
|
41
|
-
v4
|
|
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
|
|
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
|
-
`
|
|
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.
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
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",
|