@dataimago/css-neutral 0.1.0-alpha.10
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/README.md +85 -0
- package/dist/bin/dataimago-sync.mjs +342 -0
- package/dist/dataviz/dataimago-dataviz.css +42 -0
- package/dist/dataviz/dataimago-dataviz.json +42 -0
- package/dist/dataviz/dataimago-dataviz.tex +21 -0
- package/dist/dataviz/dataimago_dataviz.R +61 -0
- package/dist/latex/dataimago-tokens.sty +86 -0
- package/dist/outputs.json +13 -0
- package/dist/quarto/neutral-dark.scss +259 -0
- package/dist/quarto/neutral-light.scss +259 -0
- package/dist/tailwind-preset.js +151 -0
- package/dist/tokens.css +225 -0
- package/dist/tokens.scss +225 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @dataimago/css-neutral
|
|
2
|
+
|
|
3
|
+
Neutral-identity compiled CSS + Tailwind preset — paired with `@dataimago/tokens-neutral`. Sensible defaults for externally-owned consumers.
|
|
4
|
+
|
|
5
|
+
**Ships:**
|
|
6
|
+
- `dist/tokens.css` — CSS custom properties (copied from `@dataimago/tokens-neutral`)
|
|
7
|
+
- `dist/tokens.scss` — same, as SCSS
|
|
8
|
+
- `dist/tailwind-preset.js` — Tailwind preset (Noto Sans throughout; neutral palette: ink/stone/paper/functional only)
|
|
9
|
+
|
|
10
|
+
**Does NOT ship** a compiled `dataimago.css` (component stylesheet). The house package's compiled CSS embeds dataimago-specific component styling (forest interactive primary, copper editorial framing, accent moral-weight beats); shipping that under a "neutral" name would be dishonest. Neutral consumers use tokens + Tailwind preset and author their own component stylesheets.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add @dataimago/tokens-neutral @dataimago/css-neutral
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```css
|
|
21
|
+
/* globals.css */
|
|
22
|
+
@import '@dataimago/css-neutral/tokens.css';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// tailwind.config.js
|
|
27
|
+
module.exports = {
|
|
28
|
+
presets: [require('@dataimago/css-neutral/tailwind-preset')],
|
|
29
|
+
content: [/* your app */],
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## What's in the Tailwind preset
|
|
34
|
+
|
|
35
|
+
| Block | Source | Neutral identity |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `colors` | `@dataimago/tokens-neutral` | `ink`, `stone`, `paper`, `functional` only — no `forest`, `copper`, `accent`, `presentation-blue` |
|
|
38
|
+
| `fontFamily` | `@dataimago/tokens-neutral` | All families resolve to Noto Sans stacks (`display`, `display-editorial`, `display-documentation`, `brand`) |
|
|
39
|
+
| `fontSize` | `@dataimago/tokens-neutral` | Same scale as `@dataimago/css` (micro → heading-xl) |
|
|
40
|
+
| `spacing`, `borderRadius`, `boxShadow`, `transitionDuration` | `@dataimago/tokens` (shared) | Same as `@dataimago/css` — non-identity tokens are shared between the two packages |
|
|
41
|
+
|
|
42
|
+
## When to use this vs `@dataimago/css`
|
|
43
|
+
|
|
44
|
+
| Consumer | Use |
|
|
45
|
+
|---|---|
|
|
46
|
+
| dataimago.ai (Level-1 hub) | `@dataimago/css` |
|
|
47
|
+
| dissertation.ai, sgpc.ai, rpkg.ai (Level-2 hubs — dataimago-owned) | `@dataimago/css` |
|
|
48
|
+
| `apps/template/` per-user Level-3 apps | `@dataimago/css-neutral` |
|
|
49
|
+
| Third-party Level-2 hubs (future) | `@dataimago/css-neutral` as starting point; customize via Tailwind preset extension |
|
|
50
|
+
|
|
51
|
+
## Quarto, LaTeX and data-viz outputs
|
|
52
|
+
|
|
53
|
+
`dist/quarto/neutral-{light,dark}.scss` (the neutral Quarto theme pair), `dist/latex/dataimago-tokens.sty` and `dist/dataviz/` (the shared Okabe-Ito palette) ship here too; the house-only `components.scss` and `dataimago.sty` do not.
|
|
54
|
+
|
|
55
|
+
A consumer that does not install from npm pins a version and syncs files. Commit
|
|
56
|
+
`.dataimago/design.json`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{ "package": "@dataimago/css-neutral", "version": "<exact version>",
|
|
60
|
+
"outputs": { "quarto/neutral-light.scss": "styles/", "quarto/neutral-dark.scss": "styles/",
|
|
61
|
+
"latex/dataimago-tokens.sty": "latex/", "dataviz/dataimago_dataviz.R": "R/palette.R" } }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
then run `npx -p @dataimago/css-neutral@<version> dataimago-sync` and commit the files (each carries a
|
|
65
|
+
provenance stamp; never edit them). Put `npx -p @dataimago/css-neutral@<version> dataimago-sync --check`
|
|
66
|
+
in CI: it fails on any drift. The shipped outputs are listed in `dist/outputs.json`
|
|
67
|
+
(e.g. `https://unpkg.com/@dataimago/css-neutral@<version>/dist/outputs.json`). See
|
|
68
|
+
[`wiki/patterns/new-consumer-checklist.md`](../../wiki/patterns/new-consumer-checklist.md).
|
|
69
|
+
|
|
70
|
+
## Architectural notes
|
|
71
|
+
|
|
72
|
+
Variant A of the two-tier identity pattern — see [`wiki/decisions/tokens-neutral-sibling.md`](../../wiki/decisions/tokens-neutral-sibling.md) for the full rationale, including the iteration-4 deprecation target (Variant B: neutral-as-base + `@dataimago/dataimago-theme` overlay).
|
|
73
|
+
|
|
74
|
+
## Build
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
pnpm --filter @dataimago/css-neutral build
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Requires `@dataimago/tokens-neutral` to be built first (Turbo handles the ordering when running `pnpm -r build`).
|
|
81
|
+
|
|
82
|
+
## Related
|
|
83
|
+
|
|
84
|
+
- [`@dataimago/tokens-neutral`](../tokens-neutral) — the neutral tokens this preset reads
|
|
85
|
+
- [`@dataimago/css`](../css) — house identity counterpart
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dataimago-sync — pinned design-system sync for non-JS consumers (spec §4).
|
|
4
|
+
*
|
|
5
|
+
* npx -p @dataimago/css@<version> dataimago-sync write the files
|
|
6
|
+
* npx -p @dataimago/css@<version> dataimago-sync --check exit 1 on any drift
|
|
7
|
+
* dataimago-sync --from <dataimago-design checkout> try an unreleased build
|
|
8
|
+
* node tools/formats/dataimago-sync.mjs --self-test (repo only)
|
|
9
|
+
*
|
|
10
|
+
* Reads the consumer's .dataimago/design.json (or --config <path>):
|
|
11
|
+
* { "package": "@dataimago/css", "version": "<exact>",
|
|
12
|
+
* "outputs": { "<dist path>": "<destination dir/ or file>" } }
|
|
13
|
+
*
|
|
14
|
+
* Copies bytes from THIS package's own dist/ — no download, no transformation;
|
|
15
|
+
* every format decision lives in the dataimago-design build. Exit codes:
|
|
16
|
+
* 0 ok, 1 check failed, 2 configuration error (nothing written).
|
|
17
|
+
*
|
|
18
|
+
* Node built-ins only: this file ships in @dataimago/css and
|
|
19
|
+
* @dataimago/css-neutral dist/bin/. The stamp grammar it reads is defined in
|
|
20
|
+
* tools/formats/stamp.mjs — change both together.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
|
|
29
|
+
const LOCAL = 'local-unreleased';
|
|
30
|
+
|
|
31
|
+
class ConfigError extends Error {}
|
|
32
|
+
|
|
33
|
+
function readJson(file, what) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
throw new ConfigError(`cannot read ${what} at ${file}: ${e.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const USAGE = `dataimago-sync — copy a pinned @dataimago release's design files into this repo
|
|
42
|
+
|
|
43
|
+
dataimago-sync write the files listed in .dataimago/design.json
|
|
44
|
+
dataimago-sync --check exit 1 if any listed file differs from the release
|
|
45
|
+
dataimago-sync --from <checkout> write a local, unreleased build (--check then fails)
|
|
46
|
+
dataimago-sync --config <file> read design.json from <file>
|
|
47
|
+
dataimago-sync --help this text
|
|
48
|
+
|
|
49
|
+
Run it from the pinned release: npx -p @dataimago/css@<version> dataimago-sync`;
|
|
50
|
+
|
|
51
|
+
const FLAGS = new Set(['--check', '--help', '--self-test']);
|
|
52
|
+
const VALUE_FLAGS = new Set(['--from', '--config']);
|
|
53
|
+
|
|
54
|
+
/** Refuse unknown flags before anything runs: a typo'd --check must not silently re-sync. */
|
|
55
|
+
function checkArgs(args) {
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
if (VALUE_FLAGS.has(args[i])) { i += 1; continue; }
|
|
58
|
+
if (!FLAGS.has(args[i])) throw new ConfigError(`unknown argument ${args[i]} (see --help)`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function argValue(args, flag) {
|
|
63
|
+
const i = args.indexOf(flag);
|
|
64
|
+
if (i === -1) return null;
|
|
65
|
+
if (!args[i + 1] || args[i + 1].startsWith('--')) throw new ConfigError(`${flag} needs a path`);
|
|
66
|
+
return path.resolve(args[i + 1]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The package this bin ships in: dist/bin/dataimago-sync.mjs → ../.. (symlinks resolved). */
|
|
70
|
+
function ownPackageRoot() {
|
|
71
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Rewrite only the stamp's version tokens to mark a copy as local and unreleased. */
|
|
75
|
+
function localize(text, distPath, pkg, version, sha) {
|
|
76
|
+
const marker = `${LOCAL}(${sha})`;
|
|
77
|
+
if (distPath.endsWith('.json')) {
|
|
78
|
+
const parsed = JSON.parse(text);
|
|
79
|
+
parsed._meta.version = marker;
|
|
80
|
+
parsed._meta.restore = String(parsed._meta.restore).replace(`${pkg}@${version}`, `${pkg}@${marker}`);
|
|
81
|
+
return JSON.stringify(parsed, null, 2) + '\n';
|
|
82
|
+
}
|
|
83
|
+
const nl = text.indexOf('\n');
|
|
84
|
+
return text.slice(0, nl).split(`${pkg}@${version}`).join(`${pkg}@${marker}`) + text.slice(nl);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function gitShortSha(dir) {
|
|
88
|
+
const r = spawnSync('git', ['-C', dir, 'rev-parse', '--short', 'HEAD'], { encoding: 'utf8' });
|
|
89
|
+
return r.status === 0 ? r.stdout.trim() : 'unknown';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Validate everything, return the plan of {key, src, dest}; throws ConfigError before any write. */
|
|
93
|
+
function plan(args) {
|
|
94
|
+
const configPath = argValue(args, '--config') ?? path.resolve('.dataimago', 'design.json');
|
|
95
|
+
// The consumer root holds .dataimago/; a --config file elsewhere is its own root.
|
|
96
|
+
const configDir = path.dirname(configPath);
|
|
97
|
+
const consumerRoot = path.basename(configDir) === '.dataimago' ? path.dirname(configDir) : configDir;
|
|
98
|
+
const design = readJson(configPath, 'design.json');
|
|
99
|
+
if (typeof design.package !== 'string' || typeof design.version !== 'string' || !design.outputs || typeof design.outputs !== 'object') {
|
|
100
|
+
throw new ConfigError('design.json needs "package", "version" and an "outputs" object');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const fromCheckout = argValue(args, '--from');
|
|
104
|
+
const own = readJson(path.join(ownPackageRoot(), 'package.json'), 'package.json of this bin');
|
|
105
|
+
if (design.package !== own.name) {
|
|
106
|
+
throw new ConfigError(`design.json asks for ${design.package}, but this is ${own.name}. Run: npx -p ${design.package}@${design.version} dataimago-sync`);
|
|
107
|
+
}
|
|
108
|
+
let pkgRoot = ownPackageRoot();
|
|
109
|
+
let local = null;
|
|
110
|
+
if (fromCheckout) {
|
|
111
|
+
pkgRoot = path.join(fromCheckout, 'packages', design.package.replace(/^@dataimago\//, ''));
|
|
112
|
+
const built = readJson(path.join(pkgRoot, 'package.json'), `${design.package} in the --from checkout`);
|
|
113
|
+
local = { version: built.version, sha: gitShortSha(fromCheckout) };
|
|
114
|
+
} else if (design.version !== own.version) {
|
|
115
|
+
throw new ConfigError(`design.json pins ${design.package}@${design.version}, but this is ${own.version}. Run: npx -p ${design.package}@${design.version} dataimago-sync`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const manifest = readJson(path.join(pkgRoot, 'dist', 'outputs.json'), 'outputs.json (is the package built?)');
|
|
119
|
+
const available = Object.keys(manifest.files ?? {});
|
|
120
|
+
const steps = [];
|
|
121
|
+
const problems = [];
|
|
122
|
+
for (const [key, destSpec] of Object.entries(design.outputs)) {
|
|
123
|
+
if (!available.includes(key)) {
|
|
124
|
+
problems.push(`${design.package} does not ship "${key}". Available outputs:\n ${available.join('\n ')}`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (typeof destSpec !== 'string' || destSpec === '' || path.isAbsolute(destSpec)) {
|
|
128
|
+
problems.push(`destination for "${key}" must be a relative path inside the consumer, got ${JSON.stringify(destSpec)}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const dest = path.resolve(consumerRoot, destSpec.endsWith('/') ? path.join(destSpec, path.basename(key)) : destSpec);
|
|
132
|
+
const rel = path.relative(consumerRoot, dest);
|
|
133
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
134
|
+
problems.push(`destination for "${key}" escapes the consumer root: ${destSpec}`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (rel === '' || (fs.existsSync(dest) && fs.statSync(dest).isDirectory())) {
|
|
138
|
+
problems.push(`destination for "${key}" is a directory: write "${destSpec.replace(/\/?$/, '/')}" to put the file inside it`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (steps.some((s) => s.dest === dest)) {
|
|
142
|
+
problems.push(`two outputs are mapped to the same destination: ${rel}`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
steps.push({ key, src: path.join(pkgRoot, 'dist', key), dest, rel });
|
|
146
|
+
}
|
|
147
|
+
if (problems.length) throw new ConfigError(problems.join('\n '));
|
|
148
|
+
return { steps, design, local };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function sync(args) {
|
|
152
|
+
const { steps, design, local } = plan(args);
|
|
153
|
+
for (const { key, src, dest, rel } of steps) {
|
|
154
|
+
let bytes = fs.readFileSync(src);
|
|
155
|
+
if (local) bytes = Buffer.from(localize(bytes.toString('utf8'), key, design.package, local.version, local.sha));
|
|
156
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
157
|
+
fs.writeFileSync(dest, bytes);
|
|
158
|
+
console.log(` ✓ ${rel} ← ${design.package}@${local ? `${LOCAL}(${local.sha})` : design.version} dist/${key}`);
|
|
159
|
+
}
|
|
160
|
+
if (local) console.log(`\n⚠ local, unreleased design files: \`dataimago-sync --check\` fails until you re-sync from a release.`);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function check(args) {
|
|
165
|
+
const { steps, design } = plan(args.filter((a) => a !== '--check'));
|
|
166
|
+
const failures = [];
|
|
167
|
+
for (const { src, dest, rel } of steps) {
|
|
168
|
+
if (!fs.existsSync(dest)) { failures.push(`${rel}: missing`); continue; }
|
|
169
|
+
const have = fs.readFileSync(dest);
|
|
170
|
+
// The marker lives in the stamp: line 1, or the leading _meta object of a JSON file.
|
|
171
|
+
if (have.subarray(0, 1024).includes(LOCAL)) { failures.push(`${rel}: written by --from (local, unreleased); re-sync from a release`); continue; }
|
|
172
|
+
if (!have.equals(fs.readFileSync(src))) failures.push(`${rel}: differs from ${design.package}@${design.version}`);
|
|
173
|
+
}
|
|
174
|
+
for (const f of failures) console.error(` ✗ ${f}`);
|
|
175
|
+
if (failures.length) {
|
|
176
|
+
console.error(`\n✗ dataimago-sync --check: ${failures.length} file(s) out of sync. Run: npx -p ${design.package}@${design.version} dataimago-sync`);
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
console.log(`✓ dataimago-sync --check: ${steps.length} file(s) match ${design.package}@${design.version}`);
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function main(args) {
|
|
184
|
+
try {
|
|
185
|
+
checkArgs(args);
|
|
186
|
+
if (args.includes('--help')) {
|
|
187
|
+
console.log(USAGE);
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
if (args.includes('--check')) {
|
|
191
|
+
if (args.includes('--from')) throw new ConfigError('--check verifies a release; it cannot be combined with --from');
|
|
192
|
+
return check(args);
|
|
193
|
+
}
|
|
194
|
+
return sync(args);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
if (e instanceof ConfigError) {
|
|
197
|
+
console.error(`✗ dataimago-sync: ${e.message}`);
|
|
198
|
+
return 2;
|
|
199
|
+
}
|
|
200
|
+
throw e;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── Self-test ────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
async function selfTest() {
|
|
207
|
+
const stampUrl = new URL('./stamp.mjs', import.meta.url);
|
|
208
|
+
if (!fs.existsSync(fileURLToPath(stampUrl))) {
|
|
209
|
+
console.error('dataimago-sync --self-test runs from the dataimago-design repo (tools/formats/).');
|
|
210
|
+
process.exit(2);
|
|
211
|
+
}
|
|
212
|
+
const { stamp, stampJson } = await import(stampUrl);
|
|
213
|
+
const assert = (await import('node:assert/strict')).default;
|
|
214
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'di-sync-'));
|
|
215
|
+
|
|
216
|
+
// A fake published package: <tmp>/pkg with dist/{quarto/a.scss, dataviz/p.json, outputs.json, bin/}.
|
|
217
|
+
function makePackage(root, name, version) {
|
|
218
|
+
const files = {
|
|
219
|
+
'quarto/a.scss': stamp({ kind: 'scss', pkg: name, version, distPath: 'quarto/a.scss', body: '$x: 1 !default;\n' }),
|
|
220
|
+
'dataviz/p.json': stampJson({ pkg: name, version, distPath: 'dataviz/p.json', data: { a: '#E69F00' } }),
|
|
221
|
+
};
|
|
222
|
+
fs.mkdirSync(path.join(root, 'dist', 'bin'), { recursive: true });
|
|
223
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name, version }));
|
|
224
|
+
for (const [p, text] of Object.entries(files)) {
|
|
225
|
+
fs.mkdirSync(path.dirname(path.join(root, 'dist', p)), { recursive: true });
|
|
226
|
+
fs.writeFileSync(path.join(root, 'dist', p), text);
|
|
227
|
+
}
|
|
228
|
+
fs.writeFileSync(path.join(root, 'dist', 'outputs.json'), JSON.stringify({ package: name, version, files: Object.fromEntries(Object.keys(files).sort().map((k) => [k, 'x'])) }, null, 2));
|
|
229
|
+
fs.copyFileSync(fileURLToPath(import.meta.url), path.join(root, 'dist', 'bin', 'dataimago-sync.mjs'));
|
|
230
|
+
return { files, bin: path.join(root, 'dist', 'bin', 'dataimago-sync.mjs') };
|
|
231
|
+
}
|
|
232
|
+
const pkg = makePackage(path.join(tmp, 'pkg'), '@dataimago/css', '9.9.9');
|
|
233
|
+
// A fake dataimago-design checkout for --from: packages/css/{package.json,dist/}.
|
|
234
|
+
makePackage(path.join(tmp, 'checkout', 'packages', 'css'), '@dataimago/css', '9.9.10');
|
|
235
|
+
|
|
236
|
+
const consumer = path.join(tmp, 'consumer');
|
|
237
|
+
const writeDesign = (design) => {
|
|
238
|
+
fs.mkdirSync(path.join(consumer, '.dataimago'), { recursive: true });
|
|
239
|
+
fs.writeFileSync(path.join(consumer, '.dataimago', 'design.json'), JSON.stringify(design));
|
|
240
|
+
};
|
|
241
|
+
const base = { package: '@dataimago/css', version: '9.9.9', outputs: { 'quarto/a.scss': 'styles/', 'dataviz/p.json': 'figs/palette.json' } };
|
|
242
|
+
const run = (...args) => spawnSync(process.execPath, [pkg.bin, ...args], { cwd: consumer, encoding: 'utf8' });
|
|
243
|
+
const exists = (rel) => fs.existsSync(path.join(consumer, rel));
|
|
244
|
+
|
|
245
|
+
// (a) sync writes exact bytes; --check passes.
|
|
246
|
+
writeDesign(base);
|
|
247
|
+
let r = run();
|
|
248
|
+
assert.equal(r.status, 0, r.stderr);
|
|
249
|
+
assert.equal(fs.readFileSync(path.join(consumer, 'styles/a.scss'), 'utf8'), pkg.files['quarto/a.scss']);
|
|
250
|
+
assert.equal(fs.readFileSync(path.join(consumer, 'figs/palette.json'), 'utf8'), pkg.files['dataviz/p.json']);
|
|
251
|
+
assert.equal(run('--check').status, 0);
|
|
252
|
+
|
|
253
|
+
// (b) one appended byte fails --check, naming the file.
|
|
254
|
+
fs.appendFileSync(path.join(consumer, 'styles/a.scss'), ' ');
|
|
255
|
+
r = run('--check');
|
|
256
|
+
assert.equal(r.status, 1);
|
|
257
|
+
assert.match(r.stdout + r.stderr, /styles\/a\.scss/);
|
|
258
|
+
run();
|
|
259
|
+
|
|
260
|
+
// (c) version mismatch: config error with the matching command.
|
|
261
|
+
writeDesign({ ...base, version: '9.9.8' });
|
|
262
|
+
r = run();
|
|
263
|
+
assert.equal(r.status, 2);
|
|
264
|
+
assert.match(r.stderr, /npx -p @dataimago\/css@9\.9\.8 dataimago-sync/);
|
|
265
|
+
|
|
266
|
+
// (d) an output the package does not ship: config error, lists outputs, writes nothing.
|
|
267
|
+
fs.rmSync(path.join(consumer, 'styles'), { recursive: true });
|
|
268
|
+
writeDesign({ ...base, outputs: { ...base.outputs, 'quarto/components.scss': 'styles/' } });
|
|
269
|
+
r = run();
|
|
270
|
+
assert.equal(r.status, 2);
|
|
271
|
+
assert.match(r.stderr, /quarto\/components\.scss/);
|
|
272
|
+
assert.match(r.stderr, /dataviz\/p\.json/);
|
|
273
|
+
assert.ok(!exists('styles/a.scss'), 'nothing written on a config error');
|
|
274
|
+
|
|
275
|
+
// (e) destinations escaping the consumer root are refused before writing.
|
|
276
|
+
for (const dest of ['../x/', '/abs/']) {
|
|
277
|
+
writeDesign({ ...base, outputs: { 'quarto/a.scss': 'styles/', 'dataviz/p.json': dest } });
|
|
278
|
+
r = run();
|
|
279
|
+
assert.equal(r.status, 2, `destination ${dest} must be refused`);
|
|
280
|
+
assert.ok(!exists('styles/a.scss'), `nothing written when ${dest} is refused`);
|
|
281
|
+
}
|
|
282
|
+
assert.ok(!fs.existsSync(path.join(tmp, 'x')));
|
|
283
|
+
|
|
284
|
+
// (f) --from writes local-unreleased stamps that --check always rejects.
|
|
285
|
+
writeDesign(base);
|
|
286
|
+
r = run('--from', path.join(tmp, 'checkout'));
|
|
287
|
+
assert.equal(r.status, 0, r.stderr);
|
|
288
|
+
assert.match(fs.readFileSync(path.join(consumer, 'styles/a.scss'), 'utf8').split('\n')[0], new RegExp(LOCAL));
|
|
289
|
+
assert.match(fs.readFileSync(path.join(consumer, 'figs/palette.json'), 'utf8'), new RegExp(LOCAL));
|
|
290
|
+
r = run('--check');
|
|
291
|
+
assert.equal(r.status, 1);
|
|
292
|
+
assert.match(r.stdout + r.stderr, /--from/);
|
|
293
|
+
|
|
294
|
+
// (g) a design.json for another package is a config error.
|
|
295
|
+
writeDesign({ ...base, package: '@dataimago/css-neutral' });
|
|
296
|
+
assert.equal(run().status, 2);
|
|
297
|
+
|
|
298
|
+
// (h) --config outside .dataimago/: the consumer root is the config's own directory.
|
|
299
|
+
fs.rmSync(path.join(consumer, 'styles'), { recursive: true, force: true });
|
|
300
|
+
fs.writeFileSync(path.join(consumer, 'design.json'), JSON.stringify(base));
|
|
301
|
+
r = run('--config', path.join(consumer, 'design.json'));
|
|
302
|
+
assert.equal(r.status, 0, r.stderr);
|
|
303
|
+
assert.ok(exists('styles/a.scss'), '--config ./design.json writes inside the consumer');
|
|
304
|
+
assert.ok(!fs.existsSync(path.join(tmp, 'styles')), 'nothing written above the consumer');
|
|
305
|
+
fs.rmSync(path.join(consumer, 'design.json'));
|
|
306
|
+
|
|
307
|
+
// (i) an unknown flag (a typo'd --check) is a config error and writes nothing; --help writes nothing.
|
|
308
|
+
writeDesign(base);
|
|
309
|
+
const before = fs.readFileSync(path.join(consumer, 'styles/a.scss'));
|
|
310
|
+
fs.appendFileSync(path.join(consumer, 'styles/a.scss'), 'x');
|
|
311
|
+
r = run('--chek');
|
|
312
|
+
assert.equal(r.status, 2);
|
|
313
|
+
assert.match(r.stderr, /--chek/);
|
|
314
|
+
r = run('--help');
|
|
315
|
+
assert.equal(r.status, 0);
|
|
316
|
+
assert.match(r.stdout, /--check/);
|
|
317
|
+
assert.notDeepEqual(fs.readFileSync(path.join(consumer, 'styles/a.scss')), before, 'neither run re-synced');
|
|
318
|
+
run();
|
|
319
|
+
|
|
320
|
+
// (j) a destination that is the root or an existing directory (no trailing /) is refused up front.
|
|
321
|
+
fs.rmSync(path.join(consumer, 'figs'), { recursive: true, force: true });
|
|
322
|
+
fs.mkdirSync(path.join(consumer, 'figs'));
|
|
323
|
+
for (const dest of ['.', 'figs']) {
|
|
324
|
+
fs.rmSync(path.join(consumer, 'styles'), { recursive: true, force: true });
|
|
325
|
+
writeDesign({ ...base, outputs: { 'quarto/a.scss': 'styles/', 'dataviz/p.json': dest } });
|
|
326
|
+
r = run();
|
|
327
|
+
assert.equal(r.status, 2, `destination ${dest} must be refused`);
|
|
328
|
+
assert.ok(!exists('styles/a.scss'), `nothing written when ${dest} is refused`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// (k) two outputs on one destination is a config error.
|
|
332
|
+
writeDesign({ ...base, outputs: { 'quarto/a.scss': 'same.txt', 'dataviz/p.json': 'same.txt' } });
|
|
333
|
+
r = run();
|
|
334
|
+
assert.equal(r.status, 2);
|
|
335
|
+
assert.ok(!exists('same.txt'));
|
|
336
|
+
|
|
337
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
338
|
+
console.log('dataimago-sync self-test: all checks passed');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (process.argv.includes('--self-test')) await selfTest();
|
|
342
|
+
else process.exitCode = main(process.argv.slice(2));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* @dataimago/css-neutral@0.1.0-alpha.10 dist/dataviz/dataimago-dataviz.css · sha256:4fb36e2401c1cd21f57d50c017ac080377c3943362a5b6c5ce2cd658080e4499 · generated from tokens; do not edit · restore: npx -p @dataimago/css-neutral@0.1.0-alpha.10 dataimago-sync */
|
|
2
|
+
/* dataimago data-viz palette (Okabe-Ito). Categorical colour is never the only channel. */
|
|
3
|
+
:root, [data-bs-theme="light"], body.quarto-light {
|
|
4
|
+
--color-dataviz-okabe-ito-orange: #E69F00;
|
|
5
|
+
--color-dataviz-okabe-ito-sky-blue: #56B4E9;
|
|
6
|
+
--color-dataviz-okabe-ito-bluish-green: #009E73;
|
|
7
|
+
--color-dataviz-okabe-ito-yellow: #F0E442;
|
|
8
|
+
--color-dataviz-okabe-ito-blue: #0072B2;
|
|
9
|
+
--color-dataviz-okabe-ito-vermillion: #D55E00;
|
|
10
|
+
--color-dataviz-okabe-ito-reddish-purple: #CC79A7;
|
|
11
|
+
--color-dataviz-okabe-ito-black: #000000;
|
|
12
|
+
--color-dataviz-categorical-1: #E69F00;
|
|
13
|
+
--color-dataviz-categorical-2: #56B4E9;
|
|
14
|
+
--color-dataviz-categorical-3: #009E73;
|
|
15
|
+
--color-dataviz-categorical-4: #F0E442;
|
|
16
|
+
--color-dataviz-categorical-5: #0072B2;
|
|
17
|
+
--color-dataviz-categorical-6: #D55E00;
|
|
18
|
+
--color-dataviz-categorical-7: #CC79A7;
|
|
19
|
+
--color-dataviz-categorical-8: #000000;
|
|
20
|
+
--color-dataviz-uncertainty-outer: #D6D6D6;
|
|
21
|
+
--color-dataviz-uncertainty-union: #9E9E9E;
|
|
22
|
+
--color-dataviz-uncertainty-inner: #333333;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[data-bs-theme="dark"],
|
|
26
|
+
body.quarto-dark {
|
|
27
|
+
--color-dataviz-okabe-ito-black: #FDF9F2;
|
|
28
|
+
--color-dataviz-categorical-8: #FDF9F2;
|
|
29
|
+
--color-dataviz-uncertainty-outer: #333333;
|
|
30
|
+
--color-dataviz-uncertainty-union: #6B6B6B;
|
|
31
|
+
--color-dataviz-uncertainty-inner: #D6D6D6;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@media (prefers-color-scheme: dark) {
|
|
35
|
+
:root:where(:not([data-bs-theme="light"])) {
|
|
36
|
+
--color-dataviz-okabe-ito-black: #FDF9F2;
|
|
37
|
+
--color-dataviz-categorical-8: #FDF9F2;
|
|
38
|
+
--color-dataviz-uncertainty-outer: #333333;
|
|
39
|
+
--color-dataviz-uncertainty-union: #6B6B6B;
|
|
40
|
+
--color-dataviz-uncertainty-inner: #D6D6D6;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"package": "@dataimago/css-neutral",
|
|
4
|
+
"version": "0.1.0-alpha.10",
|
|
5
|
+
"path": "dist/dataviz/dataimago-dataviz.json",
|
|
6
|
+
"sha256": "28160e151fca196be3e36790d6b67db232d8df060e8d9556b29dfee50f4b278b",
|
|
7
|
+
"note": "generated from tokens; do not edit",
|
|
8
|
+
"restore": "npx -p @dataimago/css-neutral@0.1.0-alpha.10 dataimago-sync"
|
|
9
|
+
},
|
|
10
|
+
"okabe-ito": {
|
|
11
|
+
"orange": "#E69F00",
|
|
12
|
+
"sky-blue": "#56B4E9",
|
|
13
|
+
"bluish-green": "#009E73",
|
|
14
|
+
"yellow": "#F0E442",
|
|
15
|
+
"blue": "#0072B2",
|
|
16
|
+
"vermillion": "#D55E00",
|
|
17
|
+
"reddish-purple": "#CC79A7",
|
|
18
|
+
"black": "#000000"
|
|
19
|
+
},
|
|
20
|
+
"categorical": [
|
|
21
|
+
"#E69F00",
|
|
22
|
+
"#56B4E9",
|
|
23
|
+
"#009E73",
|
|
24
|
+
"#F0E442",
|
|
25
|
+
"#0072B2",
|
|
26
|
+
"#D55E00",
|
|
27
|
+
"#CC79A7",
|
|
28
|
+
"#000000"
|
|
29
|
+
],
|
|
30
|
+
"uncertainty": {
|
|
31
|
+
"outer": "#D6D6D6",
|
|
32
|
+
"union": "#9E9E9E",
|
|
33
|
+
"inner": "#333333"
|
|
34
|
+
},
|
|
35
|
+
"dark": {
|
|
36
|
+
"okabe-ito.black": "#FDF9F2",
|
|
37
|
+
"categorical.8": "#FDF9F2",
|
|
38
|
+
"uncertainty.outer": "#333333",
|
|
39
|
+
"uncertainty.union": "#6B6B6B",
|
|
40
|
+
"uncertainty.inner": "#D6D6D6"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
% @dataimago/css-neutral@0.1.0-alpha.10 dist/dataviz/dataimago-dataviz.tex · sha256:d7be36cb429ce8a1f19c95a7da8f57a7fcfda85fd2b0113e086913d37e9fa645 · generated from tokens; do not edit · restore: npx -p @dataimago/css-neutral@0.1.0-alpha.10 dataimago-sync
|
|
2
|
+
% dataimago data-viz palette (Okabe-Ito), light mode, for PSTricks and TikZ. Requires xcolor.
|
|
3
|
+
\definecolor{di-dataviz-okabe-ito-orange}{HTML}{E69F00}
|
|
4
|
+
\definecolor{di-dataviz-okabe-ito-sky-blue}{HTML}{56B4E9}
|
|
5
|
+
\definecolor{di-dataviz-okabe-ito-bluish-green}{HTML}{009E73}
|
|
6
|
+
\definecolor{di-dataviz-okabe-ito-yellow}{HTML}{F0E442}
|
|
7
|
+
\definecolor{di-dataviz-okabe-ito-blue}{HTML}{0072B2}
|
|
8
|
+
\definecolor{di-dataviz-okabe-ito-vermillion}{HTML}{D55E00}
|
|
9
|
+
\definecolor{di-dataviz-okabe-ito-reddish-purple}{HTML}{CC79A7}
|
|
10
|
+
\definecolor{di-dataviz-okabe-ito-black}{HTML}{000000}
|
|
11
|
+
\colorlet{di-dataviz-categorical-1}{di-dataviz-okabe-ito-orange}
|
|
12
|
+
\colorlet{di-dataviz-categorical-2}{di-dataviz-okabe-ito-sky-blue}
|
|
13
|
+
\colorlet{di-dataviz-categorical-3}{di-dataviz-okabe-ito-bluish-green}
|
|
14
|
+
\colorlet{di-dataviz-categorical-4}{di-dataviz-okabe-ito-yellow}
|
|
15
|
+
\colorlet{di-dataviz-categorical-5}{di-dataviz-okabe-ito-blue}
|
|
16
|
+
\colorlet{di-dataviz-categorical-6}{di-dataviz-okabe-ito-vermillion}
|
|
17
|
+
\colorlet{di-dataviz-categorical-7}{di-dataviz-okabe-ito-reddish-purple}
|
|
18
|
+
\colorlet{di-dataviz-categorical-8}{di-dataviz-okabe-ito-black}
|
|
19
|
+
\definecolor{di-dataviz-uncertainty-outer}{HTML}{D6D6D6}
|
|
20
|
+
\definecolor{di-dataviz-uncertainty-union}{HTML}{9E9E9E}
|
|
21
|
+
\definecolor{di-dataviz-uncertainty-inner}{HTML}{333333}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @dataimago/css-neutral@0.1.0-alpha.10 dist/dataviz/dataimago_dataviz.R · sha256:6443982f105b0f5e4a9d90ac2d1f6ce9fce2db4e8e8148b47f71bfc438940055 · generated from tokens; do not edit · restore: npx -p @dataimago/css-neutral@0.1.0-alpha.10 dataimago-sync
|
|
2
|
+
# dataimago data-viz palette (Okabe-Ito). Categorical colour is never the only channel:
|
|
3
|
+
# pair it with marker shape, line type or a label.
|
|
4
|
+
|
|
5
|
+
dataimago_okabe_ito <- c(
|
|
6
|
+
orange = "#E69F00",
|
|
7
|
+
sky_blue = "#56B4E9",
|
|
8
|
+
bluish_green = "#009E73",
|
|
9
|
+
yellow = "#F0E442",
|
|
10
|
+
blue = "#0072B2",
|
|
11
|
+
vermillion = "#D55E00",
|
|
12
|
+
reddish_purple = "#CC79A7",
|
|
13
|
+
black = "#000000"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
dataimago_categorical <- c(
|
|
17
|
+
"#E69F00",
|
|
18
|
+
"#56B4E9",
|
|
19
|
+
"#009E73",
|
|
20
|
+
"#F0E442",
|
|
21
|
+
"#0072B2",
|
|
22
|
+
"#D55E00",
|
|
23
|
+
"#CC79A7",
|
|
24
|
+
"#000000"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
dataimago_uncertainty_bands <- c(
|
|
28
|
+
outer = "#D6D6D6",
|
|
29
|
+
union = "#9E9E9E",
|
|
30
|
+
inner = "#333333"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Dark-mode variants (approved overrides; identical elsewhere).
|
|
34
|
+
|
|
35
|
+
dataimago_okabe_ito_dark <- c(
|
|
36
|
+
orange = "#E69F00",
|
|
37
|
+
sky_blue = "#56B4E9",
|
|
38
|
+
bluish_green = "#009E73",
|
|
39
|
+
yellow = "#F0E442",
|
|
40
|
+
blue = "#0072B2",
|
|
41
|
+
vermillion = "#D55E00",
|
|
42
|
+
reddish_purple = "#CC79A7",
|
|
43
|
+
black = "#FDF9F2"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
dataimago_categorical_dark <- c(
|
|
47
|
+
"#E69F00",
|
|
48
|
+
"#56B4E9",
|
|
49
|
+
"#009E73",
|
|
50
|
+
"#F0E442",
|
|
51
|
+
"#0072B2",
|
|
52
|
+
"#D55E00",
|
|
53
|
+
"#CC79A7",
|
|
54
|
+
"#FDF9F2"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
dataimago_uncertainty_bands_dark <- c(
|
|
58
|
+
outer = "#333333",
|
|
59
|
+
union = "#6B6B6B",
|
|
60
|
+
inner = "#D6D6D6"
|
|
61
|
+
)
|