@mars-stack/cli 0.2.0 → 0.2.2

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 (173) hide show
  1. package/package.json +2 -2
  2. package/template/.cursor/rules/composition-patterns.mdc +186 -0
  3. package/template/.cursor/rules/data-access.mdc +29 -0
  4. package/template/.cursor/rules/project-structure.mdc +34 -0
  5. package/template/.cursor/rules/security.mdc +25 -0
  6. package/template/.cursor/rules/testing.mdc +24 -0
  7. package/template/.cursor/rules/ui-conventions.mdc +29 -0
  8. package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
  9. package/template/.cursor/skills/add-audit-log/SKILL.md +373 -0
  10. package/template/.cursor/skills/add-blog/SKILL.md +447 -0
  11. package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
  12. package/template/.cursor/skills/add-component/SKILL.md +158 -0
  13. package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
  14. package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
  15. package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
  16. package/template/.cursor/skills/add-feature/SKILL.md +174 -0
  17. package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
  18. package/template/.cursor/skills/add-page/SKILL.md +151 -0
  19. package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
  20. package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
  21. package/template/.cursor/skills/add-role/SKILL.md +156 -0
  22. package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
  23. package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
  24. package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
  25. package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
  26. package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
  27. package/template/.cursor/skills/build-form/SKILL.md +231 -0
  28. package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
  29. package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
  30. package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
  31. package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
  32. package/template/.cursor/skills/configure-email/SKILL.md +170 -0
  33. package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
  34. package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
  35. package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
  36. package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
  37. package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
  38. package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
  39. package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
  40. package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
  41. package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
  42. package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
  43. package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
  44. package/template/.cursor/skills/configure-search/SKILL.md +581 -0
  45. package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
  46. package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
  47. package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
  48. package/template/.cursor/skills/create-seed/SKILL.md +191 -0
  49. package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
  50. package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
  51. package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
  52. package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
  53. package/template/.cursor/skills/setup-project/SKILL.md +104 -0
  54. package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
  55. package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
  56. package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
  57. package/template/AGENTS.md +104 -0
  58. package/template/ARCHITECTURE.md +102 -0
  59. package/template/docs/QUALITY_SCORE.md +20 -0
  60. package/template/docs/design-docs/conversation-as-system-record.md +70 -0
  61. package/template/docs/design-docs/core-beliefs.md +43 -0
  62. package/template/docs/design-docs/index.md +8 -0
  63. package/template/docs/exec-plans/active/.gitkeep +0 -0
  64. package/template/docs/exec-plans/completed/.gitkeep +0 -0
  65. package/template/docs/exec-plans/tech-debt.md +7 -0
  66. package/template/docs/generated/.gitkeep +0 -0
  67. package/template/docs/product-specs/index.md +7 -0
  68. package/template/docs/references/index.md +18 -0
  69. package/template/e2e/api.spec.ts +20 -0
  70. package/template/e2e/auth.spec.ts +24 -0
  71. package/template/e2e/public.spec.ts +25 -0
  72. package/template/eslint.config.mjs +24 -0
  73. package/template/next-env.d.ts +6 -0
  74. package/template/next.config.ts +45 -0
  75. package/template/package.json +80 -0
  76. package/template/playwright.config.ts +31 -0
  77. package/template/postcss.config.mjs +8 -0
  78. package/template/prisma/generated/prisma/browser.ts +49 -0
  79. package/template/prisma/generated/prisma/client.ts +73 -0
  80. package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
  81. package/template/prisma/generated/prisma/enums.ts +15 -0
  82. package/template/prisma/generated/prisma/internal/class.ts +254 -0
  83. package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
  84. package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
  85. package/template/prisma/generated/prisma/models/Account.ts +1543 -0
  86. package/template/prisma/generated/prisma/models/File.ts +1529 -0
  87. package/template/prisma/generated/prisma/models/Session.ts +1415 -0
  88. package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
  89. package/template/prisma/generated/prisma/models/User.ts +2235 -0
  90. package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
  91. package/template/prisma/generated/prisma/models.ts +17 -0
  92. package/template/prisma/schema/auth.prisma +69 -0
  93. package/template/prisma/schema/base.prisma +8 -0
  94. package/template/prisma/schema/file.prisma +15 -0
  95. package/template/prisma/schema/subscription.prisma +17 -0
  96. package/template/prisma.config.ts +13 -0
  97. package/template/scripts/check-architecture.ts +221 -0
  98. package/template/scripts/check-doc-freshness.ts +242 -0
  99. package/template/scripts/ensure-db.mjs +291 -0
  100. package/template/scripts/generate-docs.ts +143 -0
  101. package/template/scripts/generate-env-example.ts +89 -0
  102. package/template/scripts/seed.ts +56 -0
  103. package/template/scripts/update-quality-score.ts +263 -0
  104. package/template/src/__tests__/architecture.test.ts +114 -0
  105. package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
  106. package/template/src/app/(auth)/layout.tsx +11 -0
  107. package/template/src/app/(auth)/register/page.tsx +162 -0
  108. package/template/src/app/(auth)/reset-password/page.tsx +109 -0
  109. package/template/src/app/(auth)/sign-in/page.tsx +122 -0
  110. package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
  111. package/template/src/app/(auth)/verify/page.tsx +56 -0
  112. package/template/src/app/(protected)/admin/page.tsx +108 -0
  113. package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
  114. package/template/src/app/(protected)/dashboard/page.tsx +22 -0
  115. package/template/src/app/(protected)/layout.tsx +262 -0
  116. package/template/src/app/(protected)/settings/page.tsx +370 -0
  117. package/template/src/app/api/auth/forgot/route.ts +63 -0
  118. package/template/src/app/api/auth/login/route.ts +121 -0
  119. package/template/src/app/api/auth/logout/route.ts +19 -0
  120. package/template/src/app/api/auth/me/route.ts +30 -0
  121. package/template/src/app/api/auth/reset/route.ts +45 -0
  122. package/template/src/app/api/auth/signup/route.ts +85 -0
  123. package/template/src/app/api/auth/verify/route.ts +46 -0
  124. package/template/src/app/api/csrf/route.ts +12 -0
  125. package/template/src/app/api/health/route.ts +10 -0
  126. package/template/src/app/api/protected/admin/users/route.ts +24 -0
  127. package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
  128. package/template/src/app/api/protected/billing/portal/route.ts +39 -0
  129. package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
  130. package/template/src/app/api/protected/files/upload/route.ts +64 -0
  131. package/template/src/app/api/protected/user/password/route.ts +63 -0
  132. package/template/src/app/api/protected/user/profile/route.ts +35 -0
  133. package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
  134. package/template/src/app/api/protected/user/sessions/route.ts +22 -0
  135. package/template/src/app/api/readiness/route.ts +15 -0
  136. package/template/src/app/api/webhooks/stripe/route.ts +166 -0
  137. package/template/src/app/error.tsx +33 -0
  138. package/template/src/app/layout.tsx +29 -0
  139. package/template/src/app/not-found.tsx +20 -0
  140. package/template/src/app/page.tsx +136 -0
  141. package/template/src/app/privacy/page.tsx +178 -0
  142. package/template/src/app/providers.tsx +8 -0
  143. package/template/src/app/terms/page.tsx +139 -0
  144. package/template/src/config/app.config.ts +70 -0
  145. package/template/src/config/routes.ts +17 -0
  146. package/template/src/features/admin/index.ts +11 -0
  147. package/template/src/features/admin/permissions.ts +64 -0
  148. package/template/src/features/auth/context/AuthContext.tsx +96 -0
  149. package/template/src/features/auth/context/index.ts +2 -0
  150. package/template/src/features/auth/index.ts +3 -0
  151. package/template/src/features/auth/server/consent.ts +66 -0
  152. package/template/src/features/auth/server/session-revocation.ts +20 -0
  153. package/template/src/features/auth/server/sessions.ts +66 -0
  154. package/template/src/features/auth/server/user.ts +166 -0
  155. package/template/src/features/auth/types.ts +19 -0
  156. package/template/src/features/auth/validators.ts +29 -0
  157. package/template/src/features/billing/server/index.ts +66 -0
  158. package/template/src/features/billing/types.ts +43 -0
  159. package/template/src/features/uploads/server/index.ts +49 -0
  160. package/template/src/features/uploads/types.ts +26 -0
  161. package/template/src/lib/core/email/templates/base-layout.ts +122 -0
  162. package/template/src/lib/core/email/templates/index.ts +4 -0
  163. package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
  164. package/template/src/lib/core/email/templates/verification-email.ts +41 -0
  165. package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
  166. package/template/src/lib/mars.ts +56 -0
  167. package/template/src/lib/prisma.ts +19 -0
  168. package/template/src/proxy.ts +92 -0
  169. package/template/src/styles/brand.css +17 -0
  170. package/template/src/styles/globals.css +6 -0
  171. package/template/tsconfig.json +59 -0
  172. package/template/vitest.config.ts +41 -0
  173. package/template/vitest.setup.ts +24 -0
@@ -0,0 +1,309 @@
1
+ # Skill: Configure Dark Mode
2
+
3
+ Set up a dark mode toggle with system preference detection and persistence in a MARS application.
4
+
5
+ ## When to Use
6
+
7
+ Use this skill when the user asks to add dark mode, a theme toggle, light/dark switching, or system theme preference support.
8
+
9
+ ## Prerequisites
10
+
11
+ - MARS design tokens configured (the token system already supports `.dark` class on `<html>`)
12
+ - Tailwind CSS configured with `darkMode: 'class'`
13
+
14
+ ## Architecture
15
+
16
+ MARS uses OKLCH-based design tokens with semantic names like `--color-surface-primary`. The `.dark` class on `<html>` swaps all token values to their dark variants. This skill adds:
17
+
18
+ 1. **Theme context provider** — manages current theme state
19
+ 2. **Persistence** — saves preference to `localStorage`
20
+ 3. **System detection** — respects `prefers-color-scheme` media query
21
+ 4. **Flash prevention** — inline script prevents FOUC (flash of unstyled content)
22
+ 5. **Toggle component** — UI control for switching themes
23
+
24
+ ## Step 1: Theme Configuration
25
+
26
+ Add theme settings to `src/config/app.config.ts`:
27
+
28
+ ```typescript
29
+ ui: {
30
+ theme: {
31
+ default: 'system' as 'light' | 'dark' | 'system',
32
+ enableToggle: true,
33
+ },
34
+ },
35
+ ```
36
+
37
+ ## Step 2: Prevent Flash of Wrong Theme
38
+
39
+ Add an inline script to `src/app/layout.tsx` that runs before React hydration:
40
+
41
+ ```typescript
42
+ // src/app/layout.tsx
43
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
44
+ return (
45
+ <html lang="en" suppressHydrationWarning>
46
+ <head>
47
+ <script
48
+ dangerouslySetInnerHTML={{
49
+ __html: `
50
+ (function() {
51
+ try {
52
+ var stored = localStorage.getItem('mars-theme');
53
+ var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
54
+ var theme = stored || '${appConfig.ui.theme.default}';
55
+ var isDark = theme === 'dark' || (theme === 'system' && prefersDark);
56
+ document.documentElement.classList.toggle('dark', isDark);
57
+ } catch (e) {}
58
+ })();
59
+ `,
60
+ }}
61
+ />
62
+ </head>
63
+ <body>
64
+ <ThemeProvider>{children}</ThemeProvider>
65
+ </body>
66
+ </html>
67
+ );
68
+ }
69
+ ```
70
+
71
+ ## Step 3: Theme Context Provider
72
+
73
+ ```typescript
74
+ // src/lib/shared/components/providers/ThemeProvider.tsx
75
+ 'use client';
76
+
77
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
78
+
79
+ type Theme = 'light' | 'dark' | 'system';
80
+
81
+ interface ThemeContextValue {
82
+ theme: Theme;
83
+ resolvedTheme: 'light' | 'dark';
84
+ setTheme: (theme: Theme) => void;
85
+ }
86
+
87
+ const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
88
+
89
+ const STORAGE_KEY = 'mars-theme';
90
+
91
+ export function ThemeProvider({ children }: { children: React.ReactNode }) {
92
+ const [theme, setThemeState] = useState<Theme>('system');
93
+ const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light');
94
+
95
+ const applyTheme = useCallback((newTheme: Theme) => {
96
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
97
+ const isDark = newTheme === 'dark' || (newTheme === 'system' && prefersDark);
98
+ document.documentElement.classList.toggle('dark', isDark);
99
+ setResolvedTheme(isDark ? 'dark' : 'light');
100
+ }, []);
101
+
102
+ const setTheme = useCallback(
103
+ (newTheme: Theme) => {
104
+ setThemeState(newTheme);
105
+ localStorage.setItem(STORAGE_KEY, newTheme);
106
+ applyTheme(newTheme);
107
+ },
108
+ [applyTheme],
109
+ );
110
+
111
+ useEffect(() => {
112
+ const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
113
+ const initial = stored || 'system';
114
+ setThemeState(initial);
115
+ applyTheme(initial);
116
+
117
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
118
+ function handleChange() {
119
+ const current = localStorage.getItem(STORAGE_KEY) as Theme | null;
120
+ if (!current || current === 'system') {
121
+ applyTheme('system');
122
+ }
123
+ }
124
+
125
+ mediaQuery.addEventListener('change', handleChange);
126
+ return () => mediaQuery.removeEventListener('change', handleChange);
127
+ }, [applyTheme]);
128
+
129
+ const value = useMemo(
130
+ () => ({ theme, resolvedTheme, setTheme }),
131
+ [theme, resolvedTheme, setTheme],
132
+ );
133
+
134
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
135
+ }
136
+
137
+ export function useTheme(): ThemeContextValue {
138
+ const context = useContext(ThemeContext);
139
+ if (!context) {
140
+ throw new Error('useTheme must be used within a ThemeProvider');
141
+ }
142
+ return context;
143
+ }
144
+ ```
145
+
146
+ ## Step 4: Theme Toggle Component
147
+
148
+ ```typescript
149
+ // src/lib/shared/components/patterns/ThemeToggle.tsx
150
+ 'use client';
151
+
152
+ import { useTheme } from '@/lib/shared/components/providers/ThemeProvider';
153
+
154
+ export function ThemeToggle() {
155
+ const { theme, setTheme, resolvedTheme } = useTheme();
156
+
157
+ return (
158
+ <div className="flex items-center gap-1 rounded-lg bg-surface-secondary p-1">
159
+ <button
160
+ onClick={() => setTheme('light')}
161
+ className={`rounded-md px-3 py-1.5 text-sm transition-colors ${
162
+ theme === 'light'
163
+ ? 'bg-surface-primary text-content-primary shadow-sm'
164
+ : 'text-content-tertiary hover:text-content-secondary'
165
+ }`}
166
+ aria-label="Light theme"
167
+ >
168
+ <SunIcon className="h-4 w-4" />
169
+ </button>
170
+ <button
171
+ onClick={() => setTheme('system')}
172
+ className={`rounded-md px-3 py-1.5 text-sm transition-colors ${
173
+ theme === 'system'
174
+ ? 'bg-surface-primary text-content-primary shadow-sm'
175
+ : 'text-content-tertiary hover:text-content-secondary'
176
+ }`}
177
+ aria-label="System theme"
178
+ >
179
+ <MonitorIcon className="h-4 w-4" />
180
+ </button>
181
+ <button
182
+ onClick={() => setTheme('dark')}
183
+ className={`rounded-md px-3 py-1.5 text-sm transition-colors ${
184
+ theme === 'dark'
185
+ ? 'bg-surface-primary text-content-primary shadow-sm'
186
+ : 'text-content-tertiary hover:text-content-secondary'
187
+ }`}
188
+ aria-label="Dark theme"
189
+ >
190
+ <MoonIcon className="h-4 w-4" />
191
+ </button>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ function SunIcon({ className }: { className?: string }) {
197
+ return (
198
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className={className}>
199
+ <path d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zm0 13a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zm8-5a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zm10.657-5.657a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0zm-9.193 9.193a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0zm9.193 0a.75.75 0 01-1.06 0l-1.061-1.06a.75.75 0 011.06-1.061l1.061 1.06a.75.75 0 010 1.061zM5.464 6.525a.75.75 0 01-1.06 0L3.343 5.464a.75.75 0 011.06-1.06l1.061 1.06a.75.75 0 010 1.061zM10 6.5a3.5 3.5 0 100 7 3.5 3.5 0 000-7z" />
200
+ </svg>
201
+ );
202
+ }
203
+
204
+ function MoonIcon({ className }: { className?: string }) {
205
+ return (
206
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className={className}>
207
+ <path fillRule="evenodd" d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z" clipRule="evenodd" />
208
+ </svg>
209
+ );
210
+ }
211
+
212
+ function MonitorIcon({ className }: { className?: string }) {
213
+ return (
214
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className={className}>
215
+ <path fillRule="evenodd" d="M2 4.25A2.25 2.25 0 014.25 2h11.5A2.25 2.25 0 0118 4.25v8.5A2.25 2.25 0 0115.75 15h-3.105a3.501 3.501 0 001.1 1.677A.75.75 0 0113.26 18H6.74a.75.75 0 01-.484-1.323A3.501 3.501 0 007.355 15H4.25A2.25 2.25 0 012 12.75v-8.5zm1.5 0a.75.75 0 01.75-.75h11.5a.75.75 0 01.75.75v7.5a.75.75 0 01-.75.75H4.25a.75.75 0 01-.75-.75v-7.5z" clipRule="evenodd" />
216
+ </svg>
217
+ );
218
+ }
219
+ ```
220
+
221
+ ## Step 5: Simple Toggle (Alternative)
222
+
223
+ For a minimal sun/moon toggle button:
224
+
225
+ ```typescript
226
+ // src/lib/shared/components/patterns/ThemeToggleSimple.tsx
227
+ 'use client';
228
+
229
+ import { useTheme } from '@/lib/shared/components/providers/ThemeProvider';
230
+
231
+ export function ThemeToggleSimple() {
232
+ const { resolvedTheme, setTheme } = useTheme();
233
+
234
+ function toggle() {
235
+ setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');
236
+ }
237
+
238
+ return (
239
+ <button
240
+ onClick={toggle}
241
+ className="rounded-md p-2 text-content-secondary hover:bg-surface-secondary hover:text-content-primary transition-colors"
242
+ aria-label={`Switch to ${resolvedTheme === 'dark' ? 'light' : 'dark'} mode`}
243
+ >
244
+ {resolvedTheme === 'dark' ? <SunIcon className="h-5 w-5" /> : <MoonIcon className="h-5 w-5" />}
245
+ </button>
246
+ );
247
+ }
248
+ ```
249
+
250
+ ## Step 6: Add to Navbar
251
+
252
+ ```typescript
253
+ import { ThemeToggle } from '@/lib/shared/components/patterns/ThemeToggle';
254
+
255
+ // In your navbar component:
256
+ <nav className="flex items-center gap-4">
257
+ {/* ... other nav items */}
258
+ <ThemeToggle />
259
+ </nav>
260
+ ```
261
+
262
+ ## Step 7: Tailwind Configuration
263
+
264
+ Ensure `tailwind.config.ts` uses class-based dark mode:
265
+
266
+ ```typescript
267
+ import type { Config } from 'tailwindcss';
268
+
269
+ const config: Config = {
270
+ darkMode: 'class',
271
+ // ... rest of config
272
+ };
273
+ ```
274
+
275
+ The design token CSS variables already swap under `.dark`:
276
+
277
+ ```css
278
+ :root {
279
+ --color-surface-primary: oklch(1 0 0);
280
+ --color-content-primary: oklch(0.15 0 0);
281
+ /* ... */
282
+ }
283
+
284
+ .dark {
285
+ --color-surface-primary: oklch(0.15 0 0);
286
+ --color-content-primary: oklch(0.95 0 0);
287
+ /* ... */
288
+ }
289
+ ```
290
+
291
+ ## Testing
292
+
293
+ 1. Click the toggle — verify the theme switches and the `.dark` class toggles on `<html>`.
294
+ 2. Refresh the page — verify the theme persists (no flash).
295
+ 3. Set to "system", change OS preference — verify the theme follows.
296
+ 4. Clear `localStorage`, reload — verify it defaults to the `app.config.ts` setting.
297
+ 5. Verify all semantic token colors update (surfaces, content, borders, interactive).
298
+
299
+ ## Checklist
300
+
301
+ - [ ] `darkMode: 'class'` set in `tailwind.config.ts`
302
+ - [ ] Flash-prevention script added to `<head>` in root layout
303
+ - [ ] `ThemeProvider` wraps the app in root layout
304
+ - [ ] `useTheme` hook exports `theme`, `resolvedTheme`, `setTheme`
305
+ - [ ] `localStorage` persistence with `mars-theme` key
306
+ - [ ] System preference listener for `prefers-color-scheme`
307
+ - [ ] Theme toggle component placed in navbar/header
308
+ - [ ] No raw color classes used — only semantic tokens
309
+ - [ ] `suppressHydrationWarning` on `<html>` element
@@ -0,0 +1,170 @@
1
+ # Skill: Configure Email Service
2
+
3
+ Set up and switch between email providers in a MARS application.
4
+
5
+ ## When to Use
6
+
7
+ Use this skill when the user asks to configure email, set up SendGrid, add Resend, switch email providers, or send transactional emails.
8
+
9
+ ## Architecture
10
+
11
+ MARS uses a pluggable email service re-exported via `@/lib/mars`. The active provider is set in `appConfig.services.email.provider`.
12
+
13
+ Available providers:
14
+ - `console` -- logs emails to terminal (default, no setup needed)
15
+ - `sendgrid` -- SendGrid API
16
+ - `resend` -- Resend API (to be added)
17
+
18
+ ## Switching Providers
19
+
20
+ ### Step 1: Update `app.config.ts`
21
+
22
+ ```typescript
23
+ services: {
24
+ email: { provider: 'sendgrid' },
25
+ // ...
26
+ }
27
+ ```
28
+
29
+ ### Step 2: Set Environment Variables
30
+
31
+ **SendGrid:**
32
+ ```bash
33
+ SENDGRID_API_KEY="SG.your-api-key"
34
+ SENDGRID_FROM_EMAIL="noreply@yourdomain.com"
35
+ ```
36
+
37
+ **Resend:**
38
+ ```bash
39
+ RESEND_API_KEY="re_your-api-key"
40
+ RESEND_FROM_EMAIL="noreply@yourdomain.com"
41
+ ```
42
+
43
+ ### Step 3: Update Env Validation
44
+
45
+ In `src/core/env/index.ts`, the schema already conditionally validates email vars:
46
+
47
+ ```typescript
48
+ if (appConfig.services.email.provider !== 'console') {
49
+ base.SENDGRID_API_KEY = z.string().min(1).optional();
50
+ base.SENDGRID_FROM_EMAIL = z.string().email().optional();
51
+ }
52
+ ```
53
+
54
+ ## Adding a New Provider (Resend)
55
+
56
+ ### Step 1: Install the SDK
57
+
58
+ ```bash
59
+ yarn add resend
60
+ ```
61
+
62
+ ### Step 2: Add the Provider
63
+
64
+ In the email service module (wired through `@/lib/mars`):
65
+
66
+ ```typescript
67
+ async function sendWithResend(params: SendEmailParams): Promise<void> {
68
+ const { Resend } = await import('resend');
69
+
70
+ const apiKey = process.env.RESEND_API_KEY;
71
+ const fromEmail = process.env.RESEND_FROM_EMAIL;
72
+
73
+ if (!apiKey) throw new Error('RESEND_API_KEY is not set');
74
+ if (!fromEmail) throw new Error('RESEND_FROM_EMAIL is not set');
75
+
76
+ const resend = new Resend(apiKey);
77
+
78
+ await resend.emails.send({
79
+ from: `${appConfig.name} <${fromEmail}>`,
80
+ to: params.to,
81
+ subject: params.subject,
82
+ html: params.html,
83
+ text: params.text,
84
+ });
85
+ }
86
+
87
+ export async function sendEmail(params: SendEmailParams): Promise<void> {
88
+ const provider = appConfig.services.email.provider;
89
+
90
+ switch (provider) {
91
+ case 'sendgrid':
92
+ return sendWithSendGrid(params);
93
+ case 'resend':
94
+ return sendWithResend(params);
95
+ case 'console':
96
+ default:
97
+ return sendWithConsole(params);
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### Step 3: Update the Config Type
103
+
104
+ In `app.config.ts`, add the new provider to the union:
105
+
106
+ ```typescript
107
+ email: { provider: 'console' as 'sendgrid' | 'resend' | 'console' },
108
+ ```
109
+
110
+ ## Sending Emails
111
+
112
+ All email sending goes through `sendEmail()`:
113
+
114
+ ```typescript
115
+ import { sendEmail } from '@/lib/mars';
116
+ import { appConfig } from '@/config/app.config';
117
+
118
+ await sendEmail({
119
+ to: user.email,
120
+ subject: `Welcome to ${appConfig.name}`,
121
+ text: `Hello ${user.name}, welcome!`,
122
+ html: `<h1>Hello ${user.name}</h1><p>Welcome to ${appConfig.name}!</p>`,
123
+ });
124
+ ```
125
+
126
+ ## Email Templates
127
+
128
+ For complex emails, create template functions:
129
+
130
+ ```typescript
131
+ // src/features/auth/server/email-templates.ts
132
+ import { appConfig } from '@/config/app.config';
133
+
134
+ export function welcomeEmail(name: string, verifyUrl: string) {
135
+ return {
136
+ subject: `Welcome to ${appConfig.name} - Verify Your Email`,
137
+ text: `Hi ${name},\n\nVerify your email: ${verifyUrl}`,
138
+ html: `
139
+ <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
140
+ <h2>Welcome to ${appConfig.name}!</h2>
141
+ <p>Hi ${name},</p>
142
+ <p>Click the button below to verify your email:</p>
143
+ <div style="text-align: center; margin: 30px 0;">
144
+ <a href="${verifyUrl}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
145
+ Verify Email
146
+ </a>
147
+ </div>
148
+ <p style="color: #6b7280; font-size: 14px;">This link expires in 24 hours.</p>
149
+ </div>
150
+ `,
151
+ };
152
+ }
153
+ ```
154
+
155
+ ## Development Workflow
156
+
157
+ 1. Start with `console` provider -- emails are logged to terminal.
158
+ 2. Switch to `sendgrid` or `resend` when ready for real delivery.
159
+ 3. Use the console output to verify email content during development.
160
+
161
+ ## Checklist
162
+
163
+ - [ ] Provider set in `appConfig.services.email.provider`
164
+ - [ ] API key and from email in `.env`
165
+ - [ ] Env validation updated for the provider's variables
166
+ - [ ] Provider function added to the email service module
167
+ - [ ] `sendEmail` switch statement updated
168
+ - [ ] Email templates use `appConfig.name` for branding
169
+ - [ ] Tested with `console` provider first
170
+ - [ ] No secrets hardcoded in templates