@tamagui/cli 3.0.0-beta.924.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build.cjs +5 -9
- package/dist/cli.cjs +79 -11
- package/dist/generate-prompt.cjs +7 -336
- package/dist/icons.cjs +263 -0
- package/dist/migrate.cjs +100 -41
- package/dist/preview-theme.cjs +70 -0
- package/dist/setup-prompt.cjs +73 -14
- package/dist/to-tailwind-default-config.cjs +132 -35
- package/dist/upgrade.cjs +29 -21
- package/package.json +13 -11
- package/src/build.ts +10 -24
- package/src/cli.ts +78 -15
- package/src/generate-prompt.ts +18 -512
- package/src/icons.ts +308 -0
- package/src/migrate.ts +100 -41
- package/src/preview-theme.ts +54 -0
- package/src/setup-prompt.ts +68 -15
- package/src/to-tailwind-default-config.ts +131 -34
- package/src/upgrade.ts +30 -33
- package/types/build.d.ts.map +1 -1
- package/types/generate-prompt.d.ts +2 -2
- package/types/generate-prompt.d.ts.map +1 -1
- package/types/icons.d.ts +14 -0
- package/types/icons.d.ts.map +1 -0
- package/types/preview-theme.d.ts +8 -0
- package/types/preview-theme.d.ts.map +1 -0
- package/types/setup-prompt.d.ts +4 -2
- package/types/setup-prompt.d.ts.map +1 -1
- package/types/to-tailwind-default-config.d.ts.map +1 -1
- package/types/upgrade.d.ts.map +1 -1
package/dist/icons.cjs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all) __defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
16
|
+
get: () => from[key],
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var icons_exports = {};
|
|
28
|
+
__export(icons_exports, {
|
|
29
|
+
HEROICONS_VERSION: () => HEROICONS_VERSION,
|
|
30
|
+
LUCIDE_VERSION: () => LUCIDE_VERSION,
|
|
31
|
+
PHOSPHOR_VERSION: () => PHOSPHOR_VERSION,
|
|
32
|
+
addIcons: () => addIcons,
|
|
33
|
+
svgToComponent: () => svgToComponent
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(icons_exports);
|
|
36
|
+
var import_node_fs = require("node:fs");
|
|
37
|
+
var import_promises = require("node:fs/promises");
|
|
38
|
+
var import_node_path = __toESM(require("node:path"));
|
|
39
|
+
var import_chalk = __toESM(require("chalk"));
|
|
40
|
+
const LUCIDE_VERSION = "1.49.0";
|
|
41
|
+
const HEROICONS_VERSION = "2.2.0";
|
|
42
|
+
const PHOSPHOR_VERSION = "2.1.1";
|
|
43
|
+
const libraries = [
|
|
44
|
+
"lucide",
|
|
45
|
+
"heroicons",
|
|
46
|
+
"phosphor"
|
|
47
|
+
];
|
|
48
|
+
const phosphorWeights = [
|
|
49
|
+
"regular",
|
|
50
|
+
"bold",
|
|
51
|
+
"duotone",
|
|
52
|
+
"fill",
|
|
53
|
+
"light",
|
|
54
|
+
"thin"
|
|
55
|
+
];
|
|
56
|
+
const heroiconsDirs = {
|
|
57
|
+
outline: "24/outline",
|
|
58
|
+
solid: "24/solid",
|
|
59
|
+
mini: "20/solid",
|
|
60
|
+
micro: "16/solid"
|
|
61
|
+
};
|
|
62
|
+
async function addIcons(options) {
|
|
63
|
+
const cwd = options.cwd ?? process.cwd();
|
|
64
|
+
const from = options.from ?? "lucide";
|
|
65
|
+
const outDir = import_node_path.default.resolve(cwd, options.out ?? "components/icons");
|
|
66
|
+
const localDir = libraries.includes(from) ? null : import_node_path.default.resolve(cwd, from);
|
|
67
|
+
if (localDir && !((0, import_node_fs.existsSync)(localDir) && (0, import_node_fs.statSync)(localDir).isDirectory())) {
|
|
68
|
+
throw new Error(`--from must be ${libraries.join(", ")}, or a directory of svg files (got "${from}")`);
|
|
69
|
+
}
|
|
70
|
+
if (!options.names.length) {
|
|
71
|
+
throw new Error("give the icons to add, e.g. `tamagui icons add Search ChevronDown`");
|
|
72
|
+
}
|
|
73
|
+
if (options.weight && from !== "phosphor") {
|
|
74
|
+
throw new Error("--weight applies to phosphor only");
|
|
75
|
+
}
|
|
76
|
+
if (options.weight && !phosphorWeights.includes(options.weight)) {
|
|
77
|
+
throw new Error(`--weight is one of ${phosphorWeights.join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
if (options.variant && from !== "heroicons") {
|
|
80
|
+
throw new Error("--variant applies to heroicons only");
|
|
81
|
+
}
|
|
82
|
+
if (options.variant && !heroiconsDirs[options.variant]) {
|
|
83
|
+
throw new Error(`--variant is one of ${Object.keys(heroiconsDirs).join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
const icons = [...new Set(options.names)].map((arg) => {
|
|
86
|
+
const [name, source = name] = arg.split("=");
|
|
87
|
+
const isKebab = (x) => /^[a-z0-9-]+$/.test(x);
|
|
88
|
+
const isPascal = (x) => /^[A-Z][A-Za-z0-9]*$/.test(x);
|
|
89
|
+
if (!(isKebab(name) || isPascal(name)) || !(isKebab(source) || isPascal(source)) || source !== name && !isPascal(name)) {
|
|
90
|
+
throw new Error(`icon names are PascalCase, kebab-case, or Name=source-name, got "${arg}"`);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
componentName: isKebab(name) ? kebabToPascal(name) : name,
|
|
94
|
+
file: isKebab(source) ? source : pascalToKebab(stripIconSuffix(source))
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
const loaded = await Promise.all(icons.map(async (icon) => {
|
|
98
|
+
if (localDir) {
|
|
99
|
+
const file = import_node_path.default.join(localDir, `${icon.file}.svg`);
|
|
100
|
+
return {
|
|
101
|
+
...icon,
|
|
102
|
+
svg: (0, import_node_fs.existsSync)(file) ? await (0, import_promises.readFile)(file, "utf-8") : null
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const url = sourceUrl(from, icon.file, options);
|
|
106
|
+
const response = await fetch(url);
|
|
107
|
+
if (response.status === 404) return {
|
|
108
|
+
...icon,
|
|
109
|
+
svg: null
|
|
110
|
+
};
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`fetching ${url} failed (${response.status})`);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
...icon,
|
|
116
|
+
svg: await response.text()
|
|
117
|
+
};
|
|
118
|
+
}));
|
|
119
|
+
const unknown = loaded.filter((icon) => icon.svg === null);
|
|
120
|
+
if (unknown.length) {
|
|
121
|
+
throw new Error(`unknown ${from} icon${unknown.length === 1 ? "" : "s"}: ${unknown.map((icon) => `${icon.componentName} (${icon.file}.svg)`).join(", ")}`);
|
|
122
|
+
}
|
|
123
|
+
await (0, import_promises.mkdir)(outDir, { recursive: true });
|
|
124
|
+
for (const icon of loaded) {
|
|
125
|
+
const file = import_node_path.default.join(outDir, `${icon.componentName}.tsx`);
|
|
126
|
+
await (0, import_promises.writeFile)(file, svgToComponent(icon.componentName, icon.svg));
|
|
127
|
+
console.info(`${import_chalk.default.green("wrote")} ${import_node_path.default.relative(cwd, file)}`);
|
|
128
|
+
}
|
|
129
|
+
const components = (await (0, import_promises.readdir)(outDir)).filter((file) => file.endsWith(".tsx")).map((file) => file.slice(0, -".tsx".length)).sort();
|
|
130
|
+
await (0, import_promises.writeFile)(import_node_path.default.join(outDir, "index.ts"), components.map((name) => `export { ${name} } from './${name}'`).join("\n") + "\n");
|
|
131
|
+
console.info(`${import_chalk.default.green("wrote")} ${import_node_path.default.relative(cwd, import_node_path.default.join(outDir, "index.ts"))} (${components.length} icons)`);
|
|
132
|
+
}
|
|
133
|
+
function sourceUrl(library, file, options) {
|
|
134
|
+
switch (library) {
|
|
135
|
+
case "lucide": return `https://unpkg.com/lucide-static@${LUCIDE_VERSION}/icons/${file}.svg`;
|
|
136
|
+
case "heroicons": return `https://unpkg.com/heroicons@${HEROICONS_VERSION}/${heroiconsDirs[options.variant ?? "outline"]}/${file}.svg`;
|
|
137
|
+
case "phosphor": {
|
|
138
|
+
const weight = options.weight ?? "regular";
|
|
139
|
+
const name = weight === "regular" ? file : `${file}-${weight}`;
|
|
140
|
+
return `https://unpkg.com/@phosphor-icons/core@${PHOSPHOR_VERSION}/assets/${weight}/${name}.svg`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function pascalToKebab(name) {
|
|
145
|
+
return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Za-z])(\d)/g, "$1-$2").toLowerCase();
|
|
146
|
+
}
|
|
147
|
+
function kebabToPascal(name) {
|
|
148
|
+
return name.split("-").map((part) => part ? part[0].toUpperCase() + part.slice(1) : part).join("");
|
|
149
|
+
}
|
|
150
|
+
function stripIconSuffix(name) {
|
|
151
|
+
return name.endsWith("Icon") && name.length > 4 ? name.slice(0, -4) : name;
|
|
152
|
+
}
|
|
153
|
+
function parseSvg(svg) {
|
|
154
|
+
const root = {
|
|
155
|
+
tag: "",
|
|
156
|
+
attrs: [],
|
|
157
|
+
children: []
|
|
158
|
+
};
|
|
159
|
+
const stack = [root];
|
|
160
|
+
const tagPattern = /<(\/?)([A-Za-z][A-Za-z0-9]*)((?:"[^"]*"|'[^']*'|[^>"'])*?)(\/?)>/g;
|
|
161
|
+
for (const [, closing, tag, attrText, selfClose] of svg.replace(/<!--[\s\S]*?-->/g, "").matchAll(tagPattern)) {
|
|
162
|
+
if (closing) {
|
|
163
|
+
if (stack.pop()?.tag !== tag) throw new Error(`mismatched </${tag}> in svg`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const node = {
|
|
167
|
+
tag,
|
|
168
|
+
attrs: [],
|
|
169
|
+
children: []
|
|
170
|
+
};
|
|
171
|
+
for (const [, name, double, single] of attrText.matchAll(/([A-Za-z_:][A-Za-z0-9_.:-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
|
|
172
|
+
node.attrs.push([name, double ?? single]);
|
|
173
|
+
}
|
|
174
|
+
stack[stack.length - 1].children.push(node);
|
|
175
|
+
if (!selfClose) stack.push(node);
|
|
176
|
+
}
|
|
177
|
+
const svgNode = root.children.find((node) => node.tag === "svg");
|
|
178
|
+
if (stack.length !== 1 || !svgNode) throw new Error("svg source has no complete <svg> root");
|
|
179
|
+
return svgNode;
|
|
180
|
+
}
|
|
181
|
+
const elements = {
|
|
182
|
+
path: "Path",
|
|
183
|
+
circle: "Circle",
|
|
184
|
+
ellipse: "Ellipse",
|
|
185
|
+
line: "Line",
|
|
186
|
+
polyline: "Polyline",
|
|
187
|
+
polygon: "Polygon",
|
|
188
|
+
rect: "Rect",
|
|
189
|
+
g: "G",
|
|
190
|
+
defs: "Defs",
|
|
191
|
+
clipPath: "ClipPath",
|
|
192
|
+
linearGradient: "LinearGradient",
|
|
193
|
+
radialGradient: "RadialGradient",
|
|
194
|
+
stop: "Stop",
|
|
195
|
+
use: "Use"
|
|
196
|
+
};
|
|
197
|
+
const restrictedNames = [
|
|
198
|
+
"Infinity",
|
|
199
|
+
"NaN",
|
|
200
|
+
"undefined"
|
|
201
|
+
];
|
|
202
|
+
function svgToComponent(name, svg) {
|
|
203
|
+
const root = parseSvg(svg);
|
|
204
|
+
const used = /* @__PURE__ */ new Set();
|
|
205
|
+
const ref = (element) => element === name ? `_${element}` : element;
|
|
206
|
+
const jsxAttr = (attrName, value) => {
|
|
207
|
+
if (attrName === "style") {
|
|
208
|
+
throw new Error(`${name}: style attributes have no react-native-svg equivalent`);
|
|
209
|
+
}
|
|
210
|
+
const prop = attrName === "xlink:href" ? "href" : attrName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
211
|
+
return value === "currentColor" ? `${prop}={color}` : `${prop}="${value}"`;
|
|
212
|
+
};
|
|
213
|
+
const keepAttr = ([attrName]) => !(attrName === "xmlns" || attrName.startsWith("xmlns:") || attrName === "class" || attrName === "version" || attrName.startsWith("data-") || attrName.startsWith("aria-"));
|
|
214
|
+
const rootAttrs = root.attrs.filter(keepAttr);
|
|
215
|
+
const viewBox = rootAttrs.find(([attrName]) => attrName === "viewBox")?.[1];
|
|
216
|
+
if (!viewBox) throw new Error(`${name}: svg source has no viewBox`);
|
|
217
|
+
const rootStroke = rootAttrs.some(([attrName, value]) => attrName === "stroke" && value === "currentColor");
|
|
218
|
+
const strokeWidth = rootAttrs.find(([attrName]) => attrName === "stroke-width")?.[1];
|
|
219
|
+
const emit = (node, indent) => {
|
|
220
|
+
const element = elements[node.tag];
|
|
221
|
+
if (!element) throw new Error(`${name}: unsupported svg element <${node.tag}>`);
|
|
222
|
+
used.add(element);
|
|
223
|
+
const attrs = node.attrs.filter(keepAttr).map(([n, v]) => jsxAttr(n, v));
|
|
224
|
+
if (rootStroke && !node.attrs.some(([n]) => n === "stroke") && node.tag !== "g") {
|
|
225
|
+
attrs.push("stroke={color}");
|
|
226
|
+
}
|
|
227
|
+
const open = `${indent}<${ref(element)}${attrs.map((attr) => ` ${attr}`).join("")}`;
|
|
228
|
+
if (!node.children.length) return `${open} />`;
|
|
229
|
+
return `${open}>
|
|
230
|
+
${node.children.map((child) => emit(child, `${indent} `)).join("\n")}
|
|
231
|
+
${indent}</${ref(element)}>`;
|
|
232
|
+
};
|
|
233
|
+
const children = root.children.map((child) => emit(child, " "));
|
|
234
|
+
if (!children.length) throw new Error(`${name}: svg source draws nothing`);
|
|
235
|
+
const svgAttrs = [
|
|
236
|
+
"width={size}",
|
|
237
|
+
"height={size}",
|
|
238
|
+
...rootAttrs.filter(([attrName]) => attrName !== "width" && attrName !== "height").map(([attrName, value]) => jsxAttr(attrName, value)),
|
|
239
|
+
"{...otherProps}"
|
|
240
|
+
];
|
|
241
|
+
const imports = [...used].sort().map((element) => element === name ? `${element} as _${element}` : element);
|
|
242
|
+
const themedOptions = strokeWidth && strokeWidth !== "2" ? `, { defaultStrokeWidth: ${strokeWidth} }` : "";
|
|
243
|
+
return `${restrictedNames.includes(name) ? "/* eslint-disable no-shadow-restricted-names */\n" : ""}import { memo, type JSX } from 'react'
|
|
244
|
+
import { Svg, ${imports.join(", ")}, type SvgProps } from 'react-native-svg'
|
|
245
|
+
import { themed, type IconProps } from '@tamagui/helpers-icon'
|
|
246
|
+
|
|
247
|
+
export const ${name}: (props: IconProps) => JSX.Element = themed(
|
|
248
|
+
memo(function ${name}(props: IconProps) {
|
|
249
|
+
const { color = 'black', size = 24, ...otherProps } = props as SvgProps & {
|
|
250
|
+
color?: string
|
|
251
|
+
size?: number
|
|
252
|
+
}
|
|
253
|
+
return (
|
|
254
|
+
<Svg
|
|
255
|
+
${svgAttrs.map((attr) => ` ${attr}`).join("\n")}
|
|
256
|
+
>
|
|
257
|
+
${children.join("\n")}
|
|
258
|
+
</Svg>
|
|
259
|
+
)
|
|
260
|
+
})${themedOptions}
|
|
261
|
+
)
|
|
262
|
+
`;
|
|
263
|
+
}
|
package/dist/migrate.cjs
CHANGED
|
@@ -64,6 +64,26 @@ Work like a careful coding agent:
|
|
|
64
64
|
}
|
|
65
65
|
const v2ToV3Prompt = `## v2 -> v3 migration prompt
|
|
66
66
|
|
|
67
|
+
### 0. Choose the intermediate checkpoint before editing
|
|
68
|
+
|
|
69
|
+
The Tamagui package version and the config version are separate. Tamagui V3
|
|
70
|
+
supports Config v5; upgrading the runtime does not require Config v6.
|
|
71
|
+
|
|
72
|
+
- Coming from V2: first reach V3 packages and APIs with the app's existing token, theme, font, media, and animation values. Keep custom config values. If an old config entry is no longer exported, preserve its resolved values in an app-owned config; do not substitute v6 defaults.
|
|
73
|
+
- Coming from Config v5 or v5-subtle: keep that config during the V3 API upgrade. Keep the 12-step adaptive colors, spacing, radii, font scales, and breakpoint meanings. Do not remap them to the v6 scale.
|
|
74
|
+
- Already running V3 with Config v5: this intermediate checkpoint is valid. Inventory remaining V2 APIs before applying codemods; do not repeat completed work or treat Config v5 as an upgrade failure.
|
|
75
|
+
- Do not combine this pass with wholesale \`html.*\` adoption, Tailwind conversion, new skins, or a theme redesign unless separately requested.
|
|
76
|
+
|
|
77
|
+
Record the starting package/config versions and representative light/dark,
|
|
78
|
+
responsive, and native screenshots. Name the intended checkpoint in the plan:
|
|
79
|
+
"V3 API, existing design values." Visual changes during this pass need an
|
|
80
|
+
explanation or a fix, not automatic screenshot acceptance.
|
|
81
|
+
|
|
82
|
+
The checkpoint is complete when the required API migration, report review,
|
|
83
|
+
typecheck/build, and real app checks in step 15 pass. Leave Config v6 and other
|
|
84
|
+
optional design changes in a separate follow-up; do not start them just because
|
|
85
|
+
the V3 API work is finished.
|
|
86
|
+
|
|
67
87
|
### 1. Update dependencies
|
|
68
88
|
|
|
69
89
|
- Bump every \`tamagui\` and \`@tamagui/*\` package together to v3.
|
|
@@ -80,8 +100,10 @@ V3 accepts bare token/theme names and flat clauses only. Run the transactional
|
|
|
80
100
|
flat-values codemod from your project root, dry run first:
|
|
81
101
|
|
|
82
102
|
\`\`\`bash
|
|
83
|
-
npx @tamagui/codemod-flat-values --
|
|
103
|
+
npx @tamagui/codemod-flat-values --source-semantics v2-pixels \\
|
|
104
|
+
--report flat-values-report.md ./src
|
|
84
105
|
npx @tamagui/codemod-flat-values --write \\
|
|
106
|
+
--source-semantics v2-pixels \\
|
|
85
107
|
--report flat-values-report.md \\
|
|
86
108
|
--json flat-values-report.json \\
|
|
87
109
|
./src
|
|
@@ -102,46 +124,22 @@ becomes:
|
|
|
102
124
|
Resolve every report row and rerun until the app has no V2 authoring. Do not
|
|
103
125
|
add a compatibility setting or restore condition-object parsing.
|
|
104
126
|
|
|
105
|
-
### 3.
|
|
127
|
+
### 3. Preserve config values while migrating required APIs
|
|
106
128
|
|
|
107
|
-
-
|
|
108
|
-
- Existing V5 applications can keep \`@tamagui/config/v5\` or \`/v5-subtle\` while migrating incrementally. These are frozen static compatibility packs, so they preserve the V5 token and theme values but will not receive new theme-builder features.
|
|
129
|
+
- Existing Config v5 applications should keep \`@tamagui/config/v5\` or \`/v5-subtle\` while migrating incrementally. These are frozen static compatibility packs, so they preserve the V5 token and theme values but will not receive new theme-builder features.
|
|
109
130
|
- Import animations from \`@tamagui/config/animations-css\`, \`animations-rn\`, \`animations-reanimated\`, or \`animations-motion\`.
|
|
110
131
|
- Apps using Sheet or animated-number hooks with the CSS driver must import \`createAnimations\` from \`@tamagui/animations-css/extras\`. The root entry omits those hooks.
|
|
111
|
-
- Remove \`@tamagui/theme-builder\` and
|
|
112
|
-
- An app that GENERATES its v5 themes (\`createV5Theme\`, \`subtleChildrenThemes\`, \`createPalettes\`) has two ways to stay on v5 for this migration: run the builder once and serialize the result to a static literal, or import the same builders from \`@tamagui/config-v5\` (and \`@tamagui/config-v5/builder\`), the opt-in package that carries them. Take one of those when the themes are built from values the app only knows at runtime; the v6 recipe move
|
|
113
|
-
- Use \`createThemes\`, \`levels\`, scales, and the other recipe helpers from \`@tamagui/themes/builder\`.
|
|
114
|
-
- Remove \`componentThemes\`, \`templates\`, \`masks\`, \`childrenThemes\`, and \`grandChildrenThemes\`. Express hierarchy in the recipe tree, semantic values in scales, and exact one-theme overrides in \`values\`.
|
|
132
|
+
- Remove \`@tamagui/theme-builder\` and V5 builder imports from packages that no longer export them. Static \`@tamagui/themes/v5\`, \`/v5-subtle\`, and \`/v5-tokens\` imports remain supported.
|
|
133
|
+
- An app that GENERATES its v5 themes (\`createV5Theme\`, \`subtleChildrenThemes\`, \`createPalettes\`) has two ways to stay on v5 for this migration: run the builder once and serialize the result to a static literal, or import the same builders from \`@tamagui/config-v5\` (and \`@tamagui/config-v5/builder\`), the opt-in package that carries them. Take one of those when the themes are built from values the app only knows at runtime; the optional v6 recipe move in the appendix is a separate migration.
|
|
115
134
|
- Component names no longer select uppercase theme segments automatically. Replace component themes with explicit normal theme or \`level2\` boundaries in component skins.
|
|
116
135
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
- \`color2\` -> \`color2\`
|
|
121
|
-
- \`color3\` -> \`color3\`
|
|
122
|
-
- \`color4\` -> \`color4\`
|
|
123
|
-
- \`color5\` -> \`color5\`
|
|
124
|
-
- \`color6\` and \`color7\` -> \`color6\`
|
|
125
|
-
- \`color8\` -> \`color7\`
|
|
126
|
-
- \`color9\` -> \`color8\`
|
|
127
|
-
- \`color10\` -> \`color9\`
|
|
128
|
-
- \`color11\` -> \`color10\`
|
|
129
|
-
- \`color12\` -> \`color11\`
|
|
130
|
-
|
|
131
|
-
The endpoints are exact; inspect contrast in the compressed middle. Replace
|
|
132
|
-
\`surface1\` with \`level2\`, \`surface2\` with \`level3\`, and \`surface3\`
|
|
133
|
-
or \`surface4\` with \`level4\`. Levels are relative and preserve a surrounding
|
|
134
|
-
color theme when nested.
|
|
135
|
-
|
|
136
|
-
Search the config and application together:
|
|
137
|
-
|
|
138
|
-
\`\`\`bash
|
|
139
|
-
rg "@tamagui/theme-builder|v5-builder|createV5Theme|componentThemes|grandChildrenThemes|surface[1-4]|color12"
|
|
140
|
-
\`\`\`
|
|
136
|
+
Do not apply the v6 color remap or recipe conversion in the optional appendix
|
|
137
|
+
while keeping Config v5. Reserved API/theme-key spelling changes still apply;
|
|
138
|
+
preserve the resolved values behind the renamed keys.
|
|
141
139
|
|
|
142
140
|
### 4. Migrate Sheet anatomy
|
|
143
141
|
|
|
144
|
-
The flat-values codemod from step
|
|
142
|
+
The flat-values codemod from step 2 rewrites every provable \`Sheet.Frame\` to
|
|
145
143
|
\`Sheet.Container\` with a \`Sheet.Background\` first child carrying the surface
|
|
146
144
|
props, and reports spreads and \`styled(Sheet.Frame, \u2026)\` targets for review.
|
|
147
145
|
Check each rewritten callsite against these rules:
|
|
@@ -181,7 +179,7 @@ After:
|
|
|
181
179
|
Search:
|
|
182
180
|
|
|
183
181
|
\`\`\`bash
|
|
184
|
-
rg "focusable|fullscreen|themeInverse|<Theme inverse|Sheet\\.Frame|styleable\\(|inlineWhenUnflattened|\\$true|getTokenRelative|stepTokenUpOrDown|forceRemoveScrollEnabled|sizeAdjust|getExpandedShorthands|usePropsAndStyle|useProps|useStyle"
|
|
182
|
+
rg "focusable|fullscreen|themeInverse|<Theme inverse|Sheet\\.Frame|styleable\\(|inlineWhenUnflattened|\\$true|getTokenRelative|stepTokenUpOrDown|forceRemoveScrollEnabled|sizeAdjust|getExpandedShorthands|usePropsAndStyle|useProps|useStyle|Unspaced|spacedChildren"
|
|
185
183
|
\`\`\`
|
|
186
184
|
|
|
187
185
|
Replace:
|
|
@@ -200,6 +198,8 @@ Replace:
|
|
|
200
198
|
- \`createCheckbox\` \`sizeAdjust\` -> explicit sizing math or component styles.
|
|
201
199
|
- \`getExpandedShorthands\` -> \`getExpandedShorthand(key, props)\` when behavior code needs one authored prop and must accept its configured shorthand.
|
|
202
200
|
- \`useProps\`, \`useStyle\`, and \`usePropsAndStyle\` -> keep conditional values on styled Tamagui components; use \`splitStyleProps\` only when a wrapper must partition authored props.
|
|
201
|
+
- \`<Unspaced>\` -> its children, and \`spacedChildren\` -> \`gap\`: stacks no longer insert spacers.
|
|
202
|
+
- a \`theme\`, handler, or other non-style key inside \`variants\` -> set it on a wrapper one level up. Variants hold styles and aria/data attributes only; type checking flags the rest.
|
|
203
203
|
|
|
204
204
|
V2 could spread one style across its base prop, pseudo-style objects, media
|
|
205
205
|
objects, and platform objects. The removed hooks gathered those separate
|
|
@@ -251,12 +251,14 @@ const padding = getVariableValue(getSize(size)) * 0.6
|
|
|
251
251
|
|
|
252
252
|
Use explicit token keys when you need a named smaller or larger token. Use numeric multiplication when proportional sizing is intended.
|
|
253
253
|
|
|
254
|
-
### 8.
|
|
254
|
+
### 8. Migrate line heights and audit font size values
|
|
255
255
|
|
|
256
256
|
- \`fontSize={17}\` is a raw numeric platform value and keeps platform-default line-height behavior.
|
|
257
257
|
- \`fontSize="17px"\` is an exact pixel value.
|
|
258
|
-
-
|
|
259
|
-
-
|
|
258
|
+
- V3 interprets numeric Tamagui \`lineHeight\` props as ratios on web and native. The flat-values command above explicitly selects \`v2-pixels\`, so old numeric Tamagui styles become px strings, including values below 4.
|
|
259
|
+
- Keep raw React Native and Restyle numeric styles unchanged. Review every \`ambiguous-line-height-*\` report entry where a shared object, untyped expression, or extracted token value needs a boundary-specific decision.
|
|
260
|
+
- Configured font \`lineHeight\` numbers remain absolute pixels. Numeric strings such as \`"1.5"\` are ratios, while \`"24px"\` is explicit. The codemod does not rewrite font configuration.
|
|
261
|
+
- Rerun the codemod with the same source semantics until it reports no remaining line-height work. Explicit px output is idempotent.
|
|
260
262
|
|
|
261
263
|
### 9. Update FocusScope
|
|
262
264
|
|
|
@@ -315,6 +317,9 @@ Replace unsupported old web/lite usage such as momentum events, \`snapTo*\`, and
|
|
|
315
317
|
|
|
316
318
|
### 14. Optional Tailwind frontend
|
|
317
319
|
|
|
320
|
+
Skip this in the intermediate V3 API checkpoint unless Tailwind adoption was
|
|
321
|
+
separately requested. It does not require changing the whole app to Config v6.
|
|
322
|
+
|
|
318
323
|
Tailwind authoring is selected by the component package, with no global config:
|
|
319
324
|
|
|
320
325
|
\`\`\`tsx
|
|
@@ -325,15 +330,69 @@ Keep importing regular Tamagui components from \`tamagui\` or
|
|
|
325
330
|
\`@tamagui/core\`. Do not mix utility classes and Tamagui style props on the
|
|
326
331
|
same component; choose the import whose styling language that component uses.
|
|
327
332
|
|
|
333
|
+
### Required API follow-ups
|
|
334
|
+
|
|
335
|
+
- Transition values: replace arrays with \`{ preset: 'quick', opacity: 'lazy' }\`, rename the transition object's \`default\` to \`preset\`, and place physics under \`spring\`. Move \`animateOnly\` into \`transition.properties\`. Read the upgrade guide's transition section before converting driver-specific options.
|
|
336
|
+
- Groups and containers: \`group="card"\` enables \`group-hover/card:\` and other group states. Size queries require \`container="card"\` and use \`@sm/card:\`; a group alone no longer enables container measurement.
|
|
337
|
+
- Control sizes: Config v6 uses \`xs | sm | md | lg | xl\` (default \`md\`). Keep numeric token keys while retaining Config v5, then map control sizes when separately adopting v6. Shape and icon geometry still uses size tokens or numbers.
|
|
338
|
+
- Remove top-level \`createTamagui({ defaultProps })\`. Put default styles in \`styled()\` definitions and inherited non-style defaults in \`Component.Props\`.
|
|
339
|
+
- Toast: replace \`useToastController().show(title, { message })\` with \`toast(title, { description: message })\`. Import \`Toast\` and \`toast\` from \`tamagui/toast\`; mount \`Toast.Root\` and \`Toast.List\` with the desired parts once in the app. The old provider/controller API is removed.
|
|
340
|
+
- Replace \`ThemeableStack\` and \`SizableStack\` with \`YStack\` or \`XStack\` plus explicit styles; use \`elevation\` for elevation and border width/color for borders.
|
|
341
|
+
- Checked/selected states: Checkbox, Switch, Tabs and ToggleGroup read \`background-press\`; customize \`activeStyle\` to override it per instance. Audit the resulting active background against the previous app and customize its theme or skin where needed.
|
|
342
|
+
|
|
328
343
|
### 15. Verification
|
|
329
344
|
|
|
330
|
-
- Run \`npx tamagui check\`.
|
|
345
|
+
- Run \`npx tamagui check --strict\`.
|
|
331
346
|
- Run typecheck and build.
|
|
332
347
|
- Start the app and manually test screens using Sheet, Dialog, Popover, Select, FocusScope, icons, and ScrollView.
|
|
333
348
|
- Test Adapt breakpoints where popovers/selects/dialogs become sheets.
|
|
334
349
|
- Verify keyboard focus, Escape, outside click, scroll locking, and close animations.
|
|
335
350
|
- Inspect icon alignment next to text at each app size token.
|
|
336
|
-
- If the Tailwind frontend is used, compare web and native output for the classes used
|
|
351
|
+
- If the Tailwind frontend is used, compare web and native output for the classes used.
|
|
352
|
+
- Compare the same screens and resolved control dimensions with the baseline. Keeping Config v5 preserves the scales, but does not prove that changed component APIs preserve every layout.
|
|
353
|
+
- Report the final package version, retained config, manual fixes, runtime evidence, and deferred optional work. A validated V3 app on Config v5 is a completed migration.
|
|
354
|
+
|
|
355
|
+
### Optional follow-up: Config v6, only when separately requested
|
|
356
|
+
|
|
357
|
+
This appendix is not part of the intermediate V3 API checkpoint. Start a new
|
|
358
|
+
reviewable change after that checkpoint is validated. Record new visual
|
|
359
|
+
acceptance criteria before changing design values.
|
|
360
|
+
|
|
361
|
+
- New apps can start with \`defaultConfig\` from \`@tamagui/config/v6\`.
|
|
362
|
+
- For an existing app, inventory and compare resolved spacing, size, radius, font, icon, and media values. Map by resolved value when preserving appearance; the same token name can mean a different number in v6.
|
|
363
|
+
- Preserve breakpoint meaning: changing a max-width key to a min-width key can invert responsive behavior.
|
|
364
|
+
|
|
365
|
+
- Use \`createThemes\`, \`levels\`, scales, and the other recipe helpers from \`@tamagui/themes/builder\`.
|
|
366
|
+
- Remove \`componentThemes\`, \`templates\`, \`masks\`, \`childrenThemes\`, and \`grandChildrenThemes\`. Express hierarchy in the recipe tree, semantic values in scales, and exact one-theme overrides in \`values\`.
|
|
367
|
+
|
|
368
|
+
Rename the adaptive 12-step ramp approximately:
|
|
369
|
+
|
|
370
|
+
- \`color1\` -> \`color-1\`
|
|
371
|
+
- \`color2\` -> \`color-2\`
|
|
372
|
+
- \`color3\` -> \`color-3\`
|
|
373
|
+
- \`color4\` -> \`color-4\`
|
|
374
|
+
- \`color5\` -> \`color-5\`
|
|
375
|
+
- \`color6\` and \`color7\` -> \`color-6\`
|
|
376
|
+
- \`color8\` -> \`color-7\`
|
|
377
|
+
- \`color9\` -> \`color-8\`
|
|
378
|
+
- \`color10\` -> \`color-9\`
|
|
379
|
+
- \`color11\` -> \`color-10\`
|
|
380
|
+
- \`color12\` -> \`color-11\`
|
|
381
|
+
|
|
382
|
+
The endpoints are exact; inspect contrast in the compressed middle. Replace
|
|
383
|
+
\`surface1\` with \`level2\`, \`surface2\` with \`level3\`, and \`surface3\`
|
|
384
|
+
or \`surface4\` with \`level4\`. Levels are relative and preserve a surrounding
|
|
385
|
+
color theme when nested.
|
|
386
|
+
|
|
387
|
+
Search the config and application together:
|
|
388
|
+
|
|
389
|
+
\`\`\`bash
|
|
390
|
+
rg "@tamagui/theme-builder|v5-builder|createV5Theme|componentThemes|grandChildrenThemes|surface[1-4]|color12"
|
|
391
|
+
\`\`\`
|
|
392
|
+
|
|
393
|
+
Validate the config change separately with light/dark screenshots, responsive
|
|
394
|
+
layouts, text and icon alignment, and every platform the app ships. Do not
|
|
395
|
+
accept visual drift merely because the codemod and typecheck are green.`;
|
|
337
396
|
const v1ToV2Prompt = `## v1 -> v2 migration pass
|
|
338
397
|
|
|
339
398
|
Bring the app to the v2 baseline before applying v3 changes.
|
|
@@ -346,12 +405,12 @@ Bring the app to the v2 baseline before applying v3 changes.
|
|
|
346
405
|
|
|
347
406
|
### Config
|
|
348
407
|
|
|
349
|
-
-
|
|
408
|
+
- Preserve the app's existing token, theme, font, and breakpoint values while reaching the v2 API baseline. Keep supported Config v5 imports or serialize the old generated config into app-owned values when an entry is no longer exported. Config v6 is a separate optional migration after the v3 checkpoint below.
|
|
350
409
|
- Import animations separately from \`@tamagui/config/animations-css\`, \`animations-rn\`, \`animations-reanimated\`, or \`animations-motion\`.
|
|
351
410
|
- Move root \`createTamagui\` settings into \`settings\`.
|
|
352
411
|
- Account for the defaults \`flexBasis: 0\` and \`position: static\`. Use \`styleCompat: 'legacy'\` or explicit props if needed.
|
|
353
412
|
- Rename media queries: \`$2xl\` -> \`$xxl\`, \`$2xs\` -> \`$xxs\`, and max queries to kebab-case such as \`$max-md\`.
|
|
354
|
-
-
|
|
413
|
+
- Preserve resolved colors and themes; defer v6 recipe conversion to the optional follow-up below.
|
|
355
414
|
|
|
356
415
|
### v1 prop and API changes
|
|
357
416
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all) __defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
16
|
+
get: () => from[key],
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var preview_theme_exports = {};
|
|
28
|
+
__export(preview_theme_exports, { previewTheme: () => previewTheme });
|
|
29
|
+
module.exports = __toCommonJS(preview_theme_exports);
|
|
30
|
+
var import_node_fs = __toESM(require("node:fs"));
|
|
31
|
+
var import_node_path = __toESM(require("node:path"));
|
|
32
|
+
var import_chalk = __toESM(require("chalk"));
|
|
33
|
+
var import_opener = __toESM(require("opener"));
|
|
34
|
+
async function previewTheme(options) {
|
|
35
|
+
const themeInput = (options.theme || options.themeArg || "").trim();
|
|
36
|
+
let themeData = null;
|
|
37
|
+
if (themeInput) {
|
|
38
|
+
if (themeInput.startsWith("{") || themeInput.startsWith("[")) {
|
|
39
|
+
try {
|
|
40
|
+
themeData = JSON.parse(themeInput);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw new Error(`Invalid JSON provided for --theme: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
const filePath = import_node_path.default.resolve(process.cwd(), themeInput);
|
|
46
|
+
if (!import_node_fs.default.existsSync(filePath)) {
|
|
47
|
+
throw new Error(`Theme file not found at: ${filePath}`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const fileContent = import_node_fs.default.readFileSync(filePath, "utf-8");
|
|
51
|
+
themeData = JSON.parse(fileContent);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
throw new Error(`Failed to read theme file ${filePath}: ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const baseUrl = options.url || "https://tamagui.dev/theme";
|
|
58
|
+
let targetUrl = baseUrl;
|
|
59
|
+
if (themeData) {
|
|
60
|
+
const jsonStr = JSON.stringify(themeData);
|
|
61
|
+
const base64 = Buffer.from(jsonStr).toString("base64");
|
|
62
|
+
targetUrl = `${baseUrl}#theme=${encodeURIComponent(base64)}`;
|
|
63
|
+
}
|
|
64
|
+
if (options.print) {
|
|
65
|
+
console.info(targetUrl);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
console.info(import_chalk.default.green(`Opening theme preview:`), targetUrl);
|
|
69
|
+
(0, import_opener.default)(targetUrl);
|
|
70
|
+
}
|