@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
package/dist/postcss.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { atoms } from './chunk-BVON7XKC.js';
|
|
2
|
+
import postcss from 'postcss';
|
|
3
|
+
|
|
4
|
+
// src/selector-utils.ts
|
|
5
|
+
function nextQuote(quote, char) {
|
|
6
|
+
if (quote) return char === quote ? void 0 : quote;
|
|
7
|
+
return char === '"' || char === "'" ? char : quote;
|
|
8
|
+
}
|
|
9
|
+
function depthDelta(quote, char) {
|
|
10
|
+
if (quote) return 0;
|
|
11
|
+
if (char === "(" || char === "[") return 1;
|
|
12
|
+
if (char === ")" || char === "]") return -1;
|
|
13
|
+
return 0;
|
|
14
|
+
}
|
|
15
|
+
function consumeSplitChar(state, char) {
|
|
16
|
+
const { branches, depth, quote, isEscaped, current } = state;
|
|
17
|
+
if (isEscaped) {
|
|
18
|
+
return { branches, depth, quote, isEscaped: false, current: current + char };
|
|
19
|
+
}
|
|
20
|
+
if (char === "\\") {
|
|
21
|
+
return { branches, depth, quote, isEscaped: true, current: current + char };
|
|
22
|
+
}
|
|
23
|
+
const nextDepth = depth + depthDelta(quote, char);
|
|
24
|
+
if (!quote && char === "," && nextDepth === 0) {
|
|
25
|
+
return {
|
|
26
|
+
branches: [...branches, current],
|
|
27
|
+
depth: nextDepth,
|
|
28
|
+
quote,
|
|
29
|
+
isEscaped: false,
|
|
30
|
+
current: ""
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
branches,
|
|
35
|
+
depth: nextDepth,
|
|
36
|
+
quote: nextQuote(quote, char),
|
|
37
|
+
isEscaped: false,
|
|
38
|
+
current: current + char
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function splitTopLevel(selectorList) {
|
|
42
|
+
let state = {
|
|
43
|
+
branches: [],
|
|
44
|
+
depth: 0,
|
|
45
|
+
quote: void 0,
|
|
46
|
+
isEscaped: false,
|
|
47
|
+
current: ""
|
|
48
|
+
};
|
|
49
|
+
for (const char of selectorList) state = consumeSplitChar(state, char);
|
|
50
|
+
return [...state.branches, state.current];
|
|
51
|
+
}
|
|
52
|
+
function hasUnquotedAmpersand(branch) {
|
|
53
|
+
let quote;
|
|
54
|
+
let isEscaped = false;
|
|
55
|
+
for (const char of branch) {
|
|
56
|
+
if (isEscaped) {
|
|
57
|
+
isEscaped = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (char === "\\") {
|
|
61
|
+
isEscaped = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!quote && char === "&") return true;
|
|
65
|
+
quote = nextQuote(quote, char);
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
function anchorSelectorList(selectorList) {
|
|
70
|
+
return splitTopLevel(selectorList).map((branch) => {
|
|
71
|
+
const trimmed = branch.trim();
|
|
72
|
+
if (trimmed === "") {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`anchorSelectorList: empty selector branch in pseudo key "${selectorList}". Remove the stray or trailing comma: an empty branch would otherwise compile to a bare "&", matching the atom unconditionally.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return hasUnquotedAmpersand(trimmed) ? trimmed : `&${trimmed}`;
|
|
78
|
+
}).join(", ");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/postcss-nested-builders.ts
|
|
82
|
+
var DIRECTIVE = "nave";
|
|
83
|
+
function buildNestedRule(selector, decls) {
|
|
84
|
+
const rule = postcss.rule({ selector });
|
|
85
|
+
for (const [prop, value] of Object.entries(decls)) {
|
|
86
|
+
rule.append(postcss.decl({ prop, value }));
|
|
87
|
+
}
|
|
88
|
+
return rule;
|
|
89
|
+
}
|
|
90
|
+
function buildPseudoRules(pseudos) {
|
|
91
|
+
return Object.entries(pseudos).map(
|
|
92
|
+
([pseudo, decls]) => buildNestedRule(anchorSelectorList(pseudo), decls)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
function buildInnerRules(block) {
|
|
96
|
+
const rules = [];
|
|
97
|
+
if (block.declarations) rules.push(buildNestedRule("&", block.declarations));
|
|
98
|
+
if (block.pseudos) rules.push(...buildPseudoRules(block.pseudos));
|
|
99
|
+
return rules;
|
|
100
|
+
}
|
|
101
|
+
function buildAtBlock(atName, condition, block) {
|
|
102
|
+
const inner = buildInnerRules(block);
|
|
103
|
+
if (inner.length === 0) return void 0;
|
|
104
|
+
const node = postcss.atRule({ name: atName, params: condition });
|
|
105
|
+
for (const rule of inner) node.append(rule);
|
|
106
|
+
return node;
|
|
107
|
+
}
|
|
108
|
+
function buildNested(atom) {
|
|
109
|
+
const nodes = [];
|
|
110
|
+
if (atom.pseudos) nodes.push(...buildPseudoRules(atom.pseudos));
|
|
111
|
+
for (const atName of ["media", "container"]) {
|
|
112
|
+
const blocks = atom[atName] ?? {};
|
|
113
|
+
for (const [condition, block] of Object.entries(blocks)) {
|
|
114
|
+
const node = buildAtBlock(atName, condition, block);
|
|
115
|
+
if (node) nodes.push(node);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return nodes;
|
|
119
|
+
}
|
|
120
|
+
function isNestedNode(node) {
|
|
121
|
+
return node.type === "rule" || node.type === "atrule" && node.name !== DIRECTIVE;
|
|
122
|
+
}
|
|
123
|
+
function isFollowingNestedNode(rule, atRule) {
|
|
124
|
+
return rule.nodes.slice(0, rule.index(atRule)).some((node) => isNestedNode(node));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/postcss-node-utils.ts
|
|
128
|
+
function isInsideKeyframes(rule) {
|
|
129
|
+
let node = rule.parent;
|
|
130
|
+
while (node) {
|
|
131
|
+
if (node.type === "atrule" && /keyframes$/i.test(node.name)) return true;
|
|
132
|
+
node = node.parent;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
function stampSource(node, source) {
|
|
137
|
+
if (!source) return;
|
|
138
|
+
node.source = source;
|
|
139
|
+
node.walk((child) => {
|
|
140
|
+
child.source = source;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/postcss.ts
|
|
145
|
+
function reportUnknown(msg, ctx) {
|
|
146
|
+
const { atRule, onUnknown, result } = ctx;
|
|
147
|
+
if (onUnknown === "warn") {
|
|
148
|
+
atRule.warn(result, msg);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (onUnknown === "ignore") return;
|
|
152
|
+
throw atRule.error(msg);
|
|
153
|
+
}
|
|
154
|
+
function checkUnknownAtoms(names, validAtomNames, ctx) {
|
|
155
|
+
if (names.length === 0) {
|
|
156
|
+
reportUnknown("@nave: directive names no atom", ctx);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
for (const name of names) {
|
|
160
|
+
if (validAtomNames.has(name)) continue;
|
|
161
|
+
reportUnknown(
|
|
162
|
+
`@nave: unknown atom "${name}". Available: ${[...validAtomNames].join(", ")}`,
|
|
163
|
+
ctx
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function insertDeclarations(atRule, declNodes, isFollowingNested) {
|
|
168
|
+
if (declNodes.length === 0) return;
|
|
169
|
+
if (!isFollowingNested) {
|
|
170
|
+
for (const decl of declNodes) atRule.before(decl);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const wrapper = postcss.rule({ selector: "&" });
|
|
174
|
+
if (atRule.source) wrapper.source = atRule.source;
|
|
175
|
+
for (const decl of declNodes) wrapper.append(decl);
|
|
176
|
+
atRule.before(wrapper);
|
|
177
|
+
}
|
|
178
|
+
var navePlugin = (options = {}) => {
|
|
179
|
+
const { onUnknown = "error", extend = {} } = options;
|
|
180
|
+
const allAtoms = { ...atoms, ...extend };
|
|
181
|
+
const validAtomNames = new Set(
|
|
182
|
+
Object.entries(allAtoms).filter(([, definition]) => definition).map(([name]) => name)
|
|
183
|
+
);
|
|
184
|
+
return {
|
|
185
|
+
postcssPlugin: "postcss-nave",
|
|
186
|
+
AtRule(atRule, { result }) {
|
|
187
|
+
if (atRule.name !== DIRECTIVE) return;
|
|
188
|
+
const ctx = { atRule, onUnknown, result };
|
|
189
|
+
const parent = atRule.parent;
|
|
190
|
+
if (parent?.type !== "rule") {
|
|
191
|
+
reportUnknown("@nave must be the direct child of a CSS rule selector block", ctx);
|
|
192
|
+
atRule.remove();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const rule = parent;
|
|
196
|
+
if (isInsideKeyframes(rule)) {
|
|
197
|
+
reportUnknown("@nave cannot be used inside @keyframes", ctx);
|
|
198
|
+
atRule.remove();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const names = atRule.params.trim().split(/\s+/).filter(Boolean);
|
|
202
|
+
checkUnknownAtoms(names, validAtomNames, ctx);
|
|
203
|
+
const declNodes = [];
|
|
204
|
+
const nested = names.flatMap((name) => {
|
|
205
|
+
const atom = Object.hasOwn(allAtoms, name) ? allAtoms[name] : void 0;
|
|
206
|
+
if (!atom) return [];
|
|
207
|
+
if (typeof atom.declarations !== "object" || atom.declarations === null || Array.isArray(atom.declarations)) {
|
|
208
|
+
throw atRule.error(`@nave: atom "${name}" is registered without a declarations object`);
|
|
209
|
+
}
|
|
210
|
+
for (const [prop, value] of Object.entries(atom.declarations)) {
|
|
211
|
+
const decl = postcss.decl({ prop, value });
|
|
212
|
+
if (atRule.source) decl.source = atRule.source;
|
|
213
|
+
declNodes.push(decl);
|
|
214
|
+
}
|
|
215
|
+
return buildNested(atom);
|
|
216
|
+
});
|
|
217
|
+
insertDeclarations(atRule, declNodes, isFollowingNestedNode(rule, atRule));
|
|
218
|
+
atRule.remove();
|
|
219
|
+
for (const node of nested) {
|
|
220
|
+
stampSource(node, atRule.source);
|
|
221
|
+
rule.append(node);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
navePlugin.postcss = true;
|
|
227
|
+
|
|
228
|
+
export { navePlugin };
|
package/dist/reset.css
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Self-layered under @layer reset: this file ships as a published
|
|
3
|
+
* subpath export (./reset) and is imported directly by some consumers, so
|
|
4
|
+
* the layer guarantee has to be a property of the artifact rather than of
|
|
5
|
+
* index.css's import site.
|
|
6
|
+
*/
|
|
7
|
+
@layer tokens.defaults, tokens.presets, reset, atomic, components.nave, components.consumer, overrides;
|
|
8
|
+
|
|
9
|
+
@layer reset {
|
|
10
|
+
/*
|
|
11
|
+
* Nave Reset — opinionated modern cross-browser reset.
|
|
12
|
+
* Every rule fixes a real, documented cross-browser inconsistency.
|
|
13
|
+
* Tokens are available here because @layer tokens is declared first.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/* ── Box model ───────────────────────────────────────────────────────────────
|
|
17
|
+
* Border-box everywhere. The alternative is layout math errors.
|
|
18
|
+
*/
|
|
19
|
+
*,
|
|
20
|
+
*::before,
|
|
21
|
+
*::after {
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ── Root ────────────────────────────────────────────────────────────────────
|
|
26
|
+
* 1. Stop Safari on iPhone enlarging body text by itself (for example on an orientation
|
|
27
|
+
* change), so a Nave layout renders at the size it declares. Safari reads only the
|
|
28
|
+
* -webkit- prefixed property; the unprefixed one is the standard spelling. This does not
|
|
29
|
+
* take away a reader's own text-size setting in Safari, in Chrome from version 143, in
|
|
30
|
+
* Android WebView from version 145, or in Firefox: each applies that setting whatever the
|
|
31
|
+
* value here. Chrome no longer autosizes text, and Firefox for Android accepts only auto
|
|
32
|
+
* or none here, so it ignores this declaration. -moz-text-size-adjust is left out on
|
|
33
|
+
* purpose: none, its only opt-out value, would switch off Firefox's own font inflation.
|
|
34
|
+
* The value is 100%, the convention normalize.css set; on iPad and in Firefox, none
|
|
35
|
+
* would not behave the same.
|
|
36
|
+
* 2. Smooth scroll — reduced motion override is at the bottom of this file.
|
|
37
|
+
* 3. hanging-punctuation: Safari only, progressive enhancement.
|
|
38
|
+
* Punctuation hangs outside the text box correctly.
|
|
39
|
+
* 4. Font smoothing: prefer grayscale AA on macOS/iOS.
|
|
40
|
+
* subpixel-antialiased is less consistent across displays.
|
|
41
|
+
* 5. text-rendering: optimizelegibility — enables kerning and ligatures in Firefox and
|
|
42
|
+
* Safari; a no-op elsewhere. Unrelated to font-size inflation (1); kept as its own number.
|
|
43
|
+
*/
|
|
44
|
+
html {
|
|
45
|
+
-webkit-font-smoothing: antialiased; /* 4 */
|
|
46
|
+
-moz-osx-font-smoothing: grayscale; /* 4 */
|
|
47
|
+
hanging-punctuation: first last; /* 3 */
|
|
48
|
+
scroll-behavior: smooth; /* 2 */
|
|
49
|
+
text-rendering: optimizelegibility; /* 5 */
|
|
50
|
+
-webkit-text-size-adjust: 100%; /* 1 */
|
|
51
|
+
text-size-adjust: 100%; /* 1 */
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ── Body ────────────────────────────────────────────────────────────────────
|
|
55
|
+
* min-height: 100dvh — dynamic viewport height.
|
|
56
|
+
* Fixes iOS Safari bottom bar eating into 100vh layouts.
|
|
57
|
+
* dvh is Baseline 2023, safe for all modern browsers.
|
|
58
|
+
*/
|
|
59
|
+
body {
|
|
60
|
+
background-color: var(--nave-color-surface-base);
|
|
61
|
+
color: var(--nave-color-content-primary);
|
|
62
|
+
font-family: var(--nave-font-family-base);
|
|
63
|
+
font-size: var(--nave-font-size-md);
|
|
64
|
+
line-height: var(--nave-line-height-base);
|
|
65
|
+
margin: 0;
|
|
66
|
+
min-height: 100dvh;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ── Remove default margins ──────────────────────────────────────────────────
|
|
70
|
+
* Browsers apply inconsistent margins to these elements.
|
|
71
|
+
* Spacing is a layout concern, not an element default.
|
|
72
|
+
*/
|
|
73
|
+
h1,
|
|
74
|
+
h2,
|
|
75
|
+
h3,
|
|
76
|
+
h4,
|
|
77
|
+
h5,
|
|
78
|
+
h6,
|
|
79
|
+
p,
|
|
80
|
+
figure,
|
|
81
|
+
blockquote,
|
|
82
|
+
dl,
|
|
83
|
+
dd,
|
|
84
|
+
pre {
|
|
85
|
+
margin: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ── Lists ───────────────────────────────────────────────────────────────────
|
|
89
|
+
* Only remove list styles when role="list" is present.
|
|
90
|
+
* VoiceOver (Safari) removes list semantics when list-style is removed.
|
|
91
|
+
* Adding role="list" signals intentional non-list usage to the browser.
|
|
92
|
+
* Pattern: Scott O'Hara / Andy Bell.
|
|
93
|
+
*/
|
|
94
|
+
ul[role='list'],
|
|
95
|
+
ol[role='list'] {
|
|
96
|
+
list-style: none;
|
|
97
|
+
margin: 0;
|
|
98
|
+
padding: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ── Headings ─────────────────────────────────────────────────────────────────
|
|
102
|
+
* 1. Tighter line-height — body line-height is too loose for headings.
|
|
103
|
+
* 2. text-wrap: balance — prevents single orphan words on the last line.
|
|
104
|
+
* Baseline since May 2024 (Chrome 114, Firefox 121, Safari 17.5), so it is
|
|
105
|
+
* inside the browser floor (ADR 0005) and applies everywhere Nave supports.
|
|
106
|
+
*/
|
|
107
|
+
h1,
|
|
108
|
+
h2,
|
|
109
|
+
h3,
|
|
110
|
+
h4,
|
|
111
|
+
h5,
|
|
112
|
+
h6 {
|
|
113
|
+
font-family: var(--nave-font-family-display);
|
|
114
|
+
font-weight: var(--nave-font-weight-bold);
|
|
115
|
+
line-height: var(--nave-line-height-tight); /* 1 */
|
|
116
|
+
overflow-wrap: break-word;
|
|
117
|
+
text-wrap: balance; /* 2 */
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ── Paragraphs ───────────────────────────────────────────────────────────────
|
|
121
|
+
* text-wrap: pretty — optimises last 4 lines, prevents orphans.
|
|
122
|
+
* Chrome and Edge 117+, Safari 26+, not yet Firefox. Past the browser floor, so
|
|
123
|
+
* progressive enhancement: without it, paragraphs wrap normally (ADR 0005).
|
|
124
|
+
*/
|
|
125
|
+
p {
|
|
126
|
+
overflow-wrap: break-word;
|
|
127
|
+
text-wrap: pretty;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* ── Overflow wrap ────────────────────────────────────────────────────────────
|
|
131
|
+
* Prevents long words / URLs from breaking layouts.
|
|
132
|
+
*/
|
|
133
|
+
li,
|
|
134
|
+
figcaption,
|
|
135
|
+
dt,
|
|
136
|
+
dd {
|
|
137
|
+
overflow-wrap: break-word;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ── Replaced elements ────────────────────────────────────────────────────────
|
|
141
|
+
* display: block removes phantom baseline space under images.
|
|
142
|
+
* Caused by inline baseline alignment — one of CSS's oldest layout bugs.
|
|
143
|
+
* height: auto maintains aspect ratio when width is constrained.
|
|
144
|
+
*/
|
|
145
|
+
img,
|
|
146
|
+
picture,
|
|
147
|
+
video,
|
|
148
|
+
canvas {
|
|
149
|
+
display: block;
|
|
150
|
+
height: auto;
|
|
151
|
+
max-width: 100%;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ── SVG ──────────────────────────────────────────────────────────────────────
|
|
155
|
+
* SVGs without an explicit fill inherit the parent text color.
|
|
156
|
+
* Controls icon color via CSS color — no fill attribute required.
|
|
157
|
+
*/
|
|
158
|
+
svg {
|
|
159
|
+
display: block;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
svg:not([fill]) {
|
|
163
|
+
fill: currentcolor;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ── Figcaption ───────────────────────────────────────────────────────────────
|
|
167
|
+
* Safari applies font-style: italic to figcaption by default.
|
|
168
|
+
*/
|
|
169
|
+
figcaption {
|
|
170
|
+
font-style: normal;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ── Forms ───────────────────────────────────────────────────────────────────
|
|
174
|
+
* font: inherit — all browsers apply their own font to form controls,
|
|
175
|
+
* ignoring the cascade. This is the most important form reset rule.
|
|
176
|
+
* letter-spacing / word-spacing: inherit — Safari ignores these on inputs.
|
|
177
|
+
*/
|
|
178
|
+
input,
|
|
179
|
+
button,
|
|
180
|
+
textarea,
|
|
181
|
+
select,
|
|
182
|
+
optgroup {
|
|
183
|
+
color: inherit;
|
|
184
|
+
font: inherit;
|
|
185
|
+
letter-spacing: inherit;
|
|
186
|
+
word-spacing: inherit;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* ── Buttons ──────────────────────────────────────────────────────────────────
|
|
190
|
+
* Strip all browser button chrome, from <button> and every button-like <input>.
|
|
191
|
+
* The text-input rule below also unsets appearance/background for
|
|
192
|
+
* input[type=submit|reset|button] (it excludes only checkbox/radio/range), but leaves the
|
|
193
|
+
* UA's own outset border since it never targeted the button rule's selector; listed here too
|
|
194
|
+
* so all four get the same border: none.
|
|
195
|
+
* The three input types are wrapped in :where() so they carry zero specificity, same as a
|
|
196
|
+
* bare `button` selector: without it, `input[type='submit']` etc. sit at (0,1,1), which beats
|
|
197
|
+
* the Disabled rule's `[disabled], [aria-disabled='true']` at (0,1,0) and makes this rule's
|
|
198
|
+
* `cursor: pointer` win on a disabled button-like input even though the Disabled rule runs
|
|
199
|
+
* later in the file. :where() keeps this rule at (0,0,1), so the Disabled rule's cursor still
|
|
200
|
+
* wins, and a consumer overriding button chrome never needs a specificity escalation either.
|
|
201
|
+
* This rule is placed before the text-input rule below (rather than after, its previous
|
|
202
|
+
* position) because a (0,0,1) selector following a (0,1,1) one that can match the same element
|
|
203
|
+
* (a submit/reset/button input matches both) is a descending-specificity smell the linter
|
|
204
|
+
* flags; placing the lower-specificity rule first avoids that without changing which
|
|
205
|
+
* declaration wins for any element — specificity, not source order, still decides.
|
|
206
|
+
* Nave component styles define all button appearance explicitly.
|
|
207
|
+
*/
|
|
208
|
+
button,
|
|
209
|
+
input:where([type='button'], [type='reset'], [type='submit']) {
|
|
210
|
+
appearance: none;
|
|
211
|
+
background: none;
|
|
212
|
+
border: none;
|
|
213
|
+
color: inherit;
|
|
214
|
+
cursor: pointer;
|
|
215
|
+
font: inherit;
|
|
216
|
+
padding: 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* ── Text inputs & textareas ──────────────────────────────────────────────────
|
|
220
|
+
* 1. Removes iOS Safari grey background on inputs.
|
|
221
|
+
* 2. appearance: none removes Safari's rounded pill input style. Unprefixed
|
|
222
|
+
* (Baseline) rather than -webkit-appearance.
|
|
223
|
+
* 3. border-radius: 0 — iOS Safari rounds inputs regardless of appearance.
|
|
224
|
+
* Scoped to text-like inputs and textarea only (excludes checkbox, radio,
|
|
225
|
+
* range, and select) — a bare `input` or `select` selector here would strip
|
|
226
|
+
* native rendering from checkboxes/radios (invisible empty boxes) and the
|
|
227
|
+
* select dropdown indicator.
|
|
228
|
+
*/
|
|
229
|
+
input:not([type='checkbox'], [type='radio'], [type='range']),
|
|
230
|
+
textarea {
|
|
231
|
+
/* 2 */
|
|
232
|
+
appearance: none; /* 2 */
|
|
233
|
+
background-color: transparent; /* 1 */
|
|
234
|
+
border-radius: 0; /* 3 */
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* ── Links ────────────────────────────────────────────────────────────────────
|
|
238
|
+
* color: inherit — link color is a design decision, not a browser default.
|
|
239
|
+
* text-decoration-skip-ink: prevents underline from clipping descenders (g, y, p).
|
|
240
|
+
*/
|
|
241
|
+
a {
|
|
242
|
+
color: inherit;
|
|
243
|
+
text-decoration-skip-ink: auto;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* ── Tables ───────────────────────────────────────────────────────────────────
|
|
247
|
+
* border-collapse: collapse removes double borders between cells.
|
|
248
|
+
* border-color: inherit — Firefox bug where table borders don't inherit color.
|
|
249
|
+
*/
|
|
250
|
+
table {
|
|
251
|
+
border-collapse: collapse;
|
|
252
|
+
border-color: inherit;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* ── Code ─────────────────────────────────────────────────────────────────────
|
|
256
|
+
* font-size: 1em — browsers render code at ~13px by default. This overrides it.
|
|
257
|
+
*/
|
|
258
|
+
code,
|
|
259
|
+
kbd,
|
|
260
|
+
samp,
|
|
261
|
+
pre {
|
|
262
|
+
font-family: var(--nave-font-family-mono);
|
|
263
|
+
font-size: 1em;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* ── Horizontal rule ──────────────────────────────────────────────────────────
|
|
267
|
+
* Chrome and Firefox disagree on hr height and border rendering.
|
|
268
|
+
* border: none clears the UA default first — Chrome leaves border-bottom-width: 1px,
|
|
269
|
+
* border-left-width: 1px and border-bottom-style: inset when only border-top is set,
|
|
270
|
+
* rendering a three-sided box under the token-coloured top border. border-top then
|
|
271
|
+
* normalises to a single 1px line.
|
|
272
|
+
*/
|
|
273
|
+
hr {
|
|
274
|
+
border: none;
|
|
275
|
+
border-top: var(--nave-border-width-sm) solid var(--nave-color-border-default);
|
|
276
|
+
color: inherit;
|
|
277
|
+
height: 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* ── Hidden attribute ─────────────────────────────────────────────────────────
|
|
281
|
+
* display: flex / grid overrides [hidden] in some browsers.
|
|
282
|
+
* !important is correct here — [hidden] must always mean hidden — except
|
|
283
|
+
* hidden="until-found": excluded via :where() (contributes zero specificity, so the
|
|
284
|
+
* selector stays exactly as specific as plain [hidden]), so find-in-page can still reveal it.
|
|
285
|
+
* The `i` flag makes the attribute-value match case-insensitive: `hidden` is an HTML
|
|
286
|
+
* enumerated attribute, whose keywords (including "until-found") are matched ASCII
|
|
287
|
+
* case-insensitively by the spec, and a browser resolving hidden="UNTIL-FOUND" into the
|
|
288
|
+
* until-found state must not fall through to this !important display: none.
|
|
289
|
+
*/
|
|
290
|
+
[hidden]:where(:not([hidden='until-found' i])) {
|
|
291
|
+
display: none !important;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* ── Disabled ─────────────────────────────────────────────────────────────────
|
|
295
|
+
* Consistent disabled cursor across all interactive elements.
|
|
296
|
+
*/
|
|
297
|
+
[disabled],
|
|
298
|
+
[aria-disabled='true'] {
|
|
299
|
+
cursor: not-allowed;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* ── Focus ───────────────────────────────────────────────────────────────────
|
|
303
|
+
* The reset does not remove the user agent's focus indicator, and it does
|
|
304
|
+
* not replace it either. That is a statement about what Nave declines to
|
|
305
|
+
* do: no rule here strips outline from a selector that is not itself gated
|
|
306
|
+
* on :focus-visible, and no rule here ships an author-styled indicator at
|
|
307
|
+
* document scope. The user-agent default is the baseline. Consumers who
|
|
308
|
+
* want a different treatment override it via the @layer contract (reset
|
|
309
|
+
* sits early; every consumer layer wins over it).
|
|
310
|
+
*/
|
|
311
|
+
|
|
312
|
+
/* ── Color scheme ─────────────────────────────────────────────────────────────
|
|
313
|
+
* `color-scheme` is now emitted by the TOKENS layer (`@navecss/tokens`, which
|
|
314
|
+
* ships full light+dark values via `light-dark()`), not by this reset. A
|
|
315
|
+
* declaration here would win over anything the tokens package emits
|
|
316
|
+
* (`@layer tokens` loads before `@layer reset`), which would make
|
|
317
|
+
* `light-dark()` inert and leave a tokens-only consumer (a documented tier) with
|
|
318
|
+
* no dark mode at all. Do not reintroduce a `color-scheme` declaration here.
|
|
319
|
+
*/
|
|
320
|
+
|
|
321
|
+
/* ── Reduced motion ───────────────────────────────────────────────────────────
|
|
322
|
+
* Belt-and-suspenders alongside the token-level reduced motion in tokens.css.
|
|
323
|
+
* Catches animations not using Nave tokens (third-party, consumer code).
|
|
324
|
+
* 0.01ms, not 0ms — avoids triggering animationend event bugs in some browsers.
|
|
325
|
+
* The four !important flags are DELIBERATE. Important declarations reverse
|
|
326
|
+
* cascade-layer order, so these beat a consumer's own !important in any later
|
|
327
|
+
* layer; without them a universal-selector rule in an early layer loses to
|
|
328
|
+
* almost anything, and the collapse would be inert over exactly the
|
|
329
|
+
* third-party and consumer motion it exists to reach. The cost is accepted
|
|
330
|
+
* knowingly: a consumer who wants a GENTLER reduced-motion treatment rather
|
|
331
|
+
* than none cannot have one here, because their shorter duration is
|
|
332
|
+
* clobbered too. This is a deliberate exception to Nave's otherwise-
|
|
333
|
+
* overridable defaults, not an oversight. Do not remove the flags; if you
|
|
334
|
+
* think the trade is wrong, open an issue rather than editing them out.
|
|
335
|
+
*/
|
|
336
|
+
@media (prefers-reduced-motion: reduce) {
|
|
337
|
+
*,
|
|
338
|
+
*::before,
|
|
339
|
+
*::after {
|
|
340
|
+
animation-duration: 0.01ms !important;
|
|
341
|
+
animation-iteration-count: 1 !important;
|
|
342
|
+
scroll-behavior: auto !important;
|
|
343
|
+
transition-duration: 0.01ms !important;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@navecss/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Nave design system — layer architecture, reset, atomic utilities, PostCSS plugin",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"css",
|
|
9
|
+
"design-system",
|
|
10
|
+
"css-layers",
|
|
11
|
+
"atomic-css",
|
|
12
|
+
"postcss-plugin",
|
|
13
|
+
"zero-runtime"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/navecss/navecss/tree/main/packages/core#readme",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./dist/index.css",
|
|
21
|
+
"./layers": "./dist/layers.css",
|
|
22
|
+
"./no-tokens": "./dist/no-tokens.css",
|
|
23
|
+
"./reset": "./dist/reset.css",
|
|
24
|
+
"./atomic": "./dist/atomic.css",
|
|
25
|
+
"./cx": {
|
|
26
|
+
"types": "./dist/cx.d.ts",
|
|
27
|
+
"import": "./dist/cx.js"
|
|
28
|
+
},
|
|
29
|
+
"./atoms": {
|
|
30
|
+
"types": "./dist/atoms.d.ts",
|
|
31
|
+
"import": "./dist/atoms.js"
|
|
32
|
+
},
|
|
33
|
+
"./postcss": {
|
|
34
|
+
"types": "./dist/postcss.d.ts",
|
|
35
|
+
"import": "./dist/postcss.js"
|
|
36
|
+
},
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"CONSUMER-ATOMS.md",
|
|
42
|
+
"ATOMS.md"
|
|
43
|
+
],
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"postcss": "^8"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"postcss": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@navecss/tokens": "^0.1.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^26.6.2",
|
|
57
|
+
"@vitest/browser": "5.0.1",
|
|
58
|
+
"@vitest/browser-playwright": "5.0.1",
|
|
59
|
+
"@vitest/coverage-v8": "5.0.1",
|
|
60
|
+
"playwright": "1.63.0",
|
|
61
|
+
"postcss": "^8.5.28",
|
|
62
|
+
"prettier": "3.9.8",
|
|
63
|
+
"tsup": "^8.5.1",
|
|
64
|
+
"typescript": "6.0.3",
|
|
65
|
+
"vitest": "5.0.1"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=22.18"
|
|
69
|
+
},
|
|
70
|
+
"repository": {
|
|
71
|
+
"type": "git",
|
|
72
|
+
"url": "git+https://github.com/navecss/navecss.git",
|
|
73
|
+
"directory": "packages/core"
|
|
74
|
+
},
|
|
75
|
+
"bugs": {
|
|
76
|
+
"url": "https://github.com/navecss/navecss/issues"
|
|
77
|
+
},
|
|
78
|
+
"sideEffects": [
|
|
79
|
+
"**/*.css"
|
|
80
|
+
],
|
|
81
|
+
"scripts": {
|
|
82
|
+
"build": "node scripts/build-css.ts && tsup",
|
|
83
|
+
"dev": "tsup --watch",
|
|
84
|
+
"clean": "rm -rf dist",
|
|
85
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p test/browser/tsconfig.json",
|
|
86
|
+
"lint": "eslint src scripts --max-warnings 0 && stylelint \"src/**/*.css\" --max-warnings 0",
|
|
87
|
+
"lint:fix": "eslint src scripts --fix && stylelint \"src/**/*.css\" --fix",
|
|
88
|
+
"test": "node scripts/generate-consumer-theming-fixtures.ts && vitest run",
|
|
89
|
+
"test:coverage": "node scripts/generate-consumer-theming-fixtures.ts && vitest run --coverage",
|
|
90
|
+
"test:browser": "node scripts/generate-consumer-theming-fixtures.ts && vitest run --config vitest.browser.config.ts",
|
|
91
|
+
"check:pack": "node ../../scripts/without-dry-run.mjs publint && node ../../scripts/without-dry-run.mjs attw --pack . --profile esm-only --exclude-entrypoints . layers no-tokens reset atomic"
|
|
92
|
+
}
|
|
93
|
+
}
|