@ramonclaudio/create-vexpo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +183 -0
  3. package/dist/templates/default/.eas/workflows/asc-events.yml +84 -0
  4. package/dist/templates/default/.eas/workflows/deploy-production.yml +129 -0
  5. package/dist/templates/default/.eas/workflows/development-builds.yml +19 -0
  6. package/dist/templates/default/.eas/workflows/e2e-tests.yml +42 -0
  7. package/dist/templates/default/.eas/workflows/pr-preview.yml +98 -0
  8. package/dist/templates/default/.eas/workflows/release.yml +44 -0
  9. package/dist/templates/default/.eas/workflows/rollback.yml +86 -0
  10. package/dist/templates/default/.eas/workflows/rollout.yml +84 -0
  11. package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +42 -0
  12. package/dist/templates/default/.eas/workflows/testflight.yml +57 -0
  13. package/dist/templates/default/.github/workflows/check.yml +28 -0
  14. package/dist/templates/default/.maestro/launch.yaml +18 -0
  15. package/dist/templates/default/AGENTS.md +79 -0
  16. package/dist/templates/default/DESIGN.md +331 -0
  17. package/dist/templates/default/LICENSE +21 -0
  18. package/dist/templates/default/README.md +153 -0
  19. package/dist/templates/default/SETUP.md +618 -0
  20. package/dist/templates/default/__tests__/convex/constants.test.ts +49 -0
  21. package/dist/templates/default/__tests__/convex/validators.test.ts +23 -0
  22. package/dist/templates/default/__tests__/convex/webhook.test.ts +343 -0
  23. package/dist/templates/default/__tests__/lib/deep-link.test.ts +67 -0
  24. package/dist/templates/default/_easignore +22 -0
  25. package/dist/templates/default/_editorconfig +9 -0
  26. package/dist/templates/default/_env.example +34 -0
  27. package/dist/templates/default/_fingerprintignore +24 -0
  28. package/dist/templates/default/_gitattributes +7 -0
  29. package/dist/templates/default/_gitignore +69 -0
  30. package/dist/templates/default/_oxfmtrc.json +3 -0
  31. package/dist/templates/default/_oxlintrc.json +34 -0
  32. package/dist/templates/default/app/(app)/(tabs)/(home)/index.tsx +50 -0
  33. package/dist/templates/default/app/(app)/(tabs)/(home,search)/_layout.tsx +44 -0
  34. package/dist/templates/default/app/(app)/(tabs)/(search)/index.tsx +247 -0
  35. package/dist/templates/default/app/(app)/(tabs)/_layout.tsx +77 -0
  36. package/dist/templates/default/app/(app)/(tabs)/settings/_layout.tsx +37 -0
  37. package/dist/templates/default/app/(app)/(tabs)/settings/index.tsx +362 -0
  38. package/dist/templates/default/app/(app)/(tabs)/settings/preferences.tsx +184 -0
  39. package/dist/templates/default/app/(app)/_layout.tsx +73 -0
  40. package/dist/templates/default/app/(app)/debug.tsx +389 -0
  41. package/dist/templates/default/app/(app)/help.tsx +254 -0
  42. package/dist/templates/default/app/(app)/linked.tsx +116 -0
  43. package/dist/templates/default/app/(app)/privacy.tsx +159 -0
  44. package/dist/templates/default/app/(app)/profile.tsx +915 -0
  45. package/dist/templates/default/app/(app)/sessions.tsx +191 -0
  46. package/dist/templates/default/app/(app)/welcome.tsx +140 -0
  47. package/dist/templates/default/app/(auth)/_layout.tsx +31 -0
  48. package/dist/templates/default/app/(auth)/forgot-password.tsx +168 -0
  49. package/dist/templates/default/app/(auth)/reset-password.tsx +314 -0
  50. package/dist/templates/default/app/(auth)/sign-in.tsx +453 -0
  51. package/dist/templates/default/app/(auth)/sign-up.tsx +563 -0
  52. package/dist/templates/default/app/+native-intent.tsx +14 -0
  53. package/dist/templates/default/app/+not-found.tsx +51 -0
  54. package/dist/templates/default/app/_layout.tsx +102 -0
  55. package/dist/templates/default/app-store/screenshots/.gitkeep +0 -0
  56. package/dist/templates/default/app-store/screenshots/README.md +13 -0
  57. package/dist/templates/default/app.config.ts +201 -0
  58. package/dist/templates/default/app.json +11 -0
  59. package/dist/templates/default/assets/brand-icon-dark.png +0 -0
  60. package/dist/templates/default/assets/brand-icon-light.png +0 -0
  61. package/dist/templates/default/assets/fonts/Geist-Black.ttf +0 -0
  62. package/dist/templates/default/assets/fonts/Geist-BlackItalic.ttf +0 -0
  63. package/dist/templates/default/assets/fonts/Geist-Bold.ttf +0 -0
  64. package/dist/templates/default/assets/fonts/Geist-BoldItalic.ttf +0 -0
  65. package/dist/templates/default/assets/fonts/Geist-ExtraBold.ttf +0 -0
  66. package/dist/templates/default/assets/fonts/Geist-ExtraBoldItalic.ttf +0 -0
  67. package/dist/templates/default/assets/fonts/Geist-ExtraLight.ttf +0 -0
  68. package/dist/templates/default/assets/fonts/Geist-ExtraLightItalic.ttf +0 -0
  69. package/dist/templates/default/assets/fonts/Geist-Italic.ttf +0 -0
  70. package/dist/templates/default/assets/fonts/Geist-Light.ttf +0 -0
  71. package/dist/templates/default/assets/fonts/Geist-LightItalic.ttf +0 -0
  72. package/dist/templates/default/assets/fonts/Geist-Medium.ttf +0 -0
  73. package/dist/templates/default/assets/fonts/Geist-MediumItalic.ttf +0 -0
  74. package/dist/templates/default/assets/fonts/Geist-Regular.ttf +0 -0
  75. package/dist/templates/default/assets/fonts/Geist-SemiBold.ttf +0 -0
  76. package/dist/templates/default/assets/fonts/Geist-SemiBoldItalic.ttf +0 -0
  77. package/dist/templates/default/assets/fonts/Geist-Thin.ttf +0 -0
  78. package/dist/templates/default/assets/fonts/Geist-ThinItalic.ttf +0 -0
  79. package/dist/templates/default/assets/fonts/Geist-Variable-Italic.ttf +0 -0
  80. package/dist/templates/default/assets/fonts/Geist-Variable.ttf +0 -0
  81. package/dist/templates/default/assets/fonts/GeistMono-Bold.ttf +0 -0
  82. package/dist/templates/default/assets/fonts/GeistMono-BoldItalic.ttf +0 -0
  83. package/dist/templates/default/assets/fonts/GeistMono-Italic.ttf +0 -0
  84. package/dist/templates/default/assets/fonts/GeistMono-Medium.ttf +0 -0
  85. package/dist/templates/default/assets/fonts/GeistMono-MediumItalic.ttf +0 -0
  86. package/dist/templates/default/assets/fonts/GeistMono-Regular.ttf +0 -0
  87. package/dist/templates/default/assets/fonts/GeistPixel-Square.ttf +0 -0
  88. package/dist/templates/default/assets/icon.png +0 -0
  89. package/dist/templates/default/assets/sounds/notification.wav +0 -0
  90. package/dist/templates/default/assets/splash-image-dark.png +0 -0
  91. package/dist/templates/default/assets/splash-image-light.png +0 -0
  92. package/dist/templates/default/bun.lock +1860 -0
  93. package/dist/templates/default/components/auth/otp-verification.tsx +255 -0
  94. package/dist/templates/default/components/auth/password-field.tsx +121 -0
  95. package/dist/templates/default/components/auth/segmented-toggle.tsx +47 -0
  96. package/dist/templates/default/components/ui/convex-error.tsx +32 -0
  97. package/dist/templates/default/components/ui/error-boundary.tsx +57 -0
  98. package/dist/templates/default/components/ui/loading-screen.tsx +31 -0
  99. package/dist/templates/default/components/ui/material.tsx +94 -0
  100. package/dist/templates/default/components/ui/offline-banner.tsx +58 -0
  101. package/dist/templates/default/components/ui/prominent-button.tsx +71 -0
  102. package/dist/templates/default/components/ui/skeleton.tsx +107 -0
  103. package/dist/templates/default/components/ui/status-text.tsx +49 -0
  104. package/dist/templates/default/components/ui/update-banner.tsx +82 -0
  105. package/dist/templates/default/constants/layout.ts +102 -0
  106. package/dist/templates/default/constants/theme.ts +401 -0
  107. package/dist/templates/default/constants/ui.ts +77 -0
  108. package/dist/templates/default/convex/_generated/api.d.ts +77 -0
  109. package/dist/templates/default/convex/_generated/api.js +23 -0
  110. package/dist/templates/default/convex/_generated/dataModel.d.ts +60 -0
  111. package/dist/templates/default/convex/_generated/server.d.ts +143 -0
  112. package/dist/templates/default/convex/_generated/server.js +93 -0
  113. package/dist/templates/default/convex/admin.ts +102 -0
  114. package/dist/templates/default/convex/auth.config.ts +6 -0
  115. package/dist/templates/default/convex/auth.ts +335 -0
  116. package/dist/templates/default/convex/constants.ts +46 -0
  117. package/dist/templates/default/convex/convex.config.ts +11 -0
  118. package/dist/templates/default/convex/crons.ts +42 -0
  119. package/dist/templates/default/convex/email.ts +109 -0
  120. package/dist/templates/default/convex/env.ts +31 -0
  121. package/dist/templates/default/convex/errors.ts +33 -0
  122. package/dist/templates/default/convex/functions.ts +54 -0
  123. package/dist/templates/default/convex/http.ts +176 -0
  124. package/dist/templates/default/convex/log.ts +81 -0
  125. package/dist/templates/default/convex/pushTokens.ts +114 -0
  126. package/dist/templates/default/convex/rateLimit.ts +92 -0
  127. package/dist/templates/default/convex/schema.ts +28 -0
  128. package/dist/templates/default/convex/tsconfig.json +18 -0
  129. package/dist/templates/default/convex/users.ts +279 -0
  130. package/dist/templates/default/convex/validators.ts +74 -0
  131. package/dist/templates/default/convex/webhook.ts +193 -0
  132. package/dist/templates/default/convex.json +6 -0
  133. package/dist/templates/default/eas.json +56 -0
  134. package/dist/templates/default/fingerprint.config.js +9 -0
  135. package/dist/templates/default/hooks/use-debounce.ts +20 -0
  136. package/dist/templates/default/hooks/use-deep-link.ts +43 -0
  137. package/dist/templates/default/hooks/use-navigation-tracking.ts +15 -0
  138. package/dist/templates/default/hooks/use-network.ts +11 -0
  139. package/dist/templates/default/hooks/use-notifications.ts +107 -0
  140. package/dist/templates/default/hooks/use-onboarding.ts +15 -0
  141. package/dist/templates/default/hooks/use-reduced-motion.ts +11 -0
  142. package/dist/templates/default/hooks/use-theme.ts +53 -0
  143. package/dist/templates/default/hooks/use-updates.ts +86 -0
  144. package/dist/templates/default/lib/a11y.ts +5 -0
  145. package/dist/templates/default/lib/app.ts +14 -0
  146. package/dist/templates/default/lib/assets.ts +17 -0
  147. package/dist/templates/default/lib/auth-client.ts +21 -0
  148. package/dist/templates/default/lib/convex-auth.tsx +79 -0
  149. package/dist/templates/default/lib/deep-link.ts +71 -0
  150. package/dist/templates/default/lib/dev-menu.ts +119 -0
  151. package/dist/templates/default/lib/device.ts +40 -0
  152. package/dist/templates/default/lib/dynamic-font.ts +49 -0
  153. package/dist/templates/default/lib/env.ts +10 -0
  154. package/dist/templates/default/lib/haptics.ts +24 -0
  155. package/dist/templates/default/lib/notifications.ts +276 -0
  156. package/dist/templates/default/lib/preferences.ts +45 -0
  157. package/dist/templates/default/lib/schemas.ts +137 -0
  158. package/dist/templates/default/lib/storage.ts +47 -0
  159. package/dist/templates/default/lib/updates.ts +107 -0
  160. package/dist/templates/default/metro.config.js +14 -0
  161. package/dist/templates/default/package.json +129 -0
  162. package/dist/templates/default/patches/PR-368.patch +91 -0
  163. package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
  164. package/dist/templates/default/plugins/README.md +9 -0
  165. package/dist/templates/default/plugins/with-auto-signing.js +45 -0
  166. package/dist/templates/default/plugins/with-pod-deployment-target.js +35 -0
  167. package/dist/templates/default/scripts/README.md +36 -0
  168. package/dist/templates/default/scripts/_run.mjs +77 -0
  169. package/dist/templates/default/scripts/clean.ts +543 -0
  170. package/dist/templates/default/scripts/rotate-apple-jwt.mjs +80 -0
  171. package/dist/templates/default/store.config.json +58 -0
  172. package/dist/templates/default/tsconfig.json +13 -0
  173. package/dist/templates/default/vitest.config.ts +21 -0
  174. package/package.json +69 -0
@@ -0,0 +1,331 @@
1
+ ---
2
+ version: alpha
3
+ name: vexpo
4
+ description: >
5
+ Native iOS design system: shadcn neutral palette (preset b1VlJDbW) routed
6
+ through DynamicColorIOS, Geist Variable typography, iOS-native primitives
7
+ via @expo/ui/swift-ui. Brand expression through type, spacing, rounding,
8
+ and material, never through hue.
9
+ colors:
10
+ background: { light: "#FFFFFF", dark: "#0A0A0A" }
11
+ foreground: { light: "#0A0A0A", dark: "#FAFAFA" }
12
+ card: { light: "#FFFFFF", dark: "#171717" }
13
+ popover: { light: "#FFFFFF", dark: "#171717" }
14
+ primary: { light: "#171717", dark: "#E5E5E5" }
15
+ primary-foreground: { light: "#FAFAFA", dark: "#171717" }
16
+ secondary: { light: "#F5F5F5", dark: "#262626" }
17
+ muted: { light: "#F5F5F5", dark: "#262626" }
18
+ muted-foreground: { light: "#737373", dark: "#A1A1A1" }
19
+ accent: { light: "#F5F5F5", dark: "#262626" }
20
+ destructive: { light: "#E7000B", dark: "#FF6467" }
21
+ border: { light: "#E5E5E5", dark: "rgba(255,255,255,0.10)" }
22
+ input: { light: "#E5E5E5", dark: "rgba(255,255,255,0.15)" }
23
+ ring: { light: "#A1A1A1", dark: "#737373" }
24
+ typography:
25
+ family:
26
+ sans: "Geist Variable"
27
+ mono: "GeistMono"
28
+ brand-hero:
29
+ {
30
+ family: sans,
31
+ weight: 900,
32
+ role: "Standalone wordmark, social card headline, emblem center, cover wordmark",
33
+ }
34
+ brand-mark: { family: sans, weight: 700, role: "Lettermark, chiclet vx, bundle icon" }
35
+ brand-label:
36
+ { family: sans, weight: 600, role: "Combination-mark wordmark (subordinate to chiclet)" }
37
+ in-app-title:
38
+ {
39
+ family: sans,
40
+ weight: 700,
41
+ size: 30,
42
+ lineHeight: 38,
43
+ letterSpacing: -0.5,
44
+ role: "Nav titles, screen headers",
45
+ }
46
+ in-app-subtitle: { family: sans, weight: 600, size: 20, lineHeight: 26, role: "Section headers" }
47
+ in-app-default: { family: sans, weight: 400, size: 16, lineHeight: 24, role: "Body text" }
48
+ in-app-default-emphasis:
49
+ { family: sans, weight: 600, size: 16, lineHeight: 24, role: "Emphasized body" }
50
+ technical: { family: mono, weight: 400, role: "Repo URLs, version strings, technical taglines" }
51
+ radius:
52
+ none: 0
53
+ sm: 6
54
+ md: 8
55
+ default: 10
56
+ lg: 10
57
+ xl: 14
58
+ 2xl: 18
59
+ 3xl: 22
60
+ 4xl: 26
61
+ full: 9999
62
+ spacing:
63
+ xxs: 2
64
+ xs: 4
65
+ sm: 8
66
+ md: 12
67
+ lg: 16
68
+ xl: 20
69
+ 2xl: 24
70
+ 3xl: 32
71
+ 4xl: 40
72
+ base-unit: 4
73
+ sizes:
74
+ touch-target-min: 44
75
+ tab-bar-height: 80
76
+ form-max-width: 440
77
+ content-max-width: 600
78
+ materials:
79
+ ultraThin:
80
+ { ios26plus: "GlassView clear", ios164to25: "BlurView systemUltraThinMaterial intensity 30" }
81
+ thin: { ios26plus: "GlassView clear", ios164to25: "BlurView systemThinMaterial intensity 50" }
82
+ regular: { ios26plus: "GlassView regular", ios164to25: "BlurView systemMaterial intensity 70" }
83
+ thick: { ios26plus: "GlassView regular", ios164to25: "BlurView systemThickMaterial intensity 90" }
84
+ chrome:
85
+ { ios26plus: "GlassView regular", ios164to25: "BlurView systemChromeMaterial intensity 100" }
86
+ ---
87
+
88
+ ## Overview
89
+
90
+ Calm, monochrome, native. The vexpo system pairs a shadcn neutral palette (preset `b1VlJDbW`) with the iOS-native typography, materials, and primitives surface. The visual character reads as a developer tool: a quiet workspace that adapts to system appearance, scales with Dynamic Type, and respects the user's accessibility preferences.
91
+
92
+ The brand commits to Geist Variable as the single typeface for both UI and marketing assets. Color carries no brand meaning. `destructive` is the only chromatic token, reserved for irreversible actions and validation errors. Brand expression flows through type weight, spacing, rounding, and the iOS material system.
93
+
94
+ Ground truth lives in `constants/theme.ts` (color palette as `DynamicColorIOS`), `constants/layout.ts` (spacing, font sizes, line heights, fonts), and `constants/ui.ts` (opacity, materials, shadows, durations, sizes). The tokens here are the public contract for agents and contributors. The constants files are the implementation.
95
+
96
+ ## Colors
97
+
98
+ The palette is OKLCH grayscale on the neutral axis (`oklch(L 0 0)`) with no hue. Light mode runs from white surfaces through pale chrome to dark ink. Dark mode inverts. The single non-grayscale token is `destructive` (red), used only for irreversible actions and validation errors.
99
+
100
+ **Background / Foreground**: `#FFFFFF` on `#0A0A0A` (light), `#0A0A0A` on `#FAFAFA` (dark). Pure white, deep ink. No off-white drift.
101
+
102
+ **Card / Popover**: same as background in light, lifted one neutral step in dark (`#171717`) so surfaces read above the page.
103
+
104
+ **Primary**: equal to a dark neutral. Buttons are dark in light mode (`#171717`), light in dark mode (`#E5E5E5`). There is no brand color.
105
+
106
+ **Primary-Foreground**: inverse of `primary` (`#FAFAFA` light, `#171717` dark). Used for text on primary buttons and the "vx" chiclet wordmark.
107
+
108
+ **Muted / Secondary / Accent**: `#F5F5F5` light, `#262626` dark. Used for secondary buttons, hovered menu items, and field backgrounds.
109
+
110
+ **Muted-Foreground**: `#737373` light, `#A1A1A1` dark. Used for secondary labels, captions, taglines, the headline soft-fade endpoint on social cards.
111
+
112
+ **Border / Input**: `#E5E5E5` light, `rgba(255,255,255,0.10)` / `0.15` dark. Border is barely there in light mode. In dark mode it's a subtle alpha overlay rather than a visible neutral, so chrome reads against lifted surfaces.
113
+
114
+ **Ring**: `#A1A1A1` light, `#737373` dark. Focus ring. Renders as a soft halo at 30% alpha around interactive elements.
115
+
116
+ **Destructive**: `#E7000B` light, `#FF6467` dark. Reserved for delete actions, error text, validation states.
117
+
118
+ The dark-mode mapping is a deterministic inversion: backgrounds and inks swap, `card` and `popover` lift one neutral step (`#171717`) so they read above the page, `secondary` / `muted` / `accent` raise to `#262626`, `muted-foreground` lightens to `#A1A1A1`, and `border` / `input` switch to white-at-alpha so chrome reads against the lifted surfaces.
119
+
120
+ Every color in `constants/theme.ts` is wrapped in `DynamicColorIOS` with a `light`, `dark`, `highContrastLight`, and `highContrastDark` value. The high-contrast pair fires when iOS Settings → Accessibility → Display → Increase Contrast is on. Don't add a custom color without the four-variant set.
121
+
122
+ Increased Contrast (iOS Accessibility): every `DynamicColorIOS` token includes a `highContrastLight` and `highContrastDark` variant. The high-contrast values are darker / more-saturated counterparts so the kit still reads when the user has enabled the accessibility setting.
123
+
124
+ ## Typography
125
+
126
+ One face: **Geist Variable** (`@fontsource-variable/geist`-equivalent TTF in `assets/fonts/Geist-Variable.ttf`). The variable axis covers weights 100-900 in a single file. A monospace face, **GeistMono Regular**, is used as a technical accent for code-like content (repo URLs, version strings).
127
+
128
+ There is no serif, no display face, no fallback typeface drift. Geist carries every hierarchy level.
129
+
130
+ ### Brand asset weight tiers
131
+
132
+ Brand surfaces (logos, social cards, cover, splash) use a five-tier weight system:
133
+
134
+ | Tier | Weight | Family | Used for |
135
+ | :-------- | :----------- | :-------- | :---------------------------------------------------------------------------------------------------------- |
136
+ | Hero | 900 Black | Geist | Standalone wordmark, social card headline, emblem center, cover wordmark |
137
+ | Mark | 700 Bold | Geist | Lettermark, chiclet `vx` (pictorial / brand-icon / mascot / splash / bundle / combination), iOS bundle icon |
138
+ | Label | 600 SemiBold | Geist | Combination-mark wordmark (subordinate to the chiclet inside a lockup) |
139
+ | Secondary | 500 Medium | Geist | Social card name top-left, status pill, tech pills row |
140
+ | Body | 400 Regular | Geist | Social card body |
141
+ | Technical | 400 Regular | GeistMono | Repo URL, emblem ring tagline, cover tagline |
142
+
143
+ The same word "vexpo" appears at three different weights to signal role:
144
+
145
+ - **Standalone wordmark** (the logo as a graphic): Black 900. Max impact, the word _is_ the brand.
146
+ - **Combination mark wordmark** (next to a chiclet in a lockup): SemiBold 600. Subordinate to the chiclet which carries the mark.
147
+ - **Social card name** (top-left corner, label role): Medium 500. Context label, not the focal point.
148
+
149
+ ### In-app weight tiers
150
+
151
+ In-app typography uses the same Geist face but caps at Bold (700) for nav titles. Sub-headers and body sit at SemiBold (600) and Regular (400) respectively. The Black (900) weight is reserved for brand assets. Using it inside the app reads as marketing-leak, not chrome.
152
+
153
+ | Role | Weight | Size | Line Height | Letter Spacing |
154
+ | :------------------------------ | :----------- | :---- | :---------- | :------------- |
155
+ | Nav title (large title pattern) | 700 Bold | 30 | 38 | -0.5 |
156
+ | Section header | 600 SemiBold | 20 | 26 | 0 |
157
+ | Body | 400 Regular | 16 | 24 | 0 |
158
+ | Body emphasis | 600 SemiBold | 16 | 24 | 0 |
159
+ | Caption | 400 Regular | 13-14 | 20-22 | 0 |
160
+
161
+ Body and label text scales with the user's Larger Text accessibility setting via `lib/dynamic-font.ts`. The `useDynamicFont` hook multiplies the declared size by `useWindowDimensions().fontScale` before passing to `@expo/ui/swift-ui`'s `font()` modifier. Don't bypass this hook. If you do, your screen breaks at Larger Text.
162
+
163
+ Geist's variable axes are not currently exercised at runtime (we use static TTFs for individual weights via `expo-font`). The variable TTF ships in `assets/fonts/` for future use. If runtime needs variable weight interpolation, point `expo-font` at the variable file and use SwiftUI's `.fontWeight()` modifier.
164
+
165
+ ## Layout & Spacing
166
+
167
+ The base unit is **4px**. Every spacing value in the system is a multiple of 4. The named tokens in `Spacing` are the only valid spacing values. Raw numbers in component code are a smell.
168
+
169
+ ```
170
+ xxs 2 (half-base, only for hairline gaps inside chiclets)
171
+ xs 4 (1 unit)
172
+ sm 8 (2 units)
173
+ md 12 (3 units)
174
+ lg 16 (4 units)
175
+ xl 20 (5 units)
176
+ 2xl 24 (6 units)
177
+ 3xl 32 (8 units)
178
+ 4xl 40 (10 units)
179
+ ```
180
+
181
+ **Page padding**: `Spacing.xl` (20px) horizontal on iPhone form factor. Sections use `Spacing.2xl` (24px) vertical between blocks.
182
+
183
+ **Form max width**: `MaxWidth.form = 440px` so authentication forms feel intentional on iPad. Body content max width is `MaxWidth.content = 600px`.
184
+
185
+ **Touch targets**: `TouchTarget.min = 44px`. iOS HIG minimum. Buttons, tappable rows, and interactive icons all clear this.
186
+
187
+ **Hit slop**: when a touch target is visually smaller than 44px (e.g. a 24px close icon), wrap with `HitSlop.lg = 12` so the touchable region expands without changing layout.
188
+
189
+ **Tab bar**: `TAB_BAR_HEIGHT = 80`, `TAB_BAR_CLEARANCE = 96` (height + lg gap). Add `TAB_BAR_CLEARANCE` as bottom padding to scrollables that sit under the tab bar.
190
+
191
+ Negative space is generous on purpose. The page is mostly background. UI elements are punctuation, not paragraphs.
192
+
193
+ ## Elevation & Materials
194
+
195
+ Depth is signaled by surface tint and the iOS material system, not by drop shadows. The system has three elevation levels:
196
+
197
+ **Level 0 (page)**: flat. `Colors.background`. No shadow, no border. The default for screens, scrollables, list backgrounds.
198
+
199
+ **Level 1 (card / inline surface)**: `Colors.card`. In light mode equal to background. In dark mode lifted one neutral step. No shadow at this level. The lift is communicated by the color shift only.
200
+
201
+ **Level 2 (chrome / floating UI)**: the `<Material>` primitive in `components/ui/material.tsx`. Renders as `GlassView` (Liquid Glass) on iOS 26+ and `BlurView` on iOS 16.4 to 25. Reserved for navigation chrome that floats above content: tab bars, navigation bars, toolbars, sheets, popovers, alerts, notification banners. The `<OfflineBanner>` is the canonical example.
202
+
203
+ **Material variants**:
204
+
205
+ | Variant | iOS 26+ glass style | iOS 16.4-25 blur tint | Use for |
206
+ | :---------- | :------------------ | :------------------------ | :---------------------------------- |
207
+ | `ultraThin` | `clear` | `systemUltraThinMaterial` | Subtle separation over busy content |
208
+ | `thin` | `clear` | `systemThinMaterial` | Pop-up tooltips, hover cards |
209
+ | `regular` | `regular` | `systemMaterial` | Default for tab bars, toolbars |
210
+ | `thick` | `regular` | `systemThickMaterial` | Dense content panels |
211
+ | `chrome` | `regular` | `systemChromeMaterial` | High-density navigation chrome |
212
+
213
+ Materials apply a vibrancy effect that lets background content show through with controlled blur and saturation. Apple's HIG calls this the "navigation layer" and explicitly reserves it for chrome. Don't put materials on form sections or content cards.
214
+
215
+ The blur intensity values are calibrated. Don't lower them. The 35% tint overlay on the `BlurView` fallback path is also calibrated, the smallest tint that still reads as the requested color while preserving the blur.
216
+
217
+ ## Shapes / Radius
218
+
219
+ The radius ladder, generated from a `0.625rem` (10px) base, the shadcn `--radius` default:
220
+
221
+ | Token | Value | Use for |
222
+ | :------------ | :---- | :----------------------------------------------------------------- |
223
+ | `Radius.none` | 0 | Hairline rules, full-bleed edges |
224
+ | `Radius.sm` | 6 | Checkboxes, small chips, kbd hints |
225
+ | `Radius.md` | 8 | Apple Sign In button cornerRadius (matches Apple's recommendation) |
226
+ | `Radius.lg` | 10 | Default for input fields, info badges, secondary surfaces |
227
+ | `Radius.xl` | 14 | Toast banners, hover cards, sheet handles |
228
+ | `Radius.2xl` | 18 | Menu items, tertiary buttons |
229
+ | `Radius.3xl` | 22 | Cards, popovers, dropdown surfaces |
230
+ | `Radius.4xl` | 26 | Primary buttons, input groups, large pills |
231
+ | `Radius.full` | 9999 | Avatars, icon buttons, status badges |
232
+
233
+ **Apple iOS app icon**: rendered at 22.37% of icon side length. The brand chiclet uses `rx = size * 0.2237` to match this exactly (rather than a fixed `Radius` token), so a chiclet at any size reads as an iOS-app-icon shape.
234
+
235
+ **Buttons**: pill-shaped (`Radius.4xl = 26`) regardless of size. The borderedProminent variant in `@expo/ui/swift-ui` already does this. For custom buttons, match.
236
+
237
+ **Avatars and icon buttons**: always `Radius.full`.
238
+
239
+ ## Brand assets
240
+
241
+ Five PNGs in `assets/` carry the brand:
242
+
243
+ - `icon.png`: 1024×1024 flat full-bleed iOS bundle icon. iOS rounds it on the Home Screen automatically.
244
+ - `brand-icon-{light,dark}.png`: 1024×1024 in-app chiclet. Appears on welcome, sign-in, sign-up, and loading screens via `lib/assets.ts`.
245
+ - `splash-image-{light,dark}.png`: 1024×1024 transparent canvas with chiclet centered. Configured via `expo-splash-screen` in `app.config.ts` with `imageWidth: 200`.
246
+
247
+ To rebrand, replace the PNGs in place at the same dimensions and file names. The chiclet shape follows the iOS app icon spec (`rx = size × 0.2237`), so any 1024×1024 PNG with that radius reads as an iOS-app-icon shape.
248
+
249
+ ## Components
250
+
251
+ The system uses `@expo/ui/swift-ui` primitives exclusively for native rendering. Custom components in `components/ui/` add brand-specific composition on top.
252
+
253
+ ### Native primitives (use directly)
254
+
255
+ | Primitive | What it is | Where it lands |
256
+ | :-------------------------- | :---------------------------------------------------------------------------- | :------------------------------------- |
257
+ | `Host` | SwiftUI host view, top-level wrapper for any screen using `@expo/ui/swift-ui` | Every screen root |
258
+ | `VStack` / `HStack` | SwiftUI stacks with spacing + alignment | Layout primitives |
259
+ | `Form` / `Section` | Native iOS Form with grouped sections | Settings, profile editor, preferences |
260
+ | `Picker` | Segmented or wheel picker (use `pickerStyle("segmented")` for inline) | Theme mode, motion preference |
261
+ | `Toggle` | Native iOS toggle | Boolean preferences |
262
+ | `Button` | Native button with role variants (default, cancel, destructive) | All actions |
263
+ | `TextField` / `SecureField` | Native text input | Forms |
264
+ | `Text` | SwiftUI text with modifier composition | All typography |
265
+ | `Image` | SF Symbol renderer (`systemName="..."`) | All inline icons |
266
+ | `ConfirmationDialog` | Native iOS action sheet | Sign-out, delete account, photo picker |
267
+ | `BottomSheet` | Native sheet with detents | Password change, secondary forms |
268
+ | `Spacer` | Flexible space | Layout |
269
+ | `ProgressView` | Spinner or determinate progress | Loading states |
270
+ | `ContentUnavailableView` | Empty state with SF Symbol + title + description | Empty home, no results |
271
+
272
+ ### Custom composition (in `components/ui/`)
273
+
274
+ | Component | Purpose | Notes |
275
+ | :-------------------------- | :---------------------------------------------------------------------------- | :--------------------------------- |
276
+ | `Material` | Translucent surface with iOS 26+ Liquid Glass / iOS 16.4-25 BlurView fallback | Reserve for navigation chrome only |
277
+ | `OfflineBanner` | Top-of-screen notification banner using `Material` chrome variant | Shows when network unavailable |
278
+ | `LoadingScreen` | Brand-icon + spinner, themed by appearance | Suspense fallback |
279
+ | `ErrorBoundary` | Top-level crash boundary with brand recovery UI | Wraps each route segment |
280
+ | `ConvexError` | Maps Convex errors to user-readable copy | Used in error displays |
281
+ | `Skeleton` / `SkeletonLine` | Animated loading placeholders, respect Reduce Motion | Profile loading, list loading |
282
+ | `StatusText` | `ErrorText` + `SuccessText` with accessibility announcements | Form feedback |
283
+
284
+ ### Custom hooks (in `hooks/`)
285
+
286
+ | Hook | Purpose |
287
+ | :-------------------------------- | :---------------------------------------------------------------------------------------------- |
288
+ | `useThemeMode` / `useColorScheme` | App-level light/dark/system override on top of OS appearance |
289
+ | `useColors` | Returns the `Colors` palette (currently constant, kept as a hook for future per-theme variants) |
290
+ | `useThemedAsset` | Picks light or dark asset based on active appearance |
291
+ | `useDynamicFont` | Multiplies declared font sizes by accessibility fontScale before passing to `@expo/ui` `font()` |
292
+ | `useReducedMotion` | Combines OS Reduce Motion + in-app override. Drives animation duration / disable |
293
+ | `useNetwork` | Online / offline state for `OfflineBanner` |
294
+ | `useNotifications` | Push token registration + foreground handler |
295
+ | `useDeepLinkHandler` | Handles `applinks:` URLs from associated domains |
296
+ | `useUpdates` | EAS Update check + apply with branded UI |
297
+ | `useOnboarding` | First-launch welcome flow gate |
298
+ | `useDebounce` | Standard debounce |
299
+ | `useNavigationTracking` | Analytics / route logging |
300
+
301
+ ## Do's and Don'ts
302
+
303
+ **Do** use the existing primitives in `@expo/ui/swift-ui` and `components/ui/` before reaching for `react-native` View/Text. Native primitives encode iOS HIG. Ad-hoc compositions drift from it.
304
+
305
+ **Do** scale every text size through `useDynamicFont` so Larger Text works. Hard-coded sizes break at the largest accessibility setting.
306
+
307
+ **Do** wrap every custom color in `DynamicColorIOS` with all four variants (`light`, `dark`, `highContrastLight`, `highContrastDark`) so Increase Contrast works.
308
+
309
+ **Do** use the radius ladder. If something needs corners, it's almost always one of `Radius.lg` (10), `Radius.2xl` (18), `Radius.3xl` (22), `Radius.4xl` (26), or `Radius.full`.
310
+
311
+ **Do** put translucent material only on navigation chrome: tab bars, toolbars, banners, sheets, popovers. Apple HIG reserves the navigation layer for materials.
312
+
313
+ **Do** match icon size to context: `IconSize.sm` (14) for inline chips, `IconSize.md` (16) for menu items, `IconSize.lg` (18) for medium controls, `IconSize.4xl` (32) for headers, `IconSize.5xl` (48) for empty-state hero icons.
314
+
315
+ **Don't** introduce a second typeface. Geist carries every hierarchy level. If a heading needs more weight, increase `font-weight` from 600 to 700. Don't reach for a serif or a display face.
316
+
317
+ **Don't** use color to communicate brand. The only chromatic token is `destructive`, and it means "irreversible." Brand expression is through type, spacing, rounding, and material, not hue.
318
+
319
+ **Don't** add a drop shadow to a content card. Cards lay flat in the vexpo system. Only popovers / menus / sheets / banners are elevated, and they elevate via `<Material>`, not via shadow.
320
+
321
+ **Don't** use `font-weight: 900` (Black) inside the app. Black is reserved for brand assets. In-app titles cap at Bold (700).
322
+
323
+ **Don't** define a new spacing value. Use `Spacing.{xs,sm,md,lg,xl,2xl,3xl,4xl}`. If you need a value that isn't in the scale, the scale is wrong. Propose a new token rather than introducing magic numbers.
324
+
325
+ **Don't** override the system appearance picker. iOS already exposes Auto / Light / Dark in Settings. The in-app picker in `app/(app)/(tabs)/settings/preferences.tsx` is a Ray-specific override. HIG says "Avoid offering an app-specific appearance setting." We carry it as a deliberate choice. Don't add another layer on top.
326
+
327
+ **Don't** narrow forms by changing the page outer max width. Wrap a narrow form in `MaxWidth.form` (440) inside a wider page so chrome alignment is preserved across screens.
328
+
329
+ **Don't** add new brand surfaces (App Store screenshots, marketing covers, social cards) by hand-editing the PNGs in `assets/`. Generate them from a single config so light/dark stay in sync. The five PNGs in `assets/` are runtime-only. bundle icon, in-app chiclet, splash screen.
330
+
331
+ **Don't** delete a logo type because we don't use it today. The seven types (lettermark, wordmark, pictorial, abstract, mascot, combination, emblem) ship together so any future surface, App Store badge, swag, conference talk slide, can pick the right one without a re-render.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ramon Claudio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,153 @@
1
+ # vexpo
2
+
3
+ Expo SDK 56 + Convex + Better Auth + Resend, wired end-to-end for iOS. Native SwiftUI via `@expo/ui/swift-ui`, email + password + email OTP + Apple Sign In, APNs push, Universal Links, profile + active sessions with avatar uploads and device-by-device revocation. EAS for the whole build surface: 10 workflows, fingerprint-gated OTA-or-build, TestFlight, rollback, rollout, ASC events, and the Apple Sign In JWT rotation cron.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ bun install
9
+
10
+ bunx vexpo lite # 60-second path: Convex + Better Auth, simulator-ready
11
+ bunx vexpo lite --new # same, plus a Convex signup walkthrough if you don't have one
12
+ ```
13
+
14
+ Then in two terminals:
15
+
16
+ ```bash
17
+ bun run convex:dev # terminal 1
18
+ bun run ios # terminal 2
19
+ ```
20
+
21
+ Lite mode skips Apple / EAS / Resend entirely. `REQUIRE_EMAIL_VERIFICATION` is off on Convex so sign-up auto-verifies, the user lands in the app with one tap, and the UI hides the OTP / password-reset / change-email flows that need Resend to work.
22
+
23
+ When you're ready to ship, swap `lite` for `full`:
24
+
25
+ ```bash
26
+ bunx vexpo full # provisions Resend, Apple Sign In, EAS, rebrand wizard
27
+ bunx vexpo full --new # same, plus walks Apple / Convex / Expo / Resend signups
28
+ ```
29
+
30
+ `full` writes `.env.local`, sets Convex env vars (`REQUIRE_EMAIL_VERIFICATION=true` once Resend is wired), validates the ASC API key, signs the SIWA JWT, runs `eas init` and `eas env:push`, prompts the rebrand wizard. Prints the `eas build` command at the end. vexpo doesn't run it for you, you run `bunx eas build -p ios --profile production --auto-submit-with-profile testflight` when you're ready.
31
+
32
+ Run `bunx vexpo doctor` any time to auth-check every credential against the real service and cross-reference IDs across `.env.local`, Convex env, EAS env, and `app.config.ts`. Catches "wrong .p8 from another project" or ".env.prod copied from a different fork" in seconds.
33
+
34
+ Long-form walkthrough with every prompt, every env-var alternative, and recovery paths: [`SETUP.md`](./SETUP.md).
35
+
36
+ ## What's wired up
37
+
38
+ - Convex backend with reactive queries, storage, real-time sync, and rate limiting on every endpoint via `@convex-dev/rate-limiter`
39
+ - Better Auth via `@convex-dev/better-auth` (sessions, accounts, devices)
40
+ - Resend via `@convex-dev/resend` for OTP, password reset, change-email, with webhook delivery events
41
+ - Apple Sign In via Apple's official `AppleAuthenticationButton`, HIG-compliant BLACK/WHITE theme-aware, SIWA Services ID + ES256 JWT signing (180-day expiry, auto-rotated every 90 days)
42
+ - APNs push via `expo-notifications` with token registration on sign-in
43
+ - Apple Universal Links from Convex's HTTP router (AASA at `/.well-known/apple-app-site-association`)
44
+ - Profile editing with avatar uploads to Convex storage
45
+ - Active sessions screen with device-by-device revocation
46
+ - Theme switching, haptics toggle, reduced motion, dynamic type, VoiceOver labels everywhere
47
+ - Spotlight-style search tab (debounced, scored, keyword-aware)
48
+ - Skeleton placeholders during initial query loads
49
+ - Debug screen at `/debug` gated by toggle, off in production by default
50
+ - Liquid Glass on iOS 26+ via `expo-glass-effect`, UIVisualEffectView blur fallback on iOS 16.4-25 via `expo-blur`, both behind a `<Material>` primitive
51
+ - EAS Build / Update / Submit / Metadata. `runtimeVersion: { policy: "fingerprint" }`, branch/channel model, `appVersionSource: "remote"`. ASC API key managed by EAS (`eas credentials -p ios`), no `eas.json` patches
52
+ - 10 EAS Workflows under `.eas/workflows/`: dev builds, PR previews with `github-comment` + QR + fingerprint-gated OTA-or-build, deploy on `main`, TestFlight on `beta/*`, manual rollback / rollout, ASC event triggers to Slack, the SIWA JWT rotation cron, Maestro E2E
53
+ - GitHub Actions for general-purpose checks: typecheck, lint, format, tests, fingerprint diff on PR + push to `main`
54
+
55
+ ## Pre-reqs
56
+
57
+ - macOS + Xcode for the simulator and signing
58
+ - Apple Developer Program membership ($99/yr) when you're ready to ship
59
+ - A domain you control DNS for, for Resend's sending domain
60
+ - Bun or Node 20+
61
+
62
+ ## Scripts
63
+
64
+ ```
65
+ bun run dev Metro + dev client
66
+ bun run start Metro with cleared cache
67
+ bun run ios Clean prebuild + compile + run on simulator
68
+ bun run ios:dev Run on simulator (skip prebuild, fast)
69
+ bun run ios:device Clean prebuild + compile + run on physical device
70
+ bun run prebuild Generate iOS native project from config
71
+
72
+ bun run convex:dev Convex dev server (watch mode)
73
+ bun run convex:deploy Deploy Convex functions to production
74
+ bun run convex:logs Tail dev deployment logs
75
+ bun run convex:logs:prod Tail prod deployment logs
76
+ bun run convex:env List dev env vars
77
+ bun run convex:env:prod List prod env vars
78
+ bun run convex:insights OCC conflicts + resource limits (dev)
79
+ bun run convex:insights:prod Same for prod
80
+ bun run convex:dashboard Open the Convex dashboard
81
+ bun run convex:codegen Regenerate convex/_generated/
82
+
83
+ bun run eas:dev eas build -p ios --profile development:simulator
84
+ bun run eas:dev:device eas build -p ios --profile development:device
85
+ bun run eas:tf eas build -p ios --profile production --auto-submit-with-profile testflight
86
+ bun run eas:prod eas build -p ios --profile production
87
+ bun run metadata:lint eas metadata:lint
88
+ bun run metadata:push eas metadata:lint && eas metadata:push
89
+ bun run metadata:pull eas metadata:pull
90
+ bun run env:pull eas env:pull --environment development
91
+ bun run env:pull:prod eas env:pull --environment production
92
+
93
+ bun run clean Trash node_modules, ios, caches, then reinstall
94
+ bun run clean:metro Trash Metro/Babel/Haste caches only
95
+ bun run clean:state Wipe .setup-state.json + standard clean
96
+ bun run typecheck tsc --noEmit
97
+ bun run lint oxlint
98
+ bun run format oxfmt
99
+ bun run format:check oxfmt --check
100
+ bun run test vitest run
101
+ bun run test:watch vitest
102
+ bun run fp Print Expo fingerprint hash
103
+ bun run fp:diff Diff fingerprint vs base ref
104
+ bun run upgrade expo install expo@canary && expo install --fix
105
+ bun run upgrade:stable expo install expo@latest && expo install --fix
106
+ ```
107
+
108
+ Setup is one-shot, not a `package.json` script. Run `bunx vexpo lite` / `bunx vexpo full` / `bunx vexpo doctor` directly. All deletions go through `trash` (macOS Trash, recoverable).
109
+
110
+ ## Project structure
111
+
112
+ ```
113
+ app/ Expo Router screens
114
+ (auth)/ Sign in, sign up, forgot/reset password
115
+ (app)/ Authenticated screens
116
+ (tabs)/ Tab navigation
117
+ welcome.tsx, profile.tsx, sessions.tsx, debug.tsx, ...
118
+ +native-intent.tsx Deep link validation
119
+ +not-found.tsx 404 fallback
120
+ components/ Reusable UI
121
+ constants/ Theme, layout, UI tokens
122
+ convex/ Convex backend
123
+ hooks/ useNetwork, useTheme, useUpdates, etc.
124
+ lib/ Auth client, haptics, env, deep links
125
+ plugins/
126
+ with-auto-signing.js Sets CODE_SIGN_STYLE=Automatic + DEVELOPMENT_TEAM
127
+ with-pod-deployment-target.js Forces every pod to iOS 16.4
128
+ .eas/workflows/ 10 EAS Workflow YAML files
129
+ .github/workflows/check.yml Typecheck, lint, format, tests, fingerprint diff
130
+ scripts/
131
+ clean.ts Trash + reinstall
132
+ rotate-apple-jwt.mjs CI: re-sign JWT from env vars
133
+ __tests__/ Convex constants + validators + HMAC verification + deep-link
134
+ ```
135
+
136
+ ## Long-form docs
137
+
138
+ - [`SETUP.md`](./SETUP.md). Every setup phase with full prompts, env-var alternatives for non-interactive runs, recovery paths.
139
+ - [`DESIGN.md`](./DESIGN.md). Color palette, typography, spacing, radius ladder, materials, the SwiftUI primitives + custom composition surface.
140
+ - [`AGENTS.md`](./AGENTS.md). Guidance for AI coding agents working in this codebase.
141
+
142
+ ## Version pinning
143
+
144
+ Every `expo-*` package uses the same canary tag. Mismatched tags cause subtle runtime crashes.
145
+
146
+ `package.json` overrides:
147
+
148
+ - `@better-auth/passkey: 1.6.9` prevents better-auth from pulling SolidJS deps that break Metro.
149
+ - `@expo/ui: 56.0.0-canary-20260506-03817f5` unifies the version across transitive deps.
150
+
151
+ `react-native-reanimated 4.3.0` is intentionally ahead of the canary's expected 4.2.1 because Bun resolves `react-native-worklets@0.8.x` which needs Reanimated 4.3+. Listed in `expo.install.exclude` to silence the doctor warning.
152
+
153
+ `@convex-dev/better-auth@0.12.0` is the minimum compatible with `better-auth@1.6.9`. Earlier versions peer-dep `better-auth <1.6.0` and reject the `mode` field newer better-auth adds to adapter queries, breaking signup. Currently the template installs from `patches/convex-dev-better-auth-0.12.2.tgz` until [PR #368](https://github.com/get-convex/better-auth/pull/368) merges.