@navecss/core 0.1.1 → 0.3.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/dist/atomic.css CHANGED
@@ -191,7 +191,9 @@
191
191
  outline: none;
192
192
 
193
193
  &:focus-visible {
194
- outline: var(--nave-border-width-focus) solid var(--nave-color-border-focus);
194
+ outline-style: solid;
195
+ outline-width: var(--nave-border-width-focus, 2px);
196
+ outline-color: var(--nave-color-border-focus, currentColor);
195
197
  outline-offset: 2px;
196
198
  }
197
199
  }
package/dist/atoms.d.ts CHANGED
@@ -208,6 +208,10 @@ declare const atoms: {
208
208
  /**
209
209
  * focusRing — keyboard focus indicator.
210
210
  * :focus-visible — keyboard users see it, mouse users do not.
211
+ *
212
+ * Longhands with fallbacks, not an `outline` shorthand, which computes to `none` as a whole when
213
+ * a `var()` in it has no usable value: no token layer, a missing property, or a wrong-typed value
214
+ * in an unregistered property. The fallbacks are 2px and `currentColor`, never a literal colour.
211
215
  */
212
216
  readonly focusRing: {
213
217
  readonly declarations: {
@@ -215,7 +219,9 @@ declare const atoms: {
215
219
  };
216
220
  readonly pseudos: {
217
221
  readonly ':focus-visible': {
218
- readonly outline: "var(--nave-border-width-focus) solid var(--nave-color-border-focus)";
222
+ readonly 'outline-style': "solid";
223
+ readonly 'outline-width': "var(--nave-border-width-focus, 2px)";
224
+ readonly 'outline-color': "var(--nave-color-border-focus, currentColor)";
219
225
  readonly 'outline-offset': "2px";
220
226
  };
221
227
  };
@@ -405,7 +411,7 @@ type AtomName = keyof typeof atoms;
405
411
  declare const toClassName: (name: AtomName) => string;
406
412
  /**
407
413
  * Map of atom name → global CSS class name.
408
- * Used by cx() and the PostCSS plugin.
414
+ * Used by cx() and the directive core's diagnostic hints.
409
415
  * Only covers Nave built-in atoms — consumer atoms are @nave-directive only.
410
416
  */
411
417
  declare const atomClassMap: Record<AtomName, string>;
package/dist/atoms.js CHANGED
@@ -1 +1 @@
1
- export { atomClassMap, atoms, toClassName } from './chunk-BVON7XKC.js';
1
+ export { atomClassMap, atoms, toClassName } from './chunk-LIUB4EES.js';
package/dist/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/bin.js ADDED
@@ -0,0 +1,451 @@
1
+ #!/usr/bin/env node
2
+ import { runCheck } from './chunk-XQQCDUV6.js';
3
+ import { splitByteOrderMark, foldedText } from './chunk-IZQLFUQU.js';
4
+ import { formatFinding } from './chunk-UANF3FGO.js';
5
+ import { validateExtendAtomsHostFree } from './chunk-R6JJZGXC.js';
6
+ import { resolveExtendSpecifier, applyExtendModule } from './chunk-NKUZNRF4.js';
7
+ import { snapshotExtendMap, expandText } from './chunk-S7DZUVV3.js';
8
+ import { createPositionFinder, tokenize, atKeywordName } from './chunk-NFS2REPQ.js';
9
+ import './chunk-LIUB4EES.js';
10
+ import { parseArgs } from 'util';
11
+ import { realpathSync, watch, existsSync, statSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
12
+ import path2 from 'path';
13
+
14
+ // src/bin-usage.ts
15
+ var USAGE = `Usage:
16
+ navecss-core check --source=<file-or-dir> [--source=<file-or-dir> ...]
17
+ navecss-core expand --source=<file> --out=<file> [--source=<file> --out=<file> ...] [--extend=<module>] [--watch]`;
18
+ function readValues(args) {
19
+ try {
20
+ const { values, positionals } = parseArgs({
21
+ args: [...args],
22
+ options: {
23
+ extend: { type: "string", multiple: true },
24
+ out: { type: "string", multiple: true },
25
+ source: { type: "string", multiple: true },
26
+ watch: { type: "boolean" }
27
+ },
28
+ strict: true,
29
+ allowPositionals: true
30
+ });
31
+ if (positionals.length > 0) {
32
+ return `expand does not take a positional argument: ${positionals[0]}`;
33
+ }
34
+ if ((values.extend?.length ?? 0) > 1) return "expand takes one --extend.";
35
+ return {
36
+ extend: values.extend?.[0],
37
+ out: values.out ?? [],
38
+ source: values.source ?? [],
39
+ watch: values.watch === true
40
+ };
41
+ } catch (error) {
42
+ return `expand: ${error.message.split("\n", 1)[0]}`;
43
+ }
44
+ }
45
+ function isSameFile(first, second) {
46
+ if (path2.resolve(first) === path2.resolve(second)) return true;
47
+ if (!existsSync(first) || !existsSync(second)) return false;
48
+ const a = statSync(first);
49
+ const b = statSync(second);
50
+ return a.dev === b.dev && a.ino === b.ino;
51
+ }
52
+ function problemWith(raw) {
53
+ if (raw.source.length === 0) return "expand requires at least one --source=<file>.";
54
+ if (raw.out.length === 0) return "expand requires --out=<file> for each --source.";
55
+ if (raw.source.length !== raw.out.length) {
56
+ return `expand takes one --out for each --source, matched by order: got ${raw.source.length} --source and ${raw.out.length} --out.`;
57
+ }
58
+ if ([...raw.source, ...raw.out, raw.extend ?? "."].includes("")) {
59
+ return "expand: a --source, --out or --extend value is empty.";
60
+ }
61
+ const outs = raw.out.map((file) => path2.resolve(file));
62
+ const inputs = [
63
+ ...raw.source.map((file) => ({ file, name: "a --source" })),
64
+ ...raw.extend === void 0 ? [] : [{ file: raw.extend, name: "the --extend module" }]
65
+ ];
66
+ for (const out of outs) {
67
+ const input = inputs.find(({ file }) => isSameFile(file, out));
68
+ if (input !== void 0) {
69
+ return `expand: --out ${out} is also ${input.name}; it would overwrite its own input.`;
70
+ }
71
+ }
72
+ if (new Set(outs).size !== outs.length)
73
+ return "expand: two files would be written to the same --out.";
74
+ return void 0;
75
+ }
76
+ function parseExpandArgs(args) {
77
+ const raw = readValues(args);
78
+ if (typeof raw === "string") return { kind: "usageError", message: raw };
79
+ const problem = problemWith(raw);
80
+ if (problem !== void 0) return { kind: "usageError", message: problem };
81
+ return {
82
+ kind: "job",
83
+ job: {
84
+ extend: raw.extend,
85
+ pairs: raw.source.map((source, index) => ({ source, out: raw.out[index] })),
86
+ watch: raw.watch
87
+ }
88
+ };
89
+ }
90
+
91
+ // src/directive/bare-imports.ts
92
+ var OPENERS = /* @__PURE__ */ new Set(["(-token", "[-token", "function-token", "{-token"]);
93
+ var CLOSERS = /* @__PURE__ */ new Set([")-token", "]-token", "}-token"]);
94
+ var SCHEME = /^[a-z][a-z\d+.-]*:/i;
95
+ function isTrivia(token) {
96
+ return token.type === "whitespace-token" || token.type === "comment";
97
+ }
98
+ function specifierOf(prelude) {
99
+ const [first, second] = prelude.filter((token) => !isTrivia(token));
100
+ if (first?.type === "string-token" || first?.type === "url-token") {
101
+ return first.structured.value;
102
+ }
103
+ const isUrlFunction = first?.type === "function-token" && first.structured.value.toLowerCase() === "url";
104
+ if (isUrlFunction && second?.type === "string-token") {
105
+ return second.structured.value;
106
+ }
107
+ return void 0;
108
+ }
109
+ function isBare(specifier, hasFile) {
110
+ const isUrlForm = withoutQueryOrFragment(specifier) === "" || specifier.startsWith("/") || specifier.startsWith("./") || specifier.startsWith("../") || SCHEME.test(specifier);
111
+ return !isUrlForm && !hasFile(decoded(withoutQueryOrFragment(specifier)));
112
+ }
113
+ function withoutQueryOrFragment(specifier) {
114
+ const ends = [specifier.indexOf("?"), specifier.indexOf("#")].filter((at) => at !== -1);
115
+ return ends.length === 0 ? specifier : specifier.slice(0, Math.min(...ends));
116
+ }
117
+ function decoded(path4) {
118
+ try {
119
+ return decodeURIComponent(path4);
120
+ } catch {
121
+ return path4;
122
+ }
123
+ }
124
+ function endOfPrelude(tokens, from) {
125
+ const end = tokens.findIndex(
126
+ (token, index) => index >= from && (token.type === "semicolon-token" || token.type === "{-token")
127
+ );
128
+ return end === -1 ? tokens.length : end;
129
+ }
130
+ function readImport(tokens, index, hasFile) {
131
+ const end = endOfPrelude(tokens, index + 1);
132
+ const specifier = specifierOf(tokens.slice(index + 1, end));
133
+ if (specifier === void 0 || !isBare(specifier, hasFile)) return { diagnostic: void 0, end };
134
+ const lastIndex = tokens[end]?.type === "semicolon-token" ? end : end - 1;
135
+ const diagnostic = {
136
+ code: "bare-import",
137
+ offset: tokens[index].startIndex,
138
+ endOffset: tokens[lastIndex].endIndex,
139
+ text: specifier
140
+ };
141
+ return { diagnostic, end };
142
+ }
143
+ function findBareImports(css, hasFile) {
144
+ const tokens = tokenize(css);
145
+ const found = [];
146
+ let depth = 0;
147
+ let index = 0;
148
+ while (index < tokens.length) {
149
+ const token = tokens[index];
150
+ let next = index + 1;
151
+ if (OPENERS.has(token.type)) depth++;
152
+ else if (CLOSERS.has(token.type)) depth = Math.max(0, depth - 1);
153
+ else if (depth === 0 && token.type === "at-keyword-token" && atKeywordName(token) === "import") {
154
+ const read = readImport(tokens, index, hasFile);
155
+ if (read.diagnostic) found.push(read.diagnostic);
156
+ next = read.end + 1;
157
+ }
158
+ index = next;
159
+ }
160
+ return found;
161
+ }
162
+
163
+ // src/expand-pass.ts
164
+ function bareImportDiagnostics(css, source, out, outputs) {
165
+ const directory = path2.dirname(path2.resolve(out));
166
+ const positionAt = createPositionFinder(css);
167
+ const hasFile = (specifier) => {
168
+ const target = path2.resolve(directory, specifier);
169
+ return outputs.has(target) || statSync(target, { throwIfNoEntry: false })?.isFile() === true;
170
+ };
171
+ return findBareImports(css, hasFile).map((diagnostic) => {
172
+ const position = positionAt(diagnostic.offset);
173
+ return {
174
+ ...diagnostic,
175
+ severity: "error",
176
+ file: source,
177
+ line: position.line,
178
+ column: position.column + 1
179
+ };
180
+ });
181
+ }
182
+ function expandOne(pair, extend, outputs) {
183
+ const source = pair.source;
184
+ let raw;
185
+ try {
186
+ raw = readFileSync(source, "utf8");
187
+ } catch {
188
+ return { kind: "unreadable" };
189
+ }
190
+ const { bom, text } = splitByteOrderMark(raw);
191
+ try {
192
+ const result = expandText(text, { extend, onUnknown: "error", from: source });
193
+ const problems = [
194
+ ...result.diagnostics,
195
+ ...bareImportDiagnostics(text, source, pair.out, outputs)
196
+ ];
197
+ const report = foldedText(problems, source, extend);
198
+ return report === void 0 ? { kind: "expanded", css: bom + result.css } : { kind: "problems", report };
199
+ } catch (error) {
200
+ return { kind: "problems", report: `${source}: ${error.message}` };
201
+ }
202
+ }
203
+ function statusOf(unreadable, reports) {
204
+ if (unreadable.length > 0) return 2;
205
+ return reports.length > 0 ? 1 : 0;
206
+ }
207
+ function writeOutputs(outputs) {
208
+ const written = [];
209
+ for (const { css, pair } of outputs) {
210
+ try {
211
+ mkdirSync(path2.dirname(path2.resolve(pair.out)), { recursive: true });
212
+ writeFileSync(pair.out, css, "utf8");
213
+ } catch (error) {
214
+ const reason = error.message;
215
+ return {
216
+ written,
217
+ writeFailure: `Could not write ${pair.out}: ${reason}. No file after it was written.`
218
+ };
219
+ }
220
+ written.push(pair);
221
+ }
222
+ return { written, writeFailure: void 0 };
223
+ }
224
+ function expandPass(job, extend) {
225
+ const reports = [];
226
+ const unreadable = [];
227
+ const outputs = [];
228
+ const outPaths = new Set(job.pairs.map((pair) => path2.resolve(pair.out)));
229
+ for (const pair of job.pairs) {
230
+ const outcome = expandOne(pair, extend, outPaths);
231
+ if (outcome.kind === "unreadable") unreadable.push(pair.source);
232
+ else if (outcome.kind === "problems") reports.push(outcome.report);
233
+ else outputs.push({ css: outcome.css, pair });
234
+ }
235
+ const status = statusOf(unreadable, reports);
236
+ if (status !== 0) return { expanded: [], reports, status, unreadable, writeFailure: void 0 };
237
+ const { written, writeFailure } = writeOutputs(outputs);
238
+ return {
239
+ expanded: written,
240
+ reports,
241
+ status: writeFailure === void 0 ? 0 : 2,
242
+ unreadable,
243
+ writeFailure
244
+ };
245
+ }
246
+ var SETTLE_MS = 40;
247
+ function watchFiles(files, onChange) {
248
+ const wanted = new Set(files.map((file) => path2.resolve(file)));
249
+ let timer;
250
+ const schedule = () => {
251
+ clearTimeout(timer);
252
+ timer = setTimeout(onChange, SETTLE_MS);
253
+ };
254
+ const watchedNames = new Set(wanted);
255
+ const directories = new Set([...wanted].map((file) => path2.dirname(file)));
256
+ for (const file of wanted) {
257
+ try {
258
+ const target = realpathSync(file);
259
+ watchedNames.add(target);
260
+ directories.add(path2.dirname(target));
261
+ } catch {
262
+ }
263
+ }
264
+ const watchers = [];
265
+ try {
266
+ for (const directory of directories) {
267
+ watchers.push(
268
+ watch(directory, (_event, name) => {
269
+ if (name === null || watchedNames.has(path2.join(directory, name))) schedule();
270
+ })
271
+ );
272
+ }
273
+ } catch (error) {
274
+ for (const watcher of watchers) watcher.close();
275
+ throw error;
276
+ }
277
+ return () => {
278
+ clearTimeout(timer);
279
+ for (const watcher of watchers) watcher.close();
280
+ };
281
+ }
282
+
283
+ // src/expand-command.ts
284
+ async function loadExtend(file, cache) {
285
+ if (file === void 0) return {};
286
+ let loaded = {};
287
+ await applyExtendModule(file, cache, (value) => {
288
+ loaded = snapshotExtendMap(value);
289
+ validateExtendAtomsHostFree(loaded);
290
+ });
291
+ return loaded;
292
+ }
293
+ function print(result) {
294
+ for (const { source, out } of result.expanded) console.log(`Expanded ${source} to ${out}.`);
295
+ for (const report of result.reports) console.error(report);
296
+ for (const source of result.unreadable) {
297
+ console.error(`Could not read ${source}, so no file was written.`);
298
+ }
299
+ if (result.writeFailure !== void 0) console.error(result.writeFailure);
300
+ }
301
+ async function runOnce(job, extendFile, cache) {
302
+ let extend;
303
+ try {
304
+ extend = await loadExtend(extendFile, cache);
305
+ } catch (error) {
306
+ console.error(error.message);
307
+ return 2;
308
+ }
309
+ const result = expandPass(job, extend);
310
+ print(result);
311
+ return result.status;
312
+ }
313
+ async function watchAndRun(run, files) {
314
+ let isRunning = true;
315
+ let isPending = false;
316
+ const runAgain = async () => {
317
+ if (isRunning) {
318
+ isPending = true;
319
+ return;
320
+ }
321
+ isRunning = true;
322
+ do {
323
+ isPending = false;
324
+ await run();
325
+ } while (isPending);
326
+ isRunning = false;
327
+ };
328
+ try {
329
+ watchFiles(files, () => void runAgain());
330
+ } catch (error) {
331
+ console.error(`Could not watch: ${error.message}`);
332
+ return 2;
333
+ }
334
+ do {
335
+ isPending = false;
336
+ await run();
337
+ } while (isPending);
338
+ isRunning = false;
339
+ return new Promise(() => {
340
+ });
341
+ }
342
+ async function runExpand(args) {
343
+ if (args.includes("--help") || args.includes("-h")) {
344
+ console.log(USAGE);
345
+ return 0;
346
+ }
347
+ const parsed = parseExpandArgs(args);
348
+ if (parsed.kind === "usageError") {
349
+ console.error(`${parsed.message}
350
+ ${USAGE}`);
351
+ return 2;
352
+ }
353
+ const { job } = parsed;
354
+ let extendFile;
355
+ try {
356
+ extendFile = job.extend === void 0 ? void 0 : resolveExtendSpecifier(job.extend);
357
+ } catch (error) {
358
+ console.error(error.message);
359
+ return 2;
360
+ }
361
+ const cache = /* @__PURE__ */ new Map();
362
+ const run = () => runOnce(job, extendFile, cache);
363
+ if (!job.watch) return run();
364
+ const watched = [...job.pairs.map((pair) => pair.source), ...extendFile ? [extendFile] : []];
365
+ return watchAndRun(run, watched);
366
+ }
367
+
368
+ // src/bin.ts
369
+ function isHelpRequest(args) {
370
+ return args.includes("--help") || args.includes("-h");
371
+ }
372
+ function parseSourceArgs(args) {
373
+ try {
374
+ const { values, positionals } = parseArgs({
375
+ args,
376
+ options: { source: { type: "string", multiple: true } },
377
+ strict: true,
378
+ allowPositionals: true
379
+ });
380
+ if (positionals.length > 0) {
381
+ return { kind: "unexpectedArgument", argument: positionals[0] };
382
+ }
383
+ return { kind: "source", source: values.source };
384
+ } catch {
385
+ return { kind: "usageError" };
386
+ }
387
+ }
388
+ function printFindings(result) {
389
+ for (const finding of result.findings) console.log(formatFinding(finding));
390
+ }
391
+ function printCleanSummary(result, source) {
392
+ if (result.status !== 0) return;
393
+ const plural = result.stylesheetsRead === 1 ? "stylesheet" : "stylesheets";
394
+ console.log(
395
+ `Checked ${result.stylesheetsRead} ${plural} under ${source.join(", ")}; found no @nave directive.`
396
+ );
397
+ }
398
+ function reportPathProblems(result, source, unreadablePaths, notStylesheetPaths) {
399
+ for (const badPath of unreadablePaths) {
400
+ console.error(`Could not read ${badPath}, so it was not checked.`);
401
+ }
402
+ for (const badPath of notStylesheetPaths) {
403
+ console.error(`${badPath} is not a stylesheet file, so it was not checked.`);
404
+ }
405
+ if (result.status === 2 && unreadablePaths.length === 0 && notStylesheetPaths.length === 0) {
406
+ console.error(`No stylesheet found under ${source.join(", ")}; nothing was checked.`);
407
+ }
408
+ }
409
+ async function runCheck2(args) {
410
+ if (isHelpRequest(args)) {
411
+ console.log(USAGE);
412
+ return 0;
413
+ }
414
+ const parsed = parseSourceArgs(args);
415
+ if (parsed.kind === "unexpectedArgument") {
416
+ console.error(`check does not take a positional argument: ${parsed.argument}
417
+ ${USAGE}`);
418
+ return 2;
419
+ }
420
+ const source = parsed.kind === "source" ? parsed.source : void 0;
421
+ if (!source || source.length === 0 || source.includes("")) {
422
+ console.error(`check requires at least one --source=<path>.
423
+ ${USAGE}`);
424
+ return 2;
425
+ }
426
+ const { result, unreadablePaths, notStylesheetPaths } = await runCheck(source);
427
+ printFindings(result);
428
+ printCleanSummary(result, source);
429
+ reportPathProblems(result, source, unreadablePaths, notStylesheetPaths);
430
+ return result.status;
431
+ }
432
+ async function main() {
433
+ const args = process.argv.slice(2);
434
+ if (args.length === 0) {
435
+ console.error(USAGE);
436
+ return 2;
437
+ }
438
+ if (isHelpRequest([args[0]])) {
439
+ console.log(USAGE);
440
+ return 0;
441
+ }
442
+ const [subcommand, ...rest] = args;
443
+ if (subcommand === "expand") return runExpand(rest);
444
+ if (subcommand !== "check") {
445
+ console.error(`Unknown subcommand "${subcommand}".
446
+ ${USAGE}`);
447
+ return 2;
448
+ }
449
+ return runCheck2(rest);
450
+ }
451
+ process.exitCode = await main();
@@ -0,0 +1,31 @@
1
+ interface Finding {
2
+ readonly file: string;
3
+ readonly line: number;
4
+ readonly column: number;
5
+ readonly text: string;
6
+ readonly selector?: string;
7
+ }
8
+
9
+ interface CheckOptions {
10
+ readonly source: readonly string[];
11
+ }
12
+ interface CheckResult {
13
+ readonly status: 0 | 1 | 2;
14
+ readonly findings: readonly Finding[];
15
+ readonly stylesheetsRead: number;
16
+ }
17
+
18
+ /**
19
+ * `check({ source })`, the core logic behind both the `navecss-core
20
+ * check` bin and the `@navecss/core/check` subpath. Exit contract mirrors
21
+ * `navecss-tokens`: `0` read at least one stylesheet and found none, `1`
22
+ * found at least one, `2` a usage error, an unreadable path, or no
23
+ * stylesheet found at all.
24
+ */
25
+
26
+ /**
27
+ `check({ source })`: reads every stylesheet under `source`, reporting every surviving `@nave`.
28
+ */
29
+ declare function check(options: CheckOptions): Promise<CheckResult>;
30
+
31
+ export { type CheckOptions, type CheckResult, type Finding, check };
package/dist/check.js ADDED
@@ -0,0 +1,11 @@
1
+ import { runCheck } from './chunk-XQQCDUV6.js';
2
+ import './chunk-UANF3FGO.js';
3
+ import './chunk-NFS2REPQ.js';
4
+
5
+ // src/directive/check.ts
6
+ async function check(options) {
7
+ const run = await runCheck(options.source);
8
+ return run.result;
9
+ }
10
+
11
+ export { check };
@@ -0,0 +1,30 @@
1
+ import { formatDiagnostic, foldLocated } from './chunk-S7DZUVV3.js';
2
+
3
+ // src/byte-order-mark.ts
4
+ var BYTE_ORDER_MARK = "\uFEFF";
5
+ function splitByteOrderMark(raw) {
6
+ return raw.startsWith(BYTE_ORDER_MARK) ? { bom: BYTE_ORDER_MARK, text: raw.slice(BYTE_ORDER_MARK.length) } : { bom: "", text: raw };
7
+ }
8
+
9
+ // src/directive/report-text.ts
10
+ function inSourceOrder(diagnostics) {
11
+ return diagnostics.toSorted((a, b) => a.offset - b.offset);
12
+ }
13
+ function warningTexts(diagnostics, file, extend) {
14
+ return inSourceOrder(diagnostics).map(
15
+ (diagnostic) => `${file}:${diagnostic.line}:${diagnostic.column}: ${formatDiagnostic(diagnostic, { extend })}`
16
+ );
17
+ }
18
+ function foldedText(diagnostics, file, extend) {
19
+ const ordered = inSourceOrder(diagnostics);
20
+ const first = ordered[0];
21
+ if (first === void 0) return void 0;
22
+ const located = ordered.map((diagnostic) => ({
23
+ text: formatDiagnostic(diagnostic, { extend }),
24
+ line: diagnostic.line,
25
+ column: diagnostic.column
26
+ }));
27
+ return `${file}:${first.line}:${first.column}: ${foldLocated(located)}`;
28
+ }
29
+
30
+ export { foldedText, splitByteOrderMark, warningTexts };
@@ -169,6 +169,10 @@ var atoms = {
169
169
  /**
170
170
  * focusRing — keyboard focus indicator.
171
171
  * :focus-visible — keyboard users see it, mouse users do not.
172
+ *
173
+ * Longhands with fallbacks, not an `outline` shorthand, which computes to `none` as a whole when
174
+ * a `var()` in it has no usable value: no token layer, a missing property, or a wrong-typed value
175
+ * in an unregistered property. The fallbacks are 2px and `currentColor`, never a literal colour.
172
176
  */
173
177
  focusRing: {
174
178
  declarations: {
@@ -176,7 +180,9 @@ var atoms = {
176
180
  },
177
181
  pseudos: {
178
182
  ":focus-visible": {
179
- outline: "var(--nave-border-width-focus) solid var(--nave-color-border-focus)",
183
+ "outline-style": "solid",
184
+ "outline-width": "var(--nave-border-width-focus, 2px)",
185
+ "outline-color": "var(--nave-color-border-focus, currentColor)",
180
186
  "outline-offset": "2px"
181
187
  }
182
188
  }