@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.
- package/README.md +82 -16
- package/dist/cli.js +522 -286
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +519 -283
- package/dist/cli.mjs.map +1 -1
- package/dist/index.js +47 -270
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +47 -270
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/default/.env.example +1 -1
- package/templates/default/.mcp.json +8 -0
- package/templates/default/CLAUDE.md +941 -0
- package/templates/default/bundle-entry.ts +18 -0
- package/templates/default/index.ts +26 -0
- package/templates/default/package.json +37 -0
- package/templates/default/pages/about.ts +66 -0
- package/templates/default/pages/home.ts +93 -0
- package/templates/default/pages/showcase.ts +146 -0
- package/templates/default/sections/about/about-default.tsx +237 -0
- package/templates/default/sections/about/about.schema.ts +259 -0
- package/templates/default/sections/about/index.ts +15 -0
- package/templates/default/sections/cta/cta-default.tsx +180 -0
- package/templates/default/sections/cta/cta.schema.ts +210 -0
- package/templates/default/sections/cta/index.ts +11 -0
- package/templates/default/sections/features/features-default.tsx +154 -0
- package/templates/default/sections/features/features.schema.ts +330 -0
- package/templates/default/sections/features/index.ts +11 -0
- package/templates/default/sections/gallery/gallery-default.tsx +134 -0
- package/templates/default/sections/gallery/gallery.schema.ts +397 -0
- package/templates/default/sections/gallery/index.ts +11 -0
- package/templates/default/sections/hero/hero-default.tsx +212 -0
- package/templates/default/sections/hero/hero.schema.ts +273 -0
- package/templates/default/sections/hero/index.ts +15 -0
- package/templates/default/sections/stats/index.ts +11 -0
- package/templates/default/sections/stats/stats-default.tsx +103 -0
- package/templates/default/sections/stats/stats.schema.ts +266 -0
- package/templates/default/sections/testimonials/index.ts +11 -0
- package/templates/default/sections/testimonials/testimonials-default.tsx +130 -0
- package/templates/default/sections/testimonials/testimonials.schema.ts +371 -0
- package/templates/default/sections-registry.ts +32 -0
- package/templates/default/theme.config.ts +107 -0
- package/templates/default/theme.layout.ts +21 -0
- package/templates/default/tsconfig.json +16 -7
- package/templates/default/README.md.ejs +0 -129
- package/templates/default/esbuild.config.js +0 -81
- package/templates/default/package.json.ejs +0 -31
- package/templates/default/src/config.ts.ejs +0 -98
- package/templates/default/src/index.ts.ejs +0 -11
- package/templates/default/src/layout.ts +0 -23
- package/templates/default/src/manifest.ts.ejs +0 -47
- package/templates/default/src/pages/home.ts.ejs +0 -37
- package/templates/default/src/sections/footer/footer-default.tsx +0 -28
- package/templates/default/src/sections/footer/footer.schema.ts +0 -45
- package/templates/default/src/sections/footer/index.ts +0 -2
- package/templates/default/src/sections/header/header-default.tsx +0 -61
- package/templates/default/src/sections/header/header.schema.ts +0 -46
- package/templates/default/src/sections/header/index.ts +0 -2
- package/templates/default/src/sections/hero/hero-default.tsx +0 -52
- package/templates/default/src/sections/hero/hero.schema.ts +0 -52
- 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) |
|