@sellhapi/mcp 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ /**
3
+ * BuildGuard rules — extracted from buildGuard.service.ts.
4
+ * Code that violates these rules will be rejected by push_code.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.BUILD_RULES = void 0;
8
+ exports.BUILD_RULES = `
9
+ # SellHapi BuildGuard Rules
10
+
11
+ Your code MUST pass all these rules or push_code will reject it with specific violation details.
12
+
13
+ ## APPROVED IMPORTS (only these are allowed)
14
+
15
+ Exact matches:
16
+ @sellhapi/studio-runtime
17
+ react
18
+ react-dom
19
+ react-dom/client
20
+ react/jsx-runtime
21
+ react/jsx-dev-runtime
22
+ framer-motion
23
+ clsx
24
+ date-fns
25
+ zod
26
+ lucide-react
27
+
28
+ Subpath imports allowed:
29
+ date-fns/*
30
+ react-dom/*
31
+ react/*
32
+
33
+ Relative imports (./ and ../) are always allowed between project files.
34
+
35
+ EVERYTHING ELSE IS BLOCKED — no axios, no tailwindcss, no lodash, no node builtins.
36
+
37
+ ## HARD VIOLATIONS (build fails)
38
+
39
+ 1. DISALLOWED IMPORTS
40
+ Any import not in the approved list above.
41
+ Violation: "Import from X is not allowed."
42
+
43
+ 2. DYNAMIC REQUIRE
44
+ require() with non-string argument or unapproved package.
45
+ Violation: "Dynamic require() is not allowed."
46
+
47
+ 3. DYNAMIC IMPORT
48
+ import() expressions (as opposed to static import statements).
49
+ Violation: "Dynamic import() is not allowed in generated storefront code."
50
+
51
+ 4. CODE INJECTION
52
+ eval(), new Function(), Function() — all blocked.
53
+ Violation: "eval()/Function() is not allowed — code injection risk."
54
+
55
+ 5. RAW NETWORK ACCESS
56
+ fetch(), new XMLHttpRequest(), new WebSocket() — all blocked.
57
+ Violation: "Direct fetch() calls are not allowed. Use hooks from @sellhapi/studio-runtime instead."
58
+ All data must flow through runtime hooks.
59
+
60
+ 6. DANGEROUS GLOBALS
61
+ Blocked property access:
62
+ - process.env
63
+ - document.cookie, document.write(), document.writeln(), document.domain, document.execCommand()
64
+ - window.fetch, window.eval, window.Function
65
+ - window.XMLHttpRequest, window.WebSocket
66
+ - window.localStorage, window.sessionStorage, window.indexedDB
67
+ - navigator.sendBeacon
68
+ - Standalone: localStorage, sessionStorage, indexedDB
69
+ Violation: "X is not allowed in storefront code."
70
+
71
+ 7. SUBSCRIPT ACCESS ON GLOBALS
72
+ window["fetch"], globalThis["eval"], self["XMLHttpRequest"] — all blocked.
73
+ Violation: "Subscript access on 'window' is not allowed."
74
+
75
+ 8. DANGEROUS JSX
76
+ dangerouslySetInnerHTML — blocked.
77
+ Violation: "dangerouslySetInnerHTML is not allowed — XSS vector."
78
+
79
+ 9. MISSING CART DRAWER
80
+ If any file uses useCart or addItem, <CartDrawer /> MUST be rendered somewhere.
81
+ Violation: "useCart/addItem is used but <CartDrawer /> is not rendered anywhere."
82
+
83
+ ## SOFT WARNINGS (build passes, but flagged)
84
+
85
+ 1. product.images[N].url — WRONG. images is string[], each element IS the URL.
86
+ Fix: use images[N] directly or product.imageUrl for primary.
87
+
88
+ 2. addItem() without type field — CartItem.type is required.
89
+ Fix: add type: 'physical' | 'digital' to every addItem() call.
90
+
91
+ 3. campaign.raised / campaign.goalAmount — WRONG field names.
92
+ Fix: use campaign.raisedAmountKobo / campaign.goalAmountKobo (divide by 100 for display).
93
+
94
+ 4. ticketType.soldCount — WRONG. Use ticketType.sold.
95
+
96
+ ## REQUIRED: data-region ATTRIBUTES
97
+
98
+ Every section that renders live data MUST have a data-region attribute on its outermost container element.
99
+ This is required for Studio's region selector, Layers panel, and entity injection to work.
100
+
101
+ Required attributes:
102
+ data-region="<slug>" — unique kebab-case identifier, e.g. "hero", "products", "appointments"
103
+ data-label="<name>" — human-readable label shown in Studio UI, e.g. "Products Section"
104
+ data-scope="content" — always "content" for data-driven sections
105
+
106
+ Example:
107
+ <section data-region="products" data-label="Products" data-scope="content">
108
+ {/* useProducts() renders here */}
109
+ </section>
110
+
111
+ <section data-region="appointments" data-label="Consultations" data-scope="content">
112
+ {/* useServices() renders here */}
113
+ </section>
114
+
115
+ ALSO add data-region to static layout sections (hero, footer, nav) — these don't need data hooks but must still be selectable:
116
+ <section data-region="hero" data-label="Hero" data-scope="layout">
117
+ <header data-region="navigation" data-label="Navigation" data-scope="navigation">
118
+ <footer data-region="footer" data-label="Footer" data-scope="layout">
119
+
120
+ If you use ANY data hook (useProducts, useServices, useEvents, etc.) without ANY data-region attributes, the build will warn you.
121
+
122
+ ## FILE STRUCTURE
123
+
124
+ Required: App.tsx (or App.jsx) as entry point.
125
+ Recommended structure:
126
+ App.tsx — main component with routing
127
+ components/ — reusable UI components
128
+ pages/ — route-level pages (ProductPage, EventPage, etc.)
129
+ styles/ — optional shared styles
130
+
131
+ ## BUNDLE SIZE LIMITS
132
+
133
+ Hard limit: 4 MB (build fails above this)
134
+ Warning: 3.5 MB (runtime baseline is ~2.1 MB — aim to keep user code under 1.4 MB)
135
+
136
+ ## STYLING
137
+
138
+ Use inline styles (React style objects) — this is the convention.
139
+ CSS modules work but are not typical.
140
+ Tailwind is NOT available (not in approved imports).
141
+ framer-motion is available for animations.
142
+
143
+ ## KEY PATTERNS
144
+
145
+ 1. All data comes from hooks. NEVER hardcode product names, prices, or content.
146
+ 2. Show loading states: if (loading) return <div>Loading...</div>
147
+ 3. Show empty states: if (data.length === 0) return <div>No items yet</div>
148
+ 4. Format prices: (priceKobo / 100).toLocaleString() for Naira display
149
+ 5. Use useHashRouter() for multi-page navigation
150
+ 6. Commerce actions return { paymentLink } — redirect: window.location.href = result.paymentLink
151
+ `;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Per-capability UX patterns, anti-patterns, and data shape rules.
3
+ *
4
+ * Previously a hand-maintained static string — now fetched live from sell_api
5
+ * which serializes STUDIO_PROMPT_REGISTRY on every request. Zero drift.
6
+ *
7
+ * FALLBACK_PATTERNS is used when the API call fails (e.g. network error,
8
+ * server cold-starting). It reflects the last known good state and should
9
+ * be updated whenever the registry diverges significantly.
10
+ */
11
+ /**
12
+ * Fetch live capability patterns from sell_api.
13
+ * Falls back to FALLBACK_PATTERNS on any error — never throws.
14
+ */
15
+ export declare function buildCapabilityPatterns(): Promise<string>;
@@ -0,0 +1,339 @@
1
+ "use strict";
2
+ /**
3
+ * Per-capability UX patterns, anti-patterns, and data shape rules.
4
+ *
5
+ * Previously a hand-maintained static string — now fetched live from sell_api
6
+ * which serializes STUDIO_PROMPT_REGISTRY on every request. Zero drift.
7
+ *
8
+ * FALLBACK_PATTERNS is used when the API call fails (e.g. network error,
9
+ * server cold-starting). It reflects the last known good state and should
10
+ * be updated whenever the registry diverges significantly.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.buildCapabilityPatterns = buildCapabilityPatterns;
14
+ const client_js_1 = require("../client.js");
15
+ /**
16
+ * Fetch live capability patterns from sell_api.
17
+ * Falls back to FALLBACK_PATTERNS on any error — never throws.
18
+ */
19
+ async function buildCapabilityPatterns() {
20
+ try {
21
+ const data = await (0, client_js_1.get)("/api/mcp/capability-patterns");
22
+ if (data?.patterns)
23
+ return data.patterns;
24
+ return FALLBACK_PATTERNS;
25
+ }
26
+ catch {
27
+ return FALLBACK_PATTERNS;
28
+ }
29
+ }
30
+ /** Last-known-good static fallback. Used when the API is unreachable. */
31
+ const FALLBACK_PATTERNS = `
32
+ # SellHapi Capability Patterns
33
+
34
+ ## PHYSICAL PRODUCTS
35
+
36
+ Route: #/products (list) + product quick-view modal on card click
37
+ Hooks: useProducts(options?), useProduct(id), useCart(), useCartCheckout()
38
+
39
+ useProducts default limit is 6. Always pass an explicit limit:
40
+ - Homepage preview: useProducts({ limit: 4 }) — show a teaser, not the full catalog
41
+ - Full products page: useProducts({ limit: 200 }) — load the full catalog
42
+
43
+ MANDATORY: Product list is on a dedicated #/products page (not homepage).
44
+
45
+ MANDATORY: Homepage product section is a TEASER — visually distinct from the full products page.
46
+ It is an editorial/marketing section: large dominant images, 2-col or 4-col cards, product name + price only.
47
+ End it with a "Shop All Products →" button/link that navigates to #/products.
48
+ Do NOT reuse the same component as the /products page grid — different layout, different visual weight.
49
+ Example structure:
50
+ <section> {/* homepage teaser */}
51
+ <h2>Our Collection</h2>
52
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24 }}>
53
+ {products.slice(0, 4).map(p => <HomepageProductCard key={p.id} product={p} />)}
54
+ </div>
55
+ <button onClick={() => navigate('#/products')}>Shop All Products →</button>
56
+ </section>
57
+
58
+ MANDATORY: Product detail opens in a MODAL overlay on card click — never navigate to a separate page for product detail.
59
+ Modal shows: images (filtered for non-video), name, price, compareAtPriceKobo strikethrough, discountBadge,
60
+ variant selector (chip/pill buttons per option group), quantity stepper with stock cap,
61
+ "Add to Cart" button, policies section (return/shipping), reviews section.
62
+ After addItem(): MUST call openCart() to show the cart drawer.
63
+
64
+ PAGINATION on #/products page (load more pattern):
65
+ const [limit, setLimit] = React.useState(12);
66
+ const { data: products } = useProducts({ limit });
67
+ // Show "Load More" only when the returned count equals the current limit (more may exist)
68
+ {products?.length === limit && (
69
+ <button onClick={() => setLimit(l => l + 12)}>Load More</button>
70
+ )}
71
+
72
+ Price fields:
73
+ - priceKobo: main price (divide by 100 for display). BIGINT from API — always wrap in Number(): Number(product.priceKobo)
74
+ - originalPriceKobo: pre-discount / strikethrough price (number | undefined). Show as strikethrough when originalPriceKobo > priceKobo.
75
+ - discountBadge: pre-formatted sale label e.g. "20% Off" (string | null). Show as a badge overlay on the card and in the modal header. When present, originalPriceKobo is also set — use it as the strikethrough price.
76
+ - priceRange: { minKobo, maxKobo } — present when variants have different prices. Display as "₦X – ₦Y". Show instead of a single price when minKobo !== maxKobo.
77
+
78
+ Variant shape (CRITICAL — no top-level size/color fields):
79
+ Each variant has: { id, options: Array<{ field: string; value: string }>, priceKobo?, stock? }
80
+ options is a generic attribute list — e.g. [{field:"Color",value:"Red"},{field:"Size",value:"XL"}]
81
+ Render variant groups by grouping on options[*].field and showing chip/pill buttons per group.
82
+
83
+ Correct variant label helper:
84
+ function variantLabel(v) { return v.options?.map(o => o.value).join(' / ') || 'Standard'; }
85
+
86
+ Correct variant selector pattern:
87
+ // Group options by field
88
+ const fields = [...new Set(variants.flatMap(v => v.options.map(o => o.field)))];
89
+ // For each field, show a row of pill buttons
90
+ fields.map(field => (
91
+ <div key={field}>
92
+ <span>{field}</span>
93
+ {[...new Set(variants.flatMap(v => v.options.filter(o => o.field === field).map(o => o.value)))].map(val => (
94
+ <button key={val} onClick={() => setSelected(field, val)}
95
+ style={{ fontWeight: isSelected(field, val) ? 'bold' : 'normal' }}>{val}</button>
96
+ ))}
97
+ </div>
98
+ ))
99
+
100
+ Stock rules:
101
+ - stock: null = no stock tracking — treat as unlimited (never show "Out of Stock")
102
+ - stock: 0 = tracked and out of stock — show "Out of Stock", disable Add to Cart
103
+ - stock > 0 = tracked, cap quantity stepper at this value
104
+ - inStock: boolean — convenience field for grid card sold-out badge
105
+ Qty stepper: const maxStock = selected?.stock != null ? Number(selected.stock) : (product?.stock != null ? Number(product.stock) : Infinity);
106
+ Disable '+' when qty >= maxStock.
107
+
108
+ Image filtering + URL encoding:
109
+ Some image URLs contain spaces — always encode before use in <img src>.
110
+ function imageList(imgs) {
111
+ return (imgs ?? [])
112
+ .filter(u => !/\.(mp4|webm|mov)$/i.test(u))
113
+ .map(u => u.includes(' ') ? u.split('/').map(encodeURIComponent).join('/') : u);
114
+ }
115
+ Use imageList(product.images) everywhere — never pass the raw images array to <img src>.
116
+
117
+ Anti-patterns:
118
+ - NEVER hardcode product data — always use useProducts()
119
+ - NEVER access product.images[0].url — images is string[], use images[0] directly
120
+ - NEVER call addItem() without type: 'physical' or type: 'digital'
121
+ - NEVER use product.price or product.priceNaira — use product.priceKobo
122
+ - NEVER use v.name / v.size / v.color for variant labels — iterate v.options array
123
+ - NEVER use local state as substitute for openCart() — cart drawer won't appear without it
124
+ - NEVER show all products on the homepage — use a 4-item teaser + "Shop All Products →" CTA
125
+ - NEVER call useProducts() without an explicit limit — default is 6, silently truncates catalog
126
+
127
+ ---
128
+
129
+ ## DIGITAL PRODUCTS
130
+
131
+ Route: #/digital/:id → pages/DigitalProductPage.tsx
132
+ Hooks: useDigitalProducts(), useDigitalProduct(id), useProductCheckout() or useCart()
133
+
134
+ List page: product cards with imageUrl, name, priceKobo/100, description snippet.
135
+ Detail page: full product info + "Buy Now" (useProductCheckout) or "Add to Cart" (useCart).
136
+
137
+ Anti-patterns:
138
+ - Same as physical products (images is string[], priceKobo not price, etc.)
139
+ - NEVER use useProductCheckout() for multi-product stores — use cart pattern instead
140
+
141
+ ---
142
+
143
+ ## SERVICES
144
+
145
+ Route: #/services/:id → pages/ServicePage.tsx
146
+ Hooks: useServiceListings(), useServiceListing(id), useCheckoutService(), useInitiateEngagement()
147
+
148
+ List page: service cards with coverImage, title, category, priceKobo/100.
149
+ Detail page: full service info, add-ons, sub-services, customer form, checkout button.
150
+
151
+ ---
152
+
153
+ ## APPOINTMENTS
154
+
155
+ Hooks: useServices(), useAvailableDates({ serviceId, month }), useAvailability({ serviceId, from, to }), useBookAppointment()
156
+
157
+ Flow:
158
+ 1. Show services list (useServices) — each card shows name, duration, price
159
+ 2. Customer picks a service → show month calendar (useAvailableDates gives available YYYY-MM-DD dates)
160
+ 3. Customer picks a date → show time slots: useAvailability({ serviceId, from: date, to: date })
161
+ NOTE: to must be the SAME date as from — the hook internally extends it to end-of-day
162
+ 4. Customer picks a slot — render slot.startTime formatted for display
163
+ 5. Show booking form (name, email, phone)
164
+ 6. Submit: useBookAppointment().book({ serviceId, dateTime: slot.startTime, customerName, customerEmail, customerPhone })
165
+ Returns { paymentLink } — redirect with window.location.href = result.paymentLink
166
+
167
+ Price display:
168
+ service.priceKobo is a BIGINT from the API — Sequelize returns it as a STRING.
169
+ ALWAYS wrap in Number(): Number(service.priceKobo) before any arithmetic or comparison.
170
+ Display: ₦{(Number(service.priceKobo) / 100).toLocaleString()}
171
+
172
+ Slot filtering: DO NOT filter slots by status. useAvailability returns only bookable slots — pass them all through.
173
+
174
+ Anti-patterns:
175
+ - NEVER call initiate() for appointments — always use book(). useBookAppointment returns { book, initiate (alias), loading, error }
176
+ - NEVER pass slotId — pass dateTime: slot.startTime (ISO datetime string from useAvailability result)
177
+ - NEVER use slot.durationMinutes — it doesn't exist on AvailabilitySlot
178
+ - NEVER hardcode service names/prices — always use useServices()
179
+ - NEVER skip the date→slot two-step — must pick date first, then load slots for that date
180
+ - NEVER filter slots with slot.status === 'available' — drops all slots; let the hook decide
181
+
182
+ ---
183
+
184
+ ## EVENTS
185
+
186
+ Route: #/events/:id → pages/EventPage.tsx
187
+ Hooks: useEvents(), useEvent(id), usePurchaseTickets(), useCreateRSVP()
188
+
189
+ List page: event cards with name, startsAt, endsAt, location, imageUrl.
190
+ Date display: use event.startsAt (ISO string) — NOT event.date (does not exist).
191
+ Format: new Date(event.startsAt).toLocaleDateString('en-NG', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' })
192
+ If endsAt differs from startsAt date, show a range: "Fri, 5 Sep – Sun, 7 Sep 2025"
193
+ Detail page: event info + ticket type selection + customer form + purchase button.
194
+
195
+ Anti-patterns:
196
+ - NEVER use event.date — the fields are startsAt and endsAt (ISO strings)
197
+ - NEVER use ticketType.soldCount — use ticketType.sold
198
+ - NEVER hardcode event data — always use useEvents()
199
+ - Ticket availability: ticketType.available (number | null)
200
+
201
+ ---
202
+
203
+ ## DONATIONS
204
+
205
+ Route: #/campaigns/:id → pages/CampaignPage.tsx
206
+ Hooks: useDonationCampaigns(), useDonationCampaign(id), useDonate()
207
+
208
+ List page: campaign cards with name, imageUrl, progress bar (raisedAmountKobo / goalAmountKobo).
209
+ Detail page: campaign info + amount input + customer form + donate button.
210
+
211
+ Anti-patterns:
212
+ - NEVER use campaign.raised or campaign.raisedAmount — use campaign.raisedAmountKobo
213
+ - NEVER use campaign.goalAmount — use campaign.goalAmountKobo (can be null)
214
+ - Progress bar: (raisedAmountKobo / goalAmountKobo) * 100 — guard for null goalAmountKobo
215
+
216
+ ---
217
+
218
+ ## MEMBERSHIPS
219
+
220
+ Route: #/membership/:tierId → pages/MembershipPage.tsx
221
+ Hooks: useMemberships(), useSubscribeMembership()
222
+
223
+ List page: tier cards with name, priceKobo/100, billingPeriod label, benefits list.
224
+ billingPeriod labels: "monthly" → "/month", "yearly" → "/year", "one_time" → no label.
225
+ benefits: (tier.benefits ?? []).map(...) — can be null.
226
+ Detail page: tier info + benefits checklist + customer form + subscribe button.
227
+
228
+ Anti-patterns:
229
+ - NEVER use tier.price or tier.amount — use tier.priceKobo
230
+ - NEVER use tier.features or tier.perks — use tier.benefits (string[] | null)
231
+ - NEVER display "annual" — the SDK uses "yearly"
232
+ - NEVER assume tier.imageUrl exists — it is string | null, conditionally render
233
+
234
+ ---
235
+
236
+ ## CONTENT & CMS
237
+
238
+ Hooks: useContentImages(), useContentVideos(), useContentTestimonials(), useContentFaqs(),
239
+ useContentStats(), useContentFeatures(), useContentTeamMembers(), useContentForm(formId), useSubmitForm(formId)
240
+
241
+ NOTE: The team hook is useContentTeamMembers() — NOT useContentTeam() (does not exist).
242
+
243
+ All content hooks return FetchState<ContentItem[]>.
244
+ Forms: render fields dynamically from form.fields array — never hardcode form structure.
245
+
246
+ ---
247
+
248
+ ## LISTINGS
249
+
250
+ Route: #/listing/:slugOrId
251
+ Hooks: useListings(), useListingDetail(slug), useListingFilters()
252
+
253
+ Action hooks depend on listing type:
254
+ - Payment listings: useListingPayment(id).checkout(customer)
255
+ - Enquiry listings: useListingEnquiry(id).submit({ name, email, message })
256
+ - Application listings: useListingApplication(id).submit({ name, email, coverNote })
257
+ - Reserve listings: useListingReserve(id).reserve()
258
+ - Download listings: useListingDownload(id).download()
259
+
260
+ ---
261
+
262
+ ## CUSTOMER ACCOUNTS
263
+
264
+ Hooks: useCustomerAuth()
265
+
266
+ Auth flow: signUp(name, email, password) / signIn(email, password) / signOut()
267
+ Check: isLoggedIn (NOT isAuthenticated)
268
+ Protected routes: if (!isLoggedIn) show sign-in form.
269
+ Account dashboard: useMyOrders(), useMyAppointments(), useMyPurchases(), useMyMembership(), useMyProfile()
270
+
271
+ ---
272
+
273
+ ## CART DRAWER THEMING
274
+
275
+ CartDrawer reads CSS custom properties from document.documentElement to match the site design.
276
+ You MUST set these in a useEffect in App.tsx (or a top-level layout component) so the drawer
277
+ inherits the correct colors and font instead of falling back to defaults.
278
+
279
+ Required pattern in App.tsx:
280
+ useEffect(() => {
281
+ const root = document.documentElement;
282
+ root.style.setProperty('--sh-drawer-bg', '#ffffff'); // drawer background
283
+ root.style.setProperty('--sh-drawer-overlay', 'rgba(0,0,0,0.5)');
284
+ root.style.setProperty('--sh-drawer-text', '#111111'); // primary text
285
+ root.style.setProperty('--sh-drawer-muted', '#666666'); // secondary/muted text
286
+ root.style.setProperty('--sh-drawer-border', '#e5e5e5'); // dividers
287
+ root.style.setProperty('--sh-drawer-input-border', '#d1d5db'); // input field borders
288
+ root.style.setProperty('--sh-drawer-accent', '#000000'); // button/CTA background
289
+ root.style.setProperty('--sh-drawer-accent-text', '#ffffff'); // button/CTA text
290
+ root.style.setProperty('--sh-drawer-font', 'inherit'); // font-family
291
+ }, []);
292
+
293
+ Replace the values above with colors that match the site's palette (e.g. brand primary for accent).
294
+
295
+ Anti-patterns:
296
+ - NEVER skip this useEffect — CartDrawer will render with mismatched default colors
297
+ - NEVER apply these properties to a scoped container — they MUST go on document.documentElement
298
+
299
+ ---
300
+
301
+ ## UNIVERSAL RULES
302
+
303
+ 1. Price display: always (priceKobo / 100).toLocaleString() — prefix with ₦
304
+ 2. Empty states: always handle empty arrays gracefully — never show blank sections
305
+ 3. Loading states: always show a loading indicator while data is being fetched
306
+ 4. Images: always handle null imageUrl — show placeholder or hide the image element
307
+ 5. Hash routing: use useHashRouter() for navigation between pages
308
+ 6. Commerce results: all initiate/checkout functions return { paymentLink } — redirect with window.location.href
309
+ 7. Cart pattern: addItem() → openCart() → CartDrawer shows → customer proceeds to checkout
310
+
311
+ ## LAYOUT ALIGNMENT — MANDATORY
312
+
313
+ Every section on every page MUST share the same horizontal container. Nav, hero, product grids,
314
+ content sections, and footer must all align to the same left and right edges.
315
+
316
+ MANDATORY pattern — one shared container constant:
317
+ const CONTAINER: React.CSSProperties = {
318
+ maxWidth: 1200,
319
+ margin: '0 auto',
320
+ padding: '0 24px',
321
+ };
322
+ // Apply to every section wrapper and the nav inner wrapper:
323
+ <nav><div style={CONTAINER}>...</div></nav>
324
+ <section><div style={CONTAINER}>...</div></section>
325
+ <footer><div style={CONTAINER}>...</div></footer>
326
+
327
+ Rules:
328
+ - Nav inner content MUST use the same CONTAINER — nav edges must align with content edges
329
+ - Full-bleed backgrounds (hero color, section color) live on the outer element; content is always inside CONTAINER
330
+ - NEVER give different sections different maxWidth or padding values
331
+ - NEVER use margin: '0 48px' on one section and padding: '0 24px' on another
332
+ - NEVER let a section push wider or narrower than the nav content width
333
+ - Grid gaps, card padding, and inner spacing are fine to vary — only the outer container must be consistent
334
+
335
+ Anti-patterns:
336
+ - Nav at full width while sections have a max-width container → WRONG
337
+ - Hero section edge-to-edge but product section indented → WRONG
338
+ - Different sections using different padding (e.g. 16px vs 40px left/right) → WRONG
339
+ `;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * DesignDirection schema resource.
3
+ *
4
+ * Exposes the exact DesignDirection TypeScript interface + field documentation
5
+ * so AI clients can generate a valid direction JSON themselves (zero SellHapi
6
+ * Claude cost) and submit it via the apply_design_direction tool.
7
+ *
8
+ * Mirrors the interface in generate2.controller.ts. Keep in sync when that
9
+ * interface changes.
10
+ */
11
+ export declare const DESIGN_DIRECTION_SCHEMA = "\n# SellHapi Design Direction Schema\n\nUse this schema to generate a `DesignDirection` JSON object and submit it via\n`apply_design_direction`. This path costs SellHapi nothing \u2014 your own LLM does\nthe work. Only fall back to `set_design_direction` if you cannot generate\nstructured JSON reliably.\n\n---\n\n## Top-level object\n\n```typescript\n{\n businessType: string; // REQUIRED. e.g. \"fashion\", \"bakery\", \"photography\"\n capabilityIds: string[]; // REQUIRED. Active capability IDs. See /api/mcp/capabilities.\n capabilitySlots?: CapabilitySlot[]; // Optional. Richer slot-per-purpose model (see below).\n pages: Page[]; // REQUIRED. Non-empty. One entry per storefront page.\n visualDirection: VisualDirection; // REQUIRED. All visual tokens.\n businessContext: BusinessContext; // REQUIRED. Business identity.\n storefrontConfig?: StorefrontConfig; // Optional. Only for storefront-mode projects.\n displayModes?: DisplayModes; // Optional. Product display preferences.\n}\n```\n\n---\n\n## Page\n\nEach page the storefront will have. Minimum 1, typically 3\u20136.\n\n```typescript\n{\n slug: string; // URL-safe identifier. e.g. \"home\", \"products\", \"about\", \"contact\"\n title: string; // Display name. e.g. \"Home\", \"Shop\", \"About Us\"\n sections: string[]; // Section labels in order. e.g. [\"hero\", \"product-grid\", \"testimonials\", \"newsletter\"]\n}\n```\n\n**Section label conventions:**\n- Use lowercase kebab-case\n- Should describe content purpose: \"hero\", \"featured-products\", \"services-list\", \"booking-cta\",\n \"testimonials\", \"faq\", \"team\", \"newsletter\", \"about-story\", \"contact-form\", \"events-grid\"\n- Capability-specific sections should reference the capability:\n \"appointment-booking\", \"donation-campaigns\", \"membership-tiers\", \"events-list\"\n\n**Common page structures:**\n\nPhysical products store:\n```\nhome: [\"hero\", \"featured-products\", \"collections\", \"testimonials\", \"newsletter\"]\nproducts: [\"filter-bar\", \"product-grid\", \"pagination\"]\nabout: [\"brand-story\", \"values\", \"team\"]\ncontact: [\"contact-form\", \"location-map\"]\n```\n\nService business:\n```\nhome: [\"hero\", \"services-overview\", \"how-it-works\", \"testimonials\", \"booking-cta\"]\nservices: [\"services-list\", \"pricing\"]\nabout: [\"our-story\", \"team\", \"credentials\"]\ncontact: [\"contact-form\", \"faq\"]\n```\n\n---\n\n## VisualDirection\n\nAll fields except the optional ones are required.\n\n```typescript\n{\n // Descriptive aesthetic \u2014 free text. e.g. \"Minimal luxury\", \"Vibrant street market\"\n style: string;\n\n primaryColor: string; // Main brand color \u2014 hex. e.g. \"#1a1a2e\"\n accentColor: string; // CTA / highlight color \u2014 hex. e.g. \"#e94560\"\n backgroundColor: string; // Page background \u2014 hex. Can be dark, cream, off-white.\n surfaceColor: string; // Card / panel background \u2014 hex.\n textColor: string; // Primary text color \u2014 hex.\n\n headingFont: string; // Google Fonts name for headings. e.g. \"Playfair Display\", \"Space Grotesk\"\n bodyFont: string; // Google Fonts name for body copy. e.g. \"Inter\", \"DM Sans\"\n fontStyle: string; // Description. e.g. \"editorial serif headings with clean sans body\"\n\n // Enum values \u2014 use exactly as written:\n cardStyle: \"flat\" | \"soft-shadow\" | \"bordered\" | \"glassmorphism\";\n buttonStyle: \"pill\" | \"rounded\" | \"sharp\" | \"outline-first\";\n visualEffect: \"flat\" | \"subtle-gradient\" | \"glassmorphism\" | \"textured\";\n\n mood: string; // e.g. \"sophisticated and aspirational\", \"warm and approachable\"\n\n // Optional \u2014 include when you have reference site analysis or strong design intent:\n layoutPattern?: string; // e.g. \"full-bleed hero, 3-col card grid, alternating split sections\"\n spacingRhythm?: string; // e.g. \"generous whitespace, section padding 80\u2013120px\"\n typographyScale?: string; // e.g. \"oversized display ~72px, 17px body, tight heading tracking\"\n componentNotes?: string; // e.g. \"rounded cards with drop shadow, pill category tags\"\n distinctiveElements?: string; // e.g. \"full-bleed photography, sticky glassmorphic nav\"\n}\n```\n\n**Color guidance:**\n- All color fields must be valid hex strings (#rrggbb or #rgb)\n- Ensure sufficient contrast: textColor vs backgroundColor \u2265 4.5:1 (WCAG AA)\n- primaryColor and accentColor should be visually distinct\n- surfaceColor is usually a slightly lighter/darker variant of backgroundColor\n\n**Font guidance:**\n- Use Google Fonts names exactly as they appear on fonts.google.com\n- Pairing examples: \"Playfair Display\" + \"Inter\", \"Space Grotesk\" + \"DM Sans\",\n \"Syne\" + \"Outfit\", \"Cormorant Garamond\" + \"Jost\"\n\n---\n\n## BusinessContext\n\n```typescript\n{\n name: string; // REQUIRED. Business name. e.g. \"Adire Lagos\"\n tagline: string; // One-line value prop. e.g. \"Authentic Nigerian fashion for the bold\"\n targetAudience: string; // e.g. \"Nigerian professionals aged 25\u201340 who value craftsmanship\"\n uniqueSellingPoints: string[]; // 2\u20134 bullets. e.g. [\"Handcrafted in Lagos\", \"Free delivery over \u20A615,000\"]\n}\n```\n\n---\n\n## CapabilitySlot (optional, more expressive than capabilityIds alone)\n\n```typescript\n{\n purpose: string; // Human-readable job. e.g. \"Let customers book sessions\"\n capability: string; // Selected capability ID. e.g. \"appointments\"\n alternatives: string[]; // Valid swaps for this slot. Always include the selected capability.\n}\n```\n\n---\n\n## StorefrontConfig (optional)\n\n```typescript\n{\n storeType: \"fashion\" | \"beauty\" | \"food\" | \"electronics\" | \"general\";\n vendorOverrides: string[]; // Free-text additions. e.g. [\"Feature collections on homepage\"]\n}\n```\n\n---\n\n## DisplayModes (optional \u2014 only for stores with physical_products)\n\n```typescript\n{\n productGrid: boolean; // Main product grid (useProducts)\n collections: boolean; // Product collections (useProductCollections)\n sections: boolean; // Category sections (useSections)\n bundles: boolean; // Bundles & deals (useStorefrontBundles)\n}\n```\n\n---\n\n## Full example \u2014 Lagos fashion store\n\n```json\n{\n \"businessType\": \"fashion\",\n \"capabilityIds\": [\"physical_products\", \"content\", \"customer_accounts\"],\n \"pages\": [\n {\n \"slug\": \"home\",\n \"title\": \"Home\",\n \"sections\": [\"hero\", \"featured-products\", \"collections\", \"brand-story\", \"testimonials\", \"newsletter\"]\n },\n {\n \"slug\": \"shop\",\n \"title\": \"Shop\",\n \"sections\": [\"filter-bar\", \"product-grid\"]\n },\n {\n \"slug\": \"about\",\n \"title\": \"About\",\n \"sections\": [\"brand-story\", \"values\", \"team\"]\n },\n {\n \"slug\": \"contact\",\n \"title\": \"Contact\",\n \"sections\": [\"contact-form\", \"faq\"]\n }\n ],\n \"visualDirection\": {\n \"style\": \"Minimal luxury\",\n \"primaryColor\": \"#1a1a2e\",\n \"accentColor\": \"#c9a84c\",\n \"backgroundColor\": \"#fafaf8\",\n \"surfaceColor\": \"#ffffff\",\n \"textColor\": \"#1a1a2e\",\n \"headingFont\": \"Cormorant Garamond\",\n \"bodyFont\": \"Inter\",\n \"fontStyle\": \"editorial serif headings with clean sans body\",\n \"cardStyle\": \"soft-shadow\",\n \"buttonStyle\": \"sharp\",\n \"visualEffect\": \"flat\",\n \"mood\": \"sophisticated and aspirational\",\n \"layoutPattern\": \"full-bleed hero, 2-col editorial split, 3-col product grid\",\n \"spacingRhythm\": \"generous whitespace, 100\u2013120px section padding\",\n \"typographyScale\": \"oversized display 72px, 16px body, tight tracking on headings\"\n },\n \"businessContext\": {\n \"name\": \"Adire Lagos\",\n \"tagline\": \"Authentic Nigerian fashion for the bold\",\n \"targetAudience\": \"Nigerian professionals aged 25\u201340 who value craftsmanship\",\n \"uniqueSellingPoints\": [\n \"Handcrafted adire fabric from Abeokuta\",\n \"Free delivery over \u20A615,000\",\n \"30-day returns\"\n ]\n },\n \"storefrontConfig\": {\n \"storeType\": \"fashion\",\n \"vendorOverrides\": []\n }\n}\n```\n\n---\n\n## Submission\n\nOnce you have generated the direction JSON and the user has reviewed it:\n\n```\napply_design_direction(projectId, direction)\n```\n\nThe server validates the shape, filters any unrecognised capabilityIds, and stores\nthe direction on the project. Status advances to \"directing\".\n";