@tamagui/cli 3.0.0-beta.1093.1 → 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.
@@ -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,339 +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')
81
+ const syntaxChoice = options?.styleValueSyntax || settings.styleValueSyntax || 'both'
82
+
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
+ )
88
+
89
+ if (settings.defaultFont) {
90
+ sections.push(`### Default Font: \`${settings.defaultFont}\`\n\n`)
70
91
  sections.push(
71
- '**IMPORTANT:** These settings affect how you write Tamagui code in this project.\n\n'
92
+ `All text components will use the "${settings.defaultFont}" font family by default.\n\n`
72
93
  )
94
+ }
73
95
 
74
- if (settings.defaultFont) {
75
- sections.push(`### Default Font: \`${settings.defaultFont}\`\n\n`)
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')
76
103
  sections.push(
77
- `All text components will use the "${settings.defaultFont}" font family by default.\n\n`
104
+ 'See the Shorthand Properties section below for all available shorthands.\n\n'
78
105
  )
106
+ } else {
107
+ sections.push('You can use either shorthand or full property names.\n\n')
79
108
  }
80
-
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
- }
95
-
96
- if (settings.addThemeClassName !== undefined) {
97
- sections.push(`### Theme Class Name: \`${settings.addThemeClassName}\`\n\n`)
98
- if (settings.addThemeClassName === 'html') {
99
- sections.push('Theme classes are applied to the root HTML element.\n\n')
100
- }
101
- }
102
-
103
- // Check for platform-specific settings
104
- const platform = settings.platform || settings.defaultProps?.platform
105
- if (platform) {
106
- sections.push(`### Platform Mode: \`${platform}\`\n\n`)
107
-
108
- if (platform === 'web') {
109
- sections.push('This project is configured for **web only**.\n\n')
110
- } else if (platform === 'native') {
111
- sections.push('This project is configured for **React Native only**.\n\n')
112
- }
113
- }
114
-
115
- // Check for strictness settings (common patterns)
116
- const configString = JSON.stringify(config.tamaguiConfig)
117
- if (configString.includes('semi-strict-web')) {
118
- sections.push('### Mode: `semi-strict-web`\n\n')
119
- sections.push('This configuration uses semi-strict-web mode, which:\n')
120
- sections.push('- Optimizes for web performance\n')
121
- sections.push('- May have limited React Native API support\n')
122
- sections.push('- Focuses on web-first development\n\n')
123
- }
124
- }
125
-
126
- // Store components section for later (will be output at the end)
127
- const componentsSection: string[] = []
128
-
129
- const allComponents: string[] = []
130
- for (const componentModule of config.components) {
131
- const componentNames = Object.keys(componentModule.nameToInfo)
132
- allComponents.push(...componentNames)
133
109
  }
134
110
 
135
- // Group components by prefix (e.g., Dialog, DialogClose -> Dialog.Close)
136
- // Strategy: Find potential base components and check if others follow the pattern
137
- const componentGroups = new Map<string, Set<string>>()
138
- const processed = new Set<string>()
139
-
140
- // Sort components to process shorter names first (potential base components)
141
- const sortedComponents = [...allComponents].sort((a, b) => a.length - b.length)
142
-
143
- for (const name of sortedComponents) {
144
- if (processed.has(name)) continue
145
-
146
- // Check if other components start with this name followed by an uppercase letter
147
- const children = allComponents.filter(
148
- (other) =>
149
- 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'
150
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
+ }
151
126
 
152
- if (children.length > 0) {
153
- // This is a base component with children
154
- componentGroups.set(name, new Set(children))
155
- processed.add(name)
156
- 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')
157
131
  }
158
132
  }
159
133
 
160
- // Collect standalone components (not part of any group)
161
- const standaloneComponents = allComponents.filter((name) => !processed.has(name))
162
-
163
- componentsSection.push('## Components\n\n')
164
- 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
+ }
165
143
 
166
- // Combine and sort all base components (both standalone and those with children)
167
- const allBaseComponents = [
168
- ...standaloneComponents,
169
- ...Array.from(componentGroups.keys()),
170
- ].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
+ )
171
161
 
172
- // Output components
173
- for (const name of allBaseComponents) {
174
- componentsSection.push(`- ${name}\n`)
162
+ const bgProp = getPropName('background')
163
+ const pProp = getPropName('padding')
175
164
 
176
- // If this component has children, output them
177
- if (componentGroups.has(name)) {
178
- const children = Array.from(componentGroups.get(name)!).sort()
179
- for (const child of children) {
180
- const suffix = child.slice(name.length)
181
- componentsSection.push(` - ${name}.${suffix}\n`)
182
- }
183
- }
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')
184
191
  }
185
192
 
186
- componentsSection.push('\n')
187
-
188
- // Shorthands
193
+ // shorthands
189
194
  sections.push('## Shorthand Properties\n\n')
190
195
  sections.push('These shorthand properties are available for styling:\n\n')
191
-
192
196
  const shorthandEntries = Object.entries(shorthands).sort(([a], [b]) =>
193
197
  a.localeCompare(b)
194
198
  )
195
-
196
199
  sections.push(
197
200
  shorthandEntries.map(([short, full]) => `- \`${short}\` → \`${full}\``).join('\n')
198
201
  )
199
202
  sections.push('\n\n')
200
203
 
201
- // Themes
202
- sections.push('## Themes\n\n')
203
-
204
- const themes = config.tamaguiConfig?.themes || {}
205
- const themeNames = Object.keys(themes).sort()
206
-
207
- // Parse and hierarchically organize themes
208
- interface ThemeHierarchy {
209
- level1: Set<string> // light/dark
210
- level2: Set<string> // color names (blue, red, etc)
211
- level3: Set<string> // variants (alt1, alt2, etc)
212
- components: Set<string> // Component names
213
- }
214
-
215
- const hierarchy: ThemeHierarchy = {
216
- level1: new Set(),
217
- level2: new Set(),
218
- level3: new Set(),
219
- components: new Set(),
220
- }
221
-
222
- for (const themeName of themeNames) {
223
- const parts = themeName.split('_')
224
-
225
- // Level 1: light/dark
226
- if (parts[0] === 'light' || parts[0] === 'dark') {
227
- hierarchy.level1.add(parts[0])
228
-
229
- // Level 2: color names (blue, red, green, etc.)
230
- if (
231
- parts.length > 1 &&
232
- parts[1] &&
233
- !parts[1].startsWith('alt') &&
234
- parts[1] !== 'active'
235
- ) {
236
- // Check if it's not a component by looking if it starts with uppercase
237
- if (parts[1][0] === parts[1][0].toLowerCase()) {
238
- hierarchy.level2.add(parts[1])
239
- }
240
- }
241
-
242
- // Level 3: variants (alt1, alt2, etc.)
243
- for (const part of parts) {
244
- if (part.startsWith('alt') || part === 'active') {
245
- hierarchy.level3.add(part)
246
- }
247
- }
248
-
249
- // Components: parts that start with uppercase
250
- for (const part of parts) {
251
- if (
252
- part[0] &&
253
- part[0] === part[0].toUpperCase() &&
254
- part[0] !== part[0].toLowerCase()
255
- ) {
256
- hierarchy.components.add(part)
257
- }
258
- }
259
- } else {
260
- // Base theme without light/dark prefix
261
- if (parts.length === 1) {
262
- hierarchy.level1.add(themeName)
263
- }
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`)
264
219
  }
220
+ sections.push('\n')
265
221
  }
266
222
 
267
- 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()
268
227
 
269
- if (hierarchy.level1.size > 0) {
270
- 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')
271
234
  sections.push(
272
- Array.from(hierarchy.level1)
273
- .sort()
274
- .map((name) => `- ${name}`)
275
- .join('\n')
235
+ `\`\`\`tsx\n<Theme name=${JSON.stringify(themeNames[0])}>\n <Button>Uses this theme</Button>\n</Theme>\n\`\`\`\n\n`
276
236
  )
277
- sections.push('\n\n')
278
237
  }
279
238
 
280
- if (hierarchy.level2.size > 0) {
281
- 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')
282
244
  sections.push(
283
- Array.from(hierarchy.level2)
284
- .sort()
285
- .map((name) => `- ${name}`)
286
- .join('\n')
245
+ `<View ${bgProp}="background hover:background-hover" ${colorProp}="color" />\n`
287
246
  )
288
- sections.push('\n\n')
289
- }
290
-
291
- if (hierarchy.level3.size > 0) {
292
- sections.push('**Level 3 (Variants):**\n\n')
247
+ sections.push('```\n\n')
248
+ } else if (syntaxChoice === 'object') {
249
+ sections.push('```tsx\n')
293
250
  sections.push(
294
- Array.from(hierarchy.level3)
295
- .sort()
296
- .map((name) => `- ${name}`)
297
- .join('\n')
251
+ `<View ${bgProp}={{ default: 'background', hover: 'background-hover' }} ${colorProp}={{ default: 'color' }} />\n`
298
252
  )
299
- sections.push('\n\n')
300
- }
301
-
302
- if (hierarchy.components.size > 0) {
303
- sections.push('**Component Themes:**\n\n')
253
+ sections.push('```\n\n')
254
+ } else {
255
+ sections.push('```tsx\n')
256
+ sections.push('// string form\n')
304
257
  sections.push(
305
- Array.from(hierarchy.components)
306
- .sort()
307
- .map((name) => `- ${name}`)
308
- .join('\n')
258
+ `<View ${bgProp}="background hover:background-hover" ${colorProp}="color" />\n\n`
309
259
  )
310
- 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')
311
265
  }
312
266
 
313
- // Add usage documentation
314
- sections.push('### Theme Usage\n\n')
315
- sections.push(
316
- 'Themes are combined hierarchically. For example, `light_blue_alt1_Button` combines:\n'
317
- )
318
- sections.push('- Base: `light`\n')
319
- sections.push('- Color: `blue`\n')
320
- sections.push('- Variant: `alt1`\n')
321
- sections.push('- Component: `Button`\n\n')
322
-
323
- sections.push('**Basic usage:**\n\n')
324
- sections.push('```tsx\n')
325
- sections.push('// Apply a theme to components\n')
326
- sections.push('export default () => (\n')
327
- sections.push(' <Theme name="dark">\n')
328
- sections.push(" <Button>I'm a dark button</Button>\n")
329
- sections.push(' </Theme>\n')
330
- sections.push(')\n\n')
331
- sections.push('// Themes nest and combine automatically\n')
332
- sections.push('export default () => (\n')
333
- sections.push(' <Theme name="dark">\n')
334
- sections.push(' <Theme name="blue">\n')
335
- sections.push(' <Button>Uses dark_blue theme</Button>\n')
336
- sections.push(' </Theme>\n')
337
- sections.push(' </Theme>\n')
338
- sections.push(')\n')
339
- sections.push('```\n\n')
340
-
341
- sections.push('**Accessing theme values:**\n\n')
342
- sections.push('Components access theme values by their bare names:\n\n')
343
- sections.push('```tsx\n')
344
- sections.push(
345
- `<View ${getPropName('backgroundColor')}="background" ${getPropName('color')}="color" />\n`
346
- )
347
- sections.push('```\n\n')
348
-
349
267
  sections.push('**Special props:**\n\n')
350
268
  sections.push('- `theme="inverse"`: Uses the opposite light or dark sub-theme\n')
351
- sections.push('- `reset`: Reverts to grandparent theme\n\n')
352
269
 
353
- // Tokens
270
+ // tokens
354
271
  sections.push('## Tokens\n\n')
355
272
  sections.push('Tokens are design system values referenced by their bare names.\n\n')
356
273
 
357
274
  const tokens = config.tamaguiConfig?.tokens || {}
358
275
 
359
- // Space tokens
360
- if (tokens.space) {
276
+ // space tokens (only if present and non-empty)
277
+ if (tokens.space && Object.keys(tokens.space).length > 0) {
361
278
  sections.push('### Space Tokens\n\n')
362
279
  const spaceTokens = Object.entries(tokens.space).sort(([a], [b]) => {
363
- // Sort numerically where possible
364
280
  const numA = parseFloat(a)
365
281
  const numB = parseFloat(b)
366
282
  if (!isNaN(numA) && !isNaN(numB)) {
@@ -376,8 +292,8 @@ function generateMarkdown(config: any): string {
376
292
  sections.push('\n\n')
377
293
  }
378
294
 
379
- // Size tokens
380
- if (tokens.size) {
295
+ // size tokens (only if present and non-empty)
296
+ if (tokens.size && Object.keys(tokens.size).length > 0) {
381
297
  sections.push('### Size Tokens\n\n')
382
298
  const sizeTokens = Object.entries(tokens.size).sort(([a], [b]) => {
383
299
  const numA = parseFloat(a)
@@ -395,8 +311,8 @@ function generateMarkdown(config: any): string {
395
311
  sections.push('\n\n')
396
312
  }
397
313
 
398
- // Radius tokens
399
- if (tokens.radius) {
314
+ // radius tokens (only if present and non-empty)
315
+ if (tokens.radius && Object.keys(tokens.radius).length > 0) {
400
316
  sections.push('### Radius Tokens\n\n')
401
317
  const radiusTokens = Object.entries(tokens.radius).sort(([a], [b]) => {
402
318
  const numA = parseFloat(a)
@@ -414,8 +330,8 @@ function generateMarkdown(config: any): string {
414
330
  sections.push('\n\n')
415
331
  }
416
332
 
417
- // zIndex tokens
418
- if (tokens.zIndex) {
333
+ // zIndex tokens (only if present and non-empty)
334
+ if (tokens.zIndex && Object.keys(tokens.zIndex).length > 0) {
419
335
  sections.push('### Z-Index Tokens\n\n')
420
336
  const zIndexTokens = Object.entries(tokens.zIndex).sort(([a], [b]) => {
421
337
  const numA = parseFloat(a)
@@ -433,39 +349,124 @@ function generateMarkdown(config: any): string {
433
349
  sections.push('\n\n')
434
350
  }
435
351
 
436
- // Color tokens
437
- 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) {
438
356
  sections.push('### Color Tokens\n\n')
439
- const colorTokens = Object.entries(tokens.color).sort(([a], [b]) =>
440
- a.localeCompare(b)
441
- )
442
- sections.push(
443
- colorTokens
444
- .map(([key, value]) => `- \`${key}\`: ${formatTokenValue(value)}`)
445
- .join('\n')
446
- )
447
- sections.push('\n\n')
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'
448
407
  }
449
408
 
450
- // Token usage examples
409
+ // token usage examples
451
410
  sections.push('### Token Usage\n\n')
452
411
  sections.push('Tokens can be used in component props by their bare names:\n\n')
453
- sections.push('```tsx\n')
454
- sections.push('// Space tokens - for margin, padding, gap\n')
455
- sections.push(
456
- `<View ${getPropName('padding')}="4" ${getPropName('gap')}="2" ${getPropName('margin')}="3" />\n\n`
457
- )
458
- sections.push('// Size tokens - for width, height, dimensions\n')
459
- sections.push(`<View ${getPropName('width')}="10" ${getPropName('height')}="6" />\n\n`)
460
- sections.push('// Color tokens - for colors and backgrounds\n')
461
- sections.push(
462
- `<View ${getPropName('backgroundColor')}="blue5" ${getPropName('color')}="gray12" />\n\n`
463
- )
464
- sections.push('// Radius tokens - for border-radius\n')
465
- sections.push(`<View ${getPropName('borderRadius')}="4" />\n`)
466
- sections.push('```\n\n')
467
412
 
468
- // Media queries
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`
427
+ )
428
+ sections.push(
429
+ `// Size tokens - for width, height, dimensions\n<View ${widthProp}="10 sm:12" ${heightProp}="6" />\n\n`
430
+ )
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')
467
+ }
468
+
469
+ // media queries
469
470
  if (config.tamaguiConfig?.media) {
470
471
  sections.push('## Media Queries\n\n')
471
472
  sections.push('Available responsive breakpoints:\n\n')
@@ -474,7 +475,7 @@ function generateMarkdown(config: any): string {
474
475
  const mediaEntries = Object.entries(media).sort(([a], [b]) => a.localeCompare(b))
475
476
 
476
477
  for (const [name, query] of mediaEntries) {
477
- sections.push(`- **${name}**: ${JSON.stringify(query)}\n`)
478
+ sections.push(`- **${name}**: ${formatMediaQuery(query)}\n`)
478
479
  }
479
480
  sections.push('\n')
480
481
 
@@ -482,41 +483,59 @@ function generateMarkdown(config: any): string {
482
483
  sections.push(
483
484
  'Media queries can be used as style props or with the `useMedia` hook:\n\n'
484
485
  )
485
- sections.push('```tsx\n')
486
- sections.push('// As a clause in the same style value\n')
487
486
 
488
- // Get first media query name as example
489
- const firstMediaName = mediaEntries[0]?.[0]
490
- if (firstMediaName) {
491
- sections.push(`<View ${getPropName('width')}="100% ${firstMediaName}:50%" />\n\n`)
492
- }
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
+ }
493
515
 
494
- sections.push('// Using the useMedia hook\n')
495
- sections.push('const media = useMedia()\n')
496
- if (firstMediaName) {
497
- 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`)
498
519
  sections.push(' // Render for this breakpoint\n')
499
520
  sections.push('}\n')
521
+ sections.push('```\n\n')
500
522
  }
501
- sections.push('```\n\n')
502
523
  }
503
524
 
504
- // Fonts
525
+ // fonts
505
526
  if (config.tamaguiConfig?.fonts) {
506
527
  sections.push('## Fonts\n\n')
507
528
  sections.push('Available font families:\n\n')
508
-
509
529
  const fonts = config.tamaguiConfig.fonts
510
530
  const fontNames = Object.keys(fonts).sort()
511
531
  sections.push(fontNames.map((name) => `- ${name}`).join('\n'))
512
532
  sections.push('\n\n')
513
533
  }
514
534
 
515
- // Animations
535
+ // animations
516
536
  if (config.tamaguiConfig?.animations) {
517
537
  sections.push('## Animations\n\n')
518
538
  sections.push('Available animation presets:\n\n')
519
-
520
539
  const animations = config.tamaguiConfig.animations
521
540
  if (animations.animations) {
522
541
  const animationNames = Object.keys(animations.animations).sort()
@@ -525,17 +544,111 @@ function generateMarkdown(config: any): string {
525
544
  }
526
545
  }
527
546
 
528
- // Add components section at the end
529
- 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
+ }
530
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')
612
+
613
+ sections.push(...componentsSection)
531
614
  return sections.join('')
532
615
  }
533
616
 
534
617
  function formatTokenValue(value: any): string {
535
- // If it's an object with a 'val' property (token object), extract the value
536
618
  if (typeof value === 'object' && value !== null && 'val' in value) {
537
619
  return String(value.val)
538
620
  }
539
- // Otherwise, stringify it
540
621
  return String(value)
541
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
+ }