@mr.dj2u/cli 0.1.7 → 0.1.9
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/cli.d.ts.map +1 -1
- package/dist/cli.js +126 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev-tools.d.ts.map +1 -1
- package/dist/commands/dev-tools.js +22 -4
- package/dist/commands/dev-tools.js.map +1 -1
- package/dist/commands/eject.d.ts +12 -0
- package/dist/commands/eject.d.ts.map +1 -0
- package/dist/commands/eject.js +328 -0
- package/dist/commands/eject.js.map +1 -0
- package/dist/commands/onboard.d.ts +3 -0
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +15 -3
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/stylist.d.ts +25 -0
- package/dist/commands/stylist.d.ts.map +1 -0
- package/dist/commands/stylist.js +392 -0
- package/dist/commands/stylist.js.map +1 -0
- package/dist/project-memory.d.ts +1 -0
- package/dist/project-memory.d.ts.map +1 -1
- package/dist/project-memory.js +3026 -390
- package/dist/project-memory.js.map +1 -1
- package/dist/stylist-theme.d.ts +104 -0
- package/dist/stylist-theme.d.ts.map +1 -0
- package/dist/stylist-theme.js +1374 -0
- package/dist/stylist-theme.js.map +1 -0
- package/package.json +66 -65
- package/templates/embedded-fonts.template.ts +72 -0
- package/templates/expo-sdk-56-screen-universal.template.tsx +709 -0
- package/templates/project/guidelines.md +4 -3
- package/templates/stylist-screen.template.tsx +3446 -0
package/dist/project-memory.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { access, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { DEFAULT_STYLIST_THEME, renderGlobalCssThemeBlock, renderThemeTokensFile, } from './stylist-theme.js';
|
|
4
5
|
const SOFTWARE_MANSION_CORE_DEPENDENCIES = {
|
|
5
6
|
'react-native-gesture-handler': '~2.30.0',
|
|
6
|
-
'react-native-reanimated': '4.
|
|
7
|
-
'react-native-screens': '~4.
|
|
8
|
-
'react-native-svg': '15.15.
|
|
9
|
-
'react-native-keyboard-controller': '1.
|
|
10
|
-
'react-native-worklets': '0.
|
|
7
|
+
'react-native-reanimated': '4.3.1',
|
|
8
|
+
'react-native-screens': '~4.25.2',
|
|
9
|
+
'react-native-svg': '15.15.4',
|
|
10
|
+
'react-native-keyboard-controller': '1.21.6',
|
|
11
|
+
'react-native-worklets': '0.8.3',
|
|
11
12
|
};
|
|
12
13
|
const LOCAL_DATA_DEPENDENCIES = {
|
|
13
|
-
'expo-sqlite': '~
|
|
14
|
+
'expo-sqlite': '~56.0.4',
|
|
14
15
|
};
|
|
15
16
|
const SUPABASE_DEPENDENCIES = {
|
|
16
17
|
'@react-native-async-storage/async-storage': '2.2.0',
|
|
@@ -19,13 +20,25 @@ const SUPABASE_DEPENDENCIES = {
|
|
|
19
20
|
const UNIWIND_DEPENDENCIES = {
|
|
20
21
|
uniwind: '^1.6.4',
|
|
21
22
|
};
|
|
22
|
-
const
|
|
23
|
+
const STYLIST_DEPENDENCIES = {
|
|
24
|
+
'@react-native-async-storage/async-storage': '2.2.0',
|
|
25
|
+
'reanimated-color-picker': '^4.2.0',
|
|
26
|
+
};
|
|
27
|
+
const EXPO_UI_DEPENDENCIES = {
|
|
28
|
+
'@expo/ui': '~56.0.14',
|
|
29
|
+
};
|
|
30
|
+
const ANDROID_NAVIGATION_BAR_DEPENDENCIES = {
|
|
31
|
+
'expo-navigation-bar': '~56.0.3',
|
|
32
|
+
};
|
|
23
33
|
const UNIWIND_DEV_DEPENDENCIES = {
|
|
24
34
|
tailwindcss: '^4.2.4',
|
|
25
35
|
};
|
|
26
36
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
37
|
const MDS_NPX_COMMAND = 'npx -y -p @mr.dj2u/cli@latest mds';
|
|
28
38
|
const DEFAULT_GUIDELINES_TEMPLATE_PATH = path.join(PACKAGE_ROOT, 'templates', 'project', 'guidelines.md');
|
|
39
|
+
const STYLIST_SCREEN_TEMPLATE_PATH = path.join(PACKAGE_ROOT, 'templates', 'stylist-screen.template.tsx');
|
|
40
|
+
const EMBEDDED_FONTS_TEMPLATE_PATH = path.join(PACKAGE_ROOT, 'templates', 'embedded-fonts.template.ts');
|
|
41
|
+
const EXPO_SDK_56_SCREEN_UNIVERSAL_TEMPLATE_PATH = path.join(PACKAGE_ROOT, 'templates', 'expo-sdk-56-screen-universal.template.tsx');
|
|
29
42
|
const INFO_HEADINGS = [
|
|
30
43
|
'Overview',
|
|
31
44
|
'Target Users',
|
|
@@ -85,14 +98,54 @@ export async function scaffoldProjectMemory(projectPath, answers, options = {})
|
|
|
85
98
|
}
|
|
86
99
|
export async function scaffoldRichBoilerplate(projectPath, answers, force, options = { manageUniwind: true }) {
|
|
87
100
|
const results = [];
|
|
88
|
-
|
|
89
|
-
await
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
101
|
+
const needsNativeWindMetroPatch = !options.manageUniwind;
|
|
102
|
+
const navigationShell = await detectNavigationShell(projectPath);
|
|
103
|
+
const includeNativeWindUiExposition = answers.defaults.includes('nativewindui');
|
|
104
|
+
const stylistScreenTemplate = (await loadTemplateWithFallback(STYLIST_SCREEN_TEMPLATE_PATH, renderStylistScreen(answers)))
|
|
105
|
+
.split('__MDS_APP_NAME__')
|
|
106
|
+
.join(answers.appName);
|
|
107
|
+
const embeddedFontsTemplate = await loadTemplateWithFallback(EMBEDDED_FONTS_TEMPLATE_PATH, renderEmbeddedFonts());
|
|
108
|
+
const expoSdk56ScreenTemplate = answers.usesExpoUiUniversalComponents
|
|
109
|
+
? await loadTemplateWithFallback(EXPO_SDK_56_SCREEN_UNIVERSAL_TEMPLATE_PATH, renderExpoSdk56Screen(answers))
|
|
110
|
+
: renderExpoSdk56Screen(answers);
|
|
111
|
+
await mkdir(path.join(projectPath, 'src', 'features', 'home'), {
|
|
112
|
+
recursive: true,
|
|
113
|
+
});
|
|
114
|
+
await mkdir(path.join(projectPath, 'src', 'features', 'onboarding'), {
|
|
115
|
+
recursive: true,
|
|
116
|
+
});
|
|
117
|
+
await mkdir(path.join(projectPath, 'src', 'features', 'onboarding', 'components'), {
|
|
118
|
+
recursive: true,
|
|
119
|
+
});
|
|
120
|
+
await mkdir(path.join(projectPath, 'src', 'features', 'settings'), {
|
|
121
|
+
recursive: true,
|
|
122
|
+
});
|
|
123
|
+
await mkdir(path.join(projectPath, 'src', 'features', 'exposition'), {
|
|
124
|
+
recursive: true,
|
|
125
|
+
});
|
|
126
|
+
await mkdir(path.join(projectPath, 'src', 'components', 'exposition'), {
|
|
127
|
+
recursive: true,
|
|
128
|
+
});
|
|
129
|
+
if (includeNativeWindUiExposition) {
|
|
130
|
+
await mkdir(path.join(projectPath, 'src', 'components', 'nativewindui'), {
|
|
131
|
+
recursive: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
93
134
|
await mkdir(path.join(projectPath, 'src', 'data'), { recursive: true });
|
|
94
135
|
await mkdir(path.join(projectPath, 'src', 'services'), { recursive: true });
|
|
95
|
-
|
|
136
|
+
await mkdir(path.join(projectPath, 'src', 'theme'), { recursive: true });
|
|
137
|
+
await mkdir(path.join(projectPath, 'scripts'), { recursive: true });
|
|
138
|
+
results.push(await writeIfAllowed(path.join(projectPath, 'project', 'theme.json'), `${JSON.stringify(DEFAULT_STYLIST_THEME, null, 2)}\n`, force), await writeIfAllowed(path.join(projectPath, 'scripts', 'stylist-sync-android.mjs'), renderStylistSyncAndroidScript(), force), ...(needsNativeWindMetroPatch
|
|
139
|
+
? [
|
|
140
|
+
await writeIfAllowed(path.join(projectPath, 'scripts', 'patch-nativewind-metro.cjs'), renderNativeWindMetroPatchScript(), force),
|
|
141
|
+
]
|
|
142
|
+
: []), await writeIfAllowed(path.join(projectPath, 'src', 'theme', 'tokens.ts'), renderThemeTokensFile(DEFAULT_STYLIST_THEME), force), await writeIfAllowed(path.join(projectPath, 'src', 'theme', 'font-assets.ts'), renderThemeFontAssetsFile(), force), await writeIfAllowed(path.join(projectPath, 'src', 'theme', 'provider.tsx'), renderThemeProvider(), force), await writeIfAllowed(path.join(projectPath, 'src', 'data', 'mock-app.ts'), renderMockData(answers), force), await writeIfAllowed(path.join(projectPath, 'src', 'services', 'local-data.ts'), renderLocalDataService(answers), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'animated-pressable.tsx'), renderAnimatedPressable(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'gesture-card.tsx'), renderGestureCard(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'keyboard-form.tsx'), renderKeyboardForm(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'svg-mark.tsx'), renderSvgMark(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'software-mansion-logo.tsx'), renderSoftwareMansionLogo(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'screens-card.tsx'), renderScreensCard(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'notice.tsx'), renderExpositionNotice(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'package-card.tsx'), renderPackageCard(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'exposition', 'index.ts'), renderExpositionComponentIndex(), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'home', 'home-screen.tsx'), renderHomeScreen(answers, navigationShell), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'onboarding', 'onboarding-screen.tsx'), renderOnboardingScreen(), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'onboarding', 'agreement-screen.tsx'), renderAgreementScreen(), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'onboarding', 'terms-screen.tsx'), renderTermsScreen(), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'onboarding', 'account-setup-screen.tsx'), renderAccountSetupScreen(), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'onboarding', 'legal-documents.ts'), renderLegalDocuments(), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'onboarding', 'components', 'legal-document-view.tsx'), renderLegalDocumentView(), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'settings', 'settings-screen.tsx'), renderSettingsScreen(), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'exposition', 'exposition-screen.tsx'), renderExpositionScreen(includeNativeWindUiExposition), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'exposition', 'embedded-fonts.ts'), embeddedFontsTemplate, force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'exposition', 'stylist-screen.tsx'), stylistScreenTemplate, force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'exposition', 'data-screen.tsx'), renderDataScreen(answers), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'exposition', 'expo-sdk-56-screen.tsx'), expoSdk56ScreenTemplate, force));
|
|
143
|
+
if (includeNativeWindUiExposition) {
|
|
144
|
+
results.push(await writeIfAllowed(path.join(projectPath, 'src', 'features', 'exposition', 'nativewindui-screen.tsx'), renderNativeWindUiScreen(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'nativewindui', 'ActivityIndicator.tsx'), renderNativeWindUiActivityIndicator(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'nativewindui', 'Avatar.tsx'), renderNativeWindUiAvatar(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'nativewindui', 'Button.tsx'), renderNativeWindUiButton(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'nativewindui', 'DatePicker.tsx'), renderNativeWindUiDatePicker(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'nativewindui', 'Picker.tsx'), renderNativeWindUiPicker(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'nativewindui', 'ProgressIndicator.tsx'), renderNativeWindUiProgressIndicator(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'nativewindui', 'Slider.tsx'), renderNativeWindUiSlider(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'nativewindui', 'Text.tsx'), renderNativeWindUiText(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'nativewindui', 'ThemeToggle.tsx'), renderNativeWindUiThemeToggle(), force), await writeIfAllowed(path.join(projectPath, 'src', 'components', 'nativewindui', 'Toggle.tsx'), renderNativeWindUiToggle(), force));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
await removeOptionalFile(path.join(projectPath, 'src', 'features', 'exposition', 'nativewindui-screen.tsx'));
|
|
148
|
+
}
|
|
96
149
|
if (answers.dataStart === 'local') {
|
|
97
150
|
results.push(await writeIfAllowed(path.join(projectPath, 'src', 'services', 'local-data.native.ts'), renderNativeLocalDataService(), force));
|
|
98
151
|
}
|
|
@@ -100,11 +153,11 @@ export async function scaffoldRichBoilerplate(projectPath, answers, force, optio
|
|
|
100
153
|
const expositionRouteDir = path.join(appDir, 'exposition');
|
|
101
154
|
await mkdir(expositionRouteDir, { recursive: true });
|
|
102
155
|
if (await pathExists(appDir)) {
|
|
103
|
-
const routeForce =
|
|
156
|
+
const routeForce = true;
|
|
104
157
|
const shouldWriteRootLayout = routeForce && (await canWriteRichRootLayout(path.join(appDir, '_layout.tsx')));
|
|
105
|
-
results.push(
|
|
158
|
+
results.push(...(await scaffoldNavigationRoutes(projectPath, appDir, navigationShell, answers, routeForce)));
|
|
106
159
|
if (shouldWriteRootLayout) {
|
|
107
|
-
results.push(await writeIfAllowed(path.join(appDir, '_layout.tsx'), renderRichRootLayout(projectPath, appDir), routeForce));
|
|
160
|
+
results.push(await writeIfAllowed(path.join(appDir, '_layout.tsx'), renderRichRootLayout(projectPath, appDir, navigationShell, answers), routeForce));
|
|
108
161
|
}
|
|
109
162
|
if (!answers.includeCreateExpoComponents) {
|
|
110
163
|
await removeOptionalFile(path.join(appDir, 'details.tsx'));
|
|
@@ -114,7 +167,9 @@ export async function scaffoldRichBoilerplate(projectPath, answers, force, optio
|
|
|
114
167
|
results.push(await writeIfAllowed(path.join(projectPath, 'src', 'services', 'supabase.ts'), renderSupabaseClient(), force));
|
|
115
168
|
}
|
|
116
169
|
if (answers.testToMainSafeguards) {
|
|
117
|
-
await mkdir(path.join(projectPath, '.github', 'workflows'), {
|
|
170
|
+
await mkdir(path.join(projectPath, '.github', 'workflows'), {
|
|
171
|
+
recursive: true,
|
|
172
|
+
});
|
|
118
173
|
results.push(await writeIfAllowed(path.join(projectPath, '.github', 'workflows', 'mds-pr-checks.yml'), renderGitHubPrChecksWorkflow(), force), await writeIfAllowed(path.join(projectPath, 'project', 'release-flow.md'), renderReleaseFlow(answers), force));
|
|
119
174
|
}
|
|
120
175
|
if (options.manageUniwind) {
|
|
@@ -127,6 +182,7 @@ export async function scaffoldRichBoilerplate(projectPath, answers, force, optio
|
|
|
127
182
|
await removeNativeWindArtifacts(projectPath);
|
|
128
183
|
}
|
|
129
184
|
await ensureGlobalCssImport(projectPath, answers.appDirectory);
|
|
185
|
+
results.push(...(await ensureExpoRouterGroupLayouts(appDir, navigationShell, answers)));
|
|
130
186
|
return results;
|
|
131
187
|
}
|
|
132
188
|
export function renderInfo(projectPath, answers, existingInfo) {
|
|
@@ -180,6 +236,7 @@ export function renderInfo(projectPath, answers, existingInfo) {
|
|
|
180
236
|
`- Web output: ${answers.webOutput}`,
|
|
181
237
|
`- Deployed server: ${formatServerChoice(answers.deployedServer)}`,
|
|
182
238
|
`- Expo UI: ${formatBoolean(answers.usesExpoUi)}`,
|
|
239
|
+
`- Expo UI Universal components: ${formatBoolean(answers.usesExpoUiUniversalComponents)}`,
|
|
183
240
|
`- Expo Native Tabs: ${formatBoolean(answers.usesExpoNativeTabs)}`,
|
|
184
241
|
'',
|
|
185
242
|
'## Package Choices',
|
|
@@ -223,7 +280,7 @@ export function renderInfo(projectPath, answers, existingInfo) {
|
|
|
223
280
|
'',
|
|
224
281
|
'> Quick-reference stack summary for agents and collaborators. Fill in or correct any items marked below.',
|
|
225
282
|
'',
|
|
226
|
-
`- **App:** ${answers.appName}
|
|
283
|
+
`- **App:** ${answers.appName} — ${answers.audience}`,
|
|
227
284
|
'- **Language:** TypeScript',
|
|
228
285
|
'- **Package manager:** # TodoForContext(optional): pnpm / npm / yarn / bun',
|
|
229
286
|
`- **Routing:** Expo Router (${formatAppDirectory(answers.appDirectory)})`,
|
|
@@ -244,6 +301,7 @@ export function renderInfo(projectPath, answers, existingInfo) {
|
|
|
244
301
|
`- Latest Expo SDK preference: ${formatBoolean(answers.useLatestExpoSdk)}`,
|
|
245
302
|
`- MDS guidelines template: yes`,
|
|
246
303
|
`- Expo UI: ${formatBoolean(answers.usesExpoUi)}`,
|
|
304
|
+
`- Expo UI Universal components: ${formatBoolean(answers.usesExpoUiUniversalComponents)}`,
|
|
247
305
|
`- Expo Native Tabs: ${formatBoolean(answers.usesExpoNativeTabs)}`,
|
|
248
306
|
`- Test-to-main safeguards: ${formatBoolean(answers.testToMainSafeguards)}`,
|
|
249
307
|
`- Data start: ${formatDataStart(answers.dataStart)}`,
|
|
@@ -261,6 +319,8 @@ export function renderTodo(answers) {
|
|
|
261
319
|
'- [ ] Browse exposition pages to understand included base packages.',
|
|
262
320
|
"- [ ] Review styling in the 'Stylist' page.",
|
|
263
321
|
'- [ ] Review `project/` files for accuracy and planning adjustments.',
|
|
322
|
+
'- [ ] Decide whether to keep or defer `eject-stylist`; mark the decision explicitly.',
|
|
323
|
+
'- [ ] Run `mds eject exposition` and keep only the generated sections you want to retain.',
|
|
264
324
|
'- [ ] Resolve every `# TodoForContext(optional):` marker by filling the section underneath or deleting the marker line to acknowledge no extra context is needed. (There may be none of these if the agent was thorough in onboarding, but if there are any, they should be resolved before development starts.)',
|
|
265
325
|
'',
|
|
266
326
|
'- [x] Confirm app purpose, audience, and primary flows in `project/info.md`.',
|
|
@@ -268,7 +328,9 @@ export function renderTodo(answers) {
|
|
|
268
328
|
'- [ ] Keep or prune included package examples after reviewing `/exposition`.',
|
|
269
329
|
'- [ ] Remove exposition pages before production once their lessons are absorbed.',
|
|
270
330
|
...(needsReview
|
|
271
|
-
? [
|
|
331
|
+
? [
|
|
332
|
+
'- [ ] Replace generic onboarding placeholders with real app decisions before full implementation.',
|
|
333
|
+
]
|
|
272
334
|
: []),
|
|
273
335
|
'',
|
|
274
336
|
'## Phase 1: App Shell And First Flow',
|
|
@@ -278,6 +340,7 @@ export function renderTodo(answers) {
|
|
|
278
340
|
`- [ ] Use ${formatPlatformLayoutMode(answers.platformLayoutMode)} unless project memory is updated.`,
|
|
279
341
|
`- [ ] Implement the first core flow from project info: ${answers.coreFlows}.`,
|
|
280
342
|
'- [ ] Keep route files thin and move real UI into feature screens.',
|
|
343
|
+
'- [ ] Apply Stylist synced theme tokens to production UI components and screens.',
|
|
281
344
|
'',
|
|
282
345
|
'## Phase 2: Data Layer',
|
|
283
346
|
'',
|
|
@@ -300,8 +363,15 @@ export function renderTodo(answers) {
|
|
|
300
363
|
'- [ ] Verify each selected platform after the MVP flow works.',
|
|
301
364
|
...answers.targetPlatforms.map((platform) => `- [ ] Verify ${platform} behavior.`),
|
|
302
365
|
...(answers.usesExpoUi ? ['- [ ] Add Expo UI examples where they improve native feel.'] : []),
|
|
303
|
-
...(answers.
|
|
304
|
-
|
|
366
|
+
...(answers.usesExpoUiUniversalComponents
|
|
367
|
+
? ['- [ ] Review the Expo UI Universal examples before replacing generated exposition code.']
|
|
368
|
+
: []),
|
|
369
|
+
...(answers.usesExpoNativeTabs
|
|
370
|
+
? ['- [ ] Prototype Expo Native Tabs for mobile navigation.']
|
|
371
|
+
: []),
|
|
372
|
+
...(answers.easUses.length > 0
|
|
373
|
+
? answers.easUses.map((item) => `- [ ] Configure EAS for ${item}.`)
|
|
374
|
+
: []),
|
|
305
375
|
'',
|
|
306
376
|
'## Phase 4: Polish, Safeguards, And Release',
|
|
307
377
|
'',
|
|
@@ -313,7 +383,9 @@ export function renderTodo(answers) {
|
|
|
313
383
|
'- [ ] Add GitHub branch protection so PR checks pass before merging into `test` or `main`.',
|
|
314
384
|
]
|
|
315
385
|
: ['- [ ] Decide on release safeguards before production work begins.']),
|
|
316
|
-
...(answers.webOutput !== 'none'
|
|
386
|
+
...(answers.webOutput !== 'none'
|
|
387
|
+
? [`- [ ] Confirm Expo web output mode: ${answers.webOutput}.`]
|
|
388
|
+
: []),
|
|
317
389
|
...(answers.deployedServer !== 'none'
|
|
318
390
|
? [`- [ ] Plan deployed server work: ${formatServerChoice(answers.deployedServer)}.`]
|
|
319
391
|
: []),
|
|
@@ -337,6 +409,9 @@ export function renderStyle(answers, existingStyle) {
|
|
|
337
409
|
'',
|
|
338
410
|
'## Colors',
|
|
339
411
|
'',
|
|
412
|
+
'- Canonical editable tokens live in `project/theme.json`.',
|
|
413
|
+
'- Use `/exposition/stylist` and the Save button to sync style tokens into this file.',
|
|
414
|
+
'',
|
|
340
415
|
'# TodoForContext(optional): Add palette direction, semantic color meaning, and light/dark mode expectations.',
|
|
341
416
|
'',
|
|
342
417
|
'## Typography',
|
|
@@ -360,10 +435,79 @@ export function renderStyle(answers, existingStyle) {
|
|
|
360
435
|
'',
|
|
361
436
|
'# TodoForContext(optional): Add unresolved visual decisions to revisit later in `/exposition/stylist`; delete this marker if there are none.',
|
|
362
437
|
'',
|
|
438
|
+
'<!-- MDS_STYLIST_THEME_START -->',
|
|
439
|
+
'## Canonical Theme Tokens (Managed by Stylist)',
|
|
440
|
+
'',
|
|
441
|
+
'The block below mirrors `project/theme.json` and is managed by `mds stylist sync`.',
|
|
442
|
+
'',
|
|
443
|
+
'```json',
|
|
444
|
+
JSON.stringify(DEFAULT_STYLIST_THEME, null, 2),
|
|
445
|
+
'```',
|
|
446
|
+
'<!-- MDS_STYLIST_THEME_END -->',
|
|
447
|
+
'',
|
|
363
448
|
...importedNotes,
|
|
364
449
|
'',
|
|
365
450
|
].join('\n');
|
|
366
451
|
}
|
|
452
|
+
function renderThemeProvider() {
|
|
453
|
+
return [
|
|
454
|
+
"import { createContext, useContext, useMemo, useState, type Dispatch, type ReactNode, type SetStateAction } from 'react';",
|
|
455
|
+
'',
|
|
456
|
+
"import stylistThemeTokens, { type StylistColorPalette, type StylistColorScheme, type StylistThemeTokens } from './tokens';",
|
|
457
|
+
'',
|
|
458
|
+
'export type AppThemeValue = StylistThemeTokens & {',
|
|
459
|
+
' activeScheme: StylistColorScheme;',
|
|
460
|
+
' activeColors: StylistColorPalette;',
|
|
461
|
+
'};',
|
|
462
|
+
'',
|
|
463
|
+
'const AppThemeContext = createContext<AppThemeValue>({',
|
|
464
|
+
' ...stylistThemeTokens,',
|
|
465
|
+
' activeScheme: stylistThemeTokens.colorSystem.previewScheme,',
|
|
466
|
+
' activeColors: stylistThemeTokens.colors[stylistThemeTokens.colorSystem.previewScheme],',
|
|
467
|
+
'});',
|
|
468
|
+
'const AppThemeSetterContext = createContext<Dispatch<SetStateAction<StylistThemeTokens>> | null>(null);',
|
|
469
|
+
'',
|
|
470
|
+
'export function AppThemeProvider({ children }: { children: ReactNode }) {',
|
|
471
|
+
' const [theme, setTheme] = useState<StylistThemeTokens>(stylistThemeTokens);',
|
|
472
|
+
' const value = useMemo<AppThemeValue>(() => {',
|
|
473
|
+
' const activeScheme = theme.colorSystem.previewScheme;',
|
|
474
|
+
' return {',
|
|
475
|
+
' ...theme,',
|
|
476
|
+
' activeScheme,',
|
|
477
|
+
' activeColors: theme.colors[activeScheme],',
|
|
478
|
+
' };',
|
|
479
|
+
' }, [theme]);',
|
|
480
|
+
'',
|
|
481
|
+
' return (',
|
|
482
|
+
' <AppThemeSetterContext.Provider value={setTheme}>',
|
|
483
|
+
' <AppThemeContext.Provider value={value}>{children}</AppThemeContext.Provider>',
|
|
484
|
+
' </AppThemeSetterContext.Provider>',
|
|
485
|
+
' );',
|
|
486
|
+
'}',
|
|
487
|
+
'',
|
|
488
|
+
'export function useAppTheme() {',
|
|
489
|
+
' return useContext(AppThemeContext);',
|
|
490
|
+
'}',
|
|
491
|
+
'',
|
|
492
|
+
'export function useSetAppTheme() {',
|
|
493
|
+
' const setTheme = useContext(AppThemeSetterContext);',
|
|
494
|
+
' if (!setTheme) {',
|
|
495
|
+
" throw new Error('useSetAppTheme must be used inside AppThemeProvider.');",
|
|
496
|
+
' }',
|
|
497
|
+
' return setTheme;',
|
|
498
|
+
'}',
|
|
499
|
+
'',
|
|
500
|
+
].join('\n');
|
|
501
|
+
}
|
|
502
|
+
function renderThemeFontAssetsFile() {
|
|
503
|
+
return [
|
|
504
|
+
'export const THEME_FONT_ASSETS: Record<string, number> = {',
|
|
505
|
+
'};',
|
|
506
|
+
'',
|
|
507
|
+
'export default THEME_FONT_ASSETS;',
|
|
508
|
+
'',
|
|
509
|
+
].join('\n');
|
|
510
|
+
}
|
|
367
511
|
export function renderGuidelines(answers) {
|
|
368
512
|
return [
|
|
369
513
|
`# ${answers.appName} Guidelines`,
|
|
@@ -406,6 +550,14 @@ export function renderGuidelines(answers) {
|
|
|
406
550
|
'- Never expose Supabase service-role or secret keys in client code.',
|
|
407
551
|
]
|
|
408
552
|
: ['- Keep local dummy data behind an adapter so Supabase can replace it later.']),
|
|
553
|
+
...(answers.usesExpoUi
|
|
554
|
+
? [
|
|
555
|
+
'- Expo UI is stable in SDK 56 for native SwiftUI and Jetpack Compose surfaces.',
|
|
556
|
+
answers.usesExpoUiUniversalComponents
|
|
557
|
+
? '- Prefer Expo UI Universal components when one shared Android, iOS, and web component tree fits.'
|
|
558
|
+
: '- Use platform-specific Expo UI APIs only when they clearly improve native feel.',
|
|
559
|
+
]
|
|
560
|
+
: []),
|
|
409
561
|
'',
|
|
410
562
|
'## Workflow',
|
|
411
563
|
'',
|
|
@@ -413,9 +565,12 @@ export function renderGuidelines(answers) {
|
|
|
413
565
|
'- Run `mds doctor --ci` before pushing.',
|
|
414
566
|
'- Use `mds clear-expo-start` when Metro or server ports get wedged.',
|
|
415
567
|
...(answers.testToMainSafeguards
|
|
416
|
-
? [
|
|
568
|
+
? [
|
|
569
|
+
'- Develop through feature branches into `test`, then promote validated work from `test` to `main`.',
|
|
570
|
+
]
|
|
417
571
|
: []),
|
|
418
572
|
`- Latest Expo SDK preference captured during onboarding: ${formatBoolean(answers.useLatestExpoSdk)}.`,
|
|
573
|
+
`- Expo UI Universal components preference captured during onboarding: ${formatBoolean(answers.usesExpoUiUniversalComponents)}.`,
|
|
419
574
|
'- Treat monorepo scaffolding as future work until the single-app MVP is stable.',
|
|
420
575
|
'',
|
|
421
576
|
].join('\n');
|
|
@@ -452,7 +607,7 @@ export function renderClaudeMd(answers) {
|
|
|
452
607
|
`Run \`npm run clear-expo-start\` (or \`${MDS_NPX_COMMAND} clear-expo-start .\`) instead of bare \`expo start\` or \`npx expo start\`.`,
|
|
453
608
|
'Kills port 8081, clears all Metro and Expo caches (including the Windows system cache), and starts `expo start --clear`.',
|
|
454
609
|
'Expo Router API routes work automatically in this mode.',
|
|
455
|
-
'Never fall back to a non-default port
|
|
610
|
+
'Never fall back to a non-default port — always free the default port first.',
|
|
456
611
|
'',
|
|
457
612
|
];
|
|
458
613
|
const backendAlongside = answers.customBackend
|
|
@@ -460,13 +615,13 @@ export function renderClaudeMd(answers) {
|
|
|
460
615
|
'## Also start the backend API server',
|
|
461
616
|
'',
|
|
462
617
|
`Run \`node ${answers.customBackendEntry}\` from the project root in a background process alongside Expo.`,
|
|
463
|
-
'Both must be running for full local functionality
|
|
618
|
+
'Both must be running for full local functionality — Expo on port 8081, backend on its own port.',
|
|
464
619
|
'',
|
|
465
620
|
]
|
|
466
621
|
: [];
|
|
467
622
|
const spinUpProd = buildSpinUpProdSection(answers);
|
|
468
623
|
return [
|
|
469
|
-
`# ${answers.appName}
|
|
624
|
+
`# ${answers.appName} — Agent Guidelines`,
|
|
470
625
|
'',
|
|
471
626
|
'## Before every git commit',
|
|
472
627
|
'',
|
|
@@ -499,7 +654,7 @@ function buildSpinUpProdSection(answers) {
|
|
|
499
654
|
return [
|
|
500
655
|
'## Spin up prod',
|
|
501
656
|
'',
|
|
502
|
-
'Run `npm run serve:prod:fresh`
|
|
657
|
+
'Run `npm run serve:prod:fresh` — kills port 3000, builds web dist, starts the Node server.',
|
|
503
658
|
'Run `npm run serve:prod` to restart without rebuilding.',
|
|
504
659
|
'Server runs on http://localhost:3000. Mirrors your self-hosted (Plesk/VPS) environment.',
|
|
505
660
|
'',
|
|
@@ -509,7 +664,7 @@ function buildSpinUpProdSection(answers) {
|
|
|
509
664
|
return [
|
|
510
665
|
'## Spin up prod',
|
|
511
666
|
'',
|
|
512
|
-
'Run `npm run serve:prod:fresh`
|
|
667
|
+
'Run `npm run serve:prod:fresh` — builds web dist and starts `npx expo serve`.',
|
|
513
668
|
'The terminal will show the local URL when ready. Mirrors EAS hosting.',
|
|
514
669
|
'',
|
|
515
670
|
];
|
|
@@ -566,6 +721,11 @@ async function ensurePackageJson(projectPath, answers, manageUniwind) {
|
|
|
566
721
|
'mds:continue': packageJson.scripts?.['mds:continue'] ?? `${MDS_NPX_COMMAND} continue`,
|
|
567
722
|
'mds:doctor': packageJson.scripts?.['mds:doctor'] ?? `${MDS_NPX_COMMAND} doctor`,
|
|
568
723
|
'mds:doctor:ci': packageJson.scripts?.['mds:doctor:ci'] ?? `${MDS_NPX_COMMAND} doctor --ci`,
|
|
724
|
+
'mds:stylist:sync': packageJson.scripts?.['mds:stylist:sync'] ?? `${MDS_NPX_COMMAND} stylist sync .`,
|
|
725
|
+
'stylist:sync:android': packageJson.scripts?.['stylist:sync:android'] ?? 'node ./scripts/stylist-sync-android.mjs',
|
|
726
|
+
'mds:eject': packageJson.scripts?.['mds:eject'] ?? `${MDS_NPX_COMMAND} eject .`,
|
|
727
|
+
'mds:eject:exposition': packageJson.scripts?.['mds:eject:exposition'] ?? `${MDS_NPX_COMMAND} eject exposition .`,
|
|
728
|
+
'mds:eject:stylist': packageJson.scripts?.['mds:eject:stylist'] ?? `${MDS_NPX_COMMAND} eject stylist .`,
|
|
569
729
|
'free-port': packageJson.scripts?.['free-port'] ?? `${MDS_NPX_COMMAND} free-port`,
|
|
570
730
|
'clear-expo-start': packageJson.scripts?.['clear-expo-start'] ?? `${MDS_NPX_COMMAND} clear-expo-start`,
|
|
571
731
|
'expo-install-fix': packageJson.scripts?.['expo-install-fix'] ?? 'npx expo install --fix',
|
|
@@ -573,6 +733,17 @@ async function ensurePackageJson(projectPath, answers, manageUniwind) {
|
|
|
573
733
|
'post-create-check': packageJson.scripts?.['post-create-check'] ?? 'npx expo install --fix && npx expo-doctor',
|
|
574
734
|
'ci:verify': packageJson.scripts?.['ci:verify'] ?? `${MDS_NPX_COMMAND} doctor --ci`,
|
|
575
735
|
};
|
|
736
|
+
if (!manageUniwind) {
|
|
737
|
+
packageJson.scripts['patch:nativewind-metro'] =
|
|
738
|
+
packageJson.scripts['patch:nativewind-metro'] ?? 'node ./scripts/patch-nativewind-metro.cjs';
|
|
739
|
+
packageJson.scripts.prestart =
|
|
740
|
+
packageJson.scripts.prestart ?? 'node ./scripts/patch-nativewind-metro.cjs';
|
|
741
|
+
packageJson.scripts.preandroid =
|
|
742
|
+
packageJson.scripts.preandroid ?? 'node ./scripts/patch-nativewind-metro.cjs';
|
|
743
|
+
packageJson.scripts.preweb =
|
|
744
|
+
packageJson.scripts.preweb ?? 'node ./scripts/patch-nativewind-metro.cjs';
|
|
745
|
+
packageJson.scripts.postinstall = ensureNativeWindMetroPostinstall(packageJson.scripts.postinstall);
|
|
746
|
+
}
|
|
576
747
|
if (answers.webOutput !== 'none') {
|
|
577
748
|
const serveProd = deriveServeProdScript(answers);
|
|
578
749
|
const serveProdFresh = deriveServeProdFreshScript(answers);
|
|
@@ -593,6 +764,7 @@ async function ensurePackageJson(projectPath, answers, manageUniwind) {
|
|
|
593
764
|
}
|
|
594
765
|
packageJson.dependencies = {
|
|
595
766
|
...SOFTWARE_MANSION_CORE_DEPENDENCIES,
|
|
767
|
+
...STYLIST_DEPENDENCIES,
|
|
596
768
|
...packageJson.dependencies,
|
|
597
769
|
};
|
|
598
770
|
if (answers.dataStart === 'local') {
|
|
@@ -607,6 +779,18 @@ async function ensurePackageJson(projectPath, answers, manageUniwind) {
|
|
|
607
779
|
...packageJson.dependencies,
|
|
608
780
|
};
|
|
609
781
|
}
|
|
782
|
+
if (answers.usesExpoUi) {
|
|
783
|
+
packageJson.dependencies = {
|
|
784
|
+
...EXPO_UI_DEPENDENCIES,
|
|
785
|
+
...packageJson.dependencies,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
if (answers.targetPlatforms.includes('android')) {
|
|
789
|
+
packageJson.dependencies = {
|
|
790
|
+
...ANDROID_NAVIGATION_BAR_DEPENDENCIES,
|
|
791
|
+
...packageJson.dependencies,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
610
794
|
if (manageUniwind) {
|
|
611
795
|
packageJson.dependencies = {
|
|
612
796
|
...UNIWIND_DEPENDENCIES,
|
|
@@ -648,6 +832,7 @@ function applyGuidelinesTemplate(template, answers) {
|
|
|
648
832
|
webOutput: answers.webOutput,
|
|
649
833
|
deployedServer: formatServerChoice(answers.deployedServer),
|
|
650
834
|
usesExpoUi: formatBoolean(answers.usesExpoUi),
|
|
835
|
+
usesExpoUiUniversalComponents: formatBoolean(answers.usesExpoUiUniversalComponents),
|
|
651
836
|
usesExpoNativeTabs: formatBoolean(answers.usesExpoNativeTabs),
|
|
652
837
|
easUses: answers.easUses.map((item) => `- ${item}`).join('\n') || '- not planned yet',
|
|
653
838
|
dataStart: formatDataStart(answers.dataStart),
|
|
@@ -689,11 +874,16 @@ function formatServerAdapterSummary(answers) {
|
|
|
689
874
|
if (answers.webOutput === 'none')
|
|
690
875
|
return 'none (native-only)';
|
|
691
876
|
switch (answers.expoServerAdapter) {
|
|
692
|
-
case 'eas':
|
|
693
|
-
|
|
694
|
-
case '
|
|
695
|
-
|
|
696
|
-
|
|
877
|
+
case 'eas':
|
|
878
|
+
return 'EAS hosting';
|
|
879
|
+
case 'express':
|
|
880
|
+
return 'Express adapter (node server.js, port 3000)';
|
|
881
|
+
case 'bun':
|
|
882
|
+
return 'Bun adapter (node server.js)';
|
|
883
|
+
case 'other':
|
|
884
|
+
return 'custom (not yet specified)';
|
|
885
|
+
default:
|
|
886
|
+
return formatServerChoice(answers.deployedServer);
|
|
697
887
|
}
|
|
698
888
|
}
|
|
699
889
|
function deriveServeProdScript(answers) {
|
|
@@ -712,6 +902,21 @@ function formatStyleStack(answers) {
|
|
|
712
902
|
if (answers.defaults.includes('uniwind')) {
|
|
713
903
|
return 'Uniwind / Tailwind CSS v4';
|
|
714
904
|
}
|
|
905
|
+
if (answers.defaults.includes('nativewindui')) {
|
|
906
|
+
return 'NativeWindUI / NativeWind';
|
|
907
|
+
}
|
|
908
|
+
if (answers.defaults.includes('nativewind')) {
|
|
909
|
+
return 'NativeWind / Tailwind CSS';
|
|
910
|
+
}
|
|
911
|
+
if (answers.defaults.includes('unistyles')) {
|
|
912
|
+
return 'Unistyles';
|
|
913
|
+
}
|
|
914
|
+
if (answers.defaults.includes('restyle')) {
|
|
915
|
+
return 'Shopify Restyle';
|
|
916
|
+
}
|
|
917
|
+
if (answers.defaults.includes('tamagui')) {
|
|
918
|
+
return 'Tamagui';
|
|
919
|
+
}
|
|
715
920
|
return 'standard React Native StyleSheet';
|
|
716
921
|
}
|
|
717
922
|
function formatAuthSummary(answers) {
|
|
@@ -778,7 +983,9 @@ async function ensureUniwindMetroConfig(projectPath) {
|
|
|
778
983
|
async function ensureUniwindGlobalCss(projectPath) {
|
|
779
984
|
const globalCssPath = path.join(projectPath, 'global.css');
|
|
780
985
|
const existing = await readOptionalText(globalCssPath);
|
|
781
|
-
if (!existing ||
|
|
986
|
+
if (!existing ||
|
|
987
|
+
existing.includes("@import 'uniwind'") ||
|
|
988
|
+
existing.includes('@import "uniwind"')) {
|
|
782
989
|
return;
|
|
783
990
|
}
|
|
784
991
|
await writeFile(globalCssPath, renderGlobalCss(), 'utf8');
|
|
@@ -838,11 +1045,494 @@ async function ensureGlobalCssImport(projectPath, appDirectory) {
|
|
|
838
1045
|
}
|
|
839
1046
|
}
|
|
840
1047
|
function getExpoRouterAppDir(projectPath, appDirectory) {
|
|
841
|
-
return appDirectory === 'src'
|
|
1048
|
+
return appDirectory === 'src'
|
|
1049
|
+
? path.join(projectPath, 'src', 'app')
|
|
1050
|
+
: path.join(projectPath, 'app');
|
|
842
1051
|
}
|
|
843
1052
|
function renderRouteExport(routeDir, targetModulePath) {
|
|
844
1053
|
return `export { default } from '${toRelativeImportPath(routeDir, targetModulePath)}';\n`;
|
|
845
1054
|
}
|
|
1055
|
+
async function scaffoldNavigationRoutes(projectPath, appDir, navigationShell, answers, routeForce) {
|
|
1056
|
+
const results = [];
|
|
1057
|
+
const homeScreen = path.join(projectPath, 'src', 'features', 'home', 'home-screen');
|
|
1058
|
+
const onboardingScreen = path.join(projectPath, 'src', 'features', 'onboarding', 'onboarding-screen');
|
|
1059
|
+
const agreementScreen = path.join(projectPath, 'src', 'features', 'onboarding', 'agreement-screen');
|
|
1060
|
+
const termsScreen = path.join(projectPath, 'src', 'features', 'onboarding', 'terms-screen');
|
|
1061
|
+
const accountSetupScreen = path.join(projectPath, 'src', 'features', 'onboarding', 'account-setup-screen');
|
|
1062
|
+
const settingsScreen = path.join(projectPath, 'src', 'features', 'settings', 'settings-screen');
|
|
1063
|
+
const expositionScreen = path.join(projectPath, 'src', 'features', 'exposition', 'exposition-screen');
|
|
1064
|
+
const stylistScreen = path.join(projectPath, 'src', 'features', 'exposition', 'stylist-screen');
|
|
1065
|
+
const dataScreen = path.join(projectPath, 'src', 'features', 'exposition', 'data-screen');
|
|
1066
|
+
const expoSdk56Screen = path.join(projectPath, 'src', 'features', 'exposition', 'expo-sdk-56-screen');
|
|
1067
|
+
const nativeWindUiScreen = path.join(projectPath, 'src', 'features', 'exposition', 'nativewindui-screen');
|
|
1068
|
+
const includeNativeWindUiExposition = answers.defaults.includes('nativewindui');
|
|
1069
|
+
const shouldWriteExpositionRouteWrappers = navigationShell.library !== 'expo-router' || navigationShell.layout === 'stack';
|
|
1070
|
+
const rootExpositionDir = path.join(appDir, 'exposition');
|
|
1071
|
+
const onboardingDir = path.join(appDir, 'onboarding');
|
|
1072
|
+
await mkdir(rootExpositionDir, { recursive: true });
|
|
1073
|
+
await mkdir(onboardingDir, { recursive: true });
|
|
1074
|
+
results.push(await writeIfAllowed(path.join(appDir, 'onboarding.tsx'), renderRouteExport(appDir, onboardingScreen), routeForce), await writeIfAllowed(path.join(onboardingDir, 'agreement.tsx'), renderRouteExport(onboardingDir, agreementScreen), routeForce), await writeIfAllowed(path.join(onboardingDir, 'terms.tsx'), renderRouteExport(onboardingDir, termsScreen), routeForce), await writeIfAllowed(path.join(onboardingDir, 'account-setup.tsx'), renderRouteExport(onboardingDir, accountSetupScreen), routeForce), await writeIfAllowed(path.join(appDir, 'settings.tsx'), renderRouteExport(appDir, settingsScreen), routeForce), await writeIfAllowed(path.join(rootExpositionDir, 'stylist-sync+api.ts'), renderStylistSyncApiRoute(), routeForce));
|
|
1075
|
+
if (shouldWriteExpositionRouteWrappers) {
|
|
1076
|
+
results.push(await writeIfAllowed(path.join(rootExpositionDir, 'index.tsx'), renderRouteExport(rootExpositionDir, expositionScreen), routeForce), await writeIfAllowed(path.join(rootExpositionDir, 'stylist.tsx'), renderRouteExport(rootExpositionDir, stylistScreen), routeForce), await writeIfAllowed(path.join(rootExpositionDir, 'data.tsx'), renderRouteExport(rootExpositionDir, dataScreen), routeForce), await writeIfAllowed(path.join(rootExpositionDir, 'sdk-56.tsx'), renderRouteExport(rootExpositionDir, expoSdk56Screen), routeForce));
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
await removeOptionalFile(path.join(rootExpositionDir, 'index.tsx'));
|
|
1080
|
+
await removeOptionalFile(path.join(rootExpositionDir, 'stylist.tsx'));
|
|
1081
|
+
await removeOptionalFile(path.join(rootExpositionDir, 'data.tsx'));
|
|
1082
|
+
await removeOptionalFile(path.join(rootExpositionDir, 'sdk-56.tsx'));
|
|
1083
|
+
}
|
|
1084
|
+
if (includeNativeWindUiExposition) {
|
|
1085
|
+
results.push(await writeIfAllowed(path.join(rootExpositionDir, 'nativewindui.tsx'), renderRouteExport(rootExpositionDir, nativeWindUiScreen), routeForce));
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
await removeOptionalFile(path.join(rootExpositionDir, 'nativewindui.tsx'));
|
|
1089
|
+
}
|
|
1090
|
+
if (navigationShell.library !== 'expo-router') {
|
|
1091
|
+
results.push(await writeIfAllowed(path.join(appDir, 'index.tsx'), renderRouteExport(appDir, homeScreen), routeForce));
|
|
1092
|
+
return results;
|
|
1093
|
+
}
|
|
1094
|
+
if (navigationShell.layout === 'stack') {
|
|
1095
|
+
results.push(await writeIfAllowed(path.join(appDir, 'index.tsx'), renderRouteExport(appDir, homeScreen), routeForce));
|
|
1096
|
+
return results;
|
|
1097
|
+
}
|
|
1098
|
+
if (navigationShell.layout === 'tabs') {
|
|
1099
|
+
const tabsDir = path.join(appDir, '(tabs)');
|
|
1100
|
+
await mkdir(tabsDir, { recursive: true });
|
|
1101
|
+
results.push(await writeIfAllowed(path.join(tabsDir, 'index.tsx'), renderRouteExport(tabsDir, homeScreen), routeForce), await writeIfAllowed(path.join(tabsDir, 'exposition.tsx'), renderRouteExport(tabsDir, expositionScreen), routeForce), await writeIfAllowed(path.join(tabsDir, 'stylist.tsx'), renderRouteExport(tabsDir, stylistScreen), routeForce), await writeIfAllowed(path.join(tabsDir, 'data.tsx'), renderRouteExport(tabsDir, dataScreen), routeForce), await writeIfAllowed(path.join(tabsDir, 'sdk-56.tsx'), renderRouteExport(tabsDir, expoSdk56Screen), routeForce));
|
|
1102
|
+
await removeOptionalFile(path.join(tabsDir, 'two.tsx'));
|
|
1103
|
+
await removeOptionalFile(path.join(tabsDir, 'software-mansion.tsx'));
|
|
1104
|
+
await removeOptionalFile(path.join(tabsDir, 'nativewindui.tsx'));
|
|
1105
|
+
await removeOptionalFile(path.join(appDir, 'index.tsx'));
|
|
1106
|
+
return results;
|
|
1107
|
+
}
|
|
1108
|
+
const drawerDir = path.join(appDir, '(drawer)');
|
|
1109
|
+
const drawerTabsDir = path.join(drawerDir, '(tabs)');
|
|
1110
|
+
await mkdir(drawerTabsDir, { recursive: true });
|
|
1111
|
+
results.push(await writeIfAllowed(path.join(drawerDir, 'index.tsx'), renderRouteExport(drawerDir, homeScreen), routeForce), await writeIfAllowed(path.join(drawerTabsDir, 'index.tsx'), renderRouteExport(drawerTabsDir, expositionScreen), routeForce), await writeIfAllowed(path.join(drawerTabsDir, 'stylist.tsx'), renderRouteExport(drawerTabsDir, stylistScreen), routeForce), await writeIfAllowed(path.join(drawerTabsDir, 'data.tsx'), renderRouteExport(drawerTabsDir, dataScreen), routeForce), await writeIfAllowed(path.join(drawerTabsDir, 'sdk-56.tsx'), renderRouteExport(drawerTabsDir, expoSdk56Screen), routeForce));
|
|
1112
|
+
await removeOptionalFile(path.join(drawerTabsDir, 'two.tsx'));
|
|
1113
|
+
await removeOptionalFile(path.join(drawerTabsDir, 'nativewindui.tsx'));
|
|
1114
|
+
await removeOptionalFile(path.join(appDir, 'index.tsx'));
|
|
1115
|
+
return results;
|
|
1116
|
+
}
|
|
1117
|
+
async function detectNavigationShell(projectPath) {
|
|
1118
|
+
const cesRaw = await readOptionalText(path.join(projectPath, 'cesconfig.jsonc'));
|
|
1119
|
+
const fromCes = detectNavigationFromCesConfig(cesRaw);
|
|
1120
|
+
if (fromCes) {
|
|
1121
|
+
return fromCes;
|
|
1122
|
+
}
|
|
1123
|
+
const appLayout = await readOptionalText(path.join(projectPath, 'app', '_layout.tsx'));
|
|
1124
|
+
const srcLayout = await readOptionalText(path.join(projectPath, 'src', 'app', '_layout.tsx'));
|
|
1125
|
+
const layoutText = appLayout ?? srcLayout ?? '';
|
|
1126
|
+
if (layoutText.includes('Drawer')) {
|
|
1127
|
+
return { library: 'expo-router', layout: 'drawer + tabs' };
|
|
1128
|
+
}
|
|
1129
|
+
if (layoutText.includes('Tabs')) {
|
|
1130
|
+
return { library: 'expo-router', layout: 'tabs' };
|
|
1131
|
+
}
|
|
1132
|
+
const appTsx = await readOptionalText(path.join(projectPath, 'App.tsx'));
|
|
1133
|
+
if (appTsx?.includes('react-navigation')) {
|
|
1134
|
+
return { library: 'react-navigation', layout: 'stack' };
|
|
1135
|
+
}
|
|
1136
|
+
return { library: 'expo-router', layout: 'stack' };
|
|
1137
|
+
}
|
|
1138
|
+
function detectNavigationFromCesConfig(raw) {
|
|
1139
|
+
if (!raw) {
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
const sanitized = raw.replace(/^\s*\/\/.*$/gmu, '');
|
|
1144
|
+
const parsed = JSON.parse(sanitized);
|
|
1145
|
+
if (!Array.isArray(parsed.packages)) {
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
const nav = parsed.packages.find((pkg) => pkg?.type === 'navigation');
|
|
1149
|
+
if (!nav?.name) {
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
const layoutRaw = nav.options?.type;
|
|
1153
|
+
const layout = layoutRaw === 'tabs' || layoutRaw === 'drawer + tabs' || layoutRaw === 'stack'
|
|
1154
|
+
? layoutRaw
|
|
1155
|
+
: 'stack';
|
|
1156
|
+
if (nav.name === 'react-navigation') {
|
|
1157
|
+
return { library: 'react-navigation', layout };
|
|
1158
|
+
}
|
|
1159
|
+
if (nav.name === 'expo-router') {
|
|
1160
|
+
return { library: 'expo-router', layout };
|
|
1161
|
+
}
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
catch {
|
|
1165
|
+
return null;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
async function loadTemplateWithFallback(templatePath, fallback) {
|
|
1169
|
+
const template = await readOptionalText(templatePath);
|
|
1170
|
+
return template ?? fallback;
|
|
1171
|
+
}
|
|
1172
|
+
function ensureNativeWindMetroPostinstall(existing) {
|
|
1173
|
+
const command = 'node ./scripts/patch-nativewind-metro.cjs';
|
|
1174
|
+
const trimmed = existing?.trim();
|
|
1175
|
+
if (!trimmed) {
|
|
1176
|
+
return command;
|
|
1177
|
+
}
|
|
1178
|
+
if (trimmed.includes(command)) {
|
|
1179
|
+
return trimmed;
|
|
1180
|
+
}
|
|
1181
|
+
if (trimmed === 'patch-package') {
|
|
1182
|
+
return command;
|
|
1183
|
+
}
|
|
1184
|
+
return `${trimmed} && ${command}`;
|
|
1185
|
+
}
|
|
1186
|
+
function renderStylistSyncAndroidScript() {
|
|
1187
|
+
return [
|
|
1188
|
+
'#!/usr/bin/env node',
|
|
1189
|
+
"import { existsSync } from 'node:fs';",
|
|
1190
|
+
"import { readFile } from 'node:fs/promises';",
|
|
1191
|
+
"import { createRequire } from 'node:module';",
|
|
1192
|
+
"import path from 'node:path';",
|
|
1193
|
+
"import { fileURLToPath } from 'node:url';",
|
|
1194
|
+
'',
|
|
1195
|
+
"const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');",
|
|
1196
|
+
'const moduleCandidates = [',
|
|
1197
|
+
" path.resolve(projectRoot, '..', '..', 'packages', 'cli', 'dist', 'stylist-theme.js'),",
|
|
1198
|
+
" path.resolve(projectRoot, '..', 'packages', 'cli', 'dist', 'stylist-theme.js'),",
|
|
1199
|
+
" path.resolve(projectRoot, 'packages', 'cli', 'dist', 'stylist-theme.js'),",
|
|
1200
|
+
" path.resolve(projectRoot, 'node_modules', '@mr.dj2u', 'cli', 'dist', 'stylist-theme.js'),",
|
|
1201
|
+
'];',
|
|
1202
|
+
'',
|
|
1203
|
+
'const modulePath = moduleCandidates.find((candidate) => existsSync(candidate));',
|
|
1204
|
+
'if (!modulePath) {',
|
|
1205
|
+
" console.error('Could not find @mr.dj2u/cli stylist sync module. Run npm install, then retry.');",
|
|
1206
|
+
' process.exit(1);',
|
|
1207
|
+
'}',
|
|
1208
|
+
'const require = createRequire(import.meta.url);',
|
|
1209
|
+
'',
|
|
1210
|
+
'try {',
|
|
1211
|
+
' const inputFile = process.env.MDS_STYLIST_INPUT_FILE',
|
|
1212
|
+
' ? path.resolve(projectRoot, process.env.MDS_STYLIST_INPUT_FILE)',
|
|
1213
|
+
" : path.join(projectRoot, 'project', 'theme.json');",
|
|
1214
|
+
" const styleLibrary = process.env.MDS_STYLIST_STYLE_LIBRARY || 'auto';",
|
|
1215
|
+
' const writePolicy =',
|
|
1216
|
+
" process.env.MDS_STYLIST_WRITE_POLICY === 'overwrite' ? 'overwrite' : 'managed';",
|
|
1217
|
+
" const theme = JSON.parse(await readFile(inputFile, 'utf8'));",
|
|
1218
|
+
' const loaded = require(modulePath);',
|
|
1219
|
+
' const result = await loaded.syncStylistTheme(projectRoot, theme, {',
|
|
1220
|
+
' styleLibrary,',
|
|
1221
|
+
' writePolicy,',
|
|
1222
|
+
' });',
|
|
1223
|
+
' console.log(JSON.stringify(result, null, 2));',
|
|
1224
|
+
'} catch (error) {',
|
|
1225
|
+
' console.error(error instanceof Error ? error.message : String(error));',
|
|
1226
|
+
' process.exit(1);',
|
|
1227
|
+
'}',
|
|
1228
|
+
'',
|
|
1229
|
+
].join('\n');
|
|
1230
|
+
}
|
|
1231
|
+
function renderNativeWindMetroPatchScript() {
|
|
1232
|
+
return [
|
|
1233
|
+
"const fs = require('node:fs');",
|
|
1234
|
+
"const path = require('node:path');",
|
|
1235
|
+
'',
|
|
1236
|
+
'const targetPath = path.join(',
|
|
1237
|
+
' __dirname,',
|
|
1238
|
+
" '..',",
|
|
1239
|
+
" 'node_modules',",
|
|
1240
|
+
" 'react-native-css-interop',",
|
|
1241
|
+
" 'dist',",
|
|
1242
|
+
" 'metro',",
|
|
1243
|
+
" 'index.js'",
|
|
1244
|
+
');',
|
|
1245
|
+
'',
|
|
1246
|
+
'const legacy = ` haste.emit("change", {',
|
|
1247
|
+
' eventsQueue: [',
|
|
1248
|
+
' {',
|
|
1249
|
+
' filePath,',
|
|
1250
|
+
' metadata: {',
|
|
1251
|
+
' modifiedTime: Date.now(),',
|
|
1252
|
+
' size: 1,',
|
|
1253
|
+
' type: "virtual",',
|
|
1254
|
+
' },',
|
|
1255
|
+
' type: "change",',
|
|
1256
|
+
' },',
|
|
1257
|
+
' ],',
|
|
1258
|
+
' });`;',
|
|
1259
|
+
'',
|
|
1260
|
+
'const patched = ` haste.emit("change", {',
|
|
1261
|
+
' changes: {',
|
|
1262
|
+
' addedFiles: new Map(),',
|
|
1263
|
+
' modifiedFiles: new Map([',
|
|
1264
|
+
' [',
|
|
1265
|
+
' filePath,',
|
|
1266
|
+
' {',
|
|
1267
|
+
' modifiedTime: Date.now(),',
|
|
1268
|
+
' isSymlink: false,',
|
|
1269
|
+
' },',
|
|
1270
|
+
' ],',
|
|
1271
|
+
' ]),',
|
|
1272
|
+
' removedFiles: new Map(),',
|
|
1273
|
+
' },',
|
|
1274
|
+
' rootDir: "",',
|
|
1275
|
+
' });`;',
|
|
1276
|
+
'',
|
|
1277
|
+
'try {',
|
|
1278
|
+
' if (!fs.existsSync(targetPath)) {',
|
|
1279
|
+
' process.exit(0);',
|
|
1280
|
+
' }',
|
|
1281
|
+
'',
|
|
1282
|
+
" const current = fs.readFileSync(targetPath, 'utf8');",
|
|
1283
|
+
" if (current.includes('addedFiles: new Map()')) {",
|
|
1284
|
+
' process.exit(0);',
|
|
1285
|
+
' }',
|
|
1286
|
+
'',
|
|
1287
|
+
' if (!current.includes(legacy)) {',
|
|
1288
|
+
" console.warn('[MDS] NativeWind Metro patch target did not match; leaving file unchanged.');",
|
|
1289
|
+
' process.exit(0);',
|
|
1290
|
+
' }',
|
|
1291
|
+
'',
|
|
1292
|
+
" fs.writeFileSync(targetPath, current.replace(legacy, patched), 'utf8');",
|
|
1293
|
+
" console.log('[MDS] Patched react-native-css-interop Metro change event for Metro 0.85.');",
|
|
1294
|
+
'} catch (error) {',
|
|
1295
|
+
' console.warn(`[MDS] Could not patch NativeWind Metro integration: ${error.message}`);',
|
|
1296
|
+
'}',
|
|
1297
|
+
'',
|
|
1298
|
+
].join('\n');
|
|
1299
|
+
}
|
|
1300
|
+
function renderStylistSyncApiRoute() {
|
|
1301
|
+
return [
|
|
1302
|
+
"import { spawn } from 'node:child_process';",
|
|
1303
|
+
"import { access, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';",
|
|
1304
|
+
"import path from 'node:path';",
|
|
1305
|
+
'',
|
|
1306
|
+
"import stylistThemeTokens from '../../theme/tokens';",
|
|
1307
|
+
'',
|
|
1308
|
+
'interface SyncResponse {',
|
|
1309
|
+
' projectPath: string;',
|
|
1310
|
+
' updatedFiles: string[];',
|
|
1311
|
+
'}',
|
|
1312
|
+
'',
|
|
1313
|
+
'interface StylistSyncRequestBody {',
|
|
1314
|
+
' theme: unknown;',
|
|
1315
|
+
' metadata?: {',
|
|
1316
|
+
" writePolicy?: 'managed' | 'overwrite';",
|
|
1317
|
+
" styleLibrary?: 'auto' | 'uniwind' | 'nativewind' | 'nativewindui' | 'unistyles' | 'restyle' | 'tamagui' | 'stylesheet';",
|
|
1318
|
+
' };',
|
|
1319
|
+
'}',
|
|
1320
|
+
'',
|
|
1321
|
+
'function parseSyncResponse(stdout: string): SyncResponse {',
|
|
1322
|
+
' const trimmed = stdout.trim();',
|
|
1323
|
+
' if (!trimmed) {',
|
|
1324
|
+
" throw new Error('Stylist sync returned empty output.');",
|
|
1325
|
+
' }',
|
|
1326
|
+
' try {',
|
|
1327
|
+
' return JSON.parse(trimmed) as SyncResponse;',
|
|
1328
|
+
' } catch {',
|
|
1329
|
+
' const match = trimmed.match(/\\{[\\s\\S]*\\}$/);',
|
|
1330
|
+
' if (!match) {',
|
|
1331
|
+
" throw new Error('Stylist sync returned non-JSON output.');",
|
|
1332
|
+
' }',
|
|
1333
|
+
' return JSON.parse(match[0]) as SyncResponse;',
|
|
1334
|
+
' }',
|
|
1335
|
+
'}',
|
|
1336
|
+
'',
|
|
1337
|
+
'export async function POST(request: Request) {',
|
|
1338
|
+
' try {',
|
|
1339
|
+
' const payload = (await request.json()) as unknown;',
|
|
1340
|
+
' const normalized = normalizeSyncPayload(payload);',
|
|
1341
|
+
' const result = await runStylistSync(JSON.stringify(normalized.theme), normalized.metadata);',
|
|
1342
|
+
' return Response.json(result);',
|
|
1343
|
+
' } catch (error) {',
|
|
1344
|
+
' return Response.json(',
|
|
1345
|
+
" { error: error instanceof Error ? error.message : 'Unknown stylist sync error' },",
|
|
1346
|
+
' { status: 400 }',
|
|
1347
|
+
' );',
|
|
1348
|
+
' }',
|
|
1349
|
+
'}',
|
|
1350
|
+
'',
|
|
1351
|
+
'export async function GET() {',
|
|
1352
|
+
" const configPath = path.resolve(process.cwd(), 'project', 'stylist.config.json');",
|
|
1353
|
+
" const themePath = path.resolve(process.cwd(), 'project', 'theme.json');",
|
|
1354
|
+
" const stylePath = path.resolve(process.cwd(), 'project', 'style.md');",
|
|
1355
|
+
'',
|
|
1356
|
+
' const themeFromJson = await readThemeJson(themePath);',
|
|
1357
|
+
' const themeFromStyle = await readThemeFromStyleMarkdown(stylePath);',
|
|
1358
|
+
' const resolvedTheme = themeFromStyle ?? themeFromJson ?? stylistThemeTokens;',
|
|
1359
|
+
" const themeSource = themeFromStyle ? 'style.md' : themeFromJson ? 'theme.json' : 'default';",
|
|
1360
|
+
' const mismatchDetected =',
|
|
1361
|
+
' Boolean(themeFromJson) &&',
|
|
1362
|
+
' Boolean(themeFromStyle) &&',
|
|
1363
|
+
' JSON.stringify(themeFromJson) !== JSON.stringify(themeFromStyle);',
|
|
1364
|
+
' try {',
|
|
1365
|
+
" const raw = await readFile(configPath, 'utf8');",
|
|
1366
|
+
' const parsed = JSON.parse(raw) as { writePolicy?: string; styleLibrary?: string };',
|
|
1367
|
+
' return Response.json({',
|
|
1368
|
+
' hasConfig: true,',
|
|
1369
|
+
' writePolicy: parsed.writePolicy ?? null,',
|
|
1370
|
+
' styleLibrary: parsed.styleLibrary ?? null,',
|
|
1371
|
+
' theme: resolvedTheme,',
|
|
1372
|
+
' themeSource,',
|
|
1373
|
+
' mismatchDetected,',
|
|
1374
|
+
' });',
|
|
1375
|
+
' } catch {',
|
|
1376
|
+
' return Response.json({',
|
|
1377
|
+
' hasConfig: false,',
|
|
1378
|
+
' writePolicy: null,',
|
|
1379
|
+
' styleLibrary: null,',
|
|
1380
|
+
' theme: resolvedTheme,',
|
|
1381
|
+
' themeSource,',
|
|
1382
|
+
' mismatchDetected,',
|
|
1383
|
+
' });',
|
|
1384
|
+
' }',
|
|
1385
|
+
'}',
|
|
1386
|
+
'',
|
|
1387
|
+
'function normalizeSyncPayload(value: unknown): StylistSyncRequestBody {',
|
|
1388
|
+
" if (!value || typeof value !== 'object') {",
|
|
1389
|
+
" throw new Error('Invalid stylist payload.');",
|
|
1390
|
+
' }',
|
|
1391
|
+
'',
|
|
1392
|
+
' const asRecord = value as Record<string, unknown>;',
|
|
1393
|
+
" if ('theme' in asRecord && asRecord.theme) {",
|
|
1394
|
+
' return {',
|
|
1395
|
+
' theme: asRecord.theme,',
|
|
1396
|
+
' metadata:',
|
|
1397
|
+
" asRecord.metadata && typeof asRecord.metadata === 'object'",
|
|
1398
|
+
" ? (asRecord.metadata as StylistSyncRequestBody['metadata'])",
|
|
1399
|
+
' : undefined,',
|
|
1400
|
+
' };',
|
|
1401
|
+
' }',
|
|
1402
|
+
'',
|
|
1403
|
+
" if ('metadata' in asRecord) {",
|
|
1404
|
+
" throw new Error('Invalid stylist payload: missing theme.');",
|
|
1405
|
+
' }',
|
|
1406
|
+
'',
|
|
1407
|
+
' return { theme: value };',
|
|
1408
|
+
'}',
|
|
1409
|
+
'',
|
|
1410
|
+
'async function runStylistSync(',
|
|
1411
|
+
' inputJson: string,',
|
|
1412
|
+
" metadata?: StylistSyncRequestBody['metadata']",
|
|
1413
|
+
'): Promise<SyncResponse> {',
|
|
1414
|
+
" const tempDir = path.resolve(process.cwd(), '.expo', 'stylist-sync');",
|
|
1415
|
+
' await mkdir(tempDir, { recursive: true });',
|
|
1416
|
+
' const tempInputPath = path.join(',
|
|
1417
|
+
' tempDir,',
|
|
1418
|
+
' `theme-${Date.now()}-${Math.random().toString(36).slice(2)}.json`',
|
|
1419
|
+
' );',
|
|
1420
|
+
" await writeFile(tempInputPath, inputJson, 'utf8');",
|
|
1421
|
+
'',
|
|
1422
|
+
' const fileExists = async (filePath: string): Promise<boolean> => {',
|
|
1423
|
+
' try {',
|
|
1424
|
+
' await access(filePath);',
|
|
1425
|
+
' return true;',
|
|
1426
|
+
' } catch {',
|
|
1427
|
+
' return false;',
|
|
1428
|
+
' }',
|
|
1429
|
+
' };',
|
|
1430
|
+
'',
|
|
1431
|
+
' const runAttempt = async (',
|
|
1432
|
+
' command: string,',
|
|
1433
|
+
' args: string[],',
|
|
1434
|
+
' env: NodeJS.ProcessEnv',
|
|
1435
|
+
' ): Promise<SyncResponse> => {',
|
|
1436
|
+
' return await new Promise<SyncResponse>((resolve, reject) => {',
|
|
1437
|
+
' const child = spawn(command, args, {',
|
|
1438
|
+
' cwd: process.cwd(),',
|
|
1439
|
+
" stdio: ['ignore', 'pipe', 'pipe'],",
|
|
1440
|
+
' windowsHide: true,',
|
|
1441
|
+
' env,',
|
|
1442
|
+
' });',
|
|
1443
|
+
'',
|
|
1444
|
+
" let stdout = '';",
|
|
1445
|
+
" let stderr = '';",
|
|
1446
|
+
" child.stdout.on('data', (chunk) => {",
|
|
1447
|
+
' stdout += String(chunk);',
|
|
1448
|
+
' });',
|
|
1449
|
+
" child.stderr.on('data', (chunk) => {",
|
|
1450
|
+
' stderr += String(chunk);',
|
|
1451
|
+
' });',
|
|
1452
|
+
'',
|
|
1453
|
+
" child.on('error', (error) => {",
|
|
1454
|
+
' reject(error);',
|
|
1455
|
+
' });',
|
|
1456
|
+
'',
|
|
1457
|
+
' const timeout = setTimeout(() => {',
|
|
1458
|
+
' child.kill();',
|
|
1459
|
+
" reject(new Error('Stylist sync timed out after 120 seconds.'));",
|
|
1460
|
+
' }, 120000);',
|
|
1461
|
+
'',
|
|
1462
|
+
" child.on('close', (code) => {",
|
|
1463
|
+
' clearTimeout(timeout);',
|
|
1464
|
+
' if (code !== 0) {',
|
|
1465
|
+
" reject(new Error(stderr.trim() || `Stylist sync failed with exit code ${code ?? 'unknown'}.`));",
|
|
1466
|
+
' return;',
|
|
1467
|
+
' }',
|
|
1468
|
+
'',
|
|
1469
|
+
' try {',
|
|
1470
|
+
' resolve(parseSyncResponse(stdout));',
|
|
1471
|
+
' } catch (error) {',
|
|
1472
|
+
' reject(',
|
|
1473
|
+
' new Error(',
|
|
1474
|
+
" `Failed to parse stylist sync output: ${error instanceof Error ? error.message : String(error)}${stderr.trim() ? ` | stderr: ${stderr.trim()}` : ''}`",
|
|
1475
|
+
' )',
|
|
1476
|
+
' );',
|
|
1477
|
+
' }',
|
|
1478
|
+
' });',
|
|
1479
|
+
' });',
|
|
1480
|
+
' };',
|
|
1481
|
+
'',
|
|
1482
|
+
" const scriptPath = path.resolve(process.cwd(), 'scripts', 'stylist-sync-android.mjs');",
|
|
1483
|
+
' const env = {',
|
|
1484
|
+
' ...process.env,',
|
|
1485
|
+
' MDS_STYLIST_INPUT_FILE: path.relative(process.cwd(), tempInputPath),',
|
|
1486
|
+
" MDS_STYLIST_WRITE_POLICY: metadata?.writePolicy ?? 'managed',",
|
|
1487
|
+
" MDS_STYLIST_STYLE_LIBRARY: metadata?.styleLibrary ?? 'auto',",
|
|
1488
|
+
' };',
|
|
1489
|
+
'',
|
|
1490
|
+
' try {',
|
|
1491
|
+
' if (!(await fileExists(scriptPath))) {',
|
|
1492
|
+
" throw new Error('Stylist sync helper is missing. Run npm install, then retry.');",
|
|
1493
|
+
' }',
|
|
1494
|
+
' return await runAttempt(process.execPath, [scriptPath], env);',
|
|
1495
|
+
' } finally {',
|
|
1496
|
+
' try {',
|
|
1497
|
+
' await unlink(tempInputPath);',
|
|
1498
|
+
' } catch {',
|
|
1499
|
+
' // no-op',
|
|
1500
|
+
' }',
|
|
1501
|
+
' }',
|
|
1502
|
+
'}',
|
|
1503
|
+
'',
|
|
1504
|
+
'async function readThemeJson(filePath: string): Promise<unknown | null> {',
|
|
1505
|
+
' try {',
|
|
1506
|
+
" const raw = await readFile(filePath, 'utf8');",
|
|
1507
|
+
' return JSON.parse(raw) as unknown;',
|
|
1508
|
+
' } catch {',
|
|
1509
|
+
' return null;',
|
|
1510
|
+
' }',
|
|
1511
|
+
'}',
|
|
1512
|
+
'',
|
|
1513
|
+
'async function readThemeFromStyleMarkdown(filePath: string): Promise<unknown | null> {',
|
|
1514
|
+
' try {',
|
|
1515
|
+
" const raw = await readFile(filePath, 'utf8');",
|
|
1516
|
+
" const startToken = '<!-- MDS_STYLIST_THEME_START -->';",
|
|
1517
|
+
" const endToken = '<!-- MDS_STYLIST_THEME_END -->';",
|
|
1518
|
+
' const startIndex = raw.indexOf(startToken);',
|
|
1519
|
+
' const endIndex = raw.indexOf(endToken);',
|
|
1520
|
+
' if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {',
|
|
1521
|
+
' return null;',
|
|
1522
|
+
' }',
|
|
1523
|
+
' const block = raw.slice(startIndex, endIndex + endToken.length);',
|
|
1524
|
+
' const match = block.match(/```json\\s*([\\s\\S]*?)\\s*```/i);',
|
|
1525
|
+
' if (!match?.[1]) {',
|
|
1526
|
+
' return null;',
|
|
1527
|
+
' }',
|
|
1528
|
+
' return JSON.parse(match[1]) as unknown;',
|
|
1529
|
+
' } catch {',
|
|
1530
|
+
' return null;',
|
|
1531
|
+
' }',
|
|
1532
|
+
'}',
|
|
1533
|
+
'',
|
|
1534
|
+
].join('\n');
|
|
1535
|
+
}
|
|
846
1536
|
function renderGlobalCssImport(layoutPath, projectPath) {
|
|
847
1537
|
return `import '${toRelativeImportPath(path.dirname(layoutPath), path.join(projectPath, 'global.css'))}';`;
|
|
848
1538
|
}
|
|
@@ -951,7 +1641,13 @@ async function readOptionalText(filePath) {
|
|
|
951
1641
|
}
|
|
952
1642
|
}
|
|
953
1643
|
function renderGlobalCss() {
|
|
954
|
-
return [
|
|
1644
|
+
return [
|
|
1645
|
+
"@import 'tailwindcss';",
|
|
1646
|
+
"@import 'uniwind';",
|
|
1647
|
+
'',
|
|
1648
|
+
renderGlobalCssThemeBlock(DEFAULT_STYLIST_THEME),
|
|
1649
|
+
'',
|
|
1650
|
+
].join('\n');
|
|
955
1651
|
}
|
|
956
1652
|
function renderUniwindMetroConfig() {
|
|
957
1653
|
return [
|
|
@@ -1059,73 +1755,223 @@ function renderNativeLocalDataService() {
|
|
|
1059
1755
|
"import type { AppTask } from '../data/mock-app';",
|
|
1060
1756
|
'',
|
|
1061
1757
|
"const dbPromise = SQLite.openDatabaseAsync('exposition.db');",
|
|
1758
|
+
'let sqliteUnavailable = false;',
|
|
1759
|
+
'let memoryTasks: AppTask[] = [...appSnapshot.tasks];',
|
|
1062
1760
|
'',
|
|
1063
1761
|
'async function getDb() {',
|
|
1064
|
-
'
|
|
1762
|
+
' if (sqliteUnavailable) {',
|
|
1763
|
+
' return null;',
|
|
1764
|
+
' }',
|
|
1765
|
+
'',
|
|
1766
|
+
' try {',
|
|
1767
|
+
' return await dbPromise;',
|
|
1768
|
+
' } catch {',
|
|
1769
|
+
' sqliteUnavailable = true;',
|
|
1770
|
+
' return null;',
|
|
1771
|
+
' }',
|
|
1065
1772
|
'}',
|
|
1066
1773
|
'',
|
|
1067
1774
|
'export async function ensureLocalDataReady(): Promise<void> {',
|
|
1068
1775
|
' const db = await getDb();',
|
|
1069
|
-
'
|
|
1070
|
-
' CREATE TABLE IF NOT EXISTS exposition_tasks (',
|
|
1071
|
-
' id TEXT PRIMARY KEY NOT NULL,',
|
|
1072
|
-
' title TEXT NOT NULL,',
|
|
1073
|
-
' status TEXT NOT NULL',
|
|
1074
|
-
' );',
|
|
1075
|
-
' `);',
|
|
1076
|
-
" const row = await db.getFirstAsync<{ count: number }>('SELECT COUNT(*) as count FROM exposition_tasks');",
|
|
1077
|
-
' if ((row?.count ?? 0) > 0) {',
|
|
1776
|
+
' if (!db) {',
|
|
1078
1777
|
' return;',
|
|
1079
1778
|
' }',
|
|
1080
1779
|
'',
|
|
1081
|
-
'
|
|
1082
|
-
' await db.
|
|
1083
|
-
|
|
1084
|
-
'
|
|
1085
|
-
'
|
|
1086
|
-
'
|
|
1087
|
-
'
|
|
1780
|
+
' try {',
|
|
1781
|
+
' await db.execAsync(`',
|
|
1782
|
+
' CREATE TABLE IF NOT EXISTS exposition_tasks (',
|
|
1783
|
+
' id TEXT PRIMARY KEY NOT NULL,',
|
|
1784
|
+
' title TEXT NOT NULL,',
|
|
1785
|
+
' status TEXT NOT NULL',
|
|
1786
|
+
' );',
|
|
1787
|
+
' `);',
|
|
1788
|
+
" const row = await db.getFirstAsync<{ count: number }>('SELECT COUNT(*) as count FROM exposition_tasks');",
|
|
1789
|
+
' if ((row?.count ?? 0) > 0) {',
|
|
1790
|
+
' return;',
|
|
1791
|
+
' }',
|
|
1792
|
+
'',
|
|
1793
|
+
' for (const task of appSnapshot.tasks) {',
|
|
1794
|
+
' await db.runAsync(',
|
|
1795
|
+
" 'INSERT INTO exposition_tasks (id, title, status) VALUES (?, ?, ?)',",
|
|
1796
|
+
' task.id,',
|
|
1797
|
+
' task.title,',
|
|
1798
|
+
' task.status',
|
|
1799
|
+
' );',
|
|
1800
|
+
' }',
|
|
1801
|
+
' } catch {',
|
|
1802
|
+
' sqliteUnavailable = true;',
|
|
1088
1803
|
' }',
|
|
1089
1804
|
'}',
|
|
1090
1805
|
'',
|
|
1091
1806
|
'export async function getLocalAppSnapshot(): Promise<typeof appSnapshot> {',
|
|
1092
1807
|
' await ensureLocalDataReady();',
|
|
1093
1808
|
' const db = await getDb();',
|
|
1094
|
-
|
|
1095
|
-
'
|
|
1096
|
-
'
|
|
1097
|
-
'
|
|
1098
|
-
'
|
|
1809
|
+
' if (!db) {',
|
|
1810
|
+
' return { ...appSnapshot, tasks: memoryTasks };',
|
|
1811
|
+
' }',
|
|
1812
|
+
'',
|
|
1813
|
+
' try {',
|
|
1814
|
+
" const tasks = await db.getAllAsync<AppTask>('SELECT id, title, status FROM exposition_tasks ORDER BY id');",
|
|
1815
|
+
' return {',
|
|
1816
|
+
' ...appSnapshot,',
|
|
1817
|
+
' tasks,',
|
|
1818
|
+
' };',
|
|
1819
|
+
' } catch {',
|
|
1820
|
+
' sqliteUnavailable = true;',
|
|
1821
|
+
' return { ...appSnapshot, tasks: memoryTasks };',
|
|
1822
|
+
' }',
|
|
1099
1823
|
'}',
|
|
1100
1824
|
'',
|
|
1101
1825
|
"export async function addLocalTask(title = 'Try the local DB adapter'): Promise<typeof appSnapshot> {",
|
|
1102
1826
|
' await ensureLocalDataReady();',
|
|
1103
1827
|
' const db = await getDb();',
|
|
1104
1828
|
' const id = `task-${Date.now()}`;',
|
|
1105
|
-
|
|
1106
|
-
'
|
|
1829
|
+
' if (!db) {',
|
|
1830
|
+
" memoryTasks = [...memoryTasks, { id, title, status: 'todo' }];",
|
|
1831
|
+
' return { ...appSnapshot, tasks: memoryTasks };',
|
|
1832
|
+
' }',
|
|
1833
|
+
'',
|
|
1834
|
+
' try {',
|
|
1835
|
+
" await db.runAsync('INSERT INTO exposition_tasks (id, title, status) VALUES (?, ?, ?)', id, title, 'todo');",
|
|
1836
|
+
' return getLocalAppSnapshot();',
|
|
1837
|
+
' } catch {',
|
|
1838
|
+
' sqliteUnavailable = true;',
|
|
1839
|
+
" memoryTasks = [...memoryTasks, { id, title, status: 'todo' }];",
|
|
1840
|
+
' return { ...appSnapshot, tasks: memoryTasks };',
|
|
1841
|
+
' }',
|
|
1107
1842
|
'}',
|
|
1108
1843
|
'',
|
|
1109
1844
|
].join('\n');
|
|
1110
1845
|
}
|
|
1111
|
-
function renderRichRootLayout(projectPath, appDir) {
|
|
1846
|
+
function renderRichRootLayout(projectPath, appDir, navigationShell, answers) {
|
|
1847
|
+
const themeProviderImport = toRelativeImportPath(appDir, path.join(projectPath, 'src', 'theme', 'provider'));
|
|
1848
|
+
const themeFontAssetsImport = toRelativeImportPath(appDir, path.join(projectPath, 'src', 'theme', 'font-assets'));
|
|
1849
|
+
const shouldRegisterExpositionRoutes = navigationShell.library !== 'expo-router' || navigationShell.layout === 'stack';
|
|
1850
|
+
const includeNativeWindUiExposition = answers.defaults.includes('nativewindui');
|
|
1851
|
+
const expositionScreens = shouldRegisterExpositionRoutes
|
|
1852
|
+
? [
|
|
1853
|
+
' <Stack.Screen name="exposition/index" options={{ title: \'Package Exposition\' }} />',
|
|
1854
|
+
' <Stack.Screen name="exposition/stylist" options={{ title: \'Stylist\' }} />',
|
|
1855
|
+
' <Stack.Screen name="exposition/data" options={{ title: \'Data\' }} />',
|
|
1856
|
+
' <Stack.Screen name="exposition/sdk-56" options={{ title: \'Expo SDK 56\' }} />',
|
|
1857
|
+
...(includeNativeWindUiExposition
|
|
1858
|
+
? [
|
|
1859
|
+
' <Stack.Screen name="exposition/nativewindui" options={{ title: \'NativeWindUI\' }} />',
|
|
1860
|
+
]
|
|
1861
|
+
: []),
|
|
1862
|
+
]
|
|
1863
|
+
: [];
|
|
1864
|
+
const nativeWindUiScreen = [];
|
|
1865
|
+
const shellScreen = navigationShell.layout === 'tabs'
|
|
1866
|
+
? ' <Stack.Screen name="(tabs)" options={{ headerShown: false }} />'
|
|
1867
|
+
: navigationShell.layout === 'drawer + tabs'
|
|
1868
|
+
? ' <Stack.Screen name="(drawer)" options={{ headerShown: false }} />'
|
|
1869
|
+
: ' <Stack.Screen name="index" options={{ title: \'Home\' }} />';
|
|
1112
1870
|
return [
|
|
1113
1871
|
renderGlobalCssImport(path.join(appDir, '_layout.tsx'), projectPath),
|
|
1114
|
-
"import {
|
|
1872
|
+
"import type { ReactNode } from 'react';",
|
|
1873
|
+
"import { useEffect, useMemo } from 'react';",
|
|
1874
|
+
"import { DarkTheme, DefaultTheme, Link, Stack, ThemeProvider } from 'expo-router';",
|
|
1875
|
+
"import { useFonts } from 'expo-font';",
|
|
1876
|
+
"import { Platform, Pressable, StatusBar, Text, useColorScheme } from 'react-native';",
|
|
1877
|
+
"import { NavigationBar } from 'expo-navigation-bar';",
|
|
1878
|
+
"import * as SystemUI from 'expo-system-ui';",
|
|
1879
|
+
"import { GestureHandlerRootView } from 'react-native-gesture-handler';",
|
|
1880
|
+
"import { KeyboardProvider } from 'react-native-keyboard-controller';",
|
|
1115
1881
|
"import { SafeAreaProvider } from 'react-native-safe-area-context';",
|
|
1882
|
+
`import THEME_FONT_ASSETS from '${themeFontAssetsImport}';`,
|
|
1883
|
+
`import { AppThemeProvider, useAppTheme } from '${themeProviderImport}';`,
|
|
1884
|
+
'',
|
|
1885
|
+
'function RouterThemeBridge({ children }: { children: ReactNode }) {',
|
|
1886
|
+
' const theme = useAppTheme();',
|
|
1887
|
+
' const systemScheme = useColorScheme();',
|
|
1888
|
+
' const prefersDark =',
|
|
1889
|
+
" theme.colorSystem.mode === 'automatic'",
|
|
1890
|
+
" ? systemScheme === 'dark'",
|
|
1891
|
+
" : theme.colorSystem.previewScheme === 'dark';",
|
|
1892
|
+
' const base = prefersDark ? DarkTheme : DefaultTheme;',
|
|
1893
|
+
' const shellColor = theme.activeColors.background;',
|
|
1894
|
+
' const routerTheme = useMemo(',
|
|
1895
|
+
' () => ({',
|
|
1896
|
+
' ...base,',
|
|
1897
|
+
' colors: {',
|
|
1898
|
+
' ...base.colors,',
|
|
1899
|
+
' background: shellColor,',
|
|
1900
|
+
' border: theme.activeColors.surface,',
|
|
1901
|
+
' card: shellColor,',
|
|
1902
|
+
' notification: theme.activeColors.warning,',
|
|
1903
|
+
' primary: theme.activeColors.primary,',
|
|
1904
|
+
' text: theme.activeColors.text,',
|
|
1905
|
+
' },',
|
|
1906
|
+
' }),',
|
|
1907
|
+
' [base, shellColor, theme.activeColors]',
|
|
1908
|
+
' );',
|
|
1116
1909
|
'',
|
|
1117
|
-
'
|
|
1910
|
+
' useEffect(() => {',
|
|
1911
|
+
' void SystemUI.setBackgroundColorAsync?.(shellColor);',
|
|
1912
|
+
' }, [shellColor]);',
|
|
1913
|
+
'',
|
|
1914
|
+
' return <ThemeProvider value={routerTheme}>{children}</ThemeProvider>;',
|
|
1915
|
+
'}',
|
|
1916
|
+
'',
|
|
1917
|
+
'function LayoutInner() {',
|
|
1918
|
+
' const theme = useAppTheme();',
|
|
1919
|
+
' const shellColor = theme.activeColors.background;',
|
|
1118
1920
|
' return (',
|
|
1119
|
-
' <
|
|
1120
|
-
' <
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1921
|
+
' <GestureHandlerRootView style={{ flex: 1, backgroundColor: shellColor }}>',
|
|
1922
|
+
' <KeyboardProvider>',
|
|
1923
|
+
' <SafeAreaProvider>',
|
|
1924
|
+
' <RouterThemeBridge>',
|
|
1925
|
+
' <StatusBar',
|
|
1926
|
+
' backgroundColor={shellColor}',
|
|
1927
|
+
' barStyle={theme.colorSystem.previewScheme === "dark" ? "light-content" : "dark-content"}',
|
|
1928
|
+
' translucent={false}',
|
|
1929
|
+
' />',
|
|
1930
|
+
' {Platform.OS === "android" ? (',
|
|
1931
|
+
' <NavigationBar',
|
|
1932
|
+
' style={theme.colorSystem.previewScheme === "dark" ? "dark" : "light"}',
|
|
1933
|
+
' />',
|
|
1934
|
+
' ) : null}',
|
|
1935
|
+
' <Stack',
|
|
1936
|
+
' screenOptions={{',
|
|
1937
|
+
' contentStyle: { backgroundColor: shellColor },',
|
|
1938
|
+
" headerShown: Platform.OS !== 'web',",
|
|
1939
|
+
' headerRight: () => (',
|
|
1940
|
+
' <Link href="/settings" asChild>',
|
|
1941
|
+
" <Pressable accessibilityRole=\"button\" style={{ alignItems: 'center', backgroundColor: '#111827', borderRadius: 14, height: 28, justifyContent: 'center', width: 28 }}>",
|
|
1942
|
+
" <Text style={{ color: '#ffffff', fontSize: 15, fontWeight: '800' }}>i</Text>",
|
|
1943
|
+
' </Pressable>',
|
|
1944
|
+
' </Link>',
|
|
1945
|
+
' ),',
|
|
1946
|
+
' }}>',
|
|
1947
|
+
shellScreen,
|
|
1948
|
+
' <Stack.Screen name="onboarding" options={{ title: \'Onboarding\' }} />',
|
|
1949
|
+
' <Stack.Screen name="onboarding/agreement" options={{ title: \'Agreement\' }} />',
|
|
1950
|
+
' <Stack.Screen name="onboarding/terms" options={{ title: \'Terms Of Service\' }} />',
|
|
1951
|
+
' <Stack.Screen name="onboarding/account-setup" options={{ title: \'Account Setup\' }} />',
|
|
1952
|
+
...expositionScreens,
|
|
1953
|
+
...nativeWindUiScreen,
|
|
1126
1954
|
" <Stack.Screen name=\"settings\" options={{ presentation: 'modal', title: 'Settings' }} />",
|
|
1127
|
-
'
|
|
1128
|
-
'
|
|
1955
|
+
' </Stack>',
|
|
1956
|
+
' </RouterThemeBridge>',
|
|
1957
|
+
' </SafeAreaProvider>',
|
|
1958
|
+
' </KeyboardProvider>',
|
|
1959
|
+
' </GestureHandlerRootView>',
|
|
1960
|
+
' );',
|
|
1961
|
+
'}',
|
|
1962
|
+
'',
|
|
1963
|
+
'export default function Layout() {',
|
|
1964
|
+
' const hasFontAssets = Object.keys(THEME_FONT_ASSETS).length > 0;',
|
|
1965
|
+
' const [fontsLoaded, fontsError] = useFonts(THEME_FONT_ASSETS);',
|
|
1966
|
+
'',
|
|
1967
|
+
' if (hasFontAssets && !fontsLoaded && !fontsError) {',
|
|
1968
|
+
' return null;',
|
|
1969
|
+
' }',
|
|
1970
|
+
'',
|
|
1971
|
+
' return (',
|
|
1972
|
+
' <AppThemeProvider>',
|
|
1973
|
+
' <LayoutInner />',
|
|
1974
|
+
' </AppThemeProvider>',
|
|
1129
1975
|
' );',
|
|
1130
1976
|
'}',
|
|
1131
1977
|
'',
|
|
@@ -1227,12 +2073,20 @@ function renderAnimatedPressable() {
|
|
|
1227
2073
|
'const AnimatedPressableBase = Animated.createAnimatedComponent(Pressable);',
|
|
1228
2074
|
'',
|
|
1229
2075
|
'interface AnimatedPressableProps {',
|
|
2076
|
+
' backgroundColor?: string;',
|
|
1230
2077
|
' children?: ReactNode;',
|
|
1231
2078
|
' label?: string;',
|
|
1232
2079
|
' onPress?: () => void;',
|
|
2080
|
+
' textColor?: string;',
|
|
1233
2081
|
'}',
|
|
1234
2082
|
'',
|
|
1235
|
-
|
|
2083
|
+
'export function AnimatedPressable({',
|
|
2084
|
+
" backgroundColor = '#111827',",
|
|
2085
|
+
' children,',
|
|
2086
|
+
" label = 'Reanimated press demo',",
|
|
2087
|
+
' onPress,',
|
|
2088
|
+
" textColor = '#ffffff',",
|
|
2089
|
+
'}: AnimatedPressableProps) {',
|
|
1236
2090
|
' const pressed = useSharedValue(0);',
|
|
1237
2091
|
' const animatedStyle = useAnimatedStyle(() => ({',
|
|
1238
2092
|
' transform: [{ scale: withTiming(pressed.value ? 0.97 : 1, { duration: 120 }) }],',
|
|
@@ -1247,9 +2101,9 @@ function renderAnimatedPressable() {
|
|
|
1247
2101
|
' onPressOut={() => {',
|
|
1248
2102
|
' pressed.value = 0;',
|
|
1249
2103
|
' }}',
|
|
1250
|
-
' style={[styles.button, animatedStyle]}',
|
|
2104
|
+
' style={[styles.button, { backgroundColor }, animatedStyle]}',
|
|
1251
2105
|
' >',
|
|
1252
|
-
' {children ?? <Text style={styles.label}>{label}</Text>}',
|
|
2106
|
+
' {children ?? <Text style={[styles.label, { color: textColor }]}>{label}</Text>}',
|
|
1253
2107
|
' </AnimatedPressableBase>',
|
|
1254
2108
|
' );',
|
|
1255
2109
|
'}',
|
|
@@ -1262,7 +2116,6 @@ function renderAnimatedPressable() {
|
|
|
1262
2116
|
' paddingVertical: 12,',
|
|
1263
2117
|
' },',
|
|
1264
2118
|
' label: {',
|
|
1265
|
-
" color: '#ffffff',",
|
|
1266
2119
|
' fontSize: 15,',
|
|
1267
2120
|
' fontWeight: "700",',
|
|
1268
2121
|
' textAlign: "center",',
|
|
@@ -1313,9 +2166,7 @@ function renderGestureCard() {
|
|
|
1313
2166
|
' borderRadius: 12,',
|
|
1314
2167
|
' borderWidth: 1,',
|
|
1315
2168
|
' padding: 16,',
|
|
1316
|
-
|
|
1317
|
-
' shadowOpacity: 0.08,',
|
|
1318
|
-
' shadowRadius: 10,',
|
|
2169
|
+
" boxShadow: '0 6px 10px rgba(0, 0, 0, 0.08)',",
|
|
1319
2170
|
' },',
|
|
1320
2171
|
' title: {',
|
|
1321
2172
|
" color: '#111827',",
|
|
@@ -1334,17 +2185,32 @@ function renderGestureCard() {
|
|
|
1334
2185
|
}
|
|
1335
2186
|
function renderKeyboardForm() {
|
|
1336
2187
|
return [
|
|
1337
|
-
"import { StyleSheet, TextInput } from 'react-native';",
|
|
1338
|
-
"import { KeyboardAwareScrollView, KeyboardToolbar } from 'react-native-keyboard-controller';",
|
|
2188
|
+
"import { Keyboard, Platform, ScrollView, StyleSheet, TextInput } from 'react-native';",
|
|
1339
2189
|
'',
|
|
1340
2190
|
'export function KeyboardForm() {',
|
|
2191
|
+
' if (Platform.OS === "web") {',
|
|
2192
|
+
' return (',
|
|
2193
|
+
' <ScrollView contentContainerStyle={styles.form} style={styles.scroller}>',
|
|
2194
|
+
' <TextInput blurOnSubmit onSubmitEditing={Keyboard.dismiss} placeholder="Project note" returnKeyType="done" style={styles.input} />',
|
|
2195
|
+
' <TextInput blurOnSubmit multiline onSubmitEditing={Keyboard.dismiss} placeholder="Details" returnKeyType="done" style={[styles.input, styles.multiline]} />',
|
|
2196
|
+
' </ScrollView>',
|
|
2197
|
+
' );',
|
|
2198
|
+
' }',
|
|
2199
|
+
'',
|
|
2200
|
+
" const keyboardController = require('react-native-keyboard-controller') as {",
|
|
2201
|
+
' KeyboardAwareScrollView: any;',
|
|
2202
|
+
' KeyboardToolbar: any;',
|
|
2203
|
+
' };',
|
|
2204
|
+
' const KeyboardAwareScrollView = keyboardController.KeyboardAwareScrollView;',
|
|
2205
|
+
' const KeyboardToolbar = keyboardController.KeyboardToolbar;',
|
|
2206
|
+
'',
|
|
1341
2207
|
' return (',
|
|
1342
2208
|
' <>',
|
|
1343
2209
|
' <KeyboardAwareScrollView bottomOffset={72} contentContainerStyle={styles.form} style={styles.scroller}>',
|
|
1344
|
-
' <TextInput placeholder="Project note" style={styles.input} />',
|
|
1345
|
-
' <TextInput multiline placeholder="Details" style={[styles.input, styles.multiline]} />',
|
|
2210
|
+
' <TextInput blurOnSubmit onSubmitEditing={Keyboard.dismiss} placeholder="Project note" returnKeyType="done" style={styles.input} />',
|
|
2211
|
+
' <TextInput blurOnSubmit multiline onSubmitEditing={Keyboard.dismiss} placeholder="Details" returnKeyType="done" style={[styles.input, styles.multiline]} />',
|
|
1346
2212
|
' </KeyboardAwareScrollView>',
|
|
1347
|
-
' <KeyboardToolbar />',
|
|
2213
|
+
' <KeyboardToolbar onDoneCallback={Keyboard.dismiss} />',
|
|
1348
2214
|
' </>',
|
|
1349
2215
|
' );',
|
|
1350
2216
|
'}',
|
|
@@ -1375,13 +2241,13 @@ function renderKeyboardForm() {
|
|
|
1375
2241
|
}
|
|
1376
2242
|
function renderSvgMark() {
|
|
1377
2243
|
return [
|
|
1378
|
-
"import Svg, {
|
|
2244
|
+
"import Svg, { Path } from 'react-native-svg';",
|
|
1379
2245
|
'',
|
|
1380
|
-
'export function SvgMark() {',
|
|
2246
|
+
'export function SvgMark({ size = 44 }: { size?: number }) {',
|
|
1381
2247
|
' return (',
|
|
1382
|
-
' <Svg width={
|
|
1383
|
-
' <
|
|
1384
|
-
' <Path d="
|
|
2248
|
+
' <Svg width={size} height={size} viewBox="0 0 2048 2048" accessibilityRole="image">',
|
|
2249
|
+
' <Path fill="#5666ff" d="m146.9 1305.8l-14.4 30q0 32.2 89.9 62.1 91 28.9 198.6 28.9 108.8 0 238.6-32.2 129.8-32.2 225.2-78.8 96.6-46.6 158.7-97.6 63.3-51.1 63.3-88.8 0-47.7-111-88.8-111-41-322.9-89.8-210.8-50-309.6-125.4-97.6-75.5-97.6-183.1 0-108.8 77.6-207.5 77.7-98.8 202-175.3 125.4-76.6 273-136.5 305.1-123.2 565.9-123.2 99.9 0 176.4 17.8 146.5 34.4 146.5 90.9 0 56.6-10 77.7-10 20-16.6 26.7-5.6 6.6-15.6 13.3-8.9 6.6-13.3 10-48.8 41-267.4 51-75.5 3.3-82.1 7.8-6.7 4.4-12.2 4.4-4.5 0-11.1-8.9-6.7-8.9-6.7-28.8 0-20 43.3-73.3-135.4 7.8-289.6 62.2-153.2 54.4-266.3 123.1-113.2 67.7-188.7 134.3-75.4 65.5-75.4 93.2 0 26.7 16.6 44.4 46.6 49.9 258.6 104.3 219.7 56.6 292.9 91 73.2 34.4 120.9 65.5 48.9 31 77.7 63.2 67.7 77.7 67.7 157.6 0 79.9-43.3 149.8-42.1 69.9-128.7 135.4-85.4 65.4-201.9 116.5-255.3 114.3-608.1 114.3-257.5 0-328.5-136.5-18.8-34.4-18.8-71 0-37.8 16.6-67.7 17.8-30 42.2-45.5 47.7-30 67.7-30 19.9 0 19.9 13.3z" />',
|
|
2250
|
+
' <Path fill="#f66d22" d="m486.9 1709.8l-14.4 30q0 32.2 89.9 62.1 91 28.9 198.6 28.9 108.8 0 238.6-32.2 129.8-32.2 225.2-78.8 96.6-46.6 158.7-97.6 63.3-51.1 63.3-88.8 0-47.7-111-88.8-111-41-322.9-89.8-210.8-50-309.6-125.4-97.6-75.5-97.6-183.1 0-108.8 77.6-207.5 77.7-98.8 202-175.3 125.4-76.6 273-136.5 305.1-123.2 565.9-123.2 99.9 0 176.4 17.8 146.5 34.4 146.5 90.9 0 56.6-10 77.7-10 20-16.6 26.7-5.6 6.6-15.6 13.3-8.9 6.6-13.3 10-48.8 41-267.4 51-75.5 3.3-82.1 7.8-6.7 4.4-12.2 4.4-4.5 0-11.1-8.9-6.7-8.9-6.7-28.8 0-20 43.3-73.3-135.4 7.8-289.6 62.2-153.2 54.4-266.3 123.1-113.2 67.7-188.7 134.3-75.4 65.5-75.4 93.2 0 26.7 16.6 44.4 46.6 49.9 258.6 104.3 219.7 56.6 292.9 91 73.2 34.4 120.9 65.5 48.9 31 77.7 63.2 67.7 77.7 67.7 157.6 0 79.9-43.3 149.8-42.1 69.9-128.7 135.4-85.4 65.4-201.9 116.5-255.3 114.3-608.1 114.3-257.5 0-328.5-136.5-18.8-34.4-18.8-71 0-37.8 16.6-67.7 17.8-30 42.2-45.5 47.7-30 67.7-30 19.9 0 19.9 13.3z" />',
|
|
1385
2251
|
' </Svg>',
|
|
1386
2252
|
' );',
|
|
1387
2253
|
'}',
|
|
@@ -1431,40 +2297,17 @@ function renderScreensCard() {
|
|
|
1431
2297
|
].join('\n');
|
|
1432
2298
|
}
|
|
1433
2299
|
function renderExpositionNotice() {
|
|
2300
|
+
return ['export function ExpositionNotice() {', ' return null;', '}', ''].join('\n');
|
|
2301
|
+
}
|
|
2302
|
+
function renderSoftwareMansionLogo() {
|
|
1434
2303
|
return [
|
|
1435
|
-
"import {
|
|
2304
|
+
"import { SvgXml } from 'react-native-svg';",
|
|
1436
2305
|
'',
|
|
1437
|
-
'
|
|
1438
|
-
' return (',
|
|
1439
|
-
' <View style={styles.notice}>',
|
|
1440
|
-
' <Text style={styles.eyebrow}>Temporary exposition scaffold</Text>',
|
|
1441
|
-
` <Text style={styles.body}>${EXPOSITION_NOTICE}</Text>`,
|
|
1442
|
-
' </View>',
|
|
1443
|
-
' );',
|
|
1444
|
-
'}',
|
|
2306
|
+
'const softwareMansionLogoXml = `<svg fill="currentColor" viewBox="0 0 149.79 80" xmlns="http://www.w3.org/2000/svg" width="150" height="80" preserveAspectRatio="xMidYMid meet"><path d="M24.281 79.063h124.58V24.356L125.513.937H.933v54.707z" fill="#fff"></path><path d="M0 .001h125.9l23.894 23.967V80h-125.9L.002 56.033V0zm1.867 3.198v52.057l21.48 21.545V24.744zm1.321-1.324 21.48 21.545h121.94l-21.48-21.545zm144.74 23.418H25.218v52.833h122.71z"></path><path d="M47.255 46.215c0 1.873-1.246 3.496-4.234 3.496-1.308 0-2.367-.312-3.3-.686v-2.623c.996.5 2.179.812 3.237.812.997 0 1.494-.312 1.494-.875 0-1.748-4.731-1.187-4.731-4.746 0-2.185 1.744-3.498 4.172-3.498.995 0 1.929.251 2.926.813v2.685c-1.308-.812-2.242-1.062-2.989-1.062-.872 0-1.37.313-1.37.874-.062 1.625 4.794 1 4.794 4.81z"></path><path d="M49.62 43.903c0-3.184 2.614-5.807 5.79-5.807 3.175 0 5.79 2.623 5.79 5.808 0 3.185-2.615 5.807-5.79 5.807-3.176-.062-5.79-2.622-5.79-5.807zm8.716 0c0-1.748-1.307-3.06-2.927-3.06-1.618 0-2.925 1.312-2.925 3.061 0 1.748 1.307 3.06 2.925 3.06 1.62 0 2.927-1.312 2.927-3.061z"></path><path d="M67.675 37.408v.937h3.674l-1.246 2.623h-2.366v8.493H65.06v-8.556h-1.867v-2.622h1.867v-1.061c0-3.373 1.744-5.059 4.483-5.059.685 0 1.307.125 1.868.25v2.623c-.498-.187-1.058-.25-1.62-.25-1.494 0-2.116 1-2.116 2.623z"></path><path d="M76.952 40.906v4.434c0 1.187.685 1.687 1.743 1.687.685 0 1.37-.188 1.93-.562v2.747c-.747.312-1.431.5-2.427.5-2.43 0-3.923-1.312-3.923-3.998v-4.746h-1.619v-2.622h1.62v-2.873l2.676-.687v3.56h3.674v2.622h-3.674z"></path><path d="m99.988 38.346-3.549 11.115h-2.677l-2.428-7.619-2.49 7.619h-2.678l-3.549-11.115h3.051l1.992 7.432 2.367-7.432h2.676l2.367 7.432 1.992-7.432z"></path><path d="M101.36 43.903c0-3.184 2.303-5.807 5.48-5.807 1.244 0 2.24.375 2.987 1v-.75h2.678v11.115h-2.616v-.874c-.747.687-1.805 1.124-3.112 1.124-3.052-.062-5.417-2.622-5.417-5.807zm8.717 0c0-1.748-1.308-3.06-2.927-3.06s-2.926 1.312-2.926 3.061c0 1.748 1.307 3.06 2.926 3.06 1.619 0 2.927-1.312 2.927-3.061z"></path><path d="M116.3 38.346h2.615v1.498c.81-1.498 2.303-1.748 3.611-1.748v3.184c-1.868-.5-3.548.562-3.548 2.936v5.183H116.3z"></path><path d="M130.99 47.089a9.627 9.627 0 0 0 3.549-.687l-1.37 2.81a7.296 7.296 0 0 1-2.677.5c-3.923 0-6.288-2.436-6.288-5.808 0-3.185 2.365-5.808 5.79-5.808 2.303 0 4.171 1.187 4.918 2.685v4.06h-7.844c.373 1.31 1.68 2.248 3.922 2.248zm-3.985-4.371h5.79c-.373-1.313-1.494-2.124-2.926-2.124-1.37 0-2.428.874-2.864 2.124z"></path><path d="M55.223 58.83v7.306h-2.677v-6.869c0-1.187-.872-1.873-1.743-1.873-.934 0-1.744.686-1.744 1.873v6.869h-2.676v-6.869c0-1.187-.872-1.873-1.744-1.873-.934 0-1.743.686-1.743 1.873v6.869H40.22V55.02h2.615v.812c.622-.75 1.432-1 2.49-1 1.183 0 2.18.5 2.864 1.437.871-1 1.992-1.436 3.362-1.436 2.054 0 3.673 1.623 3.673 3.996z"></path><path d="M58.025 60.579c0-3.186 2.304-5.808 5.478-5.808 1.246 0 2.243.374 2.989.999v-.75h2.677v11.115h-2.615v-.874c-.747.687-1.805 1.124-3.112 1.124-3.052 0-5.417-2.622-5.417-5.807zm8.717 0c0-1.75-1.308-3.061-2.927-3.061s-2.926 1.312-2.926 3.061c0 1.748 1.308 3.06 2.926 3.06s2.927-1.312 2.927-3.061z"></path><path d="M72.843 55.02h2.614v.812c.81-.812 1.868-1.062 2.927-1.062 2.304 0 4.17 1.748 4.17 4.122v7.18h-2.676v-6.556a2.103 2.103 0 0 0-2.117-2.123 2.102 2.102 0 0 0-2.116 2.123v6.62h-2.678V55.02h-.124z"></path><path d="M93.326 62.889c0 1.873-1.245 3.496-4.234 3.496-1.307 0-2.366-.312-3.299-.686v-2.623c.996.5 2.179.812 3.238.812.996 0 1.493-.312 1.493-.874 0-1.75-4.73-1.188-4.73-4.747 0-2.186 1.742-3.497 4.17-3.497.996 0 1.93.25 2.927.812v2.685c-1.308-.812-2.242-1.062-2.989-1.062-.872 0-1.37.312-1.37.874-.062 1.687 4.794 1.063 4.794 4.81z"></path><path d="M99.116 55.02v11.115h-2.677V55.02z"></path><path d="M101.92 60.579c0-3.186 2.615-5.808 5.79-5.808s5.79 2.622 5.79 5.807-2.615 5.807-5.79 5.807-5.79-2.622-5.79-5.807zm8.717 0c0-1.75-1.307-3.061-2.927-3.061-1.618 0-2.926 1.312-2.926 3.061 0 1.748 1.308 3.06 2.926 3.06 1.62 0 2.927-1.312 2.927-3.061z"></path><path d="M116.24 55.02h2.615v.812c.81-.812 1.868-1.062 2.927-1.062 2.303 0 4.17 1.748 4.17 4.122v7.18h-2.676v-6.556a2.103 2.103 0 0 0-2.118-2.123 2.103 2.103 0 0 0-2.116 2.123v6.62h-2.677V55.02h-.125z"></path><path d="M131.24 63.076v.437h-.933v2.623h-.499v-2.623h-.933v-.437z"></path><path d="m132.3 63.076.997 2.435.933-2.435h.623v3.06h-.498v-2.248l-.934 2.248h-.436l-.934-2.248v2.248h-.498v-3.061h.747z"></path></svg>`;',
|
|
1445
2307
|
'',
|
|
1446
|
-
'
|
|
1447
|
-
'
|
|
1448
|
-
|
|
1449
|
-
" borderColor: '#fed7aa',",
|
|
1450
|
-
' borderRadius: 12,',
|
|
1451
|
-
' borderWidth: 1,',
|
|
1452
|
-
' gap: 6,',
|
|
1453
|
-
' padding: 14,',
|
|
1454
|
-
' },',
|
|
1455
|
-
' eyebrow: {',
|
|
1456
|
-
" color: '#9a3412',",
|
|
1457
|
-
' fontSize: 12,',
|
|
1458
|
-
' fontWeight: "800",',
|
|
1459
|
-
' letterSpacing: 0.4,',
|
|
1460
|
-
' textTransform: "uppercase",',
|
|
1461
|
-
' },',
|
|
1462
|
-
' body: {',
|
|
1463
|
-
" color: '#7c2d12',",
|
|
1464
|
-
' fontSize: 14,',
|
|
1465
|
-
' lineHeight: 20,',
|
|
1466
|
-
' },',
|
|
1467
|
-
'});',
|
|
2308
|
+
'export function SoftwareMansionLogo({ width = 150, height = 80 }: { width?: number; height?: number }) {',
|
|
2309
|
+
' return <SvgXml xml={softwareMansionLogoXml} width={width} height={height} accessibilityRole="image" />;',
|
|
2310
|
+
'}',
|
|
1468
2311
|
'',
|
|
1469
2312
|
].join('\n');
|
|
1470
2313
|
}
|
|
@@ -1473,6 +2316,8 @@ function renderPackageCard() {
|
|
|
1473
2316
|
"import type { ReactNode } from 'react';",
|
|
1474
2317
|
"import { StyleSheet, Text, View } from 'react-native';",
|
|
1475
2318
|
'',
|
|
2319
|
+
"import { useAppTheme } from '../../theme/provider';",
|
|
2320
|
+
'',
|
|
1476
2321
|
'interface PackageCardProps {',
|
|
1477
2322
|
' title: string;',
|
|
1478
2323
|
' packageName: string;',
|
|
@@ -1481,11 +2326,14 @@ function renderPackageCard() {
|
|
|
1481
2326
|
'}',
|
|
1482
2327
|
'',
|
|
1483
2328
|
'export function PackageCard({ title, packageName, body, children }: PackageCardProps) {',
|
|
2329
|
+
' const theme = useAppTheme();',
|
|
2330
|
+
' const colors = theme.activeColors;',
|
|
2331
|
+
'',
|
|
1484
2332
|
' return (',
|
|
1485
|
-
' <View style={styles.card}>',
|
|
1486
|
-
' <Text style={styles.packageName}>{packageName}</Text>',
|
|
1487
|
-
' <Text style={styles.title}>{title}</Text>',
|
|
1488
|
-
' <Text style={styles.body}>{body}</Text>',
|
|
2333
|
+
' <View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.primary, borderRadius: theme.layout.radius }]}>',
|
|
2334
|
+
' <Text style={[styles.packageName, { color: colors.text }]}>{packageName}</Text>',
|
|
2335
|
+
' <Text style={[styles.title, { color: colors.text, fontFamily: theme.typography.fontFamily, fontWeight: theme.typography.fontFamily === \"System\" || theme.typography.fontFamily === \"monospace\" ? \"800\" : \"normal\" }]}>{title}</Text>',
|
|
2336
|
+
' <Text style={[styles.body, { color: colors.text }]}>{body}</Text>',
|
|
1489
2337
|
' {children ? <View style={styles.demo}>{children}</View> : null}',
|
|
1490
2338
|
' </View>',
|
|
1491
2339
|
' );',
|
|
@@ -1530,68 +2378,678 @@ function renderExpositionComponentIndex() {
|
|
|
1530
2378
|
"export { KeyboardForm } from './keyboard-form';",
|
|
1531
2379
|
"export { PackageCard } from './package-card';",
|
|
1532
2380
|
"export { ScreensCard } from './screens-card';",
|
|
2381
|
+
"export { SoftwareMansionLogo } from './software-mansion-logo';",
|
|
1533
2382
|
"export { SvgMark } from './svg-mark';",
|
|
1534
2383
|
'',
|
|
1535
2384
|
].join('\n');
|
|
1536
2385
|
}
|
|
1537
|
-
function
|
|
2386
|
+
function renderNativeWindUiActivityIndicator() {
|
|
1538
2387
|
return [
|
|
1539
|
-
"import {
|
|
1540
|
-
"import {
|
|
2388
|
+
"import type { ComponentProps } from 'react';",
|
|
2389
|
+
"import { ActivityIndicator as RNActivityIndicator } from 'react-native';",
|
|
1541
2390
|
'',
|
|
1542
|
-
|
|
1543
|
-
"
|
|
2391
|
+
'export function ActivityIndicator(props: ComponentProps<typeof RNActivityIndicator>) {',
|
|
2392
|
+
' return <RNActivityIndicator color="#2563eb" {...props} />;',
|
|
2393
|
+
'}',
|
|
1544
2394
|
'',
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
'
|
|
2395
|
+
].join('\n');
|
|
2396
|
+
}
|
|
2397
|
+
function renderNativeWindUiAvatar() {
|
|
2398
|
+
return [
|
|
2399
|
+
"import type { ReactNode } from 'react';",
|
|
2400
|
+
"import { StyleSheet, View, type ViewProps } from 'react-native';",
|
|
1550
2401
|
'',
|
|
1551
|
-
'
|
|
2402
|
+
'type AvatarProps = ViewProps & {',
|
|
2403
|
+
' children?: ReactNode;',
|
|
2404
|
+
' className?: string;',
|
|
2405
|
+
'};',
|
|
2406
|
+
'',
|
|
2407
|
+
'export function Avatar({ children, className: _className, style, ...props }: AvatarProps) {',
|
|
1552
2408
|
' return (',
|
|
1553
|
-
' <
|
|
1554
|
-
'
|
|
1555
|
-
'
|
|
1556
|
-
'
|
|
1557
|
-
|
|
1558
|
-
'
|
|
1559
|
-
'
|
|
1560
|
-
'
|
|
1561
|
-
'
|
|
1562
|
-
'
|
|
1563
|
-
'
|
|
1564
|
-
'
|
|
2409
|
+
' <View style={[styles.avatar, style]} {...props}>',
|
|
2410
|
+
' {children}',
|
|
2411
|
+
' </View>',
|
|
2412
|
+
' );',
|
|
2413
|
+
'}',
|
|
2414
|
+
'',
|
|
2415
|
+
'export function AvatarFallback({ children, className: _className, style, ...props }: AvatarProps) {',
|
|
2416
|
+
' return (',
|
|
2417
|
+
' <View style={[styles.fallback, style]} {...props}>',
|
|
2418
|
+
' {children}',
|
|
2419
|
+
' </View>',
|
|
2420
|
+
' );',
|
|
2421
|
+
'}',
|
|
2422
|
+
'',
|
|
2423
|
+
'const styles = StyleSheet.create({',
|
|
2424
|
+
' avatar: {',
|
|
2425
|
+
" alignItems: 'center',",
|
|
2426
|
+
" backgroundColor: '#e2e8f0',",
|
|
2427
|
+
' borderRadius: 999,',
|
|
2428
|
+
' height: 48,',
|
|
2429
|
+
" justifyContent: 'center',",
|
|
2430
|
+
' width: 48,',
|
|
2431
|
+
' },',
|
|
2432
|
+
' fallback: {',
|
|
2433
|
+
" alignItems: 'center',",
|
|
2434
|
+
" justifyContent: 'center',",
|
|
2435
|
+
' },',
|
|
2436
|
+
'});',
|
|
2437
|
+
'',
|
|
2438
|
+
].join('\n');
|
|
2439
|
+
}
|
|
2440
|
+
function renderNativeWindUiButton() {
|
|
2441
|
+
return [
|
|
2442
|
+
"import type { ReactNode } from 'react';",
|
|
2443
|
+
"import { Pressable, StyleSheet, type PressableProps } from 'react-native';",
|
|
2444
|
+
'',
|
|
2445
|
+
"type ButtonVariant = 'primary' | 'secondary' | 'tonal' | 'plain';",
|
|
2446
|
+
'',
|
|
2447
|
+
'export interface ButtonProps extends PressableProps {',
|
|
2448
|
+
' children?: ReactNode;',
|
|
2449
|
+
' variant?: ButtonVariant;',
|
|
2450
|
+
'}',
|
|
2451
|
+
'',
|
|
2452
|
+
'export function Button({ children, style, variant = "primary", ...props }: ButtonProps) {',
|
|
2453
|
+
' return (',
|
|
2454
|
+
' <Pressable',
|
|
2455
|
+
' {...props}',
|
|
2456
|
+
" style={(state) => [styles.base, styles[variant], typeof style === 'function' ? style(state) : style]}",
|
|
2457
|
+
' accessibilityRole="button">',
|
|
2458
|
+
' {children}',
|
|
2459
|
+
' </Pressable>',
|
|
2460
|
+
' );',
|
|
2461
|
+
'}',
|
|
2462
|
+
'',
|
|
2463
|
+
'const styles = StyleSheet.create({',
|
|
2464
|
+
' base: {',
|
|
2465
|
+
" alignItems: 'center',",
|
|
2466
|
+
' borderRadius: 12,',
|
|
2467
|
+
' borderWidth: 1,',
|
|
2468
|
+
" justifyContent: 'center',",
|
|
2469
|
+
' minHeight: 40,',
|
|
2470
|
+
' paddingHorizontal: 14,',
|
|
2471
|
+
' paddingVertical: 10,',
|
|
2472
|
+
' },',
|
|
2473
|
+
' plain: {',
|
|
2474
|
+
" backgroundColor: 'transparent',",
|
|
2475
|
+
" borderColor: '#d1d5db',",
|
|
2476
|
+
' },',
|
|
2477
|
+
' primary: {',
|
|
2478
|
+
" backgroundColor: '#2563eb',",
|
|
2479
|
+
" borderColor: '#1d4ed8',",
|
|
2480
|
+
' },',
|
|
2481
|
+
' secondary: {',
|
|
2482
|
+
" backgroundColor: '#f8fafc',",
|
|
2483
|
+
" borderColor: '#94a3b8',",
|
|
2484
|
+
' },',
|
|
2485
|
+
' tonal: {',
|
|
2486
|
+
" backgroundColor: '#dbeafe',",
|
|
2487
|
+
" borderColor: '#93c5fd',",
|
|
2488
|
+
' },',
|
|
2489
|
+
'});',
|
|
2490
|
+
'',
|
|
2491
|
+
].join('\n');
|
|
2492
|
+
}
|
|
2493
|
+
function renderNativeWindUiDatePicker() {
|
|
2494
|
+
return [
|
|
2495
|
+
"import { Pressable, StyleSheet, Text, View } from 'react-native';",
|
|
2496
|
+
'',
|
|
2497
|
+
'export interface DatePickerProps {',
|
|
2498
|
+
" mode?: 'date' | 'time' | 'datetime';",
|
|
2499
|
+
' value: Date;',
|
|
2500
|
+
' onChange?: (event: unknown, selectedDate?: Date) => void;',
|
|
2501
|
+
'}',
|
|
2502
|
+
'',
|
|
2503
|
+
'export function DatePicker({ value, onChange }: DatePickerProps) {',
|
|
2504
|
+
' return (',
|
|
2505
|
+
' <View style={styles.container}>',
|
|
2506
|
+
' <Text style={styles.value}>{value.toDateString()}</Text>',
|
|
2507
|
+
' <Pressable',
|
|
2508
|
+
' onPress={() => onChange?.({ type: "set" }, new Date())}',
|
|
2509
|
+
' style={styles.button}',
|
|
2510
|
+
' accessibilityRole="button">',
|
|
2511
|
+
' <Text style={styles.buttonText}>Use Today</Text>',
|
|
2512
|
+
' </Pressable>',
|
|
2513
|
+
' </View>',
|
|
2514
|
+
' );',
|
|
2515
|
+
'}',
|
|
2516
|
+
'',
|
|
2517
|
+
'const styles = StyleSheet.create({',
|
|
2518
|
+
' button: {',
|
|
2519
|
+
" backgroundColor: '#eff6ff',",
|
|
2520
|
+
' borderRadius: 10,',
|
|
2521
|
+
" borderColor: '#bfdbfe',",
|
|
2522
|
+
' borderWidth: 1,',
|
|
2523
|
+
' paddingHorizontal: 12,',
|
|
2524
|
+
' paddingVertical: 8,',
|
|
2525
|
+
' },',
|
|
2526
|
+
' buttonText: {',
|
|
2527
|
+
" color: '#1d4ed8',",
|
|
2528
|
+
' fontWeight: "700",',
|
|
2529
|
+
' },',
|
|
2530
|
+
' container: {',
|
|
2531
|
+
" alignItems: 'center',",
|
|
2532
|
+
" flexDirection: 'row',",
|
|
2533
|
+
" justifyContent: 'space-between',",
|
|
2534
|
+
' },',
|
|
2535
|
+
' value: {',
|
|
2536
|
+
" color: '#334155',",
|
|
2537
|
+
' fontSize: 14,',
|
|
2538
|
+
' fontWeight: "600",',
|
|
2539
|
+
' },',
|
|
2540
|
+
'});',
|
|
2541
|
+
'',
|
|
2542
|
+
].join('\n');
|
|
2543
|
+
}
|
|
2544
|
+
function renderNativeWindUiPicker() {
|
|
2545
|
+
return [
|
|
2546
|
+
"import { Children, isValidElement, type ReactNode } from 'react';",
|
|
2547
|
+
"import { Pressable, StyleSheet, Text, View } from 'react-native';",
|
|
2548
|
+
'',
|
|
2549
|
+
'export interface PickerItemProps {',
|
|
2550
|
+
' label: string;',
|
|
2551
|
+
' value: string;',
|
|
2552
|
+
'}',
|
|
2553
|
+
'',
|
|
2554
|
+
'export interface PickerProps {',
|
|
2555
|
+
' selectedValue: string;',
|
|
2556
|
+
' onValueChange?: (value: string) => void;',
|
|
2557
|
+
' children?: ReactNode;',
|
|
2558
|
+
'}',
|
|
2559
|
+
'',
|
|
2560
|
+
'export function Picker({ selectedValue, onValueChange, children }: PickerProps) {',
|
|
2561
|
+
' const items = Children.toArray(children)',
|
|
2562
|
+
' .filter(isValidElement)',
|
|
2563
|
+
' .map((child) => child.props as PickerItemProps);',
|
|
2564
|
+
'',
|
|
2565
|
+
' return (',
|
|
2566
|
+
' <View style={styles.row}>',
|
|
2567
|
+
' {items.map((item) => {',
|
|
2568
|
+
' const active = item.value === selectedValue;',
|
|
2569
|
+
' return (',
|
|
2570
|
+
' <Pressable',
|
|
2571
|
+
' key={item.value}',
|
|
2572
|
+
' onPress={() => onValueChange?.(item.value)}',
|
|
2573
|
+
' style={[styles.item, active ? styles.itemActive : styles.itemIdle]}',
|
|
2574
|
+
' accessibilityRole="button">',
|
|
2575
|
+
' <Text style={active ? styles.textActive : styles.textIdle}>{item.label}</Text>',
|
|
2576
|
+
' </Pressable>',
|
|
2577
|
+
' );',
|
|
2578
|
+
' })}',
|
|
2579
|
+
' </View>',
|
|
2580
|
+
' );',
|
|
2581
|
+
'}',
|
|
2582
|
+
'',
|
|
2583
|
+
'export function PickerItem(_props: PickerItemProps) {',
|
|
2584
|
+
' return null;',
|
|
2585
|
+
'}',
|
|
2586
|
+
'',
|
|
2587
|
+
'const styles = StyleSheet.create({',
|
|
2588
|
+
' item: {',
|
|
2589
|
+
' borderRadius: 999,',
|
|
2590
|
+
' borderWidth: 1,',
|
|
2591
|
+
' minHeight: 34,',
|
|
2592
|
+
' paddingHorizontal: 12,',
|
|
2593
|
+
' paddingVertical: 8,',
|
|
2594
|
+
' },',
|
|
2595
|
+
' itemActive: {',
|
|
2596
|
+
" backgroundColor: '#2563eb',",
|
|
2597
|
+
" borderColor: '#1d4ed8',",
|
|
2598
|
+
' },',
|
|
2599
|
+
' itemIdle: {',
|
|
2600
|
+
" backgroundColor: '#f8fafc',",
|
|
2601
|
+
" borderColor: '#cbd5e1',",
|
|
2602
|
+
' },',
|
|
2603
|
+
' row: {',
|
|
2604
|
+
" flexDirection: 'row',",
|
|
2605
|
+
" flexWrap: 'wrap',",
|
|
2606
|
+
' gap: 8,',
|
|
2607
|
+
' },',
|
|
2608
|
+
' textActive: {',
|
|
2609
|
+
" color: '#ffffff',",
|
|
2610
|
+
' fontSize: 13,',
|
|
2611
|
+
' fontWeight: "700",',
|
|
2612
|
+
' },',
|
|
2613
|
+
' textIdle: {',
|
|
2614
|
+
" color: '#334155',",
|
|
2615
|
+
' fontSize: 13,',
|
|
2616
|
+
' fontWeight: "700",',
|
|
2617
|
+
' },',
|
|
2618
|
+
'});',
|
|
2619
|
+
'',
|
|
2620
|
+
].join('\n');
|
|
2621
|
+
}
|
|
2622
|
+
function renderNativeWindUiProgressIndicator() {
|
|
2623
|
+
return [
|
|
2624
|
+
"import { StyleSheet, View } from 'react-native';",
|
|
2625
|
+
'',
|
|
2626
|
+
'export interface ProgressIndicatorProps {',
|
|
2627
|
+
' value: number;',
|
|
2628
|
+
'}',
|
|
2629
|
+
'',
|
|
2630
|
+
'export function ProgressIndicator({ value }: ProgressIndicatorProps) {',
|
|
2631
|
+
' const clamped = Math.max(0, Math.min(100, Math.round(value)));',
|
|
2632
|
+
' return (',
|
|
2633
|
+
' <View style={styles.track}>',
|
|
2634
|
+
' <View style={[styles.fill, { width: `${clamped}%` }]} />',
|
|
2635
|
+
' </View>',
|
|
2636
|
+
' );',
|
|
2637
|
+
'}',
|
|
2638
|
+
'',
|
|
2639
|
+
'const styles = StyleSheet.create({',
|
|
2640
|
+
' fill: {',
|
|
2641
|
+
" backgroundColor: '#2563eb',",
|
|
2642
|
+
' borderRadius: 999,',
|
|
2643
|
+
" height: '100%',",
|
|
2644
|
+
' },',
|
|
2645
|
+
' track: {',
|
|
2646
|
+
" backgroundColor: '#dbeafe',",
|
|
2647
|
+
' borderRadius: 999,',
|
|
2648
|
+
' height: 10,',
|
|
2649
|
+
" overflow: 'hidden',",
|
|
2650
|
+
' width: "100%",',
|
|
2651
|
+
' },',
|
|
2652
|
+
'});',
|
|
2653
|
+
'',
|
|
2654
|
+
].join('\n');
|
|
2655
|
+
}
|
|
2656
|
+
function renderNativeWindUiSlider() {
|
|
2657
|
+
return [
|
|
2658
|
+
"import { Pressable, StyleSheet, Text, View } from 'react-native';",
|
|
2659
|
+
'',
|
|
2660
|
+
'export interface SliderProps {',
|
|
2661
|
+
' value: number;',
|
|
2662
|
+
' onValueChange?: (value: number) => void;',
|
|
2663
|
+
' min?: number;',
|
|
2664
|
+
' max?: number;',
|
|
2665
|
+
' step?: number;',
|
|
2666
|
+
' disabled?: boolean;',
|
|
2667
|
+
'}',
|
|
2668
|
+
'',
|
|
2669
|
+
'export function Slider({',
|
|
2670
|
+
' value,',
|
|
2671
|
+
' onValueChange,',
|
|
2672
|
+
' min = 0,',
|
|
2673
|
+
' max = 1,',
|
|
2674
|
+
' step = 0.05,',
|
|
2675
|
+
' disabled = false,',
|
|
2676
|
+
'}: SliderProps) {',
|
|
2677
|
+
' const clamp = (next: number) => Math.max(min, Math.min(max, next));',
|
|
2678
|
+
' const changeBy = (delta: number) => onValueChange?.(clamp(Number((value + delta).toFixed(3))));',
|
|
2679
|
+
'',
|
|
2680
|
+
' return (',
|
|
2681
|
+
' <View style={[styles.row, disabled && styles.disabled]}>',
|
|
2682
|
+
' <Pressable disabled={disabled} onPress={() => changeBy(-step)} style={styles.button} accessibilityRole="button">',
|
|
2683
|
+
' <Text style={styles.buttonLabel}>-</Text>',
|
|
2684
|
+
' </Pressable>',
|
|
2685
|
+
' <Text style={styles.value}>{value.toFixed(2)}</Text>',
|
|
2686
|
+
' <Pressable disabled={disabled} onPress={() => changeBy(step)} style={styles.button} accessibilityRole="button">',
|
|
2687
|
+
' <Text style={styles.buttonLabel}>+</Text>',
|
|
2688
|
+
' </Pressable>',
|
|
2689
|
+
' </View>',
|
|
2690
|
+
' );',
|
|
2691
|
+
'}',
|
|
2692
|
+
'',
|
|
2693
|
+
'const styles = StyleSheet.create({',
|
|
2694
|
+
' button: {',
|
|
2695
|
+
" alignItems: 'center',",
|
|
2696
|
+
" backgroundColor: '#eff6ff',",
|
|
2697
|
+
' borderRadius: 10,',
|
|
2698
|
+
" borderColor: '#bfdbfe',",
|
|
2699
|
+
' borderWidth: 1,',
|
|
2700
|
+
" justifyContent: 'center',",
|
|
2701
|
+
' minHeight: 36,',
|
|
2702
|
+
' minWidth: 36,',
|
|
2703
|
+
' },',
|
|
2704
|
+
' buttonLabel: {',
|
|
2705
|
+
" color: '#1d4ed8',",
|
|
2706
|
+
' fontSize: 18,',
|
|
2707
|
+
' fontWeight: "700",',
|
|
2708
|
+
' },',
|
|
2709
|
+
' disabled: {',
|
|
2710
|
+
' opacity: 0.45,',
|
|
2711
|
+
' },',
|
|
2712
|
+
' row: {',
|
|
2713
|
+
" alignItems: 'center',",
|
|
2714
|
+
" flexDirection: 'row',",
|
|
2715
|
+
' gap: 10,',
|
|
2716
|
+
' },',
|
|
2717
|
+
' value: {',
|
|
2718
|
+
" color: '#334155',",
|
|
2719
|
+
' fontSize: 14,',
|
|
2720
|
+
' fontVariant: ["tabular-nums"],',
|
|
2721
|
+
' fontWeight: "700",',
|
|
2722
|
+
' minWidth: 52,',
|
|
2723
|
+
" textAlign: 'center',",
|
|
2724
|
+
' },',
|
|
2725
|
+
'});',
|
|
2726
|
+
'',
|
|
2727
|
+
].join('\n');
|
|
2728
|
+
}
|
|
2729
|
+
function renderNativeWindUiText() {
|
|
2730
|
+
return [
|
|
2731
|
+
"import type { TextProps } from 'react-native';",
|
|
2732
|
+
"import { StyleSheet, Text as RNText } from 'react-native';",
|
|
2733
|
+
'',
|
|
2734
|
+
"type Variant = 'largeTitle' | 'heading' | 'body' | 'callout' | 'subhead' | 'footnote' | 'caption2';",
|
|
2735
|
+
"type Tone = 'primary' | 'secondary' | 'tertiary' | 'quarternary';",
|
|
2736
|
+
'',
|
|
2737
|
+
'export interface NativeWindUiTextProps extends TextProps {',
|
|
2738
|
+
' variant?: Variant;',
|
|
2739
|
+
' color?: Tone;',
|
|
2740
|
+
' className?: string;',
|
|
2741
|
+
'}',
|
|
2742
|
+
'',
|
|
2743
|
+
'const variantStyles: Record<Variant, TextProps["style"]> = {',
|
|
2744
|
+
' largeTitle: { fontSize: 30, fontWeight: "900", lineHeight: 36 },',
|
|
2745
|
+
' heading: { fontSize: 18, fontWeight: "800", lineHeight: 24 },',
|
|
2746
|
+
' body: { fontSize: 16, fontWeight: "500", lineHeight: 22 },',
|
|
2747
|
+
' callout: { fontSize: 15, fontWeight: "600", lineHeight: 21 },',
|
|
2748
|
+
' subhead: { fontSize: 14, fontWeight: "700", lineHeight: 20 },',
|
|
2749
|
+
' footnote: { fontSize: 13, fontWeight: "500", lineHeight: 18 },',
|
|
2750
|
+
' caption2: { fontSize: 12, fontWeight: "700", lineHeight: 16 },',
|
|
2751
|
+
'};',
|
|
2752
|
+
'',
|
|
2753
|
+
'const toneStyles: Record<Tone, TextProps["style"]> = {',
|
|
2754
|
+
' primary: { color: "#0f172a" },',
|
|
2755
|
+
' secondary: { color: "#334155" },',
|
|
2756
|
+
' tertiary: { color: "#475569" },',
|
|
2757
|
+
' quarternary: { color: "#64748b" },',
|
|
2758
|
+
'};',
|
|
2759
|
+
'',
|
|
2760
|
+
'export function Text({',
|
|
2761
|
+
' variant = "body",',
|
|
2762
|
+
' color = "primary",',
|
|
2763
|
+
' className: _className,',
|
|
2764
|
+
' style,',
|
|
2765
|
+
' ...props',
|
|
2766
|
+
'}: NativeWindUiTextProps) {',
|
|
2767
|
+
' return <RNText {...props} style={[styles.base, variantStyles[variant], toneStyles[color], style]} />;',
|
|
2768
|
+
'}',
|
|
2769
|
+
'',
|
|
2770
|
+
'const styles = StyleSheet.create({',
|
|
2771
|
+
' base: {',
|
|
2772
|
+
" color: '#0f172a',",
|
|
2773
|
+
' },',
|
|
2774
|
+
'});',
|
|
2775
|
+
'',
|
|
2776
|
+
].join('\n');
|
|
2777
|
+
}
|
|
2778
|
+
function renderNativeWindUiThemeToggle() {
|
|
2779
|
+
return [
|
|
2780
|
+
"import { useState } from 'react';",
|
|
2781
|
+
"import { Pressable, StyleSheet, Text } from 'react-native';",
|
|
2782
|
+
'',
|
|
2783
|
+
'export function ThemeToggle() {',
|
|
2784
|
+
' const [darkPreview, setDarkPreview] = useState(false);',
|
|
2785
|
+
' return (',
|
|
2786
|
+
' <Pressable',
|
|
2787
|
+
' onPress={() => setDarkPreview((current) => !current)}',
|
|
2788
|
+
' style={[styles.button, darkPreview ? styles.dark : styles.light]}',
|
|
2789
|
+
' accessibilityRole="button">',
|
|
2790
|
+
' <Text style={[styles.label, darkPreview ? styles.darkLabel : styles.lightLabel]}>{darkPreview ? "Dark preview" : "Light preview"}</Text>',
|
|
2791
|
+
' </Pressable>',
|
|
2792
|
+
' );',
|
|
2793
|
+
'}',
|
|
2794
|
+
'',
|
|
2795
|
+
'const styles = StyleSheet.create({',
|
|
2796
|
+
' button: {',
|
|
2797
|
+
" alignItems: 'center',",
|
|
2798
|
+
' borderRadius: 999,',
|
|
2799
|
+
' borderWidth: 1,',
|
|
2800
|
+
" justifyContent: 'center',",
|
|
2801
|
+
' minHeight: 34,',
|
|
2802
|
+
' minWidth: 120,',
|
|
2803
|
+
' paddingHorizontal: 12,',
|
|
2804
|
+
' paddingVertical: 6,',
|
|
2805
|
+
' },',
|
|
2806
|
+
' dark: {',
|
|
2807
|
+
" backgroundColor: '#0f172a',",
|
|
2808
|
+
" borderColor: '#1e293b',",
|
|
2809
|
+
' },',
|
|
2810
|
+
' darkLabel: {',
|
|
2811
|
+
" color: '#f8fafc',",
|
|
2812
|
+
' },',
|
|
2813
|
+
' label: {',
|
|
2814
|
+
' fontSize: 12,',
|
|
2815
|
+
' fontWeight: "700",',
|
|
2816
|
+
' },',
|
|
2817
|
+
' light: {',
|
|
2818
|
+
" backgroundColor: '#dbeafe',",
|
|
2819
|
+
" borderColor: '#93c5fd',",
|
|
2820
|
+
' },',
|
|
2821
|
+
' lightLabel: {',
|
|
2822
|
+
" color: '#0f172a',",
|
|
2823
|
+
' },',
|
|
2824
|
+
'});',
|
|
2825
|
+
'',
|
|
2826
|
+
].join('\n');
|
|
2827
|
+
}
|
|
2828
|
+
function renderNativeWindUiToggle() {
|
|
2829
|
+
return [
|
|
2830
|
+
"import { Switch } from 'react-native';",
|
|
2831
|
+
'',
|
|
2832
|
+
'export interface ToggleProps {',
|
|
2833
|
+
' value: boolean;',
|
|
2834
|
+
' onValueChange?: (next: boolean) => void;',
|
|
2835
|
+
'}',
|
|
2836
|
+
'',
|
|
2837
|
+
'export function Toggle({ value, onValueChange }: ToggleProps) {',
|
|
2838
|
+
' return (',
|
|
2839
|
+
' <Switch',
|
|
2840
|
+
' value={value}',
|
|
2841
|
+
' onValueChange={onValueChange}',
|
|
2842
|
+
' trackColor={{ false: "#cbd5e1", true: "#93c5fd" }}',
|
|
2843
|
+
' thumbColor={value ? "#2563eb" : "#f8fafc"}',
|
|
2844
|
+
' />',
|
|
2845
|
+
' );',
|
|
2846
|
+
'}',
|
|
2847
|
+
'',
|
|
2848
|
+
].join('\n');
|
|
2849
|
+
}
|
|
2850
|
+
function renderNativeWindUiScreen() {
|
|
2851
|
+
return [
|
|
2852
|
+
"import { useMemo, useState } from 'react';",
|
|
2853
|
+
"import { Linking, Platform, ScrollView, StyleSheet, View } from 'react-native';",
|
|
2854
|
+
'',
|
|
2855
|
+
"import { ActivityIndicator } from '../../components/nativewindui/ActivityIndicator';",
|
|
2856
|
+
"import { Avatar, AvatarFallback } from '../../components/nativewindui/Avatar';",
|
|
2857
|
+
"import { Button } from '../../components/nativewindui/Button';",
|
|
2858
|
+
"import { DatePicker } from '../../components/nativewindui/DatePicker';",
|
|
2859
|
+
"import { Picker, PickerItem } from '../../components/nativewindui/Picker';",
|
|
2860
|
+
"import { ProgressIndicator } from '../../components/nativewindui/ProgressIndicator';",
|
|
2861
|
+
"import { Slider } from '../../components/nativewindui/Slider';",
|
|
2862
|
+
"import { Text } from '../../components/nativewindui/Text';",
|
|
2863
|
+
"import { ThemeToggle } from '../../components/nativewindui/ThemeToggle';",
|
|
2864
|
+
"import { Toggle } from '../../components/nativewindui/Toggle';",
|
|
2865
|
+
"import { ExpositionNotice } from '../../components/exposition';",
|
|
2866
|
+
"import { useAppTheme } from '../../theme/provider';",
|
|
2867
|
+
'',
|
|
2868
|
+
'export default function NativeWindUiScreen() {',
|
|
2869
|
+
' const theme = useAppTheme();',
|
|
2870
|
+
' const colors = theme.activeColors;',
|
|
2871
|
+
' const [enabled, setEnabled] = useState(true);',
|
|
2872
|
+
' const [intensity, setIntensity] = useState(0.64);',
|
|
2873
|
+
" const [density, setDensity] = useState('balanced');",
|
|
2874
|
+
' const [appointmentDate, setAppointmentDate] = useState<Date>(new Date());',
|
|
2875
|
+
' const progress = useMemo(() => Math.round(intensity * 100), [intensity]);',
|
|
2876
|
+
'',
|
|
2877
|
+
' return (',
|
|
2878
|
+
' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={[styles.screen, { backgroundColor: colors.background }]}>',
|
|
2879
|
+
' <View style={styles.header}>',
|
|
2880
|
+
' <Text variant="largeTitle" className="font-black text-slate-950 dark:text-white">NativeWindUI Exposition</Text>',
|
|
2881
|
+
' <Text variant="body" color="secondary">Generated when NativeWindUI is selected; this page exercises the local NativeWindUI primitives that create-expo-stack installs.</Text>',
|
|
2882
|
+
' </View>',
|
|
2883
|
+
' <ExpositionNotice />',
|
|
2884
|
+
' <View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.primary, borderRadius: theme.layout.radius }]}>',
|
|
2885
|
+
' <Text variant="heading">Interactive primitives</Text>',
|
|
2886
|
+
' <View style={styles.feedbackRow}>',
|
|
2887
|
+
' <Avatar className="h-12 w-12">',
|
|
2888
|
+
' <AvatarFallback>',
|
|
2889
|
+
' <Text variant="caption2">NW</Text>',
|
|
2890
|
+
' </AvatarFallback>',
|
|
2891
|
+
' </Avatar>',
|
|
2892
|
+
' <View style={styles.feedbackBody}>',
|
|
2893
|
+
' <Text variant="subhead">Theme preview controls</Text>',
|
|
2894
|
+
' <Text variant="footnote" color="secondary">Avatar and ThemeToggle are local NativeWindUI primitives.</Text>',
|
|
2895
|
+
' </View>',
|
|
2896
|
+
' <ThemeToggle />',
|
|
2897
|
+
' </View>',
|
|
2898
|
+
' <View style={styles.row}>',
|
|
2899
|
+
' <Button onPress={() => Linking.openURL(\'https://nativewindui.com\')} variant="primary">',
|
|
2900
|
+
' <Text>Open NativeWindUI docs</Text>',
|
|
2901
|
+
' </Button>',
|
|
2902
|
+
' <Button variant="tonal">',
|
|
2903
|
+
' <Text>{density}</Text>',
|
|
2904
|
+
' </Button>',
|
|
2905
|
+
' </View>',
|
|
2906
|
+
' <View style={styles.controlRow}>',
|
|
2907
|
+
' <Text variant="callout">Enable generated theme bridge</Text>',
|
|
2908
|
+
' <Toggle value={enabled} onValueChange={setEnabled} />',
|
|
2909
|
+
' </View>',
|
|
2910
|
+
' <Slider value={intensity} onValueChange={setIntensity} />',
|
|
2911
|
+
' <ProgressIndicator value={progress} />',
|
|
2912
|
+
' <Text variant="footnote" color="secondary">Progress {progress}% - Toggle {enabled ? \'on\' : \'off\'}</Text>',
|
|
2913
|
+
' </View>',
|
|
2914
|
+
' <View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.primary, borderRadius: theme.layout.radius }]}>',
|
|
2915
|
+
' <Text variant="heading">Picker, DatePicker, and feedback</Text>',
|
|
2916
|
+
' <Picker selectedValue={density} onValueChange={(value) => setDensity(String(value))}>',
|
|
2917
|
+
' <PickerItem label="Compact density" value="compact" />',
|
|
2918
|
+
' <PickerItem label="Balanced density" value="balanced" />',
|
|
2919
|
+
' <PickerItem label="Spacious density" value="spacious" />',
|
|
2920
|
+
' </Picker>',
|
|
2921
|
+
' {Platform.OS !== "web" ? (',
|
|
2922
|
+
' <DatePicker mode="date" value={appointmentDate} onChange={(_event, selected) => selected && setAppointmentDate(selected)} />',
|
|
2923
|
+
' ) : (',
|
|
2924
|
+
' <Text variant="footnote" color="secondary">DatePicker preview appears on native targets.</Text>',
|
|
2925
|
+
' )}',
|
|
2926
|
+
' <View style={styles.feedbackRow}>',
|
|
2927
|
+
' <ActivityIndicator />',
|
|
2928
|
+
' <View style={styles.feedbackBody}>',
|
|
2929
|
+
' <Text variant="subhead" color="secondary">NativeWind class tokens, generated theme colors, and Expo web are rendering together.</Text>',
|
|
2930
|
+
' <Text variant="footnote" color="secondary">Date: {appointmentDate.toDateString()}</Text>',
|
|
2931
|
+
' </View>',
|
|
2932
|
+
' </View>',
|
|
2933
|
+
' </View>',
|
|
2934
|
+
' </ScrollView>',
|
|
2935
|
+
' );',
|
|
2936
|
+
'}',
|
|
2937
|
+
'',
|
|
2938
|
+
'const styles = StyleSheet.create({',
|
|
2939
|
+
' screen: {',
|
|
2940
|
+
" backgroundColor: '#f8fafc',",
|
|
2941
|
+
' flex: 1,',
|
|
2942
|
+
' },',
|
|
2943
|
+
' content: {',
|
|
2944
|
+
' gap: 16,',
|
|
2945
|
+
' padding: 20,',
|
|
2946
|
+
' paddingTop: 84,',
|
|
2947
|
+
' },',
|
|
2948
|
+
' header: {',
|
|
2949
|
+
' gap: 8,',
|
|
2950
|
+
' },',
|
|
2951
|
+
' card: {',
|
|
2952
|
+
' borderWidth: 1,',
|
|
2953
|
+
' gap: 16,',
|
|
2954
|
+
' padding: 16,',
|
|
2955
|
+
' },',
|
|
2956
|
+
' row: {',
|
|
2957
|
+
" flexDirection: 'row',",
|
|
2958
|
+
" flexWrap: 'wrap',",
|
|
2959
|
+
' gap: 10,',
|
|
2960
|
+
' },',
|
|
2961
|
+
' controlRow: {',
|
|
2962
|
+
" alignItems: 'center',",
|
|
2963
|
+
" flexDirection: 'row',",
|
|
2964
|
+
' gap: 12,',
|
|
2965
|
+
" justifyContent: 'space-between',",
|
|
2966
|
+
' },',
|
|
2967
|
+
' feedbackRow: {',
|
|
2968
|
+
" alignItems: 'center',",
|
|
2969
|
+
" flexDirection: 'row',",
|
|
2970
|
+
' gap: 12,',
|
|
2971
|
+
' },',
|
|
2972
|
+
' feedbackBody: {',
|
|
2973
|
+
' flex: 1,',
|
|
2974
|
+
' gap: 4,',
|
|
2975
|
+
' },',
|
|
2976
|
+
'});',
|
|
2977
|
+
'',
|
|
2978
|
+
].join('\n');
|
|
2979
|
+
}
|
|
2980
|
+
function renderHomeScreen(answers, navigationShell) {
|
|
2981
|
+
const includeNativeWindUiExposition = answers.defaults.includes('nativewindui');
|
|
2982
|
+
const expositionLinks = navigationShell.library === 'expo-router' && navigationShell.layout !== 'stack'
|
|
2983
|
+
? includeNativeWindUiExposition
|
|
2984
|
+
? [
|
|
2985
|
+
" { href: '/exposition/nativewindui' as const, title: 'NativeWindUI', body: 'Explore the bundled NativeWindUI components.' },",
|
|
2986
|
+
]
|
|
2987
|
+
: []
|
|
2988
|
+
: [
|
|
2989
|
+
" { href: '/exposition' as const, title: 'Exposition', body: 'Review included Software Mansion packages and decide what stays.' },",
|
|
2990
|
+
" { href: '/exposition/stylist' as const, title: 'Stylist', body: 'Test colors, type, motion, and component density.' },",
|
|
2991
|
+
" { href: '/exposition/data' as const, title: 'Data adapter', body: 'Try the local data boundary before replacing it.' },",
|
|
2992
|
+
" { href: '/exposition/sdk-56' as const, title: 'Expo SDK 56', body: 'Review the new Expo UI, Router, module, and performance changes.' },",
|
|
2993
|
+
...(includeNativeWindUiExposition
|
|
2994
|
+
? [
|
|
2995
|
+
" { href: '/exposition/nativewindui' as const, title: 'NativeWindUI', body: 'Explore the bundled NativeWindUI components.' },",
|
|
2996
|
+
]
|
|
2997
|
+
: []),
|
|
2998
|
+
];
|
|
2999
|
+
return [
|
|
3000
|
+
"import { Link, type Href } from 'expo-router';",
|
|
3001
|
+
"import { Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';",
|
|
3002
|
+
'',
|
|
3003
|
+
"import { SvgMark } from '../../components/exposition';",
|
|
3004
|
+
"import { appSnapshot } from '../../data/mock-app';",
|
|
3005
|
+
"import { useAppTheme } from '../../theme/provider';",
|
|
3006
|
+
'',
|
|
3007
|
+
'const expositionLinks: Array<{ href: Href; title: string; body: string }> = [',
|
|
3008
|
+
...expositionLinks,
|
|
3009
|
+
'];',
|
|
3010
|
+
'',
|
|
3011
|
+
'export default function HomeScreen() {',
|
|
3012
|
+
' const theme = useAppTheme();',
|
|
3013
|
+
' const colors = theme.activeColors;',
|
|
3014
|
+
'',
|
|
3015
|
+
' return (',
|
|
3016
|
+
' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={[styles.screen, { backgroundColor: colors.background }]}>',
|
|
3017
|
+
' <View style={styles.header}>',
|
|
3018
|
+
' <View style={styles.brandLockup}>',
|
|
3019
|
+
' <SvgMark size={64} />',
|
|
3020
|
+
' <View style={styles.brandText}>',
|
|
3021
|
+
' <Text style={[styles.brandLine, { color: colors.text }]}>Super</Text>',
|
|
3022
|
+
' <Text style={[styles.brandLine, { color: colors.text }]}>Stack</Text>',
|
|
3023
|
+
' </View>',
|
|
3024
|
+
' </View>',
|
|
3025
|
+
' <View style={styles.headerText}>',
|
|
3026
|
+
` <Text style={[styles.title, { color: colors.text, fontFamily: theme.typography.fontFamily, fontWeight: theme.typography.fontFamily === 'System' || theme.typography.fontFamily === 'monospace' ? '800' : 'normal' }]}>${answers.appName}</Text>`,
|
|
3027
|
+
' <Text style={[styles.subtitle, { color: colors.text }]}>{appSnapshot.audience}</Text>',
|
|
3028
|
+
' </View>',
|
|
3029
|
+
' {Platform.OS === "web" ? (',
|
|
3030
|
+
' <Link href="/settings" asChild>',
|
|
3031
|
+
' <Pressable accessibilityRole="button" style={StyleSheet.flatten([styles.infoButton, { backgroundColor: colors.primary }])}>',
|
|
3032
|
+
' <Text style={styles.infoButtonText}>i</Text>',
|
|
3033
|
+
' </Pressable>',
|
|
3034
|
+
' </Link>',
|
|
3035
|
+
' ) : null}',
|
|
1565
3036
|
' </View>',
|
|
1566
|
-
' <GestureCard',
|
|
1567
|
-
' title="Rich boilerplate is wired"',
|
|
1568
|
-
' body="Routes stay thin, feature screens hold UI, and the temporary exposition pages are reachable from this home screen."',
|
|
1569
|
-
' />',
|
|
1570
3037
|
' <View style={styles.grid}>',
|
|
1571
3038
|
' <Link href="/onboarding" asChild>',
|
|
1572
|
-
' <Pressable style={styles.primaryCard}>',
|
|
3039
|
+
' <Pressable style={StyleSheet.flatten([styles.primaryCard, { backgroundColor: colors.primary, borderRadius: theme.layout.radius }])}>',
|
|
1573
3040
|
' <Text style={styles.primaryTitle}>Onboarding preview</Text>',
|
|
1574
3041
|
' <Text style={styles.primaryBody}>Open the generated onboarding screen before the main product flow replaces it.</Text>',
|
|
1575
3042
|
' </Pressable>',
|
|
1576
3043
|
' </Link>',
|
|
1577
3044
|
' {expositionLinks.map((item) => (',
|
|
1578
|
-
' <Link key={item.href} href={item.href} asChild>',
|
|
1579
|
-
' <Pressable style={styles.linkCard}>',
|
|
1580
|
-
' <Text style={styles.linkTitle}>{item.title}</Text>',
|
|
1581
|
-
' <Text style={styles.linkBody}>{item.body}</Text>',
|
|
3045
|
+
' <Link key={String(item.href)} href={item.href} asChild>',
|
|
3046
|
+
' <Pressable style={StyleSheet.flatten([styles.linkCard, { backgroundColor: colors.surface, borderColor: colors.primary, borderRadius: theme.layout.radius }])}>',
|
|
3047
|
+
' <Text style={[styles.linkTitle, { color: colors.text }]}>{item.title}</Text>',
|
|
3048
|
+
' <Text style={[styles.linkBody, { color: colors.text }]}>{item.body}</Text>',
|
|
1582
3049
|
' </Pressable>',
|
|
1583
3050
|
' </Link>',
|
|
1584
3051
|
' ))}',
|
|
1585
3052
|
' </View>',
|
|
1586
|
-
' <View style={styles.taskList}>',
|
|
1587
|
-
' <Text style={styles.sectionTitle}>Generated next steps</Text>',
|
|
1588
|
-
' {appSnapshot.tasks.map((task) => (',
|
|
1589
|
-
' <View key={task.id} style={styles.taskCard}>',
|
|
1590
|
-
' <Text style={styles.taskTitle}>{task.title}</Text>',
|
|
1591
|
-
' <Text style={styles.taskStatus}>{task.status}</Text>',
|
|
1592
|
-
' </View>',
|
|
1593
|
-
' ))}',
|
|
1594
|
-
' </View>',
|
|
1595
3053
|
' </ScrollView>',
|
|
1596
3054
|
' );',
|
|
1597
3055
|
'}',
|
|
@@ -1602,16 +3060,35 @@ function renderHomeScreen(answers) {
|
|
|
1602
3060
|
' flex: 1,',
|
|
1603
3061
|
' },',
|
|
1604
3062
|
' content: {',
|
|
3063
|
+
' flexGrow: 1,',
|
|
1605
3064
|
' gap: 16,',
|
|
3065
|
+
' justifyContent: "center",',
|
|
1606
3066
|
' padding: 20,',
|
|
3067
|
+
' paddingTop: Platform.OS === "web" ? 84 : 20,',
|
|
1607
3068
|
' },',
|
|
1608
3069
|
' header: {',
|
|
1609
3070
|
' alignItems: "center",',
|
|
3071
|
+
' gap: 10,',
|
|
3072
|
+
' position: "relative",',
|
|
3073
|
+
' },',
|
|
3074
|
+
' brandLockup: {',
|
|
3075
|
+
' alignItems: "center",',
|
|
1610
3076
|
' flexDirection: "row",',
|
|
1611
|
-
' gap:
|
|
3077
|
+
' gap: 14,',
|
|
3078
|
+
' justifyContent: "center",',
|
|
1612
3079
|
' },',
|
|
1613
3080
|
' headerText: {',
|
|
1614
|
-
'
|
|
3081
|
+
' alignItems: "center",',
|
|
3082
|
+
' width: "100%",',
|
|
3083
|
+
' },',
|
|
3084
|
+
' brandText: {',
|
|
3085
|
+
' gap: 0,',
|
|
3086
|
+
' },',
|
|
3087
|
+
' brandLine: {',
|
|
3088
|
+
' fontSize: 16,',
|
|
3089
|
+
' fontWeight: "900",',
|
|
3090
|
+
' lineHeight: 17,',
|
|
3091
|
+
' textTransform: "uppercase",',
|
|
1615
3092
|
' },',
|
|
1616
3093
|
' infoButton: {',
|
|
1617
3094
|
' alignItems: "center",',
|
|
@@ -1619,6 +3096,9 @@ function renderHomeScreen(answers) {
|
|
|
1619
3096
|
' borderRadius: 18,',
|
|
1620
3097
|
' height: 36,',
|
|
1621
3098
|
' justifyContent: "center",',
|
|
3099
|
+
' position: "absolute",',
|
|
3100
|
+
' right: 0,',
|
|
3101
|
+
' top: 0,',
|
|
1622
3102
|
' width: 36,',
|
|
1623
3103
|
' },',
|
|
1624
3104
|
' infoButtonText: {',
|
|
@@ -1627,99 +3107,629 @@ function renderHomeScreen(answers) {
|
|
|
1627
3107
|
' fontWeight: "800",',
|
|
1628
3108
|
' },',
|
|
1629
3109
|
' title: {',
|
|
1630
|
-
" color: '#111827',",
|
|
1631
|
-
' fontSize: 22,',
|
|
3110
|
+
" color: '#111827',",
|
|
3111
|
+
' fontSize: 22,',
|
|
3112
|
+
' fontWeight: "800",',
|
|
3113
|
+
' textAlign: "center",',
|
|
3114
|
+
' },',
|
|
3115
|
+
' subtitle: {',
|
|
3116
|
+
" color: '#4b5563',",
|
|
3117
|
+
' fontSize: 14,',
|
|
3118
|
+
' marginTop: 3,',
|
|
3119
|
+
' textAlign: "center",',
|
|
3120
|
+
' },',
|
|
3121
|
+
' grid: {',
|
|
3122
|
+
' gap: 12,',
|
|
3123
|
+
' },',
|
|
3124
|
+
' primaryCard: {',
|
|
3125
|
+
" backgroundColor: '#111827',",
|
|
3126
|
+
' borderRadius: 12,',
|
|
3127
|
+
' gap: 8,',
|
|
3128
|
+
' padding: 16,',
|
|
3129
|
+
' },',
|
|
3130
|
+
' primaryTitle: {',
|
|
3131
|
+
" color: '#ffffff',",
|
|
3132
|
+
' fontSize: 18,',
|
|
3133
|
+
' fontWeight: "800",',
|
|
3134
|
+
' },',
|
|
3135
|
+
' primaryBody: {',
|
|
3136
|
+
" color: '#d1d5db',",
|
|
3137
|
+
' fontSize: 14,',
|
|
3138
|
+
' lineHeight: 20,',
|
|
3139
|
+
' },',
|
|
3140
|
+
' linkCard: {',
|
|
3141
|
+
" backgroundColor: '#ffffff',",
|
|
3142
|
+
" borderColor: '#e5e7eb',",
|
|
3143
|
+
' borderRadius: 12,',
|
|
3144
|
+
' borderWidth: 1,',
|
|
3145
|
+
' gap: 6,',
|
|
3146
|
+
' padding: 16,',
|
|
3147
|
+
' },',
|
|
3148
|
+
' linkTitle: {',
|
|
3149
|
+
" color: '#111827',",
|
|
3150
|
+
' fontSize: 16,',
|
|
3151
|
+
' fontWeight: "800",',
|
|
3152
|
+
' },',
|
|
3153
|
+
' linkBody: {',
|
|
3154
|
+
" color: '#4b5563',",
|
|
3155
|
+
' fontSize: 14,',
|
|
3156
|
+
' lineHeight: 20,',
|
|
3157
|
+
' },',
|
|
3158
|
+
'});',
|
|
3159
|
+
'',
|
|
3160
|
+
].join('\n');
|
|
3161
|
+
}
|
|
3162
|
+
function renderOnboardingScreen() {
|
|
3163
|
+
return [
|
|
3164
|
+
"import { Link, useRouter } from 'expo-router';",
|
|
3165
|
+
"import { useMemo, useState } from 'react';",
|
|
3166
|
+
"import { Pressable, StyleSheet, Switch, Text, View } from 'react-native';",
|
|
3167
|
+
'',
|
|
3168
|
+
"import { onboardingLegalDocuments } from './legal-documents';",
|
|
3169
|
+
'',
|
|
3170
|
+
'export default function OnboardingScreen() {',
|
|
3171
|
+
' const [acceptedAgreement, setAcceptedAgreement] = useState(false);',
|
|
3172
|
+
' const [acceptedTerms, setAcceptedTerms] = useState(false);',
|
|
3173
|
+
' const router = useRouter();',
|
|
3174
|
+
' const canContinue = acceptedAgreement && acceptedTerms;',
|
|
3175
|
+
' const agreementUpdated = useMemo(() => new Date(onboardingLegalDocuments.agreement.lastUpdated).toLocaleDateString(), []);',
|
|
3176
|
+
' const termsUpdated = useMemo(() => new Date(onboardingLegalDocuments.terms.lastUpdated).toLocaleDateString(), []);',
|
|
3177
|
+
'',
|
|
3178
|
+
' return (',
|
|
3179
|
+
' <View style={styles.screen}>',
|
|
3180
|
+
' <Text style={styles.title}>Legal onboarding</Text>',
|
|
3181
|
+
' <Text style={styles.body}>Review and approve the Agreement and Terms before continuing in your real auth or profile flow.</Text>',
|
|
3182
|
+
' <View style={styles.card}>',
|
|
3183
|
+
' <View style={styles.rowTop}>',
|
|
3184
|
+
' <Text style={styles.cardTitle}>Agreement</Text>',
|
|
3185
|
+
' <Text style={styles.meta}>{agreementUpdated}</Text>',
|
|
3186
|
+
' </View>',
|
|
3187
|
+
' <Text style={styles.cardBody}>A compact starter agreement with fill-in fields your team can finalize.</Text>',
|
|
3188
|
+
' <View style={styles.rowBottom}>',
|
|
3189
|
+
' <Link href="/onboarding/agreement" asChild>',
|
|
3190
|
+
' <Pressable accessibilityRole="button" style={styles.linkButton}>',
|
|
3191
|
+
' <Text style={styles.linkButtonText}>View agreement</Text>',
|
|
3192
|
+
' </Pressable>',
|
|
3193
|
+
' </Link>',
|
|
3194
|
+
' <View style={styles.acceptWrap}>',
|
|
3195
|
+
' <Text style={styles.acceptText}>Accepted</Text>',
|
|
3196
|
+
' <Switch value={acceptedAgreement} onValueChange={setAcceptedAgreement} />',
|
|
3197
|
+
' </View>',
|
|
3198
|
+
' </View>',
|
|
3199
|
+
' </View>',
|
|
3200
|
+
' <View style={styles.card}>',
|
|
3201
|
+
' <View style={styles.rowTop}>',
|
|
3202
|
+
' <Text style={styles.cardTitle}>Terms of service</Text>',
|
|
3203
|
+
' <Text style={styles.meta}>{termsUpdated}</Text>',
|
|
3204
|
+
' </View>',
|
|
3205
|
+
' <Text style={styles.cardBody}>Production-safe baseline terms with placeholders for business specifics.</Text>',
|
|
3206
|
+
' <View style={styles.rowBottom}>',
|
|
3207
|
+
' <Link href="/onboarding/terms" asChild>',
|
|
3208
|
+
' <Pressable accessibilityRole="button" style={styles.linkButton}>',
|
|
3209
|
+
' <Text style={styles.linkButtonText}>View terms</Text>',
|
|
3210
|
+
' </Pressable>',
|
|
3211
|
+
' </Link>',
|
|
3212
|
+
' <View style={styles.acceptWrap}>',
|
|
3213
|
+
' <Text style={styles.acceptText}>Accepted</Text>',
|
|
3214
|
+
' <Switch value={acceptedTerms} onValueChange={setAcceptedTerms} />',
|
|
3215
|
+
' </View>',
|
|
3216
|
+
' </View>',
|
|
3217
|
+
' </View>',
|
|
3218
|
+
' <Pressable',
|
|
3219
|
+
' accessibilityRole="button"',
|
|
3220
|
+
' disabled={!canContinue}',
|
|
3221
|
+
' onPress={() => {',
|
|
3222
|
+
' if (canContinue) router.push("/onboarding/account-setup");',
|
|
3223
|
+
' }}',
|
|
3224
|
+
' style={[styles.ctaButton, !canContinue && styles.ctaButtonDisabled]}',
|
|
3225
|
+
' >',
|
|
3226
|
+
' <Text style={styles.ctaButtonText}>Continue to account setup</Text>',
|
|
3227
|
+
' </Pressable>',
|
|
3228
|
+
' </View>',
|
|
3229
|
+
' );',
|
|
3230
|
+
'}',
|
|
3231
|
+
'',
|
|
3232
|
+
'const styles = StyleSheet.create({',
|
|
3233
|
+
' screen: {',
|
|
3234
|
+
" backgroundColor: '#ffffff',",
|
|
3235
|
+
' flex: 1,',
|
|
3236
|
+
' gap: 14,',
|
|
3237
|
+
' padding: 20,',
|
|
3238
|
+
' },',
|
|
3239
|
+
' title: {',
|
|
3240
|
+
" color: '#111827',",
|
|
3241
|
+
' fontSize: 26,',
|
|
3242
|
+
' fontWeight: "800",',
|
|
3243
|
+
' },',
|
|
3244
|
+
' body: {',
|
|
3245
|
+
" color: '#4b5563',",
|
|
3246
|
+
' fontSize: 15,',
|
|
3247
|
+
' lineHeight: 22,',
|
|
3248
|
+
' },',
|
|
3249
|
+
' card: {',
|
|
3250
|
+
" backgroundColor: '#ffffff',",
|
|
3251
|
+
" borderColor: '#d1d5db',",
|
|
3252
|
+
' borderRadius: 12,',
|
|
3253
|
+
' borderWidth: 1,',
|
|
3254
|
+
' gap: 8,',
|
|
3255
|
+
' padding: 14,',
|
|
3256
|
+
' },',
|
|
3257
|
+
' rowTop: {',
|
|
3258
|
+
' alignItems: "center",',
|
|
3259
|
+
' flexDirection: "row",',
|
|
3260
|
+
' justifyContent: "space-between",',
|
|
3261
|
+
' },',
|
|
3262
|
+
' rowBottom: {',
|
|
3263
|
+
' alignItems: "center",',
|
|
3264
|
+
' flexDirection: "row",',
|
|
3265
|
+
' justifyContent: "space-between",',
|
|
3266
|
+
' },',
|
|
3267
|
+
' cardTitle: {',
|
|
3268
|
+
" color: '#111827',",
|
|
3269
|
+
' fontSize: 18,',
|
|
3270
|
+
' fontWeight: "800",',
|
|
3271
|
+
' },',
|
|
3272
|
+
' cardBody: {',
|
|
3273
|
+
" color: '#4b5563',",
|
|
3274
|
+
' fontSize: 14,',
|
|
3275
|
+
' lineHeight: 20,',
|
|
3276
|
+
' },',
|
|
3277
|
+
' meta: {',
|
|
3278
|
+
" color: '#6b7280',",
|
|
3279
|
+
' fontSize: 12,',
|
|
3280
|
+
' fontWeight: "700",',
|
|
3281
|
+
' },',
|
|
3282
|
+
' linkButton: {',
|
|
3283
|
+
" backgroundColor: '#111827',",
|
|
3284
|
+
' borderRadius: 9,',
|
|
3285
|
+
' paddingHorizontal: 12,',
|
|
3286
|
+
' paddingVertical: 8,',
|
|
3287
|
+
' },',
|
|
3288
|
+
' linkButtonText: {',
|
|
3289
|
+
" color: '#ffffff',",
|
|
3290
|
+
' fontSize: 13,',
|
|
3291
|
+
' fontWeight: "700",',
|
|
3292
|
+
' },',
|
|
3293
|
+
' acceptWrap: {',
|
|
3294
|
+
' alignItems: "center",',
|
|
3295
|
+
' flexDirection: "row",',
|
|
3296
|
+
' gap: 8,',
|
|
3297
|
+
' },',
|
|
3298
|
+
' acceptText: {',
|
|
3299
|
+
" color: '#111827',",
|
|
3300
|
+
' fontSize: 13,',
|
|
3301
|
+
' fontWeight: "700",',
|
|
3302
|
+
' },',
|
|
3303
|
+
' ctaButton: {',
|
|
3304
|
+
' alignItems: "center",',
|
|
3305
|
+
" backgroundColor: '#0f172a',",
|
|
3306
|
+
' borderRadius: 12,',
|
|
3307
|
+
' marginTop: "auto",',
|
|
3308
|
+
' paddingVertical: 14,',
|
|
3309
|
+
' },',
|
|
3310
|
+
' ctaButtonDisabled: {',
|
|
3311
|
+
" backgroundColor: '#9ca3af',",
|
|
3312
|
+
' },',
|
|
3313
|
+
' ctaButtonText: {',
|
|
3314
|
+
" color: '#ffffff',",
|
|
3315
|
+
' fontSize: 15,',
|
|
3316
|
+
' fontWeight: "800",',
|
|
3317
|
+
' },',
|
|
3318
|
+
'});',
|
|
3319
|
+
'',
|
|
3320
|
+
].join('\n');
|
|
3321
|
+
}
|
|
3322
|
+
function renderAccountSetupScreen() {
|
|
3323
|
+
return [
|
|
3324
|
+
"import { useRouter } from 'expo-router';",
|
|
3325
|
+
"import { Pressable, StyleSheet, Text, View } from 'react-native';",
|
|
3326
|
+
'',
|
|
3327
|
+
'export default function AccountSetupScreen() {',
|
|
3328
|
+
' const router = useRouter();',
|
|
3329
|
+
'',
|
|
3330
|
+
' return (',
|
|
3331
|
+
' <View style={styles.screen}>',
|
|
3332
|
+
' <Text style={styles.title}>Account setup</Text>',
|
|
3333
|
+
' <Text style={styles.body}>This is the production-ready handoff point after legal acceptance. Replace this with your real auth and profile onboarding flow.</Text>',
|
|
3334
|
+
' <Pressable',
|
|
3335
|
+
' accessibilityRole="button"',
|
|
3336
|
+
" onPress={() => router.replace('/')}",
|
|
3337
|
+
' style={styles.homeButton}>',
|
|
3338
|
+
' <Text style={styles.homeButtonText}>Continue to home</Text>',
|
|
3339
|
+
' </Pressable>',
|
|
3340
|
+
' </View>',
|
|
3341
|
+
' );',
|
|
3342
|
+
'}',
|
|
3343
|
+
'',
|
|
3344
|
+
'const styles = StyleSheet.create({',
|
|
3345
|
+
' screen: {',
|
|
3346
|
+
" backgroundColor: '#ffffff',",
|
|
3347
|
+
' flex: 1,',
|
|
3348
|
+
' gap: 12,',
|
|
3349
|
+
' padding: 20,',
|
|
3350
|
+
' },',
|
|
3351
|
+
' title: {',
|
|
3352
|
+
" color: '#111827',",
|
|
3353
|
+
' fontSize: 26,',
|
|
3354
|
+
' fontWeight: "800",',
|
|
3355
|
+
' },',
|
|
3356
|
+
' body: {',
|
|
3357
|
+
" color: '#4b5563',",
|
|
3358
|
+
' fontSize: 15,',
|
|
3359
|
+
' lineHeight: 22,',
|
|
3360
|
+
' },',
|
|
3361
|
+
' homeButton: {',
|
|
3362
|
+
" alignItems: 'center',",
|
|
3363
|
+
" backgroundColor: '#2563eb',",
|
|
3364
|
+
' borderRadius: 12,',
|
|
3365
|
+
' marginTop: 12,',
|
|
3366
|
+
' paddingHorizontal: 18,',
|
|
3367
|
+
' paddingVertical: 14,',
|
|
3368
|
+
' },',
|
|
3369
|
+
' homeButtonText: {',
|
|
3370
|
+
" color: '#ffffff',",
|
|
3371
|
+
' fontSize: 16,',
|
|
3372
|
+
' fontWeight: "800",',
|
|
3373
|
+
' },',
|
|
3374
|
+
'});',
|
|
3375
|
+
'',
|
|
3376
|
+
].join('\n');
|
|
3377
|
+
}
|
|
3378
|
+
function renderLegalDocuments() {
|
|
3379
|
+
return [
|
|
3380
|
+
'export interface LegalDocumentSection {',
|
|
3381
|
+
' id: string;',
|
|
3382
|
+
' title: string;',
|
|
3383
|
+
' body: string;',
|
|
3384
|
+
'}',
|
|
3385
|
+
'',
|
|
3386
|
+
'export interface LegalDocument {',
|
|
3387
|
+
' id: "agreement" | "terms";',
|
|
3388
|
+
' title: string;',
|
|
3389
|
+
' summary: string;',
|
|
3390
|
+
' effectiveDate: string;',
|
|
3391
|
+
' lastUpdated: string;',
|
|
3392
|
+
' sections: LegalDocumentSection[];',
|
|
3393
|
+
'}',
|
|
3394
|
+
'',
|
|
3395
|
+
'export const onboardingLegalDocuments: Record<"agreement" | "terms", LegalDocument> = {',
|
|
3396
|
+
' agreement: {',
|
|
3397
|
+
' id: "agreement",',
|
|
3398
|
+
' title: "User Agreement",',
|
|
3399
|
+
' summary: "Agreement template for onboarding consent and account usage.",',
|
|
3400
|
+
' effectiveDate: "2026-05-24",',
|
|
3401
|
+
' lastUpdated: "2026-05-24",',
|
|
3402
|
+
' sections: [',
|
|
3403
|
+
' { id: "scope", title: "Scope", body: "This agreement covers access to [APP NAME], account conduct, and baseline obligations between [COMPANY NAME] and each user." },',
|
|
3404
|
+
' { id: "usage", title: "Acceptable Use", body: "Users agree not to misuse the service, attempt unauthorized access, or submit harmful content." },',
|
|
3405
|
+
' { id: "privacy", title: "Privacy and Data", body: "User data is handled according to the published privacy notice. Replace this section with your final privacy commitments and retention policy." },',
|
|
3406
|
+
' { id: "termination", title: "Termination", body: "Either party may terminate usage under the conditions described in this section. Add jurisdiction-specific language before production launch." },',
|
|
3407
|
+
' ],',
|
|
3408
|
+
' },',
|
|
3409
|
+
' terms: {',
|
|
3410
|
+
' id: "terms",',
|
|
3411
|
+
' title: "Terms of Service",',
|
|
3412
|
+
' summary: "Near-blank, production-oriented terms starter for legal review.",',
|
|
3413
|
+
' effectiveDate: "2026-05-24",',
|
|
3414
|
+
' lastUpdated: "2026-05-24",',
|
|
3415
|
+
' sections: [',
|
|
3416
|
+
' { id: "eligibility", title: "Eligibility", body: "Users must meet age and legal capacity requirements for their jurisdiction." },',
|
|
3417
|
+
' { id: "accounts", title: "Accounts", body: "Users are responsible for account credentials and activity performed through their account." },',
|
|
3418
|
+
' { id: "payments", title: "Payments and Billing", body: "If applicable, describe pricing, billing intervals, refunds, and failed payment handling." },',
|
|
3419
|
+
' { id: "liability", title: "Disclaimers and Liability", body: "Define limitations of liability and service disclaimers with legal counsel." },',
|
|
3420
|
+
' { id: "governing-law", title: "Governing Law", body: "Specify governing law, venue, and dispute resolution expectations." },',
|
|
3421
|
+
' ],',
|
|
3422
|
+
' },',
|
|
3423
|
+
'};',
|
|
3424
|
+
'',
|
|
3425
|
+
].join('\n');
|
|
3426
|
+
}
|
|
3427
|
+
function renderLegalDocumentView() {
|
|
3428
|
+
return [
|
|
3429
|
+
"import { ScrollView, StyleSheet, Text, View } from 'react-native';",
|
|
3430
|
+
'',
|
|
3431
|
+
"import type { LegalDocument } from '../legal-documents';",
|
|
3432
|
+
'',
|
|
3433
|
+
'interface LegalDocumentViewProps {',
|
|
3434
|
+
' document: LegalDocument;',
|
|
3435
|
+
'}',
|
|
3436
|
+
'',
|
|
3437
|
+
'function LegalDocumentMeta({ label, value }: { label: string; value: string }) {',
|
|
3438
|
+
' return (',
|
|
3439
|
+
' <View style={styles.metaItem}>',
|
|
3440
|
+
' <Text style={styles.metaLabel}>{label}</Text>',
|
|
3441
|
+
' <Text style={styles.metaValue}>{value}</Text>',
|
|
3442
|
+
' </View>',
|
|
3443
|
+
' );',
|
|
3444
|
+
'}',
|
|
3445
|
+
'',
|
|
3446
|
+
'function LegalSectionItem({ title, body }: { title: string; body: string }) {',
|
|
3447
|
+
' return (',
|
|
3448
|
+
' <View style={styles.section}>',
|
|
3449
|
+
' <Text style={styles.sectionTitle}>{title}</Text>',
|
|
3450
|
+
' <Text style={styles.sectionBody}>{body}</Text>',
|
|
3451
|
+
' </View>',
|
|
3452
|
+
' );',
|
|
3453
|
+
'}',
|
|
3454
|
+
'',
|
|
3455
|
+
'export function LegalDocumentView({ document }: LegalDocumentViewProps) {',
|
|
3456
|
+
' return (',
|
|
3457
|
+
' <ScrollView contentContainerStyle={styles.content} style={styles.screen}>',
|
|
3458
|
+
' <Text style={styles.title}>{document.title}</Text>',
|
|
3459
|
+
' <Text style={styles.summary}>{document.summary}</Text>',
|
|
3460
|
+
' <View style={styles.metaRow}>',
|
|
3461
|
+
' <LegalDocumentMeta label="Effective" value={document.effectiveDate} />',
|
|
3462
|
+
' <LegalDocumentMeta label="Last updated" value={document.lastUpdated} />',
|
|
3463
|
+
' </View>',
|
|
3464
|
+
' {document.sections.map((section) => (',
|
|
3465
|
+
' <LegalSectionItem key={section.id} title={section.title} body={section.body} />',
|
|
3466
|
+
' ))}',
|
|
3467
|
+
' </ScrollView>',
|
|
3468
|
+
' );',
|
|
3469
|
+
'}',
|
|
3470
|
+
'',
|
|
3471
|
+
'const styles = StyleSheet.create({',
|
|
3472
|
+
' screen: {',
|
|
3473
|
+
" backgroundColor: '#f8fafc',",
|
|
3474
|
+
' flex: 1,',
|
|
3475
|
+
' },',
|
|
3476
|
+
' content: {',
|
|
3477
|
+
' gap: 14,',
|
|
3478
|
+
' padding: 20,',
|
|
3479
|
+
' paddingTop: 84,',
|
|
3480
|
+
' },',
|
|
3481
|
+
' title: {',
|
|
3482
|
+
" color: '#0f172a',",
|
|
3483
|
+
' fontSize: 28,',
|
|
1632
3484
|
' fontWeight: "800",',
|
|
1633
3485
|
' },',
|
|
1634
|
-
'
|
|
1635
|
-
" color: '#
|
|
1636
|
-
' fontSize:
|
|
1637
|
-
'
|
|
3486
|
+
' summary: {',
|
|
3487
|
+
" color: '#334155',",
|
|
3488
|
+
' fontSize: 15,',
|
|
3489
|
+
' lineHeight: 22,',
|
|
1638
3490
|
' },',
|
|
1639
|
-
'
|
|
1640
|
-
'
|
|
3491
|
+
' metaRow: {',
|
|
3492
|
+
' flexDirection: "row",',
|
|
3493
|
+
' gap: 10,',
|
|
1641
3494
|
' },',
|
|
1642
|
-
'
|
|
1643
|
-
" backgroundColor: '#
|
|
1644
|
-
' borderRadius:
|
|
1645
|
-
' gap:
|
|
1646
|
-
'
|
|
3495
|
+
' metaItem: {',
|
|
3496
|
+
" backgroundColor: '#e2e8f0',",
|
|
3497
|
+
' borderRadius: 10,',
|
|
3498
|
+
' gap: 2,',
|
|
3499
|
+
' paddingHorizontal: 10,',
|
|
3500
|
+
' paddingVertical: 8,',
|
|
1647
3501
|
' },',
|
|
1648
|
-
'
|
|
1649
|
-
" color: '#
|
|
1650
|
-
' fontSize:
|
|
1651
|
-
' fontWeight: "
|
|
3502
|
+
' metaLabel: {',
|
|
3503
|
+
" color: '#475569',",
|
|
3504
|
+
' fontSize: 11,',
|
|
3505
|
+
' fontWeight: "700",',
|
|
3506
|
+
' textTransform: "uppercase",',
|
|
1652
3507
|
' },',
|
|
1653
|
-
'
|
|
1654
|
-
" color: '#
|
|
1655
|
-
' fontSize:
|
|
1656
|
-
'
|
|
3508
|
+
' metaValue: {',
|
|
3509
|
+
" color: '#0f172a',",
|
|
3510
|
+
' fontSize: 13,',
|
|
3511
|
+
' fontWeight: "700",',
|
|
1657
3512
|
' },',
|
|
1658
|
-
'
|
|
3513
|
+
' section: {',
|
|
1659
3514
|
" backgroundColor: '#ffffff',",
|
|
1660
|
-
" borderColor: '#
|
|
3515
|
+
" borderColor: '#e2e8f0',",
|
|
1661
3516
|
' borderRadius: 12,',
|
|
1662
3517
|
' borderWidth: 1,',
|
|
1663
|
-
' gap:
|
|
1664
|
-
' padding:
|
|
1665
|
-
' },',
|
|
1666
|
-
' linkTitle: {',
|
|
1667
|
-
" color: '#111827',",
|
|
1668
|
-
' fontSize: 16,',
|
|
1669
|
-
' fontWeight: "800",',
|
|
1670
|
-
' },',
|
|
1671
|
-
' linkBody: {',
|
|
1672
|
-
" color: '#4b5563',",
|
|
1673
|
-
' fontSize: 14,',
|
|
1674
|
-
' lineHeight: 20,',
|
|
1675
|
-
' },',
|
|
1676
|
-
' taskList: {',
|
|
1677
|
-
' gap: 10,',
|
|
3518
|
+
' gap: 7,',
|
|
3519
|
+
' padding: 14,',
|
|
1678
3520
|
' },',
|
|
1679
3521
|
' sectionTitle: {',
|
|
1680
|
-
" color: '#
|
|
1681
|
-
' fontSize:
|
|
3522
|
+
" color: '#0f172a',",
|
|
3523
|
+
' fontSize: 17,',
|
|
1682
3524
|
' fontWeight: "800",',
|
|
1683
3525
|
' },',
|
|
1684
|
-
'
|
|
1685
|
-
"
|
|
1686
|
-
|
|
1687
|
-
'
|
|
1688
|
-
' borderWidth: 1,',
|
|
1689
|
-
' padding: 12,',
|
|
1690
|
-
' },',
|
|
1691
|
-
' taskTitle: {',
|
|
1692
|
-
" color: '#111827',",
|
|
1693
|
-
' fontWeight: "700",',
|
|
1694
|
-
' },',
|
|
1695
|
-
' taskStatus: {',
|
|
1696
|
-
" color: '#6b7280',",
|
|
1697
|
-
' fontSize: 12,',
|
|
1698
|
-
' fontWeight: "800",',
|
|
1699
|
-
' marginTop: 4,',
|
|
1700
|
-
' textTransform: "uppercase",',
|
|
3526
|
+
' sectionBody: {',
|
|
3527
|
+
" color: '#334155',",
|
|
3528
|
+
' fontSize: 14,',
|
|
3529
|
+
' lineHeight: 21,',
|
|
1701
3530
|
' },',
|
|
1702
3531
|
'});',
|
|
1703
3532
|
'',
|
|
1704
3533
|
].join('\n');
|
|
1705
3534
|
}
|
|
1706
|
-
function
|
|
3535
|
+
function renderAgreementScreen() {
|
|
3536
|
+
return [
|
|
3537
|
+
"import { LegalDocumentView } from './components/legal-document-view';",
|
|
3538
|
+
"import { onboardingLegalDocuments } from './legal-documents';",
|
|
3539
|
+
'',
|
|
3540
|
+
'export default function AgreementScreen() {',
|
|
3541
|
+
' return <LegalDocumentView document={onboardingLegalDocuments.agreement} />;',
|
|
3542
|
+
'}',
|
|
3543
|
+
'',
|
|
3544
|
+
].join('\n');
|
|
3545
|
+
}
|
|
3546
|
+
function renderTermsScreen() {
|
|
3547
|
+
return [
|
|
3548
|
+
"import { LegalDocumentView } from './components/legal-document-view';",
|
|
3549
|
+
"import { onboardingLegalDocuments } from './legal-documents';",
|
|
3550
|
+
'',
|
|
3551
|
+
'export default function TermsScreen() {',
|
|
3552
|
+
' return <LegalDocumentView document={onboardingLegalDocuments.terms} />;',
|
|
3553
|
+
'}',
|
|
3554
|
+
'',
|
|
3555
|
+
].join('\n');
|
|
3556
|
+
}
|
|
3557
|
+
async function ensureExpoRouterGroupLayouts(appDir, navigationShell, answers) {
|
|
3558
|
+
if (navigationShell.library !== 'expo-router') {
|
|
3559
|
+
return [];
|
|
3560
|
+
}
|
|
3561
|
+
const results = [];
|
|
3562
|
+
const includeNativeWindUiExposition = answers.defaults.includes('nativewindui');
|
|
3563
|
+
if (navigationShell.layout === 'tabs') {
|
|
3564
|
+
const tabsDir = path.join(appDir, '(tabs)');
|
|
3565
|
+
await mkdir(tabsDir, { recursive: true });
|
|
3566
|
+
const layoutPath = path.join(tabsDir, '_layout.tsx');
|
|
3567
|
+
await writeFile(layoutPath, renderTabsGroupLayout(answers.usesExpoNativeTabs, includeNativeWindUiExposition), 'utf8');
|
|
3568
|
+
results.push({ filePath: layoutPath, wrote: true });
|
|
3569
|
+
return results;
|
|
3570
|
+
}
|
|
3571
|
+
if (navigationShell.layout === 'drawer + tabs') {
|
|
3572
|
+
const drawerDir = path.join(appDir, '(drawer)');
|
|
3573
|
+
const drawerTabsDir = path.join(drawerDir, '(tabs)');
|
|
3574
|
+
await mkdir(drawerTabsDir, { recursive: true });
|
|
3575
|
+
const drawerLayoutPath = path.join(drawerDir, '_layout.tsx');
|
|
3576
|
+
const drawerTabsLayoutPath = path.join(drawerTabsDir, '_layout.tsx');
|
|
3577
|
+
await writeFile(drawerLayoutPath, renderDrawerGroupLayout(), 'utf8');
|
|
3578
|
+
await writeFile(drawerTabsLayoutPath, renderDrawerTabsGroupLayout(answers.usesExpoNativeTabs, includeNativeWindUiExposition), 'utf8');
|
|
3579
|
+
results.push({ filePath: drawerLayoutPath, wrote: true }, { filePath: drawerTabsLayoutPath, wrote: true });
|
|
3580
|
+
}
|
|
3581
|
+
return results;
|
|
3582
|
+
}
|
|
3583
|
+
function renderTabsGroupLayout(usesExpoNativeTabs, includeNativeWindUiExposition) {
|
|
3584
|
+
void includeNativeWindUiExposition;
|
|
3585
|
+
if (usesExpoNativeTabs) {
|
|
3586
|
+
return [
|
|
3587
|
+
"import { NativeTabs } from 'expo-router/unstable-native-tabs';",
|
|
3588
|
+
'',
|
|
3589
|
+
"import { useAppTheme } from '../../theme/provider';",
|
|
3590
|
+
'',
|
|
3591
|
+
'export default function TabsLayout() {',
|
|
3592
|
+
' const theme = useAppTheme();',
|
|
3593
|
+
' const colors = theme.activeColors;',
|
|
3594
|
+
' const tabContentStyle = {',
|
|
3595
|
+
' backgroundColor: colors.background,',
|
|
3596
|
+
' };',
|
|
3597
|
+
'',
|
|
3598
|
+
' return (',
|
|
3599
|
+
' <NativeTabs backgroundColor={colors.background} disableTransparentOnScrollEdge minimizeBehavior="onScrollDown">',
|
|
3600
|
+
' <NativeTabs.Trigger name="index" contentStyle={tabContentStyle} disableAutomaticContentInsets>',
|
|
3601
|
+
' <NativeTabs.Trigger.Icon sf={"house.fill" as any} md={"home" as any} />',
|
|
3602
|
+
' <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>',
|
|
3603
|
+
' </NativeTabs.Trigger>',
|
|
3604
|
+
' <NativeTabs.Trigger name="exposition" contentStyle={tabContentStyle} disableAutomaticContentInsets>',
|
|
3605
|
+
' <NativeTabs.Trigger.Icon sf={"shippingbox.fill" as any} md={"deployed_code" as any} />',
|
|
3606
|
+
' <NativeTabs.Trigger.Label>Exposition</NativeTabs.Trigger.Label>',
|
|
3607
|
+
' </NativeTabs.Trigger>',
|
|
3608
|
+
' <NativeTabs.Trigger name="stylist" contentStyle={tabContentStyle} disableAutomaticContentInsets>',
|
|
3609
|
+
' <NativeTabs.Trigger.Icon sf={"paintpalette.fill" as any} md={"palette" as any} />',
|
|
3610
|
+
' <NativeTabs.Trigger.Label>Stylist</NativeTabs.Trigger.Label>',
|
|
3611
|
+
' </NativeTabs.Trigger>',
|
|
3612
|
+
' <NativeTabs.Trigger name="data" contentStyle={tabContentStyle} disableAutomaticContentInsets>',
|
|
3613
|
+
' <NativeTabs.Trigger.Icon sf={"externaldrive.fill" as any} md={"database" as any} />',
|
|
3614
|
+
' <NativeTabs.Trigger.Label>Data</NativeTabs.Trigger.Label>',
|
|
3615
|
+
' </NativeTabs.Trigger>',
|
|
3616
|
+
' <NativeTabs.Trigger name="sdk-56" contentStyle={tabContentStyle} disableAutomaticContentInsets>',
|
|
3617
|
+
' <NativeTabs.Trigger.Icon sf={"sparkles.rectangle.stack.fill" as any} md={"rocket_launch" as any} />',
|
|
3618
|
+
' <NativeTabs.Trigger.Label>SDK 56</NativeTabs.Trigger.Label>',
|
|
3619
|
+
' </NativeTabs.Trigger>',
|
|
3620
|
+
' </NativeTabs>',
|
|
3621
|
+
' );',
|
|
3622
|
+
'}',
|
|
3623
|
+
'',
|
|
3624
|
+
].join('\n');
|
|
3625
|
+
}
|
|
3626
|
+
return [
|
|
3627
|
+
"import { Tabs } from 'expo-router';",
|
|
3628
|
+
"import { Text } from 'react-native';",
|
|
3629
|
+
'',
|
|
3630
|
+
'export default function TabsLayout() {',
|
|
3631
|
+
' return (',
|
|
3632
|
+
' <Tabs>',
|
|
3633
|
+
' <Tabs.Screen name="index" options={{ title: \'Home\', tabBarIcon: () => <Text>H</Text> }} />',
|
|
3634
|
+
' <Tabs.Screen name="exposition" options={{ title: \'Exposition\', tabBarIcon: () => <Text>EX</Text> }} />',
|
|
3635
|
+
' <Tabs.Screen name="stylist" options={{ title: \'Stylist\', tabBarIcon: () => <Text>SS</Text> }} />',
|
|
3636
|
+
' <Tabs.Screen name="data" options={{ title: \'Data\', tabBarIcon: () => <Text>DB</Text> }} />',
|
|
3637
|
+
' <Tabs.Screen name="sdk-56" options={{ title: \'SDK 56\', tabBarIcon: () => <Text>56</Text> }} />',
|
|
3638
|
+
' </Tabs>',
|
|
3639
|
+
' );',
|
|
3640
|
+
'}',
|
|
3641
|
+
'',
|
|
3642
|
+
].join('\n');
|
|
3643
|
+
}
|
|
3644
|
+
function renderDrawerGroupLayout() {
|
|
3645
|
+
return [
|
|
3646
|
+
"import { Drawer } from 'expo-router/drawer';",
|
|
3647
|
+
'',
|
|
3648
|
+
'export default function DrawerLayout() {',
|
|
3649
|
+
' return (',
|
|
3650
|
+
' <Drawer>',
|
|
3651
|
+
" <Drawer.Screen name=\"index\" options={{ title: 'Home', drawerLabel: 'Home' }} />",
|
|
3652
|
+
" <Drawer.Screen name=\"(tabs)\" options={{ title: 'Exposition', drawerLabel: 'Exposition' }} />",
|
|
3653
|
+
' </Drawer>',
|
|
3654
|
+
' );',
|
|
3655
|
+
'}',
|
|
3656
|
+
'',
|
|
3657
|
+
].join('\n');
|
|
3658
|
+
}
|
|
3659
|
+
function renderDrawerTabsGroupLayout(usesExpoNativeTabs, includeNativeWindUiExposition) {
|
|
3660
|
+
void includeNativeWindUiExposition;
|
|
3661
|
+
if (usesExpoNativeTabs) {
|
|
3662
|
+
return [
|
|
3663
|
+
"import { NativeTabs } from 'expo-router/unstable-native-tabs';",
|
|
3664
|
+
'',
|
|
3665
|
+
"import { useAppTheme } from '../../../theme/provider';",
|
|
3666
|
+
'',
|
|
3667
|
+
'export default function DrawerTabsLayout() {',
|
|
3668
|
+
' const theme = useAppTheme();',
|
|
3669
|
+
' const colors = theme.activeColors;',
|
|
3670
|
+
' const tabContentStyle = {',
|
|
3671
|
+
' backgroundColor: colors.background,',
|
|
3672
|
+
' };',
|
|
3673
|
+
'',
|
|
3674
|
+
' return (',
|
|
3675
|
+
' <NativeTabs backgroundColor={colors.background} disableTransparentOnScrollEdge minimizeBehavior="onScrollDown">',
|
|
3676
|
+
' <NativeTabs.Trigger name="index" contentStyle={tabContentStyle} disableAutomaticContentInsets>',
|
|
3677
|
+
' <NativeTabs.Trigger.Icon sf={"shippingbox.fill" as any} md={"deployed_code" as any} />',
|
|
3678
|
+
' <NativeTabs.Trigger.Label>Exposition</NativeTabs.Trigger.Label>',
|
|
3679
|
+
' </NativeTabs.Trigger>',
|
|
3680
|
+
' <NativeTabs.Trigger name="stylist" contentStyle={tabContentStyle} disableAutomaticContentInsets>',
|
|
3681
|
+
' <NativeTabs.Trigger.Icon sf={"paintpalette.fill" as any} md={"palette" as any} />',
|
|
3682
|
+
' <NativeTabs.Trigger.Label>Stylist</NativeTabs.Trigger.Label>',
|
|
3683
|
+
' </NativeTabs.Trigger>',
|
|
3684
|
+
' <NativeTabs.Trigger name="data" contentStyle={tabContentStyle} disableAutomaticContentInsets>',
|
|
3685
|
+
' <NativeTabs.Trigger.Icon sf={"externaldrive.fill" as any} md={"database" as any} />',
|
|
3686
|
+
' <NativeTabs.Trigger.Label>Data</NativeTabs.Trigger.Label>',
|
|
3687
|
+
' </NativeTabs.Trigger>',
|
|
3688
|
+
' <NativeTabs.Trigger name="sdk-56" contentStyle={tabContentStyle} disableAutomaticContentInsets>',
|
|
3689
|
+
' <NativeTabs.Trigger.Icon sf={"sparkles.rectangle.stack.fill" as any} md={"rocket_launch" as any} />',
|
|
3690
|
+
' <NativeTabs.Trigger.Label>SDK 56</NativeTabs.Trigger.Label>',
|
|
3691
|
+
' </NativeTabs.Trigger>',
|
|
3692
|
+
' </NativeTabs>',
|
|
3693
|
+
' );',
|
|
3694
|
+
'}',
|
|
3695
|
+
'',
|
|
3696
|
+
].join('\n');
|
|
3697
|
+
}
|
|
3698
|
+
return [
|
|
3699
|
+
"import { Tabs } from 'expo-router';",
|
|
3700
|
+
"import { Text } from 'react-native';",
|
|
3701
|
+
'',
|
|
3702
|
+
'export default function DrawerTabsLayout() {',
|
|
3703
|
+
' return (',
|
|
3704
|
+
' <Tabs>',
|
|
3705
|
+
' <Tabs.Screen name="index" options={{ title: \'Exposition\', tabBarIcon: () => <Text>EX</Text> }} />',
|
|
3706
|
+
' <Tabs.Screen name="stylist" options={{ title: \'Stylist\', tabBarIcon: () => <Text>SS</Text> }} />',
|
|
3707
|
+
' <Tabs.Screen name="data" options={{ title: \'Data\', tabBarIcon: () => <Text>DB</Text> }} />',
|
|
3708
|
+
' <Tabs.Screen name="sdk-56" options={{ title: \'SDK 56\', tabBarIcon: () => <Text>56</Text> }} />',
|
|
3709
|
+
' </Tabs>',
|
|
3710
|
+
' );',
|
|
3711
|
+
'}',
|
|
3712
|
+
'',
|
|
3713
|
+
].join('\n');
|
|
3714
|
+
}
|
|
3715
|
+
function renderSettingsScreen() {
|
|
1707
3716
|
return [
|
|
1708
|
-
"import { Link } from 'expo-router';",
|
|
1709
3717
|
"import { StyleSheet, Text, View } from 'react-native';",
|
|
1710
3718
|
'',
|
|
1711
|
-
"import {
|
|
3719
|
+
"import { KeyboardForm } from '../../components/exposition';",
|
|
3720
|
+
"import { useAppTheme } from '../../theme/provider';",
|
|
3721
|
+
'',
|
|
3722
|
+
'export default function SettingsScreen() {',
|
|
3723
|
+
' const theme = useAppTheme();',
|
|
3724
|
+
' const colors = theme.activeColors;',
|
|
1712
3725
|
'',
|
|
1713
|
-
'export default function OnboardingScreen() {',
|
|
1714
3726
|
' return (',
|
|
1715
|
-
' <View style={styles.screen}>',
|
|
1716
|
-
' <
|
|
1717
|
-
'
|
|
1718
|
-
'
|
|
1719
|
-
' </
|
|
1720
|
-
' <
|
|
1721
|
-
' <AnimatedPressable label="Continue to home" />',
|
|
1722
|
-
' </Link>',
|
|
3727
|
+
' <View style={[styles.screen, { backgroundColor: colors.background }]}>',
|
|
3728
|
+
' <View style={styles.header}>',
|
|
3729
|
+
' <Text style={[styles.title, { color: colors.text, fontFamily: theme.typography.fontFamily, fontWeight: theme.typography.fontFamily === \"System\" || theme.typography.fontFamily === \"monospace\" ? \"800\" : \"normal\" }]}>Settings</Text>',
|
|
3730
|
+
' <Text style={[styles.body, { color: colors.text }]}>Keyboard Controller is ready for form-heavy screens.</Text>',
|
|
3731
|
+
' </View>',
|
|
3732
|
+
' <KeyboardForm />',
|
|
1723
3733
|
' </View>',
|
|
1724
3734
|
' );',
|
|
1725
3735
|
'}',
|
|
@@ -1728,10 +3738,11 @@ function renderOnboardingScreen() {
|
|
|
1728
3738
|
' screen: {',
|
|
1729
3739
|
" backgroundColor: '#ffffff',",
|
|
1730
3740
|
' flex: 1,',
|
|
1731
|
-
' gap: 16,',
|
|
1732
|
-
' justifyContent: "center",',
|
|
1733
3741
|
' padding: 20,',
|
|
1734
3742
|
' },',
|
|
3743
|
+
' header: {',
|
|
3744
|
+
' marginBottom: 12,',
|
|
3745
|
+
' },',
|
|
1735
3746
|
' title: {',
|
|
1736
3747
|
" color: '#111827',",
|
|
1737
3748
|
' fontSize: 26,',
|
|
@@ -1739,102 +3750,334 @@ function renderOnboardingScreen() {
|
|
|
1739
3750
|
' },',
|
|
1740
3751
|
' body: {',
|
|
1741
3752
|
" color: '#4b5563',",
|
|
1742
|
-
' fontSize:
|
|
1743
|
-
' lineHeight:
|
|
3753
|
+
' fontSize: 14,',
|
|
3754
|
+
' lineHeight: 20,',
|
|
3755
|
+
' marginTop: 4,',
|
|
1744
3756
|
' },',
|
|
1745
3757
|
'});',
|
|
1746
3758
|
'',
|
|
1747
3759
|
].join('\n');
|
|
1748
3760
|
}
|
|
1749
|
-
function
|
|
3761
|
+
function renderExpositionScreen(includeNativeWindUiExposition = false) {
|
|
3762
|
+
const nativeWindUiRouteCard = includeNativeWindUiExposition
|
|
3763
|
+
? [
|
|
3764
|
+
' <PackageCard',
|
|
3765
|
+
' packageName="nativewindui route"',
|
|
3766
|
+
' title="NativeWindUI route"',
|
|
3767
|
+
' body="NativeWindUI examples stay in the app as a dedicated route, linked here instead of pinned in the bottom tabs.">',
|
|
3768
|
+
' <Link href="/exposition/nativewindui" asChild>',
|
|
3769
|
+
' <Text style={styles.link}>Open NativeWindUI screen</Text>',
|
|
3770
|
+
' </Link>',
|
|
3771
|
+
' </PackageCard>',
|
|
3772
|
+
]
|
|
3773
|
+
: [];
|
|
3774
|
+
const linkImport = includeNativeWindUiExposition ? ["import { Link } from 'expo-router';"] : [];
|
|
1750
3775
|
return [
|
|
1751
|
-
|
|
3776
|
+
...linkImport,
|
|
3777
|
+
"import { Linking, Platform, ScrollView, StyleSheet, Text, View } from 'react-native';",
|
|
1752
3778
|
'',
|
|
1753
|
-
"import { KeyboardForm } from '../../components/exposition';",
|
|
3779
|
+
"import { AnimatedPressable, ExpositionNotice, GestureCard, KeyboardForm, PackageCard, ScreensCard, SoftwareMansionLogo } from '../../components/exposition';",
|
|
3780
|
+
"import { useAppTheme } from '../../theme/provider';",
|
|
3781
|
+
'',
|
|
3782
|
+
'export default function ExpositionScreen() {',
|
|
3783
|
+
' const theme = useAppTheme();',
|
|
3784
|
+
' const colors = theme.activeColors;',
|
|
1754
3785
|
'',
|
|
1755
|
-
'export default function SettingsScreen() {',
|
|
1756
3786
|
' return (',
|
|
1757
|
-
' <
|
|
1758
|
-
' <
|
|
1759
|
-
'
|
|
1760
|
-
'
|
|
1761
|
-
'
|
|
1762
|
-
'
|
|
1763
|
-
'
|
|
3787
|
+
' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={[styles.screen, { backgroundColor: colors.background }]}>',
|
|
3788
|
+
' <Text style={[styles.title, { color: colors.text, fontFamily: theme.typography.fontFamily, fontWeight: theme.typography.fontFamily === \"System\" || theme.typography.fontFamily === \"monospace\" ? \"800\" : \"normal\" }]}>Package Exposition</Text>',
|
|
3789
|
+
' <Text style={[styles.intro, { color: colors.text }]}>Browse the included Software Mansion packages, then keep only what your app needs.</Text>',
|
|
3790
|
+
' <ExpositionNotice />',
|
|
3791
|
+
' <PackageCard',
|
|
3792
|
+
' packageName="reanimated-color-picker"',
|
|
3793
|
+
' title="Stylist color editing"',
|
|
3794
|
+
' body="Stylist uses this package for the hue slider, color preview, and manual palette picker that writes theme tokens."',
|
|
3795
|
+
' >',
|
|
3796
|
+
" <Text style={styles.link} onPress={() => Linking.openURL('https://github.com/alabsi91/reanimated-color-picker')}>",
|
|
3797
|
+
' Reanimated Color Picker',
|
|
3798
|
+
' </Text>',
|
|
3799
|
+
' </PackageCard>',
|
|
3800
|
+
' <PackageCard',
|
|
3801
|
+
' packageName="@react-native-async-storage/async-storage"',
|
|
3802
|
+
' title="Stylist local preferences"',
|
|
3803
|
+
' body="Stylist stores local-only preferences such as the Google Fonts API key, dismissed banners, and editor settings with Async Storage."',
|
|
3804
|
+
' >',
|
|
3805
|
+
" <Text style={styles.link} onPress={() => Linking.openURL('https://react-native-async-storage.github.io/async-storage/')}>",
|
|
3806
|
+
' Async Storage Docs',
|
|
3807
|
+
' </Text>',
|
|
3808
|
+
' </PackageCard>',
|
|
3809
|
+
' <PackageCard',
|
|
3810
|
+
' packageName="react-native-safe-area-context"',
|
|
3811
|
+
' title="Stylist safe spacing"',
|
|
3812
|
+
' body="Stylist reads safe-area insets so editor controls stay clear of cutouts, native tabs, and device navigation areas."',
|
|
3813
|
+
' >',
|
|
3814
|
+
" <Text style={styles.link} onPress={() => Linking.openURL('https://docs.expo.dev/versions/latest/sdk/safe-area-context/')}>",
|
|
3815
|
+
' Expo SDK - SafeAreaContext',
|
|
3816
|
+
' </Text>',
|
|
3817
|
+
' </PackageCard>',
|
|
3818
|
+
' <PackageCard',
|
|
3819
|
+
' packageName="tailwindcss/colors"',
|
|
3820
|
+
' title="Stylist palette families"',
|
|
3821
|
+
' body="Stylist uses Tailwind color families and shade scales to drive the palette-family mode and accessible token previews."',
|
|
3822
|
+
' >',
|
|
3823
|
+
" <Text style={styles.link} onPress={() => Linking.openURL('https://tailwindcss.com/docs/customizing-colors')}>",
|
|
3824
|
+
' Tailwind CSS - Colors',
|
|
3825
|
+
' </Text>',
|
|
3826
|
+
' </PackageCard>',
|
|
3827
|
+
' <PackageCard',
|
|
3828
|
+
' packageName="expo-router API routes"',
|
|
3829
|
+
' title="Stylist sync endpoint"',
|
|
3830
|
+
' body="Stylist uses an Expo Router +api route so both native and web can sync theme output files by calling /exposition/stylist-sync."',
|
|
3831
|
+
' >',
|
|
3832
|
+
" <Text style={styles.link} onPress={() => Linking.openURL('https://docs.expo.dev/router/web/api-routes/')}>",
|
|
3833
|
+
' Expo Router - API Routes',
|
|
3834
|
+
' </Text>',
|
|
3835
|
+
' </PackageCard>',
|
|
3836
|
+
' <PackageCard',
|
|
3837
|
+
' packageName="react-native-reanimated + react-native-worklets"',
|
|
3838
|
+
' title="Motion that feels native"',
|
|
3839
|
+
' body="Press the button to see the Reanimated timing demo. Worklets make this kind of UI-thread animation possible."',
|
|
3840
|
+
' >',
|
|
3841
|
+
' <AnimatedPressable label="Press and hold" />',
|
|
3842
|
+
" <Text style={styles.link} onPress={() => Linking.openURL('https://docs.swmansion.com/react-native-reanimated')}>",
|
|
3843
|
+
' Software Mansion - Reanimated',
|
|
3844
|
+
' </Text>',
|
|
3845
|
+
' </PackageCard>',
|
|
3846
|
+
' <PackageCard',
|
|
3847
|
+
' packageName="react-native-gesture-handler"',
|
|
3848
|
+
' title="Gesture-first interactions"',
|
|
3849
|
+
' body="Drag the card below. If your product does not need touch-heavy interactions, this demo helps you decide what to remove."',
|
|
3850
|
+
' >',
|
|
3851
|
+
' <GestureCard title="Drag me" body="This card springs back when the gesture ends." />',
|
|
3852
|
+
" <Text style={styles.link} onPress={() => Linking.openURL('https://docs.swmansion.com/react-native-gesture-handler')}>",
|
|
3853
|
+
' Software Mansion - Gesture Handler',
|
|
3854
|
+
' </Text>',
|
|
3855
|
+
' </PackageCard>',
|
|
3856
|
+
' <PackageCard',
|
|
3857
|
+
' packageName="react-native-screens"',
|
|
3858
|
+
' title="Native navigation primitives"',
|
|
3859
|
+
' body="Screens support the navigation layer with native lifecycle and memory behavior."',
|
|
3860
|
+
' >',
|
|
3861
|
+
' <ScreensCard />',
|
|
3862
|
+
" <Text style={styles.link} onPress={() => Linking.openURL('https://docs.swmansion.com/react-native-screens')}>",
|
|
3863
|
+
' Software Mansion - Screens',
|
|
3864
|
+
' </Text>',
|
|
3865
|
+
' </PackageCard>',
|
|
3866
|
+
' <PackageCard',
|
|
3867
|
+
' packageName="react-native-svg"',
|
|
3868
|
+
' title="Portable vector UI"',
|
|
3869
|
+
' body="Use SVG for marks, badges, charts, and vector states that need to scale cleanly."',
|
|
3870
|
+
' >',
|
|
3871
|
+
' <View style={styles.svgDemo}><SoftwareMansionLogo width={150} height={80} /></View>',
|
|
3872
|
+
" <Text style={styles.link} onPress={() => Linking.openURL('https://docs.expo.dev/versions/latest/sdk/svg')}>",
|
|
3873
|
+
' Expo SDK - SVG',
|
|
3874
|
+
' </Text>',
|
|
3875
|
+
' </PackageCard>',
|
|
3876
|
+
' <PackageCard',
|
|
3877
|
+
' packageName="react-native-keyboard-controller"',
|
|
3878
|
+
' title="Keyboard-heavy screens"',
|
|
3879
|
+
' body="Use this when forms, chat, notes, or auth flows need better keyboard control than manual offsets."',
|
|
3880
|
+
' >',
|
|
3881
|
+
' <KeyboardForm />',
|
|
3882
|
+
" <Text style={styles.link} onPress={() => Linking.openURL('https://kirillzyusko.github.io/react-native-keyboard-controller/')}>",
|
|
3883
|
+
' Kirill Zyusko - Keyboard Controller',
|
|
3884
|
+
' </Text>',
|
|
3885
|
+
' </PackageCard>',
|
|
3886
|
+
...nativeWindUiRouteCard,
|
|
3887
|
+
' </ScrollView>',
|
|
1764
3888
|
' );',
|
|
1765
3889
|
'}',
|
|
1766
3890
|
'',
|
|
1767
3891
|
'const styles = StyleSheet.create({',
|
|
1768
3892
|
' screen: {',
|
|
1769
|
-
" backgroundColor: '#
|
|
3893
|
+
" backgroundColor: '#f9fafb',",
|
|
1770
3894
|
' flex: 1,',
|
|
1771
|
-
' padding: 20,',
|
|
1772
3895
|
' },',
|
|
1773
|
-
'
|
|
1774
|
-
'
|
|
3896
|
+
' content: {',
|
|
3897
|
+
' gap: 16,',
|
|
3898
|
+
' padding: 20,',
|
|
3899
|
+
" paddingTop: Platform.OS === 'web' ? 92 : 20,",
|
|
1775
3900
|
' },',
|
|
1776
3901
|
' title: {',
|
|
1777
3902
|
" color: '#111827',",
|
|
1778
|
-
' fontSize:
|
|
1779
|
-
' fontWeight: "
|
|
3903
|
+
' fontSize: 30,',
|
|
3904
|
+
' fontWeight: "900",',
|
|
1780
3905
|
' },',
|
|
1781
|
-
'
|
|
3906
|
+
' intro: {',
|
|
1782
3907
|
" color: '#4b5563',",
|
|
3908
|
+
' fontSize: 16,',
|
|
3909
|
+
' lineHeight: 24,',
|
|
3910
|
+
' },',
|
|
3911
|
+
' link: {',
|
|
3912
|
+
" color: '#1d4ed8',",
|
|
1783
3913
|
' fontSize: 14,',
|
|
3914
|
+
" fontWeight: '800',",
|
|
1784
3915
|
' lineHeight: 20,',
|
|
1785
|
-
'
|
|
3916
|
+
' },',
|
|
3917
|
+
' svgDemo: {',
|
|
3918
|
+
' alignItems: "center",',
|
|
3919
|
+
' paddingVertical: 8,',
|
|
1786
3920
|
' },',
|
|
1787
3921
|
'});',
|
|
1788
3922
|
'',
|
|
1789
3923
|
].join('\n');
|
|
1790
3924
|
}
|
|
1791
|
-
function
|
|
3925
|
+
function renderExpoSdk56Screen(answers) {
|
|
3926
|
+
const expoUiDemo = answers.usesExpoUiUniversalComponents
|
|
3927
|
+
? [
|
|
3928
|
+
'function UniversalPreview() {',
|
|
3929
|
+
' const [enabled, setEnabled] = useState(true);',
|
|
3930
|
+
' const [count, setCount] = useState(0);',
|
|
3931
|
+
' return (',
|
|
3932
|
+
' <View style={styles.exampleBox}>',
|
|
3933
|
+
' <View style={styles.componentLabelGrid}>',
|
|
3934
|
+
' <Text style={styles.componentLabel}>Host</Text>',
|
|
3935
|
+
' <Text style={styles.componentLabel}>Column</Text>',
|
|
3936
|
+
' <Text style={styles.componentLabel}>Text</Text>',
|
|
3937
|
+
' <Text style={styles.componentLabel}>Button</Text>',
|
|
3938
|
+
' <Text style={styles.componentLabel}>Switch</Text>',
|
|
3939
|
+
' </View>',
|
|
3940
|
+
' <Host matchContents>',
|
|
3941
|
+
' <Column spacing={10}>',
|
|
3942
|
+
' <ExpoUIText>{enabled ? "Feature enabled" : "Feature disabled"}</ExpoUIText>',
|
|
3943
|
+
' <ExpoUIButton variant="filled" label={`Universal button (${count})`} onPress={() => setCount((value) => value + 1)} />',
|
|
3944
|
+
' <ExpoUISwitch label="Universal switch" value={enabled} onValueChange={setEnabled} />',
|
|
3945
|
+
' </Column>',
|
|
3946
|
+
' </Host>',
|
|
3947
|
+
' </View>',
|
|
3948
|
+
' );',
|
|
3949
|
+
'}',
|
|
3950
|
+
'',
|
|
3951
|
+
]
|
|
3952
|
+
: [
|
|
3953
|
+
'function UniversalPreview() {',
|
|
3954
|
+
' return (',
|
|
3955
|
+
' <View style={styles.exampleBox}>',
|
|
3956
|
+
' <Text style={styles.exampleTitle}>Universal components are not enabled.</Text>',
|
|
3957
|
+
' <Text style={styles.exampleBody}>Turn on Expo UI Universal in onboarding to generate a Host, Column, Text, Button, and Switch demo here.</Text>',
|
|
3958
|
+
' </View>',
|
|
3959
|
+
' );',
|
|
3960
|
+
'}',
|
|
3961
|
+
'',
|
|
3962
|
+
];
|
|
1792
3963
|
return [
|
|
1793
|
-
"import {
|
|
3964
|
+
"import { useState } from 'react';",
|
|
3965
|
+
"import { Linking, ScrollView, StyleSheet, Text, View } from 'react-native';",
|
|
3966
|
+
...(answers.usesExpoUiUniversalComponents
|
|
3967
|
+
? [
|
|
3968
|
+
"import { Host, Column, Button as ExpoUIButton, Switch as ExpoUISwitch, Text as ExpoUIText } from '@expo/ui';",
|
|
3969
|
+
]
|
|
3970
|
+
: []),
|
|
3971
|
+
'',
|
|
3972
|
+
"import { ExpositionNotice, PackageCard } from '../../components/exposition';",
|
|
3973
|
+
'',
|
|
3974
|
+
'const highlights = [',
|
|
3975
|
+
" { kind: 'expo-ui', title: 'Expo UI is production-ready', packageName: '@expo/ui', body: 'SwiftUI and Jetpack Compose APIs are stable in SDK 56 with deeper native parity.', links: [{ label: 'Expo UI docs', href: 'https://docs.expo.dev/versions/latest/sdk/ui/' }] },",
|
|
3976
|
+
" { kind: 'universal', title: 'Universal components', packageName: '@expo/ui', body: 'Host, Button, Switch, Text, layout primitives, lists, and controls can live in one source tree.', links: [{ label: 'Universal components docs', href: 'https://docs.expo.dev/versions/latest/sdk/ui/universal/' }] },",
|
|
3977
|
+
" { kind: 'native-state', title: 'useNativeState', packageName: '@expo/ui/swift-ui', body: 'Native state can drive form controls and text entry without JS-thread controlled-input jitter.', links: [{ label: 'useNativeState docs', href: 'https://docs.expo.dev/versions/latest/sdk/ui/swift-ui/usenativestate/' }] },",
|
|
3978
|
+
" { kind: 'drop-in', title: 'Drop-in replacements', packageName: '@expo/ui', body: 'Expo UI maps common community UI primitives to native-backed replacements.', links: [{ label: 'Drop-in replacements docs', href: 'https://docs.expo.dev/versions/latest/sdk/ui/drop-in-replacements/' }] },",
|
|
3979
|
+
" { kind: 'inline-modules', title: 'Inline modules', packageName: 'expo-modules-core', body: 'Swift/Kotlin modules can be authored directly beside app code for project-local native features.', links: [{ label: 'Inline modules tutorial', href: 'https://docs.expo.dev/modules/inline-modules-tutorial/' }] },",
|
|
3980
|
+
" { kind: 'native-tabs', title: 'Router and native tabs', packageName: 'expo-router', body: 'Expo Router absorbs more of its stack internals and ships stronger native tabs support.', links: [{ label: 'Native tabs docs', href: 'https://docs.expo.dev/versions/latest/sdk/router/native-tabs/' }] },",
|
|
3981
|
+
" { kind: 'runtime', title: 'Runtime baseline', packageName: 'react-native + react', body: 'SDK 56 aligns to React Native 0.85, React 19.2, Hermes V1 defaults, and faster builds.', links: [] },",
|
|
3982
|
+
" { kind: 'widgets', title: 'Widgets', packageName: 'expo-widgets', body: 'Expo widgets are stable, with strong iOS support for lock-screen and home-screen experiences.', links: [{ label: 'Widgets docs', href: 'https://docs.expo.dev/versions/latest/sdk/widgets/' }] },",
|
|
3983
|
+
" { kind: 'audio', title: 'Audio and haptics updates', packageName: 'expo-audio + expo-haptics', body: 'Audio streaming primitives improved and haptics coverage keeps expanding.', links: [{ label: 'Expo Audio docs', href: 'https://docs.expo.dev/versions/latest/sdk/audio/' }] },",
|
|
3984
|
+
'];',
|
|
1794
3985
|
'',
|
|
1795
|
-
|
|
3986
|
+
...expoUiDemo,
|
|
3987
|
+
'function TopicExample({ kind }: { kind: string }) {',
|
|
3988
|
+
' if (kind === "universal") return <UniversalPreview />;',
|
|
3989
|
+
' if (kind === "expo-ui") {',
|
|
3990
|
+
' return (',
|
|
3991
|
+
' <View style={styles.exampleBox}>',
|
|
3992
|
+
' <Text style={styles.exampleTitle}>Native controls from one React surface</Text>',
|
|
3993
|
+
' <View style={styles.exampleRow}><Text style={styles.examplePill}>SwiftUI</Text><Text style={styles.exampleBody}>iOS controls render with native behavior.</Text></View>',
|
|
3994
|
+
' <View style={styles.exampleRow}><Text style={styles.examplePill}>Compose</Text><Text style={styles.exampleBody}>Android controls stay platform-native.</Text></View>',
|
|
3995
|
+
' </View>',
|
|
3996
|
+
' );',
|
|
3997
|
+
' }',
|
|
3998
|
+
' if (kind === "native-state") {',
|
|
3999
|
+
' return (',
|
|
4000
|
+
' <View style={styles.exampleBox}>',
|
|
4001
|
+
' <Text style={styles.exampleTitle}>Text input owned by native state</Text>',
|
|
4002
|
+
' <View style={styles.fakeInput}><Text style={styles.fakeInputText}>Display name</Text><Text style={styles.fakeInputValue}>Ada Lovelace</Text></View>',
|
|
4003
|
+
' </View>',
|
|
4004
|
+
' );',
|
|
4005
|
+
' }',
|
|
4006
|
+
' if (kind === "drop-in") {',
|
|
4007
|
+
' return (',
|
|
4008
|
+
' <View style={styles.exampleBox}>',
|
|
4009
|
+
' <Text style={styles.exampleTitle}>Replacement candidates</Text>',
|
|
4010
|
+
' <View style={styles.exampleRow}><Text style={styles.examplePill}>Slider</Text><Text style={styles.exampleBody}>Use the Expo UI version where native fidelity matters.</Text></View>',
|
|
4011
|
+
' <View style={styles.exampleRow}><Text style={styles.examplePill}>Picker</Text><Text style={styles.exampleBody}>Swap community picker screens one at a time.</Text></View>',
|
|
4012
|
+
' </View>',
|
|
4013
|
+
' );',
|
|
4014
|
+
' }',
|
|
4015
|
+
' if (kind === "inline-modules") {',
|
|
4016
|
+
' return (',
|
|
4017
|
+
' <View style={styles.exampleBox}>',
|
|
4018
|
+
' <Text style={styles.exampleTitle}>Project-local native module</Text>',
|
|
4019
|
+
' <Text style={styles.codeLine}>modules/LocalGreeting/index.ts</Text>',
|
|
4020
|
+
' <Text style={styles.codeLine}>modules/LocalGreeting/ios/LocalGreeting.swift</Text>',
|
|
4021
|
+
' </View>',
|
|
4022
|
+
' );',
|
|
4023
|
+
' }',
|
|
4024
|
+
' if (kind === "native-tabs") {',
|
|
4025
|
+
' return (',
|
|
4026
|
+
' <View style={styles.exampleBox}>',
|
|
4027
|
+
' <View style={styles.tabStrip}><Text style={styles.tabActive}>Home</Text><Text style={styles.tabItem}>Search</Text><Text style={styles.tabItem}>Settings</Text></View>',
|
|
4028
|
+
' </View>',
|
|
4029
|
+
' );',
|
|
4030
|
+
' }',
|
|
4031
|
+
' if (kind === "runtime") {',
|
|
4032
|
+
' return (',
|
|
4033
|
+
' <View style={styles.exampleBox}>',
|
|
4034
|
+
' <Text style={styles.exampleTitle}>Runtime versions to verify</Text>',
|
|
4035
|
+
' <View style={styles.componentLabelGrid}><Text style={styles.componentLabel}>RN 0.85</Text><Text style={styles.componentLabel}>React 19.2</Text><Text style={styles.componentLabel}>Hermes V1</Text></View>',
|
|
4036
|
+
' </View>',
|
|
4037
|
+
' );',
|
|
4038
|
+
' }',
|
|
4039
|
+
' if (kind === "widgets") {',
|
|
4040
|
+
' return (',
|
|
4041
|
+
' <View style={styles.exampleBox}>',
|
|
4042
|
+
' <View style={styles.widgetTile}><Text style={styles.widgetTitle}>Today</Text><Text style={styles.widgetBody}>3 tasks ready</Text></View>',
|
|
4043
|
+
' </View>',
|
|
4044
|
+
' );',
|
|
4045
|
+
' }',
|
|
4046
|
+
' return (',
|
|
4047
|
+
' <View style={styles.exampleBox}>',
|
|
4048
|
+
' <Text style={styles.exampleTitle}>Audio control surface</Text>',
|
|
4049
|
+
' <View style={styles.transportRow}><Text style={styles.transportButton}>Play</Text><Text style={styles.transportButton}>Pause</Text><Text style={styles.transportButton}>Haptic tap</Text></View>',
|
|
4050
|
+
' </View>',
|
|
4051
|
+
' );',
|
|
4052
|
+
'}',
|
|
1796
4053
|
'',
|
|
1797
|
-
'export default function
|
|
4054
|
+
'export default function ExpoSdk56Screen() {',
|
|
1798
4055
|
' return (',
|
|
1799
4056
|
' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
|
|
1800
|
-
|
|
1801
|
-
' <Text style={styles.intro}>
|
|
4057
|
+
' <Text style={styles.title}>Expo SDK 56 Exposition</Text>',
|
|
4058
|
+
' <Text style={styles.intro}>Review the SDK 56 changes before deciding what belongs in the real app.</Text>',
|
|
1802
4059
|
' <ExpositionNotice />',
|
|
1803
|
-
'
|
|
1804
|
-
' packageName=
|
|
1805
|
-
'
|
|
1806
|
-
'
|
|
1807
|
-
'
|
|
1808
|
-
'
|
|
1809
|
-
'
|
|
1810
|
-
'
|
|
1811
|
-
'
|
|
1812
|
-
'
|
|
1813
|
-
'
|
|
1814
|
-
'
|
|
1815
|
-
'
|
|
1816
|
-
'
|
|
1817
|
-
'
|
|
1818
|
-
'
|
|
1819
|
-
'
|
|
1820
|
-
'
|
|
1821
|
-
'
|
|
1822
|
-
' <
|
|
1823
|
-
' </
|
|
1824
|
-
' <PackageCard',
|
|
1825
|
-
' packageName="react-native-svg"',
|
|
1826
|
-
' title="Portable vector UI"',
|
|
1827
|
-
' body="Use SVG for marks, badges, charts, and vector states that need to scale cleanly."',
|
|
1828
|
-
' >',
|
|
1829
|
-
' <View style={styles.svgDemo}><SvgMark /></View>',
|
|
1830
|
-
' </PackageCard>',
|
|
1831
|
-
' <PackageCard',
|
|
1832
|
-
' packageName="react-native-keyboard-controller"',
|
|
1833
|
-
' title="Keyboard-heavy screens"',
|
|
1834
|
-
' body="Use this when forms, chat, notes, or auth flows need better keyboard control than manual offsets."',
|
|
1835
|
-
' >',
|
|
1836
|
-
' <KeyboardForm />',
|
|
1837
|
-
' </PackageCard>',
|
|
4060
|
+
' {highlights.map((item) => (',
|
|
4061
|
+
' <PackageCard key={item.title} packageName={item.packageName} title={item.title} body={item.body}>',
|
|
4062
|
+
' <View style={styles.cardChildren}>',
|
|
4063
|
+
' <TopicExample kind={item.kind} />',
|
|
4064
|
+
' {item.links.length ? (',
|
|
4065
|
+
' <View style={styles.linkList}>',
|
|
4066
|
+
' {item.links.map((link) => (',
|
|
4067
|
+
' <Text key={link.href} accessibilityRole="link" onPress={() => Linking.openURL(link.href)} style={styles.link}>',
|
|
4068
|
+
' {link.label}',
|
|
4069
|
+
' </Text>',
|
|
4070
|
+
' ))}',
|
|
4071
|
+
' </View>',
|
|
4072
|
+
' ) : null}',
|
|
4073
|
+
' </View>',
|
|
4074
|
+
' </PackageCard>',
|
|
4075
|
+
' ))}',
|
|
4076
|
+
' <View style={styles.linksCard}>',
|
|
4077
|
+
' <Text style={styles.linksTitle}>Video sources</Text>',
|
|
4078
|
+
' <Text accessibilityRole="link" onPress={() => Linking.openURL("https://www.youtube.com/watch?v=MKqGbv-Tssg&t")} style={styles.link}>What\'s New in Expo SDK 56: Expo UI, Inline Swift/Kotlin Modules, and Faster Builds by Expo</Text>',
|
|
4079
|
+
' <Text accessibilityRole="link" onPress={() => Linking.openURL("https://www.youtube.com/watch?v=ywvywq0AGPM")} style={styles.link}>Everything new in Expo SDK 56 by Code with Beto</Text>',
|
|
4080
|
+
' </View>',
|
|
1838
4081
|
' </ScrollView>',
|
|
1839
4082
|
' );',
|
|
1840
4083
|
'}',
|
|
@@ -1847,83 +4090,419 @@ function renderExpositionScreen(answers) {
|
|
|
1847
4090
|
' content: {',
|
|
1848
4091
|
' gap: 16,',
|
|
1849
4092
|
' padding: 20,',
|
|
4093
|
+
" paddingTop: Platform.OS === 'web' ? 92 : 20,",
|
|
1850
4094
|
' },',
|
|
1851
4095
|
' title: {',
|
|
1852
4096
|
" color: '#111827',",
|
|
1853
4097
|
' fontSize: 30,',
|
|
1854
4098
|
' fontWeight: "900",',
|
|
4099
|
+
' textAlign: "center",',
|
|
1855
4100
|
' },',
|
|
1856
4101
|
' intro: {',
|
|
1857
4102
|
" color: '#4b5563',",
|
|
1858
4103
|
' fontSize: 16,',
|
|
1859
4104
|
' lineHeight: 24,',
|
|
1860
4105
|
' },',
|
|
1861
|
-
'
|
|
4106
|
+
' linksWrap: {',
|
|
4107
|
+
' gap: 8,',
|
|
4108
|
+
' },',
|
|
4109
|
+
' link: {',
|
|
4110
|
+
" color: '#1d4ed8',",
|
|
4111
|
+
' fontSize: 14,',
|
|
4112
|
+
" fontWeight: '800',",
|
|
4113
|
+
' lineHeight: 20,',
|
|
4114
|
+
' },',
|
|
4115
|
+
' body: {',
|
|
4116
|
+
" color: '#4b5563',",
|
|
4117
|
+
' fontSize: 14,',
|
|
4118
|
+
' lineHeight: 20,',
|
|
4119
|
+
' },',
|
|
4120
|
+
' cardChildren: {',
|
|
4121
|
+
' gap: 12,',
|
|
4122
|
+
' marginTop: 4,',
|
|
4123
|
+
' },',
|
|
4124
|
+
' exampleBox: {',
|
|
4125
|
+
" backgroundColor: '#eff6ff',",
|
|
4126
|
+
" borderColor: '#bfdbfe',",
|
|
4127
|
+
' borderRadius: 10,',
|
|
4128
|
+
' borderWidth: 1,',
|
|
4129
|
+
' gap: 10,',
|
|
4130
|
+
' padding: 10,',
|
|
4131
|
+
' },',
|
|
4132
|
+
' exampleTitle: {',
|
|
4133
|
+
" color: '#1e3a8a',",
|
|
4134
|
+
' fontSize: 13,',
|
|
4135
|
+
' fontWeight: "800",',
|
|
4136
|
+
' },',
|
|
4137
|
+
' exampleBody: {',
|
|
4138
|
+
" color: '#1e3a8a',",
|
|
4139
|
+
' fontSize: 13,',
|
|
4140
|
+
' fontWeight: "600",',
|
|
4141
|
+
' lineHeight: 18,',
|
|
4142
|
+
' },',
|
|
4143
|
+
' exampleRow: {',
|
|
1862
4144
|
' alignItems: "center",',
|
|
4145
|
+
' flexDirection: "row",',
|
|
4146
|
+
' flexWrap: "wrap",',
|
|
4147
|
+
' gap: 8,',
|
|
4148
|
+
' },',
|
|
4149
|
+
' examplePill: {',
|
|
4150
|
+
" backgroundColor: '#ffffff',",
|
|
4151
|
+
" borderColor: '#bfdbfe',",
|
|
4152
|
+
' borderRadius: 999,',
|
|
4153
|
+
' borderWidth: 1,',
|
|
4154
|
+
" color: '#1e3a8a',",
|
|
4155
|
+
' fontSize: 12,',
|
|
4156
|
+
' fontWeight: "800",',
|
|
4157
|
+
' overflow: "hidden",',
|
|
4158
|
+
' paddingHorizontal: 9,',
|
|
4159
|
+
' paddingVertical: 4,',
|
|
4160
|
+
' },',
|
|
4161
|
+
' componentLabelGrid: {',
|
|
4162
|
+
' flexDirection: "row",',
|
|
4163
|
+
' flexWrap: "wrap",',
|
|
4164
|
+
' gap: 8,',
|
|
4165
|
+
' },',
|
|
4166
|
+
' componentLabel: {',
|
|
4167
|
+
" backgroundColor: '#dbeafe',",
|
|
4168
|
+
' borderRadius: 999,',
|
|
4169
|
+
" color: '#1e3a8a',",
|
|
4170
|
+
' fontSize: 12,',
|
|
4171
|
+
' fontWeight: "800",',
|
|
4172
|
+
' overflow: "hidden",',
|
|
4173
|
+
' paddingHorizontal: 9,',
|
|
4174
|
+
' paddingVertical: 4,',
|
|
4175
|
+
' },',
|
|
4176
|
+
' fakeInput: {',
|
|
4177
|
+
" backgroundColor: '#ffffff',",
|
|
4178
|
+
" borderColor: '#bfdbfe',",
|
|
4179
|
+
' borderRadius: 8,',
|
|
4180
|
+
' borderWidth: 1,',
|
|
4181
|
+
' gap: 3,',
|
|
4182
|
+
' padding: 10,',
|
|
4183
|
+
' },',
|
|
4184
|
+
' fakeInputText: {',
|
|
4185
|
+
" color: '#64748b',",
|
|
4186
|
+
' fontSize: 11,',
|
|
4187
|
+
' fontWeight: "700",',
|
|
4188
|
+
' textTransform: "uppercase",',
|
|
4189
|
+
' },',
|
|
4190
|
+
' fakeInputValue: {',
|
|
4191
|
+
" color: '#111827',",
|
|
4192
|
+
' fontSize: 15,',
|
|
4193
|
+
' fontWeight: "800",',
|
|
4194
|
+
' },',
|
|
4195
|
+
' codeLine: {',
|
|
4196
|
+
" backgroundColor: '#0f172a',",
|
|
4197
|
+
' borderRadius: 6,',
|
|
4198
|
+
" color: '#e5e7eb',",
|
|
4199
|
+
' fontSize: 12,',
|
|
4200
|
+
' fontWeight: "700",',
|
|
4201
|
+
' paddingHorizontal: 10,',
|
|
4202
|
+
' paddingVertical: 7,',
|
|
4203
|
+
' },',
|
|
4204
|
+
' tabStrip: {',
|
|
4205
|
+
" backgroundColor: '#ffffff',",
|
|
4206
|
+
' borderRadius: 8,',
|
|
4207
|
+
' flexDirection: "row",',
|
|
4208
|
+
' gap: 6,',
|
|
4209
|
+
' padding: 6,',
|
|
4210
|
+
' },',
|
|
4211
|
+
' tabActive: {',
|
|
4212
|
+
" backgroundColor: '#111827',",
|
|
4213
|
+
' borderRadius: 7,',
|
|
4214
|
+
" color: '#ffffff',",
|
|
4215
|
+
' flex: 1,',
|
|
4216
|
+
' fontSize: 13,',
|
|
4217
|
+
' fontWeight: "800",',
|
|
4218
|
+
' overflow: "hidden",',
|
|
4219
|
+
' padding: 8,',
|
|
4220
|
+
' textAlign: "center",',
|
|
4221
|
+
' },',
|
|
4222
|
+
' tabItem: {',
|
|
4223
|
+
" backgroundColor: '#f1f5f9',",
|
|
4224
|
+
' borderRadius: 7,',
|
|
4225
|
+
" color: '#334155',",
|
|
4226
|
+
' flex: 1,',
|
|
4227
|
+
' fontSize: 13,',
|
|
4228
|
+
' fontWeight: "700",',
|
|
4229
|
+
' overflow: "hidden",',
|
|
4230
|
+
' padding: 8,',
|
|
4231
|
+
' textAlign: "center",',
|
|
4232
|
+
' },',
|
|
4233
|
+
' widgetTile: {',
|
|
4234
|
+
" backgroundColor: '#ffffff',",
|
|
4235
|
+
" borderColor: '#bfdbfe',",
|
|
4236
|
+
' borderRadius: 10,',
|
|
4237
|
+
' borderWidth: 1,',
|
|
4238
|
+
' padding: 12,',
|
|
4239
|
+
' },',
|
|
4240
|
+
' widgetTitle: {',
|
|
4241
|
+
" color: '#111827',",
|
|
4242
|
+
' fontSize: 18,',
|
|
4243
|
+
' fontWeight: "900",',
|
|
4244
|
+
' },',
|
|
4245
|
+
' widgetBody: {',
|
|
4246
|
+
" color: '#475569',",
|
|
4247
|
+
' fontSize: 13,',
|
|
4248
|
+
' fontWeight: "700",',
|
|
4249
|
+
' marginTop: 4,',
|
|
4250
|
+
' },',
|
|
4251
|
+
' transportRow: {',
|
|
4252
|
+
' flexDirection: "row",',
|
|
4253
|
+
' flexWrap: "wrap",',
|
|
4254
|
+
' gap: 8,',
|
|
4255
|
+
' },',
|
|
4256
|
+
' transportButton: {',
|
|
4257
|
+
" backgroundColor: '#ffffff',",
|
|
4258
|
+
" borderColor: '#bfdbfe',",
|
|
4259
|
+
' borderRadius: 8,',
|
|
4260
|
+
' borderWidth: 1,',
|
|
4261
|
+
" color: '#1e3a8a',",
|
|
4262
|
+
' fontSize: 13,',
|
|
4263
|
+
' fontWeight: "800",',
|
|
4264
|
+
' overflow: "hidden",',
|
|
4265
|
+
' paddingHorizontal: 10,',
|
|
1863
4266
|
' paddingVertical: 8,',
|
|
1864
4267
|
' },',
|
|
4268
|
+
' linkList: {',
|
|
4269
|
+
' gap: 8,',
|
|
4270
|
+
' paddingTop: 2,',
|
|
4271
|
+
' },',
|
|
4272
|
+
' linksCard: {',
|
|
4273
|
+
" backgroundColor: '#eef2ff',",
|
|
4274
|
+
" borderColor: '#c7d2fe',",
|
|
4275
|
+
' borderRadius: 12,',
|
|
4276
|
+
' borderWidth: 1,',
|
|
4277
|
+
' gap: 8,',
|
|
4278
|
+
' padding: 16,',
|
|
4279
|
+
' },',
|
|
4280
|
+
' linksTitle: {',
|
|
4281
|
+
" color: '#111827',",
|
|
4282
|
+
' fontSize: 17,',
|
|
4283
|
+
' fontWeight: "800",',
|
|
4284
|
+
' },',
|
|
4285
|
+
' link: {',
|
|
4286
|
+
" color: '#1d4ed8',",
|
|
4287
|
+
' fontSize: 14,',
|
|
4288
|
+
' fontWeight: "700",',
|
|
4289
|
+
' lineHeight: 21,',
|
|
4290
|
+
' },',
|
|
1865
4291
|
'});',
|
|
1866
4292
|
'',
|
|
1867
4293
|
].join('\n');
|
|
1868
4294
|
}
|
|
1869
4295
|
function renderStylistScreen(answers) {
|
|
1870
4296
|
return [
|
|
1871
|
-
"import {
|
|
4297
|
+
"import { useMemo, useState } from 'react';",
|
|
4298
|
+
"import { Alert, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';",
|
|
4299
|
+
"import ColorPicker, { HueSlider, Panel1, Preview, Swatches } from 'reanimated-color-picker';",
|
|
1872
4300
|
'',
|
|
1873
4301
|
"import { AnimatedPressable, ExpositionNotice } from '../../components/exposition';",
|
|
1874
|
-
'',
|
|
1875
|
-
'
|
|
1876
|
-
|
|
1877
|
-
"
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
"
|
|
4302
|
+
"import stylistThemeTokens from '../../theme/tokens';",
|
|
4303
|
+
'',
|
|
4304
|
+
'type StylistTheme = typeof stylistThemeTokens;',
|
|
4305
|
+
"type ColorKey = keyof StylistTheme['colors'];",
|
|
4306
|
+
'',
|
|
4307
|
+
'const colorKeys: ColorKey[] = [',
|
|
4308
|
+
" 'background',",
|
|
4309
|
+
" 'surface',",
|
|
4310
|
+
" 'text',",
|
|
4311
|
+
" 'primary',",
|
|
4312
|
+
" 'success',",
|
|
4313
|
+
" 'warning',",
|
|
1881
4314
|
'];',
|
|
1882
4315
|
'',
|
|
4316
|
+
"const spacingKeys: Array<keyof StylistTheme['layout']['spacing']> = ['xs', 'sm', 'md', 'lg', 'xl'];",
|
|
4317
|
+
"const NATIVE_SAVE_COMMAND = 'npm run stylist:sync:android';",
|
|
4318
|
+
'',
|
|
1883
4319
|
'export default function StylistScreen() {',
|
|
4320
|
+
' const [theme, setTheme] = useState<StylistTheme>(stylistThemeTokens);',
|
|
4321
|
+
" const [selectedColor, setSelectedColor] = useState<ColorKey>('primary');",
|
|
4322
|
+
" const [saveMessage, setSaveMessage] = useState('');",
|
|
4323
|
+
" const [nativeDraft, setNativeDraft] = useState('');",
|
|
4324
|
+
' const [saving, setSaving] = useState(false);',
|
|
4325
|
+
'',
|
|
4326
|
+
' const previewCard = useMemo(',
|
|
4327
|
+
' () => ({',
|
|
4328
|
+
' backgroundColor: theme.colors.surface,',
|
|
4329
|
+
' borderColor: theme.colors.primary,',
|
|
4330
|
+
' borderRadius: theme.layout.radius,',
|
|
4331
|
+
' borderWidth: 1,',
|
|
4332
|
+
' padding: theme.layout.spacing.md,',
|
|
4333
|
+
' gap: theme.layout.spacing.sm,',
|
|
4334
|
+
' }),',
|
|
4335
|
+
' [theme]',
|
|
4336
|
+
' );',
|
|
4337
|
+
'',
|
|
4338
|
+
' function updateNumeric(path: string, raw: string) {',
|
|
4339
|
+
' const value = Number.parseFloat(raw);',
|
|
4340
|
+
' if (!Number.isFinite(value)) return;',
|
|
4341
|
+
'',
|
|
4342
|
+
" if (path === 'displaySize') {",
|
|
4343
|
+
' setTheme((prev) => ({ ...prev, typography: { ...prev.typography, displaySize: value } }));',
|
|
4344
|
+
' return;',
|
|
4345
|
+
' }',
|
|
4346
|
+
" if (path === 'headingSize') {",
|
|
4347
|
+
' setTheme((prev) => ({ ...prev, typography: { ...prev.typography, headingSize: value } }));',
|
|
4348
|
+
' return;',
|
|
4349
|
+
' }',
|
|
4350
|
+
" if (path === 'bodySize') {",
|
|
4351
|
+
' setTheme((prev) => ({ ...prev, typography: { ...prev.typography, bodySize: value } }));',
|
|
4352
|
+
' return;',
|
|
4353
|
+
' }',
|
|
4354
|
+
" if (path === 'captionSize') {",
|
|
4355
|
+
' setTheme((prev) => ({ ...prev, typography: { ...prev.typography, captionSize: value } }));',
|
|
4356
|
+
' return;',
|
|
4357
|
+
' }',
|
|
4358
|
+
" if (path === 'radius') {",
|
|
4359
|
+
' setTheme((prev) => ({ ...prev, layout: { ...prev.layout, radius: value } }));',
|
|
4360
|
+
' return;',
|
|
4361
|
+
' }',
|
|
4362
|
+
'',
|
|
4363
|
+
' setTheme((prev) => ({',
|
|
4364
|
+
' ...prev,',
|
|
4365
|
+
' layout: {',
|
|
4366
|
+
' ...prev.layout,',
|
|
4367
|
+
' spacing: { ...prev.layout.spacing, [path]: value },',
|
|
4368
|
+
' },',
|
|
4369
|
+
' }));',
|
|
4370
|
+
' }',
|
|
4371
|
+
'',
|
|
4372
|
+
' async function saveTheme() {',
|
|
4373
|
+
' setSaving(true);',
|
|
4374
|
+
" setSaveMessage('');",
|
|
4375
|
+
' try {',
|
|
4376
|
+
" if (Platform.OS === 'web') {",
|
|
4377
|
+
" const response = await fetch('/exposition/stylist-sync', {",
|
|
4378
|
+
" method: 'POST',",
|
|
4379
|
+
" headers: { 'content-type': 'application/json' },",
|
|
4380
|
+
' body: JSON.stringify(theme),',
|
|
4381
|
+
' });',
|
|
4382
|
+
' const payload = await response.json();',
|
|
4383
|
+
' if (!response.ok) {',
|
|
4384
|
+
" throw new Error(payload?.error ?? 'Stylist sync failed.');",
|
|
4385
|
+
' }',
|
|
4386
|
+
' setSaveMessage(`Synced ${payload.updatedFiles?.length ?? 0} files from Stylist.`);',
|
|
4387
|
+
' } else {',
|
|
4388
|
+
' const draft = JSON.stringify(theme, null, 2);',
|
|
4389
|
+
' setNativeDraft(draft);',
|
|
4390
|
+
" setSaveMessage('Draft saved in Stylist. Run the sync command from your project root terminal.');",
|
|
4391
|
+
' }',
|
|
4392
|
+
' } catch (error) {',
|
|
4393
|
+
" const message = error instanceof Error ? error.message : 'Unknown save error.';",
|
|
4394
|
+
' Alert.alert("Stylist save failed", message);',
|
|
4395
|
+
' setSaveMessage(message);',
|
|
4396
|
+
' } finally {',
|
|
4397
|
+
' setSaving(false);',
|
|
4398
|
+
' }',
|
|
4399
|
+
' }',
|
|
4400
|
+
'',
|
|
1884
4401
|
' return (',
|
|
1885
|
-
' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
|
|
1886
|
-
` <Text style={styles.title}
|
|
1887
|
-
' <Text style={styles.intro
|
|
4402
|
+
' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={[styles.screen, { backgroundColor: theme.colors.background }]}>',
|
|
4403
|
+
` <Text style={[styles.title, { color: theme.colors.text }]}>{'${answers.appName} Stylist'}</Text>`,
|
|
4404
|
+
' <Text style={[styles.intro, { color: theme.colors.text }]}>Adjust design tokens, then save to sync `project/theme.json`, `project/style.md`, and app theme files.</Text>',
|
|
1888
4405
|
' <ExpositionNotice />',
|
|
1889
|
-
'
|
|
1890
|
-
'
|
|
1891
|
-
' <
|
|
1892
|
-
'
|
|
1893
|
-
'
|
|
1894
|
-
'
|
|
1895
|
-
'
|
|
1896
|
-
'
|
|
1897
|
-
'
|
|
4406
|
+
'',
|
|
4407
|
+
' <View style={[styles.section, { backgroundColor: theme.colors.surface, borderRadius: theme.layout.radius }]}>',
|
|
4408
|
+
' <Text style={[styles.sectionTitle, { color: theme.colors.text }]}>Color Picker</Text>',
|
|
4409
|
+
' <View style={styles.colorRow}>',
|
|
4410
|
+
' {colorKeys.map((key) => (',
|
|
4411
|
+
' <Pressable',
|
|
4412
|
+
' key={key}',
|
|
4413
|
+
' onPress={() => setSelectedColor(key)}',
|
|
4414
|
+
' style={[',
|
|
4415
|
+
' styles.colorChip,',
|
|
4416
|
+
' { backgroundColor: theme.colors[key], borderColor: selectedColor === key ? theme.colors.text : "#9ca3af" },',
|
|
4417
|
+
' ]}',
|
|
4418
|
+
' >',
|
|
4419
|
+
' <Text style={styles.colorChipLabel}>{key}</Text>',
|
|
4420
|
+
' </Pressable>',
|
|
1898
4421
|
' ))}',
|
|
1899
4422
|
' </View>',
|
|
4423
|
+
' <ColorPicker',
|
|
4424
|
+
' value={theme.colors[selectedColor]}',
|
|
4425
|
+
' onCompleteJS={({ hex }: { hex: string }) => {',
|
|
4426
|
+
' setTheme((prev) => ({',
|
|
4427
|
+
' ...prev,',
|
|
4428
|
+
' colors: { ...prev.colors, [selectedColor]: hex },',
|
|
4429
|
+
' }));',
|
|
4430
|
+
' }}',
|
|
4431
|
+
' style={styles.picker}',
|
|
4432
|
+
' >',
|
|
4433
|
+
' <Preview hideInitialColor />',
|
|
4434
|
+
' <Panel1 />',
|
|
4435
|
+
' <HueSlider />',
|
|
4436
|
+
' <Swatches />',
|
|
4437
|
+
' </ColorPicker>',
|
|
1900
4438
|
' </View>',
|
|
1901
|
-
'
|
|
1902
|
-
'
|
|
1903
|
-
' <Text style={styles.
|
|
1904
|
-
' <
|
|
1905
|
-
' <
|
|
1906
|
-
'
|
|
4439
|
+
'',
|
|
4440
|
+
' <View style={[styles.section, { backgroundColor: theme.colors.surface, borderRadius: theme.layout.radius }]}>',
|
|
4441
|
+
' <Text style={[styles.sectionTitle, { color: theme.colors.text }]}>Typography</Text>',
|
|
4442
|
+
' <TextInput value={theme.typography.fontFamily} onChangeText={(fontFamily) => setTheme((prev) => ({ ...prev, typography: { ...prev.typography, fontFamily } }))} style={styles.input} placeholder="Font family" />',
|
|
4443
|
+
' <View style={styles.grid}>',
|
|
4444
|
+
' <NumberField label="Display" value={theme.typography.displaySize} onChange={(value) => updateNumeric("displaySize", value)} />',
|
|
4445
|
+
' <NumberField label="Heading" value={theme.typography.headingSize} onChange={(value) => updateNumeric("headingSize", value)} />',
|
|
4446
|
+
' <NumberField label="Body" value={theme.typography.bodySize} onChange={(value) => updateNumeric("bodySize", value)} />',
|
|
4447
|
+
' <NumberField label="Caption" value={theme.typography.captionSize} onChange={(value) => updateNumeric("captionSize", value)} />',
|
|
4448
|
+
' </View>',
|
|
4449
|
+
' </View>',
|
|
4450
|
+
'',
|
|
4451
|
+
' <View style={[styles.section, { backgroundColor: theme.colors.surface, borderRadius: theme.layout.radius }]}>',
|
|
4452
|
+
' <Text style={[styles.sectionTitle, { color: theme.colors.text }]}>Layout Tokens</Text>',
|
|
4453
|
+
' <NumberField label="Radius" value={theme.layout.radius} onChange={(value) => updateNumeric("radius", value)} />',
|
|
4454
|
+
' <View style={styles.grid}>',
|
|
4455
|
+
' {spacingKeys.map((key) => (',
|
|
4456
|
+
' <NumberField',
|
|
4457
|
+
' key={key}',
|
|
4458
|
+
' label={`Spacing ${key}`}',
|
|
4459
|
+
' value={theme.layout.spacing[key]}',
|
|
4460
|
+
' onChange={(value) => updateNumeric(key, value)}',
|
|
4461
|
+
' />',
|
|
4462
|
+
' ))}',
|
|
4463
|
+
' </View>',
|
|
1907
4464
|
' </View>',
|
|
1908
|
-
'
|
|
1909
|
-
'
|
|
4465
|
+
'',
|
|
4466
|
+
' <View style={previewCard}>',
|
|
4467
|
+
' <Text style={{ color: theme.colors.text, fontFamily: theme.typography.fontFamily, fontSize: theme.typography.displaySize, fontWeight: theme.typography.fontFamily === "System" || theme.typography.fontFamily === "monospace" ? "900" : "normal" }}>Display headline</Text>',
|
|
4468
|
+
' <Text style={{ color: theme.colors.text, fontFamily: theme.typography.fontFamily, fontSize: theme.typography.headingSize, fontWeight: theme.typography.fontFamily === "System" || theme.typography.fontFamily === "monospace" ? "800" : "normal" }}>Section heading</Text>',
|
|
4469
|
+
' <Text style={{ color: theme.colors.text, fontFamily: theme.typography.fontFamily, fontSize: theme.typography.bodySize }}>Readable body copy for product screens, onboarding, settings, and forms.</Text>',
|
|
4470
|
+
' <Text style={{ color: theme.colors.text, fontFamily: theme.typography.fontFamily, fontSize: theme.typography.captionSize, textTransform: "uppercase" }}>Caption and metadata text</Text>',
|
|
1910
4471
|
' <AnimatedPressable label="Primary action" />',
|
|
1911
|
-
' <TextInput placeholder="Input state" style={styles.input} />',
|
|
1912
4472
|
' </View>',
|
|
1913
|
-
'
|
|
1914
|
-
'
|
|
1915
|
-
' <
|
|
1916
|
-
'
|
|
1917
|
-
'
|
|
4473
|
+
'',
|
|
4474
|
+
' <Pressable onPress={saveTheme} disabled={saving} style={[styles.saveButton, { backgroundColor: theme.colors.primary }]}>',
|
|
4475
|
+
' <Text style={styles.saveButtonText}>{saving ? "Saving..." : "Save Theme"}</Text>',
|
|
4476
|
+
' </Pressable>',
|
|
4477
|
+
' {saveMessage ? <Text style={styles.saveMessage}>{saveMessage}</Text> : null}',
|
|
4478
|
+
' {Platform.OS !== "web" ? (',
|
|
4479
|
+
' <View style={styles.nativeHelp}>',
|
|
4480
|
+
' <Text style={styles.nativeTitle}>Native fallback</Text>',
|
|
4481
|
+
' <Text style={styles.nativeBody}>Run this command in your app root terminal:</Text>',
|
|
4482
|
+
' <Text style={styles.command}>{NATIVE_SAVE_COMMAND}</Text>',
|
|
4483
|
+
' {nativeDraft ? <Text style={styles.payload}>{nativeDraft}</Text> : null}',
|
|
1918
4484
|
' </View>',
|
|
1919
|
-
'
|
|
4485
|
+
' ) : null}',
|
|
1920
4486
|
' </ScrollView>',
|
|
1921
4487
|
' );',
|
|
1922
4488
|
'}',
|
|
1923
4489
|
'',
|
|
4490
|
+
'function NumberField(props: { label: string; value: number; onChange: (value: string) => void }) {',
|
|
4491
|
+
' return (',
|
|
4492
|
+
' <View style={styles.field}>',
|
|
4493
|
+
' <Text style={styles.fieldLabel}>{props.label}</Text>',
|
|
4494
|
+
' <TextInput',
|
|
4495
|
+
' value={String(props.value)}',
|
|
4496
|
+
' onChangeText={props.onChange}',
|
|
4497
|
+
' keyboardType="numeric"',
|
|
4498
|
+
' style={styles.input}',
|
|
4499
|
+
' />',
|
|
4500
|
+
' </View>',
|
|
4501
|
+
' );',
|
|
4502
|
+
'}',
|
|
4503
|
+
'',
|
|
1924
4504
|
'const styles = StyleSheet.create({',
|
|
1925
4505
|
' screen: {',
|
|
1926
|
-
" backgroundColor: '#ffffff',",
|
|
1927
4506
|
' flex: 1,',
|
|
1928
4507
|
' },',
|
|
1929
4508
|
' content: {',
|
|
@@ -1931,87 +4510,137 @@ function renderStylistScreen(answers) {
|
|
|
1931
4510
|
' padding: 20,',
|
|
1932
4511
|
' },',
|
|
1933
4512
|
' title: {',
|
|
1934
|
-
" color: '#111827',",
|
|
1935
4513
|
' fontSize: 30,',
|
|
1936
4514
|
' fontWeight: "900",',
|
|
1937
4515
|
' },',
|
|
1938
4516
|
' intro: {',
|
|
1939
|
-
|
|
1940
|
-
'
|
|
1941
|
-
' lineHeight: 24,',
|
|
4517
|
+
' fontSize: 15,',
|
|
4518
|
+
' lineHeight: 22,',
|
|
1942
4519
|
' },',
|
|
1943
4520
|
' section: {',
|
|
1944
|
-
" backgroundColor: '#f9fafb',",
|
|
1945
|
-
' borderRadius: 12,',
|
|
1946
4521
|
' gap: 12,',
|
|
1947
4522
|
' padding: 16,',
|
|
1948
4523
|
' },',
|
|
1949
4524
|
' sectionTitle: {',
|
|
1950
|
-
" color: '#111827',",
|
|
1951
4525
|
' fontSize: 18,',
|
|
1952
4526
|
' fontWeight: "800",',
|
|
1953
4527
|
' },',
|
|
1954
|
-
'
|
|
4528
|
+
' colorRow: {',
|
|
1955
4529
|
' flexDirection: "row",',
|
|
1956
4530
|
' flexWrap: "wrap",',
|
|
1957
|
-
' gap:
|
|
1958
|
-
' },',
|
|
1959
|
-
' swatchItem: {',
|
|
1960
|
-
' minWidth: 92,',
|
|
1961
|
-
' },',
|
|
1962
|
-
' swatch: {',
|
|
1963
|
-
' borderRadius: 10,',
|
|
1964
|
-
' height: 44,',
|
|
4531
|
+
' gap: 8,',
|
|
1965
4532
|
' },',
|
|
1966
|
-
'
|
|
1967
|
-
|
|
1968
|
-
'
|
|
1969
|
-
'
|
|
4533
|
+
' colorChip: {',
|
|
4534
|
+
' borderRadius: 999,',
|
|
4535
|
+
' borderWidth: 2,',
|
|
4536
|
+
' minWidth: 94,',
|
|
4537
|
+
' paddingHorizontal: 10,',
|
|
4538
|
+
' paddingVertical: 8,',
|
|
1970
4539
|
' },',
|
|
1971
|
-
'
|
|
1972
|
-
|
|
4540
|
+
' colorChipLabel: {',
|
|
4541
|
+
' color: "#ffffff",',
|
|
1973
4542
|
' fontSize: 12,',
|
|
4543
|
+
' fontWeight: "700",',
|
|
4544
|
+
' textTransform: "capitalize",',
|
|
1974
4545
|
' },',
|
|
1975
|
-
'
|
|
1976
|
-
|
|
1977
|
-
'
|
|
1978
|
-
' fontWeight: "900",',
|
|
4546
|
+
' picker: {',
|
|
4547
|
+
' gap: 12,',
|
|
4548
|
+
' width: "100%",',
|
|
1979
4549
|
' },',
|
|
1980
|
-
'
|
|
1981
|
-
|
|
1982
|
-
'
|
|
1983
|
-
'
|
|
4550
|
+
' grid: {',
|
|
4551
|
+
' flexDirection: "row",',
|
|
4552
|
+
' flexWrap: "wrap",',
|
|
4553
|
+
' gap: 10,',
|
|
1984
4554
|
' },',
|
|
1985
|
-
'
|
|
1986
|
-
|
|
1987
|
-
'
|
|
1988
|
-
'
|
|
4555
|
+
' field: {',
|
|
4556
|
+
' flexBasis: "48%",',
|
|
4557
|
+
' flexGrow: 1,',
|
|
4558
|
+
' gap: 6,',
|
|
1989
4559
|
' },',
|
|
1990
|
-
'
|
|
1991
|
-
|
|
4560
|
+
' fieldLabel: {',
|
|
4561
|
+
' color: "#374151",',
|
|
1992
4562
|
' fontSize: 12,',
|
|
1993
4563
|
' fontWeight: "700",',
|
|
1994
|
-
' textTransform: "uppercase",',
|
|
1995
4564
|
' },',
|
|
1996
4565
|
' input: {',
|
|
1997
|
-
|
|
4566
|
+
' backgroundColor: "#ffffff",',
|
|
4567
|
+
' borderColor: "#d1d5db",',
|
|
1998
4568
|
' borderRadius: 10,',
|
|
1999
4569
|
' borderWidth: 1,',
|
|
2000
|
-
' minHeight:
|
|
4570
|
+
' minHeight: 42,',
|
|
2001
4571
|
' paddingHorizontal: 12,',
|
|
2002
4572
|
' },',
|
|
2003
|
-
'
|
|
2004
|
-
|
|
2005
|
-
|
|
4573
|
+
' saveButton: {',
|
|
4574
|
+
' borderRadius: 12,',
|
|
4575
|
+
' minHeight: 48,',
|
|
4576
|
+
' alignItems: "center",',
|
|
4577
|
+
' justifyContent: "center",',
|
|
4578
|
+
' },',
|
|
4579
|
+
' saveButtonText: {',
|
|
4580
|
+
' color: "#ffffff",',
|
|
4581
|
+
' fontSize: 16,',
|
|
4582
|
+
' fontWeight: "800",',
|
|
4583
|
+
' },',
|
|
4584
|
+
' saveMessage: {',
|
|
4585
|
+
' color: "#374151",',
|
|
4586
|
+
' fontSize: 13,',
|
|
4587
|
+
' },',
|
|
4588
|
+
' nativeHelp: {',
|
|
4589
|
+
' backgroundColor: "#ffffff",',
|
|
4590
|
+
' borderColor: "#e5e7eb",',
|
|
2006
4591
|
' borderRadius: 12,',
|
|
2007
4592
|
' borderWidth: 1,',
|
|
2008
4593
|
' gap: 8,',
|
|
2009
|
-
' padding:
|
|
4594
|
+
' padding: 12,',
|
|
4595
|
+
' },',
|
|
4596
|
+
' nativeTitle: {',
|
|
4597
|
+
' color: "#111827",',
|
|
4598
|
+
' fontSize: 14,',
|
|
4599
|
+
' fontWeight: "800",',
|
|
4600
|
+
' },',
|
|
4601
|
+
' nativeBody: {',
|
|
4602
|
+
' color: "#374151",',
|
|
4603
|
+
' fontSize: 12,',
|
|
4604
|
+
' },',
|
|
4605
|
+
' command: {',
|
|
4606
|
+
' backgroundColor: "#111827",',
|
|
4607
|
+
' borderRadius: 8,',
|
|
4608
|
+
' color: "#f9fafb",',
|
|
4609
|
+
' fontFamily: "monospace",',
|
|
4610
|
+
' fontSize: 12,',
|
|
4611
|
+
' padding: 10,',
|
|
4612
|
+
' },',
|
|
4613
|
+
' payload: {',
|
|
4614
|
+
' color: "#1f2937",',
|
|
4615
|
+
' fontFamily: "monospace",',
|
|
4616
|
+
' fontSize: 11,',
|
|
4617
|
+
' lineHeight: 16,',
|
|
2010
4618
|
' },',
|
|
2011
4619
|
'});',
|
|
2012
4620
|
'',
|
|
2013
4621
|
].join('\n');
|
|
2014
4622
|
}
|
|
4623
|
+
function renderEmbeddedFonts() {
|
|
4624
|
+
return [
|
|
4625
|
+
'export const EMBEDDED_GOOGLE_FONTS: string[] = [',
|
|
4626
|
+
" 'Inter',",
|
|
4627
|
+
" 'DM Sans',",
|
|
4628
|
+
" 'DM Serif Display',",
|
|
4629
|
+
" 'Noto Sans',",
|
|
4630
|
+
" 'Noto Sans Display',",
|
|
4631
|
+
" 'Noto Sans Mono',",
|
|
4632
|
+
" 'Noto Serif',",
|
|
4633
|
+
" 'Noto Serif Display',",
|
|
4634
|
+
" 'Playfair Display',",
|
|
4635
|
+
" 'Roboto',",
|
|
4636
|
+
" 'Roboto Mono',",
|
|
4637
|
+
" 'Source Sans 3',",
|
|
4638
|
+
" 'Space Grotesk',",
|
|
4639
|
+
" 'Work Sans',",
|
|
4640
|
+
'];',
|
|
4641
|
+
'',
|
|
4642
|
+
].join('\n');
|
|
4643
|
+
}
|
|
2015
4644
|
function renderDataScreen(answers) {
|
|
2016
4645
|
if (answers.dataStart === 'supabase') {
|
|
2017
4646
|
return renderSupabaseDataScreen(answers);
|
|
@@ -2022,12 +4651,15 @@ function renderDataScreen(answers) {
|
|
|
2022
4651
|
'',
|
|
2023
4652
|
"import { ExpositionNotice } from '../../components/exposition';",
|
|
2024
4653
|
"import { addLocalTask, getLocalAppSnapshot } from '../../services/local-data';",
|
|
4654
|
+
"import { useAppTheme } from '../../theme/provider';",
|
|
2025
4655
|
'',
|
|
2026
4656
|
"import type { appSnapshot } from '../../data/mock-app';",
|
|
2027
4657
|
'',
|
|
2028
4658
|
'type Snapshot = typeof appSnapshot;',
|
|
2029
4659
|
'',
|
|
2030
4660
|
'export default function DataScreen() {',
|
|
4661
|
+
' const theme = useAppTheme();',
|
|
4662
|
+
' const colors = theme.activeColors;',
|
|
2031
4663
|
' const [snapshot, setSnapshot] = useState<Snapshot | null>(null);',
|
|
2032
4664
|
'',
|
|
2033
4665
|
' useEffect(() => {',
|
|
@@ -2039,17 +4671,17 @@ function renderDataScreen(answers) {
|
|
|
2039
4671
|
' }',
|
|
2040
4672
|
'',
|
|
2041
4673
|
' return (',
|
|
2042
|
-
' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
|
|
2043
|
-
' <Text style={styles.title}>Data Exposition</Text>',
|
|
2044
|
-
' <Text style={styles.intro}>This app starts with a web-safe local adapter and a native Expo SQLite adapter. Keep the boundary, then swap implementation details when Supabase is ready.</Text>',
|
|
4674
|
+
' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={[styles.screen, { backgroundColor: colors.background }]}>',
|
|
4675
|
+
' <Text style={[styles.title, { color: colors.text, fontFamily: theme.typography.fontFamily, fontWeight: theme.typography.fontFamily === \"System\" || theme.typography.fontFamily === \"monospace\" ? \"800\" : \"normal\" }]}>Data Exposition</Text>',
|
|
4676
|
+
' <Text style={[styles.intro, { color: colors.text }]}>This app starts with a web-safe local adapter and a native Expo SQLite adapter. Keep the boundary, then swap implementation details when Supabase is ready.</Text>',
|
|
2045
4677
|
' <ExpositionNotice />',
|
|
2046
|
-
' <Pressable onPress={addTask} style={styles.button}>',
|
|
4678
|
+
' <Pressable onPress={addTask} style={[styles.button, { backgroundColor: colors.primary, borderRadius: theme.layout.radius }]}>',
|
|
2047
4679
|
' <Text style={styles.buttonText}>Insert a local task</Text>',
|
|
2048
4680
|
' </Pressable>',
|
|
2049
4681
|
' {snapshot?.tasks.map((task) => (',
|
|
2050
|
-
' <View key={task.id} style={styles.taskCard}>',
|
|
2051
|
-
' <Text style={styles.taskTitle}>{task.title}</Text>',
|
|
2052
|
-
' <Text style={styles.taskStatus}>{task.status}</Text>',
|
|
4682
|
+
' <View key={task.id} style={[styles.taskCard, { backgroundColor: colors.surface, borderColor: colors.primary, borderRadius: theme.layout.radius }]}>',
|
|
4683
|
+
' <Text style={[styles.taskTitle, { color: colors.text }]}>{task.title}</Text>',
|
|
4684
|
+
' <Text style={[styles.taskStatus, { color: colors.text }]}>{task.status}</Text>',
|
|
2053
4685
|
' </View>',
|
|
2054
4686
|
' ))}',
|
|
2055
4687
|
' <View style={styles.guidance}>',
|
|
@@ -2068,12 +4700,16 @@ function renderSupabaseDataScreen(answers) {
|
|
|
2068
4700
|
"import { ScrollView, StyleSheet, Text, View } from 'react-native';",
|
|
2069
4701
|
'',
|
|
2070
4702
|
"import { ExpositionNotice } from '../../components/exposition';",
|
|
4703
|
+
"import { useAppTheme } from '../../theme/provider';",
|
|
2071
4704
|
'',
|
|
2072
4705
|
'export default function DataScreen() {',
|
|
4706
|
+
' const theme = useAppTheme();',
|
|
4707
|
+
' const colors = theme.activeColors;',
|
|
4708
|
+
'',
|
|
2073
4709
|
' return (',
|
|
2074
|
-
' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
|
|
2075
|
-
' <Text style={styles.title}>Data Exposition</Text>',
|
|
2076
|
-
` <Text style={styles.intro}>${answers.appName} is set to start with Supabase. Keep the adapter boundary in src/services so screens stay independent from backend details.</Text>`,
|
|
4710
|
+
' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={[styles.screen, { backgroundColor: colors.background }]}>',
|
|
4711
|
+
' <Text style={[styles.title, { color: colors.text, fontFamily: theme.typography.fontFamily, fontWeight: theme.typography.fontFamily === \"System\" || theme.typography.fontFamily === \"monospace\" ? \"800\" : \"normal\" }]}>Data Exposition</Text>',
|
|
4712
|
+
` <Text style={[styles.intro, { color: colors.text }]}>${answers.appName} is set to start with Supabase. Keep the adapter boundary in src/services so screens stay independent from backend details.</Text>`,
|
|
2077
4713
|
' <ExpositionNotice />',
|
|
2078
4714
|
' <View style={styles.guidance}>',
|
|
2079
4715
|
' <Text style={styles.sectionTitle}>Two Supabase projects</Text>',
|