@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 +109 -0
- package/CONSUMER-ATOMS.md +332 -0
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/dist/atomic.css +288 -0
- package/dist/atoms.d.ts +413 -0
- package/dist/atoms.js +1 -0
- package/dist/chunk-BVON7XKC.js +394 -0
- package/dist/cx.d.ts +72 -0
- package/dist/cx.js +9 -0
- package/dist/index.css +48 -0
- package/dist/layers.css +15 -0
- package/dist/no-tokens.css +52 -0
- package/dist/postcss.d.ts +64 -0
- package/dist/postcss.js +228 -0
- package/dist/reset.css +346 -0
- package/package.json +93 -0
|
@@ -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');
|
package/dist/layers.css
ADDED
|
@@ -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 };
|