@tamagui/cli 2.7.7 → 3.0.0-beta.1097.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.
Files changed (46) hide show
  1. package/dist/add.cjs +76 -87
  2. package/dist/build.cjs +425 -278
  3. package/dist/cli.cjs +361 -340
  4. package/dist/generate-prompt.cjs +484 -362
  5. package/dist/generate.cjs +45 -54
  6. package/dist/index.cjs +1 -1
  7. package/dist/migrate.cjs +437 -0
  8. package/dist/setup-prompt.cjs +216 -0
  9. package/dist/to-tailwind-default-config.cjs +789 -0
  10. package/dist/to-tailwind.cjs +213 -0
  11. package/dist/update-template.cjs +43 -52
  12. package/dist/update.cjs +14 -18
  13. package/dist/upgrade.cjs +417 -401
  14. package/dist/utils.cjs +84 -101
  15. package/metro.cjs +1 -0
  16. package/metro.d.ts +1 -0
  17. package/metro.mjs +1 -0
  18. package/package.json +43 -13
  19. package/src/build.ts +594 -362
  20. package/src/cli.ts +113 -12
  21. package/src/generate-prompt.ts +428 -329
  22. package/src/migrate.ts +422 -0
  23. package/src/setup-prompt.ts +192 -0
  24. package/src/to-tailwind-default-config.ts +767 -0
  25. package/src/to-tailwind.ts +313 -0
  26. package/src/upgrade.ts +30 -33
  27. package/src/utils.ts +11 -9
  28. package/types/add.d.ts +1 -1
  29. package/types/add.d.ts.map +1 -1
  30. package/types/build.d.ts +2 -1
  31. package/types/build.d.ts.map +1 -1
  32. package/types/generate-prompt.d.ts +6 -2
  33. package/types/generate-prompt.d.ts.map +1 -1
  34. package/types/migrate.d.ts +7 -0
  35. package/types/migrate.d.ts.map +1 -0
  36. package/types/setup-prompt.d.ts +5 -0
  37. package/types/setup-prompt.d.ts.map +1 -0
  38. package/types/to-tailwind-default-config.d.ts +53 -0
  39. package/types/to-tailwind-default-config.d.ts.map +1 -0
  40. package/types/to-tailwind.d.ts +16 -0
  41. package/types/to-tailwind.d.ts.map +1 -0
  42. package/types/upgrade.d.ts.map +1 -1
  43. package/types/utils.d.ts.map +1 -1
  44. package/vite.cjs +1 -0
  45. package/vite.d.ts +1 -0
  46. package/vite.mjs +1 -0
@@ -2,22 +2,20 @@ import { join } from 'node:path'
2
2
  import * as FS from 'fs-extra'
3
3
  import type { CLIResolvedOptions } from '@tamagui/types'
4
4
 
5
- interface GeneratePromptOptions extends CLIResolvedOptions {
5
+ export interface GeneratePromptOptions extends CLIResolvedOptions {
6
6
  output?: string
7
+ styleValueSyntax?: 'string' | 'object' | 'both'
7
8
  }
8
9
 
9
10
  export async function generatePrompt(options: GeneratePromptOptions) {
10
11
  const { paths, output } = options
11
12
 
12
- // Regenerate the config first
13
+ // regenerate the config first
13
14
  const { loadTamagui } = require('@tamagui/static/loadTamagui')
14
15
  process.env.TAMAGUI_KEEP_THEMES = '1'
15
- await loadTamagui({
16
- ...options.tamaguiOptions,
17
- platform: 'web',
18
- })
16
+ await loadTamagui({ ...options.tamaguiOptions, platform: 'web' }, true)
19
17
 
20
- // Read the generated config
18
+ // read the generated config
21
19
  const configPath = join(paths.dotDir, 'tamagui.config.json')
22
20
 
23
21
  if (!FS.existsSync(configPath)) {
@@ -28,349 +26,257 @@ export async function generatePrompt(options: GeneratePromptOptions) {
28
26
 
29
27
  const config = await FS.readJSON(configPath)
30
28
 
31
- // Generate markdown
32
- const markdown = generateMarkdown(config)
29
+ // resolve styleValueSyntax: options -> env -> config setting -> interactive prompt if tty / default 'both'
30
+ const configSetting = config.tamaguiConfig?.settings?.styleValueSyntax
31
+ const explicitChoice =
32
+ options.styleValueSyntax ||
33
+ (process.env.TAMAGUI_STYLE_VALUE_SYNTAX as any) ||
34
+ configSetting
35
+ const { resolveStyleValueSyntax } = require('./setup-prompt')
36
+ const resolvedSyntax = await resolveStyleValueSyntax(explicitChoice)
37
+
38
+ // generate markdown
39
+ const markdown = generateMarkdown(config, { styleValueSyntax: resolvedSyntax })
33
40
 
34
- // Write to file
41
+ // write to file
35
42
  const outputPath = output || join(process.cwd(), 'tamagui-prompt.md')
36
43
  await FS.writeFile(outputPath, markdown, 'utf-8')
37
44
 
38
45
  console.info(`\n ✓ Generated prompt file at ${outputPath}\n`)
39
46
  }
40
47
 
41
- function generateMarkdown(config: any): string {
48
+ export interface GenerateMarkdownOptions {
49
+ styleValueSyntax?: 'string' | 'object' | 'both'
50
+ }
51
+
52
+ export function generateMarkdown(config: any, options?: GenerateMarkdownOptions): string {
42
53
  const sections: string[] = []
43
54
 
44
- // Header
55
+ // header
45
56
  sections.push('# Tamagui Configuration\n\n')
46
57
  sections.push(
47
58
  'This document provides an overview of the Tamagui configuration for this project.\n\n'
48
59
  )
49
60
 
50
- // Get shorthands for use throughout the document
61
+ // get shorthands for use throughout the document
51
62
  const shorthands = config.tamaguiConfig?.shorthands || {}
52
63
  const reverseShorthands: Record<string, string> = {}
53
64
  for (const [short, full] of Object.entries(shorthands)) {
54
65
  reverseShorthands[full as string] = short
55
66
  }
56
67
 
57
- // Helper function to get the correct property name based on settings
68
+ // helper function to get the correct property name based on settings
58
69
  const getPropName = (fullProp: string): string => {
59
70
  const settings = config.tamaguiConfig?.settings || {}
60
- if (settings.onlyAllowShorthands && reverseShorthands[fullProp]) {
61
- return reverseShorthands[fullProp]
71
+ if (settings.onlyAllowShorthands) {
72
+ if (reverseShorthands[fullProp]) return reverseShorthands[fullProp]
73
+ if (fullProp === 'backgroundColor' && reverseShorthands['background']) {
74
+ return reverseShorthands['background']
75
+ }
62
76
  }
63
77
  return fullProp
64
78
  }
65
79
 
66
- // Settings (moved to top)
67
80
  const settings = config.tamaguiConfig?.settings || {}
68
- if (Object.keys(settings).length > 0) {
69
- sections.push('## Configuration Settings\n\n')
70
- sections.push(
71
- '**IMPORTANT:** These settings affect how you write Tamagui code in this project.\n\n'
72
- )
81
+ const syntaxChoice = options?.styleValueSyntax || settings.styleValueSyntax || 'both'
73
82
 
74
- if (settings.defaultFont) {
75
- sections.push(`### Default Font: \`${settings.defaultFont}\`\n\n`)
76
- sections.push(
77
- `All text components will use the "${settings.defaultFont}" font family by default.\n\n`
78
- )
79
- }
83
+ // configuration settings
84
+ sections.push('## Configuration Settings\n\n')
85
+ sections.push(
86
+ '**IMPORTANT:** These settings affect how you write Tamagui code in this project.\n\n'
87
+ )
80
88
 
81
- if (settings.onlyAllowShorthands !== undefined) {
82
- sections.push(`### Only Allow Shorthands: \`${settings.onlyAllowShorthands}\`\n\n`)
83
- if (settings.onlyAllowShorthands) {
84
- sections.push('**You MUST use shorthand properties in this project.**\n\n')
85
- sections.push('Full property names are not allowed. For example:\n')
86
- sections.push('- ✅ `<View w="$10" />` (correct)\n')
87
- sections.push('- ❌ `<View width="$10" />` (will error)\n\n')
88
- sections.push(
89
- 'See the Shorthand Properties section below for all available shorthands.\n\n'
90
- )
91
- } else {
92
- sections.push('You can use either shorthand or full property names.\n\n')
93
- }
94
- }
89
+ if (settings.defaultFont) {
90
+ sections.push(`### Default Font: \`${settings.defaultFont}\`\n\n`)
91
+ sections.push(
92
+ `All text components will use the "${settings.defaultFont}" font family by default.\n\n`
93
+ )
94
+ }
95
95
 
96
- if (settings.themeClassNameOnRoot !== undefined) {
96
+ if (settings.onlyAllowShorthands !== undefined) {
97
+ sections.push(`### Only Allow Shorthands: \`${settings.onlyAllowShorthands}\`\n\n`)
98
+ if (settings.onlyAllowShorthands) {
99
+ sections.push('**You MUST use shorthand properties in this project.**\n\n')
100
+ sections.push('Full property names are not allowed. For example:\n')
101
+ sections.push('- ✅ `<View w="10" />` (correct)\n')
102
+ sections.push('- ❌ `<View width="10" />` (will error)\n\n')
97
103
  sections.push(
98
- `### Theme Class Name on Root: \`${settings.themeClassNameOnRoot}\`\n\n`
104
+ 'See the Shorthand Properties section below for all available shorthands.\n\n'
99
105
  )
100
- if (settings.themeClassNameOnRoot) {
101
- sections.push('Theme classes are applied to the root HTML element.\n\n')
102
- }
103
- }
104
-
105
- // Check for platform-specific settings
106
- const platform = settings.platform || settings.defaultProps?.platform
107
- if (platform) {
108
- sections.push(`### Platform Mode: \`${platform}\`\n\n`)
109
-
110
- if (platform === 'web') {
111
- sections.push('This project is configured for **web only**.\n\n')
112
- } else if (platform === 'native') {
113
- sections.push('This project is configured for **React Native only**.\n\n')
114
- }
115
- }
116
-
117
- // Check for web-specific optimizations
118
- if (settings.webContainerType) {
119
- sections.push(`### Web Container Type: \`${settings.webContainerType}\`\n\n`)
120
- sections.push('Enables web-specific container query optimizations.\n\n')
121
- }
122
-
123
- // Check for strictness settings (common patterns)
124
- const configString = JSON.stringify(config.tamaguiConfig)
125
- if (configString.includes('semi-strict-web')) {
126
- sections.push('### Mode: `semi-strict-web`\n\n')
127
- sections.push('This configuration uses semi-strict-web mode, which:\n')
128
- sections.push('- Optimizes for web performance\n')
129
- sections.push('- May have limited React Native API support\n')
130
- sections.push('- Focuses on web-first development\n\n')
106
+ } else {
107
+ sections.push('You can use either shorthand or full property names.\n\n')
131
108
  }
132
109
  }
133
110
 
134
- // Store components section for later (will be output at the end)
135
- const componentsSection: string[] = []
136
-
137
- const allComponents: string[] = []
138
- for (const componentModule of config.components) {
139
- const componentNames = Object.keys(componentModule.nameToInfo)
140
- allComponents.push(...componentNames)
141
- }
142
-
143
- // Group components by prefix (e.g., Dialog, DialogClose -> Dialog.Close)
144
- // Strategy: Find potential base components and check if others follow the pattern
145
- const componentGroups = new Map<string, Set<string>>()
146
- const processed = new Set<string>()
147
-
148
- // Sort components to process shorter names first (potential base components)
149
- const sortedComponents = [...allComponents].sort((a, b) => a.length - b.length)
150
-
151
- for (const name of sortedComponents) {
152
- if (processed.has(name)) continue
153
-
154
- // Check if other components start with this name followed by an uppercase letter
155
- const children = allComponents.filter(
156
- (other) =>
157
- other !== name && other.startsWith(name) && other[name.length]?.match(/[A-Z]/)
111
+ // style value syntax section
112
+ if (syntaxChoice === 'string') {
113
+ sections.push('### Style value syntax: `string`\n\n')
114
+ sections.push(
115
+ 'Only the string form is allowed in this project (e.g. `bg="red hover:blue"`).\n\n'
158
116
  )
117
+ } else if (syntaxChoice === 'object') {
118
+ sections.push('### Style value syntax: `object`\n\n')
119
+ sections.push(
120
+ "Only the object form is allowed in this project (e.g. `bg={{ default: 'red', hover: 'blue' }}`).\n\n"
121
+ )
122
+ } else {
123
+ sections.push('### Style value syntax\n\n')
124
+ sections.push('Both string and object style value syntax are allowed.\n\n')
125
+ }
159
126
 
160
- if (children.length > 0) {
161
- // This is a base component with children
162
- componentGroups.set(name, new Set(children))
163
- processed.add(name)
164
- children.forEach((child) => processed.add(child))
127
+ if (settings.addThemeClassName !== undefined) {
128
+ sections.push(`### Theme Class Name: \`${settings.addThemeClassName}\`\n\n`)
129
+ if (settings.addThemeClassName === 'html') {
130
+ sections.push('Theme classes are applied to the root HTML element.\n\n')
165
131
  }
166
132
  }
167
133
 
168
- // Collect standalone components (not part of any group)
169
- const standaloneComponents = allComponents.filter((name) => !processed.has(name))
170
-
171
- componentsSection.push('## Components\n\n')
172
- componentsSection.push('The following components are available:\n\n')
134
+ if (settings.allowedStyleValues) {
135
+ sections.push('### Allowed Style Values\n\n')
136
+ sections.push(
137
+ `Type validation: \`${JSON.stringify(settings.allowedStyleValues)}\`.\n\n`
138
+ )
139
+ sections.push(
140
+ 'Single-token values are type-checked. Run `tamagui check --strict` to also validate conditional payloads.\n\n'
141
+ )
142
+ }
173
143
 
174
- // Combine and sort all base components (both standalone and those with children)
175
- const allBaseComponents = [
176
- ...standaloneComponents,
177
- ...Array.from(componentGroups.keys()),
178
- ].sort()
144
+ // flat value grammar section
145
+ sections.push('## Flat Value Grammar\n\n')
146
+ sections.push('Conditional style values follow this grammar:\n\n')
147
+ sections.push('```txt\n')
148
+ sections.push('value := base? clause*\n')
149
+ sections.push('clause := modifier(:modifier)*:payload\n')
150
+ sections.push('```\n\n')
151
+ sections.push(
152
+ '- No `$` sigils: bare token names (`bg="background"`, not `bg="$background"`).\n'
153
+ )
154
+ sections.push(
155
+ '- Kebab-case theme names: `background-hover`, `border-color`, `shadow-color`.\n'
156
+ )
157
+ sections.push('- Numbers are px: `p={4}` is 4px, while `p="4"` is space token 4.\n')
158
+ sections.push(
159
+ '- Specificity precedence: platform (`ios:` > `native:` > bare) > condition count > category (media < container < theme < group < state).\n\n'
160
+ )
179
161
 
180
- // Output components
181
- for (const name of allBaseComponents) {
182
- componentsSection.push(`- ${name}\n`)
162
+ const bgProp = getPropName('background')
163
+ const pProp = getPropName('padding')
183
164
 
184
- // If this component has children, output them
185
- if (componentGroups.has(name)) {
186
- const children = Array.from(componentGroups.get(name)!).sort()
187
- for (const child of children) {
188
- const suffix = child.slice(name.length)
189
- componentsSection.push(` - ${name}.${suffix}\n`)
190
- }
191
- }
165
+ if (syntaxChoice === 'string') {
166
+ sections.push('**String form:**\n\n')
167
+ sections.push('```tsx\n')
168
+ sections.push(
169
+ `<View ${bgProp}="background hover:background-hover dark:blue-500" ${pProp}="4 sm:6 max-sm:2" />\n`
170
+ )
171
+ sections.push('```\n\n')
172
+ } else if (syntaxChoice === 'object') {
173
+ sections.push('**Object form:**\n\n')
174
+ sections.push('```tsx\n')
175
+ sections.push(
176
+ `<View ${bgProp}={{ default: 'background', hover: 'background-hover', dark: 'blue-500' }} ${pProp}={{ default: '4', sm: '6', 'max-sm': '2' }} />\n`
177
+ )
178
+ sections.push('```\n\n')
179
+ } else {
180
+ sections.push('Both forms are allowed:\n\n')
181
+ sections.push('```tsx\n')
182
+ sections.push('// string form\n')
183
+ sections.push(
184
+ `<View ${bgProp}="background hover:background-hover dark:blue-500" ${pProp}="4 sm:6 max-sm:2" />\n\n`
185
+ )
186
+ sections.push('// object form\n')
187
+ sections.push(
188
+ `<View ${bgProp}={{ default: 'background', hover: 'background-hover', dark: 'blue-500' }} ${pProp}={{ default: '4', sm: '6', 'max-sm': '2' }} />\n`
189
+ )
190
+ sections.push('```\n\n')
192
191
  }
193
192
 
194
- componentsSection.push('\n')
195
-
196
- // Shorthands
193
+ // shorthands
197
194
  sections.push('## Shorthand Properties\n\n')
198
195
  sections.push('These shorthand properties are available for styling:\n\n')
199
-
200
196
  const shorthandEntries = Object.entries(shorthands).sort(([a], [b]) =>
201
197
  a.localeCompare(b)
202
198
  )
203
-
204
199
  sections.push(
205
200
  shorthandEntries.map(([short, full]) => `- \`${short}\` → \`${full}\``).join('\n')
206
201
  )
207
202
  sections.push('\n\n')
208
203
 
209
- // Themes
210
- sections.push('## Themes\n\n')
211
-
212
- const themes = config.tamaguiConfig?.themes || {}
213
- const themeNames = Object.keys(themes).sort()
214
-
215
- // Parse and hierarchically organize themes
216
- interface ThemeHierarchy {
217
- level1: Set<string> // light/dark
218
- level2: Set<string> // color names (blue, red, etc)
219
- level3: Set<string> // variants (alt1, alt2, etc)
220
- components: Set<string> // Component names
221
- }
222
-
223
- const hierarchy: ThemeHierarchy = {
224
- level1: new Set(),
225
- level2: new Set(),
226
- level3: new Set(),
227
- components: new Set(),
228
- }
229
-
230
- for (const themeName of themeNames) {
231
- const parts = themeName.split('_')
232
-
233
- // Level 1: light/dark
234
- if (parts[0] === 'light' || parts[0] === 'dark') {
235
- hierarchy.level1.add(parts[0])
236
-
237
- // Level 2: color names (blue, red, green, etc.)
238
- if (
239
- parts.length > 1 &&
240
- parts[1] &&
241
- !parts[1].startsWith('alt') &&
242
- parts[1] !== 'active'
243
- ) {
244
- // Check if it's not a component by looking if it starts with uppercase
245
- if (parts[1][0] === parts[1][0].toLowerCase()) {
246
- hierarchy.level2.add(parts[1])
247
- }
248
- }
249
-
250
- // Level 3: variants (alt1, alt2, etc.)
251
- for (const part of parts) {
252
- if (part.startsWith('alt') || part === 'active') {
253
- hierarchy.level3.add(part)
254
- }
255
- }
256
-
257
- // Components: parts that start with uppercase
258
- for (const part of parts) {
259
- if (
260
- part[0] &&
261
- part[0] === part[0].toUpperCase() &&
262
- part[0] !== part[0].toLowerCase()
263
- ) {
264
- hierarchy.components.add(part)
265
- }
266
- }
267
- } else {
268
- // Base theme without light/dark prefix
269
- if (parts.length === 1) {
270
- hierarchy.level1.add(themeName)
271
- }
204
+ // named control sizes table
205
+ const sizes = config.tamaguiConfig?.sizes
206
+ if (sizes && typeof sizes === 'object') {
207
+ sections.push('## Named Control Sizes\n\n')
208
+ sections.push(
209
+ `Control components (Button, Input, etc.) use the configured names below. Default is \`${sizes.default ?? 'md'}\`.\n\n`
210
+ )
211
+ sections.push('| Size | Configuration |\n')
212
+ sections.push('|---|---|\n')
213
+ for (const [sizeKey, val] of Object.entries(sizes)) {
214
+ if (sizeKey === 'default') continue
215
+ const valStr =
216
+ typeof val === 'object' && val !== null ? JSON.stringify(val) : String(val)
217
+ const isDefault = sizes.default === sizeKey ? ' (default)' : ''
218
+ sections.push(`| \`${sizeKey}\`${isDefault} | ${valStr} |\n`)
272
219
  }
220
+ sections.push('\n')
273
221
  }
274
222
 
275
- sections.push('Themes are organized hierarchically and can be combined:\n\n')
223
+ // themes
224
+ sections.push('## Themes\n\n')
225
+ const themes = config.tamaguiConfig?.themes || {}
226
+ const themeNames = Object.keys(themes).sort()
276
227
 
277
- if (hierarchy.level1.size > 0) {
278
- sections.push('**Level 1 (Base):**\n\n')
228
+ sections.push(themeNames.map((name) => `- \`${name}\``).join('\n'))
229
+ sections.push(
230
+ '\n\nTheme names above are exact configured names. Nested themes resolve relative to their parent. Use an explicit theme boundary in a component skin.\n\n'
231
+ )
232
+ if (themeNames.length) {
233
+ sections.push('### Theme Usage\n\n')
279
234
  sections.push(
280
- Array.from(hierarchy.level1)
281
- .sort()
282
- .map((name) => `- ${name}`)
283
- .join('\n')
235
+ `\`\`\`tsx\n<Theme name=${JSON.stringify(themeNames[0])}>\n <Button>Uses this theme</Button>\n</Theme>\n\`\`\`\n\n`
284
236
  )
285
- sections.push('\n\n')
286
237
  }
287
238
 
288
- if (hierarchy.level2.size > 0) {
289
- sections.push('**Level 2 (Color Schemes):**\n\n')
239
+ sections.push('**Accessing theme values:**\n\n')
240
+ sections.push('Components access theme values by their bare names:\n\n')
241
+ const colorProp = getPropName('color')
242
+ if (syntaxChoice === 'string') {
243
+ sections.push('```tsx\n')
290
244
  sections.push(
291
- Array.from(hierarchy.level2)
292
- .sort()
293
- .map((name) => `- ${name}`)
294
- .join('\n')
245
+ `<View ${bgProp}="background hover:background-hover" ${colorProp}="color" />\n`
295
246
  )
296
- sections.push('\n\n')
297
- }
298
-
299
- if (hierarchy.level3.size > 0) {
300
- sections.push('**Level 3 (Variants):**\n\n')
247
+ sections.push('```\n\n')
248
+ } else if (syntaxChoice === 'object') {
249
+ sections.push('```tsx\n')
301
250
  sections.push(
302
- Array.from(hierarchy.level3)
303
- .sort()
304
- .map((name) => `- ${name}`)
305
- .join('\n')
251
+ `<View ${bgProp}={{ default: 'background', hover: 'background-hover' }} ${colorProp}={{ default: 'color' }} />\n`
306
252
  )
307
- sections.push('\n\n')
308
- }
309
-
310
- if (hierarchy.components.size > 0) {
311
- sections.push('**Component Themes:**\n\n')
253
+ sections.push('```\n\n')
254
+ } else {
255
+ sections.push('```tsx\n')
256
+ sections.push('// string form\n')
312
257
  sections.push(
313
- Array.from(hierarchy.components)
314
- .sort()
315
- .map((name) => `- ${name}`)
316
- .join('\n')
258
+ `<View ${bgProp}="background hover:background-hover" ${colorProp}="color" />\n\n`
317
259
  )
318
- sections.push('\n\n')
260
+ sections.push('// object form\n')
261
+ sections.push(
262
+ `<View ${bgProp}={{ default: 'background', hover: 'background-hover' }} ${colorProp}={{ default: 'color' }} />\n`
263
+ )
264
+ sections.push('```\n\n')
319
265
  }
320
266
 
321
- // Add usage documentation
322
- sections.push('### Theme Usage\n\n')
323
- sections.push(
324
- 'Themes are combined hierarchically. For example, `light_blue_alt1_Button` combines:\n'
325
- )
326
- sections.push('- Base: `light`\n')
327
- sections.push('- Color: `blue`\n')
328
- sections.push('- Variant: `alt1`\n')
329
- sections.push('- Component: `Button`\n\n')
330
-
331
- sections.push('**Basic usage:**\n\n')
332
- sections.push('```tsx\n')
333
- sections.push('// Apply a theme to components\n')
334
- sections.push('export default () => (\n')
335
- sections.push(' <Theme name="dark">\n')
336
- sections.push(" <Button>I'm a dark button</Button>\n")
337
- sections.push(' </Theme>\n')
338
- sections.push(')\n\n')
339
- sections.push('// Themes nest and combine automatically\n')
340
- sections.push('export default () => (\n')
341
- sections.push(' <Theme name="dark">\n')
342
- sections.push(' <Theme name="blue">\n')
343
- sections.push(' <Button>Uses dark_blue theme</Button>\n')
344
- sections.push(' </Theme>\n')
345
- sections.push(' </Theme>\n')
346
- sections.push(')\n')
347
- sections.push('```\n\n')
348
-
349
- sections.push('**Accessing theme values:**\n\n')
350
- sections.push('Components can access theme values using `$` token syntax:\n\n')
351
- sections.push('```tsx\n')
352
- sections.push(
353
- `<View ${getPropName('backgroundColor')}="$background" ${getPropName('color')}="$color" />\n`
354
- )
355
- sections.push('```\n\n')
356
-
357
267
  sections.push('**Special props:**\n\n')
358
- sections.push('- `inverse`: Automatically swaps light dark themes\n')
359
- sections.push('- `reset`: Reverts to grandparent theme\n\n')
268
+ sections.push('- `theme="inverse"`: Uses the opposite light or dark sub-theme\n')
360
269
 
361
- // Tokens
270
+ // tokens
362
271
  sections.push('## Tokens\n\n')
363
- sections.push(
364
- 'Tokens are design system values that can be referenced using the `$` prefix.\n\n'
365
- )
272
+ sections.push('Tokens are design system values referenced by their bare names.\n\n')
366
273
 
367
274
  const tokens = config.tamaguiConfig?.tokens || {}
368
275
 
369
- // Space tokens
370
- if (tokens.space) {
276
+ // space tokens (only if present and non-empty)
277
+ if (tokens.space && Object.keys(tokens.space).length > 0) {
371
278
  sections.push('### Space Tokens\n\n')
372
279
  const spaceTokens = Object.entries(tokens.space).sort(([a], [b]) => {
373
- // Sort numerically where possible
374
280
  const numA = parseFloat(a)
375
281
  const numB = parseFloat(b)
376
282
  if (!isNaN(numA) && !isNaN(numB)) {
@@ -386,8 +292,8 @@ function generateMarkdown(config: any): string {
386
292
  sections.push('\n\n')
387
293
  }
388
294
 
389
- // Size tokens
390
- if (tokens.size) {
295
+ // size tokens (only if present and non-empty)
296
+ if (tokens.size && Object.keys(tokens.size).length > 0) {
391
297
  sections.push('### Size Tokens\n\n')
392
298
  const sizeTokens = Object.entries(tokens.size).sort(([a], [b]) => {
393
299
  const numA = parseFloat(a)
@@ -405,8 +311,8 @@ function generateMarkdown(config: any): string {
405
311
  sections.push('\n\n')
406
312
  }
407
313
 
408
- // Radius tokens
409
- if (tokens.radius) {
314
+ // radius tokens (only if present and non-empty)
315
+ if (tokens.radius && Object.keys(tokens.radius).length > 0) {
410
316
  sections.push('### Radius Tokens\n\n')
411
317
  const radiusTokens = Object.entries(tokens.radius).sort(([a], [b]) => {
412
318
  const numA = parseFloat(a)
@@ -424,8 +330,8 @@ function generateMarkdown(config: any): string {
424
330
  sections.push('\n\n')
425
331
  }
426
332
 
427
- // zIndex tokens
428
- if (tokens.zIndex) {
333
+ // zIndex tokens (only if present and non-empty)
334
+ if (tokens.zIndex && Object.keys(tokens.zIndex).length > 0) {
429
335
  sections.push('### Z-Index Tokens\n\n')
430
336
  const zIndexTokens = Object.entries(tokens.zIndex).sort(([a], [b]) => {
431
337
  const numA = parseFloat(a)
@@ -443,41 +349,124 @@ function generateMarkdown(config: any): string {
443
349
  sections.push('\n\n')
444
350
  }
445
351
 
446
- // Color tokens
447
- if (tokens.color) {
352
+ // color tokens (only if present and non-empty; summarized if large palette set)
353
+ let sampleColorBg = 'blue-500'
354
+ let sampleColorText = 'color'
355
+ if (tokens.color && Object.keys(tokens.color).length > 0) {
448
356
  sections.push('### Color Tokens\n\n')
449
- const colorTokens = Object.entries(tokens.color).sort(([a], [b]) =>
450
- a.localeCompare(b)
357
+ const colorKeys = Object.keys(tokens.color).sort()
358
+
359
+ // detect tailwind style palette keys: <palette>-<step>
360
+ const paletteMap = new Map<string, Set<string>>()
361
+ const standaloneColors: Array<[string, any]> = []
362
+
363
+ for (const key of colorKeys) {
364
+ const match = key.match(/^([a-z]+)-(\d+)$/)
365
+ if (match) {
366
+ const [, palette, step] = match
367
+ if (!paletteMap.has(palette)) {
368
+ paletteMap.set(palette, new Set())
369
+ }
370
+ paletteMap.get(palette)!.add(step)
371
+ } else {
372
+ standaloneColors.push([key, tokens.color[key]])
373
+ }
374
+ }
375
+
376
+ if (paletteMap.size >= 4) {
377
+ // summarize palettes compactly instead of 300+ line dump
378
+ const palettes = Array.from(paletteMap.keys()).sort().join(', ')
379
+ sections.push(`**Tailwind Palettes (\`<name>-<50..950>\`):** ${palettes}\n\n`)
380
+ if (standaloneColors.length > 0) {
381
+ sections.push('**Named Colors:**\n\n')
382
+ sections.push(
383
+ standaloneColors
384
+ .map(([k, v]) => `- \`${k}\`: ${formatTokenValue(v)}`)
385
+ .join('\n')
386
+ )
387
+ sections.push('\n\n')
388
+ }
389
+ sampleColorBg = colorKeys.find((k) => k.startsWith('blue-')) || colorKeys[0]
390
+ sampleColorText =
391
+ colorKeys.find((k) => k.startsWith('gray-') || k.startsWith('zinc-')) ||
392
+ colorKeys[1] ||
393
+ 'color'
394
+ } else {
395
+ sections.push(
396
+ colorKeys
397
+ .map((key) => `- \`${key}\`: ${formatTokenValue(tokens.color[key])}`)
398
+ .join('\n')
399
+ )
400
+ sections.push('\n\n')
401
+ sampleColorBg = colorKeys[0] || 'background'
402
+ sampleColorText = colorKeys[1] || 'color'
403
+ }
404
+ } else {
405
+ sampleColorBg = 'background'
406
+ sampleColorText = 'color'
407
+ }
408
+
409
+ // token usage examples
410
+ sections.push('### Token Usage\n\n')
411
+ sections.push('Tokens can be used in component props by their bare names:\n\n')
412
+
413
+ const paddingProp = getPropName('padding')
414
+ const gapProp = getPropName('gap')
415
+ const marginProp = getPropName('margin')
416
+ const widthProp = getPropName('width')
417
+ const heightProp = getPropName('height')
418
+ const radiusProp = getPropName('borderRadius')
419
+ const sampleRadius = tokens.radius?.md
420
+ ? 'md'
421
+ : Object.keys(tokens.radius || {})[0] || '0px'
422
+
423
+ if (syntaxChoice === 'string') {
424
+ sections.push('```tsx\n')
425
+ sections.push(
426
+ `// Space tokens - for margin, padding, gap\n<View ${paddingProp}="4 sm:6" ${gapProp}="2" ${marginProp}="3" />\n\n`
451
427
  )
452
428
  sections.push(
453
- colorTokens
454
- .map(([key, value]) => `- \`${key}\`: ${formatTokenValue(value)}`)
455
- .join('\n')
429
+ `// Size tokens - for width, height, dimensions\n<View ${widthProp}="10 sm:12" ${heightProp}="6" />\n\n`
456
430
  )
457
- sections.push('\n\n')
431
+ sections.push(
432
+ `// Color tokens - for colors and backgrounds\n<View ${bgProp}="${sampleColorBg} hover:${sampleColorBg}" ${colorProp}="${sampleColorText}" />\n\n`
433
+ )
434
+ sections.push(
435
+ `// Radius tokens - for border-radius\n<View ${radiusProp}="${sampleRadius}" />\n`
436
+ )
437
+ sections.push('```\n\n')
438
+ } else if (syntaxChoice === 'object') {
439
+ sections.push('```tsx\n')
440
+ sections.push(
441
+ `// Space tokens - for margin, padding, gap\n<View ${paddingProp}={{ default: '4', sm: '6' }} ${gapProp}="2" ${marginProp}="3" />\n\n`
442
+ )
443
+ sections.push(
444
+ `// Size tokens - for width, height, dimensions\n<View ${widthProp}={{ default: '10', sm: '12' }} ${heightProp}="6" />\n\n`
445
+ )
446
+ sections.push(
447
+ `// Color tokens - for colors and backgrounds\n<View ${bgProp}={{ default: '${sampleColorBg}', hover: '${sampleColorBg}' }} ${colorProp}={{ default: '${sampleColorText}' }} />\n\n`
448
+ )
449
+ sections.push(
450
+ `// Radius tokens - for border-radius\n<View ${radiusProp}="${sampleRadius}" />\n`
451
+ )
452
+ sections.push('```\n\n')
453
+ } else {
454
+ sections.push('```tsx\n')
455
+ sections.push('// String form\n')
456
+ sections.push(
457
+ `<View ${paddingProp}="4 sm:6" ${widthProp}="10 sm:12" ${bgProp}="${sampleColorBg}" />\n\n`
458
+ )
459
+ sections.push('// Object form\n')
460
+ sections.push(
461
+ `<View ${paddingProp}={{ default: '4', sm: '6' }} ${widthProp}={{ default: '10', sm: '12' }} ${bgProp}={{ default: '${sampleColorBg}' }} />\n\n`
462
+ )
463
+ sections.push(
464
+ `// Space and radius tokens\n<View ${gapProp}="2" ${marginProp}="3" ${heightProp}="6" ${radiusProp}="${sampleRadius}" />\n`
465
+ )
466
+ sections.push('```\n\n')
458
467
  }
459
468
 
460
- // Token usage examples
461
- sections.push('### Token Usage\n\n')
462
- sections.push('Tokens can be used in component props with the `$` prefix:\n\n')
463
- sections.push('```tsx\n')
464
- sections.push('// Space tokens - for margin, padding, gap\n')
465
- sections.push(
466
- `<View ${getPropName('padding')}="$4" ${getPropName('gap')}="$2" ${getPropName('margin')}="$3" />\n\n`
467
- )
468
- sections.push('// Size tokens - for width, height, dimensions\n')
469
- sections.push(
470
- `<View ${getPropName('width')}="$10" ${getPropName('height')}="$6" />\n\n`
471
- )
472
- sections.push('// Color tokens - for colors and backgrounds\n')
473
- sections.push(
474
- `<View ${getPropName('backgroundColor')}="$blue5" ${getPropName('color')}="$gray12" />\n\n`
475
- )
476
- sections.push('// Radius tokens - for border-radius\n')
477
- sections.push(`<View ${getPropName('borderRadius')}="$4" />\n`)
478
- sections.push('```\n\n')
479
-
480
- // Media queries
469
+ // media queries
481
470
  if (config.tamaguiConfig?.media) {
482
471
  sections.push('## Media Queries\n\n')
483
472
  sections.push('Available responsive breakpoints:\n\n')
@@ -486,7 +475,7 @@ function generateMarkdown(config: any): string {
486
475
  const mediaEntries = Object.entries(media).sort(([a], [b]) => a.localeCompare(b))
487
476
 
488
477
  for (const [name, query] of mediaEntries) {
489
- sections.push(`- **${name}**: ${JSON.stringify(query)}\n`)
478
+ sections.push(`- **${name}**: ${formatMediaQuery(query)}\n`)
490
479
  }
491
480
  sections.push('\n')
492
481
 
@@ -494,43 +483,59 @@ function generateMarkdown(config: any): string {
494
483
  sections.push(
495
484
  'Media queries can be used as style props or with the `useMedia` hook:\n\n'
496
485
  )
497
- sections.push('```tsx\n')
498
- sections.push('// As style props (prefix with $)\n')
499
486
 
500
- // Get first media query name as example
501
- const firstMediaName = mediaEntries[0]?.[0]
502
- if (firstMediaName) {
503
- sections.push(
504
- `<View ${getPropName('width')}="100%" $${firstMediaName}={{ ${getPropName('width')}: "50%" }} />\n\n`
505
- )
506
- }
487
+ // pick a representative media query (prefer sm or md over height-*)
488
+ const representative =
489
+ mediaEntries.find(([n]) => n === 'sm' || n === 'md')?.[0] ||
490
+ mediaEntries.find(([n]) => !n.includes('-'))?.[0] ||
491
+ mediaEntries[0]?.[0]
492
+
493
+ if (representative) {
494
+ const isIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(representative)
495
+ const mediaAccess = isIdentifier
496
+ ? `media.${representative}`
497
+ : `media['${representative}']`
498
+
499
+ sections.push('```tsx\n')
500
+ if (syntaxChoice === 'string') {
501
+ sections.push('// As a clause in the same style value\n')
502
+ sections.push(`<View ${widthProp}="100% ${representative}:50%" />\n\n`)
503
+ } else if (syntaxChoice === 'object') {
504
+ sections.push('// Using the object form\n')
505
+ sections.push(
506
+ `<View ${widthProp}={{ default: '100%', '${representative}': '50%' }} />\n\n`
507
+ )
508
+ } else {
509
+ sections.push('// String form and object form\n')
510
+ sections.push(`<View ${widthProp}="100% ${representative}:50%" />\n`)
511
+ sections.push(
512
+ `<View ${widthProp}={{ default: '100%', '${representative}': '50%' }} />\n\n`
513
+ )
514
+ }
507
515
 
508
- sections.push('// Using the useMedia hook\n')
509
- sections.push('const media = useMedia()\n')
510
- if (firstMediaName) {
511
- sections.push(`if (media.${firstMediaName}) {\n`)
516
+ sections.push('// Using the useMedia hook\n')
517
+ sections.push('const media = useMedia()\n')
518
+ sections.push(`if (${mediaAccess}) {\n`)
512
519
  sections.push(' // Render for this breakpoint\n')
513
520
  sections.push('}\n')
521
+ sections.push('```\n\n')
514
522
  }
515
- sections.push('```\n\n')
516
523
  }
517
524
 
518
- // Fonts
525
+ // fonts
519
526
  if (config.tamaguiConfig?.fonts) {
520
527
  sections.push('## Fonts\n\n')
521
528
  sections.push('Available font families:\n\n')
522
-
523
529
  const fonts = config.tamaguiConfig.fonts
524
530
  const fontNames = Object.keys(fonts).sort()
525
531
  sections.push(fontNames.map((name) => `- ${name}`).join('\n'))
526
532
  sections.push('\n\n')
527
533
  }
528
534
 
529
- // Animations
535
+ // animations
530
536
  if (config.tamaguiConfig?.animations) {
531
537
  sections.push('## Animations\n\n')
532
538
  sections.push('Available animation presets:\n\n')
533
-
534
539
  const animations = config.tamaguiConfig.animations
535
540
  if (animations.animations) {
536
541
  const animationNames = Object.keys(animations.animations).sort()
@@ -539,17 +544,111 @@ function generateMarkdown(config: any): string {
539
544
  }
540
545
  }
541
546
 
542
- // Add components section at the end
543
- sections.push(...componentsSection)
547
+ // components section: emit barrel's public import surface
548
+ const componentsSection: string[] = []
549
+ const componentSet = new Set<string>()
550
+
551
+ // try loading public exports from tamagui or candidate modules
552
+ let barrelExported = false
553
+ const candidateModules = [
554
+ 'tamagui',
555
+ ...(config.components || []).map((c: any) => c.moduleName),
556
+ ]
557
+ for (const modName of candidateModules) {
558
+ if (!modName) continue
559
+ try {
560
+ const mod = require(modName)
561
+ for (const [key, val] of Object.entries(mod)) {
562
+ if (
563
+ /^[A-Z]/.test(key) &&
564
+ (typeof val === 'function' || (typeof val === 'object' && val !== null))
565
+ ) {
566
+ if (
567
+ !key.endsWith('Context') &&
568
+ !key.endsWith('Provider') &&
569
+ key !== 'Fragment'
570
+ ) {
571
+ componentSet.add(key)
572
+ barrelExported = true
573
+ }
574
+ }
575
+ }
576
+ if (barrelExported) break
577
+ } catch {
578
+ // fallback to config.components below
579
+ }
580
+ }
581
+
582
+ if (!barrelExported && config.components) {
583
+ for (const componentModule of config.components) {
584
+ for (const name of Object.keys(componentModule.nameToInfo || {})) {
585
+ componentSet.add(name)
586
+ }
587
+ }
588
+ }
589
+
590
+ // filter internal frames ending with Frame when parent or part exists
591
+ const allComponents = Array.from(componentSet).filter((name) => {
592
+ if (name.endsWith('Frame')) {
593
+ const base = name.replace(/Frame$/, '')
594
+ if (
595
+ componentSet.has(base) ||
596
+ name.startsWith('Popper') ||
597
+ name.startsWith('DialogPortal') ||
598
+ name.startsWith('SelectScrollButton')
599
+ ) {
600
+ return false
601
+ }
602
+ }
603
+ return true
604
+ })
605
+
606
+ componentsSection.push('## Components\n\n')
607
+ componentsSection.push('Available named exports (import these names directly):\n\n')
608
+ for (const name of allComponents.sort()) {
609
+ componentsSection.push(`- ${name}\n`)
610
+ }
611
+ componentsSection.push('\n')
544
612
 
613
+ sections.push(...componentsSection)
545
614
  return sections.join('')
546
615
  }
547
616
 
548
617
  function formatTokenValue(value: any): string {
549
- // If it's an object with a 'val' property (token object), extract the value
550
618
  if (typeof value === 'object' && value !== null && 'val' in value) {
551
619
  return String(value.val)
552
620
  }
553
- // Otherwise, stringify it
554
621
  return String(value)
555
622
  }
623
+
624
+ function formatMediaQuery(query: any): string {
625
+ if (typeof query !== 'object' || query === null) {
626
+ return String(query)
627
+ }
628
+ const parts: string[] = []
629
+ if (query.minWidth !== undefined) {
630
+ parts.push(`min-width: ${query.minWidth}px (screens >= ${query.minWidth}px wide)`)
631
+ }
632
+ if (query.maxWidth !== undefined) {
633
+ parts.push(`max-width: ${query.maxWidth}px (screens <= ${query.maxWidth}px wide)`)
634
+ }
635
+ if (query.minHeight !== undefined) {
636
+ parts.push(`min-height: ${query.minHeight}px (screens >= ${query.minHeight}px tall)`)
637
+ }
638
+ if (query.maxHeight !== undefined) {
639
+ parts.push(`max-height: ${query.maxHeight}px (screens <= ${query.maxHeight}px tall)`)
640
+ }
641
+ if (query.hover !== undefined) {
642
+ parts.push(`pointer: hover (${query.hover})`)
643
+ }
644
+ if (query.pointer !== undefined) {
645
+ parts.push(`pointer: ${query.pointer}`)
646
+ }
647
+ if (query.prefersReducedMotion !== undefined) {
648
+ parts.push(`prefers-reduced-motion: ${query.prefersReducedMotion}`)
649
+ }
650
+ if (parts.length === 0) {
651
+ return JSON.stringify(query)
652
+ }
653
+ return parts.join(', ')
654
+ }