@transtyle/exporter-radix 0.1.0-alpha.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.
Files changed (2) hide show
  1. package/package.json +42 -0
  2. package/src/index.js +170 -0
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@transtyle/exporter-radix",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Transtyle exporter for Radix Colors 12-step scales — the role grid's acceptance test.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "dependencies": {
11
+ "@transtyle/ir": "0.1.0-alpha.0"
12
+ },
13
+ "transtyle": {
14
+ "kind": "exporter",
15
+ "name": "radix",
16
+ "irSpec": "v0-draft",
17
+ "pluginApi": "0",
18
+ "targets": {
19
+ "radix": [
20
+ "*"
21
+ ]
22
+ },
23
+ "modes": [
24
+ "color-scheme"
25
+ ],
26
+ "capabilities": [
27
+ "build"
28
+ ]
29
+ },
30
+ "files": [
31
+ "src"
32
+ ],
33
+ "publishConfig": { "access": "public" },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/transtyle/transtyle.git",
37
+ "directory": "packages/exporter-radix"
38
+ },
39
+ "homepage": "https://github.com/transtyle/transtyle#readme",
40
+ "bugs": "https://github.com/transtyle/transtyle/issues",
41
+ "license": "MIT"
42
+ }
package/src/index.js ADDED
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @transtyle/exporter-radix — emits Radix Colors-shaped 12-step scales from
3
+ * the resolved IR. Spec: docs/specs/exporters/radix.md.
4
+ *
5
+ * This is the role grid's acceptance test (proposal 0001 §7): Radix's 12
6
+ * steps ARE the grid, just numbered instead of named. If every step maps
7
+ * cleanly from an existing catalog cell (only 2 of 12 need a fresh mix —
8
+ * steps 2 and 6, both flagged `approximated`), the grid is validated as a
9
+ * universal projection, not a shape invented to fit shadcn or Bootstrap.
10
+ *
11
+ * Mapping (per role, both modes):
12
+ * 1 <- elevation.0.surface (app bg) 7/8 <- outline / outline-hover
13
+ * 2 <- mix(solid, surface(0), 0.96) [approx.] 9/10 <- solid / solid-hover
14
+ * 3/4/5 <- tint / tint-hover / tint-active 11/12 <- text / text-strong
15
+ * 6 <- mix(solid, surface(1), 0.78) [approx.] contrast <- on-solid
16
+ * Alpha steps (`-a1`..`-a12`): the same 12 colors, alpha computed from a
17
+ * fixed ramp (not Radix's real per-color alpha derivation — approximated,
18
+ * documented). `neutral` is also emitted as `--gray-*`, Radix's conventional
19
+ * paired-gray name, alongside `--neutral-*`.
20
+ */
21
+
22
+ import { COLOR_ROLES, droppedDimensions } from '@transtyle/ir';
23
+
24
+ const S = 'semantic.color.';
25
+ const ALPHA_RAMP = [0.05, 0.1, 0.15, 0.22, 0.3, 0.4, 0.5, 0.6, 0.75, 0.85, 0.92, 0.97];
26
+
27
+ export default {
28
+ name: 'radix',
29
+
30
+ emit(normalized, ctx) {
31
+ const light = normalized.modes.light ?? normalized.modes[normalized.defaultMode];
32
+ const dark = normalized.modes.dark;
33
+ const coverage = [];
34
+
35
+ const buildMode = (map, isFirst) => {
36
+ const surface0 = map.get(`${S}elevation.0.surface`)?.value;
37
+ const surface1 = map.get(`${S}elevation.1.surface`)?.value;
38
+ const lines = [];
39
+ for (const role of COLOR_ROLES) {
40
+ const get = (cell) => map.get(`${S}${role}.${cell}`)?.value;
41
+ const solid = get('solid');
42
+ if (!solid) continue;
43
+
44
+ const steps = {
45
+ 1: surface0,
46
+ 2: surface0 && ctx.mix(solid, surface0, 0.96),
47
+ 3: get('tint'),
48
+ 4: get('tint-hover'),
49
+ 5: get('tint-active'),
50
+ 6: surface1 && ctx.mix(solid, surface1, 0.78),
51
+ 7: get('outline'),
52
+ 8: get('outline-hover'),
53
+ 9: solid,
54
+ 10: get('solid-hover'),
55
+ 11: get('text'),
56
+ 12: get('text-strong'),
57
+ };
58
+
59
+ for (const [step, value] of Object.entries(steps)) {
60
+ if (!value) continue;
61
+ const name = `--${role}-${step}`;
62
+ lines.push(` ${name}: ${ctx.formatColor(value)};`);
63
+ if (isFirst) {
64
+ const mixed = step === '2' || step === '6';
65
+ const clamped = ctx.formatHex(value).clamped;
66
+ const notes = [
67
+ ...(mixed ? ['no direct grid cell — mixed toward the surface at a ratio filling the gap between tint and outline'] : []),
68
+ ...(clamped ? ['out of sRGB gamut at this lightness/chroma combination — browsers gamut-map oklch(), which may render noticeably differently than intended'] : []),
69
+ ];
70
+ coverage.push({ variable: name, slot: `${S}${role}.*`, class: (mixed || clamped) ? 'approximated' : 'native', ...(notes.length && { note: notes.join('; ') }) });
71
+ }
72
+ const alpha = ALPHA_RAMP[Number(step) - 1];
73
+ const aName = `--${role}-a${step}`;
74
+ lines.push(` ${aName}: ${ctx.formatColor({ ...value, alpha })};`);
75
+ if (isFirst) coverage.push({ variable: aName, slot: `${S}${role}.*`, class: 'approximated', note: 'fixed alpha ramp, not a colorimetric derivation of Radix\'s real per-color alpha' });
76
+ }
77
+
78
+ const onSolid = get('on-solid');
79
+ if (onSolid) {
80
+ lines.push(` --${role}-contrast: ${ctx.formatColor(onSolid)};`);
81
+ if (isFirst) coverage.push({ variable: `--${role}-contrast`, slot: `${S}${role}.on-solid`, class: 'native' });
82
+ }
83
+
84
+ if (role === 'neutral') {
85
+ for (const [step] of Object.entries(steps)) {
86
+ lines.push(` --gray-${step}: var(--neutral-${step});`);
87
+ lines.push(` --gray-a${step}: var(--neutral-a${step});`);
88
+ }
89
+ lines.push(' --gray-contrast: var(--neutral-contrast);');
90
+ if (isFirst) coverage.push({ variable: '--gray-*', slot: `${S}neutral.*`, class: 'native', note: 'Radix\'s conventional paired-gray name, aliased from neutral' });
91
+ }
92
+ }
93
+ return lines;
94
+ };
95
+
96
+ const lightLines = buildMode(light, true);
97
+ const darkLines = dark ? buildMode(dark, false) : [];
98
+
99
+ coverage.push({ variable: '(P3/wide-gamut variants)', slot: '—', class: 'dropped', note: 'Radix ships a P3 pair per scale for wide-gamut displays; the engine has one OKLCH value per slot, not a gamut-mapped pair' });
100
+ // Mode dimensions this exporter doesn't express (T8, ir.md#modes) — a
101
+ // no-op unless the compile actually declares one, e.g. `density`.
102
+ coverage.push(...droppedDimensions(normalized.dimensionNames, ['color-scheme']));
103
+
104
+ const css = [
105
+ '/*',
106
+ ` * GENERATED by transtyle — do not edit; source: ${ctx.projectName} token files`,
107
+ ' * Target: Radix Colors (12-step scales + alpha + contrast) · rules standard@1',
108
+ ' * A custom palette per https://www.radix-ui.com/colors/docs/overview/naming — ',
109
+ ' * override an existing Radix Themes scale name (see usage.md) to use it with',
110
+ ' * the <Theme accentColor="..."> component, or consume the variables directly.',
111
+ ' */',
112
+ '',
113
+ ':root {',
114
+ ...lightLines,
115
+ '}',
116
+ ...(darkLines.length ? ['', '.dark {', ...darkLines, '}'] : []),
117
+ '',
118
+ ].join('\n');
119
+
120
+ return {
121
+ files: [
122
+ { path: 'radix-colors.transtyle.css', contents: css, kind: 'stylesheet' },
123
+ { path: 'usage.md', contents: renderUsage(ctx, coverage), kind: 'doc' },
124
+ ],
125
+ coverage,
126
+ };
127
+ },
128
+ };
129
+
130
+ function renderUsage(ctx, coverage) {
131
+ const counts = {};
132
+ for (const c of coverage) counts[c.class] = (counts[c.class] ?? 0) + 1;
133
+ const summary = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(' · ');
134
+ return `# Using this Radix Colors palette
135
+
136
+ Generated from the **${ctx.projectName}** design system by transtyle: a 12-step scale (+ 12 alpha steps + a contrast color) per role, in Radix Colors' own naming convention. Coverage: ${summary}.
137
+
138
+ ## Standalone (no React, just the CSS variables)
139
+
140
+ \`\`\`html
141
+ <link rel="stylesheet" href="radix-colors.transtyle.css">
142
+ \`\`\`
143
+
144
+ \`\`\`css
145
+ .my-button { background: var(--primary-9); color: var(--primary-contrast); }
146
+ .my-button:hover { background: var(--primary-10); }
147
+ .my-card { background: var(--primary-2); border: 1px solid var(--primary-6); }
148
+ \`\`\`
149
+
150
+ ## With \`@radix-ui/themes\`
151
+
152
+ The \`<Theme accentColor="...">\` component only accepts Radix's own preset names (it can't take an arbitrary string) — so to drive real Radix Themes components from your brand, **override one existing preset's variables** with your role's scale, then pass that preset's name:
153
+
154
+ \`\`\`css
155
+ /* after @radix-ui/themes/styles.css, before your app renders */
156
+ :root {
157
+ --violet-1: var(--primary-1); --violet-2: var(--primary-2); /* ...through 12, plus -a1..a12 and -contrast */
158
+ }
159
+ \`\`\`
160
+
161
+ \`\`\`jsx
162
+ <Theme accentColor="violet" grayColor="gray">{/* --gray-* is already Radix's own name — no override needed */}</Theme>
163
+ \`\`\`
164
+
165
+ ## Regenerating
166
+
167
+ Never edit this file — change the design system tokens and run \`transtyle build radix\`.
168
+ See \`report.json\` for the full coverage/provenance breakdown.
169
+ `;
170
+ }