@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.
@@ -0,0 +1,394 @@
1
+ import { media } from '@navecss/tokens/breakpoints';
2
+
3
+ // src/atoms.ts
4
+ var srOnlyDeclarations = {
5
+ position: "absolute",
6
+ width: "1px",
7
+ height: "1px",
8
+ padding: "0",
9
+ margin: "-1px",
10
+ overflow: "hidden",
11
+ "clip-path": "inset(50%)",
12
+ "white-space": "nowrap",
13
+ "border-width": "0"
14
+ };
15
+ var srOnlyRevealed = {
16
+ position: "static",
17
+ width: "auto",
18
+ height: "auto",
19
+ margin: "0",
20
+ overflow: "visible",
21
+ "clip-path": "none",
22
+ "white-space": "normal"
23
+ };
24
+ var atoms = {
25
+ // ── Display ────────────────────────────────────────────────────────────────
26
+ flex: {
27
+ declarations: { display: "flex" }
28
+ },
29
+ inlineFlex: {
30
+ declarations: { display: "inline-flex" }
31
+ },
32
+ grid: {
33
+ declarations: { display: "grid" }
34
+ },
35
+ block: {
36
+ declarations: { display: "block" }
37
+ },
38
+ inlineBlock: {
39
+ declarations: { display: "inline-block" }
40
+ },
41
+ hidden: {
42
+ declarations: { display: "none" }
43
+ },
44
+ // ── Flex ───────────────────────────────────────────────────────────────────
45
+ flexCol: {
46
+ declarations: { "flex-direction": "column" }
47
+ },
48
+ flexWrap: {
49
+ declarations: { "flex-wrap": "wrap" }
50
+ },
51
+ itemsCenter: {
52
+ declarations: { "align-items": "center" }
53
+ },
54
+ itemsStart: {
55
+ declarations: { "align-items": "flex-start" }
56
+ },
57
+ itemsEnd: {
58
+ declarations: { "align-items": "flex-end" }
59
+ },
60
+ justifyCenter: {
61
+ declarations: { "justify-content": "center" }
62
+ },
63
+ justifyBetween: {
64
+ declarations: { "justify-content": "space-between" }
65
+ },
66
+ justifyEnd: {
67
+ declarations: { "justify-content": "flex-end" }
68
+ },
69
+ flexGrow: {
70
+ declarations: { "flex-grow": "1" }
71
+ },
72
+ flexShrink0: {
73
+ declarations: { "flex-shrink": "0" }
74
+ },
75
+ gap: {
76
+ declarations: { gap: "var(--nave-spacing-content-md)" }
77
+ },
78
+ // ── Position ───────────────────────────────────────────────────────────────
79
+ relative: {
80
+ declarations: { position: "relative" }
81
+ },
82
+ absolute: {
83
+ declarations: { position: "absolute" }
84
+ },
85
+ insetFull: {
86
+ declarations: { inset: "0" }
87
+ },
88
+ // ── Sizing ─────────────────────────────────────────────────────────────────
89
+ wFull: {
90
+ declarations: { width: "100%" }
91
+ },
92
+ hFull: {
93
+ declarations: { height: "100%" }
94
+ },
95
+ minW0: {
96
+ declarations: { "min-width": "0" }
97
+ },
98
+ // ── Typography ─────────────────────────────────────────────────────────────
99
+ truncate: {
100
+ declarations: {
101
+ overflow: "hidden",
102
+ "text-overflow": "ellipsis",
103
+ "white-space": "nowrap"
104
+ }
105
+ },
106
+ /**
107
+ * srOnly — visually hidden, accessible to screen readers.
108
+ * Every property in the shared declaration set is load-bearing: the 1x1 box
109
+ * stays in the accessibility tree where display: none would not, the box is
110
+ * 1px rather than 0 because a zero-size box is dropped from that tree by some
111
+ * assistive technology, the negative margin cancels it out of layout flow,
112
+ * clip-path is used rather than clip so it keeps working if a consumer
113
+ * overrides position, and nowrap stops long text rendering as a visible
114
+ * column of single characters. Do not simplify.
115
+ */
116
+ srOnly: {
117
+ declarations: srOnlyDeclarations
118
+ },
119
+ /**
120
+ * srOnlyFocusable — srOnly that reveals itself when it (or a descendant)
121
+ * receives keyboard focus. The skip-link pattern needs this: an element
122
+ * hidden until it receives keyboard focus. Plain srOnly on a skip link
123
+ * produces a focusable element that is permanently invisible, so a
124
+ * keyboard user tabs into apparent nothingness.
125
+ *
126
+ * :focus-visible — the element itself is the focusable (e.g. the skip
127
+ * link anchor carries this atom directly).
128
+ * :focus-within — the hidden element wraps the focusable rather than
129
+ * being it.
130
+ */
131
+ srOnlyFocusable: {
132
+ declarations: srOnlyDeclarations,
133
+ pseudos: {
134
+ ":focus-visible": srOnlyRevealed,
135
+ ":focus-within": srOnlyRevealed
136
+ }
137
+ },
138
+ noWrap: {
139
+ declarations: { "white-space": "nowrap" }
140
+ },
141
+ breakWord: {
142
+ declarations: { "overflow-wrap": "break-word" }
143
+ },
144
+ textLeft: {
145
+ declarations: { "text-align": "left" }
146
+ },
147
+ textCenter: {
148
+ declarations: { "text-align": "center" }
149
+ },
150
+ textRight: {
151
+ declarations: { "text-align": "right" }
152
+ },
153
+ // ── Interaction ────────────────────────────────────────────────────────────
154
+ /**
155
+ * interactive — base for any clickable non-button element.
156
+ * -webkit-tap-highlight-color removes the grey tap flash on iOS/Android.
157
+ * -webkit-user-select is required alongside user-select: every released Safari and every
158
+ * iOS browser (all WebKit-based, regardless of the label on the tin) reads only the
159
+ * prefixed property; the unprefixed one is Safari-preview-only per BCD.
160
+ */
161
+ interactive: {
162
+ declarations: {
163
+ cursor: "pointer",
164
+ "-webkit-user-select": "none",
165
+ "user-select": "none",
166
+ "-webkit-tap-highlight-color": "transparent"
167
+ }
168
+ },
169
+ /**
170
+ * focusRing — keyboard focus indicator.
171
+ * :focus-visible — keyboard users see it, mouse users do not.
172
+ */
173
+ focusRing: {
174
+ declarations: {
175
+ outline: "none"
176
+ },
177
+ pseudos: {
178
+ ":focus-visible": {
179
+ outline: "var(--nave-border-width-focus) solid var(--nave-color-border-focus)",
180
+ "outline-offset": "2px"
181
+ }
182
+ }
183
+ },
184
+ /**
185
+ * disabledState — visual + behavioural disabled treatment.
186
+ * Applies from either the native `disabled` attribute or
187
+ * `aria-disabled="true"`. On the aria-disabled branch the element stays
188
+ * focusable by design: this atom only blocks pointer activation
189
+ * (pointer-events: none), so the component's own activation handler must
190
+ * also check the attribute and no-op on Enter and Space, since CSS cannot
191
+ * prevent keyboard activation.
192
+ *
193
+ * Expresses disablement through the dedicated disabled colour tokens
194
+ * (content.disabled, border.disabled) rather than a blanket opacity: a
195
+ * uniform dim composites everything the element paints, including a
196
+ * focusRing outline on the aria-disabled branch.
197
+ * `color` also carries inline icons (reset.css's `svg:not([fill])
198
+ * { fill: currentcolor }`); `border-color` is inert on a borderless
199
+ * element. Deliberately silent on `background-color`: the atom cannot
200
+ * know whether the element is a filled control, a ghost button, a link
201
+ * or a label, so a filled control's disabled fill is a component
202
+ * obligation, not this atom's.
203
+ *
204
+ * No `cursor` here: `pointer-events: none` stops the element from ever being hit-tested,
205
+ * so a `cursor` declared on it can never paint (the browser resolves the pointer against
206
+ * whatever is underneath instead). That is equally true of reset.css's
207
+ * `[disabled], [aria-disabled='true'] { cursor: not-allowed }` on an element carrying this
208
+ * atom: while the atom applies, no cursor paints on that element at all. The reset rule
209
+ * serves the disabled elements that do NOT carry this atom. What is removed here is a
210
+ * declaration that never rendered, so nothing a user sees changes.
211
+ */
212
+ disabledState: {
213
+ declarations: {},
214
+ pseudos: {
215
+ ':disabled, [aria-disabled="true"]': {
216
+ color: "var(--nave-color-content-disabled)",
217
+ "border-color": "var(--nave-color-border-disabled)",
218
+ "pointer-events": "none"
219
+ }
220
+ }
221
+ },
222
+ // ── Visual ─────────────────────────────────────────────────────────────────
223
+ rounded: {
224
+ declarations: { "border-radius": "var(--nave-radius-control)" }
225
+ },
226
+ roundedCard: {
227
+ declarations: { "border-radius": "var(--nave-radius-card)" }
228
+ },
229
+ roundedFull: {
230
+ declarations: { "border-radius": "var(--nave-radius-full)" }
231
+ },
232
+ border: {
233
+ declarations: {
234
+ border: "var(--nave-border-width-sm) solid var(--nave-color-border-default)"
235
+ }
236
+ },
237
+ overflowHidden: {
238
+ declarations: { overflow: "hidden" }
239
+ },
240
+ overflowAuto: {
241
+ declarations: { overflow: "auto" }
242
+ },
243
+ // ── Transition ─────────────────────────────────────────────────────────────
244
+ transition: {
245
+ declarations: {
246
+ "transition-property": "color, background-color, border-color, opacity, box-shadow",
247
+ "transition-duration": "var(--nave-motion-duration-base)",
248
+ "transition-timing-function": "var(--nave-motion-easing-standard)"
249
+ }
250
+ },
251
+ // ── Containment ────────────────────────────────────────────────────────────
252
+ //
253
+ // container establishes a containment context so that child components
254
+ // can use @container queries to respond to available space rather than
255
+ // viewport size. This is the correct tool for component-level responsiveness.
256
+ //
257
+ // Usage:
258
+ // <div className={styles.cardWrapper}> ← apply container atom here
259
+ // <Card /> ← Card uses @container internally
260
+ // </div>
261
+ //
262
+ // Named containers:
263
+ // If you need to query a specific named container, define it in your
264
+ // component's CSS Module directly — the container shorthand with a name
265
+ // requires a string value that cannot be expressed as a static atom:
266
+ // .wrapper { container: my-sidebar / inline-size; }
267
+ //
268
+ // container-type: inline-size is the correct default for most cases.
269
+ // It queries the inline (horizontal) dimension only, which is what
270
+ // almost all responsive component layouts need. Using `size` queries
271
+ // both dimensions and requires the container to have a known block size,
272
+ // which is rarely what you want for standard flow layout.
273
+ /**
274
+ * container
275
+ * Establishes an inline-size containment context.
276
+ * Apply to the wrapper of any component that uses @container queries.
277
+ * Required — container queries have no effect without a containment ancestor.
278
+ */
279
+ container: {
280
+ declarations: {
281
+ "container-type": "inline-size"
282
+ }
283
+ },
284
+ // ── Responsive ─────────────────────────────────────────────────────────────
285
+ //
286
+ // Philosophy: mobile-first. All responsive atoms start from the smallest
287
+ // context and expand upward using min-width (>=) queries, except phoneOnly
288
+ // which is the one legitimate max-width exception — it targets a specific
289
+ // device class, not "below X."
290
+ //
291
+ // Media strings come from @navecss/tokens/breakpoints.
292
+ // Never write breakpoint values as magic numbers here or in consumer atoms.
293
+ //
294
+ // Naming: [behaviour]-[when]
295
+ // hide* — visibility
296
+ // stack* — flex-direction change
297
+ // wFull* — width change
298
+ // [when] — matches the media export name (phoneOnly, tabletPortraitUp, etc.)
299
+ //
300
+ // Why no show* atoms?
301
+ // Restoring display requires knowing the original value (block, flex, grid).
302
+ // That context belongs in the component. Use hide* on the element that has
303
+ // a replacement, rather than show* on the replacement itself.
304
+ //
305
+ // Why only phoneOnly for stack* and wFull*?
306
+ // Stack and full-width behaviours applied above phone are layout decisions
307
+ // that belong in component-specific consumer atoms, not shared utilities.
308
+ // The built-in set models good mobile-first habits — it doesn't provide
309
+ // shortcuts around them.
310
+ /**
311
+ * hidePhoneOnly
312
+ * Hidden on phone. Visible on tablet portrait and above.
313
+ * Use for: elements that have a dedicated phone replacement.
314
+ */
315
+ hidePhoneOnly: {
316
+ declarations: {},
317
+ media: {
318
+ [media.phoneOnly]: {
319
+ declarations: { display: "none" }
320
+ }
321
+ }
322
+ },
323
+ /**
324
+ * hideTabletPortraitUp
325
+ * Hidden on tablet portrait and above. Visible on phone only.
326
+ * Use for: mobile-only elements (hamburger triggers, bottom nav, etc.)
327
+ */
328
+ hideTabletPortraitUp: {
329
+ declarations: {},
330
+ media: {
331
+ [media.tabletPortraitUp]: {
332
+ declarations: { display: "none" }
333
+ }
334
+ }
335
+ },
336
+ /**
337
+ * hideTabletLandscapeUp
338
+ * Hidden on tablet landscape and above. Visible on phone and tablet portrait.
339
+ * Use for: compact tablet navigation that gives way to a full desktop nav.
340
+ */
341
+ hideTabletLandscapeUp: {
342
+ declarations: {},
343
+ media: {
344
+ [media.tabletLandscapeUp]: {
345
+ declarations: { display: "none" }
346
+ }
347
+ }
348
+ },
349
+ /**
350
+ * hideDesktopUp
351
+ * Hidden on desktop and above. Visible on tablet landscape and below.
352
+ * Use for: mobile/tablet UI that has a desktop structural replacement.
353
+ */
354
+ hideDesktopUp: {
355
+ declarations: {},
356
+ media: {
357
+ [media.desktopUp]: {
358
+ declarations: { display: "none" }
359
+ }
360
+ }
361
+ },
362
+ /**
363
+ * stackPhoneOnly
364
+ * flex-direction: column on phone. Assumes the element is display: flex.
365
+ * Use for: button groups, form rows, icon+label pairs that stack on mobile.
366
+ */
367
+ stackPhoneOnly: {
368
+ declarations: {},
369
+ media: {
370
+ [media.phoneOnly]: {
371
+ declarations: { "flex-direction": "column" }
372
+ }
373
+ }
374
+ },
375
+ /**
376
+ * wFullPhoneOnly
377
+ * width: 100% on phone only.
378
+ * Use for: buttons and inputs that should be full-width on mobile.
379
+ */
380
+ wFullPhoneOnly: {
381
+ declarations: {},
382
+ media: {
383
+ [media.phoneOnly]: {
384
+ declarations: { width: "100%" }
385
+ }
386
+ }
387
+ }
388
+ };
389
+ var toClassName = (name) => `nave-${name.replaceAll(/([A-Z])/g, "-$1").toLowerCase()}`;
390
+ var atomClassMap = Object.fromEntries(
391
+ Object.keys(atoms).map((name) => [name, toClassName(name)])
392
+ );
393
+
394
+ export { atomClassMap, atoms, toClassName };
package/dist/cx.d.ts ADDED
@@ -0,0 +1,72 @@
1
+ import { AtomName } from './atoms.js';
2
+
3
+ /**
4
+ * cx — typed atomic class composer for Nave.
5
+ *
6
+ * Two channels, because the two things they carry are different:
7
+ *
8
+ * cx(...) — Nave built-in atoms only. Typed, autocompleted, and an
9
+ * unknown name is a compile error.
10
+ * cx.raw(...) — any class string, returned untouched. The declared way to
11
+ * step outside the system.
12
+ *
13
+ * Both accept falsy arguments (undefined, null, false) and filter them out, so
14
+ * conditional classes work the same on either side. Both return a
15
+ * space-separated class string for className={}.
16
+ *
17
+ * Validation, not just autocomplete:
18
+ * cx() takes AtomName, so a typo'd name ('interactve'), a name typed from
19
+ * the CSS side ('sr-only' — the atom key is srOnly and the emitted class is
20
+ * nave-sr-only; neither spelling is the key), and a class from anywhere
21
+ * outside Nave are all refused by your own tsc, with nothing installed from
22
+ * us. This is TypeScript only: a JavaScript consumer gets none of it.
23
+ *
24
+ * Composing with your own classes:
25
+ * A CSS Module hash is a string, not an atom, so it no longer goes inside
26
+ * cx(). Compose in the template literal, the way the @nave directive
27
+ * composes in the rule body:
28
+ *
29
+ * className={`${cx('interactive', 'focusRing')} ${styles.root}`}
30
+ *
31
+ * and reach for cx.raw() when the class comes from outside any system Nave
32
+ * can see — a legacy global class, a third-party widget's class:
33
+ *
34
+ * className={`${cx('interactive')} ${cx.raw('legacy-card')}`}
35
+ *
36
+ * A CONDITIONAL class goes inside a call, never into a slot: cx.raw()
37
+ * filters a falsy argument out, while `${isActive && styles.active}`
38
+ * interpolates the string 'false' into your className.
39
+ *
40
+ * An atom name held in a variable needs a literal type. `const n = 'flex'`
41
+ * is one; a let binding, an array element or an object property widens to
42
+ * string, so annotate it with the exported AtomName type (or `as const`).
43
+ *
44
+ * Why cx.raw() never maps:
45
+ * cx() resolves an atom name to that atom's global class, so cx('container')
46
+ * is 'nave-container'. cx.raw() does not consult the atom map at all, so
47
+ * cx.raw('container') is the literal 'container'. That is what makes it
48
+ * impossible for Nave to shadow one of your own class names on this channel,
49
+ * rather than merely documented — and the names at issue are the ordinary
50
+ * ones: container, hidden, grid, flex, block, border, rounded, transition,
51
+ * relative, absolute, gap, truncate, interactive.
52
+ *
53
+ * What is NOT checked:
54
+ * Nothing here looks at the rest of the className attribute. A bare string
55
+ * sitting beside these calls is not seen by anything, so cx.raw() is a
56
+ * DECLARED escape channel (greppable: `grep -r 'cx.raw'`), not an enforced
57
+ * one.
58
+ *
59
+ * Note on consumer atoms:
60
+ * Atoms defined via navePlugin({ extend }) are @nave-directive only.
61
+ * They do not generate global CSS classes and are not available in cx().
62
+ * cx() covers Nave built-in atoms only.
63
+ */
64
+
65
+ type Falsy = false | null | undefined;
66
+ interface Cx {
67
+ (...args: (AtomName | Falsy)[]): string;
68
+ raw: (...args: (Falsy | string)[]) => string;
69
+ }
70
+ declare const cx: Cx;
71
+
72
+ export { AtomName, cx };
package/dist/cx.js ADDED
@@ -0,0 +1,9 @@
1
+ import { atomClassMap } from './chunk-BVON7XKC.js';
2
+
3
+ // src/cx.ts
4
+ var cx = (...args) => args.filter(Boolean).map(
5
+ (arg) => Object.hasOwn(atomClassMap, arg) ? atomClassMap[arg] : arg
6
+ ).join(" ");
7
+ cx.raw = (...args) => args.filter(Boolean).join(" ");
8
+
9
+ export { cx };
package/dist/index.css ADDED
@@ -0,0 +1,48 @@
1
+ /*
2
+ * Nave Design System — core entry point
3
+ * Import this once in your app, before any stylesheet of yours that
4
+ * declares a layer.
5
+ *
6
+ * The @layer statement below is the cascade contract: seven names across five
7
+ * top-level layers (tokens, reset, atomic, components, overrides), each name
8
+ * beating every name before it. Do not reorder. The contract holds only if
9
+ * this statement is the first @layer declaration the document sees: a layer
10
+ * your CSS declares earlier keeps that earlier position, and the order
11
+ * inverts. Author into a sublayer, never into a bare parent (a parent
12
+ * outranks its own sublayers). Unlayered CSS beats every layer. All of this
13
+ * is for normal declarations: !important reverses layer order, which the
14
+ * reset uses on purpose. Full contract:
15
+ * https://github.com/navecss/navecss/blob/main/docs/04-adr/0003-layer-cascade-contract.md
16
+ *
17
+ * tokens.defaults → CSS custom properties (read-only, set by @navecss/tokens)
18
+ * tokens.presets → RESERVED: 0.1.0 ships zero presets, but the
19
+ * slot is declared now so a preset always beats the defaults
20
+ * regardless of import order, and a consumer's `overrides`
21
+ * always beats a preset. Retrofitting this slot later would be
22
+ * a breaking change to the published cascade contract.
23
+ * A project's own `navecss-tokens build` output lands here too.
24
+ * reset → Cross-browser normalisation
25
+ * atomic → Global atom classes that cx() names: defaults, below
26
+ * component CSS (@nave inlines into your own rule instead)
27
+ * components.nave → Components Nave publishes
28
+ * components.consumer → Your component CSS: @layer components.consumer { … }
29
+ * overrides → Your deliberate exceptions to everything above
30
+ */
31
+ @layer tokens.defaults, tokens.presets, reset, atomic, components.nave, components.consumer, overrides;
32
+
33
+ /* Token layer — all CSS custom properties.
34
+ * Imported plain: tokens.css declares its own `@layer tokens.defaults` so that
35
+ * consumers importing @navecss/tokens/css directly are layered correctly too.
36
+ * Adding layer(tokens.defaults) here would nest it a second time. */
37
+ @import url('@navecss/tokens/css');
38
+
39
+ /* Reset layer. Imported plain: dist/reset.css now declares its own
40
+ * `@layer reset` so that consumers importing ./reset directly are layered
41
+ * correctly too. Adding layer(reset) here would nest it a second time. */
42
+ @import url('./reset.css');
43
+
44
+ /* Atomic layer. Imported plain: dist/atomic.css now declares its own
45
+ * `@layer atomic` so that consumers importing ./atomic directly are
46
+ * layered correctly too. Adding layer(atomic) here would nest it a second
47
+ * time. */
48
+ @import url('./atomic.css');
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Nave Design System — the @layer order statement, alone.
3
+ *
4
+ * Import this first, before any other stylesheet. CSS fixes a layer's
5
+ * position the first time the page declares it, so if a stylesheet of yours
6
+ * that declares a layer loads before this statement, that layer's position is
7
+ * fixed first and Nave's order inverts with no error. Imported first, this one
8
+ * line keeps the order wherever the rest of Nave and your own stylesheets land
9
+ * in the import graph. Importing '@navecss/core' afterwards is correct: its
10
+ * own copy of this statement is identical and changes nothing.
11
+ *
12
+ * Full contract:
13
+ * https://github.com/navecss/navecss/blob/main/docs/04-adr/0003-layer-cascade-contract.md
14
+ */
15
+ @layer tokens.defaults, tokens.presets, reset, atomic, components.nave, components.consumer, overrides;
@@ -0,0 +1,52 @@
1
+ /*
2
+ * Nave Design System, entry point for projects that generate their own tokens.
3
+ *
4
+ * Import this INSTEAD OF '@navecss/core' if you generate your Nave token
5
+ * layer yourself and commit it into your project. If you use Nave's shipped
6
+ * token values (the normal case, and what the README shows), import
7
+ * '@navecss/core' and ignore this file.
8
+ *
9
+ * @import '@navecss/core/no-tokens';
10
+ * @import './your-generated-tokens.css';
11
+ *
12
+ * WHAT YOU MUST SUPPLY. This entry ships Nave's reset and atomic layers and
13
+ * imports no token values at all, so every '--nave-' custom property those
14
+ * two layers read has to come from your own stylesheet, not only the colours.
15
+ * A property you do not define is not an error: the declaration that reads it is
16
+ * discarded, and the property does NOT fall back to whatever Nave's earlier rules
17
+ * set for it, so the element silently loses that piece of styling.
18
+ * To pick one that matters, the focus outline on '.nave-focus-ring' is built
19
+ * from '--nave-border-width-focus', so a token layer that defines every colour
20
+ * and not that width leaves an element carrying that class with no visible
21
+ * outline when it is focused.
22
+ *
23
+ * Nave ships a contract manifest listing the custom properties it requires
24
+ * ('@navecss/tokens/core-contract'). The manifest is generated by scanning
25
+ * the source behind the two layers this entry ships, across every '--nave-'
26
+ * property that source reads and not the colour namespace alone. What it
27
+ * checks is NAMES: a clean run means every one of those names is declared in
28
+ * your layer, and says nothing about the value you gave it.
29
+ *
30
+ * WHY A SEPARATE ENTRY. The default entry imports Nave's own token stylesheet.
31
+ * If you load your own generated token layer as well, the document carries two
32
+ * sets of the same custom properties. Your own values win, but any property
33
+ * your layer does not define falls back to Nave's without saying so, which
34
+ * makes an incomplete token layer look complete. This entry imports no token
35
+ * stylesheet, so what you supply is all there is, and each custom property is
36
+ * registered exactly once.
37
+ *
38
+ * Everything else is identical to the default entry: the same @layer order
39
+ * statement, the same reset and atomic imports.
40
+ */
41
+ @layer tokens.defaults, tokens.presets, reset, atomic, components.nave, components.consumer, overrides;
42
+
43
+ /* Reset layer. Imported plain: dist/reset.css declares its own
44
+ * `@layer reset` so that consumers importing ./reset directly are layered
45
+ * correctly too. Adding layer(reset) here would nest it a second time. */
46
+ @import url('./reset.css');
47
+
48
+ /* Atomic layer. Imported plain: dist/atomic.css declares its own
49
+ * `@layer atomic` so that consumers importing ./atomic directly are
50
+ * layered correctly too. Adding layer(atomic) here would nest it a second
51
+ * time. */
52
+ @import url('./atomic.css');
@@ -0,0 +1,64 @@
1
+ import { Plugin } from 'postcss';
2
+ import { AtomDefinition } from './atoms.js';
3
+
4
+ /**
5
+ * Nave PostCSS plugin — resolves @nave directives.
6
+ *
7
+ * Inlines atomic utility declarations at build time.
8
+ * Pseudo rules, @media and @container blocks are emitted as native CSS
9
+ * nesting inside the parent rule (`&:focus-visible { … }`), never hoisted
10
+ * out as sibling rules. Browser floor: Baseline 2024.
11
+ * See https://github.com/navecss/navecss/blob/main/docs/04-adr/0001-native-css-nesting.md
12
+ *
13
+ * Setup:
14
+ * import { navePlugin } from '@navecss/core/postcss'
15
+ * navePlugin() // Nave atoms only, fails build on unknown atom
16
+ * navePlugin({ extend: myAtoms }) // Nave + consumer atoms
17
+ * navePlugin({ onUnknown: 'warn' }) // Log and skip instead of failing the build
18
+ *
19
+ * Consumer atoms:
20
+ * import type { AtomDefinition } from '@navecss/core/postcss'
21
+ * import { media } from '@navecss/tokens/breakpoints'
22
+ *
23
+ * const myAtoms: Record<string, AtomDefinition> = {
24
+ * primaryButton: {
25
+ * declarations: {
26
+ * background: 'var(--nave-color-action-primary)',
27
+ * color: 'var(--nave-color-on-action-primary)',
28
+ * padding: 'var(--nave-spacing-control-md) var(--nave-spacing-control-lg)',
29
+ * 'border-radius': 'var(--nave-radius-control)',
30
+ * },
31
+ * pseudos: {
32
+ * ':hover': { background: 'var(--nave-color-action-primary-hover)' },
33
+ * },
34
+ * media: {
35
+ * [media.phoneOnly]: {
36
+ * declarations: { width: '100%' },
37
+ * },
38
+ * },
39
+ * },
40
+ * }
41
+ */
42
+
43
+ interface NavePluginOptions {
44
+ /**
45
+ * Consumer-defined atoms merged with Nave built-in atoms.
46
+ * Consumer atoms win on name collision — your system owns its vocabulary.
47
+ * These atoms resolve via @nave only. No global class. Not available in cx().
48
+ */
49
+ extend?: Record<string, AtomDefinition>;
50
+ /**
51
+ * Behaviour on an unknown atom name, or a @nave directive that names no
52
+ * atom at all.
53
+ * 'warn' — log and skip
54
+ * 'error' — throw, failing the build (default)
55
+ * 'ignore' — silently skip
56
+ */
57
+ onUnknown?: 'warn' | 'error' | 'ignore';
58
+ }
59
+ declare const navePlugin: {
60
+ (options?: NavePluginOptions): Plugin;
61
+ postcss: boolean;
62
+ };
63
+
64
+ export { AtomDefinition, type NavePluginOptions, navePlugin };