@navecss/core 0.2.0 → 0.4.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/ATOMS.md +1 -1
- package/CONSUMER-ATOMS.md +41 -8
- package/LICENSE +36 -0
- package/README.md +611 -46
- package/dist/atom-names-2OreuBx9.d.ts +27 -0
- package/dist/atomic.css +3 -1
- package/dist/atoms.d.ts +10 -14
- package/dist/atoms.js +2 -1
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +453 -0
- package/dist/check.d.ts +31 -0
- package/dist/check.js +11 -0
- package/dist/chunk-2A7IUZBH.js +31 -0
- package/dist/{chunk-BVON7XKC.js → chunk-CUL5PLPI.js} +8 -6
- package/dist/chunk-E5F6YF7A.js +2428 -0
- package/dist/chunk-GZL2VW7C.js +57 -0
- package/dist/chunk-LADFXRWQ.js +1089 -0
- package/dist/chunk-LQSGQ2OA.js +94 -0
- package/dist/chunk-NFS2REPQ.js +747 -0
- package/dist/chunk-NKUZNRF4.js +42 -0
- package/dist/chunk-SYNQJNVB.js +302 -0
- package/dist/chunk-UANF3FGO.js +115 -0
- package/dist/chunk-XQQCDUV6.js +152 -0
- package/dist/collect.d.ts +279 -0
- package/dist/collect.js +4 -0
- package/dist/cx.d.ts +23 -4
- package/dist/cx.js +19 -1
- package/dist/lightningcss.d.ts +51 -0
- package/dist/lightningcss.js +44 -0
- package/dist/no-tokens.css +7 -7
- package/dist/postcss.cjs +1 -0
- package/dist/postcss.d.cts +2 -0
- package/dist/postcss.d.ts +30 -15
- package/dist/postcss.js +237 -243
- package/dist/standalone.css +1139 -0
- package/dist/vite-options-D1-hGA9L.d.ts +86 -0
- package/dist/vite.d.ts +368 -0
- package/dist/vite.js +3318 -0
- package/package.json +55 -17
- package/skills/navecss/SKILL.md +7 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @navecss/core
|
|
2
2
|
|
|
3
|
-
> Layer architecture, reset, atomic utilities, and PostCSS
|
|
3
|
+
> Layer architecture, reset, atomic utilities, the Vite and PostCSS plugins, a Lightning CSS adapter and the `navecss-core` command for Nave.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -15,23 +15,40 @@ pnpm add @navecss/core
|
|
|
15
15
|
most of them are also derived with relative colour syntax. An older browser
|
|
16
16
|
drops whichever of them it does not support, without an error: surfaces and
|
|
17
17
|
text lose their colours. Your CSS build has to target the same floor, and
|
|
18
|
-
[
|
|
18
|
+
[Vite plugin setup](#vite-plugin-setup) has the line for Vite.
|
|
19
19
|
[Why this floor](https://github.com/navecss/navecss/blob/main/docs/04-adr/0005-browser-floor.md).
|
|
20
20
|
- **A resolver that reads `exports` maps.** Every entry point, the stylesheet
|
|
21
21
|
included, is reachable only through the package's `exports` map: there is no
|
|
22
22
|
`main` field to fall back on.
|
|
23
|
-
- **ES modules.** `@navecss/core/cx`, `@navecss/core/atoms
|
|
24
|
-
`@navecss/core/
|
|
23
|
+
- **ES modules.** `@navecss/core/cx`, `@navecss/core/atoms`,
|
|
24
|
+
`@navecss/core/vite`, `@navecss/core/lightningcss`, `@navecss/core/check` and `@navecss/core/collect` load through `import` only. A `require()` of any of
|
|
25
25
|
them fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`, which reads as though the
|
|
26
26
|
entry point did not exist. In TypeScript, set `moduleResolution` to `bundler`,
|
|
27
27
|
`node16` or `nodenext`; under `node10` their types are not found. A CommonJS
|
|
28
28
|
file type-checks against them and then fails when it runs, so import them from
|
|
29
|
-
an ES module.
|
|
29
|
+
an ES module. `@navecss/core/postcss` also loads through `require()` (Node
|
|
30
|
+
loads the ES module for you, which the Node version below includes), so a
|
|
31
|
+
CommonJS `postcss.config.js` can use it.
|
|
32
|
+
For TypeScript to check a `require()` of it, use `module` `nodenext`
|
|
33
|
+
(TypeScript 5.8 or later) or `node20` (5.9 or later) with `moduleResolution`
|
|
34
|
+
unset or `nodenext`, or `module` `preserve` with `moduleResolution` `bundler`,
|
|
35
|
+
or turn on `skipLibCheck`. Under `module` `node16`, or `nodenext` before 5.8,
|
|
36
|
+
TypeScript describes a Node that cannot `require()` an ES module and reports
|
|
37
|
+
an error in the package's types.
|
|
30
38
|
- **Node 22.18 or later.**
|
|
31
|
-
- **
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
- **A build step that resolves `@nave`: the Vite plugin, PostCSS 8, the Lightning
|
|
40
|
+
CSS adapter or the command.** On
|
|
41
|
+
Vite, `@navecss/core/vite` needs nothing installed beside Vite, and
|
|
42
|
+
[Vite plugin setup](#vite-plugin-setup) is its whole setup. On any other
|
|
43
|
+
pipeline that runs PostCSS plugins, use the PostCSS plugin: `postcss` is an
|
|
44
|
+
optional peer dependency, so nothing installs it for you. Add it yourself if
|
|
45
|
+
you use that route; without it, importing `@navecss/core/postcss` fails with
|
|
46
|
+
`Cannot find package 'postcss'`. [PostCSS plugin setup](#postcss-plugin-setup)
|
|
47
|
+
has it. A host that runs Lightning CSS itself uses
|
|
48
|
+
[the Lightning CSS adapter](#lightning-css-adapter-setup), which needs nothing
|
|
49
|
+
installed beside Lightning CSS, and a project with no bundler uses
|
|
50
|
+
[`navecss-core expand`](#navecss-core-expand). The stylesheets and `cx()` need
|
|
51
|
+
none of them.
|
|
35
52
|
|
|
36
53
|
## Setup
|
|
37
54
|
|
|
@@ -74,7 +91,7 @@ Every built-in atom, with the declarations it applies, is listed in
|
|
|
74
91
|
|
|
75
92
|
### Primary — @nave directives
|
|
76
93
|
|
|
77
|
-
Requires the PostCSS plugin. Everything stays in CSS files.
|
|
94
|
+
Requires the Vite plugin or the PostCSS plugin. Everything stays in CSS files.
|
|
78
95
|
|
|
79
96
|
```css
|
|
80
97
|
/* button.module.css */
|
|
@@ -97,7 +114,7 @@ floor under [Requirements](#requirements).
|
|
|
97
114
|
|
|
98
115
|
### Escape hatch — cx() utility
|
|
99
116
|
|
|
100
|
-
No
|
|
117
|
+
No build plugin required. Atom names move to JSX — autocompleted, and type-checked.
|
|
101
118
|
|
|
102
119
|
```tsx
|
|
103
120
|
import { cx } from '@navecss/core/cx'
|
|
@@ -106,7 +123,10 @@ import styles from './button.module.css'
|
|
|
106
123
|
;<button className={`${cx('interactive', 'focusRing', 'transition')} ${styles.root}`} />
|
|
107
124
|
```
|
|
108
125
|
|
|
109
|
-
Use this if you cannot or do not want to add a PostCSS plugin to your build.
|
|
126
|
+
Use this if you cannot or do not want to add a Vite or PostCSS plugin to your build.
|
|
127
|
+
With the Vite plugin, the build reads every `cx()` call to choose which atoms ship, and fails on one
|
|
128
|
+
whose atoms it cannot read, so a name chosen at run time goes through `cx.dynamic()`: see
|
|
129
|
+
[Which atoms the build ships](#which-atoms-the-build-ships).
|
|
110
130
|
|
|
111
131
|
A `cx()` class is a default, not an override: it sits in the `atomic` layer,
|
|
112
132
|
below your component CSS, so your own rule wins wherever the two set the same
|
|
@@ -136,8 +156,9 @@ What that buys you, and what it does not:
|
|
|
136
156
|
_emits_ is `nave-sr-only`; neither spelling is the key), and any class that is
|
|
137
157
|
not a Nave atom. **TypeScript only:
|
|
138
158
|
a JavaScript consumer gets none of it.** For a build-level check that does not
|
|
139
|
-
depend on types, use the
|
|
140
|
-
|
|
159
|
+
depend on types, use the Vite plugin, which by default fails the build on an
|
|
160
|
+
unknown atom in a `cx()` call, or the `@nave` directive, whose build steps all
|
|
161
|
+
fail on an unknown atom by default.
|
|
141
162
|
- **`cx.raw()` cannot shadow one of your classes.** It never consults the atom
|
|
142
163
|
map, so `cx.raw('container')` is the literal `container`. Inside `cx()` an
|
|
143
164
|
atom name still resolves to that atom's global class — `cx('container')` is
|
|
@@ -155,7 +176,7 @@ What that buys you, and what it does not:
|
|
|
155
176
|
|
|
156
177
|
### Tokens only
|
|
157
178
|
|
|
158
|
-
For library authors or teams building on top of
|
|
179
|
+
For library authors or teams building on top of Radix UI or another headless library. On Base UI, [`@navecss/base-ui`](https://github.com/navecss/navecss/tree/main/packages/base-ui#readme) styles its components with these same tokens.
|
|
159
180
|
CSS custom properties are always available — no atomic layer required.
|
|
160
181
|
|
|
161
182
|
```css
|
|
@@ -170,27 +191,382 @@ Every custom property Nave declares is in the stylesheet you already installed,
|
|
|
170
191
|
|
|
171
192
|
---
|
|
172
193
|
|
|
173
|
-
##
|
|
194
|
+
## Setting up `@nave`
|
|
195
|
+
|
|
196
|
+
On Vite, use the Vite plugin: it is the one route this package teaches for Vite. On
|
|
197
|
+
Next.js, webpack, or any other pipeline that runs PostCSS plugins, use the PostCSS plugin. A host
|
|
198
|
+
that runs Lightning CSS directly uses [the Lightning CSS adapter](#lightning-css-adapter-setup),
|
|
199
|
+
and a project with no bundler uses [`navecss-core expand`](#navecss-core-expand).
|
|
200
|
+
|
|
201
|
+
### Vite plugin setup
|
|
174
202
|
|
|
175
203
|
```ts
|
|
176
204
|
// vite.config.ts
|
|
177
205
|
import { defineConfig } from 'vite'
|
|
178
|
-
import { navePlugin } from '@navecss/core/
|
|
206
|
+
import { navePlugin } from '@navecss/core/vite'
|
|
179
207
|
|
|
180
208
|
export default defineConfig({
|
|
181
|
-
// your existing options
|
|
182
|
-
|
|
209
|
+
// your existing options stay as they are; add navePlugin() to your existing plugins
|
|
210
|
+
plugins: [navePlugin()],
|
|
183
211
|
build: { cssTarget: ['chrome125', 'edge125', 'firefox128', 'safari18', 'ios18'] },
|
|
184
212
|
})
|
|
185
213
|
```
|
|
186
214
|
|
|
215
|
+
`navePlugin()` takes the same `extend` and `onUnknown` options as the PostCSS plugin
|
|
216
|
+
([Options](#options)) and returns two Vite plugins in an array, which `plugins` takes as one entry.
|
|
217
|
+
It has no dependency and no peer. It expands `@nave` after Vite's own CSS step has run, so a stylesheet reached only
|
|
218
|
+
through `@import`, a Sass file (including a `@mixin` that holds a directive), a CSS Module, an
|
|
219
|
+
`?inline` or `?url` import, a Vue or Svelte style block and a `.css` inside `node_modules` are all
|
|
220
|
+
read as the CSS they compile to, in `vite build` and in the dev server, under either
|
|
221
|
+
`css.transformer`. A `?raw` import returns the file's text as it is. Astro is not covered: it was
|
|
222
|
+
not measured at this release, and no fixture of it runs.
|
|
223
|
+
|
|
224
|
+
**It also chooses which atoms ship.** A `cx()` call in your code whose atoms the build cannot read,
|
|
225
|
+
such as `cx(variant)` with a prop, is an error in the dev server and fails `vite build`.
|
|
226
|
+
By default only the atoms Nave can read a use for are served, in the dev server as in the build. A `nave-*` class neither can see, such as one in a server template or in CMS text, or one a package listed in `keepFor` applies at run time, has no rule unless its atom is listed in `keep` or in a `keepFor` entry. Run your checks, keyboard, screen-reader and automated accessibility checks included, against a production build as well as in dev, because the two can still differ; [Which atoms the build ships](#which-atoms-the-build-ships) says what the build reads and where dev differs.
|
|
227
|
+
|
|
187
228
|
`build.cssTarget` is the browser floor, in Vite's terms. Vite's default targets
|
|
188
229
|
older browsers, and building for them gains you nothing, because the output
|
|
189
230
|
still needs the floor. It does cost you something: Vite rewrites `light-dark()`
|
|
190
231
|
in Nave's colours into an emulation that a `color-scheme` set from script, or on
|
|
191
|
-
part of the page, does not switch.
|
|
192
|
-
|
|
193
|
-
|
|
232
|
+
part of the page, does not switch. The plugin sets no floor of its own, because the floor is a
|
|
233
|
+
requirement of Nave's stylesheets, not of the directive: a project that never writes `@nave`
|
|
234
|
+
needs the same key.
|
|
235
|
+
|
|
236
|
+
**Migrating from the PostCSS plugin.** Move `navePlugin()` from `css.postcss` or
|
|
237
|
+
`postcss.config.js` to `plugins`, importing it from `@navecss/core/vite`. Leaving both is
|
|
238
|
+
harmless, because whichever pass runs second finds no directive left. Moving also changes which
|
|
239
|
+
atoms ship: the PostCSS plugin ships every atom, and this one chooses them from the uses it can
|
|
240
|
+
read, in the dev server too, so after the move a `cx()` call it cannot read fails the build, and a
|
|
241
|
+
Nave class it cannot see has no rule unless its atom is listed in `keep`.
|
|
242
|
+
|
|
243
|
+
**Under `css.transformer: 'lightningcss'`** the plugin expands exactly as it does under the default
|
|
244
|
+
transformer once both floor keys below are set. At Vite's default targets, which sit below the
|
|
245
|
+
floor, Lightning CSS lowers CSS nesting before any plugin runs, and two things change.
|
|
246
|
+
A directive written after a declaration that follows a nested rule can be moved ahead of that
|
|
247
|
+
declaration: in `.a { &:hover { color: red; } display: grid; @nave block; }`, `grid` wins with no
|
|
248
|
+
message, where the default transformer makes `block` win. A directive in a group rule nested in a
|
|
249
|
+
style rule (`.a { @media (...) { @nave flex; } }`) still fails the build, but the message no longer
|
|
250
|
+
names the `& { @nave ...; }` workaround, and its position is in the CSS as Lightning CSS rewrote
|
|
251
|
+
it, not in your file. The plugin also drops one message and only that one: Lightning CSS's
|
|
252
|
+
`Unknown at rule: @nave` warning, which Vite prints for every directive before any plugin runs.
|
|
253
|
+
It does so by wrapping the logger Vite resolved. Stated cost: that is a logger Vite owns, which is
|
|
254
|
+
your own object when you pass a `customLogger`. The filter matches case-insensitively, and an
|
|
255
|
+
escaped spelling (`@n\61ve`) may still warn. It fails open: if Vite changes its prefix or its
|
|
256
|
+
logging path the warnings come back and nothing is lost, and the end-of-build scan below catches
|
|
257
|
+
any directive that did survive. A warning about any other unknown at-rule is still printed. Set both
|
|
258
|
+
floor keys:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// vite.config.ts, with css.transformer: 'lightningcss'
|
|
262
|
+
import { defineConfig } from 'vite'
|
|
263
|
+
import { navePlugin } from '@navecss/core/vite'
|
|
264
|
+
|
|
265
|
+
export default defineConfig({
|
|
266
|
+
plugins: [navePlugin()],
|
|
267
|
+
build: { cssTarget: ['chrome125', 'edge125', 'firefox128', 'safari18', 'ios18'] },
|
|
268
|
+
css: {
|
|
269
|
+
transformer: 'lightningcss',
|
|
270
|
+
lightningcss: {
|
|
271
|
+
targets: {
|
|
272
|
+
chrome: 125 << 16,
|
|
273
|
+
edge: 125 << 16,
|
|
274
|
+
firefox: 128 << 16,
|
|
275
|
+
safari: 18 << 16,
|
|
276
|
+
ios_saf: 18 << 16,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
})
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`build.cssTarget` alone still rewrites `light-dark()` under that transformer, so the two keys go
|
|
284
|
+
together.
|
|
285
|
+
|
|
286
|
+
**The end of every build is checked.** The plugin reads the CSS files the build wrote and fails the
|
|
287
|
+
build if a `@nave` directive is left in one, with the same lines
|
|
288
|
+
[`navecss-core check`](#navecss-core-check) prints, under the plugin name `nave`. There is no
|
|
289
|
+
option to turn it off: a directive in shipped CSS is never wanted, and `onUnknown: 'ignore'`
|
|
290
|
+
does not change it. Four places are not covered. The dev server serves no bundle, so there is no
|
|
291
|
+
scan in dev. CSS that ends inside a JavaScript string (`?inline`) is not a CSS file the build wrote,
|
|
292
|
+
so it is not scanned. Files under Vite's `public/` directory are copied as they are and are not
|
|
293
|
+
scanned. A CSS file another plugin adds after the scan has run is not scanned either: the scan runs
|
|
294
|
+
in a post-ordered `generateBundle`, so it misses a file emitted from a post-ordered
|
|
295
|
+
`generateBundle` in a plugin listed after `navePlugin()`, and anything a plugin writes in
|
|
296
|
+
`writeBundle`. List `navePlugin()` after plugins that emit CSS files; a file written in
|
|
297
|
+
`writeBundle` stays outside the scan wherever the plugin is listed.
|
|
298
|
+
|
|
299
|
+
**A change to your atoms re-runs the stylesheets that use them.** Pass `extend` as a path to a
|
|
300
|
+
module (resolved from Vite's project root, whose default export is the atoms object), and the
|
|
301
|
+
plugin declares the file to Vite: edit it, and the dev server serves the new value with no restart,
|
|
302
|
+
and `vite build --watch` rebuilds. An object written inline in your config is not a file Vite can
|
|
303
|
+
watch.
|
|
304
|
+
|
|
305
|
+
The supported Vite range is measured, not declared: the fixtures run on Vite 8.2.1 and on the
|
|
306
|
+
newest 8.x at the time of each release.
|
|
307
|
+
|
|
308
|
+
#### Which atoms the build ships
|
|
309
|
+
|
|
310
|
+
The Vite plugin also chooses which of Nave's atoms reach your stylesheet. It reads the `cx()` calls
|
|
311
|
+
in the code the build compiles, dependencies included, and ships the rules for the atoms those calls
|
|
312
|
+
name, for the atom of each Nave class written as text (`nave-grid` in `index.html` or in a string in
|
|
313
|
+
your code, say), and for the atoms you list in `keep` and `keepFor`; `srOnlyFocusable` also ships
|
|
314
|
+
whenever `srOnly` does. The dev server serves the same atoms, from the code it has read. This is
|
|
315
|
+
what `navePlugin()` does with no options. On the CSS-first path, where `@nave` copies an atom's
|
|
316
|
+
declarations into your own rule, a project that never calls `cx()` and writes no Nave class ships no
|
|
317
|
+
rule from the atom layer.
|
|
318
|
+
|
|
319
|
+
In a build, the plugin also turns each `cx()` call it read into the class string the call returns, so
|
|
320
|
+
the map from atom names to classes does not ship: `cx('flex', 'grid')` becomes `"nave-flex nave-grid"`,
|
|
321
|
+
and `cx('flex', on && 'grid')` becomes `["nave-flex", on && "nave-grid"].filter(Boolean).join(" ")`,
|
|
322
|
+
with each argument evaluated once, in the order you wrote it. A direct `cx.raw()` call becomes the
|
|
323
|
+
same join, and a direct `cx.dynamic()` call a lookup in a map of the atoms in `keep` and `keepFor`.
|
|
324
|
+
The dev server keeps the run-time functions, which return the same strings. A bundle holds neither
|
|
325
|
+
the name map nor the atoms' declarations when every use of `cx` in it is one of those direct calls
|
|
326
|
+
and no module that imports `cx` calls `eval` directly. Two forms keep the map, since the build
|
|
327
|
+
cannot follow the alias: `const r = cx.raw` and `const { raw } = cx`. So does a package listed in
|
|
328
|
+
`keepFor` whose calls the build cannot read, because those calls stay calls to the run-time `cx()`.
|
|
329
|
+
A module that calls `eval` directly keeps its import of `cx`, and with it the map, whatever text it
|
|
330
|
+
runs (`eval('1')` too), because that text can read the module's names and the build cannot read the
|
|
331
|
+
text. An indirect call, `(0, eval)(text)` or `globalThis.eval(text)`, runs its text in the global
|
|
332
|
+
scope, where `cx` is not visible, and does not keep the import.
|
|
333
|
+
|
|
334
|
+
So that nothing is dropped on a guess, a `cx()` call whose atoms the build cannot read fails the
|
|
335
|
+
build. `vite build` reads every module first and fails once, at the end, with a report that lists
|
|
336
|
+
each such use with its file, and its line and column when the source maps lead back to them, then
|
|
337
|
+
says how to fix them:
|
|
338
|
+
|
|
339
|
+
```text
|
|
340
|
+
2 problems in 2 files: the build cannot tell which atoms these apply, and it ships only the atoms it can read.
|
|
341
|
+
src/Card.tsx:7:26: cx(variant): the argument is not a literal atom name.
|
|
342
|
+
src/List.tsx:12:26: cx is passed to map() as a value instead of being called.
|
|
343
|
+
Name the atoms at the call: a string literal, a const in the same file, or a condition choosing between them, as in cx(on ? 'flex' : 'grid').
|
|
344
|
+
To choose by a value, call cx() once per case and pick between the results: ({ row: cx('flex'), grid: cx('grid') })[variant].
|
|
345
|
+
If the name comes from data the build never sees, write cx.dynamic(name) and list every atom it can take in keep in navePlugin().
|
|
346
|
+
Call cx() where the classes are applied, with the atom names as its arguments: the build reads calls, not a cx that is assigned, passed or spread.
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
The dev server reports the problems in your own code one module at a time. It reads your
|
|
350
|
+
dependencies' `cx()` calls as well, so their atoms are served in dev as in the build, but their
|
|
351
|
+
problems show in `vite build` only: in dev, Vite pre-bundles dependencies into files that can mix
|
|
352
|
+
several packages, so a problem there cannot be traced to one package. To read those calls, the
|
|
353
|
+
plugin keeps `@navecss/core` itself out of Vite's dependency pre-bundling (`optimizeDeps.exclude`),
|
|
354
|
+
so in dev the browser loads it as a few separate modules.
|
|
355
|
+
|
|
356
|
+
A stylesheet you import with `?inline` or `?raw` becomes text in your JavaScript, which the plugin
|
|
357
|
+
does not filter: if it holds Nave's atom layer, every atom in that layer ships, and `vite build`
|
|
358
|
+
warns, naming the file. Import it without the query to have the layer filtered.
|
|
359
|
+
|
|
360
|
+
**What the build can read.** An argument whose possible values the build can list: a string
|
|
361
|
+
literal; `null`, `undefined`, `false` or `''`; a condition choosing between those
|
|
362
|
+
(`on && 'flex'`, `on ? 'flex' : 'grid'`, `a || b`, `a ?? b`); or a variable in the same file
|
|
363
|
+
bound to one of them and never assigned again, such as a `const`. A prop, a parameter, a variable
|
|
364
|
+
that is assigned again, an import from another file, an array element, an object property and a
|
|
365
|
+
spread are not, and `cx` itself must be called, never passed on (`list.map(cx)` fails). Types do
|
|
366
|
+
not count: a value typed `AtomName` is still a value the build cannot list, and a cast is gone
|
|
367
|
+
before the build reads the call. A string holding two names (`cx('flex gap')`) fails too: pass
|
|
368
|
+
each name as its own argument.
|
|
369
|
+
|
|
370
|
+
To choose by a value, such as a `variant` prop, call `cx()` once per case and pick between the
|
|
371
|
+
results:
|
|
372
|
+
|
|
373
|
+
```tsx
|
|
374
|
+
import { cx } from '@navecss/core/cx'
|
|
375
|
+
|
|
376
|
+
const layouts = { row: cx('flex'), grid: cx('grid') }
|
|
377
|
+
|
|
378
|
+
export function List({ variant }: { variant: keyof typeof layouts }) {
|
|
379
|
+
return <ul className={layouts[variant]} />
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Names chosen at run time.** When a name is known only at run time, because it comes from data,
|
|
384
|
+
pass it to `cx.dynamic()` and list every atom it can take in `keep`:
|
|
385
|
+
|
|
386
|
+
```tsx
|
|
387
|
+
// vite.config.ts
|
|
388
|
+
import { defineConfig } from 'vite'
|
|
389
|
+
import { navePlugin } from '@navecss/core/vite'
|
|
390
|
+
|
|
391
|
+
export default defineConfig({
|
|
392
|
+
plugins: [navePlugin({ keep: ['flex', 'grid'] })],
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// Block.tsx, whose layout comes from your CMS
|
|
396
|
+
import { cx, type AtomName } from '@navecss/core/cx'
|
|
397
|
+
|
|
398
|
+
export const Block = ({ layout }: { layout: AtomName }) => <div className={cx.dynamic(layout)} />
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`cx.dynamic()` takes one name. A name listed in `keep` (or in a `keepFor` list) gets its atom's
|
|
402
|
+
class, in dev and in the build; any other value gets none (`''`), and in dev the console names any
|
|
403
|
+
name it gave no class and says why. Every atom in `keep` ships, used or not. A `cx.dynamic()` call
|
|
404
|
+
in your code with `keep` empty is an error, in the dev server as in `vite build`, since every such
|
|
405
|
+
call would apply nothing. The plugin hands `cx.dynamic()` the atoms of `keep` and `keepFor` as static data, and
|
|
406
|
+
adds `@navecss/core` to every server environment's `resolve.noExternal`, so `cx.dynamic()` in a
|
|
407
|
+
server build reads them too.
|
|
408
|
+
|
|
409
|
+
**Code you import.** A module that re-exports `cx`, such as a design system's `index.ts` holding
|
|
410
|
+
`export { cx } from '@navecss/core/cx'`, is a build error until you list it in `cxModules`. The
|
|
411
|
+
build then reads a file that imports `cx` from it as if that file imported `cx` from
|
|
412
|
+
`@navecss/core/cx`. It follows a listed module one step: the listed module must re-export Nave's
|
|
413
|
+
`cx` from `@navecss/core/cx` itself, under the name `cx`. Write an entry as a path from Vite's
|
|
414
|
+
`root`, an alias your config defines, or a package name, which is how you list a published package
|
|
415
|
+
that re-exports `cx`. An import of a listed module through a spelling the build does not recognise
|
|
416
|
+
fails `vite build`, which names the file and says what to write instead.
|
|
417
|
+
|
|
418
|
+
```ts
|
|
419
|
+
// vite.config.ts
|
|
420
|
+
import { defineConfig } from 'vite'
|
|
421
|
+
import { navePlugin } from '@navecss/core/vite'
|
|
422
|
+
|
|
423
|
+
export default defineConfig({
|
|
424
|
+
plugins: [navePlugin({ cxModules: ['./src/ui/index.ts'] })],
|
|
425
|
+
})
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
`@navecss/eslint-plugin` has a `cxModules` setting with the same name and the same entries, so one
|
|
429
|
+
list can serve both, with two differences. The lint reads a relative entry from the directory it
|
|
430
|
+
runs in, and the build from Vite's `root`: where those differ, as in a monorepo, write the entries
|
|
431
|
+
as aliases or package names. And the lint resolves an import as Node's `require()` does, where the
|
|
432
|
+
build uses Vite's resolver: the build recognises spellings the lint cannot resolve, such as an alias
|
|
433
|
+
set only in your Vite config, and where the lint cannot resolve a `#` import it matches a `#` entry
|
|
434
|
+
written the same way, which the build never does.
|
|
435
|
+
|
|
436
|
+
A dependency whose `cx()` calls the build cannot read fails the build with the package named,
|
|
437
|
+
because its code is not yours to change. List the package in `keepFor`, under its name, with the
|
|
438
|
+
atoms its calls can produce, which its documentation should give you:
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
// vite.config.ts
|
|
442
|
+
import { defineConfig } from 'vite'
|
|
443
|
+
import { navePlugin } from '@navecss/core/vite'
|
|
444
|
+
|
|
445
|
+
export default defineConfig({
|
|
446
|
+
plugins: [navePlugin({ keepFor: { '@acme/ui': ['flex', 'inlineFlex', 'itemsCenter'] } })],
|
|
447
|
+
})
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Those atoms ship as if they were in `keep`, beside the atoms of the package's calls the build does
|
|
451
|
+
read. The calls it cannot read are not changed: one that produces any other atom still puts that
|
|
452
|
+
atom's class on the element, and the class has no rule, in dev as in the build, unless the atom
|
|
453
|
+
ships for another reason. An empty list, `keepFor: { '@acme/ui': [] }`, says that the calls the
|
|
454
|
+
build cannot read produce no atom. A `cx.dynamic()` call in the package needs its atoms in the
|
|
455
|
+
list: with an empty list, or none, it fails the build. Each client build records, in the
|
|
456
|
+
`nave-used-atoms.json` file described below, the atoms it read from each listed package's calls,
|
|
457
|
+
where the calls it could not read are, and any `keepFor` key that names no package the build read.
|
|
458
|
+
|
|
459
|
+
`keepFor` stands in for the package's own calls, never for a call your code makes, so three problems
|
|
460
|
+
in a listed package still fail the build. A module of the package that re-exports `cx` under the
|
|
461
|
+
name `cx` still fails when a file of yours imports `cx` from that module itself, in an `import` or
|
|
462
|
+
`export ... from` statement, or a dependency that does more than re-export it imports it from there,
|
|
463
|
+
and the report gives the `cxModules` entry that lets the build read those calls. Through a module
|
|
464
|
+
that only re-exports it, such as the package's own entry file or another package, a listed module
|
|
465
|
+
would be two steps away, so there the report names each file of yours that takes it: import `cx`
|
|
466
|
+
from `@navecss/core/cx` there instead. A module that gives `cx` out under another name, as a default
|
|
467
|
+
or inside a namespace, still fails when your code takes that export, and the report names each file
|
|
468
|
+
of yours that does: import `cx` from `@navecss/core/cx` there instead. And a module of the package
|
|
469
|
+
that you list in `cxModules` but that does not re-export Nave's `cx` under that name still fails:
|
|
470
|
+
remove it from `cxModules`.
|
|
471
|
+
|
|
472
|
+
A package inside your own workspace is your code, not a dependency: Vite resolves it by its real
|
|
473
|
+
path, outside `node_modules`, so its calls are reported as yours, in the dev server too, and
|
|
474
|
+
`keepFor` does not cover it.
|
|
475
|
+
|
|
476
|
+
**What the build cannot see.**
|
|
477
|
+
|
|
478
|
+
A class the build does not read (in a server template or CMS text, assembled where the build cannot see it, or applied at run time by a package listed in `keepFor`) has no rule in the production stylesheet unless its atom is listed in `keep` or in a `keepFor` entry (`nave-sr-only-focusable` also has its rule whenever `nave-sr-only` has one). An element carrying such a class renders as if the class were absent: a `nave-sr-only` or `nave-sr-only-focusable` element is shown, a `nave-hidden` or `nave-hide-*` element is displayed, and a `nave-focus-ring` element falls back to the browser's default focus indicator. A `nave-hidden` or `nave-hide-*` element that other styles also keep out of sight (moved off-screen, made transparent or clipped) stays out of sight but can take keyboard focus and is exposed to screen readers. The dev server serves the atoms the build would ship, chosen from the code it has read, so a class the build does not read has no rule in dev either. The dev server can still keep a rule the build drops, for code it read and the build does not (such as a module the page stopped importing while the dev server ran), and lack one the build keeps, for a class rendered only on a server that runs outside the dev server's process, so a page can still look and behave differently in dev than in the build. Before you ship, run your checks, keyboard, screen-reader and automated accessibility checks included, against the production build: `vite build` then `vite preview` for a client-rendered app, or your production server for a server-rendered one.
|
|
479
|
+
|
|
480
|
+
A class your code takes at run time from `@navecss/core/atoms` (`atomClassMap[variant]`,
|
|
481
|
+
`toClassName('grid')`) is one the build does not read: it neither reads nor refuses it, so list its
|
|
482
|
+
atom in `keep`.
|
|
483
|
+
|
|
484
|
+
If your pages are rendered outside the build, by a backend that serves its own templates and uses
|
|
485
|
+
Vite for its assets only, the build reads none of their classes. While `keep` is empty, the plugin
|
|
486
|
+
warns when it has read no HTML page, no server render and no use of Nave at all: no `cx()` call, no
|
|
487
|
+
Nave class and no `@nave` directive. A backend whose scripts call `cx()`, or whose CSS uses `@nave`,
|
|
488
|
+
gets no warning, and the classes in its templates have no rule all the same. List the atoms your
|
|
489
|
+
templates use in `keep`, or, if they are many, ship every atom (below).
|
|
490
|
+
|
|
491
|
+
The PostCSS plugin and the Lightning CSS adapter read no `cx()` call and leave every atom in the
|
|
492
|
+
layer, on Next.js and webpack alike; so does a page with no bundler, which links the standalone
|
|
493
|
+
stylesheet.
|
|
494
|
+
|
|
495
|
+
A server build leaves packages in `node_modules` external by default, so it does not read their
|
|
496
|
+
code. For such a package that depends on `@navecss/core`, the build warns, naming it, unless it is
|
|
497
|
+
listed in `keepFor`. A listed package is still checked where a server file of yours imports from it:
|
|
498
|
+
taking a `cx` export the client build found in that module fails the build (with two commands, the
|
|
499
|
+
next client build) with the report the client would give, and where the build cannot tell what your
|
|
500
|
+
file takes, because no environment read the file the server loads or your file reaches the package
|
|
501
|
+
in a way the build cannot read (a `require()`, say, or a `#` import), it warns, naming your file and
|
|
502
|
+
the module. On the server, its `cx.dynamic()` calls also map through every atom rather than through
|
|
503
|
+
`keep`, so the class such a call renders can be an atom outside the set the build ships, its emitted
|
|
504
|
+
set. Where the build ships no rule for that atom, the element renders as if the class were absent;
|
|
505
|
+
where another use put that atom in the emitted set, the server-rendered element carries the atom's
|
|
506
|
+
styling, and can keep it after the page hydrates: React, for one, does not guarantee to correct an
|
|
507
|
+
attribute that differs between the server's markup and the client's render. Add such a package to
|
|
508
|
+
`ssr.noExternal` in your Vite config: the build then reads it, and its `cx.dynamic()` calls on the
|
|
509
|
+
server read `keep` too. Listing it in `keepFor` instead stops that warning and ships its listed
|
|
510
|
+
atoms, but its `cx.dynamic()` calls on the server still map through every atom, and your server
|
|
511
|
+
files that import from it are still checked as above.
|
|
512
|
+
|
|
513
|
+
If you build the client and the server in two commands, build the server first: `vite build --ssr`,
|
|
514
|
+
then `vite build`. The two share a file in Vite's cache directory, so the client build also ships
|
|
515
|
+
the atoms the server build read. The client build also checks what each server build recorded there
|
|
516
|
+
of your server files' imports from packages listed in `keepFor`, so after you change server code,
|
|
517
|
+
build the server again before the client. A record whose server file no longer exists is dropped;
|
|
518
|
+
one from a server build you no longer run stays until you delete `nave-used-server-takes.json` from
|
|
519
|
+
Vite's cache directory and run each server build you do. Built the other way round, a server atom
|
|
520
|
+
the client's CSS lacks fails the server build, which records the atom for the next client build, and
|
|
521
|
+
the report says how to clear it. A server build that finds the client's list already used cannot
|
|
522
|
+
check the CSS, so it warns instead, naming each atom the last client build did not ship, and records
|
|
523
|
+
them the same way.
|
|
524
|
+
|
|
525
|
+
That file is `nave-used-atoms.json` in Vite's cache directory (`node_modules/.vite/` unless you set
|
|
526
|
+
`cacheDir`). Each `vite build` writes its atoms there, sorted, as the `emitted` list; after a client
|
|
527
|
+
build, that list is the set of atoms its CSS holds, so you can diff it between builds.
|
|
528
|
+
|
|
529
|
+
**Shipping every atom.** `navePlugin({ atomic: 'all' })` reads no `cx()` call and ships every atom,
|
|
530
|
+
as the PostCSS plugin does. It suits a project whose Nave classes are mostly written where no build
|
|
531
|
+
reads them, such as a backend's own templates or HTML from a CMS, where listing them in `keep` is
|
|
532
|
+
not practical. It gives up what this section describes: no `cx()` call is read, checked or turned
|
|
533
|
+
into its class string, so the name map ships, a name that is no atom passes through `cx()` as text,
|
|
534
|
+
as it does without the plugin, and `cx.dynamic()` maps through every atom. The dev server serves
|
|
535
|
+
every atom too, the plugin sets neither
|
|
536
|
+
`optimizeDeps.exclude` nor `resolve.noExternal`, no build writes `nave-used-atoms.json`, and
|
|
537
|
+
`keep`, `keepFor` and `cxModules` are still checked for well-formed values and otherwise do nothing.
|
|
538
|
+
It is also the way out that the plugin's own check names when a build's atom layer does not hold
|
|
539
|
+
exactly the atoms the build chose: removing the others relies on a part of Vite's CSS step that is
|
|
540
|
+
not a public Vite API, measured on Vite 8.2.1 and the newest 8.x at the time of each release, and on
|
|
541
|
+
a Vite where it does not take, every build fails that check until the plugin supports that version.
|
|
542
|
+
|
|
543
|
+
**If you publish components that call `cx()`.** Your users' builds read your published code. Write
|
|
544
|
+
every name your components choose at run time through `cx.dynamic()`, and every other call so the
|
|
545
|
+
build can read it. Then export the atoms those calls can take, so a user lists them in one line that
|
|
546
|
+
moves with your version:
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
// in your package, published as @acme/ui/nave-atoms
|
|
550
|
+
import type { AtomName } from '@navecss/core/cx'
|
|
551
|
+
|
|
552
|
+
export const naveAtoms = ['flex', 'grid', 'itemsCenter'] as const satisfies readonly AtomName[]
|
|
553
|
+
|
|
554
|
+
// in your user's vite.config.ts
|
|
555
|
+
// import { naveAtoms } from '@acme/ui/nave-atoms'
|
|
556
|
+
// navePlugin({ keepFor: { '@acme/ui': naveAtoms } })
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
A user whose app sits in the same repository as your package, as a workspace package, passes the
|
|
560
|
+
list to `keep` instead (`navePlugin({ keep: naveAtoms })`): the build reads a workspace package as
|
|
561
|
+
their own code, and `keepFor` does not reach it.
|
|
562
|
+
|
|
563
|
+
List every atom your components can apply at run time, `focusRing`, `srOnly`, `srOnlyFocusable`, `hidden` and the `hide*` atoms included where you use them: a consumer passes them to `keepFor` under your package's name, and one left out renders as if the class were absent in their production build.
|
|
564
|
+
|
|
565
|
+
### PostCSS plugin setup
|
|
566
|
+
|
|
567
|
+
For Next.js, webpack under `postcss-loader`, and any other pipeline that runs PostCSS plugins.
|
|
568
|
+
|
|
569
|
+
Add it to your PostCSS config:
|
|
194
570
|
|
|
195
571
|
```js
|
|
196
572
|
// postcss.config.js
|
|
@@ -201,31 +577,59 @@ export default {
|
|
|
201
577
|
}
|
|
202
578
|
```
|
|
203
579
|
|
|
204
|
-
|
|
580
|
+
A CommonJS config works too. Name it `postcss.config.cjs`, or keep `.js` in a project
|
|
581
|
+
without `"type": "module"`:
|
|
205
582
|
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
onUnknown: 'error',
|
|
212
|
-
})
|
|
583
|
+
```js
|
|
584
|
+
// postcss.config.cjs
|
|
585
|
+
module.exports = {
|
|
586
|
+
plugins: [require('@navecss/core/postcss')()],
|
|
587
|
+
}
|
|
213
588
|
```
|
|
214
589
|
|
|
215
|
-
To add atoms of your own to `@nave`, pass them as `extend`: see
|
|
216
|
-
[CONSUMER-ATOMS.md](./CONSUMER-ATOMS.md).
|
|
217
|
-
|
|
218
590
|
`postcss` is an optional peer dependency.
|
|
219
591
|
If you are using only `cx()` or tokens, you do not need to install it.
|
|
220
592
|
|
|
593
|
+
#### Next.js
|
|
594
|
+
|
|
595
|
+
Next.js reads a PostCSS plugin by its package name, so give it the name as a key and do not
|
|
596
|
+
call `navePlugin()`:
|
|
597
|
+
|
|
598
|
+
```js
|
|
599
|
+
// postcss.config.mjs
|
|
600
|
+
export default {
|
|
601
|
+
plugins: { '@navecss/core/postcss': {} },
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
The `navePlugin()` form above fails on Next.js's webpack pipeline with
|
|
606
|
+
`An unknown PostCSS plugin was provided`: that is Next 15's default and `next build --webpack`
|
|
607
|
+
on Next 16. Next 16's default, Turbopack, accepts either form.
|
|
608
|
+
|
|
609
|
+
Next.js also needs your browser floor, in `package.json`:
|
|
610
|
+
|
|
611
|
+
```json
|
|
612
|
+
{
|
|
613
|
+
"browserslist": ["chrome 125", "edge 125", "firefox 128", "safari 18", "ios_saf 18"]
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Without it, Turbopack compiles for older browsers and rewrites every `light-dark()` in your CSS,
|
|
618
|
+
Nave's colours included, into `--lightningcss-light` and `--lightningcss-dark` variables. Spell
|
|
619
|
+
the floor out as above: Next 15 rejects the shorter `baseline 2024` with `Unknown browser baseline`.
|
|
620
|
+
|
|
221
621
|
**If your Vite config sets `css.transformer: 'lightningcss'`, this plugin never
|
|
222
622
|
runs.** That option replaces Vite's CSS pipeline with Lightning CSS, which does
|
|
223
623
|
not run PostCSS plugins at all: the build stays green, `@nave` reaches the
|
|
224
624
|
browser as an unknown at-rule, and the browser drops it, so the rule renders
|
|
225
|
-
with none of the declarations its atoms were going to give it.
|
|
226
|
-
|
|
625
|
+
with none of the declarations its atoms were going to give it. On Vite, use
|
|
626
|
+
[the Vite plugin](#vite-plugin-setup), which runs under that option; to stay on this plugin,
|
|
627
|
+
leave the default transformer in place. A host that runs Lightning CSS directly, not through
|
|
628
|
+
Vite, uses [the Lightning CSS adapter](#lightning-css-adapter-setup). Add
|
|
629
|
+
[`navecss-core check`](#navecss-core-check) to your build script as well, and a
|
|
630
|
+
build that skips the plugin this way fails instead of shipping.
|
|
227
631
|
|
|
228
|
-
|
|
632
|
+
#### Plugin order
|
|
229
633
|
|
|
230
634
|
Put `navePlugin()` before any autoprefixing or syntax-down-levelling plugin (`autoprefixer`,
|
|
231
635
|
`postcss-preset-env`, and the like) in your `plugins` array. `navePlugin()` resolves `@nave`
|
|
@@ -235,6 +639,71 @@ ship the vendor-prefixed properties their declarations need (`interactive`'s
|
|
|
235
639
|
`-webkit-user-select` alongside `user-select`, for one), so this is only a concern for your own
|
|
236
640
|
CSS sharing the same pipeline.
|
|
237
641
|
|
|
642
|
+
### Lightning CSS adapter setup
|
|
643
|
+
|
|
644
|
+
For a host that runs Lightning CSS directly: your own script, or a tool that calls its
|
|
645
|
+
`transform()` or `bundleAsync()`. It is not the route for Vite: on Vite, use
|
|
646
|
+
[the Vite plugin](#vite-plugin-setup), which also runs under `css.transformer: 'lightningcss'`.
|
|
647
|
+
|
|
648
|
+
```js
|
|
649
|
+
import { readFile } from 'node:fs/promises'
|
|
650
|
+
import { bundleAsync, transform } from 'lightningcss'
|
|
651
|
+
import { navePlugin } from '@navecss/core/lightningcss'
|
|
652
|
+
|
|
653
|
+
const nave = navePlugin()
|
|
654
|
+
|
|
655
|
+
// One file, through transform(): expand it first, then pass the code and the map on.
|
|
656
|
+
const { code, map } = nave.expand(await readFile('src/app.css'), 'src/app.css')
|
|
657
|
+
const result = transform({ filename: 'src/app.css', code, inputSourceMap: map, sourceMap: true })
|
|
658
|
+
|
|
659
|
+
// An entry file and everything it @imports, through bundleAsync().
|
|
660
|
+
const bundled = await bundleAsync({ filename: 'src/app.css', resolver: nave.resolver })
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
The adapter expands the text before Lightning CSS reads it, so Lightning CSS never sees a
|
|
664
|
+
directive: no `Unknown at rule` warning, no Lightning CSS parse error on a malformed directive, and
|
|
665
|
+
Nave's own messages, with the file's path and the line and column of each problem.
|
|
666
|
+
`expand(code, filename)` takes the text or its bytes and returns `{ code, map }`, the code as bytes
|
|
667
|
+
and the map as a string, ready for `transform()`'s `code` and `inputSourceMap`. `resolver.read` reads each file
|
|
668
|
+
`bundleAsync()` asks for and returns its expanded text, so a file reached through `@import` is
|
|
669
|
+
covered too. It takes `extend` (an object of your own atoms) and `onUnknown` as the other plugins
|
|
670
|
+
do ([Options](#options)); under `'warn'` it prints each problem with `console.warn`.
|
|
671
|
+
|
|
672
|
+
The adapter imports nothing from `lightningcss`, types included, and declares no peer: you bring
|
|
673
|
+
your own copy. The supported range is documented, not declared: `lightningcss` 1.24.1 and later.
|
|
674
|
+
Earlier releases can drop the message of an error a resolver throws during `bundleAsync()`, so a
|
|
675
|
+
malformed directive can fail a build with an empty error instead of Nave's message. The nested rules
|
|
676
|
+
the adapter writes also need CSS nesting, on by default only from 1.22 (1.20 and 1.21 cannot parse
|
|
677
|
+
them). The fixtures run on 1.24.1 and on the newest release at the time of each release of this
|
|
678
|
+
package. One cost, on the `bundleAsync()` path: `read` returns a string and no source
|
|
679
|
+
map, so Nave's insertions are not in the output map there. The inserted text adds no line breaks,
|
|
680
|
+
so line numbers hold. On the `transform()` path the map is chained through `inputSourceMap`.
|
|
681
|
+
|
|
682
|
+
### Options
|
|
683
|
+
|
|
684
|
+
```ts
|
|
685
|
+
navePlugin({
|
|
686
|
+
// 'error' (default) — throw, failing the build
|
|
687
|
+
// 'warn' — log and skip
|
|
688
|
+
// 'ignore' — silently skip
|
|
689
|
+
onUnknown: 'error',
|
|
690
|
+
})
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Under `'error'`, every problem this option covers in one stylesheet is reported
|
|
694
|
+
in one error, positioned at the first: its message, then
|
|
695
|
+
`N more in this stylesheet:` and one `line:column:` line for each of the
|
|
696
|
+
others. The report covers one stylesheet, so a bundler that stops at the first
|
|
697
|
+
failing file reports one file per build. Under `'warn'`, each problem is a
|
|
698
|
+
warning of its own.
|
|
699
|
+
|
|
700
|
+
To add atoms of your own to `@nave`, pass them as `extend`: see
|
|
701
|
+
[CONSUMER-ATOMS.md](./CONSUMER-ATOMS.md). An `extend` object written inline in
|
|
702
|
+
a cached host's config, or imported into it, does not invalidate that host's
|
|
703
|
+
cache; pass `extend` as a path to the module instead when that matters. Only
|
|
704
|
+
the named file is re-read and declared to the host as a dependency; a module
|
|
705
|
+
it imports is neither (same doc).
|
|
706
|
+
|
|
238
707
|
### Where `@nave` is valid
|
|
239
708
|
|
|
240
709
|
`@nave` must be the direct child of a CSS rule selector block. Any rule will
|
|
@@ -254,6 +723,97 @@ failure, not a silently incomplete build):
|
|
|
254
723
|
|
|
255
724
|
---
|
|
256
725
|
|
|
726
|
+
## `navecss-core check`
|
|
727
|
+
|
|
728
|
+
A survival check: it reads built CSS and reports every `@nave` directive that
|
|
729
|
+
reached it, so a missing PostCSS pipeline or a bypassed one (Vite's
|
|
730
|
+
`css.transformer: 'lightningcss'`, for one) fails the build instead of
|
|
731
|
+
shipping a page with none of the declarations its atoms were going to give
|
|
732
|
+
it.
|
|
733
|
+
|
|
734
|
+
```json
|
|
735
|
+
{
|
|
736
|
+
"scripts": {
|
|
737
|
+
"build": "vite build && navecss-core check --source=dist"
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
For Next.js, point it at the build's static output:
|
|
743
|
+
|
|
744
|
+
```json
|
|
745
|
+
{
|
|
746
|
+
"scripts": {
|
|
747
|
+
"build": "next build && navecss-core check --source=.next/static"
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
`--source=` is repeatable, and each value is a file or a directory read
|
|
753
|
+
recursively for `.css`. Exit codes:
|
|
754
|
+
|
|
755
|
+
- `0` — at least one stylesheet was read, and none held `@nave`.
|
|
756
|
+
- `1` — at least one stylesheet held `@nave`; the findings are printed, one
|
|
757
|
+
line each (file, line, column, the directive as written, and its enclosing
|
|
758
|
+
selector when known).
|
|
759
|
+
- `2` — a usage error, an unreadable `--source` path, or no stylesheet found
|
|
760
|
+
at all. An unreadable path gives `2` even when a stylesheet that was read
|
|
761
|
+
held `@nave`; those findings are still printed.
|
|
762
|
+
|
|
763
|
+
Run it from a `package.json` script, chained with `&&` after the build that
|
|
764
|
+
should have resolved every directive, so it reads that build's output. The
|
|
765
|
+
command comes with `@navecss/core`; it is not a package of its own.
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
## `navecss-core expand`
|
|
770
|
+
|
|
771
|
+
For a project with no bundler. It reads each stylesheet, expands every `@nave`, and writes the
|
|
772
|
+
result:
|
|
773
|
+
|
|
774
|
+
```json
|
|
775
|
+
{
|
|
776
|
+
"scripts": {
|
|
777
|
+
"build": "navecss-core expand --source=src/app.css --out=app.css",
|
|
778
|
+
"watch": "navecss-core expand --source=src/app.css --out=app.css --watch"
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
`--source=` and `--out=` are repeated as pairs, matched by order, one pair per file; unequal
|
|
784
|
+
counts are a usage error. `--extend=<module>` is the path to a module whose default export is
|
|
785
|
+
your own atoms, read the way the PostCSS plugin reads one. `--watch` rewrites each `--out` when
|
|
786
|
+
its source, or the `--extend` module, changes, and keeps running after a problem. Every problem
|
|
787
|
+
across every file is printed in one run, one report per stylesheet. Exit codes:
|
|
788
|
+
|
|
789
|
+
- `0` — every file was expanded and written.
|
|
790
|
+
- `1` — at least one stylesheet held a problem; the problems are printed and `--out` is not
|
|
791
|
+
written, for any of the pairs.
|
|
792
|
+
- `2` — the run could not be done as asked: a usage error (such as a missing or unmatched `--out`,
|
|
793
|
+
an unknown flag, or an `--out` that is also a `--source`), an `--extend` module that could not be
|
|
794
|
+
used, a `--source` that could not be read, an `--out` that could not be written, or, under
|
|
795
|
+
`--watch`, a directory that could not be watched. Nothing is written, except that files written
|
|
796
|
+
before an `--out` that failed stay written.
|
|
797
|
+
|
|
798
|
+
It does not resolve or inline `@import`; that would make it a CSS bundler. It
|
|
799
|
+
expands only the files you name with `--source`, so a stylesheet you import that
|
|
800
|
+
holds `@nave` needs a pair of its own. An `@import` of a relative or absolute URL
|
|
801
|
+
is kept as written and the file it names is not read, so its path has to work
|
|
802
|
+
from where the output is served, not from the source's directory. A bare import,
|
|
803
|
+
such as one of a package (`@import url('@navecss/core')`, which is how the
|
|
804
|
+
bundler route's `app.css` starts), is reported as a `bare-import` problem,
|
|
805
|
+
because a browser cannot load it. For Nave's stylesheets, link
|
|
806
|
+
`@navecss/core/standalone` instead, which has no `@import` in it, by path
|
|
807
|
+
(`node_modules/@navecss/core/dist/standalone.css`) or from a CDN
|
|
808
|
+
(`https://cdn.jsdelivr.net/npm/@navecss/core@<version>/dist/standalone.css`, with
|
|
809
|
+
`<version>` the version you installed), before your expanded stylesheet. For
|
|
810
|
+
`@navecss/core/no-tokens`, link that entry's own file, `dist/no-tokens.css`, the
|
|
811
|
+
same way: the standalone stylesheet holds Nave's token layer, which that entry
|
|
812
|
+
leaves out. [Without a bundler](https://github.com/navecss/navecss#without-a-bundler)
|
|
813
|
+
has the whole setup.
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
257
817
|
## Editor, linter and coding agent
|
|
258
818
|
|
|
259
819
|
`@nave` is new to these tools, so each one needs a step, and this package ships what each of
|
|
@@ -321,13 +881,18 @@ Consumers of `@navecss/tokens` alone get no guide: it ships only in this package
|
|
|
321
881
|
|
|
322
882
|
## Exports
|
|
323
883
|
|
|
324
|
-
| Export
|
|
325
|
-
|
|
|
326
|
-
| `@navecss/core`
|
|
327
|
-
| `@navecss/core/layers`
|
|
328
|
-
| `@navecss/core/no-tokens`
|
|
329
|
-
| `@navecss/core/reset`
|
|
330
|
-
| `@navecss/core/atomic`
|
|
331
|
-
| `@navecss/core/cx`
|
|
332
|
-
| `@navecss/core/atoms`
|
|
333
|
-
| `@navecss/core/
|
|
884
|
+
| Export | Description |
|
|
885
|
+
| ---------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
886
|
+
| `@navecss/core` | CSS entry point — `@import url('@navecss/core')` |
|
|
887
|
+
| `@navecss/core/layers` | The `@layer` order statement alone. Import it first, before any other stylesheet |
|
|
888
|
+
| `@navecss/core/no-tokens` | Entry point for projects that generate their own Nave token layer; see the header comment in the file |
|
|
889
|
+
| `@navecss/core/reset` | Reset stylesheet only. It reads Nave's custom properties, so it needs a token layer beside it |
|
|
890
|
+
| `@navecss/core/atomic` | Generated atomic CSS (global `nave-` classes) |
|
|
891
|
+
| `@navecss/core/cx` | `cx()` / `cx.raw()` utilities + `AtomName` type |
|
|
892
|
+
| `@navecss/core/atoms` | Atom definitions + `atomClassMap` |
|
|
893
|
+
| `@navecss/core/vite` | Vite plugin — `navePlugin()` |
|
|
894
|
+
| `@navecss/core/lightningcss` | Lightning CSS adapter — `navePlugin()`, returning `expand` and `resolver` |
|
|
895
|
+
| `@navecss/core/standalone` | The whole stylesheet in one file, with no `@import`, for projects with no bundler |
|
|
896
|
+
| `@navecss/core/postcss` | PostCSS plugin — `navePlugin()` |
|
|
897
|
+
| `@navecss/core/check` | The survival check as a function — `check({ source })` |
|
|
898
|
+
| `@navecss/core/collect` | Reads which atoms a module uses, for build tools; not meant for application code |
|