@mr.dj2u/cli 0.1.5 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/bundles/claude-code/.claude-plugin/plugin.json +20 -12
  2. package/bundles/claude-code/.mcp.json +11 -8
  3. package/bundles/claude-code/README.md +31 -11
  4. package/bundles/claude-code/commands/continue-development.md +3 -3
  5. package/bundles/claude-code/commands/create-expo-super-stack.md +1 -1
  6. package/bundles/claude-code/commands/fix-seo.md +1 -1
  7. package/bundles/claude-code/commands/prepare-deploy.md +2 -4
  8. package/bundles/claude-code/commands/project-research-plan.md +1 -1
  9. package/bundles/claude-code/commands/push-merge-loop.md +25 -0
  10. package/bundles/claude-code/commands/review-expo-project.md +3 -6
  11. package/bundles/claude-code/commands/run-doctor.md +4 -6
  12. package/bundles/claude-code/commands/wrap-up.md +67 -0
  13. package/bundles/claude-code/skills/continue-development/SKILL.md +3 -3
  14. package/bundles/claude-code/skills/create-expo-super-stack/SKILL.md +1 -1
  15. package/bundles/claude-code/skills/dev-server-management/SKILL.md +1 -1
  16. package/bundles/claude-code/skills/fix-seo/SKILL.md +1 -1
  17. package/bundles/claude-code/skills/prepare-deploy/SKILL.md +2 -4
  18. package/bundles/claude-code/skills/project-research-plan/SKILL.md +1 -1
  19. package/bundles/claude-code/skills/push-merge-loop/SKILL.md +30 -0
  20. package/bundles/claude-code/skills/review-expo-project/SKILL.md +3 -6
  21. package/bundles/claude-code/skills/run-doctor/SKILL.md +4 -6
  22. package/bundles/claude-code/skills/wrap-up/SKILL.md +72 -0
  23. package/bundles/codex/.codex-plugin/plugin.json +4 -4
  24. package/bundles/codex/commands/continue-development.md +3 -3
  25. package/bundles/codex/commands/create-expo-super-stack.md +1 -1
  26. package/bundles/codex/commands/fix-seo.md +1 -1
  27. package/bundles/codex/commands/prepare-deploy.md +2 -4
  28. package/bundles/codex/commands/project-research-plan.md +1 -1
  29. package/bundles/codex/commands/push-merge-loop.md +24 -0
  30. package/bundles/codex/commands/review-expo-project.md +3 -6
  31. package/bundles/codex/commands/run-doctor.md +4 -6
  32. package/bundles/codex/commands/wrap-up.md +67 -0
  33. package/bundles/codex/skills/workflow-continue-development/SKILL.md +3 -3
  34. package/bundles/codex/skills/workflow-create-expo-super-stack/SKILL.md +1 -1
  35. package/bundles/codex/skills/workflow-fix-seo/SKILL.md +1 -1
  36. package/bundles/codex/skills/workflow-prepare-deploy/SKILL.md +2 -4
  37. package/bundles/codex/skills/workflow-project-research-plan/SKILL.md +1 -1
  38. package/bundles/codex/skills/workflow-push-merge-loop/SKILL.md +36 -0
  39. package/bundles/codex/skills/workflow-review-expo-project/SKILL.md +3 -6
  40. package/bundles/codex/skills/workflow-run-doctor/SKILL.md +4 -6
  41. package/bundles/codex/skills/workflow-wrap-up/SKILL.md +79 -0
  42. package/bundles/vscode-copilot/.github/prompts/continue-development.prompt.md +3 -3
  43. package/bundles/vscode-copilot/.github/prompts/create-expo-super-stack.prompt.md +1 -1
  44. package/bundles/vscode-copilot/.github/prompts/fix-seo.prompt.md +1 -1
  45. package/bundles/vscode-copilot/.github/prompts/prepare-deploy.prompt.md +2 -4
  46. package/bundles/vscode-copilot/.github/prompts/project-research-plan.prompt.md +1 -1
  47. package/bundles/vscode-copilot/.github/prompts/push-merge-loop.prompt.md +30 -0
  48. package/bundles/vscode-copilot/.github/prompts/review-expo-project.prompt.md +3 -6
  49. package/bundles/vscode-copilot/.github/prompts/run-doctor.prompt.md +4 -6
  50. package/bundles/vscode-copilot/.github/prompts/wrap-up.prompt.md +72 -0
  51. package/bundles/vscode-copilot/user/.copilot/skills/workflow-continue-development/SKILL.md +3 -3
  52. package/bundles/vscode-copilot/user/.copilot/skills/workflow-create-expo-super-stack/SKILL.md +1 -1
  53. package/bundles/vscode-copilot/user/.copilot/skills/workflow-fix-seo/SKILL.md +1 -1
  54. package/bundles/vscode-copilot/user/.copilot/skills/workflow-prepare-deploy/SKILL.md +2 -4
  55. package/bundles/vscode-copilot/user/.copilot/skills/workflow-project-research-plan/SKILL.md +1 -1
  56. package/bundles/vscode-copilot/user/.copilot/skills/workflow-push-merge-loop/SKILL.md +30 -0
  57. package/bundles/vscode-copilot/user/.copilot/skills/workflow-review-expo-project/SKILL.md +3 -6
  58. package/bundles/vscode-copilot/user/.copilot/skills/workflow-run-doctor/SKILL.md +4 -6
  59. package/bundles/vscode-copilot/user/.copilot/skills/workflow-wrap-up/SKILL.md +72 -0
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +135 -1
  62. package/dist/cli.js.map +1 -1
  63. package/dist/commands/continue.d.ts +1 -1
  64. package/dist/commands/continue.d.ts.map +1 -1
  65. package/dist/commands/continue.js +21 -0
  66. package/dist/commands/continue.js.map +1 -1
  67. package/dist/commands/dev-tools.d.ts.map +1 -1
  68. package/dist/commands/dev-tools.js +22 -4
  69. package/dist/commands/dev-tools.js.map +1 -1
  70. package/dist/commands/eject.d.ts +12 -0
  71. package/dist/commands/eject.d.ts.map +1 -0
  72. package/dist/commands/eject.js +328 -0
  73. package/dist/commands/eject.js.map +1 -0
  74. package/dist/commands/onboard.d.ts +29 -1
  75. package/dist/commands/onboard.d.ts.map +1 -1
  76. package/dist/commands/onboard.js +223 -23
  77. package/dist/commands/onboard.js.map +1 -1
  78. package/dist/commands/stylist.d.ts +25 -0
  79. package/dist/commands/stylist.d.ts.map +1 -0
  80. package/dist/commands/stylist.js +392 -0
  81. package/dist/commands/stylist.js.map +1 -0
  82. package/dist/project-memory.d.ts +2 -0
  83. package/dist/project-memory.d.ts.map +1 -1
  84. package/dist/project-memory.js +3043 -399
  85. package/dist/project-memory.js.map +1 -1
  86. package/dist/stylist-theme.d.ts +104 -0
  87. package/dist/stylist-theme.d.ts.map +1 -0
  88. package/dist/stylist-theme.js +1374 -0
  89. package/dist/stylist-theme.js.map +1 -0
  90. package/package.json +1 -1
  91. package/templates/embedded-fonts.template.ts +72 -0
  92. package/templates/expo-sdk-56-screen-universal.template.tsx +709 -0
  93. package/templates/project/guidelines.md +4 -3
  94. package/templates/stylist-screen.template.tsx +3446 -0
@@ -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.2.1',
7
- 'react-native-screens': '~4.23.0',
8
- 'react-native-svg': '15.15.3',
9
- 'react-native-keyboard-controller': '1.20.7',
10
- 'react-native-worklets': '0.7.4',
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': '~55.0.15',
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 EXPOSITION_NOTICE = 'These exposition pages are temporary developer and client-research scaffolds. Use them to evaluate styling, base packages, and data direction, then delete or prune them before production once the app direction is settled.';
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',
@@ -33,6 +46,7 @@ const INFO_HEADINGS = [
33
46
  'Non-Goals',
34
47
  'Core Features',
35
48
  'Core User Flows',
49
+ 'Must-Include Screens Or Flows',
36
50
  'Data And Backend',
37
51
  'Platforms',
38
52
  'Package Choices',
@@ -84,14 +98,54 @@ export async function scaffoldProjectMemory(projectPath, answers, options = {})
84
98
  }
85
99
  export async function scaffoldRichBoilerplate(projectPath, answers, force, options = { manageUniwind: true }) {
86
100
  const results = [];
87
- await mkdir(path.join(projectPath, 'src', 'features', 'home'), { recursive: true });
88
- await mkdir(path.join(projectPath, 'src', 'features', 'onboarding'), { recursive: true });
89
- await mkdir(path.join(projectPath, 'src', 'features', 'settings'), { recursive: true });
90
- await mkdir(path.join(projectPath, 'src', 'features', 'exposition'), { recursive: true });
91
- await mkdir(path.join(projectPath, 'src', 'components', 'exposition'), { recursive: true });
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
+ }
92
134
  await mkdir(path.join(projectPath, 'src', 'data'), { recursive: true });
93
135
  await mkdir(path.join(projectPath, 'src', 'services'), { recursive: true });
94
- results.push(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', '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), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'onboarding', 'onboarding-screen.tsx'), renderOnboardingScreen(), 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(answers), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'exposition', 'style-guide-screen.tsx'), renderStyleGuideScreen(answers), force), await writeIfAllowed(path.join(projectPath, 'src', 'features', 'exposition', 'data-screen.tsx'), renderDataScreen(answers), force));
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
+ }
95
149
  if (answers.dataStart === 'local') {
96
150
  results.push(await writeIfAllowed(path.join(projectPath, 'src', 'services', 'local-data.native.ts'), renderNativeLocalDataService(), force));
97
151
  }
@@ -99,11 +153,11 @@ export async function scaffoldRichBoilerplate(projectPath, answers, force, optio
99
153
  const expositionRouteDir = path.join(appDir, 'exposition');
100
154
  await mkdir(expositionRouteDir, { recursive: true });
101
155
  if (await pathExists(appDir)) {
102
- const routeForce = force || !answers.includeCreateExpoComponents;
156
+ const routeForce = true;
103
157
  const shouldWriteRootLayout = routeForce && (await canWriteRichRootLayout(path.join(appDir, '_layout.tsx')));
104
- results.push(await writeIfAllowed(path.join(appDir, 'index.tsx'), renderRouteExport(appDir, path.join(projectPath, 'src', 'features', 'home', 'home-screen')), routeForce), await writeIfAllowed(path.join(appDir, 'onboarding.tsx'), renderRouteExport(appDir, path.join(projectPath, 'src', 'features', 'onboarding', 'onboarding-screen')), routeForce), await writeIfAllowed(path.join(appDir, 'settings.tsx'), renderRouteExport(appDir, path.join(projectPath, 'src', 'features', 'settings', 'settings-screen')), routeForce), await writeIfAllowed(path.join(expositionRouteDir, 'index.tsx'), renderRouteExport(expositionRouteDir, path.join(projectPath, 'src', 'features', 'exposition', 'exposition-screen')), routeForce), await writeIfAllowed(path.join(expositionRouteDir, 'style-guide.tsx'), renderRouteExport(expositionRouteDir, path.join(projectPath, 'src', 'features', 'exposition', 'style-guide-screen')), routeForce), await writeIfAllowed(path.join(expositionRouteDir, 'data.tsx'), renderRouteExport(expositionRouteDir, path.join(projectPath, 'src', 'features', 'exposition', 'data-screen')), routeForce));
158
+ results.push(...(await scaffoldNavigationRoutes(projectPath, appDir, navigationShell, answers, routeForce)));
105
159
  if (shouldWriteRootLayout) {
106
- 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));
107
161
  }
108
162
  if (!answers.includeCreateExpoComponents) {
109
163
  await removeOptionalFile(path.join(appDir, 'details.tsx'));
@@ -113,7 +167,9 @@ export async function scaffoldRichBoilerplate(projectPath, answers, force, optio
113
167
  results.push(await writeIfAllowed(path.join(projectPath, 'src', 'services', 'supabase.ts'), renderSupabaseClient(), force));
114
168
  }
115
169
  if (answers.testToMainSafeguards) {
116
- await mkdir(path.join(projectPath, '.github', 'workflows'), { recursive: true });
170
+ await mkdir(path.join(projectPath, '.github', 'workflows'), {
171
+ recursive: true,
172
+ });
117
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));
118
174
  }
119
175
  if (options.manageUniwind) {
@@ -126,6 +182,7 @@ export async function scaffoldRichBoilerplate(projectPath, answers, force, optio
126
182
  await removeNativeWindArtifacts(projectPath);
127
183
  }
128
184
  await ensureGlobalCssImport(projectPath, answers.appDirectory);
185
+ results.push(...(await ensureExpoRouterGroupLayouts(appDir, navigationShell, answers)));
129
186
  return results;
130
187
  }
131
188
  export function renderInfo(projectPath, answers, existingInfo) {
@@ -157,6 +214,12 @@ export function renderInfo(projectPath, answers, existingInfo) {
157
214
  '',
158
215
  answers.coreFlows,
159
216
  '',
217
+ '## Must-Include Screens Or Flows',
218
+ '',
219
+ answers.screens?.trim()
220
+ ? answers.screens
221
+ : '# TodoForContext(optional): List any known screens or flows that must be included in planning and implementation.',
222
+ '',
160
223
  '## Data And Backend',
161
224
  '',
162
225
  answers.dataNeeds,
@@ -173,6 +236,7 @@ export function renderInfo(projectPath, answers, existingInfo) {
173
236
  `- Web output: ${answers.webOutput}`,
174
237
  `- Deployed server: ${formatServerChoice(answers.deployedServer)}`,
175
238
  `- Expo UI: ${formatBoolean(answers.usesExpoUi)}`,
239
+ `- Expo UI Universal components: ${formatBoolean(answers.usesExpoUiUniversalComponents)}`,
176
240
  `- Expo Native Tabs: ${formatBoolean(answers.usesExpoNativeTabs)}`,
177
241
  '',
178
242
  '## Package Choices',
@@ -216,7 +280,7 @@ export function renderInfo(projectPath, answers, existingInfo) {
216
280
  '',
217
281
  '> Quick-reference stack summary for agents and collaborators. Fill in or correct any items marked below.',
218
282
  '',
219
- `- **App:** ${answers.appName} ${answers.audience}`,
283
+ `- **App:** ${answers.appName} — ${answers.audience}`,
220
284
  '- **Language:** TypeScript',
221
285
  '- **Package manager:** # TodoForContext(optional): pnpm / npm / yarn / bun',
222
286
  `- **Routing:** Expo Router (${formatAppDirectory(answers.appDirectory)})`,
@@ -237,6 +301,7 @@ export function renderInfo(projectPath, answers, existingInfo) {
237
301
  `- Latest Expo SDK preference: ${formatBoolean(answers.useLatestExpoSdk)}`,
238
302
  `- MDS guidelines template: yes`,
239
303
  `- Expo UI: ${formatBoolean(answers.usesExpoUi)}`,
304
+ `- Expo UI Universal components: ${formatBoolean(answers.usesExpoUiUniversalComponents)}`,
240
305
  `- Expo Native Tabs: ${formatBoolean(answers.usesExpoNativeTabs)}`,
241
306
  `- Test-to-main safeguards: ${formatBoolean(answers.testToMainSafeguards)}`,
242
307
  `- Data start: ${formatDataStart(answers.dataStart)}`,
@@ -249,22 +314,23 @@ export function renderTodo(answers) {
249
314
  return [
250
315
  `# ${answers.appName} TODO`,
251
316
  '',
252
- '## Next Steps After Onboarding',
317
+ '## Phase 0: Orientation And Planning',
253
318
  '',
254
- '- [ ] Play with styling in the style-guide page.',
255
319
  '- [ ] Browse exposition pages to understand included base packages.',
320
+ "- [ ] Review styling in the 'Stylist' page.",
256
321
  '- [ ] Review `project/` files for accuracy and planning adjustments.',
257
- '- [ ] Resolve every `# TodoForContext(optional):` marker by filling the section underneath or deleting the marker line to acknowledge no extra context is needed.',
258
- '- [ ] Tell the agent to commence development phase by phase.',
259
- '',
260
- '## Phase 0: Orientation And Planning',
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.',
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.)',
261
325
  '',
262
- '- [ ] Confirm app purpose, audience, and primary flows in `project/info.md`.',
263
- '- [ ] Confirm visual direction in `project/style.md` after using the style-guide page.',
326
+ '- [x] Confirm app purpose, audience, and primary flows in `project/info.md`.',
327
+ '- [ ] Confirm visual direction in `project/style.md` after using the Stylist page.',
264
328
  '- [ ] Keep or prune included package examples after reviewing `/exposition`.',
265
329
  '- [ ] Remove exposition pages before production once their lessons are absorbed.',
266
330
  ...(needsReview
267
- ? ['- [ ] Replace generic onboarding placeholders with real app decisions before full implementation.']
331
+ ? [
332
+ '- [ ] Replace generic onboarding placeholders with real app decisions before full implementation.',
333
+ ]
268
334
  : []),
269
335
  '',
270
336
  '## Phase 1: App Shell And First Flow',
@@ -274,6 +340,7 @@ export function renderTodo(answers) {
274
340
  `- [ ] Use ${formatPlatformLayoutMode(answers.platformLayoutMode)} unless project memory is updated.`,
275
341
  `- [ ] Implement the first core flow from project info: ${answers.coreFlows}.`,
276
342
  '- [ ] Keep route files thin and move real UI into feature screens.',
343
+ '- [ ] Apply Stylist synced theme tokens to production UI components and screens.',
277
344
  '',
278
345
  '## Phase 2: Data Layer',
279
346
  '',
@@ -296,8 +363,15 @@ export function renderTodo(answers) {
296
363
  '- [ ] Verify each selected platform after the MVP flow works.',
297
364
  ...answers.targetPlatforms.map((platform) => `- [ ] Verify ${platform} behavior.`),
298
365
  ...(answers.usesExpoUi ? ['- [ ] Add Expo UI examples where they improve native feel.'] : []),
299
- ...(answers.usesExpoNativeTabs ? ['- [ ] Prototype Expo Native Tabs for mobile navigation.'] : []),
300
- ...(answers.easUses.length > 0 ? answers.easUses.map((item) => `- [ ] Configure EAS for ${item}.`) : []),
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
+ : []),
301
375
  '',
302
376
  '## Phase 4: Polish, Safeguards, And Release',
303
377
  '',
@@ -309,7 +383,9 @@ export function renderTodo(answers) {
309
383
  '- [ ] Add GitHub branch protection so PR checks pass before merging into `test` or `main`.',
310
384
  ]
311
385
  : ['- [ ] Decide on release safeguards before production work begins.']),
312
- ...(answers.webOutput !== 'none' ? [`- [ ] Confirm Expo web output mode: ${answers.webOutput}.`] : []),
386
+ ...(answers.webOutput !== 'none'
387
+ ? [`- [ ] Confirm Expo web output mode: ${answers.webOutput}.`]
388
+ : []),
313
389
  ...(answers.deployedServer !== 'none'
314
390
  ? [`- [ ] Plan deployed server work: ${formatServerChoice(answers.deployedServer)}.`]
315
391
  : []),
@@ -333,6 +409,9 @@ export function renderStyle(answers, existingStyle) {
333
409
  '',
334
410
  '## Colors',
335
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
+ '',
336
415
  '# TodoForContext(optional): Add palette direction, semantic color meaning, and light/dark mode expectations.',
337
416
  '',
338
417
  '## Typography',
@@ -354,12 +433,81 @@ export function renderStyle(answers, existingStyle) {
354
433
  '',
355
434
  '## Style Questions To Revisit',
356
435
  '',
357
- '# TodoForContext(optional): Add unresolved visual decisions to revisit later in `/exposition/style-guide`; delete this marker if there are none.',
436
+ '# TodoForContext(optional): Add unresolved visual decisions to revisit later in `/exposition/stylist`; delete this marker if there are none.',
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 -->',
358
447
  '',
359
448
  ...importedNotes,
360
449
  '',
361
450
  ].join('\n');
362
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
+ }
363
511
  export function renderGuidelines(answers) {
364
512
  return [
365
513
  `# ${answers.appName} Guidelines`,
@@ -402,6 +550,14 @@ export function renderGuidelines(answers) {
402
550
  '- Never expose Supabase service-role or secret keys in client code.',
403
551
  ]
404
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
+ : []),
405
561
  '',
406
562
  '## Workflow',
407
563
  '',
@@ -409,9 +565,12 @@ export function renderGuidelines(answers) {
409
565
  '- Run `mds doctor --ci` before pushing.',
410
566
  '- Use `mds clear-expo-start` when Metro or server ports get wedged.',
411
567
  ...(answers.testToMainSafeguards
412
- ? ['- Develop through feature branches into `test`, then promote validated work from `test` to `main`.']
568
+ ? [
569
+ '- Develop through feature branches into `test`, then promote validated work from `test` to `main`.',
570
+ ]
413
571
  : []),
414
572
  `- Latest Expo SDK preference captured during onboarding: ${formatBoolean(answers.useLatestExpoSdk)}.`,
573
+ `- Expo UI Universal components preference captured during onboarding: ${formatBoolean(answers.usesExpoUiUniversalComponents)}.`,
415
574
  '- Treat monorepo scaffolding as future work until the single-app MVP is stable.',
416
575
  '',
417
576
  ].join('\n');
@@ -448,7 +607,7 @@ export function renderClaudeMd(answers) {
448
607
  `Run \`npm run clear-expo-start\` (or \`${MDS_NPX_COMMAND} clear-expo-start .\`) instead of bare \`expo start\` or \`npx expo start\`.`,
449
608
  'Kills port 8081, clears all Metro and Expo caches (including the Windows system cache), and starts `expo start --clear`.',
450
609
  'Expo Router API routes work automatically in this mode.',
451
- 'Never fall back to a non-default port always free the default port first.',
610
+ 'Never fall back to a non-default port — always free the default port first.',
452
611
  '',
453
612
  ];
454
613
  const backendAlongside = answers.customBackend
@@ -456,13 +615,13 @@ export function renderClaudeMd(answers) {
456
615
  '## Also start the backend API server',
457
616
  '',
458
617
  `Run \`node ${answers.customBackendEntry}\` from the project root in a background process alongside Expo.`,
459
- 'Both must be running for full local functionality Expo on port 8081, backend on its own port.',
618
+ 'Both must be running for full local functionality — Expo on port 8081, backend on its own port.',
460
619
  '',
461
620
  ]
462
621
  : [];
463
622
  const spinUpProd = buildSpinUpProdSection(answers);
464
623
  return [
465
- `# ${answers.appName} Agent Guidelines`,
624
+ `# ${answers.appName} — Agent Guidelines`,
466
625
  '',
467
626
  '## Before every git commit',
468
627
  '',
@@ -495,7 +654,7 @@ function buildSpinUpProdSection(answers) {
495
654
  return [
496
655
  '## Spin up prod',
497
656
  '',
498
- 'Run `npm run serve:prod:fresh` kills port 3000, builds web dist, starts the Node server.',
657
+ 'Run `npm run serve:prod:fresh` — kills port 3000, builds web dist, starts the Node server.',
499
658
  'Run `npm run serve:prod` to restart without rebuilding.',
500
659
  'Server runs on http://localhost:3000. Mirrors your self-hosted (Plesk/VPS) environment.',
501
660
  '',
@@ -505,7 +664,7 @@ function buildSpinUpProdSection(answers) {
505
664
  return [
506
665
  '## Spin up prod',
507
666
  '',
508
- 'Run `npm run serve:prod:fresh` builds web dist and starts `npx expo serve`.',
667
+ 'Run `npm run serve:prod:fresh` — builds web dist and starts `npx expo serve`.',
509
668
  'The terminal will show the local URL when ready. Mirrors EAS hosting.',
510
669
  '',
511
670
  ];
@@ -562,6 +721,11 @@ async function ensurePackageJson(projectPath, answers, manageUniwind) {
562
721
  'mds:continue': packageJson.scripts?.['mds:continue'] ?? `${MDS_NPX_COMMAND} continue`,
563
722
  'mds:doctor': packageJson.scripts?.['mds:doctor'] ?? `${MDS_NPX_COMMAND} doctor`,
564
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 .`,
565
729
  'free-port': packageJson.scripts?.['free-port'] ?? `${MDS_NPX_COMMAND} free-port`,
566
730
  'clear-expo-start': packageJson.scripts?.['clear-expo-start'] ?? `${MDS_NPX_COMMAND} clear-expo-start`,
567
731
  'expo-install-fix': packageJson.scripts?.['expo-install-fix'] ?? 'npx expo install --fix',
@@ -569,6 +733,17 @@ async function ensurePackageJson(projectPath, answers, manageUniwind) {
569
733
  'post-create-check': packageJson.scripts?.['post-create-check'] ?? 'npx expo install --fix && npx expo-doctor',
570
734
  'ci:verify': packageJson.scripts?.['ci:verify'] ?? `${MDS_NPX_COMMAND} doctor --ci`,
571
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
+ }
572
747
  if (answers.webOutput !== 'none') {
573
748
  const serveProd = deriveServeProdScript(answers);
574
749
  const serveProdFresh = deriveServeProdFreshScript(answers);
@@ -589,6 +764,7 @@ async function ensurePackageJson(projectPath, answers, manageUniwind) {
589
764
  }
590
765
  packageJson.dependencies = {
591
766
  ...SOFTWARE_MANSION_CORE_DEPENDENCIES,
767
+ ...STYLIST_DEPENDENCIES,
592
768
  ...packageJson.dependencies,
593
769
  };
594
770
  if (answers.dataStart === 'local') {
@@ -603,6 +779,18 @@ async function ensurePackageJson(projectPath, answers, manageUniwind) {
603
779
  ...packageJson.dependencies,
604
780
  };
605
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
+ }
606
794
  if (manageUniwind) {
607
795
  packageJson.dependencies = {
608
796
  ...UNIWIND_DEPENDENCIES,
@@ -630,6 +818,7 @@ function applyGuidelinesTemplate(template, answers) {
630
818
  appName: answers.appName,
631
819
  audience: answers.audience,
632
820
  coreFlows: answers.coreFlows,
821
+ screens: answers.screens ?? '',
633
822
  dataNeeds: answers.dataNeeds,
634
823
  deploymentTarget: answers.deploymentTarget,
635
824
  advancedPackageSetup: formatBoolean(answers.advancedPackageSetup),
@@ -643,6 +832,7 @@ function applyGuidelinesTemplate(template, answers) {
643
832
  webOutput: answers.webOutput,
644
833
  deployedServer: formatServerChoice(answers.deployedServer),
645
834
  usesExpoUi: formatBoolean(answers.usesExpoUi),
835
+ usesExpoUiUniversalComponents: formatBoolean(answers.usesExpoUiUniversalComponents),
646
836
  usesExpoNativeTabs: formatBoolean(answers.usesExpoNativeTabs),
647
837
  easUses: answers.easUses.map((item) => `- ${item}`).join('\n') || '- not planned yet',
648
838
  dataStart: formatDataStart(answers.dataStart),
@@ -684,11 +874,16 @@ function formatServerAdapterSummary(answers) {
684
874
  if (answers.webOutput === 'none')
685
875
  return 'none (native-only)';
686
876
  switch (answers.expoServerAdapter) {
687
- case 'eas': return 'EAS hosting';
688
- case 'express': return 'Express adapter (node server.js, port 3000)';
689
- case 'bun': return 'Bun adapter (node server.js)';
690
- case 'other': return 'custom (not yet specified)';
691
- default: return formatServerChoice(answers.deployedServer);
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);
692
887
  }
693
888
  }
694
889
  function deriveServeProdScript(answers) {
@@ -707,6 +902,21 @@ function formatStyleStack(answers) {
707
902
  if (answers.defaults.includes('uniwind')) {
708
903
  return 'Uniwind / Tailwind CSS v4';
709
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
+ }
710
920
  return 'standard React Native StyleSheet';
711
921
  }
712
922
  function formatAuthSummary(answers) {
@@ -737,6 +947,9 @@ function hasThinOnboardingAnswers(answers) {
737
947
  'Local state first; add backend only when needed',
738
948
  'Expo web/native deployment',
739
949
  ]);
950
+ if (!answers.screens?.trim()) {
951
+ return true;
952
+ }
740
953
  return [answers.audience, answers.coreFlows, answers.dataNeeds, answers.deploymentTarget].some((value) => genericValues.has(value.trim()));
741
954
  }
742
955
  async function ensureUniwindMetroConfig(projectPath) {
@@ -770,7 +983,9 @@ async function ensureUniwindMetroConfig(projectPath) {
770
983
  async function ensureUniwindGlobalCss(projectPath) {
771
984
  const globalCssPath = path.join(projectPath, 'global.css');
772
985
  const existing = await readOptionalText(globalCssPath);
773
- if (!existing || existing.includes("@import 'uniwind'") || existing.includes('@import "uniwind"')) {
986
+ if (!existing ||
987
+ existing.includes("@import 'uniwind'") ||
988
+ existing.includes('@import "uniwind"')) {
774
989
  return;
775
990
  }
776
991
  await writeFile(globalCssPath, renderGlobalCss(), 'utf8');
@@ -830,11 +1045,494 @@ async function ensureGlobalCssImport(projectPath, appDirectory) {
830
1045
  }
831
1046
  }
832
1047
  function getExpoRouterAppDir(projectPath, appDirectory) {
833
- return appDirectory === 'src' ? path.join(projectPath, 'src', 'app') : path.join(projectPath, 'app');
1048
+ return appDirectory === 'src'
1049
+ ? path.join(projectPath, 'src', 'app')
1050
+ : path.join(projectPath, 'app');
834
1051
  }
835
1052
  function renderRouteExport(routeDir, targetModulePath) {
836
1053
  return `export { default } from '${toRelativeImportPath(routeDir, targetModulePath)}';\n`;
837
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
+ }
838
1536
  function renderGlobalCssImport(layoutPath, projectPath) {
839
1537
  return `import '${toRelativeImportPath(path.dirname(layoutPath), path.join(projectPath, 'global.css'))}';`;
840
1538
  }
@@ -943,7 +1641,13 @@ async function readOptionalText(filePath) {
943
1641
  }
944
1642
  }
945
1643
  function renderGlobalCss() {
946
- return ["@import 'tailwindcss';", "@import 'uniwind';", ''].join('\n');
1644
+ return [
1645
+ "@import 'tailwindcss';",
1646
+ "@import 'uniwind';",
1647
+ '',
1648
+ renderGlobalCssThemeBlock(DEFAULT_STYLIST_THEME),
1649
+ '',
1650
+ ].join('\n');
947
1651
  }
948
1652
  function renderUniwindMetroConfig() {
949
1653
  return [
@@ -1051,73 +1755,223 @@ function renderNativeLocalDataService() {
1051
1755
  "import type { AppTask } from '../data/mock-app';",
1052
1756
  '',
1053
1757
  "const dbPromise = SQLite.openDatabaseAsync('exposition.db');",
1758
+ 'let sqliteUnavailable = false;',
1759
+ 'let memoryTasks: AppTask[] = [...appSnapshot.tasks];',
1054
1760
  '',
1055
1761
  'async function getDb() {',
1056
- ' return dbPromise;',
1762
+ ' if (sqliteUnavailable) {',
1763
+ ' return null;',
1764
+ ' }',
1765
+ '',
1766
+ ' try {',
1767
+ ' return await dbPromise;',
1768
+ ' } catch {',
1769
+ ' sqliteUnavailable = true;',
1770
+ ' return null;',
1771
+ ' }',
1057
1772
  '}',
1058
1773
  '',
1059
1774
  'export async function ensureLocalDataReady(): Promise<void> {',
1060
1775
  ' const db = await getDb();',
1061
- ' await db.execAsync(`',
1062
- ' CREATE TABLE IF NOT EXISTS exposition_tasks (',
1063
- ' id TEXT PRIMARY KEY NOT NULL,',
1064
- ' title TEXT NOT NULL,',
1065
- ' status TEXT NOT NULL',
1066
- ' );',
1067
- ' `);',
1068
- " const row = await db.getFirstAsync<{ count: number }>('SELECT COUNT(*) as count FROM exposition_tasks');",
1069
- ' if ((row?.count ?? 0) > 0) {',
1776
+ ' if (!db) {',
1070
1777
  ' return;',
1071
1778
  ' }',
1072
1779
  '',
1073
- ' for (const task of appSnapshot.tasks) {',
1074
- ' await db.runAsync(',
1075
- " 'INSERT INTO exposition_tasks (id, title, status) VALUES (?, ?, ?)',",
1076
- ' task.id,',
1077
- ' task.title,',
1078
- ' task.status',
1079
- ' );',
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;',
1080
1803
  ' }',
1081
1804
  '}',
1082
1805
  '',
1083
1806
  'export async function getLocalAppSnapshot(): Promise<typeof appSnapshot> {',
1084
1807
  ' await ensureLocalDataReady();',
1085
1808
  ' const db = await getDb();',
1086
- " const tasks = await db.getAllAsync<AppTask>('SELECT id, title, status FROM exposition_tasks ORDER BY id');",
1087
- ' return {',
1088
- ' ...appSnapshot,',
1089
- ' tasks,',
1090
- ' };',
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
+ ' }',
1091
1823
  '}',
1092
1824
  '',
1093
1825
  "export async function addLocalTask(title = 'Try the local DB adapter'): Promise<typeof appSnapshot> {",
1094
1826
  ' await ensureLocalDataReady();',
1095
1827
  ' const db = await getDb();',
1096
1828
  ' const id = `task-${Date.now()}`;',
1097
- " await db.runAsync('INSERT INTO exposition_tasks (id, title, status) VALUES (?, ?, ?)', id, title, 'todo');",
1098
- ' return getLocalAppSnapshot();',
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
+ ' }',
1099
1842
  '}',
1100
1843
  '',
1101
1844
  ].join('\n');
1102
1845
  }
1103
- 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\' }} />';
1104
1870
  return [
1105
1871
  renderGlobalCssImport(path.join(appDir, '_layout.tsx'), projectPath),
1106
- "import { Stack } from 'expo-router';",
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';",
1107
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
+ ' );',
1108
1909
  '',
1109
- 'export default function Layout() {',
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;',
1110
1920
  ' return (',
1111
- ' <SafeAreaProvider>',
1112
- ' <Stack>',
1113
- " <Stack.Screen name=\"index\" options={{ title: 'Home' }} />",
1114
- " <Stack.Screen name=\"onboarding\" options={{ title: 'Onboarding' }} />",
1115
- " <Stack.Screen name=\"exposition/index\" options={{ title: 'Exposition' }} />",
1116
- " <Stack.Screen name=\"exposition/style-guide\" options={{ title: 'Style Guide' }} />",
1117
- " <Stack.Screen name=\"exposition/data\" options={{ title: 'Data' }} />",
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,
1118
1954
  " <Stack.Screen name=\"settings\" options={{ presentation: 'modal', title: 'Settings' }} />",
1119
- ' </Stack>',
1120
- ' </SafeAreaProvider>',
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>',
1121
1975
  ' );',
1122
1976
  '}',
1123
1977
  '',
@@ -1219,12 +2073,20 @@ function renderAnimatedPressable() {
1219
2073
  'const AnimatedPressableBase = Animated.createAnimatedComponent(Pressable);',
1220
2074
  '',
1221
2075
  'interface AnimatedPressableProps {',
2076
+ ' backgroundColor?: string;',
1222
2077
  ' children?: ReactNode;',
1223
2078
  ' label?: string;',
1224
2079
  ' onPress?: () => void;',
2080
+ ' textColor?: string;',
1225
2081
  '}',
1226
2082
  '',
1227
- "export function AnimatedPressable({ children, label = 'Reanimated press demo', onPress }: AnimatedPressableProps) {",
2083
+ 'export function AnimatedPressable({',
2084
+ " backgroundColor = '#111827',",
2085
+ ' children,',
2086
+ " label = 'Reanimated press demo',",
2087
+ ' onPress,',
2088
+ " textColor = '#ffffff',",
2089
+ '}: AnimatedPressableProps) {',
1228
2090
  ' const pressed = useSharedValue(0);',
1229
2091
  ' const animatedStyle = useAnimatedStyle(() => ({',
1230
2092
  ' transform: [{ scale: withTiming(pressed.value ? 0.97 : 1, { duration: 120 }) }],',
@@ -1239,9 +2101,9 @@ function renderAnimatedPressable() {
1239
2101
  ' onPressOut={() => {',
1240
2102
  ' pressed.value = 0;',
1241
2103
  ' }}',
1242
- ' style={[styles.button, animatedStyle]}',
2104
+ ' style={[styles.button, { backgroundColor }, animatedStyle]}',
1243
2105
  ' >',
1244
- ' {children ?? <Text style={styles.label}>{label}</Text>}',
2106
+ ' {children ?? <Text style={[styles.label, { color: textColor }]}>{label}</Text>}',
1245
2107
  ' </AnimatedPressableBase>',
1246
2108
  ' );',
1247
2109
  '}',
@@ -1254,7 +2116,6 @@ function renderAnimatedPressable() {
1254
2116
  ' paddingVertical: 12,',
1255
2117
  ' },',
1256
2118
  ' label: {',
1257
- " color: '#ffffff',",
1258
2119
  ' fontSize: 15,',
1259
2120
  ' fontWeight: "700",',
1260
2121
  ' textAlign: "center",',
@@ -1305,9 +2166,7 @@ function renderGestureCard() {
1305
2166
  ' borderRadius: 12,',
1306
2167
  ' borderWidth: 1,',
1307
2168
  ' padding: 16,',
1308
- ' shadowColor: "#000000",',
1309
- ' shadowOpacity: 0.08,',
1310
- ' shadowRadius: 10,',
2169
+ " boxShadow: '0 6px 10px rgba(0, 0, 0, 0.08)',",
1311
2170
  ' },',
1312
2171
  ' title: {',
1313
2172
  " color: '#111827',",
@@ -1326,17 +2185,32 @@ function renderGestureCard() {
1326
2185
  }
1327
2186
  function renderKeyboardForm() {
1328
2187
  return [
1329
- "import { StyleSheet, TextInput } from 'react-native';",
1330
- "import { KeyboardAwareScrollView, KeyboardToolbar } from 'react-native-keyboard-controller';",
2188
+ "import { Keyboard, Platform, ScrollView, StyleSheet, TextInput } from 'react-native';",
1331
2189
  '',
1332
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
+ '',
1333
2207
  ' return (',
1334
2208
  ' <>',
1335
2209
  ' <KeyboardAwareScrollView bottomOffset={72} contentContainerStyle={styles.form} style={styles.scroller}>',
1336
- ' <TextInput placeholder="Project note" style={styles.input} />',
1337
- ' <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]} />',
1338
2212
  ' </KeyboardAwareScrollView>',
1339
- ' <KeyboardToolbar />',
2213
+ ' <KeyboardToolbar onDoneCallback={Keyboard.dismiss} />',
1340
2214
  ' </>',
1341
2215
  ' );',
1342
2216
  '}',
@@ -1367,13 +2241,13 @@ function renderKeyboardForm() {
1367
2241
  }
1368
2242
  function renderSvgMark() {
1369
2243
  return [
1370
- "import Svg, { Circle, Path } from 'react-native-svg';",
2244
+ "import Svg, { Path } from 'react-native-svg';",
1371
2245
  '',
1372
- 'export function SvgMark() {',
2246
+ 'export function SvgMark({ size = 44 }: { size?: number }) {',
1373
2247
  ' return (',
1374
- ' <Svg width={44} height={44} viewBox="0 0 44 44" accessibilityRole="image">',
1375
- ' <Circle cx={22} cy={22} r={20} fill="#111827" />',
1376
- ' <Path d="M14 23.5 19.5 29 31 15" stroke="#ffffff" strokeWidth={3} strokeLinecap="round" strokeLinejoin="round" />',
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" />',
1377
2251
  ' </Svg>',
1378
2252
  ' );',
1379
2253
  '}',
@@ -1423,40 +2297,17 @@ function renderScreensCard() {
1423
2297
  ].join('\n');
1424
2298
  }
1425
2299
  function renderExpositionNotice() {
2300
+ return ['export function ExpositionNotice() {', ' return null;', '}', ''].join('\n');
2301
+ }
2302
+ function renderSoftwareMansionLogo() {
1426
2303
  return [
1427
- "import { StyleSheet, Text, View } from 'react-native';",
2304
+ "import { SvgXml } from 'react-native-svg';",
1428
2305
  '',
1429
- 'export function ExpositionNotice() {',
1430
- ' return (',
1431
- ' <View style={styles.notice}>',
1432
- ' <Text style={styles.eyebrow}>Temporary exposition scaffold</Text>',
1433
- ` <Text style={styles.body}>${EXPOSITION_NOTICE}</Text>`,
1434
- ' </View>',
1435
- ' );',
1436
- '}',
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>`;',
1437
2307
  '',
1438
- 'const styles = StyleSheet.create({',
1439
- ' notice: {',
1440
- " backgroundColor: '#fff7ed',",
1441
- " borderColor: '#fed7aa',",
1442
- ' borderRadius: 12,',
1443
- ' borderWidth: 1,',
1444
- ' gap: 6,',
1445
- ' padding: 14,',
1446
- ' },',
1447
- ' eyebrow: {',
1448
- " color: '#9a3412',",
1449
- ' fontSize: 12,',
1450
- ' fontWeight: "800",',
1451
- ' letterSpacing: 0.4,',
1452
- ' textTransform: "uppercase",',
1453
- ' },',
1454
- ' body: {',
1455
- " color: '#7c2d12',",
1456
- ' fontSize: 14,',
1457
- ' lineHeight: 20,',
1458
- ' },',
1459
- '});',
2308
+ 'export function SoftwareMansionLogo({ width = 150, height = 80 }: { width?: number; height?: number }) {',
2309
+ ' return <SvgXml xml={softwareMansionLogoXml} width={width} height={height} accessibilityRole="image" />;',
2310
+ '}',
1460
2311
  '',
1461
2312
  ].join('\n');
1462
2313
  }
@@ -1465,6 +2316,8 @@ function renderPackageCard() {
1465
2316
  "import type { ReactNode } from 'react';",
1466
2317
  "import { StyleSheet, Text, View } from 'react-native';",
1467
2318
  '',
2319
+ "import { useAppTheme } from '../../theme/provider';",
2320
+ '',
1468
2321
  'interface PackageCardProps {',
1469
2322
  ' title: string;',
1470
2323
  ' packageName: string;',
@@ -1473,11 +2326,14 @@ function renderPackageCard() {
1473
2326
  '}',
1474
2327
  '',
1475
2328
  'export function PackageCard({ title, packageName, body, children }: PackageCardProps) {',
2329
+ ' const theme = useAppTheme();',
2330
+ ' const colors = theme.activeColors;',
2331
+ '',
1476
2332
  ' return (',
1477
- ' <View style={styles.card}>',
1478
- ' <Text style={styles.packageName}>{packageName}</Text>',
1479
- ' <Text style={styles.title}>{title}</Text>',
1480
- ' <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>',
1481
2337
  ' {children ? <View style={styles.demo}>{children}</View> : null}',
1482
2338
  ' </View>',
1483
2339
  ' );',
@@ -1522,68 +2378,678 @@ function renderExpositionComponentIndex() {
1522
2378
  "export { KeyboardForm } from './keyboard-form';",
1523
2379
  "export { PackageCard } from './package-card';",
1524
2380
  "export { ScreensCard } from './screens-card';",
2381
+ "export { SoftwareMansionLogo } from './software-mansion-logo';",
1525
2382
  "export { SvgMark } from './svg-mark';",
1526
2383
  '',
1527
2384
  ].join('\n');
1528
2385
  }
1529
- function renderHomeScreen(answers) {
2386
+ function renderNativeWindUiActivityIndicator() {
1530
2387
  return [
1531
- "import { Link } from 'expo-router';",
1532
- "import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';",
2388
+ "import type { ComponentProps } from 'react';",
2389
+ "import { ActivityIndicator as RNActivityIndicator } from 'react-native';",
1533
2390
  '',
1534
- "import { GestureCard, SvgMark } from '../../components/exposition';",
1535
- "import { appSnapshot } from '../../data/mock-app';",
2391
+ 'export function ActivityIndicator(props: ComponentProps<typeof RNActivityIndicator>) {',
2392
+ ' return <RNActivityIndicator color="#2563eb" {...props} />;',
2393
+ '}',
1536
2394
  '',
1537
- 'const expositionLinks = [',
1538
- " { href: '/exposition' as const, title: 'Package exposition', body: 'Review included base packages and decide what stays.' },",
1539
- " { href: '/exposition/style-guide' as const, title: 'Style guide', body: 'Test colors, type, motion, and component density.' },",
1540
- " { href: '/exposition/data' as const, title: 'Data adapter', body: 'Try the local data boundary before replacing it.' },",
1541
- '];',
2395
+ ].join('\n');
2396
+ }
2397
+ function renderNativeWindUiAvatar() {
2398
+ return [
2399
+ "import type { ReactNode } from 'react';",
2400
+ "import { StyleSheet, View, type ViewProps } from 'react-native';",
1542
2401
  '',
1543
- 'export default function HomeScreen() {',
1544
- ' return (',
1545
- ' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
2402
+ 'type AvatarProps = ViewProps & {',
2403
+ ' children?: ReactNode;',
2404
+ ' className?: string;',
2405
+ '};',
2406
+ '',
2407
+ 'export function Avatar({ children, className: _className, style, ...props }: AvatarProps) {',
2408
+ ' return (',
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 }]}>',
1546
3017
  ' <View style={styles.header}>',
1547
- ' <SvgMark />',
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>',
1548
3025
  ' <View style={styles.headerText}>',
1549
- ` <Text style={styles.title}>${answers.appName}</Text>`,
1550
- ' <Text style={styles.subtitle}>{appSnapshot.audience}</Text>',
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>',
1551
3028
  ' </View>',
1552
- ' <Link href="/settings" asChild>',
1553
- ' <Pressable accessibilityRole="button" style={styles.infoButton}>',
1554
- ' <Text style={styles.infoButtonText}>i</Text>',
1555
- ' </Pressable>',
1556
- ' </Link>',
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}',
1557
3036
  ' </View>',
1558
- ' <GestureCard',
1559
- ' title="Rich boilerplate is wired"',
1560
- ' body="Routes stay thin, feature screens hold UI, and the temporary exposition pages are reachable from this home screen."',
1561
- ' />',
1562
3037
  ' <View style={styles.grid}>',
1563
3038
  ' <Link href="/onboarding" asChild>',
1564
- ' <Pressable style={styles.primaryCard}>',
3039
+ ' <Pressable style={StyleSheet.flatten([styles.primaryCard, { backgroundColor: colors.primary, borderRadius: theme.layout.radius }])}>',
1565
3040
  ' <Text style={styles.primaryTitle}>Onboarding preview</Text>',
1566
3041
  ' <Text style={styles.primaryBody}>Open the generated onboarding screen before the main product flow replaces it.</Text>',
1567
3042
  ' </Pressable>',
1568
3043
  ' </Link>',
1569
3044
  ' {expositionLinks.map((item) => (',
1570
- ' <Link key={item.href} href={item.href} asChild>',
1571
- ' <Pressable style={styles.linkCard}>',
1572
- ' <Text style={styles.linkTitle}>{item.title}</Text>',
1573
- ' <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>',
1574
3049
  ' </Pressable>',
1575
3050
  ' </Link>',
1576
3051
  ' ))}',
1577
3052
  ' </View>',
1578
- ' <View style={styles.taskList}>',
1579
- ' <Text style={styles.sectionTitle}>Generated next steps</Text>',
1580
- ' {appSnapshot.tasks.map((task) => (',
1581
- ' <View key={task.id} style={styles.taskCard}>',
1582
- ' <Text style={styles.taskTitle}>{task.title}</Text>',
1583
- ' <Text style={styles.taskStatus}>{task.status}</Text>',
1584
- ' </View>',
1585
- ' ))}',
1586
- ' </View>',
1587
3053
  ' </ScrollView>',
1588
3054
  ' );',
1589
3055
  '}',
@@ -1594,16 +3060,35 @@ function renderHomeScreen(answers) {
1594
3060
  ' flex: 1,',
1595
3061
  ' },',
1596
3062
  ' content: {',
3063
+ ' flexGrow: 1,',
1597
3064
  ' gap: 16,',
3065
+ ' justifyContent: "center",',
1598
3066
  ' padding: 20,',
3067
+ ' paddingTop: Platform.OS === "web" ? 84 : 20,',
1599
3068
  ' },',
1600
3069
  ' header: {',
1601
3070
  ' alignItems: "center",',
3071
+ ' gap: 10,',
3072
+ ' position: "relative",',
3073
+ ' },',
3074
+ ' brandLockup: {',
3075
+ ' alignItems: "center",',
1602
3076
  ' flexDirection: "row",',
1603
- ' gap: 12,',
3077
+ ' gap: 14,',
3078
+ ' justifyContent: "center",',
1604
3079
  ' },',
1605
3080
  ' headerText: {',
1606
- ' flex: 1,',
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",',
1607
3092
  ' },',
1608
3093
  ' infoButton: {',
1609
3094
  ' alignItems: "center",',
@@ -1611,6 +3096,9 @@ function renderHomeScreen(answers) {
1611
3096
  ' borderRadius: 18,',
1612
3097
  ' height: 36,',
1613
3098
  ' justifyContent: "center",',
3099
+ ' position: "absolute",',
3100
+ ' right: 0,',
3101
+ ' top: 0,',
1614
3102
  ' width: 36,',
1615
3103
  ' },',
1616
3104
  ' infoButtonText: {',
@@ -1619,99 +3107,629 @@ function renderHomeScreen(answers) {
1619
3107
  ' fontWeight: "800",',
1620
3108
  ' },',
1621
3109
  ' title: {',
1622
- " color: '#111827',",
1623
- ' 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,',
1624
3484
  ' fontWeight: "800",',
1625
3485
  ' },',
1626
- ' subtitle: {',
1627
- " color: '#4b5563',",
1628
- ' fontSize: 14,',
1629
- ' marginTop: 3,',
3486
+ ' summary: {',
3487
+ " color: '#334155',",
3488
+ ' fontSize: 15,',
3489
+ ' lineHeight: 22,',
1630
3490
  ' },',
1631
- ' grid: {',
1632
- ' gap: 12,',
3491
+ ' metaRow: {',
3492
+ ' flexDirection: "row",',
3493
+ ' gap: 10,',
1633
3494
  ' },',
1634
- ' primaryCard: {',
1635
- " backgroundColor: '#111827',",
1636
- ' borderRadius: 12,',
1637
- ' gap: 8,',
1638
- ' padding: 16,',
3495
+ ' metaItem: {',
3496
+ " backgroundColor: '#e2e8f0',",
3497
+ ' borderRadius: 10,',
3498
+ ' gap: 2,',
3499
+ ' paddingHorizontal: 10,',
3500
+ ' paddingVertical: 8,',
1639
3501
  ' },',
1640
- ' primaryTitle: {',
1641
- " color: '#ffffff',",
1642
- ' fontSize: 18,',
1643
- ' fontWeight: "800",',
3502
+ ' metaLabel: {',
3503
+ " color: '#475569',",
3504
+ ' fontSize: 11,',
3505
+ ' fontWeight: "700",',
3506
+ ' textTransform: "uppercase",',
1644
3507
  ' },',
1645
- ' primaryBody: {',
1646
- " color: '#d1d5db',",
1647
- ' fontSize: 14,',
1648
- ' lineHeight: 20,',
3508
+ ' metaValue: {',
3509
+ " color: '#0f172a',",
3510
+ ' fontSize: 13,',
3511
+ ' fontWeight: "700",',
1649
3512
  ' },',
1650
- ' linkCard: {',
3513
+ ' section: {',
1651
3514
  " backgroundColor: '#ffffff',",
1652
- " borderColor: '#e5e7eb',",
3515
+ " borderColor: '#e2e8f0',",
1653
3516
  ' borderRadius: 12,',
1654
3517
  ' borderWidth: 1,',
1655
- ' gap: 6,',
1656
- ' padding: 16,',
1657
- ' },',
1658
- ' linkTitle: {',
1659
- " color: '#111827',",
1660
- ' fontSize: 16,',
1661
- ' fontWeight: "800",',
1662
- ' },',
1663
- ' linkBody: {',
1664
- " color: '#4b5563',",
1665
- ' fontSize: 14,',
1666
- ' lineHeight: 20,',
1667
- ' },',
1668
- ' taskList: {',
1669
- ' gap: 10,',
3518
+ ' gap: 7,',
3519
+ ' padding: 14,',
1670
3520
  ' },',
1671
3521
  ' sectionTitle: {',
1672
- " color: '#111827',",
1673
- ' fontSize: 18,',
3522
+ " color: '#0f172a',",
3523
+ ' fontSize: 17,',
1674
3524
  ' fontWeight: "800",',
1675
3525
  ' },',
1676
- ' taskCard: {',
1677
- " backgroundColor: '#ffffff',",
1678
- " borderColor: '#e5e7eb',",
1679
- ' borderRadius: 10,',
1680
- ' borderWidth: 1,',
1681
- ' padding: 12,',
1682
- ' },',
1683
- ' taskTitle: {',
1684
- " color: '#111827',",
1685
- ' fontWeight: "700",',
1686
- ' },',
1687
- ' taskStatus: {',
1688
- " color: '#6b7280',",
1689
- ' fontSize: 12,',
1690
- ' fontWeight: "800",',
1691
- ' marginTop: 4,',
1692
- ' textTransform: "uppercase",',
3526
+ ' sectionBody: {',
3527
+ " color: '#334155',",
3528
+ ' fontSize: 14,',
3529
+ ' lineHeight: 21,',
1693
3530
  ' },',
1694
3531
  '});',
1695
3532
  '',
1696
3533
  ].join('\n');
1697
3534
  }
1698
- function renderOnboardingScreen() {
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() {
1699
3716
  return [
1700
- "import { Link } from 'expo-router';",
1701
3717
  "import { StyleSheet, Text, View } from 'react-native';",
1702
3718
  '',
1703
- "import { AnimatedPressable } from '../../components/exposition';",
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;',
1704
3725
  '',
1705
- 'export default function OnboardingScreen() {',
1706
3726
  ' return (',
1707
- ' <View style={styles.screen}>',
1708
- ' <Text style={styles.title}>Start with intent</Text>',
1709
- ' <Text style={styles.body}>',
1710
- ' Replace this screen with the first real onboarding step once the product flow is settled.',
1711
- ' </Text>',
1712
- ' <Link href="/" asChild>',
1713
- ' <AnimatedPressable label="Continue to home" />',
1714
- ' </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 />',
1715
3733
  ' </View>',
1716
3734
  ' );',
1717
3735
  '}',
@@ -1720,10 +3738,11 @@ function renderOnboardingScreen() {
1720
3738
  ' screen: {',
1721
3739
  " backgroundColor: '#ffffff',",
1722
3740
  ' flex: 1,',
1723
- ' gap: 16,',
1724
- ' justifyContent: "center",',
1725
3741
  ' padding: 20,',
1726
3742
  ' },',
3743
+ ' header: {',
3744
+ ' marginBottom: 12,',
3745
+ ' },',
1727
3746
  ' title: {',
1728
3747
  " color: '#111827',",
1729
3748
  ' fontSize: 26,',
@@ -1731,102 +3750,334 @@ function renderOnboardingScreen() {
1731
3750
  ' },',
1732
3751
  ' body: {',
1733
3752
  " color: '#4b5563',",
1734
- ' fontSize: 16,',
1735
- ' lineHeight: 24,',
3753
+ ' fontSize: 14,',
3754
+ ' lineHeight: 20,',
3755
+ ' marginTop: 4,',
1736
3756
  ' },',
1737
3757
  '});',
1738
3758
  '',
1739
3759
  ].join('\n');
1740
3760
  }
1741
- function renderSettingsScreen() {
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';"] : [];
1742
3775
  return [
1743
- "import { StyleSheet, Text, View } from 'react-native';",
3776
+ ...linkImport,
3777
+ "import { Linking, Platform, ScrollView, StyleSheet, Text, View } from 'react-native';",
1744
3778
  '',
1745
- "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;',
1746
3785
  '',
1747
- 'export default function SettingsScreen() {',
1748
3786
  ' return (',
1749
- ' <View style={styles.screen}>',
1750
- ' <View style={styles.header}>',
1751
- ' <Text style={styles.title}>Settings</Text>',
1752
- ' <Text style={styles.body}>Keyboard Controller is ready for form-heavy screens.</Text>',
1753
- ' </View>',
1754
- ' <KeyboardForm />',
1755
- ' </View>',
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>',
1756
3888
  ' );',
1757
3889
  '}',
1758
3890
  '',
1759
3891
  'const styles = StyleSheet.create({',
1760
3892
  ' screen: {',
1761
- " backgroundColor: '#ffffff',",
3893
+ " backgroundColor: '#f9fafb',",
1762
3894
  ' flex: 1,',
1763
- ' padding: 20,',
1764
3895
  ' },',
1765
- ' header: {',
1766
- ' marginBottom: 12,',
3896
+ ' content: {',
3897
+ ' gap: 16,',
3898
+ ' padding: 20,',
3899
+ " paddingTop: Platform.OS === 'web' ? 92 : 20,",
1767
3900
  ' },',
1768
3901
  ' title: {',
1769
3902
  " color: '#111827',",
1770
- ' fontSize: 26,',
1771
- ' fontWeight: "800",',
3903
+ ' fontSize: 30,',
3904
+ ' fontWeight: "900",',
1772
3905
  ' },',
1773
- ' body: {',
3906
+ ' intro: {',
1774
3907
  " color: '#4b5563',",
3908
+ ' fontSize: 16,',
3909
+ ' lineHeight: 24,',
3910
+ ' },',
3911
+ ' link: {',
3912
+ " color: '#1d4ed8',",
1775
3913
  ' fontSize: 14,',
3914
+ " fontWeight: '800',",
1776
3915
  ' lineHeight: 20,',
1777
- ' marginTop: 4,',
3916
+ ' },',
3917
+ ' svgDemo: {',
3918
+ ' alignItems: "center",',
3919
+ ' paddingVertical: 8,',
1778
3920
  ' },',
1779
3921
  '});',
1780
3922
  '',
1781
3923
  ].join('\n');
1782
3924
  }
1783
- function renderExpositionScreen(answers) {
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
+ ];
1784
3963
  return [
1785
- "import { ScrollView, StyleSheet, Text, View } from 'react-native';",
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
+ '];',
1786
3985
  '',
1787
- "import { AnimatedPressable, ExpositionNotice, GestureCard, KeyboardForm, PackageCard, ScreensCard, SvgMark } from '../../components/exposition';",
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
+ '}',
1788
4053
  '',
1789
- 'export default function ExpositionScreen() {',
4054
+ 'export default function ExpoSdk56Screen() {',
1790
4055
  ' return (',
1791
4056
  ' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
1792
- ` <Text style={styles.title}>${answers.appName} Exposition</Text>`,
1793
- ' <Text style={styles.intro}>Browse the included base packages, then delete what the app does not need.</Text>',
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>',
1794
4059
  ' <ExpositionNotice />',
1795
- ' <PackageCard',
1796
- ' packageName="react-native-reanimated + react-native-worklets"',
1797
- ' title="Motion that feels native"',
1798
- ' body="Press the button to see the Reanimated timing demo. Worklets make this kind of UI-thread animation possible."',
1799
- ' >',
1800
- ' <AnimatedPressable label="Press and hold" />',
1801
- ' </PackageCard>',
1802
- ' <PackageCard',
1803
- ' packageName="react-native-gesture-handler"',
1804
- ' title="Gesture-first interactions"',
1805
- ' body="Drag the card below. If your product does not need touch-heavy interactions, this demo helps you decide what to remove."',
1806
- ' >',
1807
- ' <GestureCard title="Drag me" body="This card springs back when the gesture ends." />',
1808
- ' </PackageCard>',
1809
- ' <PackageCard',
1810
- ' packageName="react-native-screens"',
1811
- ' title="Native navigation primitives"',
1812
- ' body="Screens support the navigation layer with native lifecycle and memory behavior."',
1813
- ' >',
1814
- ' <ScreensCard />',
1815
- ' </PackageCard>',
1816
- ' <PackageCard',
1817
- ' packageName="react-native-svg"',
1818
- ' title="Portable vector UI"',
1819
- ' body="Use SVG for marks, badges, charts, and vector states that need to scale cleanly."',
1820
- ' >',
1821
- ' <View style={styles.svgDemo}><SvgMark /></View>',
1822
- ' </PackageCard>',
1823
- ' <PackageCard',
1824
- ' packageName="react-native-keyboard-controller"',
1825
- ' title="Keyboard-heavy screens"',
1826
- ' body="Use this when forms, chat, notes, or auth flows need better keyboard control than manual offsets."',
1827
- ' >',
1828
- ' <KeyboardForm />',
1829
- ' </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>',
1830
4081
  ' </ScrollView>',
1831
4082
  ' );',
1832
4083
  '}',
@@ -1839,83 +4090,419 @@ function renderExpositionScreen(answers) {
1839
4090
  ' content: {',
1840
4091
  ' gap: 16,',
1841
4092
  ' padding: 20,',
4093
+ " paddingTop: Platform.OS === 'web' ? 92 : 20,",
1842
4094
  ' },',
1843
4095
  ' title: {',
1844
4096
  " color: '#111827',",
1845
4097
  ' fontSize: 30,',
1846
4098
  ' fontWeight: "900",',
4099
+ ' textAlign: "center",',
1847
4100
  ' },',
1848
4101
  ' intro: {',
1849
4102
  " color: '#4b5563',",
1850
4103
  ' fontSize: 16,',
1851
4104
  ' lineHeight: 24,',
1852
4105
  ' },',
1853
- ' svgDemo: {',
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: {',
1854
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,',
1855
4266
  ' paddingVertical: 8,',
1856
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
+ ' },',
1857
4291
  '});',
1858
4292
  '',
1859
4293
  ].join('\n');
1860
4294
  }
1861
- function renderStyleGuideScreen(answers) {
4295
+ function renderStylistScreen(answers) {
1862
4296
  return [
1863
- "import { ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';",
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';",
1864
4300
  '',
1865
4301
  "import { AnimatedPressable, ExpositionNotice } from '../../components/exposition';",
1866
- '',
1867
- 'const colors = [',
1868
- " ['Ink', '#111827'],",
1869
- " ['Cloud', '#f9fafb'],",
1870
- " ['Accent', '#2563eb'],",
1871
- " ['Success', '#16a34a'],",
1872
- " ['Warning', '#f97316'],",
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',",
1873
4314
  '];',
1874
4315
  '',
1875
- 'export default function StyleGuideScreen() {',
4316
+ "const spacingKeys: Array<keyof StylistTheme['layout']['spacing']> = ['xs', 'sm', 'md', 'lg', 'xl'];",
4317
+ "const NATIVE_SAVE_COMMAND = 'npm run stylist:sync:android';",
4318
+ '',
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
+ '',
1876
4401
  ' return (',
1877
- ' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
1878
- ` <Text style={styles.title}>${answers.appName} Style Guide</Text>`,
1879
- ' <Text style={styles.intro}>Use this page to explore type, spacing, color, and component tone with the client before the production UI hardens.</Text>',
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>',
1880
4405
  ' <ExpositionNotice />',
1881
- ' <View style={styles.section}>',
1882
- ' <Text style={styles.sectionTitle}>Color Palette</Text>',
1883
- ' <View style={styles.swatchGrid}>',
1884
- ' {colors.map(([name, color]) => (',
1885
- ' <View key={name} style={styles.swatchItem}>',
1886
- ' <View style={[styles.swatch, { backgroundColor: color }]} />',
1887
- ' <Text style={styles.swatchLabel}>{name}</Text>',
1888
- ' <Text style={styles.swatchValue}>{color}</Text>',
1889
- ' </View>',
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>',
1890
4421
  ' ))}',
1891
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>',
1892
4438
  ' </View>',
1893
- ' <View style={styles.section}>',
1894
- ' <Text style={styles.sectionTitle}>Typography</Text>',
1895
- ' <Text style={styles.display}>Display headline</Text>',
1896
- ' <Text style={styles.heading}>Section heading</Text>',
1897
- ' <Text style={styles.body}>Readable body copy for product screens, onboarding, settings, and forms.</Text>',
1898
- ' <Text style={styles.caption}>Caption and metadata text</Text>',
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>',
1899
4464
  ' </View>',
1900
- ' <View style={styles.section}>',
1901
- ' <Text style={styles.sectionTitle}>Controls</Text>',
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>',
1902
4471
  ' <AnimatedPressable label="Primary action" />',
1903
- ' <TextInput placeholder="Input state" style={styles.input} />',
1904
4472
  ' </View>',
1905
- ' <View style={styles.section}>',
1906
- ' <Text style={styles.sectionTitle}>Card Language</Text>',
1907
- ' <View style={styles.card}>',
1908
- ' <Text style={styles.heading}>Decision card</Text>',
1909
- ' <Text style={styles.body}>Use cards like this to compare concepts during research, then promote only the useful patterns into production components.</Text>',
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}',
1910
4484
  ' </View>',
1911
- ' </View>',
4485
+ ' ) : null}',
1912
4486
  ' </ScrollView>',
1913
4487
  ' );',
1914
4488
  '}',
1915
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
+ '',
1916
4504
  'const styles = StyleSheet.create({',
1917
4505
  ' screen: {',
1918
- " backgroundColor: '#ffffff',",
1919
4506
  ' flex: 1,',
1920
4507
  ' },',
1921
4508
  ' content: {',
@@ -1923,87 +4510,137 @@ function renderStyleGuideScreen(answers) {
1923
4510
  ' padding: 20,',
1924
4511
  ' },',
1925
4512
  ' title: {',
1926
- " color: '#111827',",
1927
4513
  ' fontSize: 30,',
1928
4514
  ' fontWeight: "900",',
1929
4515
  ' },',
1930
4516
  ' intro: {',
1931
- " color: '#4b5563',",
1932
- ' fontSize: 16,',
1933
- ' lineHeight: 24,',
4517
+ ' fontSize: 15,',
4518
+ ' lineHeight: 22,',
1934
4519
  ' },',
1935
4520
  ' section: {',
1936
- " backgroundColor: '#f9fafb',",
1937
- ' borderRadius: 12,',
1938
4521
  ' gap: 12,',
1939
4522
  ' padding: 16,',
1940
4523
  ' },',
1941
4524
  ' sectionTitle: {',
1942
- " color: '#111827',",
1943
4525
  ' fontSize: 18,',
1944
4526
  ' fontWeight: "800",',
1945
4527
  ' },',
1946
- ' swatchGrid: {',
4528
+ ' colorRow: {',
1947
4529
  ' flexDirection: "row",',
1948
4530
  ' flexWrap: "wrap",',
1949
- ' gap: 12,',
1950
- ' },',
1951
- ' swatchItem: {',
1952
- ' minWidth: 92,',
1953
- ' },',
1954
- ' swatch: {',
1955
- ' borderRadius: 10,',
1956
- ' height: 44,',
4531
+ ' gap: 8,',
1957
4532
  ' },',
1958
- ' swatchLabel: {',
1959
- " color: '#111827',",
1960
- ' fontWeight: "700",',
1961
- ' marginTop: 6,',
4533
+ ' colorChip: {',
4534
+ ' borderRadius: 999,',
4535
+ ' borderWidth: 2,',
4536
+ ' minWidth: 94,',
4537
+ ' paddingHorizontal: 10,',
4538
+ ' paddingVertical: 8,',
1962
4539
  ' },',
1963
- ' swatchValue: {',
1964
- " color: '#6b7280',",
4540
+ ' colorChipLabel: {',
4541
+ ' color: "#ffffff",',
1965
4542
  ' fontSize: 12,',
4543
+ ' fontWeight: "700",',
4544
+ ' textTransform: "capitalize",',
1966
4545
  ' },',
1967
- ' display: {',
1968
- " color: '#111827',",
1969
- ' fontSize: 32,',
1970
- ' fontWeight: "900",',
4546
+ ' picker: {',
4547
+ ' gap: 12,',
4548
+ ' width: "100%",',
1971
4549
  ' },',
1972
- ' heading: {',
1973
- " color: '#111827',",
1974
- ' fontSize: 20,',
1975
- ' fontWeight: "800",',
4550
+ ' grid: {',
4551
+ ' flexDirection: "row",',
4552
+ ' flexWrap: "wrap",',
4553
+ ' gap: 10,',
1976
4554
  ' },',
1977
- ' body: {',
1978
- " color: '#4b5563',",
1979
- ' fontSize: 15,',
1980
- ' lineHeight: 22,',
4555
+ ' field: {',
4556
+ ' flexBasis: "48%",',
4557
+ ' flexGrow: 1,',
4558
+ ' gap: 6,',
1981
4559
  ' },',
1982
- ' caption: {',
1983
- " color: '#6b7280',",
4560
+ ' fieldLabel: {',
4561
+ ' color: "#374151",',
1984
4562
  ' fontSize: 12,',
1985
4563
  ' fontWeight: "700",',
1986
- ' textTransform: "uppercase",',
1987
4564
  ' },',
1988
4565
  ' input: {',
1989
- " borderColor: '#d1d5db',",
4566
+ ' backgroundColor: "#ffffff",',
4567
+ ' borderColor: "#d1d5db",',
1990
4568
  ' borderRadius: 10,',
1991
4569
  ' borderWidth: 1,',
1992
- ' minHeight: 44,',
4570
+ ' minHeight: 42,',
1993
4571
  ' paddingHorizontal: 12,',
1994
4572
  ' },',
1995
- ' card: {',
1996
- " backgroundColor: '#ffffff',",
1997
- " borderColor: '#e5e7eb',",
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",',
1998
4591
  ' borderRadius: 12,',
1999
4592
  ' borderWidth: 1,',
2000
4593
  ' gap: 8,',
2001
- ' padding: 16,',
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,',
2002
4618
  ' },',
2003
4619
  '});',
2004
4620
  '',
2005
4621
  ].join('\n');
2006
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
+ }
2007
4644
  function renderDataScreen(answers) {
2008
4645
  if (answers.dataStart === 'supabase') {
2009
4646
  return renderSupabaseDataScreen(answers);
@@ -2014,12 +4651,15 @@ function renderDataScreen(answers) {
2014
4651
  '',
2015
4652
  "import { ExpositionNotice } from '../../components/exposition';",
2016
4653
  "import { addLocalTask, getLocalAppSnapshot } from '../../services/local-data';",
4654
+ "import { useAppTheme } from '../../theme/provider';",
2017
4655
  '',
2018
4656
  "import type { appSnapshot } from '../../data/mock-app';",
2019
4657
  '',
2020
4658
  'type Snapshot = typeof appSnapshot;',
2021
4659
  '',
2022
4660
  'export default function DataScreen() {',
4661
+ ' const theme = useAppTheme();',
4662
+ ' const colors = theme.activeColors;',
2023
4663
  ' const [snapshot, setSnapshot] = useState<Snapshot | null>(null);',
2024
4664
  '',
2025
4665
  ' useEffect(() => {',
@@ -2031,17 +4671,17 @@ function renderDataScreen(answers) {
2031
4671
  ' }',
2032
4672
  '',
2033
4673
  ' return (',
2034
- ' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
2035
- ' <Text style={styles.title}>Data Exposition</Text>',
2036
- ' <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>',
2037
4677
  ' <ExpositionNotice />',
2038
- ' <Pressable onPress={addTask} style={styles.button}>',
4678
+ ' <Pressable onPress={addTask} style={[styles.button, { backgroundColor: colors.primary, borderRadius: theme.layout.radius }]}>',
2039
4679
  ' <Text style={styles.buttonText}>Insert a local task</Text>',
2040
4680
  ' </Pressable>',
2041
4681
  ' {snapshot?.tasks.map((task) => (',
2042
- ' <View key={task.id} style={styles.taskCard}>',
2043
- ' <Text style={styles.taskTitle}>{task.title}</Text>',
2044
- ' <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>',
2045
4685
  ' </View>',
2046
4686
  ' ))}',
2047
4687
  ' <View style={styles.guidance}>',
@@ -2060,12 +4700,16 @@ function renderSupabaseDataScreen(answers) {
2060
4700
  "import { ScrollView, StyleSheet, Text, View } from 'react-native';",
2061
4701
  '',
2062
4702
  "import { ExpositionNotice } from '../../components/exposition';",
4703
+ "import { useAppTheme } from '../../theme/provider';",
2063
4704
  '',
2064
4705
  'export default function DataScreen() {',
4706
+ ' const theme = useAppTheme();',
4707
+ ' const colors = theme.activeColors;',
4708
+ '',
2065
4709
  ' return (',
2066
- ' <ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={styles.content} style={styles.screen}>',
2067
- ' <Text style={styles.title}>Data Exposition</Text>',
2068
- ` <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>`,
2069
4713
  ' <ExpositionNotice />',
2070
4714
  ' <View style={styles.guidance}>',
2071
4715
  ' <Text style={styles.sectionTitle}>Two Supabase projects</Text>',