@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/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/src/icons.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// `tamagui icons add`: writes one themed react-native-svg component per icon,
|
|
2
|
+
// plus an index of every component in the output dir. sources are pinned
|
|
3
|
+
// upstream releases fetched from unpkg, or a local dir of svg files.
|
|
4
|
+
|
|
5
|
+
import { existsSync, statSync } from 'node:fs'
|
|
6
|
+
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
|
|
9
|
+
import chalk from 'chalk'
|
|
10
|
+
|
|
11
|
+
// pinned so a cli release only changes output when these move
|
|
12
|
+
export const LUCIDE_VERSION = '1.49.0'
|
|
13
|
+
export const HEROICONS_VERSION = '2.2.0'
|
|
14
|
+
export const PHOSPHOR_VERSION = '2.1.1'
|
|
15
|
+
|
|
16
|
+
const libraries = ['lucide', 'heroicons', 'phosphor'] as const
|
|
17
|
+
type Library = (typeof libraries)[number]
|
|
18
|
+
|
|
19
|
+
const phosphorWeights = ['regular', 'bold', 'duotone', 'fill', 'light', 'thin']
|
|
20
|
+
// heroicons ships outline only at 24px; solid at 24px, mini at 20px, micro at 16px
|
|
21
|
+
const heroiconsDirs: Record<string, string> = {
|
|
22
|
+
outline: '24/outline',
|
|
23
|
+
solid: '24/solid',
|
|
24
|
+
mini: '20/solid',
|
|
25
|
+
micro: '16/solid',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type AddIconsOptions = {
|
|
29
|
+
names: string[]
|
|
30
|
+
// a library name, or a dir holding <kebab-name>.svg files
|
|
31
|
+
from?: string
|
|
32
|
+
weight?: string
|
|
33
|
+
variant?: string
|
|
34
|
+
out?: string
|
|
35
|
+
cwd?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function addIcons(options: AddIconsOptions) {
|
|
39
|
+
const cwd = options.cwd ?? process.cwd()
|
|
40
|
+
const from = options.from ?? 'lucide'
|
|
41
|
+
const outDir = path.resolve(cwd, options.out ?? 'components/icons')
|
|
42
|
+
const localDir = libraries.includes(from as Library) ? null : path.resolve(cwd, from)
|
|
43
|
+
|
|
44
|
+
if (localDir && !(existsSync(localDir) && statSync(localDir).isDirectory())) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`--from must be ${libraries.join(', ')}, or a directory of svg files (got "${from}")`
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
if (!options.names.length) {
|
|
50
|
+
throw new Error('give the icons to add, e.g. `tamagui icons add Search ChevronDown`')
|
|
51
|
+
}
|
|
52
|
+
if (options.weight && from !== 'phosphor') {
|
|
53
|
+
throw new Error('--weight applies to phosphor only')
|
|
54
|
+
}
|
|
55
|
+
if (options.weight && !phosphorWeights.includes(options.weight)) {
|
|
56
|
+
throw new Error(`--weight is one of ${phosphorWeights.join(', ')}`)
|
|
57
|
+
}
|
|
58
|
+
if (options.variant && from !== 'heroicons') {
|
|
59
|
+
throw new Error('--variant applies to heroicons only')
|
|
60
|
+
}
|
|
61
|
+
if (options.variant && !heroiconsDirs[options.variant]) {
|
|
62
|
+
throw new Error(`--variant is one of ${Object.keys(heroiconsDirs).join(', ')}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// `Search=magnifying-glass` keeps the component name while drawing another
|
|
66
|
+
// source icon, so a set can switch libraries without touching its imports
|
|
67
|
+
const icons = [...new Set(options.names)].map((arg) => {
|
|
68
|
+
const [name, source = name] = arg.split('=')
|
|
69
|
+
const isKebab = (x: string) => /^[a-z0-9-]+$/.test(x)
|
|
70
|
+
const isPascal = (x: string) => /^[A-Z][A-Za-z0-9]*$/.test(x)
|
|
71
|
+
if (
|
|
72
|
+
!(isKebab(name) || isPascal(name)) ||
|
|
73
|
+
!(isKebab(source) || isPascal(source)) ||
|
|
74
|
+
(source !== name && !isPascal(name))
|
|
75
|
+
) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`icon names are PascalCase, kebab-case, or Name=source-name, got "${arg}"`
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
componentName: isKebab(name) ? kebabToPascal(name) : name,
|
|
82
|
+
file: isKebab(source) ? source : pascalToKebab(stripIconSuffix(source)),
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const loaded = await Promise.all(
|
|
87
|
+
icons.map(async (icon) => {
|
|
88
|
+
if (localDir) {
|
|
89
|
+
const file = path.join(localDir, `${icon.file}.svg`)
|
|
90
|
+
return { ...icon, svg: existsSync(file) ? await readFile(file, 'utf-8') : null }
|
|
91
|
+
}
|
|
92
|
+
const url = sourceUrl(from as Library, icon.file, options)
|
|
93
|
+
const response = await fetch(url)
|
|
94
|
+
if (response.status === 404) return { ...icon, svg: null }
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error(`fetching ${url} failed (${response.status})`)
|
|
97
|
+
}
|
|
98
|
+
return { ...icon, svg: await response.text() }
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const unknown = loaded.filter((icon) => icon.svg === null)
|
|
103
|
+
if (unknown.length) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`unknown ${from} icon${unknown.length === 1 ? '' : 's'}: ${unknown
|
|
106
|
+
.map((icon) => `${icon.componentName} (${icon.file}.svg)`)
|
|
107
|
+
.join(', ')}`
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await mkdir(outDir, { recursive: true })
|
|
112
|
+
for (const icon of loaded) {
|
|
113
|
+
const file = path.join(outDir, `${icon.componentName}.tsx`)
|
|
114
|
+
await writeFile(file, svgToComponent(icon.componentName, icon.svg!))
|
|
115
|
+
console.info(`${chalk.green('wrote')} ${path.relative(cwd, file)}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// the index lists every component in the dir, so repeated adds accumulate
|
|
119
|
+
const components = (await readdir(outDir))
|
|
120
|
+
.filter((file) => file.endsWith('.tsx'))
|
|
121
|
+
.map((file) => file.slice(0, -'.tsx'.length))
|
|
122
|
+
.sort()
|
|
123
|
+
await writeFile(
|
|
124
|
+
path.join(outDir, 'index.ts'),
|
|
125
|
+
components.map((name) => `export { ${name} } from './${name}'`).join('\n') + '\n'
|
|
126
|
+
)
|
|
127
|
+
console.info(
|
|
128
|
+
`${chalk.green('wrote')} ${path.relative(cwd, path.join(outDir, 'index.ts'))} (${components.length} icons)`
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sourceUrl(library: Library, file: string, options: AddIconsOptions) {
|
|
133
|
+
switch (library) {
|
|
134
|
+
case 'lucide':
|
|
135
|
+
return `https://unpkg.com/lucide-static@${LUCIDE_VERSION}/icons/${file}.svg`
|
|
136
|
+
case 'heroicons':
|
|
137
|
+
return `https://unpkg.com/heroicons@${HEROICONS_VERSION}/${heroiconsDirs[options.variant ?? 'outline']}/${file}.svg`
|
|
138
|
+
case 'phosphor': {
|
|
139
|
+
const weight = options.weight ?? 'regular'
|
|
140
|
+
const name = weight === 'regular' ? file : `${file}-${weight}`
|
|
141
|
+
return `https://unpkg.com/@phosphor-icons/core@${PHOSPHOR_VERSION}/assets/${weight}/${name}.svg`
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function pascalToKebab(name: string) {
|
|
147
|
+
return name
|
|
148
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
149
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
150
|
+
.replace(/([A-Za-z])(\d)/g, '$1-$2')
|
|
151
|
+
.toLowerCase()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function kebabToPascal(name: string) {
|
|
155
|
+
return name
|
|
156
|
+
.split('-')
|
|
157
|
+
.map((part) => (part ? part[0].toUpperCase() + part.slice(1) : part))
|
|
158
|
+
.join('')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// one trailing Icon is dropped for the lookup only: HeartIcon finds heart
|
|
162
|
+
function stripIconSuffix(name: string) {
|
|
163
|
+
return name.endsWith('Icon') && name.length > 4 ? name.slice(0, -4) : name
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type SvgNode = {
|
|
167
|
+
tag: string
|
|
168
|
+
attrs: [name: string, value: string][]
|
|
169
|
+
children: SvgNode[]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// upstream icon files are machine-written and shallow; this handles nesting so
|
|
173
|
+
// a grouped source never drops shapes
|
|
174
|
+
function parseSvg(svg: string): SvgNode {
|
|
175
|
+
const root: SvgNode = { tag: '', attrs: [], children: [] }
|
|
176
|
+
const stack = [root]
|
|
177
|
+
const tagPattern = /<(\/?)([A-Za-z][A-Za-z0-9]*)((?:"[^"]*"|'[^']*'|[^>"'])*?)(\/?)>/g
|
|
178
|
+
for (const [, closing, tag, attrText, selfClose] of svg
|
|
179
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
180
|
+
.matchAll(tagPattern)) {
|
|
181
|
+
if (closing) {
|
|
182
|
+
if (stack.pop()?.tag !== tag) throw new Error(`mismatched </${tag}> in svg`)
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
const node: SvgNode = { tag, attrs: [], children: [] }
|
|
186
|
+
for (const [, name, double, single] of attrText.matchAll(
|
|
187
|
+
/([A-Za-z_:][A-Za-z0-9_.:-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g
|
|
188
|
+
)) {
|
|
189
|
+
node.attrs.push([name, double ?? single])
|
|
190
|
+
}
|
|
191
|
+
stack[stack.length - 1].children.push(node)
|
|
192
|
+
if (!selfClose) stack.push(node)
|
|
193
|
+
}
|
|
194
|
+
const svgNode = root.children.find((node) => node.tag === 'svg')
|
|
195
|
+
if (stack.length !== 1 || !svgNode)
|
|
196
|
+
throw new Error('svg source has no complete <svg> root')
|
|
197
|
+
return svgNode
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const elements: Record<string, string> = {
|
|
201
|
+
path: 'Path',
|
|
202
|
+
circle: 'Circle',
|
|
203
|
+
ellipse: 'Ellipse',
|
|
204
|
+
line: 'Line',
|
|
205
|
+
polyline: 'Polyline',
|
|
206
|
+
polygon: 'Polygon',
|
|
207
|
+
rect: 'Rect',
|
|
208
|
+
g: 'G',
|
|
209
|
+
defs: 'Defs',
|
|
210
|
+
clipPath: 'ClipPath',
|
|
211
|
+
linearGradient: 'LinearGradient',
|
|
212
|
+
radialGradient: 'RadialGradient',
|
|
213
|
+
stop: 'Stop',
|
|
214
|
+
use: 'Use',
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// globals a component name would shadow; oxlint/eslint flag these
|
|
218
|
+
const restrictedNames = ['Infinity', 'NaN', 'undefined']
|
|
219
|
+
|
|
220
|
+
export function svgToComponent(name: string, svg: string) {
|
|
221
|
+
const root = parseSvg(svg)
|
|
222
|
+
const used = new Set<string>()
|
|
223
|
+
const ref = (element: string) => (element === name ? `_${element}` : element)
|
|
224
|
+
|
|
225
|
+
const jsxAttr = (attrName: string, value: string) => {
|
|
226
|
+
if (attrName === 'style') {
|
|
227
|
+
throw new Error(`${name}: style attributes have no react-native-svg equivalent`)
|
|
228
|
+
}
|
|
229
|
+
const prop =
|
|
230
|
+
attrName === 'xlink:href'
|
|
231
|
+
? 'href'
|
|
232
|
+
: attrName.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase())
|
|
233
|
+
return value === 'currentColor' ? `${prop}={color}` : `${prop}="${value}"`
|
|
234
|
+
}
|
|
235
|
+
const keepAttr = ([attrName]: [string, string]) =>
|
|
236
|
+
!(
|
|
237
|
+
attrName === 'xmlns' ||
|
|
238
|
+
attrName.startsWith('xmlns:') ||
|
|
239
|
+
attrName === 'class' ||
|
|
240
|
+
attrName === 'version' ||
|
|
241
|
+
attrName.startsWith('data-') ||
|
|
242
|
+
attrName.startsWith('aria-')
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
const rootAttrs = root.attrs.filter(keepAttr)
|
|
246
|
+
const viewBox = rootAttrs.find(([attrName]) => attrName === 'viewBox')?.[1]
|
|
247
|
+
if (!viewBox) throw new Error(`${name}: svg source has no viewBox`)
|
|
248
|
+
// native draws children with their own stroke, so a currentColor stroke on
|
|
249
|
+
// the root is repeated on each child that has none
|
|
250
|
+
const rootStroke = rootAttrs.some(
|
|
251
|
+
([attrName, value]) => attrName === 'stroke' && value === 'currentColor'
|
|
252
|
+
)
|
|
253
|
+
const strokeWidth = rootAttrs.find(([attrName]) => attrName === 'stroke-width')?.[1]
|
|
254
|
+
|
|
255
|
+
const emit = (node: SvgNode, indent: string): string => {
|
|
256
|
+
const element = elements[node.tag]
|
|
257
|
+
if (!element) throw new Error(`${name}: unsupported svg element <${node.tag}>`)
|
|
258
|
+
used.add(element)
|
|
259
|
+
const attrs = node.attrs.filter(keepAttr).map(([n, v]) => jsxAttr(n, v))
|
|
260
|
+
if (rootStroke && !node.attrs.some(([n]) => n === 'stroke') && node.tag !== 'g') {
|
|
261
|
+
attrs.push('stroke={color}')
|
|
262
|
+
}
|
|
263
|
+
const open = `${indent}<${ref(element)}${attrs.map((attr) => ` ${attr}`).join('')}`
|
|
264
|
+
if (!node.children.length) return `${open} />`
|
|
265
|
+
return `${open}>\n${node.children.map((child) => emit(child, `${indent} `)).join('\n')}\n${indent}</${ref(element)}>`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const children = root.children.map((child) => emit(child, ' '))
|
|
269
|
+
if (!children.length) throw new Error(`${name}: svg source draws nothing`)
|
|
270
|
+
|
|
271
|
+
const svgAttrs = [
|
|
272
|
+
'width={size}',
|
|
273
|
+
'height={size}',
|
|
274
|
+
...rootAttrs
|
|
275
|
+
.filter(([attrName]) => attrName !== 'width' && attrName !== 'height')
|
|
276
|
+
.map(([attrName, value]) => jsxAttr(attrName, value)),
|
|
277
|
+
'{...otherProps}',
|
|
278
|
+
]
|
|
279
|
+
const imports = [...used]
|
|
280
|
+
.sort()
|
|
281
|
+
.map((element) => (element === name ? `${element} as _${element}` : element))
|
|
282
|
+
const themedOptions =
|
|
283
|
+
strokeWidth && strokeWidth !== '2' ? `, { defaultStrokeWidth: ${strokeWidth} }` : ''
|
|
284
|
+
|
|
285
|
+
// the explicit component type lets isolatedDeclarations builds emit types.
|
|
286
|
+
// themed resolves theme and token values before the inner component runs, so
|
|
287
|
+
// it reads plain svg values rather than the app's token-typed IconProps
|
|
288
|
+
return `${restrictedNames.includes(name) ? '/* eslint-disable no-shadow-restricted-names */\n' : ''}import { memo, type JSX } from 'react'
|
|
289
|
+
import { Svg, ${imports.join(', ')}, type SvgProps } from 'react-native-svg'
|
|
290
|
+
import { themed, type IconProps } from '@tamagui/helpers-icon'
|
|
291
|
+
|
|
292
|
+
export const ${name}: (props: IconProps) => JSX.Element = themed(
|
|
293
|
+
memo(function ${name}(props: IconProps) {
|
|
294
|
+
const { color = 'black', size = 24, ...otherProps } = props as SvgProps & {
|
|
295
|
+
color?: string
|
|
296
|
+
size?: number
|
|
297
|
+
}
|
|
298
|
+
return (
|
|
299
|
+
<Svg
|
|
300
|
+
${svgAttrs.map((attr) => ` ${attr}`).join('\n')}
|
|
301
|
+
>
|
|
302
|
+
${children.join('\n')}
|
|
303
|
+
</Svg>
|
|
304
|
+
)
|
|
305
|
+
})${themedOptions}
|
|
306
|
+
)
|
|
307
|
+
`
|
|
308
|
+
}
|
package/src/migrate.ts
CHANGED
|
@@ -48,6 +48,26 @@ Work like a careful coding agent:
|
|
|
48
48
|
|
|
49
49
|
const v2ToV3Prompt = `## v2 -> v3 migration prompt
|
|
50
50
|
|
|
51
|
+
### 0. Choose the intermediate checkpoint before editing
|
|
52
|
+
|
|
53
|
+
The Tamagui package version and the config version are separate. Tamagui V3
|
|
54
|
+
supports Config v5; upgrading the runtime does not require Config v6.
|
|
55
|
+
|
|
56
|
+
- 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.
|
|
57
|
+
- 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.
|
|
58
|
+
- 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.
|
|
59
|
+
- Do not combine this pass with wholesale \`html.*\` adoption, Tailwind conversion, new skins, or a theme redesign unless separately requested.
|
|
60
|
+
|
|
61
|
+
Record the starting package/config versions and representative light/dark,
|
|
62
|
+
responsive, and native screenshots. Name the intended checkpoint in the plan:
|
|
63
|
+
"V3 API, existing design values." Visual changes during this pass need an
|
|
64
|
+
explanation or a fix, not automatic screenshot acceptance.
|
|
65
|
+
|
|
66
|
+
The checkpoint is complete when the required API migration, report review,
|
|
67
|
+
typecheck/build, and real app checks in step 15 pass. Leave Config v6 and other
|
|
68
|
+
optional design changes in a separate follow-up; do not start them just because
|
|
69
|
+
the V3 API work is finished.
|
|
70
|
+
|
|
51
71
|
### 1. Update dependencies
|
|
52
72
|
|
|
53
73
|
- Bump every \`tamagui\` and \`@tamagui/*\` package together to v3.
|
|
@@ -64,8 +84,10 @@ V3 accepts bare token/theme names and flat clauses only. Run the transactional
|
|
|
64
84
|
flat-values codemod from your project root, dry run first:
|
|
65
85
|
|
|
66
86
|
\`\`\`bash
|
|
67
|
-
npx @tamagui/codemod-flat-values --
|
|
87
|
+
npx @tamagui/codemod-flat-values --source-semantics v2-pixels \\
|
|
88
|
+
--report flat-values-report.md ./src
|
|
68
89
|
npx @tamagui/codemod-flat-values --write \\
|
|
90
|
+
--source-semantics v2-pixels \\
|
|
69
91
|
--report flat-values-report.md \\
|
|
70
92
|
--json flat-values-report.json \\
|
|
71
93
|
./src
|
|
@@ -86,46 +108,22 @@ becomes:
|
|
|
86
108
|
Resolve every report row and rerun until the app has no V2 authoring. Do not
|
|
87
109
|
add a compatibility setting or restore condition-object parsing.
|
|
88
110
|
|
|
89
|
-
### 3.
|
|
111
|
+
### 3. Preserve config values while migrating required APIs
|
|
90
112
|
|
|
91
|
-
-
|
|
92
|
-
- 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.
|
|
113
|
+
- 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.
|
|
93
114
|
- Import animations from \`@tamagui/config/animations-css\`, \`animations-rn\`, \`animations-reanimated\`, or \`animations-motion\`.
|
|
94
115
|
- 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.
|
|
95
|
-
- Remove \`@tamagui/theme-builder\` and
|
|
96
|
-
- 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
|
|
97
|
-
- Use \`createThemes\`, \`levels\`, scales, and the other recipe helpers from \`@tamagui/themes/builder\`.
|
|
98
|
-
- Remove \`componentThemes\`, \`templates\`, \`masks\`, \`childrenThemes\`, and \`grandChildrenThemes\`. Express hierarchy in the recipe tree, semantic values in scales, and exact one-theme overrides in \`values\`.
|
|
116
|
+
- 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.
|
|
117
|
+
- 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.
|
|
99
118
|
- Component names no longer select uppercase theme segments automatically. Replace component themes with explicit normal theme or \`level2\` boundaries in component skins.
|
|
100
119
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
- \`color2\` -> \`color2\`
|
|
105
|
-
- \`color3\` -> \`color3\`
|
|
106
|
-
- \`color4\` -> \`color4\`
|
|
107
|
-
- \`color5\` -> \`color5\`
|
|
108
|
-
- \`color6\` and \`color7\` -> \`color6\`
|
|
109
|
-
- \`color8\` -> \`color7\`
|
|
110
|
-
- \`color9\` -> \`color8\`
|
|
111
|
-
- \`color10\` -> \`color9\`
|
|
112
|
-
- \`color11\` -> \`color10\`
|
|
113
|
-
- \`color12\` -> \`color11\`
|
|
114
|
-
|
|
115
|
-
The endpoints are exact; inspect contrast in the compressed middle. Replace
|
|
116
|
-
\`surface1\` with \`level2\`, \`surface2\` with \`level3\`, and \`surface3\`
|
|
117
|
-
or \`surface4\` with \`level4\`. Levels are relative and preserve a surrounding
|
|
118
|
-
color theme when nested.
|
|
119
|
-
|
|
120
|
-
Search the config and application together:
|
|
121
|
-
|
|
122
|
-
\`\`\`bash
|
|
123
|
-
rg "@tamagui/theme-builder|v5-builder|createV5Theme|componentThemes|grandChildrenThemes|surface[1-4]|color12"
|
|
124
|
-
\`\`\`
|
|
120
|
+
Do not apply the v6 color remap or recipe conversion in the optional appendix
|
|
121
|
+
while keeping Config v5. Reserved API/theme-key spelling changes still apply;
|
|
122
|
+
preserve the resolved values behind the renamed keys.
|
|
125
123
|
|
|
126
124
|
### 4. Migrate Sheet anatomy
|
|
127
125
|
|
|
128
|
-
The flat-values codemod from step
|
|
126
|
+
The flat-values codemod from step 2 rewrites every provable \`Sheet.Frame\` to
|
|
129
127
|
\`Sheet.Container\` with a \`Sheet.Background\` first child carrying the surface
|
|
130
128
|
props, and reports spreads and \`styled(Sheet.Frame, …)\` targets for review.
|
|
131
129
|
Check each rewritten callsite against these rules:
|
|
@@ -165,7 +163,7 @@ After:
|
|
|
165
163
|
Search:
|
|
166
164
|
|
|
167
165
|
\`\`\`bash
|
|
168
|
-
rg "focusable|fullscreen|themeInverse|<Theme inverse|Sheet\\.Frame|styleable\\(|inlineWhenUnflattened|\\$true|getTokenRelative|stepTokenUpOrDown|forceRemoveScrollEnabled|sizeAdjust|getExpandedShorthands|usePropsAndStyle|useProps|useStyle"
|
|
166
|
+
rg "focusable|fullscreen|themeInverse|<Theme inverse|Sheet\\.Frame|styleable\\(|inlineWhenUnflattened|\\$true|getTokenRelative|stepTokenUpOrDown|forceRemoveScrollEnabled|sizeAdjust|getExpandedShorthands|usePropsAndStyle|useProps|useStyle|Unspaced|spacedChildren"
|
|
169
167
|
\`\`\`
|
|
170
168
|
|
|
171
169
|
Replace:
|
|
@@ -184,6 +182,8 @@ Replace:
|
|
|
184
182
|
- \`createCheckbox\` \`sizeAdjust\` -> explicit sizing math or component styles.
|
|
185
183
|
- \`getExpandedShorthands\` -> \`getExpandedShorthand(key, props)\` when behavior code needs one authored prop and must accept its configured shorthand.
|
|
186
184
|
- \`useProps\`, \`useStyle\`, and \`usePropsAndStyle\` -> keep conditional values on styled Tamagui components; use \`splitStyleProps\` only when a wrapper must partition authored props.
|
|
185
|
+
- \`<Unspaced>\` -> its children, and \`spacedChildren\` -> \`gap\`: stacks no longer insert spacers.
|
|
186
|
+
- 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.
|
|
187
187
|
|
|
188
188
|
V2 could spread one style across its base prop, pseudo-style objects, media
|
|
189
189
|
objects, and platform objects. The removed hooks gathered those separate
|
|
@@ -235,12 +235,14 @@ const padding = getVariableValue(getSize(size)) * 0.6
|
|
|
235
235
|
|
|
236
236
|
Use explicit token keys when you need a named smaller or larger token. Use numeric multiplication when proportional sizing is intended.
|
|
237
237
|
|
|
238
|
-
### 8.
|
|
238
|
+
### 8. Migrate line heights and audit font size values
|
|
239
239
|
|
|
240
240
|
- \`fontSize={17}\` is a raw numeric platform value and keeps platform-default line-height behavior.
|
|
241
241
|
- \`fontSize="17px"\` is an exact pixel value.
|
|
242
|
-
-
|
|
243
|
-
-
|
|
242
|
+
- 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.
|
|
243
|
+
- 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.
|
|
244
|
+
- 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.
|
|
245
|
+
- Rerun the codemod with the same source semantics until it reports no remaining line-height work. Explicit px output is idempotent.
|
|
244
246
|
|
|
245
247
|
### 9. Update FocusScope
|
|
246
248
|
|
|
@@ -299,6 +301,9 @@ Replace unsupported old web/lite usage such as momentum events, \`snapTo*\`, and
|
|
|
299
301
|
|
|
300
302
|
### 14. Optional Tailwind frontend
|
|
301
303
|
|
|
304
|
+
Skip this in the intermediate V3 API checkpoint unless Tailwind adoption was
|
|
305
|
+
separately requested. It does not require changing the whole app to Config v6.
|
|
306
|
+
|
|
302
307
|
Tailwind authoring is selected by the component package, with no global config:
|
|
303
308
|
|
|
304
309
|
\`\`\`tsx
|
|
@@ -309,15 +314,69 @@ Keep importing regular Tamagui components from \`tamagui\` or
|
|
|
309
314
|
\`@tamagui/core\`. Do not mix utility classes and Tamagui style props on the
|
|
310
315
|
same component; choose the import whose styling language that component uses.
|
|
311
316
|
|
|
317
|
+
### Required API follow-ups
|
|
318
|
+
|
|
319
|
+
- 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.
|
|
320
|
+
- 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.
|
|
321
|
+
- 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.
|
|
322
|
+
- Remove top-level \`createTamagui({ defaultProps })\`. Put default styles in \`styled()\` definitions and inherited non-style defaults in \`Component.Props\`.
|
|
323
|
+
- 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.
|
|
324
|
+
- Replace \`ThemeableStack\` and \`SizableStack\` with \`YStack\` or \`XStack\` plus explicit styles; use \`elevation\` for elevation and border width/color for borders.
|
|
325
|
+
- 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.
|
|
326
|
+
|
|
312
327
|
### 15. Verification
|
|
313
328
|
|
|
314
|
-
- Run \`npx tamagui check\`.
|
|
329
|
+
- Run \`npx tamagui check --strict\`.
|
|
315
330
|
- Run typecheck and build.
|
|
316
331
|
- Start the app and manually test screens using Sheet, Dialog, Popover, Select, FocusScope, icons, and ScrollView.
|
|
317
332
|
- Test Adapt breakpoints where popovers/selects/dialogs become sheets.
|
|
318
333
|
- Verify keyboard focus, Escape, outside click, scroll locking, and close animations.
|
|
319
334
|
- Inspect icon alignment next to text at each app size token.
|
|
320
|
-
- If the Tailwind frontend is used, compare web and native output for the classes used
|
|
335
|
+
- If the Tailwind frontend is used, compare web and native output for the classes used.
|
|
336
|
+
- 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.
|
|
337
|
+
- 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.
|
|
338
|
+
|
|
339
|
+
### Optional follow-up: Config v6, only when separately requested
|
|
340
|
+
|
|
341
|
+
This appendix is not part of the intermediate V3 API checkpoint. Start a new
|
|
342
|
+
reviewable change after that checkpoint is validated. Record new visual
|
|
343
|
+
acceptance criteria before changing design values.
|
|
344
|
+
|
|
345
|
+
- New apps can start with \`defaultConfig\` from \`@tamagui/config/v6\`.
|
|
346
|
+
- 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.
|
|
347
|
+
- Preserve breakpoint meaning: changing a max-width key to a min-width key can invert responsive behavior.
|
|
348
|
+
|
|
349
|
+
- Use \`createThemes\`, \`levels\`, scales, and the other recipe helpers from \`@tamagui/themes/builder\`.
|
|
350
|
+
- Remove \`componentThemes\`, \`templates\`, \`masks\`, \`childrenThemes\`, and \`grandChildrenThemes\`. Express hierarchy in the recipe tree, semantic values in scales, and exact one-theme overrides in \`values\`.
|
|
351
|
+
|
|
352
|
+
Rename the adaptive 12-step ramp approximately:
|
|
353
|
+
|
|
354
|
+
- \`color1\` -> \`color-1\`
|
|
355
|
+
- \`color2\` -> \`color-2\`
|
|
356
|
+
- \`color3\` -> \`color-3\`
|
|
357
|
+
- \`color4\` -> \`color-4\`
|
|
358
|
+
- \`color5\` -> \`color-5\`
|
|
359
|
+
- \`color6\` and \`color7\` -> \`color-6\`
|
|
360
|
+
- \`color8\` -> \`color-7\`
|
|
361
|
+
- \`color9\` -> \`color-8\`
|
|
362
|
+
- \`color10\` -> \`color-9\`
|
|
363
|
+
- \`color11\` -> \`color-10\`
|
|
364
|
+
- \`color12\` -> \`color-11\`
|
|
365
|
+
|
|
366
|
+
The endpoints are exact; inspect contrast in the compressed middle. Replace
|
|
367
|
+
\`surface1\` with \`level2\`, \`surface2\` with \`level3\`, and \`surface3\`
|
|
368
|
+
or \`surface4\` with \`level4\`. Levels are relative and preserve a surrounding
|
|
369
|
+
color theme when nested.
|
|
370
|
+
|
|
371
|
+
Search the config and application together:
|
|
372
|
+
|
|
373
|
+
\`\`\`bash
|
|
374
|
+
rg "@tamagui/theme-builder|v5-builder|createV5Theme|componentThemes|grandChildrenThemes|surface[1-4]|color12"
|
|
375
|
+
\`\`\`
|
|
376
|
+
|
|
377
|
+
Validate the config change separately with light/dark screenshots, responsive
|
|
378
|
+
layouts, text and icon alignment, and every platform the app ships. Do not
|
|
379
|
+
accept visual drift merely because the codemod and typecheck are green.`
|
|
321
380
|
|
|
322
381
|
const v1ToV2Prompt = `## v1 -> v2 migration pass
|
|
323
382
|
|
|
@@ -331,12 +390,12 @@ Bring the app to the v2 baseline before applying v3 changes.
|
|
|
331
390
|
|
|
332
391
|
### Config
|
|
333
392
|
|
|
334
|
-
-
|
|
393
|
+
- 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.
|
|
335
394
|
- Import animations separately from \`@tamagui/config/animations-css\`, \`animations-rn\`, \`animations-reanimated\`, or \`animations-motion\`.
|
|
336
395
|
- Move root \`createTamagui\` settings into \`settings\`.
|
|
337
396
|
- Account for the defaults \`flexBasis: 0\` and \`position: static\`. Use \`styleCompat: 'legacy'\` or explicit props if needed.
|
|
338
397
|
- Rename media queries: \`$2xl\` -> \`$xxl\`, \`$2xs\` -> \`$xxs\`, and max queries to kebab-case such as \`$max-md\`.
|
|
339
|
-
-
|
|
398
|
+
- Preserve resolved colors and themes; defer v6 recipe conversion to the optional follow-up below.
|
|
340
399
|
|
|
341
400
|
### v1 prop and API changes
|
|
342
401
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import open from 'opener'
|
|
5
|
+
|
|
6
|
+
export interface PreviewThemeOptions {
|
|
7
|
+
theme?: string
|
|
8
|
+
themeArg?: string
|
|
9
|
+
url?: string
|
|
10
|
+
print?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function previewTheme(options: PreviewThemeOptions) {
|
|
14
|
+
const themeInput = (options.theme || options.themeArg || '').trim()
|
|
15
|
+
let themeData: any = null
|
|
16
|
+
|
|
17
|
+
if (themeInput) {
|
|
18
|
+
if (themeInput.startsWith('{') || themeInput.startsWith('[')) {
|
|
19
|
+
try {
|
|
20
|
+
themeData = JSON.parse(themeInput)
|
|
21
|
+
} catch (err: any) {
|
|
22
|
+
throw new Error(`Invalid JSON provided for --theme: ${err.message}`)
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
const filePath = path.resolve(process.cwd(), themeInput)
|
|
26
|
+
if (!fs.existsSync(filePath)) {
|
|
27
|
+
throw new Error(`Theme file not found at: ${filePath}`)
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8')
|
|
31
|
+
themeData = JSON.parse(fileContent)
|
|
32
|
+
} catch (err: any) {
|
|
33
|
+
throw new Error(`Failed to read theme file ${filePath}: ${err.message}`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const baseUrl = options.url || 'https://tamagui.dev/theme'
|
|
39
|
+
let targetUrl = baseUrl
|
|
40
|
+
|
|
41
|
+
if (themeData) {
|
|
42
|
+
const jsonStr = JSON.stringify(themeData)
|
|
43
|
+
const base64 = Buffer.from(jsonStr).toString('base64')
|
|
44
|
+
targetUrl = `${baseUrl}#theme=${encodeURIComponent(base64)}`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (options.print) {
|
|
48
|
+
console.info(targetUrl)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.info(chalk.green(`Opening theme preview:`), targetUrl)
|
|
53
|
+
open(targetUrl)
|
|
54
|
+
}
|