@pieai/swimmer-ui-kit 1.3.2 → 1.5.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/CHANGELOG.md +79 -0
- package/README.md +33 -4
- package/bin/swimmer-ui-assets.mjs +83 -0
- package/bin/swimmer-ui-check.mjs +159 -2
- package/dist/index.d.ts +20 -3
- package/dist/index.js +515 -498
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,85 @@
|
|
|
3
3
|
All notable changes to `@pieai/swimmer-ui-kit`.
|
|
4
4
|
Format: [Keep a Changelog](https://keepachangelog.com); versioning: semver.
|
|
5
5
|
|
|
6
|
+
## 1.5.0 — 2026-08-22
|
|
7
|
+
|
|
8
|
+
`swimmer-ui-check` now also fails on token pairs that cannot be read.
|
|
9
|
+
|
|
10
|
+
The raw-colour rule kept consumers on tokens. It never stopped them choosing
|
|
11
|
+
two tokens that do not contrast, and one pairing is genuinely inviting:
|
|
12
|
+
`--game-ui-accent-ink` reads like "the ink for accent things" and means the
|
|
13
|
+
opposite — accent-COLOURED ink, for a surface. Painted on `--game-ui-accent`
|
|
14
|
+
it measures 1.48:1 on night and 1.91:1 on light. It reached a shipping
|
|
15
|
+
product's primary button, the one control every user has to find, and every
|
|
16
|
+
test that product had was green.
|
|
17
|
+
|
|
18
|
+
PRODUCT.md promises contrast-safe token combinations. That promise only ever
|
|
19
|
+
covered the pairs the kit uses itself; nothing checked the pairs a consumer
|
|
20
|
+
built. Now something does.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Contrast checking in `swimmer-ui-check`. Rules that set both a background
|
|
25
|
+
and a colour from bare tokens are resolved against this package's own
|
|
26
|
+
`dist/styles.css`, per theme, and reported below WCAG AA (4.5:1) with the
|
|
27
|
+
ratio and the theme named.
|
|
28
|
+
|
|
29
|
+
### Notes on what it deliberately does not do
|
|
30
|
+
|
|
31
|
+
- Only bare `var(--game-ui-x)` values are judged. `color-mix`, gradients and
|
|
32
|
+
anything composited are skipped: a tint of the accent behind accent-coloured
|
|
33
|
+
text is readable, and reading the first token out of the expression scores it
|
|
34
|
+
1.00:1. The first draft did that and flagged four rules that were fine — a
|
|
35
|
+
linter that cries wolf gets the next real finding skimmed too.
|
|
36
|
+
- Tokens carrying alpha are skipped for the same reason. What they composite
|
|
37
|
+
against is not knowable from a stylesheet.
|
|
38
|
+
|
|
39
|
+
### Compatibility
|
|
40
|
+
|
|
41
|
+
Additive. Existing raw-colour behaviour is unchanged; a project that was clean
|
|
42
|
+
stays clean unless it genuinely has an unreadable pair.
|
|
43
|
+
|
|
44
|
+
## 1.4.0 — 2026-08-22
|
|
45
|
+
|
|
46
|
+
The kit shipped an icon set that no consumer could see. Nothing was broken and
|
|
47
|
+
nothing errored, which was the problem: with no setup the kit draws lettered
|
|
48
|
+
placeholder squares, and a placeholder that looks deliberate gets shipped. A
|
|
49
|
+
product could put eight identical coloured squares in its navigation and never
|
|
50
|
+
file a bug, because it reads as a design choice rather than a missing step.
|
|
51
|
+
|
|
52
|
+
The sculpted PNGs were never missing — 350 files travel inside the package, in
|
|
53
|
+
`dist/assets`. What was missing was any route from there to a path the host
|
|
54
|
+
actually serves. `CLAY_ASSET_BASE_PATH` names an absolute URL on the
|
|
55
|
+
*consumer's* origin, and nothing copied, exported or explained it. The kit's own
|
|
56
|
+
Storybook worked throughout, because its assets sit in `public/`.
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- `swimmer-ui-assets [dir] [--base=…] [--force]` — a `bin` that copies the
|
|
61
|
+
sculpted set into a host's static root. Bundler-agnostic and explicit; no
|
|
62
|
+
postinstall, because a package that writes into your repo on install is worse
|
|
63
|
+
than the problem it solves.
|
|
64
|
+
- `"./assets/*"` export, so bundler-based consumers can import an individual
|
|
65
|
+
file and get a content-hashed URL instead of copying the whole tree.
|
|
66
|
+
- `setClayAssetBasePath()` / `getClayAssetBasePath()` for a CDN or a sub-path
|
|
67
|
+
deploy. Resolution rebases on the way out, so the variant table stays a
|
|
68
|
+
single source of truth.
|
|
69
|
+
- A one-time `console.warn` the first time a placeholder is actually drawn,
|
|
70
|
+
naming the two commands that fix it, and `acknowledgeClayPlaceholders()` for
|
|
71
|
+
anyone using them on purpose.
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- README states plainly that the default is placeholders rather than the icon
|
|
76
|
+
set, and records that the sculpted family is 96px art which cannot take a
|
|
77
|
+
`currentColor` tint — so it is the wrong family for a dense nav rail or
|
|
78
|
+
toolbar however correctly it is served.
|
|
79
|
+
|
|
80
|
+
### Compatibility
|
|
81
|
+
|
|
82
|
+
Additive only. Defaults are unchanged, so every existing consumer renders
|
|
83
|
+
exactly what it rendered before and gains one console line telling it why.
|
|
84
|
+
|
|
6
85
|
## 1.3.2 — 2026-08-17
|
|
7
86
|
|
|
8
87
|
Follows 1.3.1 with the contrast work that release only half did. The new off
|
package/README.md
CHANGED
|
@@ -71,10 +71,39 @@ import '@pieai/swimmer-ui-kit/tailwind.css';
|
|
|
71
71
|
- **Official themes**: light (default) and `night`
|
|
72
72
|
(`<html data-game-ui-theme="night">`). Downstream theming = overriding
|
|
73
73
|
semantic tokens; see the design system guide.
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
- **`swimmer-ui-check`**: lints your CSS for raw colour literals in component
|
|
75
|
+
rules *and* for token pairs that cannot be read. Two tokens are not
|
|
76
|
+
automatically safe together — `--game-ui-accent-ink` is accent-COLOURED ink
|
|
77
|
+
for a surface, `--game-ui-accent-contrast` is the ink meant to sit **on**
|
|
78
|
+
`--game-ui-accent`. Pairing the first with the accent measures 1.48:1.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx swimmer-ui-check src
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- **Clay assets**: two lines of setup, and **skipping them is not a no-op**.
|
|
85
|
+
Out of the box the kit draws *placeholders* — one rounded square per icon
|
|
86
|
+
with a letter in it — not the icon set. They exist so a fresh install
|
|
87
|
+
renders something instead of a broken image, and they are not shippable.
|
|
88
|
+
The real sculpted PNGs travel inside the package and need serving:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx swimmer-ui-assets public # copies dist/assets into your static root
|
|
92
|
+
```
|
|
93
|
+
```ts
|
|
94
|
+
import { setClayAssetMode } from '@pieai/swimmer-ui-kit';
|
|
95
|
+
setClayAssetMode('source'); // once, at your entry
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Serving them somewhere else — a CDN, a sub-path deploy — is
|
|
99
|
+
`setClayAssetBasePath('/my/path')`, and `swimmer-ui-assets public --base=/my/path`
|
|
100
|
+
mirrors the layout to match. If you *want* placeholders, say so with
|
|
101
|
+
`acknowledgeClayPlaceholders()` and the console notice goes quiet.
|
|
102
|
+
|
|
103
|
+
Note on sizing: the sculpted family is 96px art. Below roughly 24px it turns
|
|
104
|
+
to mud, and being PNG it cannot take a `currentColor` tint, so it is the
|
|
105
|
+
wrong family for a dense navigation rail or toolbar. Use the `line` style
|
|
106
|
+
there, or your own glyphs.
|
|
78
107
|
- **Audio helper**: `playGameInteractionSound` (SSR-safe, opt-in via the
|
|
79
108
|
`sound` prop on `GameButton`).
|
|
80
109
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copy the sculpted clay asset set out of the package and into a directory the
|
|
3
|
+
// consuming app actually serves.
|
|
4
|
+
//
|
|
5
|
+
// The kit resolves sculpted icons to absolute URLs under CLAY_ASSET_BASE_PATH
|
|
6
|
+
// ('/assets/game/ui/clay/…'), which is a path on the *host's* origin. The files
|
|
7
|
+
// themselves ship inside the package, in dist/assets. Nothing was closing that
|
|
8
|
+
// gap, so every consumer had to discover it, and the default placeholder mode
|
|
9
|
+
// meant the failure looked like a design choice rather than a missing step.
|
|
10
|
+
//
|
|
11
|
+
// Usage: swimmer-ui-assets [targetDir] [--base=/assets/game/ui/clay/...] [--force]
|
|
12
|
+
// targetDir where the host serves static files from. Default "public".
|
|
13
|
+
// --base the URL path to mirror under targetDir. Defaults to the kit's
|
|
14
|
+
// CLAY_ASSET_BASE_PATH, which is what setClayAssetMode('source')
|
|
15
|
+
// expects with no further configuration.
|
|
16
|
+
// --force overwrite files that already exist (default: skip them).
|
|
17
|
+
//
|
|
18
|
+
// After running this, call setClayAssetMode('source') once at your app entry.
|
|
19
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
20
|
+
import { dirname, join, resolve } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_BASE = '/assets/game/ui/clay/phase03-clay-kit';
|
|
24
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const flags = new Set(args.filter((a) => a.startsWith('--')));
|
|
28
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
29
|
+
|
|
30
|
+
// Two different paths that are easy to conflate: where the files sit inside
|
|
31
|
+
// this package (fixed, mirrors the default URL under dist/) and where the host
|
|
32
|
+
// will serve them from (--base, defaults to the same thing so the common case
|
|
33
|
+
// needs no configuration).
|
|
34
|
+
const trim = (p) => p.replace(/^\/+|\/+$/g, '');
|
|
35
|
+
const baseFlag = args.find((a) => a.startsWith('--base='));
|
|
36
|
+
const servedBase = trim(baseFlag ? baseFlag.slice('--base='.length) : DEFAULT_BASE);
|
|
37
|
+
const targetRoot = resolve(process.cwd(), positional[0] ?? 'public');
|
|
38
|
+
const force = flags.has('--force');
|
|
39
|
+
|
|
40
|
+
const source = join(packageRoot, 'dist', trim(DEFAULT_BASE));
|
|
41
|
+
if (!existsSync(source)) {
|
|
42
|
+
console.error(`swimmer-ui-assets: nothing to copy — ${source} does not exist.`);
|
|
43
|
+
console.error('This package may have been installed without its dist/assets tree.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const destination = join(targetRoot, servedBase);
|
|
48
|
+
|
|
49
|
+
function count(dir) {
|
|
50
|
+
let files = 0;
|
|
51
|
+
let bytes = 0;
|
|
52
|
+
for (const entry of readdirSync(dir)) {
|
|
53
|
+
const full = join(dir, entry);
|
|
54
|
+
const stat = statSync(full);
|
|
55
|
+
if (stat.isDirectory()) {
|
|
56
|
+
const inner = count(full);
|
|
57
|
+
files += inner.files;
|
|
58
|
+
bytes += inner.bytes;
|
|
59
|
+
} else {
|
|
60
|
+
files += 1;
|
|
61
|
+
bytes += stat.size;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { files, bytes };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { files, bytes } = count(source);
|
|
68
|
+
|
|
69
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
70
|
+
cpSync(source, destination, { recursive: true, force, errorOnExist: false });
|
|
71
|
+
|
|
72
|
+
const megabytes = (bytes / 1024 / 1024).toFixed(1);
|
|
73
|
+
console.log(`swimmer-ui-assets: copied ${files} files (${megabytes} MB)`);
|
|
74
|
+
console.log(` from ${source}`);
|
|
75
|
+
console.log(` to ${destination}`);
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log('Now call this once at your app entry, or the kit keeps drawing placeholders:');
|
|
78
|
+
console.log(" import { setClayAssetMode } from '@pieai/swimmer-ui-kit';");
|
|
79
|
+
console.log(" setClayAssetMode('source');");
|
|
80
|
+
if (!force) {
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log('Existing files were left alone. Re-run with --force to overwrite.');
|
|
83
|
+
}
|
package/bin/swimmer-ui-check.mjs
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
// dir directory to scan, default "src"
|
|
17
17
|
// --ext comma-separated extensions to scan, default "css"
|
|
18
18
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
19
|
-
import { extname, join, relative } from 'node:path';
|
|
19
|
+
import { dirname, extname, join, relative, resolve } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
20
21
|
|
|
21
22
|
const RAW_COLOR = /#[0-9a-fA-F]{3,8}\b|\brgba?\(|\bhsla?\(|\boklch\(/g;
|
|
22
23
|
const TOKEN_BLOCK_SELECTOR = /:root\b|\[data-[\w-]*(?:theme|tone)[\w-]*\s*=/i;
|
|
@@ -94,6 +95,139 @@ function findViolations(css) {
|
|
|
94
95
|
return violations;
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/* ---------------------------------------------------------------------------
|
|
99
|
+
* Token pairs that cannot be read.
|
|
100
|
+
*
|
|
101
|
+
* The raw-colour rule above keeps consumers on tokens. It does not stop them
|
|
102
|
+
* choosing two tokens that do not contrast, and the names make one pairing
|
|
103
|
+
* genuinely inviting: `--game-ui-accent-ink` sounds like "the ink for accent
|
|
104
|
+
* things" and means the opposite — accent-COLOURED ink for a surface. Painted
|
|
105
|
+
* on `--game-ui-accent` it measured 1.48:1 in a shipping product, on that
|
|
106
|
+
* product's primary button, and every test it had was green.
|
|
107
|
+
*
|
|
108
|
+
* PRODUCT.md promises "contrast-safe token combinations". That promise only
|
|
109
|
+
* covers the pairs the kit uses itself unless something checks the consumer's.
|
|
110
|
+
* So: read the theme values out of the kit's own styles.css, find rules that
|
|
111
|
+
* set both a token background and a token colour, and compute the ratio.
|
|
112
|
+
* ------------------------------------------------------------------------- */
|
|
113
|
+
|
|
114
|
+
const AA_NORMAL = 4.5;
|
|
115
|
+
|
|
116
|
+
function parseHex(value) {
|
|
117
|
+
const hex = value.trim().replace(/^#/, '');
|
|
118
|
+
if (!/^[0-9a-fA-F]{3,8}$/.test(hex)) return null;
|
|
119
|
+
const full =
|
|
120
|
+
hex.length === 3 || hex.length === 4
|
|
121
|
+
? hex
|
|
122
|
+
.slice(0, 3)
|
|
123
|
+
.split('')
|
|
124
|
+
.map((c) => c + c)
|
|
125
|
+
.join('')
|
|
126
|
+
: hex.slice(0, 6);
|
|
127
|
+
if (full.length !== 6) return null;
|
|
128
|
+
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function relativeLuminance([r, g, b]) {
|
|
132
|
+
const channel = (v) => {
|
|
133
|
+
const s = v / 255;
|
|
134
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
135
|
+
};
|
|
136
|
+
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function contrastRatio(a, b) {
|
|
140
|
+
const la = relativeLuminance(a);
|
|
141
|
+
const lb = relativeLuminance(b);
|
|
142
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Token values per theme, read from the kit's shipped stylesheet.
|
|
147
|
+
*
|
|
148
|
+
* Only fully opaque hex values are kept. A token carrying alpha composites
|
|
149
|
+
* against whatever is behind it, and guessing that would produce confident
|
|
150
|
+
* numbers about a colour nobody can know from here.
|
|
151
|
+
*/
|
|
152
|
+
function themeTokens() {
|
|
153
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
154
|
+
// dist/ is what a consumer installs. src/theme.css is what exists in this
|
|
155
|
+
// repository before a build — and `pnpm test` runs before `pnpm build`, so
|
|
156
|
+
// reading only dist made the check a silent no-op in its own CI while
|
|
157
|
+
// passing locally off a stale dist. Silence is the failure mode this whole
|
|
158
|
+
// check exists to remove, so it must not be the failure mode of the check.
|
|
159
|
+
const sources = [join(packageRoot, 'dist', 'styles.css'), join(packageRoot, 'src', 'theme.css')];
|
|
160
|
+
let css = null;
|
|
161
|
+
for (const candidate of sources) {
|
|
162
|
+
try {
|
|
163
|
+
css = readFileSync(candidate, 'utf8');
|
|
164
|
+
break;
|
|
165
|
+
} catch {
|
|
166
|
+
// try the next one
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (css === null) return null;
|
|
170
|
+
const themes = new Map();
|
|
171
|
+
// The built stylesheet is minified and the attribute value loses its quotes,
|
|
172
|
+
// so both forms have to match or every theme but the default is invisible.
|
|
173
|
+
const blockRe = /(:root|\[data-game-ui-theme=['"]?([\w-]+)['"]?\])\s*\{([^}]*)\}/g;
|
|
174
|
+
for (const match of css.matchAll(blockRe)) {
|
|
175
|
+
const name = match[2] ?? 'light';
|
|
176
|
+
const values = themes.get(name) ?? new Map(themes.get('light') ?? []);
|
|
177
|
+
for (const decl of match[3].matchAll(/(--game-ui-[\w-]+)\s*:\s*(#[0-9a-fA-F]{3,8})/g)) {
|
|
178
|
+
const rgb = decl[2].length === 9 || decl[2].length === 5 ? null : parseHex(decl[2]);
|
|
179
|
+
if (rgb) values.set(decl[1], rgb);
|
|
180
|
+
}
|
|
181
|
+
themes.set(name, values);
|
|
182
|
+
}
|
|
183
|
+
return themes;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* A bare token reference and nothing else: `var(--game-ui-x)` or
|
|
188
|
+
* `var(--game-ui-x, #fallback)`.
|
|
189
|
+
*
|
|
190
|
+
* Deliberately refuses `color-mix(in srgb, var(--game-ui-accent) 18%,
|
|
191
|
+
* transparent)` and gradients. A tint of the accent behind accent-coloured
|
|
192
|
+
* text is perfectly readable, and reading the first token out of the
|
|
193
|
+
* expression would call it 1.00:1 — the first draft of this check did exactly
|
|
194
|
+
* that and flagged four rules that were fine. A linter that cries wolf is
|
|
195
|
+
* worse than no linter, because the next real finding gets skimmed too.
|
|
196
|
+
*/
|
|
197
|
+
const BARE_TOKEN = /^\s*var\(\s*(--game-ui-[\w-]+)\s*(?:,[^()]*)?\)\s*$/;
|
|
198
|
+
|
|
199
|
+
function findContrastViolations(css, themes) {
|
|
200
|
+
if (!themes || themes.size === 0) return [];
|
|
201
|
+
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
|
|
202
|
+
const lineOf = lineFinder(withoutComments);
|
|
203
|
+
const out = [];
|
|
204
|
+
const ruleRe = /\{([^{}]*)\}/g;
|
|
205
|
+
for (const rule of withoutComments.matchAll(ruleRe)) {
|
|
206
|
+
const body = rule[1];
|
|
207
|
+
const bg = /(?:^|[;\s])background(?:-color)?\s*:\s*([^;]+)/.exec(body);
|
|
208
|
+
const fg = /(?:^|[;\s])color\s*:\s*([^;]+)/.exec(body);
|
|
209
|
+
if (!bg || !fg) continue;
|
|
210
|
+
const bgToken = BARE_TOKEN.exec(bg[1])?.[1];
|
|
211
|
+
const fgToken = BARE_TOKEN.exec(fg[1])?.[1];
|
|
212
|
+
if (!bgToken || !fgToken) continue;
|
|
213
|
+
for (const [theme, values] of themes) {
|
|
214
|
+
const bgRgb = values.get(bgToken);
|
|
215
|
+
const fgRgb = values.get(fgToken);
|
|
216
|
+
if (!bgRgb || !fgRgb) continue;
|
|
217
|
+
const ratio = contrastRatio(bgRgb, fgRgb);
|
|
218
|
+
if (ratio >= AA_NORMAL) continue;
|
|
219
|
+
out.push({
|
|
220
|
+
line: lineOf(rule.index + 1),
|
|
221
|
+
theme,
|
|
222
|
+
fgToken,
|
|
223
|
+
bgToken,
|
|
224
|
+
ratio: ratio.toFixed(2),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
97
231
|
const args = process.argv.slice(2);
|
|
98
232
|
const target = args.find((a) => !a.startsWith('--')) ?? 'src';
|
|
99
233
|
const extArg = args.find((a) => a.startsWith('--ext='));
|
|
@@ -111,9 +245,24 @@ try {
|
|
|
111
245
|
process.exit(2);
|
|
112
246
|
}
|
|
113
247
|
|
|
248
|
+
const themes = themeTokens();
|
|
249
|
+
if (!themes || themes.size === 0) {
|
|
250
|
+
console.error(
|
|
251
|
+
"swimmer-ui-check: could not read this package's theme tokens, so contrast was NOT checked. " +
|
|
252
|
+
'Raw-colour linting below still ran.',
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
let contrastCount = 0;
|
|
114
256
|
let violationCount = 0;
|
|
115
257
|
for (const file of files) {
|
|
116
258
|
const text = readFileSync(file, 'utf8');
|
|
259
|
+
for (const pair of findContrastViolations(text, themes)) {
|
|
260
|
+
console.log(
|
|
261
|
+
`${relative(process.cwd(), file)}:${pair.line}: ${pair.fgToken} on ${pair.bgToken} is ` +
|
|
262
|
+
`${pair.ratio}:1 on the ${pair.theme} theme — below AA (${AA_NORMAL}:1)`,
|
|
263
|
+
);
|
|
264
|
+
contrastCount += 1;
|
|
265
|
+
}
|
|
117
266
|
for (const violation of findViolations(text)) {
|
|
118
267
|
console.log(
|
|
119
268
|
`${relative(process.cwd(), file)}:${violation.line}: raw color literal "${violation.text}" — use var(--game-ui-*) instead`,
|
|
@@ -122,7 +271,15 @@ for (const file of files) {
|
|
|
122
271
|
}
|
|
123
272
|
}
|
|
124
273
|
|
|
125
|
-
if (
|
|
274
|
+
if (contrastCount > 0) {
|
|
275
|
+
console.error(
|
|
276
|
+
`\nswimmer-ui-check: ${contrastCount} unreadable token pair(s). Two tokens are not ` +
|
|
277
|
+
'automatically safe together: --game-ui-accent-ink is accent-COLOURED ink for a surface, ' +
|
|
278
|
+
'while --game-ui-accent-contrast is the ink meant to sit on --game-ui-accent.',
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (violationCount > 0 || contrastCount > 0) {
|
|
126
283
|
console.error(
|
|
127
284
|
`\nswimmer-ui-check: ${violationCount} raw color literal(s) in ${files.length} file(s) under "${target}". ` +
|
|
128
285
|
'Raw colors are expected inside :root / [data-*theme*=...] / [data-*tone*=...] token blocks ' +
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ import { ReactNode } from 'react';
|
|
|
6
6
|
import { RefAttributes } from 'react';
|
|
7
7
|
import { TextareaHTMLAttributes } from 'react';
|
|
8
8
|
|
|
9
|
+
/** Opt in to placeholders on purpose, and stop being told about it. */
|
|
10
|
+
export declare function acknowledgeClayPlaceholders(): void;
|
|
11
|
+
|
|
9
12
|
export declare const CLAY_ASSET_BASE_PATH: "/assets/game/ui/clay/phase03-clay-kit";
|
|
10
13
|
|
|
11
14
|
export declare const CLAY_ASSET_SIZE_TOKENS: {
|
|
@@ -1852,6 +1855,8 @@ export declare interface GameWindowPanelProps extends Omit<HTMLAttributes<HTMLEl
|
|
|
1852
1855
|
|
|
1853
1856
|
export declare type GameWindowState = 'normal' | 'minimized' | 'maximized';
|
|
1854
1857
|
|
|
1858
|
+
export declare function getClayAssetBasePath(): string;
|
|
1859
|
+
|
|
1855
1860
|
export declare function getClayAssetMode(): ClayAssetMode;
|
|
1856
1861
|
|
|
1857
1862
|
export declare function getClayCatalogPaths(): typeof CLAY_ASSETS.catalog;
|
|
@@ -1867,11 +1872,23 @@ export declare function playGameInteractionSound(options?: GameInteractionSoundO
|
|
|
1867
1872
|
|
|
1868
1873
|
export declare function playGameInteractionSoundForContext(audioContext: GameInteractionAudioContext, options?: GameInteractionSoundOptions): boolean;
|
|
1869
1874
|
|
|
1875
|
+
/**
|
|
1876
|
+
* Point the sculpted set at somewhere other than the default path — a CDN, a
|
|
1877
|
+
* sub-path deploy, a host whose static root is not `/`. Pass no trailing slash.
|
|
1878
|
+
*
|
|
1879
|
+
* The copy command mirrors the default layout, so a consumer that runs
|
|
1880
|
+
* `swimmer-ui-assets public` never needs this.
|
|
1881
|
+
*/
|
|
1882
|
+
export declare function setClayAssetBasePath(basePath: string): void;
|
|
1883
|
+
|
|
1870
1884
|
/**
|
|
1871
1885
|
* Switch how clay icons resolve globally.
|
|
1872
|
-
* - 'inline' (default):
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
1886
|
+
* - 'inline' (default): lettered placeholders. They are not the icon set —
|
|
1887
|
+
* every one is the same rounded square with a different glyph and colour.
|
|
1888
|
+
* They exist so a fresh install renders *something* rather than a broken
|
|
1889
|
+
* image, and shipping them is a bug.
|
|
1890
|
+
* - 'source': the real sculpted PNGs under the base path. Get the files there
|
|
1891
|
+
* with `npx swimmer-ui-assets <publicDir>`.
|
|
1875
1892
|
*/
|
|
1876
1893
|
export declare function setClayAssetMode(mode: ClayAssetMode): void;
|
|
1877
1894
|
|