@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/ATOMS.md ADDED
@@ -0,0 +1,109 @@
1
+ # ATOMS.md
2
+
3
+ Generated from `src/atoms.ts`; do not edit by hand.
4
+
5
+ Every built-in atom Nave ships: the name as written in `@nave` and `cx()`, the global class
6
+ it emits, the declarations it applies, and its pseudo-class, `@media` or `@container`
7
+ variants, if any.
8
+
9
+ ## Display
10
+
11
+ | Atom | Class | Declarations | Variants |
12
+ | ------------- | ------------------- | ------------------------ | -------- |
13
+ | `flex` | `nave-flex` | `display: flex;` | — |
14
+ | `inlineFlex` | `nave-inline-flex` | `display: inline-flex;` | — |
15
+ | `grid` | `nave-grid` | `display: grid;` | — |
16
+ | `block` | `nave-block` | `display: block;` | — |
17
+ | `inlineBlock` | `nave-inline-block` | `display: inline-block;` | — |
18
+ | `hidden` | `nave-hidden` | `display: none;` | — |
19
+
20
+ ## Flex
21
+
22
+ | Atom | Class | Declarations | Variants |
23
+ | ---------------- | ---------------------- | -------------------------------------- | -------- |
24
+ | `flexCol` | `nave-flex-col` | `flex-direction: column;` | — |
25
+ | `flexWrap` | `nave-flex-wrap` | `flex-wrap: wrap;` | — |
26
+ | `itemsCenter` | `nave-items-center` | `align-items: center;` | — |
27
+ | `itemsStart` | `nave-items-start` | `align-items: flex-start;` | — |
28
+ | `itemsEnd` | `nave-items-end` | `align-items: flex-end;` | — |
29
+ | `justifyCenter` | `nave-justify-center` | `justify-content: center;` | — |
30
+ | `justifyBetween` | `nave-justify-between` | `justify-content: space-between;` | — |
31
+ | `justifyEnd` | `nave-justify-end` | `justify-content: flex-end;` | — |
32
+ | `flexGrow` | `nave-flex-grow` | `flex-grow: 1;` | — |
33
+ | `flexShrink0` | `nave-flex-shrink0` | `flex-shrink: 0;` | — |
34
+ | `gap` | `nave-gap` | `gap: var(--nave-spacing-content-md);` | — |
35
+
36
+ ## Position
37
+
38
+ | Atom | Class | Declarations | Variants |
39
+ | ----------- | ----------------- | --------------------- | -------- |
40
+ | `relative` | `nave-relative` | `position: relative;` | — |
41
+ | `absolute` | `nave-absolute` | `position: absolute;` | — |
42
+ | `insetFull` | `nave-inset-full` | `inset: 0;` | — |
43
+
44
+ ## Sizing
45
+
46
+ | Atom | Class | Declarations | Variants |
47
+ | ------- | ------------- | --------------- | -------- |
48
+ | `wFull` | `nave-w-full` | `width: 100%;` | — |
49
+ | `hFull` | `nave-h-full` | `height: 100%;` | — |
50
+ | `minW0` | `nave-min-w0` | `min-width: 0;` | — |
51
+
52
+ ## Typography
53
+
54
+ | Atom | Class | Declarations | Variants |
55
+ | ----------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
56
+ | `truncate` | `nave-truncate` | `overflow: hidden;`<br>`text-overflow: ellipsis;`<br>`white-space: nowrap;` | — |
57
+ | `srOnly` | `nave-sr-only` | `position: absolute;`<br>`width: 1px;`<br>`height: 1px;`<br>`padding: 0;`<br>`margin: -1px;`<br>`overflow: hidden;`<br>`clip-path: inset(50%);`<br>`white-space: nowrap;`<br>`border-width: 0;` | — |
58
+ | `srOnlyFocusable` | `nave-sr-only-focusable` | `position: absolute;`<br>`width: 1px;`<br>`height: 1px;`<br>`padding: 0;`<br>`margin: -1px;`<br>`overflow: hidden;`<br>`clip-path: inset(50%);`<br>`white-space: nowrap;`<br>`border-width: 0;` | `:focus-visible`<br>`:focus-within` |
59
+ | `noWrap` | `nave-no-wrap` | `white-space: nowrap;` | — |
60
+ | `breakWord` | `nave-break-word` | `overflow-wrap: break-word;` | — |
61
+ | `textLeft` | `nave-text-left` | `text-align: left;` | — |
62
+ | `textCenter` | `nave-text-center` | `text-align: center;` | — |
63
+ | `textRight` | `nave-text-right` | `text-align: right;` | — |
64
+
65
+ ## Interaction
66
+
67
+ | Atom | Class | Declarations | Variants |
68
+ | --------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
69
+ | `interactive` | `nave-interactive` | `cursor: pointer;`<br>`-webkit-user-select: none;`<br>`user-select: none;`<br>`-webkit-tap-highlight-color: transparent;` | — |
70
+ | `focusRing` | `nave-focus-ring` | `outline: none;` | `:focus-visible` |
71
+ | `disabledState` | `nave-disabled-state` | — | `:disabled, [aria-disabled="true"]` |
72
+
73
+ ## Visual
74
+
75
+ | Atom | Class | Declarations | Variants |
76
+ | ---------------- | ---------------------- | ----------------------------------------------------------------------------- | -------- |
77
+ | `rounded` | `nave-rounded` | `border-radius: var(--nave-radius-control);` | — |
78
+ | `roundedCard` | `nave-rounded-card` | `border-radius: var(--nave-radius-card);` | — |
79
+ | `roundedFull` | `nave-rounded-full` | `border-radius: var(--nave-radius-full);` | — |
80
+ | `border` | `nave-border` | `border: var(--nave-border-width-sm) solid var(--nave-color-border-default);` | — |
81
+ | `overflowHidden` | `nave-overflow-hidden` | `overflow: hidden;` | — |
82
+ | `overflowAuto` | `nave-overflow-auto` | `overflow: auto;` | — |
83
+
84
+ ## Transition
85
+
86
+ | Atom | Class | Declarations | Variants |
87
+ | ------------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
88
+ | `transition` | `nave-transition` | `transition-property: color, background-color, border-color, opacity, box-shadow;`<br>`transition-duration: var(--nave-motion-duration-base);`<br>`transition-timing-function: var(--nave-motion-easing-standard);` | — |
89
+
90
+ ## Containment
91
+
92
+ | Atom | Class | Declarations | Variants |
93
+ | ----------- | ---------------- | ------------------------------ | -------- |
94
+ | `container` | `nave-container` | `container-type: inline-size;` | — |
95
+
96
+ ## Responsive
97
+
98
+ | Atom | Class | Declarations | Variants |
99
+ | ----------------------- | ------------------------------- | ------------ | --------------------------- |
100
+ | `hidePhoneOnly` | `nave-hide-phone-only` | — | `@media (width < 37.5em)` |
101
+ | `hideTabletPortraitUp` | `nave-hide-tablet-portrait-up` | — | `@media (width >= 37.5em)` |
102
+ | `hideTabletLandscapeUp` | `nave-hide-tablet-landscape-up` | — | `@media (width >= 56.25em)` |
103
+ | `hideDesktopUp` | `nave-hide-desktop-up` | — | `@media (width >= 75em)` |
104
+ | `stackPhoneOnly` | `nave-stack-phone-only` | — | `@media (width < 37.5em)` |
105
+ | `wFullPhoneOnly` | `nave-w-full-phone-only` | — | `@media (width < 37.5em)` |
106
+
107
+ ## Pairing notes
108
+
109
+ - `truncate`: pairs with `minW0` on a flex or grid child, or the text never has a width to truncate against
@@ -0,0 +1,332 @@
1
+ # Consumer atoms
2
+
3
+ Nave's built-in atoms ([ATOMS.md](./ATOMS.md)) are a small set of universal utilities.
4
+ Your design system will accumulate patterns specific to your brand —
5
+ interactive behaviors, component conventions, responsive rules — that
6
+ don't belong in Nave's core but should be reusable across your codebase.
7
+
8
+ Consumer atoms are the answer. They extend the @nave directive with your
9
+ own vocabulary, follow the same AtomDefinition shape, and grow with your system.
10
+
11
+ ## Defining consumer atoms
12
+
13
+ The example below imports from `@navecss/tokens`, so add it as a direct dependency
14
+ of your own (`pnpm add -D @navecss/tokens`). Installed only as a dependency of
15
+ `@navecss/core`, it may not be importable from your own config: under pnpm it is
16
+ not.
17
+
18
+ ```ts
19
+ // src/design-system/atoms.ts
20
+ import type { AtomDefinition } from '@navecss/core/postcss'
21
+ import { media } from '@navecss/tokens/breakpoints'
22
+
23
+ export 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
+ 'font-weight': 'var(--nave-font-weight-medium)',
31
+ },
32
+ pseudos: {
33
+ ':hover': {
34
+ background: 'var(--nave-color-action-primary-hover)',
35
+ },
36
+ ':active': {
37
+ background: 'var(--nave-color-action-primary-active)',
38
+ },
39
+ },
40
+ media: {
41
+ // Always use media strings from @navecss/tokens/breakpoints.
42
+ // Never write breakpoint values as magic numbers.
43
+ [media.phoneOnly]: {
44
+ declarations: { width: '100%' },
45
+ },
46
+ },
47
+ },
48
+ }
49
+ ```
50
+
51
+ ## Registering with the plugin
52
+
53
+ ```ts
54
+ // vite.config.ts
55
+ import { defineConfig } from 'vite'
56
+ import { navePlugin } from '@navecss/core/postcss'
57
+ import { myAtoms } from './src/design-system/atoms'
58
+
59
+ export default defineConfig({
60
+ // your existing options, plugins included, stay as they are
61
+ css: { postcss: { plugins: [navePlugin({ extend: myAtoms })] } },
62
+ build: { cssTarget: ['chrome125', 'edge125', 'firefox128', 'safari18', 'ios18'] },
63
+ })
64
+ ```
65
+
66
+ `build.cssTarget` is the browser floor, in Vite's terms:
67
+ [PostCSS plugin setup](./README.md#postcss-plugin-setup) says what goes wrong
68
+ without it.
69
+
70
+ ## Using consumer atoms
71
+
72
+ Like the rest of your component CSS, the rule goes in `@layer components.consumer`,
73
+ and the directive expands inside it:
74
+
75
+ ```css
76
+ /* button.module.css */
77
+ @layer components.consumer {
78
+ .root {
79
+ @nave interactive focusRing transition primaryButton;
80
+ }
81
+ }
82
+ ```
83
+
84
+ Compiles to:
85
+
86
+ ```css
87
+ @layer components.consumer {
88
+ .root {
89
+ cursor: pointer;
90
+ -webkit-user-select: none;
91
+ user-select: none;
92
+ -webkit-tap-highlight-color: transparent;
93
+ outline: none;
94
+ transition-property: color, background-color, border-color, opacity, box-shadow;
95
+ transition-duration: var(--nave-motion-duration-base);
96
+ transition-timing-function: var(--nave-motion-easing-standard);
97
+ background: var(--nave-color-action-primary);
98
+ color: var(--nave-color-on-action-primary);
99
+ padding: var(--nave-spacing-control-md) var(--nave-spacing-control-lg);
100
+ border-radius: var(--nave-radius-control);
101
+ font-weight: var(--nave-font-weight-medium);
102
+ &:focus-visible {
103
+ outline: var(--nave-border-width-focus) solid var(--nave-color-border-focus);
104
+ outline-offset: 2px;
105
+ }
106
+ &:hover {
107
+ background: var(--nave-color-action-primary-hover);
108
+ }
109
+ &:active {
110
+ background: var(--nave-color-action-primary-active);
111
+ }
112
+ @media (width < 37.5em) {
113
+ & {
114
+ width: 100%;
115
+ }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ ## Rules and conventions
122
+
123
+ **Always use media strings from @navecss/tokens/breakpoints.**
124
+ Never write breakpoint values as magic numbers.
125
+ One source of truth — the token file — for every breakpoint in your system.
126
+
127
+ **Consumer atoms win on name collision with Nave atoms.**
128
+ Your system owns its vocabulary. If you need to override a Nave atom's
129
+ behaviour, name your atom the same key. Use this power deliberately.
130
+
131
+ **Consumer atoms are @nave-directive only.**
132
+ They do not generate global CSS classes and are not available in cx().
133
+ cx() is for Nave's built-in atoms: passing a name that is not one of them
134
+ is a TypeScript error rather than the silent pass-through it used to be.
135
+ The exception is a key you deliberately collided with a built-in (above):
136
+ cx() still accepts it and still returns the BUILT-IN atom's global class,
137
+ never your override, so compose that one with @nave in the rule body.
138
+
139
+ **A `pseudos` key compounds onto the element, not a descendant of it.**
140
+ `':hover'` compiles to `&:hover`, matching the element
141
+ itself. A key can also be a comma-separated selector list
142
+ (`':disabled, [aria-disabled="true"]'`), and every branch is anchored with
143
+ `&` automatically, so each branch still matches the element itself rather
144
+ than a descendant of it. A branch that already contains `&`
145
+ (`'&[aria-disabled="true"]'`, or an author-written relative branch like
146
+ `'.foo &'`) is left exactly as authored, since it is already relative to the
147
+ parent selector by construction. One deliberate consequence: **leading
148
+ whitespace in a key is not honoured as a descendant combinator.** `' .icon'`
149
+ still compounds onto the element (`&.icon`, once trimmed), it does not
150
+ select a `.icon` descendant — write a real descendant rule directly in your
151
+ CSS Module if that is what you need, not as a `pseudos` key.
152
+
153
+ **Curate your atoms.ts deliberately.**
154
+ Over time this file becomes the precise record of every reusable interactive
155
+ pattern in your design system. New team members read it to understand the
156
+ system's conventions. Designers reference atom names in handoff. Treat it
157
+ with the same care as your tokens.json.
158
+
159
+ **Mobile-first always.**
160
+ Follow the same convention as Nave's built-in responsive atoms.
161
+ Start from the smallest context and expand upward.
162
+ The only legitimate max-width query is phoneOnly.
163
+
164
+ ## Container queries
165
+
166
+ Container queries let components respond to their container's available space
167
+ rather than the viewport. This is the correct tool for component-level
168
+ responsiveness — use media queries for page-level layout decisions.
169
+
170
+ ### Establishing a containment context
171
+
172
+ Apply the built-in `container` atom to any wrapper element whose children
173
+ should use `@container` queries:
174
+
175
+ ```css
176
+ /* card-wrapper.module.css */
177
+ @layer components.consumer {
178
+ .wrapper {
179
+ @nave container;
180
+ }
181
+ }
182
+ ```
183
+
184
+ ```tsx
185
+ <div className={styles.wrapper}>
186
+ <Card />
187
+ </div>
188
+ ```
189
+
190
+ ### Consumer atoms with container queries
191
+
192
+ Define component-specific container behavior in your consumer atoms.
193
+ Container thresholds are component-specific — there is no universal
194
+ "compact" width. Choose thresholds that match your component's content.
195
+
196
+ ```ts
197
+ // src/design-system/atoms.ts
198
+ import type { AtomDefinition } from '@navecss/core/postcss'
199
+
200
+ export const myAtoms: Record<string, AtomDefinition> = {
201
+ adaptiveCard: {
202
+ // Default: stacked layout (mobile-first, smallest context)
203
+ declarations: {
204
+ display: 'flex',
205
+ 'flex-direction': 'column',
206
+ gap: 'var(--nave-spacing-content-md)',
207
+ },
208
+ // When container has room: switch to horizontal
209
+ container: {
210
+ '(width >= 28rem)': {
211
+ declarations: {
212
+ 'flex-direction': 'row',
213
+ 'align-items': 'center',
214
+ },
215
+ },
216
+ },
217
+ },
218
+
219
+ compactDataRow: {
220
+ declarations: {
221
+ display: 'grid',
222
+ 'grid-template-columns': '1fr',
223
+ gap: 'var(--nave-spacing-content-sm)',
224
+ },
225
+ container: {
226
+ '(width >= 40rem)': {
227
+ declarations: {
228
+ 'grid-template-columns': 'repeat(3, 1fr)',
229
+ },
230
+ },
231
+ '(width >= 60rem)': {
232
+ declarations: {
233
+ 'grid-template-columns': 'repeat(5, 1fr)',
234
+ },
235
+ },
236
+ },
237
+ },
238
+ }
239
+ ```
240
+
241
+ ### Usage
242
+
243
+ ```css
244
+ /* card.module.css */
245
+ @layer components.consumer {
246
+ .root {
247
+ @nave adaptiveCard;
248
+ background: var(--nave-color-surface-raised);
249
+ border-radius: var(--nave-radius-card);
250
+ padding: var(--nave-spacing-content-md);
251
+ }
252
+ }
253
+ ```
254
+
255
+ Compiles to:
256
+
257
+ ```css
258
+ @layer components.consumer {
259
+ .root {
260
+ display: flex;
261
+ flex-direction: column;
262
+ gap: var(--nave-spacing-content-md);
263
+ background: var(--nave-color-surface-raised);
264
+ border-radius: var(--nave-radius-card);
265
+ padding: var(--nave-spacing-content-md);
266
+ @container (width >= 28rem) {
267
+ & {
268
+ flex-direction: row;
269
+ align-items: center;
270
+ }
271
+ }
272
+ }
273
+ }
274
+ ```
275
+
276
+ ### Container query units
277
+
278
+ Inside a `@container` block, you can use container query length units
279
+ (`cqi`, `cqb`, `cqw`, `cqh`) to size elements relative to the container.
280
+ These are not atom concerns — use them directly in your CSS Module declarations.
281
+
282
+ ```css
283
+ @layer components.consumer {
284
+ .title {
285
+ font-size: clamp(var(--nave-font-size-md), 4cqi, var(--nave-font-size-2xl));
286
+ }
287
+ }
288
+ ```
289
+
290
+ ### Named containers
291
+
292
+ If you need to query a specific ancestor by name rather than the nearest
293
+ containment ancestor, define it directly in your CSS Module. The container
294
+ shorthand requires a name string that cannot be expressed as a static atom:
295
+
296
+ ```css
297
+ /* layout.module.css */
298
+ @layer components.consumer {
299
+ .sidebar {
300
+ container: sidebar / inline-size;
301
+ }
302
+ }
303
+
304
+ /* widget.module.css — targets the named sidebar container specifically */
305
+ @layer components.consumer {
306
+ .root {
307
+ /* base styles */
308
+ }
309
+
310
+ /* This rule cannot be expressed as a consumer atom: write it directly */
311
+ @container sidebar (width < 20rem) {
312
+ .root {
313
+ display: none;
314
+ }
315
+ }
316
+ }
317
+ ```
318
+
319
+ ### Media queries vs container queries — when to use which
320
+
321
+ | Concern | Use |
322
+ | ------------------------------------------------- | --------------------------------- |
323
+ | Page skeleton — columns appearing, nav collapsing | `@media` + breakpoint atoms |
324
+ | User preference — dark mode, reduced motion | `@media` |
325
+ | Component layout — card stacking, grid reflow | `@container` + consumer atoms |
326
+ | Typography scaling relative to container | `@container` + cqi units |
327
+ | Sidebar-aware component behavior | `@container` with named container |
328
+
329
+ The clearest rule: if the component would behave differently in a sidebar
330
+ vs in a main column at the same viewport width, use a container query.
331
+ If the component always behaves the same way regardless of where it sits,
332
+ use a media query.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nave Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.