@onexapis/cli 1.1.17 → 1.1.18

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 (61) hide show
  1. package/README.md +82 -16
  2. package/dist/cli.js +522 -286
  3. package/dist/cli.js.map +1 -1
  4. package/dist/cli.mjs +519 -283
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/index.js +47 -270
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +47 -270
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +1 -1
  11. package/templates/default/.env.example +1 -1
  12. package/templates/default/.mcp.json +8 -0
  13. package/templates/default/CLAUDE.md +941 -0
  14. package/templates/default/bundle-entry.ts +18 -0
  15. package/templates/default/index.ts +26 -0
  16. package/templates/default/package.json +37 -0
  17. package/templates/default/pages/about.ts +66 -0
  18. package/templates/default/pages/home.ts +93 -0
  19. package/templates/default/pages/showcase.ts +146 -0
  20. package/templates/default/sections/about/about-default.tsx +237 -0
  21. package/templates/default/sections/about/about.schema.ts +259 -0
  22. package/templates/default/sections/about/index.ts +15 -0
  23. package/templates/default/sections/cta/cta-default.tsx +180 -0
  24. package/templates/default/sections/cta/cta.schema.ts +210 -0
  25. package/templates/default/sections/cta/index.ts +11 -0
  26. package/templates/default/sections/features/features-default.tsx +154 -0
  27. package/templates/default/sections/features/features.schema.ts +330 -0
  28. package/templates/default/sections/features/index.ts +11 -0
  29. package/templates/default/sections/gallery/gallery-default.tsx +134 -0
  30. package/templates/default/sections/gallery/gallery.schema.ts +397 -0
  31. package/templates/default/sections/gallery/index.ts +11 -0
  32. package/templates/default/sections/hero/hero-default.tsx +212 -0
  33. package/templates/default/sections/hero/hero.schema.ts +273 -0
  34. package/templates/default/sections/hero/index.ts +15 -0
  35. package/templates/default/sections/stats/index.ts +11 -0
  36. package/templates/default/sections/stats/stats-default.tsx +103 -0
  37. package/templates/default/sections/stats/stats.schema.ts +266 -0
  38. package/templates/default/sections/testimonials/index.ts +11 -0
  39. package/templates/default/sections/testimonials/testimonials-default.tsx +130 -0
  40. package/templates/default/sections/testimonials/testimonials.schema.ts +371 -0
  41. package/templates/default/sections-registry.ts +32 -0
  42. package/templates/default/theme.config.ts +107 -0
  43. package/templates/default/theme.layout.ts +21 -0
  44. package/templates/default/tsconfig.json +16 -7
  45. package/templates/default/README.md.ejs +0 -129
  46. package/templates/default/esbuild.config.js +0 -81
  47. package/templates/default/package.json.ejs +0 -31
  48. package/templates/default/src/config.ts.ejs +0 -98
  49. package/templates/default/src/index.ts.ejs +0 -11
  50. package/templates/default/src/layout.ts +0 -23
  51. package/templates/default/src/manifest.ts.ejs +0 -47
  52. package/templates/default/src/pages/home.ts.ejs +0 -37
  53. package/templates/default/src/sections/footer/footer-default.tsx +0 -28
  54. package/templates/default/src/sections/footer/footer.schema.ts +0 -45
  55. package/templates/default/src/sections/footer/index.ts +0 -2
  56. package/templates/default/src/sections/header/header-default.tsx +0 -61
  57. package/templates/default/src/sections/header/header.schema.ts +0 -46
  58. package/templates/default/src/sections/header/index.ts +0 -2
  59. package/templates/default/src/sections/hero/hero-default.tsx +0 -52
  60. package/templates/default/src/sections/hero/hero.schema.ts +0 -52
  61. package/templates/default/src/sections/hero/index.ts +0 -2
@@ -0,0 +1,941 @@
1
+ # CLAUDE.md — OneX Theme Development Guide
2
+
3
+ This file provides context to AI assistants (Claude, Cursor, Copilot) for developing OneX themes.
4
+
5
+ ## CRITICAL RULES — READ FIRST
6
+
7
+ **IMPORTANT: When creating new sections, blocks, or modifying theme code, you MUST follow these rules. Violations will cause the editor and storefront to break.**
8
+
9
+ ### Creating New Sections
10
+
11
+ Every section MUST use `ComponentRenderer` and `BlockRenderer` from `@onexapis/core` to render content. Sections define LAYOUT only — all content (text, images, buttons) is rendered through Blocks and Components.
12
+
13
+ You can use `onexthm_create_section` MCP tool or `onexthm create:section` CLI to scaffold, but if writing manually, every section MUST have:
14
+
15
+ 1. **`"use client"` directive** at the top
16
+ 2. **Import from `@onexapis/core/renderers`** — use `ComponentRenderer` and `BlockRenderer`
17
+ 3. **Import from `@onexapis/core/utils`** — use `getSectionValues`, `toComponentInstance`, `filterEnabledComponents`
18
+ 4. **`data-section-id={section.id}`** attribute on the root element
19
+ 5. **`data-section-type={section.type}`** attribute on the root element
20
+ 6. **`data-block-id={block.id}`** and **`data-block-type={block.type}`** on every block wrapper
21
+ 7. **Handle `isEditing` prop** — show placeholders when empty, disable links
22
+ 8. **A matching `.schema.ts` file** with type, name, category, settings, defaults
23
+ 9. **An `index.ts`** that re-exports the component and schema
24
+ 10. **Registration in `sections-registry.ts`**
25
+
26
+ ### NEVER do these
27
+
28
+ - **NEVER render text/buttons/icons directly** — always use `ComponentRenderer` from core
29
+ - **NEVER use `<h1>`, `<p>`, `<button>` for content** — use `ComponentRenderer` which renders heading, paragraph, button components with proper editor integration
30
+ - **NEVER hardcode content** — content comes from `section.components` and `section.blocks`, rendered via `ComponentRenderer`/`BlockRenderer`
31
+ - **NEVER skip data attributes** — without `data-section-id`, `data-block-id`, the editor cannot select or edit sections
32
+ - **NEVER use `useEffect` for data fetching** — use `useProducts()`, `useBlogs()` hooks
33
+ - **NEVER import from `@onexapis/core/internal`**
34
+
35
+ ### Section Template (MUST follow exactly)
36
+
37
+ ```tsx
38
+ "use client";
39
+
40
+ import type { SectionComponentProps } from "@onexapis/core/types";
41
+ import coreRenderers from "@onexapis/core/renderers";
42
+ import coreUtils from "@onexapis/core/utils";
43
+
44
+ const { ComponentRenderer, BlockRenderer } = coreRenderers;
45
+ const { toComponentInstance, getSectionValues, filterEnabledComponents } =
46
+ coreUtils;
47
+
48
+ export function MySectionDefault({
49
+ section,
50
+ schema,
51
+ isEditing,
52
+ }: SectionComponentProps) {
53
+ const { settings } = getSectionValues(section, schema);
54
+ const components = filterEnabledComponents(section.components || []);
55
+ const blocks = (section.blocks || []).filter((b) => b.enabled !== false);
56
+
57
+ return (
58
+ <section
59
+ data-section-id={section.id}
60
+ data-section-type={section.type}
61
+ data-section-template="default"
62
+ className="py-16"
63
+ style={{ backgroundColor: String(settings.backgroundColor || "#FFFFFF") }}
64
+ >
65
+ <div className="container mx-auto px-4 max-w-6xl">
66
+ {/* MUST use ComponentRenderer for section-level components */}
67
+ {components.map((comp) => (
68
+ <ComponentRenderer
69
+ key={comp.id}
70
+ instance={toComponentInstance(comp)}
71
+ sectionId={section.id}
72
+ isEditing={isEditing}
73
+ />
74
+ ))}
75
+
76
+ {/* MUST use BlockRenderer for blocks */}
77
+ {blocks.map((block) => (
78
+ <div
79
+ key={block.id}
80
+ data-section-id={section.id}
81
+ data-block-id={block.id}
82
+ data-block-type={block.type}
83
+ >
84
+ <BlockRenderer
85
+ block={block}
86
+ sectionId={section.id}
87
+ isEditing={isEditing}
88
+ />
89
+ </div>
90
+ ))}
91
+
92
+ {/* MUST show empty state in editor */}
93
+ {blocks.length === 0 && isEditing && (
94
+ <div className="text-center py-12 border-2 border-dashed border-gray-300 rounded-lg">
95
+ <p className="text-gray-500">Add blocks to populate this section</p>
96
+ </div>
97
+ )}
98
+ </div>
99
+ </section>
100
+ );
101
+ }
102
+
103
+ export default MySectionDefault;
104
+ ```
105
+
106
+ ### How content rendering works
107
+
108
+ - **Sections** define LAYOUT (grid, padding, background color)
109
+ - **Components** (from core) render CONTENT (text, images, buttons)
110
+ - A section component NEVER renders `<h1>Hello</h1>` directly — instead:
111
+
112
+ ```tsx
113
+ // WRONG ❌
114
+ <h1>{settings.title}</h1>;
115
+
116
+ // CORRECT ✅
117
+ const titleComp = components.find((c) => c.slot === "section-title");
118
+ {
119
+ titleComp && (
120
+ <ComponentRenderer
121
+ instance={toComponentInstance(titleComp)}
122
+ sectionId={section.id}
123
+ isEditing={isEditing}
124
+ />
125
+ );
126
+ }
127
+ ```
128
+
129
+ - The editor manages component content — users edit text/images through the sidebar, not through section settings
130
+
131
+ ---
132
+
133
+ ## System Overview
134
+
135
+ A OneX theme is a collection of **sections** compiled into a browser-ready JS bundle. Themes are:
136
+
137
+ 1. Developed locally with `onexthm dev`
138
+ 2. Compiled with `onexthm build` (esbuild → ES module)
139
+ 3. Uploaded to S3 with `onexthm upload` (bundle.zip)
140
+ 4. Loaded by the storefront (customer-facing site) and editor (visual editor)
141
+
142
+ Both storefront and editor render the same bundle — sections must look identical in both.
143
+
144
+ ## Architecture
145
+
146
+ ```
147
+ Theme
148
+ ├── Sections Top-level page blocks (hero, features, pricing, footer)
149
+ │ ├── Blocks Nested containers inside sections (feature-item, pricing-tier)
150
+ │ └── Components Atomic UI rendered by @onexapis/core (heading, paragraph, button, icon, badge, divider, image)
151
+ └── Pages Page configs that list which sections to show
152
+ ```
153
+
154
+ **Key principle**: Sections define layout & structure. Components (from core) handle rendering text, images, buttons with inline styles. The section component orchestrates how core components are arranged.
155
+
156
+ ## File Structure
157
+
158
+ ```
159
+ my-theme/
160
+ ├── theme.config.ts # Colors, typography, spacing, breakpoints
161
+ ├── bundle-entry.ts # Build entry point (DO NOT import React here)
162
+ ├── sections-registry.ts # Lazy-loaded section imports
163
+ ├── theme.layout.ts # Header/footer section configuration
164
+ ├── package.json # @onexapis/core as dependency
165
+ ├── tsconfig.json # ES2020 target, react-jsx
166
+ ├── esbuild.config.js # Bundle → dist/bundle.mjs
167
+ ├── sections/
168
+ │ └── hero/
169
+ │ ├── hero-default.tsx # React component (the visual)
170
+ │ ├── hero.schema.ts # Schema (settings, fields, defaults)
171
+ │ └── index.ts # Re-exports component + schema
172
+ ├── pages/
173
+ │ └── home.ts # Page config with section instances
174
+ └── CLAUDE.md # This file
175
+ ```
176
+
177
+ ## Section Component Pattern
178
+
179
+ Every section component receives `SectionComponentProps` and uses core renderers:
180
+
181
+ ```tsx
182
+ "use client";
183
+
184
+ import type { SectionComponentProps } from "@onexapis/core/types";
185
+ import coreRenderers from "@onexapis/core/renderers";
186
+ import coreUtils from "@onexapis/core/utils";
187
+
188
+ const { ComponentRenderer, BlockRenderer } = coreRenderers;
189
+ const { toComponentInstance, getSectionValues, filterEnabledComponents } =
190
+ coreUtils;
191
+
192
+ export function MySection({
193
+ section,
194
+ schema,
195
+ isEditing,
196
+ data,
197
+ }: SectionComponentProps) {
198
+ const { settings } = getSectionValues(section, schema);
199
+ const { title, backgroundColor } = settings;
200
+
201
+ // Section-level components (title, subtitle, etc.)
202
+ const components = filterEnabledComponents(section.components || []);
203
+ const titleComp = components.find((c) => c.slot === "section-title");
204
+
205
+ // Nested blocks
206
+ const blocks = (section.blocks || []).filter((b) => b.enabled !== false);
207
+
208
+ return (
209
+ <section
210
+ className="py-16"
211
+ style={{ backgroundColor: String(backgroundColor || "#FFFFFF") }}
212
+ data-section-id={section.id}
213
+ data-section-type={section.type}
214
+ >
215
+ <div className="container mx-auto px-4 max-w-6xl">
216
+ {/* Render a core component (heading, paragraph, etc.) */}
217
+ {titleComp && (
218
+ <ComponentRenderer
219
+ instance={toComponentInstance(titleComp)}
220
+ sectionId={section.id}
221
+ isEditing={isEditing}
222
+ />
223
+ )}
224
+
225
+ {/* Render blocks (nested containers with their own components) */}
226
+ {blocks.map((block) => (
227
+ <div
228
+ key={block.id}
229
+ data-section-id={section.id}
230
+ data-block-id={block.id}
231
+ data-block-type={block.type}
232
+ >
233
+ <BlockRenderer
234
+ block={block}
235
+ sectionId={section.id}
236
+ isEditing={isEditing}
237
+ />
238
+ </div>
239
+ ))}
240
+
241
+ {/* Editor empty state */}
242
+ {blocks.length === 0 && isEditing && (
243
+ <div className="text-center py-12 border-2 border-dashed border-gray-300 rounded-lg">
244
+ <p className="text-gray-500">Add blocks to populate this section</p>
245
+ </div>
246
+ )}
247
+ </div>
248
+ </section>
249
+ );
250
+ }
251
+
252
+ export default MySection;
253
+ ```
254
+
255
+ ## Schema Definition Pattern
256
+
257
+ Every section needs a schema that defines its settings, templates, and defaults:
258
+
259
+ ```tsx
260
+ import type { SectionSchema } from "@onexapis/core/types";
261
+
262
+ export const mySchema: SectionSchema = {
263
+ type: "my-section",
264
+ name: "My Section",
265
+ description: "A custom section",
266
+ icon: "layout",
267
+ category: "content", // header | hero | content | features | testimonials | cta | gallery | pricing | faq | team | contact | footer | products | blog | newsletter
268
+ templates: [
269
+ { id: "default", name: "Default", isDefault: true },
270
+ { id: "minimal", name: "Minimal" },
271
+ ],
272
+ settings: [
273
+ // Text fields
274
+ { id: "title", type: "text", label: "Title", default: "Hello World" },
275
+ { id: "subtitle", type: "textarea", label: "Subtitle" },
276
+
277
+ // Numbers
278
+ { id: "columns", type: "number", label: "Columns", default: 3 },
279
+ { id: "limit", type: "range", label: "Items", default: 8, min: 1, max: 20 },
280
+
281
+ // Selection
282
+ {
283
+ id: "layout",
284
+ type: "select",
285
+ label: "Layout",
286
+ default: "grid",
287
+ options: [
288
+ { value: "grid", label: "Grid" },
289
+ { value: "list", label: "List" },
290
+ ],
291
+ },
292
+
293
+ // Boolean
294
+ { id: "showTitle", type: "checkbox", label: "Show Title", default: true },
295
+
296
+ // Colors — use hex directly (standard approach)
297
+ {
298
+ id: "backgroundColor",
299
+ type: "color",
300
+ label: "Background",
301
+ default: "#FFFFFF",
302
+ },
303
+ {
304
+ id: "textColor",
305
+ type: "color_token",
306
+ label: "Text Color",
307
+ default: "#1F2937",
308
+ },
309
+
310
+ // Images
311
+ { id: "image", type: "image", label: "Background Image" },
312
+ ],
313
+ defaults: {
314
+ title: "Hello World",
315
+ columns: 3,
316
+ backgroundColor: "#FFFFFF",
317
+ },
318
+ // Declare if this section needs commerce data for SSR
319
+ dataRequirements: {
320
+ products: false,
321
+ blogs: false,
322
+ settings: false,
323
+ },
324
+ };
325
+ ```
326
+
327
+ ## Available Field Types
328
+
329
+ | Type | Description | Example Use |
330
+ | ------------------------ | ------------------------------------------------- | ----------------------------- |
331
+ | `text` | Single-line text input | Title, label, button text |
332
+ | `textarea` | Multi-line text | Description, bio |
333
+ | `number` | Numeric input | Columns, count, limit |
334
+ | `range` | Slider with min/max | Opacity, spacing, items count |
335
+ | `checkbox` / `boolean` | Toggle switch | Show/hide elements |
336
+ | `select` | Dropdown selector | Layout variant, size |
337
+ | `radio` | Radio button group | Alignment, position |
338
+ | `color` | Color picker (hex + system tokens) | Any color |
339
+ | `color_token` | Color picker (tokens: Primary/Secondary + Custom) | Text color, icon color |
340
+ | `color_background` | Solid color or gradient | Section background |
341
+ | `image` / `image_picker` | Image upload/selection | Hero image, avatar |
342
+ | `video_url` | Video embed URL | Background video |
343
+ | `font` / `font_picker` | Font family selector | Section font override |
344
+ | `text_alignment` | Left / center / right | Text alignment |
345
+ | `richtext` | Rich text editor | Content blocks, articles |
346
+ | `inline_richtext` | Inline rich text | Short formatted text |
347
+ | `html` | Raw HTML/code editor | Custom embeds, scripts |
348
+ | `url` | URL input | Link href |
349
+ | `array` / `repeater` | List of repeating items | Feature list, social links |
350
+
351
+ ## Available Hooks
352
+
353
+ Import from `@onexapis/core/hooks`:
354
+
355
+ ```tsx
356
+ import {
357
+ useProducts,
358
+ useCart,
359
+ useAuth,
360
+ useDesignSystem,
361
+ } from "@onexapis/core/hooks";
362
+ ```
363
+
364
+ ### Commerce Data (React Query)
365
+
366
+ ```tsx
367
+ // Products
368
+ const { data, isLoading } = useProducts({ limit: 8, page: 0 });
369
+ const products = data?.data ?? [];
370
+
371
+ // Single product
372
+ const { data: product } = useProductBySlug("blue-t-shirt");
373
+
374
+ // Blogs
375
+ const { data, isLoading } = useBlogs({ limit: 6 });
376
+ const blogs = data?.data ?? [];
377
+
378
+ // Website settings
379
+ const { data: settings } = useSettings();
380
+ ```
381
+
382
+ ### State Management
383
+
384
+ ```tsx
385
+ // Shopping cart
386
+ const { items, addItem, removeItem, totalPrice, clearCart } = useCart();
387
+
388
+ // Authentication
389
+ const auth = useAuth();
390
+ await auth.login(email, password);
391
+ await auth.signup({ username, email, password, name });
392
+ auth.logout();
393
+ await auth.forgotPassword(email);
394
+
395
+ // Orders
396
+ const { createOrder, getOrders } = useOrders();
397
+ ```
398
+
399
+ ### Theme & Context
400
+
401
+ ```tsx
402
+ // Design system tokens
403
+ const { colors, typography, colorMode } = useDesignSystem();
404
+ // colors.primary, colors.secondary, colors.background, etc.
405
+
406
+ // Server-side data from SectionComponentProps.data
407
+ const { products, blogs, settings, company } = useCommerceData(data);
408
+
409
+ // Other contexts
410
+ const { mode, isDark } = useThemeMode();
411
+ const { locale, setLocale } = useLocale();
412
+ const { isMobile, isDesktop } = useViewport();
413
+ const motionTokens = useMotion();
414
+ ```
415
+
416
+ ## Color System
417
+
418
+ Colors in OneX themes work with **two approaches** (both are valid):
419
+
420
+ ### Approach 1: Direct hex colors (standard — used by simple, cool-store themes)
421
+
422
+ ```tsx
423
+ // In schema
424
+ { id: "backgroundColor", type: "color", label: "Background", default: "#3B82F6" }
425
+
426
+ // In component
427
+ <section style={{ backgroundColor: String(backgroundColor || "#FFFFFF") }}>
428
+ ```
429
+
430
+ Components store and render hex colors directly (`#3B82F6`). This is simple and works everywhere.
431
+
432
+ ### Approach 2: Design system tokens (optional — for theme-aware colors)
433
+
434
+ ```tsx
435
+ // In schema
436
+ { id: "textColor", type: "color_token", label: "Text Color", default: "token:primary" }
437
+
438
+ // Resolves to CSS variable: var(--theme-color-primary)
439
+ // Changes automatically when user updates Primary Color in theme settings
440
+ ```
441
+
442
+ Token values: `token:primary`, `token:secondary`, `token:accent`, `token:background`, `token:foreground`, `token:muted`
443
+ Custom values: `custom:#FF5733` or direct `#FF5733`
444
+
445
+ ## Data Attributes (Required for Editor)
446
+
447
+ Sections and blocks MUST include `data-` attributes so the editor's visual inspector can identify and select them:
448
+
449
+ ```tsx
450
+ // Section wrapper
451
+ <section
452
+ data-section-id={section.id} // REQUIRED
453
+ data-section-type={section.type} // REQUIRED
454
+ data-section-template="default" // Optional
455
+ data-section-name="My Section" // Optional
456
+ >
457
+
458
+ // Block wrapper
459
+ <div
460
+ data-section-id={section.id} // REQUIRED — parent section
461
+ data-block-id={block.id} // REQUIRED
462
+ data-block-type={block.type} // REQUIRED
463
+ >
464
+ ```
465
+
466
+ Without these, the editor cannot select sections/blocks for editing.
467
+
468
+ ## Built-in Components
469
+
470
+ 34 built-in components from `@onexapis/core` that render inside sections via `ComponentRenderer`:
471
+
472
+ | Component | Description | Key Settings |
473
+ | ------------------ | ------------------ | --------------------------------------------------------------------- |
474
+ | `heading` | H1-H6 text | text, tag, fontSize, fontWeight, colorToken, textAlign |
475
+ | `paragraph` | Body text | text, fontSize, color, lineHeight, textAlign |
476
+ | `button` | Clickable button | text, url, target, variant (default/outline/ghost/link), size |
477
+ | `icon` | Lucide icon | iconName, size (xs-2xl), color, strokeWidth, rotation |
478
+ | `badge` | Label/tag | text, variant (default/primary/success/warning/danger), size, rounded |
479
+ | `image` | Image display | src, alt, width, height, objectFit, borderRadius, aspectRatio |
480
+ | `video` | Video embed | videoType (youtube/vimeo/hosted), videoUrl, autoplay, loop |
481
+ | `divider` | Separator line | color, width, style (solid/dashed/dotted) |
482
+ | `quote` | Block quote | text, author, color, fontStyle |
483
+ | `alert` | Alert box | title, message, type (info/success/warning/error), dismissible |
484
+ | `progress` | Progress bar | value (0-100), label, color |
485
+ | `rating` | Star rating | value (1-5), readOnly, size |
486
+ | `social-links` | Social media links | Reads from WebsiteSettings context |
487
+ | `hotline-contacts` | Contact info | Reads from WebsiteSettings context |
488
+ | `company-info` | Company details | Reads from WebsiteSettings context |
489
+ | `product-card` | Product display | product (Product), showQuickAdd |
490
+ | `blog-card` | Blog post card | blog (Blog) |
491
+
492
+ Components are rendered via `ComponentRenderer` — you don't import them directly.
493
+
494
+ ## Component Slots
495
+
496
+ Components use `slot` to semantically identify their role in a section:
497
+
498
+ ```tsx
499
+ // In section component
500
+ const titleComp = components.find((c) => c.slot === "section-title");
501
+ const ctaButton = components.find((c) => c.slot === "cta-button");
502
+ const subtitle = components.find((c) => c.slot === "description");
503
+ ```
504
+
505
+ Common slot names: `section-title`, `section-subtitle`, `description`, `cta-button`, `secondary-cta`, `badge`, `icon`, `image`
506
+
507
+ ## Block System
508
+
509
+ Blocks are nested containers inside sections. They can contain components AND other blocks (recursive):
510
+
511
+ ```tsx
512
+ // Section → Blocks → Components
513
+ <section> // Section: "features"
514
+ <div> // Block: "features-container" (layout)
515
+ <div> // Block: "feature-item" (repeating card)
516
+ <ComponentRenderer ... /> // Component: icon
517
+ <ComponentRenderer ... /> // Component: heading
518
+ <ComponentRenderer ... /> // Component: paragraph
519
+ </div>
520
+ <div> // Block: "feature-item" (another card)
521
+ ...
522
+ </div>
523
+ </div>
524
+ </section>
525
+ ```
526
+
527
+ Use `BlockRenderer` to render blocks — it handles recursive nesting automatically.
528
+
529
+ ## Animation System
530
+
531
+ Sections, blocks, and components support animations via framer-motion:
532
+
533
+ ### Animation Types (22)
534
+
535
+ `fadeIn`, `fadeInUp`, `fadeInDown`, `fadeInLeft`, `fadeInRight`, `slideInUp`, `slideInDown`, `slideInLeft`, `slideInRight`, `scaleIn`, `scaleInUp`, `rotate`, `rotateIn`, `blur`, `bounce`, `pulse`, `shake`, `flip`, `typing`, `counter`, `parallax`, `none`
536
+
537
+ ### Animation Triggers
538
+
539
+ - `onLoad` — animate immediately on mount
540
+ - `onScroll` — animate when section enters viewport (default, recommended)
541
+ - `onHover` — animate on hover
542
+ - `onClick` — animate on click
543
+
544
+ ### Adding Animation to Schema
545
+
546
+ ```tsx
547
+ // In schema settings
548
+ { id: "animationType", type: "select", label: "Animation",
549
+ default: "none",
550
+ options: [
551
+ { value: "none", label: "None" },
552
+ { value: "fadeInUp", label: "Fade In Up" },
553
+ { value: "scaleIn", label: "Scale In" },
554
+ ]
555
+ },
556
+ { id: "animationTrigger", type: "select", label: "Trigger",
557
+ default: "onScroll",
558
+ options: [
559
+ { value: "onScroll", label: "On Scroll" },
560
+ { value: "onLoad", label: "On Load" },
561
+ ]
562
+ },
563
+ ```
564
+
565
+ Animations are applied automatically by `SectionRenderer` and `BlockRenderer` via `AnimationWrapper`.
566
+
567
+ ### Motion Tokens
568
+
569
+ Themes can customize animation feel via `MotionTokens`:
570
+
571
+ - `duration`: instant(0.1s), quick(0.2s), moderate(0.4s), deliberate(0.6s), slow(0.8s)
572
+ - `easing`: standard, entrance, exit, expressive (cubic-bezier)
573
+ - `intensity`: 0 (no motion/accessibility) to 1 (full motion)
574
+ - `staggerDelay`: delay between child animations (default 0.1s)
575
+
576
+ Access via `useMotion()` hook from `@onexapis/core/hooks`.
577
+
578
+ ## Data Requirements (SSR)
579
+
580
+ Sections can declare what data they need for server-side rendering:
581
+
582
+ ```tsx
583
+ // In schema
584
+ dataRequirements: {
585
+ products: true, // Section needs product data
586
+ blogs: false, // No blog data needed
587
+ settings: true, // Needs website settings
588
+ }
589
+ ```
590
+
591
+ When `dataRequirements.products = true`:
592
+
593
+ - Server pre-fetches products before rendering
594
+ - Data is passed via `SectionComponentProps.data`
595
+ - Use `useCommerceData(data)` to access it
596
+ - Falls back to client-side `useProducts()` if not pre-fetched
597
+
598
+ ## Product & Blog Types
599
+
600
+ ### Product
601
+
602
+ ```tsx
603
+ {
604
+ id, title, slug, category, categoryLabel,
605
+ image: { url, alt },
606
+ images: [{ url, alt }],
607
+ salePrice, originalPrice, discount,
608
+ inStock: boolean,
609
+ badge: "hot" | "new" | "sale" | null,
610
+ rating, reviewCount,
611
+ variants: [{ sku, options, prices }],
612
+ tags: string[],
613
+ }
614
+ ```
615
+
616
+ ### Blog
617
+
618
+ ```tsx
619
+ {
620
+ id, title, slug, description, excerpt,
621
+ content: string (HTML, detail only),
622
+ image, coverImage,
623
+ category, categoryLabel,
624
+ author: { name, avatar },
625
+ createdAt, publishedAt,
626
+ tags: string[],
627
+ locale,
628
+ }
629
+ ```
630
+
631
+ ### WebsiteSettings
632
+
633
+ ```tsx
634
+ {
635
+ social_connections: [{ name, icon, link, platform, enabled }],
636
+ hotline_connections: [{ name, type, value, enabled }],
637
+ company_info: { name, tagline, description, logo_url },
638
+ contact_email: { email, enabled },
639
+ }
640
+ ```
641
+
642
+ ## Context Provider Hierarchy
643
+
644
+ The rendering tree wraps sections in multiple context providers:
645
+
646
+ ```
647
+ PageDataProvider (products, blogs, settings, company)
648
+ → MotionProvider (animation tokens)
649
+ → ViewportProvider (isEditing, viewportMode)
650
+ → LocaleProvider (locale, supportedLocales)
651
+ → ThemeModeProvider (light/dark mode)
652
+ → CartProvider (shopping cart)
653
+ → SectionListRenderer
654
+ → SectionRenderer (per section)
655
+ → AnimationWrapper
656
+ → Your Section Component
657
+ ```
658
+
659
+ All contexts are accessible via hooks from `@onexapis/core/hooks`.
660
+
661
+ ## Editor Integration
662
+
663
+ When `isEditing = true` (section is rendered in the editor preview):
664
+
665
+ ```tsx
666
+ export function MySection({ section, isEditing }: SectionComponentProps) {
667
+ return (
668
+ <section>
669
+ {/* Show empty state placeholder in editor */}
670
+ {items.length === 0 && isEditing && (
671
+ <div className="border-2 border-dashed border-gray-300 p-8 text-center text-gray-500">
672
+ Add items to this section
673
+ </div>
674
+ )}
675
+
676
+ {/* Disable navigation in editor */}
677
+ {isEditing ? (
678
+ <span className="cursor-default">{linkText}</span>
679
+ ) : (
680
+ <a href={url}>{linkText}</a>
681
+ )}
682
+ </section>
683
+ );
684
+ }
685
+ ```
686
+
687
+ ## Dark/Light Mode
688
+
689
+ Themes support light, dark, and system color modes via `useThemeMode()`:
690
+
691
+ ```tsx
692
+ import { useThemeMode } from "@onexapis/core/hooks";
693
+
694
+ export function MySection({ section }: SectionComponentProps) {
695
+ const { mode, isDark } = useThemeMode();
696
+ // mode: "light" | "dark" | "system"
697
+ // isDark: boolean (resolved — accounts for system preference)
698
+
699
+ return (
700
+ <section
701
+ style={{
702
+ backgroundColor: isDark ? "#1F2937" : "#FFFFFF",
703
+ color: isDark ? "#F9FAFB" : "#111827",
704
+ }}
705
+ >
706
+ {/* Content adapts to color mode */}
707
+ </section>
708
+ );
709
+ }
710
+ ```
711
+
712
+ ### How dark mode works
713
+
714
+ - The `ThemeModeProvider` at the app root sets a `dark` class on the HTML element
715
+ - Tailwind's `dark:` variants work automatically: `className="bg-white dark:bg-gray-900"`
716
+ - `useThemeMode()` gives you the current mode for JS-driven styling
717
+ - The design system's `colorMode` field determines the default mode
718
+ - Users can toggle via the editor's theme settings
719
+
720
+ ### Best practices
721
+
722
+ - Use Tailwind's `dark:` variant for simple color swaps: `text-gray-900 dark:text-white`
723
+ - Use `useThemeMode()` for complex conditional logic
724
+ - Test sections in both modes — don't assume light mode only
725
+ - CSS variables (`--background`, `--foreground`) auto-switch with dark mode
726
+
727
+ ## Locale / i18n
728
+
729
+ Themes support multiple languages via `useLocale()`:
730
+
731
+ ```tsx
732
+ import { useLocale } from "@onexapis/core/hooks";
733
+
734
+ export function MySection({ section }: SectionComponentProps) {
735
+ const { locale, defaultLocale, supportedLocales } = useLocale();
736
+ // locale: "vi" | "en" | ...
737
+ // defaultLocale: "vi"
738
+ // supportedLocales: ["vi", "en"]
739
+
740
+ return (
741
+ <section>{locale === "vi" ? <h1>Xin chào</h1> : <h1>Hello</h1>}</section>
742
+ );
743
+ }
744
+ ```
745
+
746
+ ### How i18n works
747
+
748
+ - The default locale is `"vi"` (Vietnamese)
749
+ - Supported locales: `["vi", "en"]`
750
+ - Page content is stored per-locale in the database
751
+ - The API supports `?locale=en` query parameter to get translated content
752
+ - Translation overrides are stored as diffs from the base (default locale) page
753
+ - The `LocaleProvider` at the app root provides the current locale via context
754
+
755
+ ### Best practices
756
+
757
+ - Section components usually DON'T need i18n logic — the content comes from the editor (already translated)
758
+ - The `ComponentRenderer` renders text from `component.content.text` which is locale-specific
759
+ - Only use `useLocale()` if you need locale-aware formatting (dates, numbers, currency)
760
+ - Use `toLocaleString()` for numbers: `price.toLocaleString("vi-VN")`
761
+ - Use `Intl.DateTimeFormat` for dates: `new Intl.DateTimeFormat(locale).format(date)`
762
+
763
+ ## Section Registry
764
+
765
+ Every section must be registered in `sections-registry.ts` with lazy imports:
766
+
767
+ ```tsx
768
+ export const sectionsRegistry = {
769
+ hero: () => import("./sections/hero"),
770
+ features: () => import("./sections/features"),
771
+ pricing: () => import("./sections/pricing"),
772
+ // Add new sections here
773
+ };
774
+ ```
775
+
776
+ The build system uses this registry to code-split sections into the bundle.
777
+
778
+ ## Rules
779
+
780
+ ### DO
781
+
782
+ - Use `ComponentRenderer` / `BlockRenderer` from `@onexapis/core/renderers` for nested content
783
+ - Use `getSectionValues(section, schema)` to extract typed settings
784
+ - Use `filterEnabledComponents()` to skip disabled components
785
+ - Use `toComponentInstance()` to convert raw component data for ComponentRenderer
786
+ - Include `data-section-id`, `data-block-id` attributes on wrapper elements
787
+ - Handle the `isEditing` prop (show placeholders, disable navigation links)
788
+ - Use `"use client"` directive at the top of section components
789
+ - Use Tailwind CSS classes for layout (included in the build)
790
+ - Declare `dataRequirements` in schema if section needs products/blogs/settings
791
+
792
+ ### DON'T
793
+
794
+ - Don't import from `@onexapis/core/internal` — it's a private API
795
+ - Don't use `document` or `window` without checking `typeof window !== "undefined"`
796
+ - Don't hardcode API URLs — use hooks (`useProducts`, `useBlogs`)
797
+ - Don't use `useEffect` for data fetching — use React Query hooks instead
798
+ - Don't mutate `section.settings` directly — use the `onSettingsChange` callback
799
+ - Don't use `require()` — themes are ES modules
800
+ - Don't import React explicitly — it's auto-injected by the JSX transform
801
+
802
+ ## CLI Commands
803
+
804
+ ```bash
805
+ onexthm init my-theme # Scaffold new theme project
806
+ onexthm create:section hero # Create section (component + schema + index)
807
+ onexthm create:block card # Create block
808
+ onexthm create:component badge # Create component
809
+ onexthm list # List all sections/blocks/components
810
+ onexthm validate # Validate theme structure
811
+ onexthm dev # Start dev server with live preview
812
+ onexthm build # Compile theme for production
813
+ onexthm upload --theme my-theme # Upload bundle.zip to S3
814
+ onexthm clone simple # Clone an existing theme from S3
815
+ onexthm download -t simple # Download compiled theme
816
+ onexthm config # Configure AWS/API credentials
817
+ ```
818
+
819
+ ## Example: Product Grid Section
820
+
821
+ ```tsx
822
+ "use client";
823
+
824
+ import type { SectionComponentProps } from "@onexapis/core/types";
825
+ import { useProducts, useCart } from "@onexapis/core/hooks";
826
+ import coreUtils from "@onexapis/core/utils";
827
+
828
+ const { getSectionValues } = coreUtils;
829
+
830
+ export function ProductGrid({
831
+ section,
832
+ schema,
833
+ isEditing,
834
+ }: SectionComponentProps) {
835
+ const { settings } = getSectionValues(section, schema);
836
+ const limit = Number(settings.limit) || 8;
837
+ const columns = Number(settings.columns) || 3;
838
+
839
+ const { data, isLoading } = useProducts({ limit });
840
+ const { addItem } = useCart();
841
+ const products = data?.data ?? [];
842
+
843
+ if (isLoading) {
844
+ return (
845
+ <section
846
+ data-section-id={section.id}
847
+ data-section-type="product-grid"
848
+ className="py-16"
849
+ >
850
+ <div className="container mx-auto px-4">
851
+ <div className="animate-pulse grid grid-cols-3 gap-4">
852
+ {Array.from({ length: limit }).map((_, i) => (
853
+ <div key={i} className="h-64 bg-gray-200 rounded-lg" />
854
+ ))}
855
+ </div>
856
+ </div>
857
+ </section>
858
+ );
859
+ }
860
+
861
+ const colClass =
862
+ columns === 2
863
+ ? "md:grid-cols-2"
864
+ : columns === 4
865
+ ? "md:grid-cols-4"
866
+ : "md:grid-cols-3";
867
+
868
+ return (
869
+ <section
870
+ data-section-id={section.id}
871
+ data-section-type="product-grid"
872
+ className="py-16"
873
+ style={{ backgroundColor: String(settings.backgroundColor || "#FFFFFF") }}
874
+ >
875
+ <div className="container mx-auto px-4 max-w-6xl">
876
+ <div className={`grid grid-cols-1 ${colClass} gap-6`}>
877
+ {products.map((product) => (
878
+ <div
879
+ key={product.id}
880
+ className="border rounded-lg p-4 hover:shadow-md transition-shadow"
881
+ >
882
+ {product.image?.url && (
883
+ <img
884
+ src={product.image.url}
885
+ alt={product.name}
886
+ className="w-full h-48 object-cover rounded mb-3"
887
+ />
888
+ )}
889
+ <h3 className="font-semibold text-lg">{product.name}</h3>
890
+ <p className="text-gray-600 mt-1">
891
+ {product.price?.toLocaleString()}đ
892
+ </p>
893
+ {!isEditing && (
894
+ <button
895
+ onClick={() => addItem(product, 1)}
896
+ className="mt-3 w-full py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
897
+ >
898
+ Add to Cart
899
+ </button>
900
+ )}
901
+ </div>
902
+ ))}
903
+ </div>
904
+ </div>
905
+ </section>
906
+ );
907
+ }
908
+
909
+ export default ProductGrid;
910
+ ```
911
+
912
+ ## MCP Servers
913
+
914
+ This project has TWO separate MCP servers. Do NOT confuse them:
915
+
916
+ ### `onexthm` MCP (Theme Development) — USE THIS
917
+
918
+ Registered in `.mcp.json` in this project. Provides theme-specific tools:
919
+
920
+ - `onexthm_create_section` — Generate section files (component + schema + index)
921
+ - `onexthm_validate` — Validate theme structure
922
+ - `onexthm_list_hooks` — List available hooks with examples
923
+ - `onexthm_generate_schema` — Generate schema from natural language
924
+
925
+ ### `onex-platform` MCP (Backend Services) — DO NOT USE FOR THEMES
926
+
927
+ This is for managing microservices on the OneXEOS platform. Its tools are:
928
+
929
+ - `onex_invoke`, `onex_status`, `onex_deploy`, `onex_logs`, `onex_test`
930
+ - These manage backend services (auth, product, order, etc.)
931
+ - **Do NOT use these for theme development** — they have nothing to do with sections, schemas, or theme components
932
+
933
+ ### When to use which
934
+
935
+ | Task | MCP to use |
936
+ | ------------------------ | ---------------------------- |
937
+ | Create a new section | `onexthm_create_section` |
938
+ | Validate theme structure | `onexthm_validate` |
939
+ | Look up available hooks | `onexthm_list_hooks` |
940
+ | Deploy a backend service | `onex_deploy` (platform MCP) |
941
+ | Check service health | `onex_status` (platform MCP) |