@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,438 @@
1
+ # Skill: Add Command Palette
2
+
3
+ Set up a Cmd+K command palette with fuzzy search, action registry, and keyboard navigation in a MARS application.
4
+
5
+ ## When to Use
6
+
7
+ Use this skill when the user asks to add a command palette, Cmd+K menu, keyboard shortcuts, quick search, or a spotlight-like interface.
8
+
9
+ ## Prerequisites
10
+
11
+ - MARS template app with React
12
+ - A modal/dialog primitive available (or use the native `<dialog>` element)
13
+
14
+ ## Architecture
15
+
16
+ The command palette consists of:
17
+ 1. **Keyboard shortcut listener** — `Cmd+K` (Mac) / `Ctrl+K` (Windows/Linux) opens the palette
18
+ 2. **Action registry** — centralized list of commands (navigation, theme toggle, user actions)
19
+ 3. **Fuzzy search** — filters actions as the user types
20
+ 4. **Command palette UI** — search input, results list, keyboard navigation
21
+ 5. **`cmdk` library** — recommended for accessible, production-quality implementation
22
+
23
+ ## Step 1: Install `cmdk`
24
+
25
+ ```bash
26
+ yarn add cmdk
27
+ ```
28
+
29
+ The `cmdk` library provides an unstyled, composable command palette component with built-in keyboard navigation, fuzzy search, and accessibility.
30
+
31
+ ## Step 2: Define the Action Registry
32
+
33
+ ```typescript
34
+ // src/features/command-palette/actions.ts
35
+ import type { ReactNode } from 'react';
36
+
37
+ export interface CommandAction {
38
+ id: string;
39
+ label: string;
40
+ description?: string;
41
+ icon?: ReactNode;
42
+ keywords?: string[];
43
+ shortcut?: string[];
44
+ section: string;
45
+ onSelect: () => void;
46
+ }
47
+
48
+ export function createNavigationActions(router: { push: (path: string) => void }): CommandAction[] {
49
+ return [
50
+ {
51
+ id: 'nav-dashboard',
52
+ label: 'Go to Dashboard',
53
+ keywords: ['home', 'overview'],
54
+ section: 'Navigation',
55
+ shortcut: ['G', 'D'],
56
+ onSelect: () => router.push('/dashboard'),
57
+ },
58
+ {
59
+ id: 'nav-settings',
60
+ label: 'Go to Settings',
61
+ keywords: ['preferences', 'account'],
62
+ section: 'Navigation',
63
+ shortcut: ['G', 'S'],
64
+ onSelect: () => router.push('/settings'),
65
+ },
66
+ {
67
+ id: 'nav-profile',
68
+ label: 'Go to Profile',
69
+ keywords: ['account', 'user'],
70
+ section: 'Navigation',
71
+ onSelect: () => router.push('/settings/profile'),
72
+ },
73
+ {
74
+ id: 'nav-blog',
75
+ label: 'Go to Blog',
76
+ keywords: ['posts', 'articles'],
77
+ section: 'Navigation',
78
+ onSelect: () => router.push('/blog'),
79
+ },
80
+ ];
81
+ }
82
+
83
+ export function createThemeActions(setTheme: (theme: string) => void): CommandAction[] {
84
+ return [
85
+ {
86
+ id: 'theme-light',
87
+ label: 'Switch to Light Mode',
88
+ keywords: ['theme', 'appearance'],
89
+ section: 'Theme',
90
+ onSelect: () => setTheme('light'),
91
+ },
92
+ {
93
+ id: 'theme-dark',
94
+ label: 'Switch to Dark Mode',
95
+ keywords: ['theme', 'appearance'],
96
+ section: 'Theme',
97
+ onSelect: () => setTheme('dark'),
98
+ },
99
+ {
100
+ id: 'theme-system',
101
+ label: 'Use System Theme',
102
+ keywords: ['theme', 'appearance', 'auto'],
103
+ section: 'Theme',
104
+ onSelect: () => setTheme('system'),
105
+ },
106
+ ];
107
+ }
108
+
109
+ export function createUserActions(callbacks: {
110
+ onLogout: () => void;
111
+ onCopyUserId?: () => void;
112
+ }): CommandAction[] {
113
+ return [
114
+ {
115
+ id: 'user-logout',
116
+ label: 'Sign Out',
117
+ keywords: ['logout', 'sign out', 'exit'],
118
+ section: 'Account',
119
+ onSelect: callbacks.onLogout,
120
+ },
121
+ ...(callbacks.onCopyUserId
122
+ ? [
123
+ {
124
+ id: 'user-copy-id',
125
+ label: 'Copy User ID',
126
+ keywords: ['id', 'identifier'],
127
+ section: 'Account',
128
+ onSelect: callbacks.onCopyUserId,
129
+ },
130
+ ]
131
+ : []),
132
+ ];
133
+ }
134
+ ```
135
+
136
+ ## Step 3: Command Palette Component
137
+
138
+ ```typescript
139
+ // src/features/command-palette/components/CommandPalette.tsx
140
+ 'use client';
141
+
142
+ import { Command } from 'cmdk';
143
+ import { useEffect, useState, useCallback } from 'react';
144
+ import { useRouter } from 'next/navigation';
145
+ import { createNavigationActions, createThemeActions, createUserActions, type CommandAction } from '../actions';
146
+
147
+ export function CommandPalette() {
148
+ const [open, setOpen] = useState(false);
149
+ const [search, setSearch] = useState('');
150
+ const router = useRouter();
151
+
152
+ useEffect(() => {
153
+ function handleKeyDown(e: KeyboardEvent) {
154
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
155
+ e.preventDefault();
156
+ setOpen((prev) => !prev);
157
+ }
158
+
159
+ if (e.key === 'Escape') {
160
+ setOpen(false);
161
+ }
162
+ }
163
+
164
+ document.addEventListener('keydown', handleKeyDown);
165
+ return () => document.removeEventListener('keydown', handleKeyDown);
166
+ }, []);
167
+
168
+ const handleSelect = useCallback(
169
+ (action: CommandAction) => {
170
+ setOpen(false);
171
+ setSearch('');
172
+ action.onSelect();
173
+ },
174
+ [],
175
+ );
176
+
177
+ const navigationActions = createNavigationActions(router);
178
+ const themeActions = createThemeActions((theme) => {
179
+ localStorage.setItem('mars-theme', theme);
180
+ const isDark = theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
181
+ document.documentElement.classList.toggle('dark', isDark);
182
+ });
183
+ const userActions = createUserActions({
184
+ onLogout: () => router.push('/api/auth/logout'),
185
+ });
186
+
187
+ const allActions = [...navigationActions, ...themeActions, ...userActions];
188
+ const sections = [...new Set(allActions.map((a) => a.section))];
189
+
190
+ if (!open) return null;
191
+
192
+ return (
193
+ <div className="fixed inset-0 z-50">
194
+ {/* Backdrop */}
195
+ <div
196
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
197
+ onClick={() => setOpen(false)}
198
+ />
199
+
200
+ {/* Palette */}
201
+ <div className="absolute left-1/2 top-[20%] w-full max-w-lg -translate-x-1/2">
202
+ <Command
203
+ className="overflow-hidden rounded-xl border border-border-primary bg-surface-primary shadow-2xl"
204
+ shouldFilter={true}
205
+ >
206
+ <div className="flex items-center border-b border-border-primary px-4">
207
+ <SearchIcon className="mr-2 h-4 w-4 shrink-0 text-content-tertiary" />
208
+ <Command.Input
209
+ value={search}
210
+ onValueChange={setSearch}
211
+ placeholder="Type a command or search..."
212
+ className="flex h-12 w-full bg-transparent text-content-primary placeholder:text-content-tertiary focus:outline-none"
213
+ />
214
+ <kbd className="ml-2 shrink-0 rounded border border-border-primary bg-surface-secondary px-1.5 py-0.5 text-xs text-content-tertiary">
215
+ ESC
216
+ </kbd>
217
+ </div>
218
+
219
+ <Command.List className="max-h-80 overflow-y-auto p-2">
220
+ <Command.Empty className="px-4 py-8 text-center text-sm text-content-tertiary">
221
+ No results found.
222
+ </Command.Empty>
223
+
224
+ {sections.map((section) => (
225
+ <Command.Group
226
+ key={section}
227
+ heading={section}
228
+ className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-content-tertiary"
229
+ >
230
+ {allActions
231
+ .filter((a) => a.section === section)
232
+ .map((action) => (
233
+ <Command.Item
234
+ key={action.id}
235
+ value={`${action.label} ${action.keywords?.join(' ') || ''}`}
236
+ onSelect={() => handleSelect(action)}
237
+ className="flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2.5 text-sm text-content-primary aria-selected:bg-surface-secondary"
238
+ >
239
+ {action.icon && (
240
+ <span className="flex h-5 w-5 items-center justify-center text-content-tertiary">
241
+ {action.icon}
242
+ </span>
243
+ )}
244
+ <div className="flex-1">
245
+ <span>{action.label}</span>
246
+ {action.description && (
247
+ <span className="ml-2 text-content-tertiary">{action.description}</span>
248
+ )}
249
+ </div>
250
+ {action.shortcut && (
251
+ <div className="flex items-center gap-1">
252
+ {action.shortcut.map((key) => (
253
+ <kbd
254
+ key={key}
255
+ className="rounded border border-border-primary bg-surface-secondary px-1.5 py-0.5 text-xs text-content-tertiary"
256
+ >
257
+ {key}
258
+ </kbd>
259
+ ))}
260
+ </div>
261
+ )}
262
+ </Command.Item>
263
+ ))}
264
+ </Command.Group>
265
+ ))}
266
+ </Command.List>
267
+
268
+ <div className="flex items-center justify-between border-t border-border-primary px-4 py-2 text-xs text-content-tertiary">
269
+ <span>Navigate with ↑↓ keys</span>
270
+ <span>Select with ↵</span>
271
+ </div>
272
+ </Command>
273
+ </div>
274
+ </div>
275
+ );
276
+ }
277
+
278
+ function SearchIcon({ className }: { className?: string }) {
279
+ return (
280
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className={className}>
281
+ <path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
282
+ </svg>
283
+ );
284
+ }
285
+ ```
286
+
287
+ ## Step 4: Recent Items Support
288
+
289
+ ```typescript
290
+ // src/features/command-palette/recent.ts
291
+ const RECENT_KEY = 'mars-command-palette-recent';
292
+ const MAX_RECENT = 5;
293
+
294
+ export function getRecentActions(): string[] {
295
+ if (typeof window === 'undefined') return [];
296
+ const stored = localStorage.getItem(RECENT_KEY);
297
+ return stored ? JSON.parse(stored) : [];
298
+ }
299
+
300
+ export function addRecentAction(actionId: string): void {
301
+ const recent = getRecentActions().filter((id) => id !== actionId);
302
+ recent.unshift(actionId);
303
+ localStorage.setItem(RECENT_KEY, JSON.stringify(recent.slice(0, MAX_RECENT)));
304
+ }
305
+ ```
306
+
307
+ Integrate into the `handleSelect` callback:
308
+
309
+ ```typescript
310
+ const handleSelect = useCallback(
311
+ (action: CommandAction) => {
312
+ addRecentAction(action.id);
313
+ setOpen(false);
314
+ setSearch('');
315
+ action.onSelect();
316
+ },
317
+ [],
318
+ );
319
+ ```
320
+
321
+ Then render a "Recent" section at the top of the command list when the search is empty:
322
+
323
+ ```typescript
324
+ {!search && recentIds.length > 0 && (
325
+ <Command.Group heading="Recent">
326
+ {recentIds
327
+ .map((id) => allActions.find((a) => a.id === id))
328
+ .filter(Boolean)
329
+ .map((action) => (
330
+ <Command.Item key={action!.id} /* ... */ />
331
+ ))}
332
+ </Command.Group>
333
+ )}
334
+ ```
335
+
336
+ ## Step 5: Register Custom Actions
337
+
338
+ Allow features to register their own commands:
339
+
340
+ ```typescript
341
+ // src/features/command-palette/registry.ts
342
+ import type { CommandAction } from './actions';
343
+
344
+ const customActions: CommandAction[] = [];
345
+
346
+ export function registerAction(action: CommandAction): void {
347
+ const exists = customActions.findIndex((a) => a.id === action.id);
348
+ if (exists >= 0) {
349
+ customActions[exists] = action;
350
+ } else {
351
+ customActions.push(action);
352
+ }
353
+ }
354
+
355
+ export function getCustomActions(): CommandAction[] {
356
+ return [...customActions];
357
+ }
358
+ ```
359
+
360
+ Use from any feature:
361
+
362
+ ```typescript
363
+ import { registerAction } from '@/features/command-palette/registry';
364
+
365
+ registerAction({
366
+ id: 'billing-upgrade',
367
+ label: 'Upgrade Plan',
368
+ keywords: ['billing', 'subscription', 'pro'],
369
+ section: 'Billing',
370
+ onSelect: () => router.push('/billing/upgrade'),
371
+ });
372
+ ```
373
+
374
+ ## Step 6: Add to Root Layout
375
+
376
+ ```typescript
377
+ // src/app/(protected)/layout.tsx
378
+ import { CommandPalette } from '@/features/command-palette/components/CommandPalette';
379
+
380
+ export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
381
+ return (
382
+ <>
383
+ <CommandPalette />
384
+ {children}
385
+ </>
386
+ );
387
+ }
388
+ ```
389
+
390
+ ## Step 7: Trigger Button (Optional)
391
+
392
+ For discoverability, add a search bar that opens the palette:
393
+
394
+ ```typescript
395
+ // src/features/command-palette/components/CommandTrigger.tsx
396
+ 'use client';
397
+
398
+ export function CommandTrigger() {
399
+ return (
400
+ <button
401
+ onClick={() => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true }))}
402
+ className="flex items-center gap-2 rounded-lg border border-border-primary bg-surface-secondary px-3 py-1.5 text-sm text-content-tertiary transition-colors hover:border-interactive-primary hover:text-content-secondary"
403
+ >
404
+ <SearchIcon className="h-4 w-4" />
405
+ <span>Search...</span>
406
+ <kbd className="rounded border border-border-primary bg-surface-primary px-1.5 py-0.5 text-xs">
407
+ ⌘K
408
+ </kbd>
409
+ </button>
410
+ );
411
+ }
412
+ ```
413
+
414
+ ## Testing
415
+
416
+ 1. Press `Cmd+K` — verify the palette opens with focus in the search input.
417
+ 2. Type a query — verify results filter with fuzzy matching.
418
+ 3. Use arrow keys — verify keyboard navigation highlights items.
419
+ 4. Press Enter — verify the selected action executes and the palette closes.
420
+ 5. Press Escape — verify the palette closes.
421
+ 6. Click the backdrop — verify the palette closes.
422
+ 7. Select an action — verify it appears in "Recent" on the next open.
423
+ 8. Register a custom action — verify it appears in the palette.
424
+
425
+ ## Checklist
426
+
427
+ - [ ] `cmdk` library installed
428
+ - [ ] Keyboard shortcut (`Cmd+K` / `Ctrl+K`) opens and closes the palette
429
+ - [ ] Action registry with navigation, theme, and user actions
430
+ - [ ] Fuzzy search filters actions as user types
431
+ - [ ] Keyboard navigation with arrow keys and Enter
432
+ - [ ] Recent items tracked in `localStorage`
433
+ - [ ] Action sections with headings
434
+ - [ ] Keyboard shortcuts displayed on action items
435
+ - [ ] Custom action registration via `registerAction`
436
+ - [ ] Palette placed in protected layout
437
+ - [ ] Uses semantic design tokens (no raw colors)
438
+ - [ ] Accessible — focus management, aria labels, screen reader support via `cmdk`
@@ -0,0 +1,158 @@
1
+ # Skill: Add a UI Component
2
+
3
+ Create a new React component following the MARS design system conventions.
4
+
5
+ ## When to Use
6
+
7
+ Use this skill when the user asks to create a new UI component, button variant, form element, or reusable UI pattern.
8
+
9
+ ## Decision: Primitive, Pattern, or Feature Component?
10
+
11
+ | Type | Location | Business Logic? | Example |
12
+ |------|----------|----------------|---------|
13
+ | Primitive | `@mars-stack/ui` (primitives) | No | Button, Input, Badge, Toggle |
14
+ | Pattern | `@mars-stack/ui` (patterns) | No | Card, Modal, DataTable, Pagination |
15
+ | Feature | `src/features/<name>/components/` | Yes | BillingCard, UserProfileEditor |
16
+
17
+ ## Design Token System
18
+
19
+ MARS uses a three-layer token system defined in `src/styles/`. **Never use raw Tailwind colours** like `text-gray-900` or `bg-blue-500`. Always use semantic tokens.
20
+
21
+ ### Token Quick Reference
22
+
23
+ **Surfaces** (backgrounds):
24
+ - `bg-surface-background` -- page background
25
+ - `bg-surface-card` -- card/panel backgrounds
26
+ - `bg-surface-elevated` -- elevated elements (dropdowns, popovers)
27
+ - `bg-surface-input` -- form input backgrounds
28
+ - `bg-surface-overlay` -- modal/dialog backdrops
29
+
30
+ **Text**:
31
+ - `text-text-primary` -- main body text
32
+ - `text-text-secondary` -- supporting text
33
+ - `text-text-muted` -- placeholder, disabled text
34
+ - `text-text-link` / `hover:text-text-link-hover` -- links
35
+ - `text-text-on-brand` -- text on brand-coloured backgrounds
36
+ - `text-text-error` / `text-text-success` / `text-text-warning` / `text-text-info`
37
+
38
+ **Borders**:
39
+ - `border-border-default` -- default borders
40
+ - `border-border-input` -- form input borders
41
+ - `border-border-focus` -- focus state borders
42
+ - `border-border-error` -- error state borders
43
+ - `border-border-divider` -- horizontal/vertical dividers
44
+
45
+ **Brand / Interactive**:
46
+ - `bg-brand-primary` / `hover:bg-brand-primary-hover` -- primary actions
47
+ - `bg-ghost-hover` / `bg-ghost-active` -- ghost/subtle button states
48
+ - `bg-danger-bg` / `hover:bg-danger-hover` -- destructive actions
49
+ - `focus:ring-ring-focus` -- focus rings
50
+
51
+ **Feedback**:
52
+ - `bg-success-muted` / `bg-error-muted` / `bg-warning-muted` / `bg-info-muted` -- subtle feedback backgrounds
53
+
54
+ ## Primitive Component Template
55
+
56
+ ```typescript
57
+ import { clsx } from 'clsx';
58
+ import { type ComponentPropsWithoutRef, forwardRef } from 'react';
59
+
60
+ export interface ToggleProps extends Omit<ComponentPropsWithoutRef<'button'>, 'onChange'> {
61
+ checked: boolean;
62
+ onChange: (checked: boolean) => void;
63
+ label?: string;
64
+ size?: 'sm' | 'md';
65
+ }
66
+
67
+ export const Toggle = forwardRef<HTMLButtonElement, ToggleProps>(
68
+ ({ checked, onChange, label, size = 'md', className, disabled, ...rest }, ref) => {
69
+ const trackSize = size === 'sm' ? 'h-5 w-9' : 'h-6 w-11';
70
+ const thumbSize = size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4';
71
+ const thumbTranslate = size === 'sm' ? 'translate-x-4' : 'translate-x-5';
72
+
73
+ return (
74
+ <label className={clsx('inline-flex items-center gap-2', className)}>
75
+ <button
76
+ ref={ref}
77
+ role="switch"
78
+ type="button"
79
+ aria-checked={checked}
80
+ disabled={disabled}
81
+ onClick={() => onChange(!checked)}
82
+ className={clsx(
83
+ 'relative inline-flex shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200',
84
+ trackSize,
85
+ checked ? 'bg-brand-primary' : 'bg-surface-active',
86
+ disabled && 'opacity-(--disabled-opacity) cursor-not-allowed',
87
+ 'focus:outline-none focus:ring-2 focus:ring-ring-focus focus:ring-offset-2',
88
+ )}
89
+ {...rest}
90
+ >
91
+ <span
92
+ className={clsx(
93
+ 'pointer-events-none inline-block transform rounded-full bg-white shadow-sm transition-transform duration-200',
94
+ thumbSize,
95
+ checked ? thumbTranslate : 'translate-x-0.5',
96
+ )}
97
+ />
98
+ </button>
99
+ {label && (
100
+ <span className={clsx('text-text-primary', size === 'sm' ? 'text-sm' : 'text-base')}>
101
+ {label}
102
+ </span>
103
+ )}
104
+ </label>
105
+ );
106
+ },
107
+ );
108
+
109
+ Toggle.displayName = 'Toggle';
110
+ ```
111
+
112
+ ### Key Conventions
113
+
114
+ 1. Use `forwardRef` for primitives that wrap native elements.
115
+ 2. Accept `className` prop for overrides, merge with `clsx`.
116
+ 3. Export named (never default).
117
+ 4. Support common variants via props (`size`, `variant`).
118
+ 5. Use semantic tokens for all colours.
119
+ 6. Include proper `aria-*` attributes for accessibility.
120
+
121
+ ### Input Adornments (leading/trailing overlay buttons)
122
+
123
+ When adding overlay buttons inside an input (e.g. password visibility toggle):
124
+
125
+ - Wrap the input in a `relative` container; position the button with `absolute top-0 right-0 bottom-0`.
126
+ - Give the button explicit dimensions: `h-full w-10` (40px matches input `pr-10`).
127
+ - Center the icon: `flex items-center justify-center p-0` (reset button padding to avoid misalignment).
128
+ - Add `shrink-0` to the button and icon so flex does not distort them.
129
+ - Reserve input padding: add `pr-10` to the input when the adornment is present.
130
+
131
+ ```tsx
132
+ <div className="relative">
133
+ <input className={clsx(showPasswordToggle && 'pr-10')} ... />
134
+ {showPasswordToggle && (
135
+ <button
136
+ type="button"
137
+ className="absolute top-0 right-0 bottom-0 flex h-full w-10 shrink-0 items-center justify-center p-0 text-text-muted hover:text-text-secondary"
138
+ >
139
+ <svg className="h-4 w-4 shrink-0" ... />
140
+ </button>
141
+ )}
142
+ </div>
143
+ ```
144
+
145
+ ## After Creating
146
+
147
+ 1. Add the export to the `@mars-stack/ui` package barrel file.
148
+ 2. Export both the component and its props type.
149
+
150
+ ## Checklist
151
+
152
+ - [ ] Correct directory (primitive / pattern / feature)
153
+ - [ ] All colours use semantic tokens (no raw Tailwind colours)
154
+ - [ ] `className` prop accepted and merged with `clsx`
155
+ - [ ] Proper TypeScript interface exported
156
+ - [ ] Accessibility attributes included
157
+ - [ ] Dark mode works automatically (via token system)
158
+ - [ ] Added to barrel export