@tamagui/cli 3.0.0-beta.922.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/cli.ts
CHANGED
|
@@ -15,28 +15,45 @@ const COMMAND_MAP = {
|
|
|
15
15
|
'--verbose': Boolean,
|
|
16
16
|
'--styles-only': Boolean,
|
|
17
17
|
'--deps-only': Boolean,
|
|
18
|
+
'--strict': Boolean,
|
|
18
19
|
},
|
|
19
20
|
async run() {
|
|
20
21
|
const { _, ...flags } = arg(this.flags)
|
|
21
22
|
const options = await getOptions({
|
|
22
23
|
debug: flags['--debug'] ? (flags['--verbose'] ? 'verbose' : true) : false,
|
|
24
|
+
loadTamaguiOptions: flags['--strict'] && !flags['--deps-only'],
|
|
23
25
|
})
|
|
24
26
|
if (!flags['--styles-only']) {
|
|
25
27
|
const { checkDeps } = require('@tamagui/static/checkDeps')
|
|
26
28
|
await checkDeps(options.paths.root)
|
|
27
29
|
}
|
|
28
30
|
if (flags['--deps-only']) return
|
|
31
|
+
if (flags['--strict']) {
|
|
32
|
+
if (!options.tamaguiOptions.config) {
|
|
33
|
+
throw new Error('Strict style checking requires a Tamagui config.')
|
|
34
|
+
}
|
|
35
|
+
const { loadTamagui } = require('@tamagui/static/loadTamagui')
|
|
36
|
+
process.env.TAMAGUI_KEEP_THEMES = '1'
|
|
37
|
+
const loaded = await loadTamagui(
|
|
38
|
+
{ ...options.tamaguiOptions, platform: 'web' },
|
|
39
|
+
true
|
|
40
|
+
)
|
|
41
|
+
if (!loaded?.tamaguiConfig) {
|
|
42
|
+
throw new Error('Unable to load the Tamagui config for strict style checking.')
|
|
43
|
+
}
|
|
44
|
+
}
|
|
29
45
|
const { checkStyleFiles, formatCheckResults, MissingConfigArtifactError } =
|
|
30
|
-
require('@tamagui/language-service/check') as
|
|
46
|
+
require('@tamagui/language-service/check') as any
|
|
31
47
|
try {
|
|
32
48
|
const result = checkStyleFiles({
|
|
33
49
|
root: options.paths.root,
|
|
34
50
|
configPath: options.paths.conf,
|
|
51
|
+
strict: flags['--strict'],
|
|
35
52
|
})
|
|
36
53
|
console.info(formatCheckResults(result))
|
|
37
54
|
if (result.diagnosticCount > 0) process.exitCode = 1
|
|
38
|
-
} catch (error) {
|
|
39
|
-
if (error instanceof MissingConfigArtifactError) {
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
if (error instanceof MissingConfigArtifactError && !flags['--strict']) {
|
|
40
57
|
console.warn(chalk.yellow(`skipping flat value check: ${error.message}`))
|
|
41
58
|
return
|
|
42
59
|
}
|
|
@@ -61,18 +78,7 @@ const COMMAND_MAP = {
|
|
|
61
78
|
})
|
|
62
79
|
const { loadTamagui } = require('@tamagui/static/loadTamagui')
|
|
63
80
|
process.env.TAMAGUI_KEEP_THEMES = '1'
|
|
64
|
-
await loadTamagui({
|
|
65
|
-
...options.tamaguiOptions,
|
|
66
|
-
platform: 'web',
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
// also generate prompt to .tamagui/prompt.md
|
|
70
|
-
const { generatePrompt } = require('./generate-prompt')
|
|
71
|
-
const { join } = require('node:path')
|
|
72
|
-
await generatePrompt({
|
|
73
|
-
...options,
|
|
74
|
-
output: join(options.paths.dotDir, 'prompt.md'),
|
|
75
|
-
})
|
|
81
|
+
await loadTamagui({ ...options.tamaguiOptions, platform: 'web' }, true)
|
|
76
82
|
},
|
|
77
83
|
},
|
|
78
84
|
|
|
@@ -161,6 +167,38 @@ const COMMAND_MAP = {
|
|
|
161
167
|
},
|
|
162
168
|
},
|
|
163
169
|
|
|
170
|
+
icons: {
|
|
171
|
+
shorthands: [],
|
|
172
|
+
description: `Generate themed SVG icon components from Lucide, Heroicons, Phosphor, or a folder of svgs.`,
|
|
173
|
+
usage: `$ tamagui icons add Search ChevronDown
|
|
174
|
+
$ tamagui icons add Copy --from phosphor --weight bold
|
|
175
|
+
$ tamagui icons add Clipboard --from heroicons --variant solid
|
|
176
|
+
$ tamagui icons add Logo --from ./assets/icons --out src/icons
|
|
177
|
+
$ tamagui icons add Search=magnifying-glass --from phosphor`,
|
|
178
|
+
flags: {
|
|
179
|
+
'--help': Boolean,
|
|
180
|
+
'--from': String,
|
|
181
|
+
'--weight': String,
|
|
182
|
+
'--variant': String,
|
|
183
|
+
'--out': String,
|
|
184
|
+
},
|
|
185
|
+
async run() {
|
|
186
|
+
const { _, ...flags } = arg(this.flags)
|
|
187
|
+
const [_cmd, subcommand, ...names] = _
|
|
188
|
+
if (subcommand !== 'add') {
|
|
189
|
+
throw new Error('usage: tamagui icons add <Name...> [--from] [--out]')
|
|
190
|
+
}
|
|
191
|
+
const { addIcons } = require('./icons')
|
|
192
|
+
await addIcons({
|
|
193
|
+
names,
|
|
194
|
+
from: flags['--from'],
|
|
195
|
+
weight: flags['--weight'],
|
|
196
|
+
variant: flags['--variant'],
|
|
197
|
+
out: flags['--out'],
|
|
198
|
+
})
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
|
|
164
202
|
build: {
|
|
165
203
|
shorthands: ['b'],
|
|
166
204
|
description: `Use to pre-build a Tamagui component directory. Use -- to run a command after optimization, then auto-restore files.`,
|
|
@@ -346,6 +384,31 @@ $ tamagui migrate --from v1`,
|
|
|
346
384
|
})
|
|
347
385
|
},
|
|
348
386
|
},
|
|
387
|
+
|
|
388
|
+
'preview-theme': {
|
|
389
|
+
shorthands: ['preview'],
|
|
390
|
+
description: `Preview a theme configuration in the browser on tamagui.dev/theme`,
|
|
391
|
+
usage: `$ tamagui preview-theme ./theme.json
|
|
392
|
+
$ tamagui preview-theme --theme '{"palettes": ...}'
|
|
393
|
+
$ tamagui preview-theme ./theme.json --print`,
|
|
394
|
+
flags: {
|
|
395
|
+
'--help': Boolean,
|
|
396
|
+
'--theme': String,
|
|
397
|
+
'--url': String,
|
|
398
|
+
'--print': Boolean,
|
|
399
|
+
},
|
|
400
|
+
async run() {
|
|
401
|
+
const { _, ...flags } = arg(this.flags)
|
|
402
|
+
const [_cmd, themeArg] = _
|
|
403
|
+
const { previewTheme } = require('./preview-theme')
|
|
404
|
+
await previewTheme({
|
|
405
|
+
theme: flags['--theme'],
|
|
406
|
+
themeArg,
|
|
407
|
+
url: flags['--url'],
|
|
408
|
+
print: flags['--print'],
|
|
409
|
+
})
|
|
410
|
+
},
|
|
411
|
+
},
|
|
349
412
|
}
|
|
350
413
|
|
|
351
414
|
type CommandDefinitions = typeof COMMAND_MAP
|
package/src/generate-prompt.ts
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
+
import { generateConfigMarkdown } from '@tamagui/static'
|
|
1
2
|
import { join } from 'node:path'
|
|
2
3
|
import * as FS from 'fs-extra'
|
|
3
4
|
import type { CLIResolvedOptions } from '@tamagui/types'
|
|
4
5
|
|
|
5
|
-
interface GeneratePromptOptions extends CLIResolvedOptions {
|
|
6
|
+
export interface GeneratePromptOptions extends CLIResolvedOptions {
|
|
6
7
|
output?: string
|
|
8
|
+
styleValueSyntax?: 'string' | 'object' | 'both'
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export async function generatePrompt(options: GeneratePromptOptions) {
|
|
10
12
|
const { paths, output } = options
|
|
11
13
|
|
|
12
|
-
//
|
|
14
|
+
// regenerate the config first
|
|
13
15
|
const { loadTamagui } = require('@tamagui/static/loadTamagui')
|
|
14
16
|
process.env.TAMAGUI_KEEP_THEMES = '1'
|
|
15
|
-
await loadTamagui({
|
|
16
|
-
...options.tamaguiOptions,
|
|
17
|
-
platform: 'web',
|
|
18
|
-
})
|
|
17
|
+
await loadTamagui({ ...options.tamaguiOptions, platform: 'web' }, true)
|
|
19
18
|
|
|
20
|
-
//
|
|
19
|
+
// read the generated config
|
|
21
20
|
const configPath = join(paths.dotDir, 'tamagui.config.json')
|
|
22
21
|
|
|
23
22
|
if (!FS.existsSync(configPath)) {
|
|
@@ -28,514 +27,21 @@ export async function generatePrompt(options: GeneratePromptOptions) {
|
|
|
28
27
|
|
|
29
28
|
const config = await FS.readJSON(configPath)
|
|
30
29
|
|
|
31
|
-
//
|
|
32
|
-
const
|
|
30
|
+
// resolve styleValueSyntax: options -> env -> config setting -> interactive prompt if tty / default 'both'
|
|
31
|
+
const configSetting = config.tamaguiConfig?.settings?.styleValueSyntax
|
|
32
|
+
const explicitChoice =
|
|
33
|
+
options.styleValueSyntax ||
|
|
34
|
+
(process.env.TAMAGUI_STYLE_VALUE_SYNTAX as any) ||
|
|
35
|
+
configSetting
|
|
36
|
+
const { resolveStyleValueSyntax } = require('./setup-prompt')
|
|
37
|
+
const resolvedSyntax = await resolveStyleValueSyntax(explicitChoice)
|
|
33
38
|
|
|
34
|
-
//
|
|
39
|
+
// generate markdown
|
|
40
|
+
const markdown = generateConfigMarkdown(config, { styleValueSyntax: resolvedSyntax })
|
|
41
|
+
|
|
42
|
+
// write to file
|
|
35
43
|
const outputPath = output || join(process.cwd(), 'tamagui-prompt.md')
|
|
36
44
|
await FS.writeFile(outputPath, markdown, 'utf-8')
|
|
37
45
|
|
|
38
46
|
console.info(`\n ✓ Generated prompt file at ${outputPath}\n`)
|
|
39
47
|
}
|
|
40
|
-
|
|
41
|
-
function generateMarkdown(config: any): string {
|
|
42
|
-
const sections: string[] = []
|
|
43
|
-
|
|
44
|
-
// Header
|
|
45
|
-
sections.push('# Tamagui Configuration\n\n')
|
|
46
|
-
sections.push(
|
|
47
|
-
'This document provides an overview of the Tamagui configuration for this project.\n\n'
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
// Get shorthands for use throughout the document
|
|
51
|
-
const shorthands = config.tamaguiConfig?.shorthands || {}
|
|
52
|
-
const reverseShorthands: Record<string, string> = {}
|
|
53
|
-
for (const [short, full] of Object.entries(shorthands)) {
|
|
54
|
-
reverseShorthands[full as string] = short
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Helper function to get the correct property name based on settings
|
|
58
|
-
const getPropName = (fullProp: string): string => {
|
|
59
|
-
const settings = config.tamaguiConfig?.settings || {}
|
|
60
|
-
if (settings.onlyAllowShorthands && reverseShorthands[fullProp]) {
|
|
61
|
-
return reverseShorthands[fullProp]
|
|
62
|
-
}
|
|
63
|
-
return fullProp
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Settings (moved to top)
|
|
67
|
-
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
|
-
)
|
|
73
|
-
|
|
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
|
-
}
|
|
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
|
-
}
|
|
134
|
-
|
|
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]/)
|
|
150
|
-
)
|
|
151
|
-
|
|
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))
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
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')
|
|
165
|
-
|
|
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()
|
|
171
|
-
|
|
172
|
-
// Output components
|
|
173
|
-
for (const name of allBaseComponents) {
|
|
174
|
-
componentsSection.push(`- ${name}\n`)
|
|
175
|
-
|
|
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
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
componentsSection.push('\n')
|
|
187
|
-
|
|
188
|
-
// Shorthands
|
|
189
|
-
sections.push('## Shorthand Properties\n\n')
|
|
190
|
-
sections.push('These shorthand properties are available for styling:\n\n')
|
|
191
|
-
|
|
192
|
-
const shorthandEntries = Object.entries(shorthands).sort(([a], [b]) =>
|
|
193
|
-
a.localeCompare(b)
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
sections.push(
|
|
197
|
-
shorthandEntries.map(([short, full]) => `- \`${short}\` → \`${full}\``).join('\n')
|
|
198
|
-
)
|
|
199
|
-
sections.push('\n\n')
|
|
200
|
-
|
|
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
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
sections.push('Themes are organized hierarchically and can be combined:\n\n')
|
|
268
|
-
|
|
269
|
-
if (hierarchy.level1.size > 0) {
|
|
270
|
-
sections.push('**Level 1 (Base):**\n\n')
|
|
271
|
-
sections.push(
|
|
272
|
-
Array.from(hierarchy.level1)
|
|
273
|
-
.sort()
|
|
274
|
-
.map((name) => `- ${name}`)
|
|
275
|
-
.join('\n')
|
|
276
|
-
)
|
|
277
|
-
sections.push('\n\n')
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (hierarchy.level2.size > 0) {
|
|
281
|
-
sections.push('**Level 2 (Color Schemes):**\n\n')
|
|
282
|
-
sections.push(
|
|
283
|
-
Array.from(hierarchy.level2)
|
|
284
|
-
.sort()
|
|
285
|
-
.map((name) => `- ${name}`)
|
|
286
|
-
.join('\n')
|
|
287
|
-
)
|
|
288
|
-
sections.push('\n\n')
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (hierarchy.level3.size > 0) {
|
|
292
|
-
sections.push('**Level 3 (Variants):**\n\n')
|
|
293
|
-
sections.push(
|
|
294
|
-
Array.from(hierarchy.level3)
|
|
295
|
-
.sort()
|
|
296
|
-
.map((name) => `- ${name}`)
|
|
297
|
-
.join('\n')
|
|
298
|
-
)
|
|
299
|
-
sections.push('\n\n')
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (hierarchy.components.size > 0) {
|
|
303
|
-
sections.push('**Component Themes:**\n\n')
|
|
304
|
-
sections.push(
|
|
305
|
-
Array.from(hierarchy.components)
|
|
306
|
-
.sort()
|
|
307
|
-
.map((name) => `- ${name}`)
|
|
308
|
-
.join('\n')
|
|
309
|
-
)
|
|
310
|
-
sections.push('\n\n')
|
|
311
|
-
}
|
|
312
|
-
|
|
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
|
-
sections.push('**Special props:**\n\n')
|
|
350
|
-
sections.push('- `theme="inverse"`: Uses the opposite light or dark sub-theme\n')
|
|
351
|
-
sections.push('- `reset`: Reverts to grandparent theme\n\n')
|
|
352
|
-
|
|
353
|
-
// Tokens
|
|
354
|
-
sections.push('## Tokens\n\n')
|
|
355
|
-
sections.push('Tokens are design system values referenced by their bare names.\n\n')
|
|
356
|
-
|
|
357
|
-
const tokens = config.tamaguiConfig?.tokens || {}
|
|
358
|
-
|
|
359
|
-
// Space tokens
|
|
360
|
-
if (tokens.space) {
|
|
361
|
-
sections.push('### Space Tokens\n\n')
|
|
362
|
-
const spaceTokens = Object.entries(tokens.space).sort(([a], [b]) => {
|
|
363
|
-
// Sort numerically where possible
|
|
364
|
-
const numA = parseFloat(a)
|
|
365
|
-
const numB = parseFloat(b)
|
|
366
|
-
if (!isNaN(numA) && !isNaN(numB)) {
|
|
367
|
-
return numA - numB
|
|
368
|
-
}
|
|
369
|
-
return a.localeCompare(b)
|
|
370
|
-
})
|
|
371
|
-
sections.push(
|
|
372
|
-
spaceTokens
|
|
373
|
-
.map(([key, value]) => `- \`${key}\`: ${formatTokenValue(value)}`)
|
|
374
|
-
.join('\n')
|
|
375
|
-
)
|
|
376
|
-
sections.push('\n\n')
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Size tokens
|
|
380
|
-
if (tokens.size) {
|
|
381
|
-
sections.push('### Size Tokens\n\n')
|
|
382
|
-
const sizeTokens = Object.entries(tokens.size).sort(([a], [b]) => {
|
|
383
|
-
const numA = parseFloat(a)
|
|
384
|
-
const numB = parseFloat(b)
|
|
385
|
-
if (!isNaN(numA) && !isNaN(numB)) {
|
|
386
|
-
return numA - numB
|
|
387
|
-
}
|
|
388
|
-
return a.localeCompare(b)
|
|
389
|
-
})
|
|
390
|
-
sections.push(
|
|
391
|
-
sizeTokens
|
|
392
|
-
.map(([key, value]) => `- \`${key}\`: ${formatTokenValue(value)}`)
|
|
393
|
-
.join('\n')
|
|
394
|
-
)
|
|
395
|
-
sections.push('\n\n')
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Radius tokens
|
|
399
|
-
if (tokens.radius) {
|
|
400
|
-
sections.push('### Radius Tokens\n\n')
|
|
401
|
-
const radiusTokens = Object.entries(tokens.radius).sort(([a], [b]) => {
|
|
402
|
-
const numA = parseFloat(a)
|
|
403
|
-
const numB = parseFloat(b)
|
|
404
|
-
if (!isNaN(numA) && !isNaN(numB)) {
|
|
405
|
-
return numA - numB
|
|
406
|
-
}
|
|
407
|
-
return a.localeCompare(b)
|
|
408
|
-
})
|
|
409
|
-
sections.push(
|
|
410
|
-
radiusTokens
|
|
411
|
-
.map(([key, value]) => `- \`${key}\`: ${formatTokenValue(value)}`)
|
|
412
|
-
.join('\n')
|
|
413
|
-
)
|
|
414
|
-
sections.push('\n\n')
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// zIndex tokens
|
|
418
|
-
if (tokens.zIndex) {
|
|
419
|
-
sections.push('### Z-Index Tokens\n\n')
|
|
420
|
-
const zIndexTokens = Object.entries(tokens.zIndex).sort(([a], [b]) => {
|
|
421
|
-
const numA = parseFloat(a)
|
|
422
|
-
const numB = parseFloat(b)
|
|
423
|
-
if (!isNaN(numA) && !isNaN(numB)) {
|
|
424
|
-
return numA - numB
|
|
425
|
-
}
|
|
426
|
-
return a.localeCompare(b)
|
|
427
|
-
})
|
|
428
|
-
sections.push(
|
|
429
|
-
zIndexTokens
|
|
430
|
-
.map(([key, value]) => `- \`${key}\`: ${formatTokenValue(value)}`)
|
|
431
|
-
.join('\n')
|
|
432
|
-
)
|
|
433
|
-
sections.push('\n\n')
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Color tokens
|
|
437
|
-
if (tokens.color) {
|
|
438
|
-
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')
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Token usage examples
|
|
451
|
-
sections.push('### Token Usage\n\n')
|
|
452
|
-
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
|
-
|
|
468
|
-
// Media queries
|
|
469
|
-
if (config.tamaguiConfig?.media) {
|
|
470
|
-
sections.push('## Media Queries\n\n')
|
|
471
|
-
sections.push('Available responsive breakpoints:\n\n')
|
|
472
|
-
|
|
473
|
-
const media = config.tamaguiConfig.media
|
|
474
|
-
const mediaEntries = Object.entries(media).sort(([a], [b]) => a.localeCompare(b))
|
|
475
|
-
|
|
476
|
-
for (const [name, query] of mediaEntries) {
|
|
477
|
-
sections.push(`- **${name}**: ${JSON.stringify(query)}\n`)
|
|
478
|
-
}
|
|
479
|
-
sections.push('\n')
|
|
480
|
-
|
|
481
|
-
sections.push('### Media Query Usage\n\n')
|
|
482
|
-
sections.push(
|
|
483
|
-
'Media queries can be used as style props or with the `useMedia` hook:\n\n'
|
|
484
|
-
)
|
|
485
|
-
sections.push('```tsx\n')
|
|
486
|
-
sections.push('// As a clause in the same style value\n')
|
|
487
|
-
|
|
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
|
-
}
|
|
493
|
-
|
|
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`)
|
|
498
|
-
sections.push(' // Render for this breakpoint\n')
|
|
499
|
-
sections.push('}\n')
|
|
500
|
-
}
|
|
501
|
-
sections.push('```\n\n')
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Fonts
|
|
505
|
-
if (config.tamaguiConfig?.fonts) {
|
|
506
|
-
sections.push('## Fonts\n\n')
|
|
507
|
-
sections.push('Available font families:\n\n')
|
|
508
|
-
|
|
509
|
-
const fonts = config.tamaguiConfig.fonts
|
|
510
|
-
const fontNames = Object.keys(fonts).sort()
|
|
511
|
-
sections.push(fontNames.map((name) => `- ${name}`).join('\n'))
|
|
512
|
-
sections.push('\n\n')
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// Animations
|
|
516
|
-
if (config.tamaguiConfig?.animations) {
|
|
517
|
-
sections.push('## Animations\n\n')
|
|
518
|
-
sections.push('Available animation presets:\n\n')
|
|
519
|
-
|
|
520
|
-
const animations = config.tamaguiConfig.animations
|
|
521
|
-
if (animations.animations) {
|
|
522
|
-
const animationNames = Object.keys(animations.animations).sort()
|
|
523
|
-
sections.push(animationNames.map((name) => `- ${name}`).join('\n'))
|
|
524
|
-
sections.push('\n\n')
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Add components section at the end
|
|
529
|
-
sections.push(...componentsSection)
|
|
530
|
-
|
|
531
|
-
return sections.join('')
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function formatTokenValue(value: any): string {
|
|
535
|
-
// If it's an object with a 'val' property (token object), extract the value
|
|
536
|
-
if (typeof value === 'object' && value !== null && 'val' in value) {
|
|
537
|
-
return String(value.val)
|
|
538
|
-
}
|
|
539
|
-
// Otherwise, stringify it
|
|
540
|
-
return String(value)
|
|
541
|
-
}
|