@tamagui/cli 3.0.0-beta.924.1 → 3.0.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/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 --report flat-values-report.md ./src
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. Migrate config and themes
127
+ ### 3. Preserve config values while migrating required APIs
106
128
 
107
- - New applications should use \`defaultConfig\` from \`@tamagui/config/v6\`.
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 any V5 builder imports. Static \`@tamagui/themes/v5\`, \`/v5-subtle\`, and \`/v5-tokens\` imports remain supported.
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 below is a separate migration.
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
- Rename the adaptive 12-step ramp approximately:
118
-
119
- - \`color1\` -> \`color1\`
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 1 rewrites every provable \`Sheet.Frame\` to
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. Audit font size values
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
- - Configured font \`size\` and \`lineHeight\` tokens should use px strings when exact web pixels are intended.
259
- - Convert custom config font tokens to px strings if exact pixels were intended.
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
- - Move to \`@tamagui/config/v6\`.
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
- - Update colors and themes to the v3 recipe helpers in \`@tamagui/themes/builder\`.
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
+ }