@hegemonart/get-design-done 1.33.6 → 1.34.2
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/.claude-plugin/marketplace.json +7 -3
- package/.claude-plugin/plugin.json +6 -2
- package/CHANGELOG.md +46 -0
- package/README.md +18 -0
- package/agents/compose-executor.md +142 -0
- package/agents/design-context-builder.md +37 -1
- package/agents/design-verifier.md +21 -18
- package/agents/email-executor.md +148 -0
- package/agents/flutter-executor.md +147 -0
- package/agents/swift-executor.md +226 -0
- package/connections/android-emulator.md +107 -0
- package/connections/connections.md +6 -0
- package/connections/litmus.md +134 -0
- package/connections/xcode-simulator.md +108 -0
- package/package.json +1 -1
- package/reference/email-design.md +219 -0
- package/reference/native-platforms.md +273 -0
- package/reference/registry.json +14 -0
- package/scripts/lib/design-tokens/_native-shared.cjs +206 -0
- package/scripts/lib/design-tokens/compose.cjs +150 -0
- package/scripts/lib/design-tokens/flutter.cjs +128 -0
- package/scripts/lib/design-tokens/index.cjs +13 -0
- package/scripts/lib/design-tokens/swift.cjs +122 -0
- package/scripts/lib/email/validate-email-html.cjs +157 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-tokens/_native-shared.cjs — shared precision-contract helpers for the
|
|
3
|
+
* native emitters (swift.cjs / compose.cjs / flutter.cjs), Phase 34.1 Plan 01.
|
|
4
|
+
*
|
|
5
|
+
* The single implementation of the PRECISION CONTRACT documented in
|
|
6
|
+
* reference/native-platforms.md, so all three emitters convert color /
|
|
7
|
+
* dimension / typography / non-mappable values identically:
|
|
8
|
+
* COLOR hex #RGB/#RGBA/#RRGGBB/#RRGGBBAA -> 8-bit channels EXACT;
|
|
9
|
+
* #RGB/#RGBA expand by nibble duplication; alpha tracked.
|
|
10
|
+
* DIMENSION Npx | unit-less -> integer (round-half-up) + logical-px double.
|
|
11
|
+
* TYPOGRAPHY strings -> pass-through.
|
|
12
|
+
* NON-MAPPABLE var()/calc()/gradient/rem/em -> verbatim, excluded.
|
|
13
|
+
*
|
|
14
|
+
* Pure: no fs, no network, no Date, no process.env, no child_process (D-10).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
// Markers embedded in every emitted line so the symmetric re-extractors can
|
|
20
|
+
// recover the EXACT canonical token key (native identifiers are mangled, e.g.
|
|
21
|
+
// `color-primary` -> `colorPrimary`, so the key must travel alongside).
|
|
22
|
+
const TOKEN_MARKER = '// token: ';
|
|
23
|
+
const NONMAPPABLE_MARKER = '// non-mappable: ';
|
|
24
|
+
|
|
25
|
+
const HEX_RE = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
26
|
+
const PX_RE = /^-?\d+(?:\.\d+)?px$/;
|
|
27
|
+
const UNITLESS_RE = /^-?\d+(?:\.\d+)?$/;
|
|
28
|
+
const NONMAPPABLE_RE = /(^var\(|^calc\(|gradient\(|\b(rem|em)$|\d(rem|em)$)/i;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read `.tokens` from a Phase-23 TokenSet or a bare {tokens} map.
|
|
32
|
+
* @param {*} tokenSet
|
|
33
|
+
* @returns {Record<string,string>}
|
|
34
|
+
*/
|
|
35
|
+
function readTokens(tokenSet) {
|
|
36
|
+
if (
|
|
37
|
+
!tokenSet ||
|
|
38
|
+
typeof tokenSet !== 'object' ||
|
|
39
|
+
!tokenSet.tokens ||
|
|
40
|
+
typeof tokenSet.tokens !== 'object'
|
|
41
|
+
) {
|
|
42
|
+
throw new TypeError(
|
|
43
|
+
'native emitter: expected { tokens: Record<string,string> } (or a Phase-23 TokenSet)',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return tokenSet.tokens;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stable, sorted [key, value] entries so emit(x) === emit(x) byte-for-byte.
|
|
51
|
+
* @param {Record<string,string>} tokens
|
|
52
|
+
* @returns {[string,string][]}
|
|
53
|
+
*/
|
|
54
|
+
function sortedEntries(tokens) {
|
|
55
|
+
return Object.keys(tokens)
|
|
56
|
+
.sort()
|
|
57
|
+
.map((k) => [k, String(tokens[k])]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Classify a token value per the precision contract. The value is the
|
|
62
|
+
* authority; the key prefix is only a hint (see native-platforms.md §2).
|
|
63
|
+
* @param {string} _key
|
|
64
|
+
* @param {string} value
|
|
65
|
+
* @returns {'non-mappable'|'color'|'dimension'|'typography'}
|
|
66
|
+
*/
|
|
67
|
+
function classify(_key, value) {
|
|
68
|
+
const v = String(value).trim();
|
|
69
|
+
if (NONMAPPABLE_RE.test(v)) return 'non-mappable';
|
|
70
|
+
if (HEX_RE.test(v)) return 'color';
|
|
71
|
+
if (PX_RE.test(v) || UNITLESS_RE.test(v)) return 'dimension';
|
|
72
|
+
return 'typography';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Expand #RGB/#RGBA shorthand to #RRGGBB/#RRGGBBAA and split into 8-bit
|
|
77
|
+
* channels. Alpha defaults to opaque (255) when absent; `hadAlpha` records
|
|
78
|
+
* whether the source carried an alpha channel so the re-extractor can emit a
|
|
79
|
+
* 6- or 8-digit hex faithfully.
|
|
80
|
+
* @param {string} hex
|
|
81
|
+
* @returns {{r:number,g:number,b:number,a:number,hadAlpha:boolean}}
|
|
82
|
+
*/
|
|
83
|
+
function parseHexChannels(hex) {
|
|
84
|
+
let h = String(hex).trim().replace(/^#/, '');
|
|
85
|
+
let hadAlpha = false;
|
|
86
|
+
if (h.length === 3) {
|
|
87
|
+
h = h.split('').map((c) => c + c).join('');
|
|
88
|
+
} else if (h.length === 4) {
|
|
89
|
+
h = h.split('').map((c) => c + c).join('');
|
|
90
|
+
hadAlpha = true;
|
|
91
|
+
} else if (h.length === 8) {
|
|
92
|
+
hadAlpha = true;
|
|
93
|
+
}
|
|
94
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
95
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
96
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
97
|
+
const a = hadAlpha ? parseInt(h.slice(6, 8), 16) : 255;
|
|
98
|
+
return { r, g, b, a, hadAlpha };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Recover the canonical hex from 8-bit channels. Emits 6-digit #RRGGBB when
|
|
103
|
+
* the original had no alpha, 8-digit #RRGGBBAA when it did (contract: implied
|
|
104
|
+
* opaque alpha is dropped on the way back so #3B82F6 round-trips exactly).
|
|
105
|
+
* @param {number} r
|
|
106
|
+
* @param {number} g
|
|
107
|
+
* @param {number} b
|
|
108
|
+
* @param {number} a
|
|
109
|
+
* @param {boolean} hadAlpha
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
function channelsToHex(r, g, b, a, hadAlpha) {
|
|
113
|
+
const h2 = (n) => (n & 0xff).toString(16).padStart(2, '0');
|
|
114
|
+
const base = `#${h2(r)}${h2(g)}${h2(b)}`;
|
|
115
|
+
return hadAlpha ? `${base}${h2(a)}` : base;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Pack channels into a Compose/Flutter 0xAARRGGBB literal (uppercase digits).
|
|
120
|
+
* @param {{r:number,g:number,b:number,a:number}} ch
|
|
121
|
+
* @returns {string} e.g. "0xFF3B82F6"
|
|
122
|
+
*/
|
|
123
|
+
function channelsToArgbLiteral({ r, g, b, a }) {
|
|
124
|
+
const h2 = (n) => (n & 0xff).toString(16).toUpperCase().padStart(2, '0');
|
|
125
|
+
return `0x${h2(a)}${h2(r)}${h2(g)}${h2(b)}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Strip a 0xAARRGGBB literal back into channels.
|
|
130
|
+
* @param {string} literal e.g. "0xFF3B82F6"
|
|
131
|
+
* @returns {{r:number,g:number,b:number,a:number}}
|
|
132
|
+
*/
|
|
133
|
+
function argbLiteralToChannels(literal) {
|
|
134
|
+
const h = String(literal).replace(/^0x/i, '').padStart(8, '0');
|
|
135
|
+
return {
|
|
136
|
+
a: parseInt(h.slice(0, 2), 16),
|
|
137
|
+
r: parseInt(h.slice(2, 4), 16),
|
|
138
|
+
g: parseInt(h.slice(4, 6), 16),
|
|
139
|
+
b: parseInt(h.slice(6, 8), 16),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* px (or unit-less) -> nearest integer, round-half-up (for pt/dp).
|
|
145
|
+
* @param {string} value
|
|
146
|
+
* @returns {number}
|
|
147
|
+
*/
|
|
148
|
+
function pxToInt(value) {
|
|
149
|
+
const n = parseFloat(String(value));
|
|
150
|
+
return Math.floor(n + 0.5);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* px (or unit-less) -> logical-px double form string for Dart (`16` -> `16.0`).
|
|
155
|
+
* @param {string} value
|
|
156
|
+
* @returns {string}
|
|
157
|
+
*/
|
|
158
|
+
function pxToDouble(value) {
|
|
159
|
+
const n = parseFloat(String(value));
|
|
160
|
+
return Number.isInteger(n) ? `${n}.0` : String(n);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Recover the canonical `Npx` string from an emitted numeric form.
|
|
165
|
+
* @param {string|number} num
|
|
166
|
+
* @returns {string}
|
|
167
|
+
*/
|
|
168
|
+
function numToPx(num) {
|
|
169
|
+
const n = parseFloat(String(num));
|
|
170
|
+
return Number.isInteger(n) ? `${n}px` : `${n}px`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* dash-case token -> camelCase Swift/Kotlin/Dart identifier.
|
|
175
|
+
* `color-primary` -> `colorPrimary`; leading digit gets a `t` prefix.
|
|
176
|
+
* @param {string} key
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
function swiftIdent(key) {
|
|
180
|
+
let id = String(key).replace(/[^A-Za-z0-9]+(.)?/g, (_, c) =>
|
|
181
|
+
c ? c.toUpperCase() : '',
|
|
182
|
+
);
|
|
183
|
+
if (!id) id = 'token';
|
|
184
|
+
if (/^[0-9]/.test(id)) id = 't' + id;
|
|
185
|
+
return id;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
TOKEN_MARKER,
|
|
190
|
+
NONMAPPABLE_MARKER,
|
|
191
|
+
HEX_RE,
|
|
192
|
+
PX_RE,
|
|
193
|
+
UNITLESS_RE,
|
|
194
|
+
NONMAPPABLE_RE,
|
|
195
|
+
readTokens,
|
|
196
|
+
sortedEntries,
|
|
197
|
+
classify,
|
|
198
|
+
parseHexChannels,
|
|
199
|
+
channelsToHex,
|
|
200
|
+
channelsToArgbLiteral,
|
|
201
|
+
argbLiteralToChannels,
|
|
202
|
+
pxToInt,
|
|
203
|
+
pxToDouble,
|
|
204
|
+
numToPx,
|
|
205
|
+
swiftIdent,
|
|
206
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-tokens/compose.cjs — Jetpack Compose (Kotlin) theme emitter +
|
|
3
|
+
* re-extractor (Phase 34.1 Plan 01).
|
|
4
|
+
*
|
|
5
|
+
* Consumes the canonical Phase-23 token map ({ tokens: Record<string,string> })
|
|
6
|
+
* and emits a deterministic Kotlin theme source string: `Color` vals, a
|
|
7
|
+
* `Shapes` block (radius tokens), a `Typography` block (font/text tokens), and
|
|
8
|
+
* a `MaterialTheme` wiring object — per the PRECISION CONTRACT in
|
|
9
|
+
* reference/native-platforms.md.
|
|
10
|
+
*
|
|
11
|
+
* Precision contract:
|
|
12
|
+
* COLOR hex -> Color(0xAARRGGBB) long. 8-bit channels EXACT; #RGB
|
|
13
|
+
* expands; alpha 0xFF when absent.
|
|
14
|
+
* DIMENSION Npx -> N.dp (integer dp, round-half-up).
|
|
15
|
+
* TYPOGRAPHY strings -> Kotlin string literal, pass-through.
|
|
16
|
+
* NON-MAPPABLE var()/calc()/gradient/rem/em -> verbatim comment, EXCLUDED.
|
|
17
|
+
*
|
|
18
|
+
* Each emitted token line carries a `// token: <original-key>` marker so the
|
|
19
|
+
* symmetric `reextractCompose(source)` recovers the exact canonical key+value.
|
|
20
|
+
* Pure: no fs, no network, no Date, no process.env, no child_process (D-10).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
TOKEN_MARKER,
|
|
27
|
+
NONMAPPABLE_MARKER,
|
|
28
|
+
readTokens,
|
|
29
|
+
sortedEntries,
|
|
30
|
+
classify,
|
|
31
|
+
parseHexChannels,
|
|
32
|
+
channelsToArgbLiteral,
|
|
33
|
+
argbLiteralToChannels,
|
|
34
|
+
channelsToHex,
|
|
35
|
+
pxToInt,
|
|
36
|
+
swiftIdent,
|
|
37
|
+
} = require('./_native-shared.cjs');
|
|
38
|
+
|
|
39
|
+
const RADIUS_RE = /(^|[^a-z])radius/i;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Emit a Jetpack Compose / Kotlin theme source string from a token set.
|
|
43
|
+
*
|
|
44
|
+
* @param {{tokens: Record<string,string>}|Record<string,string>} tokenSet
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function emitCompose(tokenSet) {
|
|
48
|
+
const tokens = readTokens(tokenSet);
|
|
49
|
+
const entries = sortedEntries(tokens);
|
|
50
|
+
const lines = [];
|
|
51
|
+
lines.push('// Generated by get-design-done — Jetpack Compose theme tokens.');
|
|
52
|
+
lines.push('// See reference/native-platforms.md (token-bridge precision contract).');
|
|
53
|
+
lines.push('import androidx.compose.ui.graphics.Color');
|
|
54
|
+
lines.push('import androidx.compose.ui.unit.dp');
|
|
55
|
+
lines.push('import androidx.compose.material3.Shapes');
|
|
56
|
+
lines.push('import androidx.compose.material3.Typography');
|
|
57
|
+
lines.push('import androidx.compose.foundation.shape.RoundedCornerShape');
|
|
58
|
+
lines.push('');
|
|
59
|
+
lines.push('object GDDTheme {');
|
|
60
|
+
|
|
61
|
+
// Colors
|
|
62
|
+
for (const [key, value] of entries) {
|
|
63
|
+
if (classify(key, value) !== 'color') continue;
|
|
64
|
+
const { r, g, b, a, hadAlpha } = parseHexChannels(value);
|
|
65
|
+
lines.push(
|
|
66
|
+
` val ${swiftIdent(key)} = Color(${channelsToArgbLiteral({ r, g, b, a })}) ${TOKEN_MARKER}${key} hadAlpha=${hadAlpha ? 1 : 0}`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Dimensions (dp)
|
|
71
|
+
for (const [key, value] of entries) {
|
|
72
|
+
if (classify(key, value) !== 'dimension') continue;
|
|
73
|
+
lines.push(
|
|
74
|
+
` val ${swiftIdent(key)} = ${pxToInt(value)}.dp ${TOKEN_MARKER}${key}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Typography (strings)
|
|
79
|
+
for (const [key, value] of entries) {
|
|
80
|
+
if (classify(key, value) !== 'typography') continue;
|
|
81
|
+
lines.push(
|
|
82
|
+
` val ${swiftIdent(key)} = ${JSON.stringify(value)} ${TOKEN_MARKER}${key}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Non-mappable — verbatim, excluded from identity set
|
|
87
|
+
for (const [key, value] of entries) {
|
|
88
|
+
if (classify(key, value) !== 'non-mappable') continue;
|
|
89
|
+
lines.push(` ${NONMAPPABLE_MARKER}${key} = ${value}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Shapes block (from radius dimension tokens) — wires dp into RoundedCornerShape
|
|
93
|
+
lines.push('');
|
|
94
|
+
lines.push(' val Shapes = Shapes(');
|
|
95
|
+
for (const [key, value] of entries) {
|
|
96
|
+
if (classify(key, value) !== 'dimension') continue;
|
|
97
|
+
if (!RADIUS_RE.test(key)) continue;
|
|
98
|
+
lines.push(` // ${key}: RoundedCornerShape(${pxToInt(value)}.dp)`);
|
|
99
|
+
}
|
|
100
|
+
lines.push(' )');
|
|
101
|
+
|
|
102
|
+
// Typography block (MaterialTheme wiring) — references the font tokens above
|
|
103
|
+
lines.push(' val Typography = Typography()');
|
|
104
|
+
lines.push('}');
|
|
105
|
+
lines.push('');
|
|
106
|
+
return lines.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const COMPOSE_COLOR_RE = new RegExp(
|
|
110
|
+
String.raw`val \w+ = Color\((0x[0-9a-fA-F]{8})\)\s*` +
|
|
111
|
+
TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
112
|
+
String.raw`(\S+)\s+hadAlpha=([01])`,
|
|
113
|
+
);
|
|
114
|
+
const COMPOSE_DIM_RE = new RegExp(
|
|
115
|
+
String.raw`val \w+ = (\d+)\.dp\s*` +
|
|
116
|
+
TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
117
|
+
String.raw`(\S+)`,
|
|
118
|
+
);
|
|
119
|
+
const COMPOSE_STR_RE = new RegExp(
|
|
120
|
+
String.raw`val \w+ = ("(?:[^"\\]|\\.)*")\s*` +
|
|
121
|
+
TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
122
|
+
String.raw`(\S+)`,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Recover the canonical token map from an emitted Compose source string.
|
|
127
|
+
* Non-mappable lines and the Shapes-comment lines are NOT recovered.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} source
|
|
130
|
+
* @returns {{tokens: Record<string,string>}}
|
|
131
|
+
*/
|
|
132
|
+
function reextractCompose(source) {
|
|
133
|
+
/** @type {Record<string,string>} */
|
|
134
|
+
const tokens = {};
|
|
135
|
+
for (const line of String(source).split(/\r?\n/)) {
|
|
136
|
+
if (line.includes(NONMAPPABLE_MARKER)) continue;
|
|
137
|
+
let m;
|
|
138
|
+
if ((m = COMPOSE_COLOR_RE.exec(line))) {
|
|
139
|
+
const ch = argbLiteralToChannels(m[1]);
|
|
140
|
+
tokens[m[2]] = channelsToHex(ch.r, ch.g, ch.b, ch.a, m[3] === '1');
|
|
141
|
+
} else if ((m = COMPOSE_DIM_RE.exec(line))) {
|
|
142
|
+
tokens[m[2]] = `${m[1]}px`;
|
|
143
|
+
} else if ((m = COMPOSE_STR_RE.exec(line))) {
|
|
144
|
+
tokens[m[2]] = JSON.parse(m[1]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { tokens };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { emitCompose, reextractCompose };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-tokens/flutter.cjs — Flutter (Dart) theme emitter + re-extractor
|
|
3
|
+
* (Phase 34.1 Plan 01).
|
|
4
|
+
*
|
|
5
|
+
* Consumes the canonical Phase-23 token map ({ tokens: Record<string,string> })
|
|
6
|
+
* and emits a deterministic Dart theme source string: a `GDDTheme` constants
|
|
7
|
+
* class plus a `themeData()` builder wiring `ColorScheme` + `TextTheme` into a
|
|
8
|
+
* `ThemeData` — per the PRECISION CONTRACT in reference/native-platforms.md.
|
|
9
|
+
*
|
|
10
|
+
* Precision contract:
|
|
11
|
+
* COLOR hex -> Color(0xAARRGGBB). 8-bit channels EXACT; #RGB expands;
|
|
12
|
+
* alpha 0xFF when absent.
|
|
13
|
+
* DIMENSION Npx -> logical-px double (`16` -> `16.0`); NOT rounded.
|
|
14
|
+
* TYPOGRAPHY strings -> Dart string literal, pass-through.
|
|
15
|
+
* NON-MAPPABLE var()/calc()/gradient/rem/em -> verbatim comment, EXCLUDED.
|
|
16
|
+
*
|
|
17
|
+
* Each emitted token line carries a `// token: <original-key>` marker so the
|
|
18
|
+
* symmetric `reextractFlutter(source)` recovers the exact canonical key+value.
|
|
19
|
+
* Pure: no fs, no network, no Date, no process.env, no child_process (D-10).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
TOKEN_MARKER,
|
|
26
|
+
NONMAPPABLE_MARKER,
|
|
27
|
+
readTokens,
|
|
28
|
+
sortedEntries,
|
|
29
|
+
classify,
|
|
30
|
+
parseHexChannels,
|
|
31
|
+
channelsToArgbLiteral,
|
|
32
|
+
argbLiteralToChannels,
|
|
33
|
+
channelsToHex,
|
|
34
|
+
pxToDouble,
|
|
35
|
+
swiftIdent,
|
|
36
|
+
} = require('./_native-shared.cjs');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Emit a Flutter / Dart theme source string from a token set.
|
|
40
|
+
*
|
|
41
|
+
* @param {{tokens: Record<string,string>}|Record<string,string>} tokenSet
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function emitFlutter(tokenSet) {
|
|
45
|
+
const tokens = readTokens(tokenSet);
|
|
46
|
+
const entries = sortedEntries(tokens);
|
|
47
|
+
const lines = [];
|
|
48
|
+
lines.push('// Generated by get-design-done — Flutter theme tokens.');
|
|
49
|
+
lines.push('// See reference/native-platforms.md (token-bridge precision contract).');
|
|
50
|
+
lines.push("import 'package:flutter/material.dart';");
|
|
51
|
+
lines.push('');
|
|
52
|
+
lines.push('class GDDTheme {');
|
|
53
|
+
|
|
54
|
+
for (const [key, value] of entries) {
|
|
55
|
+
const kind = classify(key, value);
|
|
56
|
+
const ident = swiftIdent(key);
|
|
57
|
+
if (kind === 'color') {
|
|
58
|
+
const { r, g, b, a, hadAlpha } = parseHexChannels(value);
|
|
59
|
+
lines.push(
|
|
60
|
+
` static const ${ident} = Color(${channelsToArgbLiteral({ r, g, b, a })}); ${TOKEN_MARKER}${key} hadAlpha=${hadAlpha ? 1 : 0}`,
|
|
61
|
+
);
|
|
62
|
+
} else if (kind === 'dimension') {
|
|
63
|
+
lines.push(
|
|
64
|
+
` static const ${ident} = ${pxToDouble(value)}; ${TOKEN_MARKER}${key}`,
|
|
65
|
+
);
|
|
66
|
+
} else if (kind === 'typography') {
|
|
67
|
+
lines.push(
|
|
68
|
+
` static const ${ident} = ${JSON.stringify(value)}; ${TOKEN_MARKER}${key}`,
|
|
69
|
+
);
|
|
70
|
+
} else {
|
|
71
|
+
lines.push(` ${NONMAPPABLE_MARKER}${key} = ${value}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ThemeData builder wiring ColorScheme + TextTheme (MaterialTheme analog).
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(' static ThemeData themeData() => ThemeData(');
|
|
78
|
+
lines.push(' colorScheme: ColorScheme.fromSeed(seedColor: GDDTheme.colorPrimary),');
|
|
79
|
+
lines.push(' textTheme: const TextTheme(),');
|
|
80
|
+
lines.push(' );');
|
|
81
|
+
lines.push('}');
|
|
82
|
+
lines.push('');
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const FLUTTER_COLOR_RE = new RegExp(
|
|
87
|
+
String.raw`static const \w+ = Color\((0x[0-9a-fA-F]{8})\);\s*` +
|
|
88
|
+
TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
89
|
+
String.raw`(\S+)\s+hadAlpha=([01])`,
|
|
90
|
+
);
|
|
91
|
+
const FLUTTER_DIM_RE = new RegExp(
|
|
92
|
+
String.raw`static const \w+ = (\d+(?:\.\d+)?);\s*` +
|
|
93
|
+
TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
94
|
+
String.raw`(\S+)`,
|
|
95
|
+
);
|
|
96
|
+
const FLUTTER_STR_RE = new RegExp(
|
|
97
|
+
String.raw`static const \w+ = ("(?:[^"\\]|\\.)*");\s*` +
|
|
98
|
+
TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
99
|
+
String.raw`(\S+)`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Recover the canonical token map from an emitted Flutter source string.
|
|
104
|
+
* Non-mappable lines + the ThemeData builder lines are NOT recovered.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} source
|
|
107
|
+
* @returns {{tokens: Record<string,string>}}
|
|
108
|
+
*/
|
|
109
|
+
function reextractFlutter(source) {
|
|
110
|
+
/** @type {Record<string,string>} */
|
|
111
|
+
const tokens = {};
|
|
112
|
+
for (const line of String(source).split(/\r?\n/)) {
|
|
113
|
+
if (line.includes(NONMAPPABLE_MARKER)) continue;
|
|
114
|
+
let m;
|
|
115
|
+
if ((m = FLUTTER_COLOR_RE.exec(line))) {
|
|
116
|
+
const ch = argbLiteralToChannels(m[1]);
|
|
117
|
+
tokens[m[2]] = channelsToHex(ch.r, ch.g, ch.b, ch.a, m[3] === '1');
|
|
118
|
+
} else if ((m = FLUTTER_DIM_RE.exec(line))) {
|
|
119
|
+
// Recover canonical Npx: a logical-px double `16.0` -> `16px`.
|
|
120
|
+
tokens[m[2]] = `${parseFloat(m[1])}px`;
|
|
121
|
+
} else if ((m = FLUTTER_STR_RE.exec(line))) {
|
|
122
|
+
tokens[m[2]] = JSON.parse(m[1]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { tokens };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { emitFlutter, reextractFlutter };
|
|
@@ -17,6 +17,12 @@ const { readJsConst } = require('./js-const.cjs');
|
|
|
17
17
|
const { readTailwind } = require('./tailwind.cjs');
|
|
18
18
|
const { readFigma } = require('./figma.cjs');
|
|
19
19
|
|
|
20
|
+
// Phase 34.1 — native theme emitters (token-bridge, D-02: extend the engine,
|
|
21
|
+
// do not fork a native IR). See reference/native-platforms.md.
|
|
22
|
+
const { emitSwift, reextractSwift } = require('./swift.cjs');
|
|
23
|
+
const { emitCompose, reextractCompose } = require('./compose.cjs');
|
|
24
|
+
const { emitFlutter, reextractFlutter } = require('./flutter.cjs');
|
|
25
|
+
|
|
20
26
|
/**
|
|
21
27
|
* @typedef {Object} TokenSet
|
|
22
28
|
* @property {Object<string, string>} tokens
|
|
@@ -97,4 +103,11 @@ module.exports = {
|
|
|
97
103
|
readJsConst,
|
|
98
104
|
readTailwind,
|
|
99
105
|
readFigma,
|
|
106
|
+
// Phase 34.1 native token-bridge emitters + symmetric re-extractors.
|
|
107
|
+
emitSwift,
|
|
108
|
+
emitCompose,
|
|
109
|
+
emitFlutter,
|
|
110
|
+
reextractSwift,
|
|
111
|
+
reextractCompose,
|
|
112
|
+
reextractFlutter,
|
|
100
113
|
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-tokens/swift.cjs — SwiftUI theme emitter + re-extractor
|
|
3
|
+
* (Phase 34.1 Plan 01).
|
|
4
|
+
*
|
|
5
|
+
* Consumes the canonical Phase-23 token map ({ tokens: Record<string,string> })
|
|
6
|
+
* and emits a deterministic SwiftUI theme source string: an `enum GDDTheme`
|
|
7
|
+
* exposing static `Color` / `CGFloat` (pt) / `String` constants per the
|
|
8
|
+
* PRECISION CONTRACT in reference/native-platforms.md.
|
|
9
|
+
*
|
|
10
|
+
* Precision contract (the round-trip's definition of "identity preserved"):
|
|
11
|
+
* COLOR hex #RGB/#RRGGBB/#RRGGBBAA -> Color(red:G/255 ...). 8-bit
|
|
12
|
+
* channels EXACT (numerator-over-255 form avoids float drift);
|
|
13
|
+
* #RGB expands to #RRGGBB; alpha opaque (255) when absent.
|
|
14
|
+
* DIMENSION Npx -> CGFloat integer points (round-half-up).
|
|
15
|
+
* TYPOGRAPHY font-family / strings -> String literal, pass-through.
|
|
16
|
+
* NON-MAPPABLE var()/calc()/gradient/rem/em -> verbatim trailing comment,
|
|
17
|
+
* EXCLUDED from the round-trip identity set.
|
|
18
|
+
*
|
|
19
|
+
* Each emitted token line carries a `// token: <original-key>` marker so the
|
|
20
|
+
* symmetric `reextractSwift(source)` recovers the exact canonical key + value.
|
|
21
|
+
* Pure: no fs, no network, no Date, no process.env, no child_process (D-10).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
TOKEN_MARKER,
|
|
28
|
+
NONMAPPABLE_MARKER,
|
|
29
|
+
readTokens,
|
|
30
|
+
sortedEntries,
|
|
31
|
+
classify,
|
|
32
|
+
parseHexChannels,
|
|
33
|
+
channelsToHex,
|
|
34
|
+
pxToInt,
|
|
35
|
+
swiftIdent,
|
|
36
|
+
} = require('./_native-shared.cjs');
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Emit a SwiftUI theme source string from a token set.
|
|
40
|
+
*
|
|
41
|
+
* @param {{tokens: Record<string,string>}|Record<string,string>} tokenSet
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function emitSwift(tokenSet) {
|
|
45
|
+
const tokens = readTokens(tokenSet);
|
|
46
|
+
const lines = [];
|
|
47
|
+
lines.push('// Generated by get-design-done — SwiftUI theme tokens.');
|
|
48
|
+
lines.push('// See reference/native-platforms.md (token-bridge precision contract).');
|
|
49
|
+
lines.push('import SwiftUI');
|
|
50
|
+
lines.push('');
|
|
51
|
+
lines.push('enum GDDTheme {');
|
|
52
|
+
for (const [key, value] of sortedEntries(tokens)) {
|
|
53
|
+
const ident = swiftIdent(key);
|
|
54
|
+
const kind = classify(key, value);
|
|
55
|
+
if (kind === 'color') {
|
|
56
|
+
const { r, g, b, a, hadAlpha } = parseHexChannels(value);
|
|
57
|
+
const alphaSlot = hadAlpha
|
|
58
|
+
? `, opacity: ${a}.0/255.0`
|
|
59
|
+
: `, opacity: 255.0/255.0`;
|
|
60
|
+
lines.push(
|
|
61
|
+
` static let ${ident} = Color(red: ${r}.0/255.0, green: ${g}.0/255.0, blue: ${b}.0/255.0${alphaSlot}) ${TOKEN_MARKER}${key} hadAlpha=${hadAlpha ? 1 : 0}`,
|
|
62
|
+
);
|
|
63
|
+
} else if (kind === 'dimension') {
|
|
64
|
+
lines.push(
|
|
65
|
+
` static let ${ident}: CGFloat = ${pxToInt(value)} ${TOKEN_MARKER}${key}`,
|
|
66
|
+
);
|
|
67
|
+
} else if (kind === 'typography') {
|
|
68
|
+
lines.push(
|
|
69
|
+
` static let ${ident} = ${JSON.stringify(value)} ${TOKEN_MARKER}${key}`,
|
|
70
|
+
);
|
|
71
|
+
} else {
|
|
72
|
+
// non-mappable — verbatim, excluded from identity set
|
|
73
|
+
lines.push(` ${NONMAPPABLE_MARKER}${key} = ${value}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
lines.push('}');
|
|
77
|
+
lines.push('');
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const SWIFT_COLOR_RE = new RegExp(
|
|
82
|
+
String.raw`Color\(red:\s*(\d+)\.0/255\.0,\s*green:\s*(\d+)\.0/255\.0,\s*blue:\s*(\d+)\.0/255\.0,\s*opacity:\s*(\d+)\.0/255\.0\)\s*` +
|
|
83
|
+
TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
84
|
+
String.raw`(\S+)\s+hadAlpha=([01])`,
|
|
85
|
+
);
|
|
86
|
+
const SWIFT_DIM_RE = new RegExp(
|
|
87
|
+
String.raw`:\s*CGFloat\s*=\s*(\d+)\s*` +
|
|
88
|
+
TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
89
|
+
String.raw`(\S+)`,
|
|
90
|
+
);
|
|
91
|
+
const SWIFT_STR_RE = new RegExp(
|
|
92
|
+
String.raw`static let \w+ = ("(?:[^"\\]|\\.)*")\s*` +
|
|
93
|
+
TOKEN_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
|
|
94
|
+
String.raw`(\S+)`,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Recover the canonical token map from an emitted SwiftUI source string.
|
|
99
|
+
* Non-mappable lines (NONMAPPABLE_MARKER) are intentionally NOT recovered.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} source
|
|
102
|
+
* @returns {{tokens: Record<string,string>}}
|
|
103
|
+
*/
|
|
104
|
+
function reextractSwift(source) {
|
|
105
|
+
/** @type {Record<string,string>} */
|
|
106
|
+
const tokens = {};
|
|
107
|
+
for (const line of String(source).split(/\r?\n/)) {
|
|
108
|
+
if (line.includes(NONMAPPABLE_MARKER)) continue;
|
|
109
|
+
let m;
|
|
110
|
+
if ((m = SWIFT_COLOR_RE.exec(line))) {
|
|
111
|
+
const [, r, g, b, a, key, hadAlpha] = m;
|
|
112
|
+
tokens[key] = channelsToHex(+r, +g, +b, +a, hadAlpha === '1');
|
|
113
|
+
} else if ((m = SWIFT_DIM_RE.exec(line))) {
|
|
114
|
+
tokens[m[2]] = `${m[1]}px`;
|
|
115
|
+
} else if ((m = SWIFT_STR_RE.exec(line))) {
|
|
116
|
+
tokens[m[2]] = JSON.parse(m[1]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { tokens };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { emitSwift, reextractSwift };
|