@onexapis/cli 1.1.27 → 1.1.29
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/dist/preview/preview-app.tsx +121 -14
- package/package.json +2 -2
- package/templates/default/.mcp.json +6 -1
- package/templates/default/CLAUDE.md +225 -11
|
@@ -13,7 +13,6 @@ import * as coreContexts from "@onexapis/core/contexts";
|
|
|
13
13
|
import * as coreComponents from "@onexapis/core/components";
|
|
14
14
|
import * as coreRegistry from "@onexapis/core/registry";
|
|
15
15
|
import * as coreCommerce from "@onexapis/core/commerce";
|
|
16
|
-
import * as coreHooks from "@onexapis/core/hooks";
|
|
17
16
|
import * as coreCommerceHooks from "@onexapis/core/commerce/hooks";
|
|
18
17
|
import * as coreAuth from "@onexapis/core/auth";
|
|
19
18
|
import * as coreCart from "@onexapis/core/cart";
|
|
@@ -23,9 +22,26 @@ import * as coreBlog from "@onexapis/core/blog";
|
|
|
23
22
|
import * as coreFinance from "@onexapis/core/finance";
|
|
24
23
|
import * as coreInternal from "@onexapis/core/internal";
|
|
25
24
|
import * as coreTypes from "@onexapis/core/types";
|
|
26
|
-
import { CartProvider } from "@onexapis/core/contexts";
|
|
25
|
+
import { CartProvider, FlyToCartProvider } from "@onexapis/core/contexts";
|
|
27
26
|
import { LocaleProvider } from "@onexapis/core/contexts";
|
|
28
27
|
|
|
28
|
+
// Compose @onexapis/core/hooks from published subpaths
|
|
29
|
+
// Only include use* hook functions — spreading entire modules includes
|
|
30
|
+
// Zustand stores and React providers that cause "selector is not a function" errors.
|
|
31
|
+
const coreHooks = {
|
|
32
|
+
...coreCommerceHooks,
|
|
33
|
+
};
|
|
34
|
+
// Add context/state hooks individually (avoid spreading full modules)
|
|
35
|
+
for (const mod of [coreContexts, coreAuth, coreOrders, coreCart] as any[]) {
|
|
36
|
+
if (mod && typeof mod === "object") {
|
|
37
|
+
for (const [k, v] of Object.entries(mod)) {
|
|
38
|
+
if (typeof v === "function" && k.startsWith("use") && !(k in coreHooks)) {
|
|
39
|
+
(coreHooks as any)[k] = v;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
// Set React globals
|
|
30
46
|
(globalThis as any).__ONEX_REACT__ = React;
|
|
31
47
|
(globalThis as any).__ONEX_REACT_DOM__ = { createRoot };
|
|
@@ -337,24 +353,91 @@ function discoverPageConfigs(
|
|
|
337
353
|
|
|
338
354
|
// ===== 8. PREVIEW APP COMPONENT =====
|
|
339
355
|
|
|
356
|
+
/**
|
|
357
|
+
* Match a URL path against a route pattern with [param] segments.
|
|
358
|
+
* Returns extracted params if matched, null otherwise.
|
|
359
|
+
* E.g. matchDynamicPath("/products/[slug]", "/products/my-product") → { slug: "my-product" }
|
|
360
|
+
*/
|
|
361
|
+
function matchDynamicPath(
|
|
362
|
+
pattern: string,
|
|
363
|
+
pathname: string
|
|
364
|
+
): Record<string, string> | null {
|
|
365
|
+
const patternParts = pattern.replace(/^\/+|\/+$/g, "").split("/");
|
|
366
|
+
const pathParts = pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
367
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
368
|
+
const params: Record<string, string> = {};
|
|
369
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
370
|
+
const segMatch = patternParts[i].match(/^\[(\w+)\]$/);
|
|
371
|
+
if (segMatch) {
|
|
372
|
+
params[segMatch[1]] = pathParts[i];
|
|
373
|
+
} else if (patternParts[i].toLowerCase() !== pathParts[i].toLowerCase()) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return params;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const PREVIEW_SUPPORTED_LOCALES = ["vi", "en"];
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Extract locale prefix from URL pathname.
|
|
384
|
+
* "/vi/san-pham/slug" → { locale: "vi", rest: "/san-pham/slug" }
|
|
385
|
+
* "/products/slug" → { locale: "en", rest: "/products/slug" }
|
|
386
|
+
*/
|
|
387
|
+
function extractLocaleFromUrl(pathname: string): { locale: string; rest: string } {
|
|
388
|
+
const parts = pathname.replace(/^\/+/, "").split("/");
|
|
389
|
+
if (parts[0] && PREVIEW_SUPPORTED_LOCALES.includes(parts[0])) {
|
|
390
|
+
return { locale: parts[0], rest: "/" + parts.slice(1).join("/") };
|
|
391
|
+
}
|
|
392
|
+
return { locale: "en", rest: pathname };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get the effective path for a page in a given locale.
|
|
397
|
+
* Uses localizedPaths[locale] if available, falls back to path.
|
|
398
|
+
*/
|
|
399
|
+
function getPagePathForLocale(config: any, locale: string): string | undefined {
|
|
400
|
+
return config?.localizedPaths?.[locale] || config?.path;
|
|
401
|
+
}
|
|
402
|
+
|
|
340
403
|
/**
|
|
341
404
|
* Match URL pathname to a page index.
|
|
342
|
-
*
|
|
405
|
+
* Supports: static pages, dynamic routes, locale-prefixed URLs, and localizedPaths.
|
|
343
406
|
*/
|
|
344
407
|
function getInitialPageFromURL(
|
|
345
|
-
pages: Array<{ key: string; label: string; config: any }
|
|
408
|
+
pages: Array<{ key: string; label: string; config: any }>,
|
|
409
|
+
locale: string,
|
|
410
|
+
restPath: string
|
|
346
411
|
): number {
|
|
347
|
-
const pathname =
|
|
348
|
-
|
|
349
|
-
.toLowerCase();
|
|
350
|
-
if (!pathname || pathname === "home") return 0; // home is always first after sorting
|
|
412
|
+
const pathname = restPath.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
413
|
+
if (!pathname || pathname === "home") return 0;
|
|
351
414
|
|
|
352
|
-
|
|
415
|
+
// 1. Try exact static match (by label or key)
|
|
416
|
+
let idx = pages.findIndex((p) => {
|
|
353
417
|
const pageSlug = p.label.toLowerCase().replace(/\s+/g, "-");
|
|
354
418
|
const pageKey = p.key.replace("PageConfig", "").toLowerCase();
|
|
355
419
|
return pageSlug === pathname || pageKey === pathname;
|
|
356
420
|
});
|
|
357
|
-
|
|
421
|
+
if (idx >= 0) return idx;
|
|
422
|
+
|
|
423
|
+
// 2. Try matching against localizedPaths[locale] or fallback path
|
|
424
|
+
for (let i = 0; i < pages.length; i++) {
|
|
425
|
+
const config = pages[i].config;
|
|
426
|
+
const effectivePath = getPagePathForLocale(config, locale);
|
|
427
|
+
if (!effectivePath) continue;
|
|
428
|
+
|
|
429
|
+
// Static locale path match (e.g., /gioi-thieu matches localizedPaths.vi: "/gioi-thieu")
|
|
430
|
+
const normalizedEffective = effectivePath.replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
431
|
+
if (normalizedEffective === pathname) return i;
|
|
432
|
+
|
|
433
|
+
// Dynamic locale path match (e.g., /san-pham/my-slug matches localizedPaths.vi: "/san-pham/[slug]")
|
|
434
|
+
if (config?.isDynamic) {
|
|
435
|
+
const params = matchDynamicPath(effectivePath, `/${pathname}`);
|
|
436
|
+
if (params) return i;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return -2; // page not found
|
|
358
441
|
}
|
|
359
442
|
|
|
360
443
|
function PreviewApp() {
|
|
@@ -459,9 +542,16 @@ function PreviewApp() {
|
|
|
459
542
|
const pages = discoverPageConfigs(themeExports);
|
|
460
543
|
const sectionRegistry = buildSectionRegistry(themeExports);
|
|
461
544
|
|
|
545
|
+
// Extract locale from URL (e.g., /vi/san-pham/slug → locale="vi", rest="/san-pham/slug")
|
|
546
|
+
const { locale: urlLocale, rest: urlRestPath } = extractLocaleFromUrl(
|
|
547
|
+
window.location.pathname
|
|
548
|
+
);
|
|
549
|
+
|
|
462
550
|
// Resolve initial page from URL path (once pages are discovered)
|
|
463
551
|
const resolvedPage =
|
|
464
|
-
selectedPage === -1
|
|
552
|
+
selectedPage === -1
|
|
553
|
+
? getInitialPageFromURL(pages, urlLocale, urlRestPath)
|
|
554
|
+
: selectedPage;
|
|
465
555
|
|
|
466
556
|
// Page not found (-2)
|
|
467
557
|
if (resolvedPage === -2) {
|
|
@@ -538,13 +628,25 @@ function PreviewApp() {
|
|
|
538
628
|
);
|
|
539
629
|
}
|
|
540
630
|
|
|
541
|
-
//
|
|
631
|
+
// Extract route params for dynamic pages using locale-aware path
|
|
632
|
+
let routeParams: Record<string, string> = {};
|
|
633
|
+
if (currentPage.config?.isDynamic) {
|
|
634
|
+
const effectivePath = getPagePathForLocale(currentPage.config, urlLocale);
|
|
635
|
+
if (effectivePath) {
|
|
636
|
+
const params = matchDynamicPath(effectivePath, urlRestPath);
|
|
637
|
+
if (params) routeParams = params;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Build section data
|
|
542
642
|
const sectionData = {
|
|
543
643
|
theme: themeExports.themeConfig || {},
|
|
544
644
|
page: currentPage.config,
|
|
545
645
|
products: [],
|
|
546
646
|
blogs: [],
|
|
547
647
|
settings: {},
|
|
648
|
+
routeParams,
|
|
649
|
+
locale: urlLocale,
|
|
548
650
|
};
|
|
549
651
|
|
|
550
652
|
// Resolve layout sections (Gap 1)
|
|
@@ -630,12 +732,17 @@ const queryClient = new QueryClient({
|
|
|
630
732
|
},
|
|
631
733
|
});
|
|
632
734
|
|
|
735
|
+
// Detect locale from URL for the root provider
|
|
736
|
+
const _rootLocale = extractLocaleFromUrl(window.location.pathname).locale;
|
|
737
|
+
|
|
633
738
|
const root = createRoot(document.getElementById("onex-preview-root")!);
|
|
634
739
|
root.render(
|
|
635
740
|
<QueryClientProvider client={queryClient}>
|
|
636
|
-
<LocaleProvider locale=
|
|
741
|
+
<LocaleProvider locale={_rootLocale as any}>
|
|
637
742
|
<CartProvider>
|
|
638
|
-
<
|
|
743
|
+
<FlyToCartProvider>
|
|
744
|
+
<PreviewApp />
|
|
745
|
+
</FlyToCartProvider>
|
|
639
746
|
</CartProvider>
|
|
640
747
|
</LocaleProvider>
|
|
641
748
|
</QueryClientProvider>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onexapis/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.29",
|
|
4
4
|
"description": "CLI tool for OneX theme development - scaffolds themes using @onexapis/core",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@aws-sdk/client-s3": "^3.470.0",
|
|
53
|
-
"@onexapis/core": "
|
|
53
|
+
"@onexapis/core": "workspace:*",
|
|
54
54
|
"@tanstack/react-query": "^5.90.16",
|
|
55
55
|
"adm-zip": "^0.5.16",
|
|
56
56
|
"archiver": "^7.0.1",
|
|
@@ -595,6 +595,98 @@ When `dataRequirements.products = true`:
|
|
|
595
595
|
- Use `useCommerceData(data)` to access it
|
|
596
596
|
- Falls back to client-side `useProducts()` if not pre-fetched
|
|
597
597
|
|
|
598
|
+
## Dynamic Pages (Product Detail, Blog Detail)
|
|
599
|
+
|
|
600
|
+
Dynamic pages use URL parameters like `/products/[slug]` to render detail views.
|
|
601
|
+
|
|
602
|
+
### Define a dynamic page in `pages/`
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
// pages/product-detail.ts
|
|
606
|
+
import type { PageConfig } from "@onexapis/core/types";
|
|
607
|
+
|
|
608
|
+
export const productDetailPageConfig: Omit<PageConfig, "id" | "createdAt" | "updatedAt"> = {
|
|
609
|
+
title: "Product Detail",
|
|
610
|
+
handle: "product-detail",
|
|
611
|
+
path: "/products/[slug]", // Dynamic segment in brackets
|
|
612
|
+
isDynamic: true, // Flag as dynamic
|
|
613
|
+
dynamicSegments: ["slug"], // Parameter names
|
|
614
|
+
type: "product",
|
|
615
|
+
renderMode: "sections",
|
|
616
|
+
themeId: "my-theme",
|
|
617
|
+
editable: true,
|
|
618
|
+
published: true,
|
|
619
|
+
sections: [
|
|
620
|
+
{
|
|
621
|
+
id: "product-detail-1",
|
|
622
|
+
type: "my-product-detail",
|
|
623
|
+
template: "default",
|
|
624
|
+
order: 0,
|
|
625
|
+
enabled: true,
|
|
626
|
+
settings: {},
|
|
627
|
+
components: [],
|
|
628
|
+
blocks: [],
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
};
|
|
632
|
+
export default productDetailPageConfig;
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Export in `bundle-entry.ts`:
|
|
636
|
+
```typescript
|
|
637
|
+
export { default as productDetailPageConfig } from "./pages/product-detail";
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Access route params in sections
|
|
641
|
+
|
|
642
|
+
Sections receive `data.routeParams` with the extracted URL parameters:
|
|
643
|
+
|
|
644
|
+
```tsx
|
|
645
|
+
export function ProductDetail({ section, schema, isEditing, data }: SectionComponentProps) {
|
|
646
|
+
const routeParams = (data?.routeParams || {}) as Record<string, string>;
|
|
647
|
+
const slug = routeParams.slug || ""; // "blue-shirt" from /products/blue-shirt
|
|
648
|
+
|
|
649
|
+
const { data: product, isLoading } = useProductBySlug(slug, !!slug);
|
|
650
|
+
// ...
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Locale-aware paths
|
|
655
|
+
|
|
656
|
+
Use `localizedPaths` to define different URL slugs per locale:
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
export const productDetailPageConfig = {
|
|
660
|
+
handle: "product-detail",
|
|
661
|
+
path: "/products/[slug]", // Default/fallback
|
|
662
|
+
isDynamic: true,
|
|
663
|
+
dynamicSegments: ["slug"],
|
|
664
|
+
localizedPaths: { // Locale-specific paths (typed by Locale enum)
|
|
665
|
+
vi: "/san-pham/[slug]", // /vi/san-pham/blue-shirt
|
|
666
|
+
en: "/products/[slug]", // /en/products/blue-shirt
|
|
667
|
+
},
|
|
668
|
+
type: "product",
|
|
669
|
+
// ...
|
|
670
|
+
};
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
The `Locale` type (`"vi" | "en"`) is from `@onexapis/core/types` — gives autocomplete.
|
|
674
|
+
|
|
675
|
+
Sections also receive `data.locale` to know the current locale:
|
|
676
|
+
```tsx
|
|
677
|
+
const locale = data?.locale as string; // "vi" or "en"
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Key conventions
|
|
681
|
+
|
|
682
|
+
- `path` uses bracket syntax: `/products/[slug]`, `/blog/[slug]`
|
|
683
|
+
- `localizedPaths` maps `Locale` → path for locale-specific URLs
|
|
684
|
+
- `dynamicSegments` lists parameter names extracted from the path
|
|
685
|
+
- `type: "product"` or `"blog"` indicates what kind of detail page
|
|
686
|
+
- Sections access params via `data.routeParams.slug` (or whatever parameter name)
|
|
687
|
+
- Sections access locale via `data.locale` or `useLocale()` hook
|
|
688
|
+
- Use `useProductBySlug()` / `useBlogBySlug()` hooks to fetch detail data
|
|
689
|
+
|
|
598
690
|
## Product & Blog Types
|
|
599
691
|
|
|
600
692
|
### Product
|
|
@@ -909,6 +1001,123 @@ export function ProductGrid({
|
|
|
909
1001
|
export default ProductGrid;
|
|
910
1002
|
```
|
|
911
1003
|
|
|
1004
|
+
## Cart & Fly-to-Cart Animation
|
|
1005
|
+
|
|
1006
|
+
The cart system uses `useCart()` for state and a flexible fly-to-cart animation triggered by `data-fly-to-cart-target`.
|
|
1007
|
+
|
|
1008
|
+
### Cart State
|
|
1009
|
+
|
|
1010
|
+
```tsx
|
|
1011
|
+
import { useCart } from "@onexapis/core/hooks";
|
|
1012
|
+
|
|
1013
|
+
const { items, addItem, removeItem, updateQuantity, clearCart, itemCount, subtotal } = useCart();
|
|
1014
|
+
|
|
1015
|
+
// Add to cart
|
|
1016
|
+
addItem({
|
|
1017
|
+
productId: product.id,
|
|
1018
|
+
name: product.title,
|
|
1019
|
+
image: product.image,
|
|
1020
|
+
price: product.salePrice,
|
|
1021
|
+
slug: product.slug,
|
|
1022
|
+
});
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
### Fly-to-Cart Animation
|
|
1026
|
+
|
|
1027
|
+
When a user clicks "Add to Cart", the product thumbnail flies from the button to the cart icon in the header.
|
|
1028
|
+
|
|
1029
|
+
**Step 1: Mark any element as the cart target** (in header section):
|
|
1030
|
+
|
|
1031
|
+
```tsx
|
|
1032
|
+
// Option A: Use any element — just add the data attribute
|
|
1033
|
+
<div data-fly-to-cart-target className="my-cart-icon">
|
|
1034
|
+
<ShoppingCartIcon />
|
|
1035
|
+
<span>{itemCount}</span>
|
|
1036
|
+
</div>
|
|
1037
|
+
|
|
1038
|
+
// Option B: Use CartIcon convenience component (auto-adds data-fly-to-cart-target)
|
|
1039
|
+
import { CartIcon } from "@onexapis/core/components";
|
|
1040
|
+
<CartIcon count={itemCount} onClick={() => openCartDrawer()} />
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
**Step 2: ProductCard triggers animation automatically** — just pass `onAddToCart`:
|
|
1044
|
+
|
|
1045
|
+
```tsx
|
|
1046
|
+
<ProductCard
|
|
1047
|
+
product={product}
|
|
1048
|
+
onAddToCart={(p) => addItem({ productId: p.id, name: p.title, image: p.image, price: p.salePrice })}
|
|
1049
|
+
/>
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
**Step 3: Custom trigger** (for buttons that aren't ProductCard):
|
|
1053
|
+
|
|
1054
|
+
```tsx
|
|
1055
|
+
import { useFlyToCart } from "@onexapis/core/hooks";
|
|
1056
|
+
|
|
1057
|
+
const { flyToCart } = useFlyToCart();
|
|
1058
|
+
|
|
1059
|
+
<button onClick={(e) => {
|
|
1060
|
+
flyToCart(e.currentTarget, product.image); // animation
|
|
1061
|
+
addItem({ ... }); // cart logic
|
|
1062
|
+
}}>
|
|
1063
|
+
Buy Now
|
|
1064
|
+
</button>
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
### How it works
|
|
1068
|
+
|
|
1069
|
+
- `data-fly-to-cart-target` on any element → animation lands there (detected via `querySelector`)
|
|
1070
|
+
- `FlyToCartProvider` renders a portal with the flying thumbnail (CSS transition)
|
|
1071
|
+
- No refs or context wiring needed — just add the data attribute
|
|
1072
|
+
- Without `data-fly-to-cart-target` → cart still works, no animation
|
|
1073
|
+
|
|
1074
|
+
## Server-Side APIs
|
|
1075
|
+
|
|
1076
|
+
Server-side data fetching is available from `@onexapis/core/server`. Use in Next.js server components, server actions, or API routes.
|
|
1077
|
+
|
|
1078
|
+
```tsx
|
|
1079
|
+
import {
|
|
1080
|
+
fetchProducts,
|
|
1081
|
+
fetchBlogs,
|
|
1082
|
+
fetchSettings,
|
|
1083
|
+
fetchCompany,
|
|
1084
|
+
fetchTheme,
|
|
1085
|
+
fetchPage,
|
|
1086
|
+
prefetchSectionData,
|
|
1087
|
+
} from "@onexapis/core/server";
|
|
1088
|
+
|
|
1089
|
+
// Fetch products (server-side with ISR caching)
|
|
1090
|
+
const { data: products, pagination } = await fetchProducts(companyId, { limit: 12 });
|
|
1091
|
+
|
|
1092
|
+
// Fetch website settings
|
|
1093
|
+
const settings = await fetchSettings(companyId);
|
|
1094
|
+
|
|
1095
|
+
// Smart prefetch — scans sections for dataRequirements, fetches in parallel
|
|
1096
|
+
const data = await prefetchSectionData({
|
|
1097
|
+
companyId,
|
|
1098
|
+
sections: allSections,
|
|
1099
|
+
dataRequirements: manifest.dataRequirements,
|
|
1100
|
+
});
|
|
1101
|
+
// data.products, data.blogs, data.settings
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
### Advanced: Custom server client
|
|
1105
|
+
|
|
1106
|
+
```tsx
|
|
1107
|
+
import { CommerceServerClient, noopCacheAdapter } from "@onexapis/core/server";
|
|
1108
|
+
|
|
1109
|
+
const client = new CommerceServerClient({
|
|
1110
|
+
apiUrl: "https://api.example.com",
|
|
1111
|
+
companyId: "xxx",
|
|
1112
|
+
cacheAdapter: noopCacheAdapter, // For non-Next.js environments
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
const products = await client.getProducts({ limit: 8 });
|
|
1116
|
+
const blog = await client.getBlogBySlug("my-post");
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
Environment variables auto-detected: `NEXT_PUBLIC_COMMERCE_API_URL`, `NEXT_PUBLIC_API_URL`.
|
|
1120
|
+
|
|
912
1121
|
## MCP Servers
|
|
913
1122
|
|
|
914
1123
|
This project has THREE MCP servers. Do NOT confuse them:
|
|
@@ -924,11 +1133,13 @@ Registered in `.mcp.json` in this project. Provides theme-specific tools:
|
|
|
924
1133
|
- `onexthm_from_figma` — **Convert Figma design to OneX section** (see Figma Integration below)
|
|
925
1134
|
|
|
926
1135
|
Resources (auto-loaded context):
|
|
1136
|
+
|
|
927
1137
|
- `onexthm://rules` — Theme development rules (DOs/DON'Ts)
|
|
928
1138
|
- `onexthm://field-types` — All available field types and categories
|
|
929
1139
|
- `onexthm://hooks` — Hooks reference with examples
|
|
930
1140
|
|
|
931
1141
|
Prompts (guided workflows):
|
|
1142
|
+
|
|
932
1143
|
- `create_section` — Guided section creation workflow
|
|
933
1144
|
- `review_theme` — Review theme for issues
|
|
934
1145
|
- `figma_to_section` — Full Figma-to-OneX conversion pipeline
|
|
@@ -944,6 +1155,7 @@ Registered in `.mcp.json`. Reads Figma designs for design-to-code conversion:
|
|
|
944
1155
|
- `search_design_system` — Search components, variables, styles from libraries
|
|
945
1156
|
|
|
946
1157
|
**Setup**: Requires Figma API key in `.mcp.json`:
|
|
1158
|
+
|
|
947
1159
|
```json
|
|
948
1160
|
{
|
|
949
1161
|
"figma": {
|
|
@@ -952,6 +1164,7 @@ Registered in `.mcp.json`. Reads Figma designs for design-to-code conversion:
|
|
|
952
1164
|
}
|
|
953
1165
|
}
|
|
954
1166
|
```
|
|
1167
|
+
|
|
955
1168
|
Get your key: Figma → Settings → Account → Personal access tokens.
|
|
956
1169
|
|
|
957
1170
|
### `onex-platform` MCP (Backend Services) — DO NOT USE FOR THEMES
|
|
@@ -964,17 +1177,17 @@ This is for managing microservices on the OneXEOS platform. Its tools are:
|
|
|
964
1177
|
|
|
965
1178
|
### When to use which
|
|
966
1179
|
|
|
967
|
-
| Task
|
|
968
|
-
|
|
|
969
|
-
| Create a new section
|
|
970
|
-
| Convert Figma design to section
|
|
971
|
-
| Validate theme structure
|
|
972
|
-
| Look up available hooks
|
|
973
|
-
| Generate schema from description
|
|
974
|
-
| Read Figma design layers
|
|
975
|
-
| Get Figma design tokens
|
|
976
|
-
| Deploy a backend service
|
|
977
|
-
| Check service health
|
|
1180
|
+
| Task | MCP to use |
|
|
1181
|
+
| -------------------------------- | ------------------------------------ |
|
|
1182
|
+
| Create a new section | `onexthm_create_section` |
|
|
1183
|
+
| Convert Figma design to section | `onexthm_from_figma` + `figma` tools |
|
|
1184
|
+
| Validate theme structure | `onexthm_validate` |
|
|
1185
|
+
| Look up available hooks | `onexthm_list_hooks` |
|
|
1186
|
+
| Generate schema from description | `onexthm_generate_schema` |
|
|
1187
|
+
| Read Figma design layers | `figma:get_metadata` |
|
|
1188
|
+
| Get Figma design tokens | `figma:get_variable_defs` |
|
|
1189
|
+
| Deploy a backend service | `onex_deploy` (platform MCP) |
|
|
1190
|
+
| Check service health | `onex_status` (platform MCP) |
|
|
978
1191
|
|
|
979
1192
|
## Figma → OneX Conversion
|
|
980
1193
|
|
|
@@ -983,6 +1196,7 @@ Convert Figma designs directly to OneX theme sections using the combined Figma +
|
|
|
983
1196
|
### Quick Start
|
|
984
1197
|
|
|
985
1198
|
Use the `figma_to_section` prompt for a guided workflow:
|
|
1199
|
+
|
|
986
1200
|
```
|
|
987
1201
|
"Convert the selected Figma frame to a section called 'hero'"
|
|
988
1202
|
```
|