@navecss/core 0.1.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/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # @navecss/core
2
+
3
+ > Layer architecture, reset, atomic utilities, and PostCSS plugin for Nave.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @navecss/core
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ - **Browsers: Chrome and Edge 125, Firefox 128, Safari 18 (Baseline 2024).**
14
+ Every semantic colour in Nave's token stylesheet is a `light-dark()` pair, and
15
+ most of them are also derived with relative colour syntax. An older browser
16
+ drops whichever of them it does not support, without an error: surfaces and
17
+ text lose their colours. Your CSS build has to target the same floor, and
18
+ [PostCSS plugin setup](#postcss-plugin-setup) has the line for Vite.
19
+ [Why this floor](https://github.com/navecss/navecss/blob/main/docs/04-adr/0005-browser-floor.md).
20
+ - **A resolver that reads `exports` maps.** Every entry point, the stylesheet
21
+ included, is reachable only through the package's `exports` map: there is no
22
+ `main` field to fall back on.
23
+ - **ES modules.** `@navecss/core/cx`, `@navecss/core/atoms` and
24
+ `@navecss/core/postcss` load through `import` only. A `require()` of any of
25
+ them fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`, which reads as though the
26
+ entry point did not exist. In TypeScript, set `moduleResolution` to `bundler`,
27
+ `node16` or `nodenext`; under `node10` their types are not found. A CommonJS
28
+ file type-checks against them and then fails when it runs, so import them from
29
+ an ES module.
30
+ - **Node 22.18 or later.**
31
+ - **PostCSS 8, for `@nave` only.** `postcss` is an optional peer dependency, so
32
+ nothing installs it for you: add it yourself if you use `@nave`. Without it,
33
+ importing `@navecss/core/postcss` fails with `Cannot find package 'postcss'`.
34
+ The stylesheets and `cx()` need no PostCSS.
35
+
36
+ ## Setup
37
+
38
+ Import once at your app entry point:
39
+
40
+ ```css
41
+ /* app.css */
42
+ @import url('@navecss/core/layers');
43
+ @import url('@navecss/core');
44
+ ```
45
+
46
+ This imports the full layer stack in the correct order:
47
+ `tokens.defaults → tokens.presets → reset → atomic → components.nave → components.consumer → overrides`
48
+
49
+ The order holds only if it is the first `@layer` declaration your page sees:
50
+ a stylesheet of yours that declares a layer and loads earlier fixes that
51
+ layer's position first, and the order inverts with no error. So keep
52
+ `@navecss/core/layers`, the order statement alone, as the first line of
53
+ `app.css`, and import `app.css` in your entry file before anything that brings
54
+ its own stylesheet, components included. The `@navecss/core` import after it
55
+ repeats the same statement, which changes nothing
56
+ ([why, and where your CSS goes](https://github.com/navecss/navecss/blob/main/docs/04-adr/0003-layer-cascade-contract.md)).
57
+
58
+ Your component CSS goes in `@layer components.consumer`, and a deliberate
59
+ exception goes in `@layer overrides`, which beats every other layer. Left outside
60
+ any layer, your CSS would beat all of them, `overrides` included. Do not write
61
+ into a bare `@layer components` or `@layer tokens`: a rule written directly into a
62
+ layer outranks everything in that layer's sublayers.
63
+
64
+ ---
65
+
66
+ ## Styling components
67
+
68
+ Nave gives you three ways to apply atomic utilities. Start with @nave directives.
69
+ Every built-in atom, with the declarations it applies, is listed in
70
+ [ATOMS.md](./ATOMS.md).
71
+
72
+ ### Primary — @nave directives
73
+
74
+ Requires the PostCSS plugin. Everything stays in CSS files.
75
+
76
+ ```css
77
+ /* button.module.css */
78
+ @layer components.consumer {
79
+ .root {
80
+ @nave interactive focusRing transition;
81
+ background: var(--nave-color-action-primary);
82
+ color: var(--nave-color-on-action-primary);
83
+ padding: var(--nave-spacing-control-md) var(--nave-spacing-control-lg);
84
+ border-radius: var(--nave-radius-control);
85
+ }
86
+ }
87
+ ```
88
+
89
+ Declarations are inlined at build time, and nothing of `@nave` is left at run time.
90
+ Pseudo-class rules (`:focus-visible` from `focusRing`), `@media` and
91
+ `@container` blocks are emitted as native CSS nesting inside your rule, so they
92
+ apply to every selector in a selector list. Native nesting is inside the browser
93
+ floor under [Requirements](#requirements).
94
+
95
+ ### Escape hatch — cx() utility
96
+
97
+ No PostCSS required. Atom names move to JSX — autocompleted, and type-checked.
98
+
99
+ ```tsx
100
+ import { cx } from '@navecss/core/cx'
101
+ import styles from './button.module.css'
102
+
103
+ ;<button className={`${cx('interactive', 'focusRing', 'transition')} ${styles.root}`} />
104
+ ```
105
+
106
+ Use this if you cannot or do not want to add a PostCSS plugin to your build.
107
+
108
+ A `cx()` class is a default, not an override: it sits in the `atomic` layer,
109
+ below your component CSS, so your own rule wins wherever the two set the same
110
+ property.
111
+
112
+ Two channels, because they carry different things. `cx()` takes Nave's built-in
113
+ atoms and nothing else. Your own classes compose in the template literal, the
114
+ way your own declarations sit beside `@nave` in a rule body. `cx.raw()` is for a
115
+ class from outside any system Nave can see — a legacy global class, a
116
+ third-party widget's class — and it returns what you give it, untouched:
117
+
118
+ ```tsx
119
+ ;<div className={`${cx('interactive')} ${cx.raw('legacy-card')}`} />
120
+ ```
121
+
122
+ **A conditional class goes in a call, not in a slot.** Both channels filter
123
+ falsy arguments, so `cx('interactive', isActive && 'focusRing')` and
124
+ `cx.raw(isActive && styles.active)` each drop the argument when the condition
125
+ is false. A template-literal slot does not: `${isActive && styles.active}`
126
+ interpolates the string `false`, and an undefined one interpolates `undefined`.
127
+
128
+ What that buys you, and what it does not:
129
+
130
+ - **An unknown name is a compile error**, in your own `tsc`, with nothing
131
+ installed from us. That covers a typo (`cx('interactve')`), a name typed
132
+ from the CSS side (`cx('sr-only')` — the atom is `srOnly`, and the class it
133
+ _emits_ is `nave-sr-only`; neither spelling is the key), and any class that is
134
+ not a Nave atom. **TypeScript only:
135
+ a JavaScript consumer gets none of it.** For a build-level check that does not
136
+ depend on types, use the `@nave` directive with `navePlugin()` — it fails the
137
+ build on an unknown atom by default.
138
+ - **`cx.raw()` cannot shadow one of your classes.** It never consults the atom
139
+ map, so `cx.raw('container')` is the literal `container`. Inside `cx()` an
140
+ atom name still resolves to that atom's global class — `cx('container')` is
141
+ `nave-container` — which is why the ordinary-sounding names (`container`,
142
+ `hidden`, `grid`, `flex`, `block`, `border`, `rounded`, `transition`,
143
+ `relative`, `absolute`, `gap`, `truncate`, `interactive`) belong on the
144
+ `cx.raw()` side when you mean your own.
145
+ - **Nothing checks the rest of the `className` attribute.** A bare
146
+ `'legacy-card'` sitting beside these calls is seen by nothing and still works.
147
+ So `cx.raw()` is a **declared** escape channel, not an enforced one: its value
148
+ today is that every deliberate step outside the system leaves a token you can
149
+ find (`grep -r 'cx.raw'`). A lint that closes the attribute itself is not part
150
+ of this release.
151
+
152
+ ### Tokens only
153
+
154
+ For library authors or teams building on top of Base UI or Radix UI.
155
+ CSS custom properties are always available — no atomic layer required.
156
+
157
+ ```css
158
+ .root {
159
+ background: var(--nave-color-action-primary);
160
+ border-radius: var(--nave-radius-control);
161
+ transition-duration: var(--nave-motion-duration-base);
162
+ }
163
+ ```
164
+
165
+ Every custom property Nave declares is in the stylesheet you already installed, at `@navecss/tokens/css`.
166
+
167
+ ---
168
+
169
+ ## PostCSS plugin setup
170
+
171
+ ```ts
172
+ // vite.config.ts
173
+ import { defineConfig } from 'vite'
174
+ import { navePlugin } from '@navecss/core/postcss'
175
+
176
+ export default defineConfig({
177
+ // your existing options, plugins included, stay as they are
178
+ css: { postcss: { plugins: [navePlugin()] } },
179
+ build: { cssTarget: ['chrome125', 'edge125', 'firefox128', 'safari18', 'ios18'] },
180
+ })
181
+ ```
182
+
183
+ `build.cssTarget` is the browser floor, in Vite's terms. Vite's default targets
184
+ older browsers, and building for them gains you nothing, because the output
185
+ still needs the floor. It does cost you something: Vite rewrites `light-dark()`
186
+ in Nave's colours into an emulation that a `color-scheme` set from script, or on
187
+ part of the page, does not switch. If your project already has a
188
+ `postcss.config.js`, put `navePlugin()` there instead: Vite reads no PostCSS
189
+ config file once `css.postcss` is set inline.
190
+
191
+ ```js
192
+ // postcss.config.js
193
+ import { navePlugin } from '@navecss/core/postcss'
194
+
195
+ export default {
196
+ plugins: [navePlugin()],
197
+ }
198
+ ```
199
+
200
+ ### Options
201
+
202
+ ```ts
203
+ navePlugin({
204
+ // 'error' (default) — throw, failing the build
205
+ // 'warn' — log and skip
206
+ // 'ignore' — silently skip
207
+ onUnknown: 'error',
208
+ })
209
+ ```
210
+
211
+ To add atoms of your own to `@nave`, pass them as `extend`: see
212
+ [CONSUMER-ATOMS.md](./CONSUMER-ATOMS.md).
213
+
214
+ `postcss` is an optional peer dependency.
215
+ If you are using only `cx()` or tokens, you do not need to install it.
216
+
217
+ **If your Vite config sets `css.transformer: 'lightningcss'`, this plugin never
218
+ runs.** That option replaces Vite's CSS pipeline with Lightning CSS, which does
219
+ not run PostCSS plugins at all: the build stays green, `@nave` reaches the
220
+ browser as an unknown at-rule, and the browser drops it, so the rule renders
221
+ with none of the declarations its atoms were going to give it. Leave the
222
+ default transformer in place on a project using `@nave`.
223
+
224
+ ### Plugin order
225
+
226
+ Put `navePlugin()` before any autoprefixing or syntax-down-levelling plugin (`autoprefixer`,
227
+ `postcss-preset-env`, and the like) in your `plugins` array. `navePlugin()` resolves `@nave`
228
+ directives into literal declarations; a prefixer ordered before it only ever sees the
229
+ unexpanded directive, so it has nothing of Nave's to add a prefix to. Nave's own atoms already
230
+ ship the vendor-prefixed properties their declarations need (`interactive`'s
231
+ `-webkit-user-select` alongside `user-select`, for one), so this is only a concern for your own
232
+ CSS sharing the same pipeline.
233
+
234
+ ### Where `@nave` is valid
235
+
236
+ `@nave` must be the direct child of a CSS rule selector block. Any rule will
237
+ do and the nesting depth does not matter, so `.card { @nave flex; }`,
238
+ `.card { &:hover { @nave flex; } }`, `.card { .badge { @nave flex; } }` and
239
+ `@supports (display: grid) { .card { @nave flex; } }` are all valid. Two
240
+ shapes that parse without error are still rejected, through the same
241
+ `onUnknown` option as an unknown atom name (so the default is a build
242
+ failure, not a silently incomplete build):
243
+
244
+ - **Nested inside `@media` or `@container` with no rule in between** —
245
+ `.card { @media (width >= 37.5em) { @nave flex; } }`. Write the directive
246
+ inside a rule instead: `.card { @media (width >= 37.5em) { & { @nave flex; } } }`.
247
+ - **Inside `@keyframes`** — a keyframe step (`to`, `from`, `50%`) parses as a
248
+ rule, but `&` has no meaning there and a browser drops the nesting with no
249
+ other symptom, so it is rejected the same way.
250
+
251
+ ---
252
+
253
+ ## Exports
254
+
255
+ | Export | Description |
256
+ | ------------------------- | ----------------------------------------------------------------------------------------------------- |
257
+ | `@navecss/core` | CSS entry point — `@import url('@navecss/core')` |
258
+ | `@navecss/core/layers` | The `@layer` order statement alone. Import it first, before any other stylesheet |
259
+ | `@navecss/core/no-tokens` | Entry point for projects that generate their own Nave token layer; see the header comment in the file |
260
+ | `@navecss/core/reset` | Reset stylesheet only. It reads Nave's custom properties, so it needs a token layer beside it |
261
+ | `@navecss/core/atomic` | Generated atomic CSS (global `nave-` classes) |
262
+ | `@navecss/core/cx` | `cx()` / `cx.raw()` utilities + `AtomName` type |
263
+ | `@navecss/core/atoms` | Atom definitions + `atomClassMap` |
264
+ | `@navecss/core/postcss` | PostCSS plugin — `navePlugin()` |
@@ -0,0 +1,288 @@
1
+ /*
2
+ * Nave atomic utilities — generated from src/atoms.ts
3
+ * Do not edit directly. Run `pnpm build` in packages/core to regenerate.
4
+ *
5
+ * Primary path: .root { @nave interactive focusRing; } (postcss plugin)
6
+ * Escape hatch: `${cx('interactive', 'focusRing')} ${styles.root}`
7
+ *
8
+ * Classes are global, namespaced with nave- prefix.
9
+ * Consumer atoms (navePlugin extend) are NOT included —
10
+ * they are resolved inline by the PostCSS plugin only.
11
+ *
12
+ * Self-layered under @layer atomic: this file ships as a published subpath
13
+ * export (./atomic) and is imported directly by some consumers, so the
14
+ * layer guarantee has to be a property of the artifact rather than of
15
+ * index.css's import site.
16
+ */
17
+
18
+ @layer tokens.defaults, tokens.presets, reset, atomic, components.nave, components.consumer, overrides;
19
+
20
+ @layer atomic {
21
+ .nave-flex {
22
+ display: flex;
23
+ }
24
+
25
+ .nave-inline-flex {
26
+ display: inline-flex;
27
+ }
28
+
29
+ .nave-grid {
30
+ display: grid;
31
+ }
32
+
33
+ .nave-block {
34
+ display: block;
35
+ }
36
+
37
+ .nave-inline-block {
38
+ display: inline-block;
39
+ }
40
+
41
+ .nave-hidden {
42
+ display: none;
43
+ }
44
+
45
+ .nave-flex-col {
46
+ flex-direction: column;
47
+ }
48
+
49
+ .nave-flex-wrap {
50
+ flex-wrap: wrap;
51
+ }
52
+
53
+ .nave-items-center {
54
+ align-items: center;
55
+ }
56
+
57
+ .nave-items-start {
58
+ align-items: flex-start;
59
+ }
60
+
61
+ .nave-items-end {
62
+ align-items: flex-end;
63
+ }
64
+
65
+ .nave-justify-center {
66
+ justify-content: center;
67
+ }
68
+
69
+ .nave-justify-between {
70
+ justify-content: space-between;
71
+ }
72
+
73
+ .nave-justify-end {
74
+ justify-content: flex-end;
75
+ }
76
+
77
+ .nave-flex-grow {
78
+ flex-grow: 1;
79
+ }
80
+
81
+ .nave-flex-shrink0 {
82
+ flex-shrink: 0;
83
+ }
84
+
85
+ .nave-gap {
86
+ gap: var(--nave-spacing-content-md);
87
+ }
88
+
89
+ .nave-relative {
90
+ position: relative;
91
+ }
92
+
93
+ .nave-absolute {
94
+ position: absolute;
95
+ }
96
+
97
+ .nave-inset-full {
98
+ inset: 0;
99
+ }
100
+
101
+ .nave-w-full {
102
+ width: 100%;
103
+ }
104
+
105
+ .nave-h-full {
106
+ height: 100%;
107
+ }
108
+
109
+ .nave-min-w0 {
110
+ min-width: 0;
111
+ }
112
+
113
+ .nave-truncate {
114
+ overflow: hidden;
115
+ text-overflow: ellipsis;
116
+ white-space: nowrap;
117
+ }
118
+
119
+ .nave-sr-only {
120
+ position: absolute;
121
+ width: 1px;
122
+ height: 1px;
123
+ padding: 0;
124
+ margin: -1px;
125
+ overflow: hidden;
126
+ clip-path: inset(50%);
127
+ white-space: nowrap;
128
+ border-width: 0;
129
+ }
130
+
131
+ .nave-sr-only-focusable {
132
+ position: absolute;
133
+ width: 1px;
134
+ height: 1px;
135
+ padding: 0;
136
+ margin: -1px;
137
+ overflow: hidden;
138
+ clip-path: inset(50%);
139
+ white-space: nowrap;
140
+ border-width: 0;
141
+
142
+ &:focus-visible {
143
+ position: static;
144
+ width: auto;
145
+ height: auto;
146
+ margin: 0;
147
+ overflow: visible;
148
+ clip-path: none;
149
+ white-space: normal;
150
+ }
151
+
152
+ &:focus-within {
153
+ position: static;
154
+ width: auto;
155
+ height: auto;
156
+ margin: 0;
157
+ overflow: visible;
158
+ clip-path: none;
159
+ white-space: normal;
160
+ }
161
+ }
162
+
163
+ .nave-no-wrap {
164
+ white-space: nowrap;
165
+ }
166
+
167
+ .nave-break-word {
168
+ overflow-wrap: break-word;
169
+ }
170
+
171
+ .nave-text-left {
172
+ text-align: left;
173
+ }
174
+
175
+ .nave-text-center {
176
+ text-align: center;
177
+ }
178
+
179
+ .nave-text-right {
180
+ text-align: right;
181
+ }
182
+
183
+ .nave-interactive {
184
+ cursor: pointer;
185
+ -webkit-user-select: none;
186
+ user-select: none;
187
+ -webkit-tap-highlight-color: transparent;
188
+ }
189
+
190
+ .nave-focus-ring {
191
+ outline: none;
192
+
193
+ &:focus-visible {
194
+ outline: var(--nave-border-width-focus) solid var(--nave-color-border-focus);
195
+ outline-offset: 2px;
196
+ }
197
+ }
198
+
199
+ .nave-disabled-state {
200
+ &:disabled, &[aria-disabled="true"] {
201
+ color: var(--nave-color-content-disabled);
202
+ border-color: var(--nave-color-border-disabled);
203
+ pointer-events: none;
204
+ }
205
+ }
206
+
207
+ .nave-rounded {
208
+ border-radius: var(--nave-radius-control);
209
+ }
210
+
211
+ .nave-rounded-card {
212
+ border-radius: var(--nave-radius-card);
213
+ }
214
+
215
+ .nave-rounded-full {
216
+ border-radius: var(--nave-radius-full);
217
+ }
218
+
219
+ .nave-border {
220
+ border: var(--nave-border-width-sm) solid var(--nave-color-border-default);
221
+ }
222
+
223
+ .nave-overflow-hidden {
224
+ overflow: hidden;
225
+ }
226
+
227
+ .nave-overflow-auto {
228
+ overflow: auto;
229
+ }
230
+
231
+ .nave-transition {
232
+ transition-property: color, background-color, border-color, opacity, box-shadow;
233
+ transition-duration: var(--nave-motion-duration-base);
234
+ transition-timing-function: var(--nave-motion-easing-standard);
235
+ }
236
+
237
+ .nave-container {
238
+ container-type: inline-size;
239
+ }
240
+
241
+ .nave-hide-phone-only {
242
+ @media (width < 37.5em) {
243
+ & {
244
+ display: none;
245
+ }
246
+ }
247
+ }
248
+
249
+ .nave-hide-tablet-portrait-up {
250
+ @media (width >= 37.5em) {
251
+ & {
252
+ display: none;
253
+ }
254
+ }
255
+ }
256
+
257
+ .nave-hide-tablet-landscape-up {
258
+ @media (width >= 56.25em) {
259
+ & {
260
+ display: none;
261
+ }
262
+ }
263
+ }
264
+
265
+ .nave-hide-desktop-up {
266
+ @media (width >= 75em) {
267
+ & {
268
+ display: none;
269
+ }
270
+ }
271
+ }
272
+
273
+ .nave-stack-phone-only {
274
+ @media (width < 37.5em) {
275
+ & {
276
+ flex-direction: column;
277
+ }
278
+ }
279
+ }
280
+
281
+ .nave-w-full-phone-only {
282
+ @media (width < 37.5em) {
283
+ & {
284
+ width: 100%;
285
+ }
286
+ }
287
+ }
288
+ }