create-appystack 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 (99) hide show
  1. package/README.md +54 -0
  2. package/bin/index.js +243 -0
  3. package/package.json +39 -0
  4. package/template/.claude/skills/recipe/SKILL.md +71 -0
  5. package/template/.claude/skills/recipe/domains/care-provider-operations.md +185 -0
  6. package/template/.claude/skills/recipe/domains/youtube-launch-optimizer.md +154 -0
  7. package/template/.claude/skills/recipe/references/file-crud.md +295 -0
  8. package/template/.claude/skills/recipe/references/nav-shell.md +233 -0
  9. package/template/.dockerignore +39 -0
  10. package/template/.env.example +13 -0
  11. package/template/.github/workflows/ci.yml +43 -0
  12. package/template/.husky/pre-commit +1 -0
  13. package/template/.prettierignore +7 -0
  14. package/template/.prettierrc +8 -0
  15. package/template/.vscode/launch.json +59 -0
  16. package/template/CLAUDE.md +114 -0
  17. package/template/Dockerfile +56 -0
  18. package/template/README.md +219 -0
  19. package/template/client/index.html +13 -0
  20. package/template/client/package.json +43 -0
  21. package/template/client/src/App.test.tsx +67 -0
  22. package/template/client/src/App.tsx +11 -0
  23. package/template/client/src/components/ErrorFallback.test.tsx +64 -0
  24. package/template/client/src/components/ErrorFallback.tsx +18 -0
  25. package/template/client/src/config/env.test.ts +64 -0
  26. package/template/client/src/config/env.ts +34 -0
  27. package/template/client/src/contexts/AppContext.test.tsx +81 -0
  28. package/template/client/src/contexts/AppContext.tsx +52 -0
  29. package/template/client/src/demo/ContactForm.test.tsx +97 -0
  30. package/template/client/src/demo/ContactForm.tsx +100 -0
  31. package/template/client/src/demo/DemoPage.tsx +56 -0
  32. package/template/client/src/demo/SocketDemo.test.tsx +160 -0
  33. package/template/client/src/demo/SocketDemo.tsx +65 -0
  34. package/template/client/src/demo/StatusGrid.test.tsx +181 -0
  35. package/template/client/src/demo/StatusGrid.tsx +77 -0
  36. package/template/client/src/demo/TechStackDisplay.test.tsx +63 -0
  37. package/template/client/src/demo/TechStackDisplay.tsx +75 -0
  38. package/template/client/src/hooks/useServerStatus.test.ts +133 -0
  39. package/template/client/src/hooks/useServerStatus.ts +67 -0
  40. package/template/client/src/hooks/useSocket.test.ts +152 -0
  41. package/template/client/src/hooks/useSocket.ts +43 -0
  42. package/template/client/src/lib/utils.test.ts +33 -0
  43. package/template/client/src/lib/utils.ts +14 -0
  44. package/template/client/src/main.test.tsx +113 -0
  45. package/template/client/src/main.tsx +14 -0
  46. package/template/client/src/pages/LandingPage.test.tsx +30 -0
  47. package/template/client/src/pages/LandingPage.tsx +29 -0
  48. package/template/client/src/styles/index.css +50 -0
  49. package/template/client/src/test/msw/browser.ts +4 -0
  50. package/template/client/src/test/msw/handlers.ts +12 -0
  51. package/template/client/src/test/msw/msw-example.test.ts +69 -0
  52. package/template/client/src/test/msw/server.ts +14 -0
  53. package/template/client/src/test/setup.ts +10 -0
  54. package/template/client/src/utils/api.test.ts +79 -0
  55. package/template/client/src/utils/api.ts +42 -0
  56. package/template/client/src/vite-env.d.ts +13 -0
  57. package/template/client/tsconfig.json +17 -0
  58. package/template/client/vite.config.ts +38 -0
  59. package/template/client/vitest.config.ts +36 -0
  60. package/template/docker-compose.yml +19 -0
  61. package/template/e2e/smoke.test.ts +95 -0
  62. package/template/e2e/socket.test.ts +96 -0
  63. package/template/eslint.config.js +2 -0
  64. package/template/package.json +50 -0
  65. package/template/playwright.config.ts +14 -0
  66. package/template/scripts/customize.ts +175 -0
  67. package/template/server/nodemon.json +5 -0
  68. package/template/server/package.json +45 -0
  69. package/template/server/src/app.test.ts +103 -0
  70. package/template/server/src/config/env.test.ts +97 -0
  71. package/template/server/src/config/env.ts +29 -0
  72. package/template/server/src/config/logger.test.ts +58 -0
  73. package/template/server/src/config/logger.ts +17 -0
  74. package/template/server/src/helpers/response.test.ts +53 -0
  75. package/template/server/src/helpers/response.ts +17 -0
  76. package/template/server/src/index.ts +118 -0
  77. package/template/server/src/middleware/errorHandler.test.ts +84 -0
  78. package/template/server/src/middleware/errorHandler.ts +27 -0
  79. package/template/server/src/middleware/rateLimiter.test.ts +68 -0
  80. package/template/server/src/middleware/rateLimiter.ts +8 -0
  81. package/template/server/src/middleware/requestLogger.test.ts +111 -0
  82. package/template/server/src/middleware/requestLogger.ts +17 -0
  83. package/template/server/src/middleware/validate.test.ts +213 -0
  84. package/template/server/src/middleware/validate.ts +23 -0
  85. package/template/server/src/routes/health.test.ts +17 -0
  86. package/template/server/src/routes/health.ts +12 -0
  87. package/template/server/src/routes/info.test.ts +20 -0
  88. package/template/server/src/routes/info.ts +19 -0
  89. package/template/server/src/shared.test.ts +53 -0
  90. package/template/server/src/shutdown.test.ts +98 -0
  91. package/template/server/src/socket.test.ts +185 -0
  92. package/template/server/src/static.test.ts +166 -0
  93. package/template/server/tsconfig.json +16 -0
  94. package/template/server/vitest.config.ts +22 -0
  95. package/template/shared/package.json +19 -0
  96. package/template/shared/src/constants.ts +11 -0
  97. package/template/shared/src/index.ts +8 -0
  98. package/template/shared/src/types.ts +33 -0
  99. package/template/shared/tsconfig.json +10 -0
@@ -0,0 +1,154 @@
1
+ # Sample: YouTube Launch Optimizer
2
+
3
+ A content production tool for YouTube creators. Manages the full lifecycle of a video from idea through launch — scripts, thumbnails, SEO, and launch checklist tasks.
4
+
5
+ **Use with**: `file-crud` recipe. Optionally combine with `nav-shell` recipe.
6
+
7
+ ---
8
+
9
+ ## Entities
10
+
11
+ ### Channel
12
+ A YouTube channel being managed. Most users will have one; power users may manage several.
13
+
14
+ | Field | Type | Notes |
15
+ |-------|------|-------|
16
+ | `name` | string | **namish field** — channel display name |
17
+ | `handle` | string | e.g. `@AppyDave` |
18
+ | `niche` | string | e.g. 'AI Tools', 'Coding Tutorials' |
19
+ | `targetAudience` | string | brief description |
20
+ | `contentPillars` | string[] | e.g. ['Claude Code', 'BMAD Method', 'AppyStack'] |
21
+
22
+ Namish field: `name`
23
+ Example filename: `appydave-a1b2c.json`
24
+
25
+ ---
26
+
27
+ ### Video
28
+ A video being produced. Central entity — most other entities link to a video.
29
+
30
+ | Field | Type | Notes |
31
+ |-------|------|-------|
32
+ | `title` | string | **namish field** — working title |
33
+ | `channelId` | string | FK → Channel |
34
+ | `status` | string | 'idea' / 'scripting' / 'recorded' / 'editing' / 'ready' / 'published' |
35
+ | `publishDate` | string | ISO date, optional |
36
+ | `youtubeId` | string | once published |
37
+ | `hooks` | string[] | attention-grabbing opening lines |
38
+ | `seoKeyword` | string | primary keyword |
39
+ | `description` | string | YouTube description |
40
+ | `tags` | string[] | |
41
+
42
+ Namish field: `title`
43
+ Example filename: `how-to-build-your-first-appystack-app-k3p7r.json`
44
+ Relationship: `channelId` → Channel
45
+
46
+ ---
47
+
48
+ ### Script
49
+ The script or detailed outline for a video.
50
+
51
+ | Field | Type | Notes |
52
+ |-------|------|-------|
53
+ | `title` | string | **namish field** — usually matches video title |
54
+ | `videoId` | string | FK → Video |
55
+ | `version` | number | allows multiple script iterations |
56
+ | `hook` | string | opening line |
57
+ | `sections` | object[] | array of `{ heading, content, duration }` |
58
+ | `cta` | string | call to action |
59
+ | `totalDuration` | number | estimated minutes |
60
+
61
+ Namish field: `title` + version composite → `how-to-build-appystack-v2-m9x4k.json`
62
+ Relationship: `videoId` → Video
63
+
64
+ ---
65
+
66
+ ### ThumbnailVariant
67
+ A thumbnail concept or A/B test variant for a video.
68
+
69
+ | Field | Type | Notes |
70
+ |-------|------|-------|
71
+ | `label` | string | **namish field** — e.g. 'Face + text', 'Curiosity gap', 'Before/After' |
72
+ | `videoId` | string | FK → Video |
73
+ | `concept` | string | description of the visual idea |
74
+ | `mainText` | string | large text on thumbnail |
75
+ | `subText` | string | smaller supporting text |
76
+ | `emotion` | string | the feeling to convey |
77
+ | `selected` | boolean | which variant was used |
78
+ | `ctrEstimate` | number | optional estimated CTR % |
79
+
80
+ Namish field: `label`
81
+ Example filename: `face-plus-text-variant-p2m8n.json`
82
+ Relationship: `videoId` → Video
83
+
84
+ ---
85
+
86
+ ### LaunchTask
87
+ A checklist item to complete before or after publishing a video.
88
+
89
+ | Field | Type | Notes |
90
+ |-------|------|-------|
91
+ | `title` | string | **namish field** — task description |
92
+ | `videoId` | string | FK → Video |
93
+ | `category` | string | 'pre-launch' / 'day-of' / 'post-launch' |
94
+ | `completed` | boolean | |
95
+ | `dueDate` | string | optional |
96
+ | `notes` | string | optional |
97
+
98
+ Namish field: `title`
99
+ Example filename: `upload-thumbnail-to-youtube-studio-r7k2m.json`
100
+ Relationship: `videoId` → Video
101
+
102
+ ---
103
+
104
+ ## Entity Classification
105
+
106
+ | Entity | Type | Notes |
107
+ |--------|------|-------|
108
+ | Channel | System / configuration | Set up once |
109
+ | Video | Domain / operational | Core working entity |
110
+ | Script | Domain / operational | Created per video, iterated |
111
+ | ThumbnailVariant | Domain / operational | Multiple per video for A/B |
112
+ | LaunchTask | Domain / operational | Checklist items per video |
113
+
114
+ ---
115
+
116
+ ## Suggested Nav Mapping (for nav-shell recipe)
117
+
118
+ | Nav Item | View Key | Entity | Tier |
119
+ |----------|----------|--------|------|
120
+ | Dashboard | `dashboard` | — (videos by status) | primary |
121
+ | Videos | `videos` | Video | primary |
122
+ | Scripts | `scripts` | Script | primary |
123
+ | Thumbnails | `thumbnails` | ThumbnailVariant | primary |
124
+ | Launch | `launch` | LaunchTask | primary |
125
+ | Channels | `channels` | Channel | secondary |
126
+
127
+ **Context-aware nav suggestion**: When viewing a specific Video, the sidebar could switch to show Script, Thumbnails, and LaunchTasks for that video (context-aware menu pattern from nav-shell recipe).
128
+
129
+ ---
130
+
131
+ ## Data Folder Structure
132
+
133
+ ```
134
+ data/
135
+ ├── channels/
136
+ │ └── appydave-a1b2c.json
137
+ ├── videos/
138
+ │ └── how-to-build-your-first-appystack-app-k3p7r.json
139
+ ├── scripts/
140
+ │ └── how-to-build-appystack-v2-m9x4k.json
141
+ ├── thumbnail-variants/
142
+ │ └── face-plus-text-variant-p2m8n.json
143
+ └── launch-tasks/
144
+ └── upload-thumbnail-to-youtube-studio-r7k2m.json
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Notes
150
+
151
+ - Videos are the central entity. Most other entities have a `videoId` FK — consider always showing which video context you're in.
152
+ - The context-aware menu pattern from `nav-shell` is especially useful here: when you open a Video's detail view, the sidebar can switch to show that video's Scripts, Thumbnails, and LaunchTasks directly.
153
+ - `LaunchTask` items could be seeded from a template for every new video (same checklist each time). This is a developer-implemented pattern; the recipe scaffolds the entity, not the seeding logic.
154
+ - `ThumbnailVariant` with `selected: true` indicates which variant was actually used. Only one should be selected per video.
@@ -0,0 +1,295 @@
1
+ # Recipe: File-Based JSON Persistence
2
+
3
+ Multiple entities stored as individual JSON files on the server filesystem. Socket.IO bridges client actions to the filesystem and broadcasts changes back to all connected clients in real time. No database required — the `data/` folder IS the database.
4
+
5
+ This recipe suits local tools, small-team apps, and rapid prototyping. It also enables a **local-first workflow**: users work with local files, then publish to a central database when ready (see Local-First Pattern below).
6
+
7
+ ---
8
+
9
+ ## Recipe Anatomy
10
+
11
+ **Intent**
12
+ Scaffold server-side JSON file persistence for one or more domain entities, with real-time Socket.io sync to connected clients. Each record is a human-readable JSON file. All clients see updates live.
13
+
14
+ **Type**: Seed for initial entities. Migration-friendly for adding new entities (additive, non-destructive). Run once with all entities upfront, or entity-by-entity.
15
+
16
+ **Stack Assumptions**
17
+ - Express 5, Socket.io, TypeScript, Node.js `fs/promises`
18
+ - chokidar (verify in `server/package.json`; add if missing)
19
+ - `data/` folder at repo root
20
+
21
+ **Idempotency Check**
22
+ Does `server/src/data/fileStore.ts` exist? If yes → infrastructure is already installed. Only generate entity-specific route and type files for new entities.
23
+
24
+ **Does Not Touch**
25
+ - `client/` — client-side views are the shell recipe's or developer's concern
26
+ - `server/src/index.ts` — instructs developer to mount the generated routes; does not rewrite it
27
+ - Authentication or authorization
28
+ - Entity relationships (foreign keys, join queries) — flagged as TODOs in generated type stubs
29
+ - Contents of `data/` — creates folder structure only
30
+
31
+ **Composes With**
32
+ - `nav-shell` recipe — each entity can have a nav item; views switch to entity CRUD screens
33
+ - `domain-preparation` step — before combining shell + persistence, collect entity definitions once
34
+
35
+ ---
36
+
37
+ ## Folder Structure
38
+
39
+ ```
40
+ project-root/
41
+ ├── data/ ← at repo root (not inside client or server)
42
+ │ └── {entity-plural}/ ← one folder per entity (e.g. companies/, sites/)
43
+ │ └── {name-slug}-{5char}.json ← individual records
44
+ ├── server/src/
45
+ │ ├── data/
46
+ │ │ ├── fileStore.ts ← read/write/delete individual record files
47
+ │ │ ├── idgen.ts ← 5-char alphanum ID generator
48
+ │ │ └── watcher.ts ← chokidar watcher, emits Socket.io events on change
49
+ │ ├── routes/
50
+ │ │ └── {entity}.ts ← REST endpoints (initial loads + server-to-server)
51
+ │ └── sockets/
52
+ │ └── {entity}Handlers.ts ← Socket.io event handlers per entity
53
+ └── shared/src/types/
54
+ ├── entity.ts ← base EntityRecord, EntityIndex types
55
+ └── {entity}.ts ← entity-specific field types (generated stubs)
56
+ ```
57
+
58
+ ---
59
+
60
+ ## File Naming Convention and Primary Key
61
+
62
+ **The 5-char alphanumeric ID is the record's primary key.** It is embedded in the filename, generated once at creation, and never changes — even when the record's name changes.
63
+
64
+ ```
65
+ File name pattern: {name-slug}-{5char-id}.json
66
+
67
+ Examples:
68
+ name = "NERO Banana" → nero-banana-a7k3p.json
69
+ name = "David Kruwys" → david-kruwys-a1f4q.json (ID: a1f4q)
70
+ name corrected to "David Cruwys" → david-cruwys-a1f4q.json (same ID, new slug)
71
+ name = "Acme Corp" → acme-corp-x9q2m.json
72
+ name = "St. Mary's Group Home" → st-marys-group-home-k3p7r.json
73
+ ```
74
+
75
+ **Why this matters**: If the name changes, the slug prefix in the filename changes but the 5-char suffix stays constant. The ID is how you find, update, and delete a record regardless of what the name is. This is the stable identity; the slug is the human label.
76
+
77
+ **Why this avoids Git merge conflicts**: Two users creating different records produce two different files. No shared file is written per-operation (unlike a stored `index.json`). Two users can create, rename, or delete different records simultaneously and push to the same Git branch with zero conflicts.
78
+
79
+ ### Slug Rules
80
+
81
+ ```
82
+ - Take the entity's "namish" field (usually `name`; sometimes a composite or alternative field)
83
+ - Lowercase
84
+ - Replace spaces and special characters with hyphens
85
+ - Collapse multiple hyphens to one
86
+ - Strip leading/trailing hyphens
87
+ - Maximum 40 characters (truncate without breaking mid-word where possible)
88
+ ```
89
+
90
+ ### ID Generation
91
+
92
+ ```typescript
93
+ // server/src/data/idgen.ts
94
+ export function generateId(): string {
95
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
96
+ return Array.from({ length: 5 }, () =>
97
+ chars[Math.floor(Math.random() * chars.length)]
98
+ ).join('')
99
+ }
100
+ // Collision probability at 1,000 records: ~0.015% — acceptable for local/small-team use
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Index Strategy: Inferred (Not Stored)
106
+
107
+ **There is no `index.json` file.** The list of records is built by reading the entity folder at request time and optionally reading each file for summary fields. This is inferred, not stored.
108
+
109
+ **Why**: A stored `index.json` is written on every create, rename, and delete — it becomes a conflict magnet when multiple users work simultaneously on Git. With inferred indexing, each user's changes touch only their own files.
110
+
111
+ **Performance**: Folder scan at 1,000 records = ~5-20ms (filenames only). Reading all files for summary fields = ~50-200ms. Acceptable for local/small-team tools. Cache mitigates repeated calls.
112
+
113
+ ### In-Memory Cache
114
+
115
+ The server maintains a per-entity index cache. chokidar invalidates it on any file change.
116
+
117
+ ```typescript
118
+ // server/src/data/fileStore.ts
119
+
120
+ const indexCache = new Map<string, EntityIndex[]>()
121
+
122
+ export async function listRecords(entity: string): Promise<EntityIndex[]> {
123
+ if (indexCache.has(entity)) return indexCache.get(entity)!
124
+
125
+ const folder = path.join('./data', entity)
126
+ const files = (await fs.readdir(folder)).filter(f => f.endsWith('.json'))
127
+
128
+ const index = await Promise.all(files.map(async (filename) => {
129
+ const id = extractId(filename) // last segment before .json
130
+ const slug = extractSlug(filename) // everything before the last -xxxxx
131
+ const content = JSON.parse(await fs.readFile(path.join(folder, filename), 'utf-8'))
132
+ return { id, name: content.name ?? slugToName(slug), filename }
133
+ }))
134
+
135
+ indexCache.set(entity, index)
136
+ return index
137
+ }
138
+
139
+ // Called by chokidar on any change in data/
140
+ export function invalidateCache(entity: string) {
141
+ indexCache.delete(entity)
142
+ }
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Shared Types
148
+
149
+ ```typescript
150
+ // shared/src/types/entity.ts
151
+
152
+ export interface EntityRecord {
153
+ id: string // extracted from filename (primary key), NOT stored in the file body
154
+ name: string // the "namish" field — used to derive the filename slug
155
+ [key: string]: unknown
156
+ }
157
+
158
+ export interface EntityIndex {
159
+ id: string
160
+ name: string
161
+ filename: string
162
+ }
163
+
164
+ export function extractId(filename: string): string {
165
+ return filename.replace(/\.json$/, '').split('-').pop() ?? ''
166
+ }
167
+ ```
168
+
169
+ Entity-specific types extend `EntityRecord`:
170
+
171
+ ```typescript
172
+ // shared/src/types/company.ts
173
+ export interface Company extends EntityRecord {
174
+ name: string
175
+ // TODO: add domain-specific fields
176
+ // TODO: add relationship FKs if needed (e.g. siteIds: string[])
177
+ }
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Socket.io Events
183
+
184
+ ```typescript
185
+ // Client → Server (commands)
186
+ socket.emit('entity:list', { entity: 'company' })
187
+ socket.emit('entity:get', { entity: 'company', id: 'a7k3p' })
188
+ socket.emit('entity:save', { entity: 'company', record: { name: 'NERO Banana', ...fields } })
189
+ // record.id present → update; record.id absent → create
190
+ socket.emit('entity:delete', { entity: 'company', id: 'a7k3p' })
191
+
192
+ // Server → requesting client
193
+ socket.emit('entity:list:result', { entity: 'company', records: EntityIndex[] })
194
+ socket.emit('entity:get:result', { entity: 'company', record: EntityRecord })
195
+
196
+ // Server → ALL clients (broadcast on state change)
197
+ io.emit('entity:created', { entity: 'company', record: EntityRecord, index: EntityIndex })
198
+ io.emit('entity:updated', { entity: 'company', record: EntityRecord, index: EntityIndex })
199
+ io.emit('entity:deleted', { entity: 'company', id: string })
200
+
201
+ // Server → ALL clients (chokidar: external file change detected)
202
+ io.emit('entity:external-change', { entity: 'company', changeType: 'add'|'change'|'unlink', id: string })
203
+ ```
204
+
205
+ **`entity:created` vs `entity:updated` are separate events.** The client UI often treats them differently — scroll to new record, highlight on update, different toast messages.
206
+
207
+ **REST endpoints** (`server/src/routes/{entity}.ts`) are generated alongside Socket.io handlers. REST is useful for initial page loads, debugging, health checks, and server-to-server calls. Socket.io is the primary real-time path.
208
+
209
+ ---
210
+
211
+ ## CRUD Operations Summary
212
+
213
+ | Operation | Client emits | Server does | Server broadcasts |
214
+ |-----------|-------------|-------------|-------------------|
215
+ | List | `entity:list` | reads folder, builds index (cached) | `entity:list:result` to requester |
216
+ | Get | `entity:get` | reads record file | `entity:get:result` to requester |
217
+ | Create | `entity:save` (no id) | generates id, writes file, invalidates cache | `entity:created` to all |
218
+ | Update | `entity:save` (with id) | renames file if name changed, writes, invalidates cache | `entity:updated` to all |
219
+ | Delete | `entity:delete` | deletes file, invalidates cache | `entity:deleted` to all |
220
+ | External change | — | chokidar detects file change | `entity:external-change` to all |
221
+
222
+ ---
223
+
224
+ ## Multi-Entity Support
225
+
226
+ When the recipe is applied for multiple entities (e.g. Company, Site, User, Incident):
227
+
228
+ **What the recipe generates:**
229
+ - `server/src/data/fileStore.ts` — entity-agnostic (handles any entity name as a string param)
230
+ - `server/src/data/idgen.ts` — shared ID generator
231
+ - `server/src/data/watcher.ts` — watches `data/` folder, entity-agnostic
232
+ - Per entity: `server/src/sockets/{entity}Handlers.ts`
233
+ - Per entity: `server/src/routes/{entity}.ts`
234
+ - Per entity: `shared/src/types/{entity}.ts` — stub with TODO comments
235
+
236
+ **What remains the developer's job:**
237
+ - Define actual field shapes in entity type stubs
238
+ - Implement entity relationship lookups (foreign key references)
239
+ - Mount generated routes in `server/src/index.ts`
240
+ - Add validation rules for entity-specific business logic
241
+ - Build client-side view components (`useEntity` hook is generated; views are not — unless shell recipe ran first)
242
+
243
+ ---
244
+
245
+ ## Local-First Pattern
246
+
247
+ The file-based persistence layer also enables a local-first workflow:
248
+
249
+ **The pattern:**
250
+ 1. Users run the app locally — all data lives in `data/` as JSON files
251
+ 2. Multiple users can work simultaneously (different records = different files = no conflicts)
252
+ 3. When ready, a **publish** action pushes the local JSON data to a central database or API
253
+ 4. This can be triggered manually (publish button) or automatically on save
254
+
255
+ **The use case:** Build all application screens and let stakeholders interact with real data shapes before the backend database exists. When the backend is ready, wire up a publish step that sends the local JSON records to it. The frontend never changes — only the persistence layer swaps out.
256
+
257
+ **What the recipe provides:** The local file layer. The publish-to-database step is a separate recipe (`publish-to-db`) that wraps the file layer and adds the sync mechanism. Not included in this recipe.
258
+
259
+ ---
260
+
261
+ ## Sample Data Patterns
262
+
263
+ Domain-specific entity setups for common application types. See the `samples/` folder:
264
+
265
+ - [`samples/support-signal.md`](../samples/support-signal.md) — NDIS/disability support: Company, Site, User, Incident, MomentThatMatters
266
+ - [`samples/youtube-launch-optimizer.md`](../samples/youtube-launch-optimizer.md) — YouTube content: Channel, Video, Script, ThumbnailVariant, LaunchTask
267
+
268
+ Each sample defines: entity names, namish fields, key domain fields, relationships, and suggested nav mapping. Use a sample as the input to the recipe's entity questions.
269
+
270
+ ---
271
+
272
+ ## Alternative Persistence Recipes
273
+
274
+ These are separate recipes for when file-based JSON is not the right fit:
275
+
276
+ | Recipe | When to Use |
277
+ |--------|-------------|
278
+ | `file-csv` | Humans need to edit data in Excel/Numbers; export to external systems; compliance reporting |
279
+ | `sqlite-drizzle` | Entity count grows large (thousands+); complex queries; referential integrity required |
280
+ | `in-memory` | Development and testing only; full Socket.io loop without touching the filesystem |
281
+
282
+ The Socket.io event spec is identical across all persistence recipes. The client never knows whether the server is reading files, a database, or memory. Persistence is swappable.
283
+
284
+ ---
285
+
286
+ ## What to Generate in the Build Prompt
287
+
288
+ When generating the prompt for this recipe, collect:
289
+
290
+ 1. **Entity names** — what entities does the app need? (e.g. Company, Site, User)
291
+ 2. **Namish field** — for each entity, what field is used to name/slug the file? (usually `name`; sometimes a composite)
292
+ 3. **Key fields** — domain-specific fields per entity
293
+ 4. **Relationships** — which entities reference others? → adds TODO FK comments to type stubs
294
+ 5. **Status field?** — does each entity need an active/inactive or draft/published status?
295
+ 6. **Gitignore `data/`?** — recommended: yes for production data, no for seeded sample data
@@ -0,0 +1,233 @@
1
+ # Recipe: Visual Shell
2
+
3
+ A collapsible left-sidebar navigation shell with header, main content area, and optional footer/status bar. Clicking a nav item switches the tool rendered in the content area. Menus can change dynamically when a sub-tool is active. Domain-agnostic — the shell knows about layout and navigation only.
4
+
5
+ ---
6
+
7
+ ## Recipe Anatomy
8
+
9
+ **Intent**
10
+ Scaffold the application's structural container. The shell provides layout, navigation state, and view switching. It has no knowledge of data, entities, or business logic — those are filled in later.
11
+
12
+ **Type**: Seed — apply once to a new project. Re-applying is guarded by idempotency check.
13
+
14
+ **Stack Assumptions**
15
+ - React 19, TypeScript, TailwindCSS v4
16
+ - No additional libraries required beyond what AppyStack ships with
17
+
18
+ **Idempotency Check**
19
+ Does `client/src/components/AppShell.tsx` exist? If yes → PRESENT, skip unless `--force`.
20
+
21
+ **Does Not Touch**
22
+ - `client/src/App.tsx` beyond mounting AppShell
23
+ - `server/` — this is client-only
24
+ - `data/` folder
25
+ - Domain logic inside view components (stubs only)
26
+ - Socket.io wiring (belongs to a data or feature recipe)
27
+
28
+ **Composes With**
29
+ - `file-crud` recipe — persistence recipe populates view stubs with real data
30
+ - `domain-preparation` step — before combining shell + persistence, collect domain context once
31
+
32
+ ---
33
+
34
+ ## Layout Structure
35
+
36
+ ```
37
+ ┌────────────────────────────────────────────────────────┐
38
+ │ Header (app name left │ right: actions, settings cog) │
39
+ ├──────────────────┬─────────────────────────────────────┤
40
+ │ [≡] Sidebar │ │
41
+ │ │ Content Panel │
42
+ │ ▼ Group Label │ (active view renders here) │
43
+ │ ● Primary │ │
44
+ │ ● Primary │ │
45
+ │ · Secondary │ │
46
+ │ │ │
47
+ │ ▼ Group Label │ │
48
+ │ ● Primary │ │
49
+ │ │ │
50
+ │ [collapse ◀] │ │
51
+ ├──────────────────┴─────────────────────────────────────┤
52
+ │ Footer / Status Bar (optional) │
53
+ └────────────────────────────────────────────────────────┘
54
+ ```
55
+
56
+ When the sidebar is collapsed, it shrinks to an icon strip (or zero width). A toggle handle is always visible.
57
+
58
+ ---
59
+
60
+ ## Component Structure
61
+
62
+ ```
63
+ client/src/
64
+ ├── components/
65
+ │ ├── AppShell.tsx ← outer layout, composes header + sidebar + content
66
+ │ ├── Header.tsx ← app title left, actions right (cog, user, etc.)
67
+ │ ├── Sidebar.tsx ← collapsible, renders nav groups and items
68
+ │ ├── SidebarGroup.tsx ← one group: primary items + optional secondary items
69
+ │ └── ContentPanel.tsx ← renders the active view via viewMap
70
+ ├── views/ ← one stub per nav item destination
71
+ │ └── [ViewName]View.tsx
72
+ ├── config/
73
+ │ └── nav.ts ← static nav config (root-level default nav)
74
+ └── contexts/
75
+ └── NavContext.tsx ← shell state: activeView, collapsed, contextNav
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Nav Config Shape
81
+
82
+ Define nav structure as data, not hardcoded JSX:
83
+
84
+ ```typescript
85
+ // client/src/config/nav.ts
86
+
87
+ export type NavItemTier = 'primary' | 'secondary'
88
+
89
+ export interface NavItem {
90
+ key: string // unique view key, used by ContentPanel
91
+ label: string // display text
92
+ icon?: string // optional icon identifier
93
+ tier?: NavItemTier // defaults to 'primary' if omitted
94
+ }
95
+
96
+ export interface NavGroup {
97
+ label: string
98
+ items: NavItem[]
99
+ }
100
+
101
+ export type NavConfig = NavGroup[]
102
+
103
+ export const navConfig: NavConfig = [
104
+ {
105
+ label: 'Main',
106
+ items: [
107
+ { key: 'dashboard', label: 'Dashboard', tier: 'primary' },
108
+ { key: 'settings', label: 'Settings', tier: 'secondary' },
109
+ ],
110
+ },
111
+ ]
112
+ ```
113
+
114
+ **Primary items** are the main actions — rendered prominently. **Secondary items** render smaller or in a visual sub-cluster within the group, for less-frequently-used actions.
115
+
116
+ ---
117
+
118
+ ## State Model
119
+
120
+ All shell state lives in `NavContext`:
121
+
122
+ ```typescript
123
+ // client/src/contexts/NavContext.tsx
124
+
125
+ interface NavContextValue {
126
+ activeView: string // which view is currently rendering
127
+ collapsed: boolean // is the sidebar collapsed?
128
+ contextNav: NavConfig | null // sub-tool override nav; null = use static navConfig
129
+
130
+ navigate: (viewKey: string) => void
131
+ toggleCollapsed: () => void
132
+ setContextNav: (nav: NavConfig | null) => void
133
+ }
134
+ ```
135
+
136
+ The `collapsed` state controls sidebar width: `collapsed ? 'w-12' : 'w-56'`. Use Tailwind transition utilities for smooth animation. A collapse toggle handle renders at the sidebar bottom (or top) — always visible even when collapsed.
137
+
138
+ ---
139
+
140
+ ## Content Panel Switching
141
+
142
+ ```typescript
143
+ // ContentPanel.tsx
144
+ const viewMap: Record<string, React.ComponentType> = {
145
+ dashboard: DashboardView,
146
+ settings: SettingsView,
147
+ // one entry per nav item key
148
+ }
149
+
150
+ const View = viewMap[activeView] ?? viewMap['dashboard']
151
+ return <View />
152
+ ```
153
+
154
+ View stubs are generated empty. Domain logic is filled in separately (by the developer, a data recipe, or a compound recipe step).
155
+
156
+ ---
157
+
158
+ ## Context-Aware Menus
159
+
160
+ When a sub-tool needs its own nav items, it replaces the sidebar temporarily using `setContextNav`. When the user navigates away, the view unmounts and clears the context nav automatically.
161
+
162
+ ```typescript
163
+ // Inside a sub-tool view component
164
+ const { setContextNav } = useNav()
165
+
166
+ useEffect(() => {
167
+ setContextNav([
168
+ {
169
+ label: 'Incident Tools',
170
+ items: [
171
+ { key: 'incident-list', label: 'All Incidents', tier: 'primary' },
172
+ { key: 'incident-create', label: 'New Incident', tier: 'primary' },
173
+ { key: 'incident-export', label: 'Export', tier: 'secondary' },
174
+ ],
175
+ },
176
+ ])
177
+ return () => setContextNav(null) // cleanup: restore static nav on unmount
178
+ }, [])
179
+ ```
180
+
181
+ The Sidebar renders `contextNav ?? navConfig` — context nav wins when set, falls back to static config when cleared.
182
+
183
+ ---
184
+
185
+ ## Optional Footer / Status Bar
186
+
187
+ If the app has a footer (connection status, version number, last-sync time, etc.), add a `Footer.tsx` component and include it in `AppShell`. The recipe generates a stub if requested. If not needed, `AppShell` simply omits it.
188
+
189
+ ```
190
+ client/src/components/Footer.tsx ← optional; only generated if requested
191
+ ```
192
+
193
+ ---
194
+
195
+ ## What the Recipe Asks at Use-Time
196
+
197
+ Before generating the build prompt, collect:
198
+
199
+ 1. **App name** — displayed in the Header
200
+ 2. **Main tools / pages** (2–6) — generates view stubs and initial nav items
201
+ 3. **Nav groups** — which items group together, what each group is called
202
+ 4. **Primary vs secondary** — for each item, is it a primary action or secondary?
203
+ 5. **Default view** — which view loads on startup
204
+ 6. **Context-aware nav?** — does any tool need its own sidebar menu when active? (Yes → adds `setContextNav` scaffolding to that view stub)
205
+ 7. **Footer / status bar?** — yes or no → generates Footer stub if yes
206
+
207
+ The recipe does **not** ask about data models, API calls, or business logic. Those are not its concern.
208
+
209
+ ---
210
+
211
+ ## Other Shell Patterns (Future Recipe Ideas)
212
+
213
+ These are identified but not yet built. Noted here so they're visible when planning.
214
+
215
+ **Top-Nav Shell**
216
+ Horizontal tab or link bar across the top. No sidebar. For apps with 3–5 flat sections of equal weight. Navigation is shallow; no hierarchy needed.
217
+
218
+ **Workspace Shell** (VS Code style)
219
+ Activity bar (icon strip, far left) selects a panel type. The panel area changes completely per activity — not just nav items but full tool panels, file trees, form widgets. Two levels of state: `activeActivity` + `activeView`. Best for tool-heavy apps where each "mode" has its own set of controls (e.g. FliHub's recording workflow vs settings vs file browser).
220
+
221
+ **Dashboard Shell**
222
+ A fixed or responsive grid of widget panels. Each widget independently fetches and displays data. No single `activeView` — multiple views live simultaneously. Per-widget Socket.io subscriptions. Best for monitoring, ops tools, reporting apps where seeing multiple things at once is the goal.
223
+
224
+ ---
225
+
226
+ ## Styling Notes
227
+
228
+ - Sidebar width: `w-56` expanded, `w-12` collapsed (icon-only or just toggle handle)
229
+ - Header height: `h-14`
230
+ - Content panel: fills remaining space, independently scrollable
231
+ - Active nav item: distinct background highlight (CSS variable for theming)
232
+ - Transitions: `transition-all duration-200` on sidebar width change
233
+ - Use TailwindCSS v4 CSS variables for sidebar/header colours — define in `client/src/styles/index.css`