@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.
@@ -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
- * "/" or "/home" home page, "/showcase" showcase page, etc.
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 = window.location.pathname
348
- .replace(/^\/+|\/+$/g, "")
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
- const idx = pages.findIndex((p) => {
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
- return idx >= 0 ? idx : -2; // -2 = page not found
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 ? getInitialPageFromURL(pages) : selectedPage;
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
- // Build section data (Gap 4)
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="en">
741
+ <LocaleProvider locale={_rootLocale as any}>
637
742
  <CartProvider>
638
- <PreviewApp />
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.27",
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": "^1.0.2",
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",
@@ -6,7 +6,12 @@
6
6
  },
7
7
  "figma": {
8
8
  "command": "npx",
9
- "args": ["-y", "figma-developer-mcp", "--figma-api-key=__FIGMA_API_KEY__", "--stdio"]
9
+ "args": [
10
+ "-y",
11
+ "figma-developer-mcp",
12
+ "--figma-api-key=__FIGMA_API_KEY__",
13
+ "--stdio"
14
+ ]
10
15
  }
11
16
  }
12
17
  }
@@ -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 | MCP to use |
968
- | --------------------------------- | ----------------------------------------- |
969
- | Create a new section | `onexthm_create_section` |
970
- | Convert Figma design to section | `onexthm_from_figma` + `figma` tools |
971
- | Validate theme structure | `onexthm_validate` |
972
- | Look up available hooks | `onexthm_list_hooks` |
973
- | Generate schema from description | `onexthm_generate_schema` |
974
- | Read Figma design layers | `figma:get_metadata` |
975
- | Get Figma design tokens | `figma:get_variable_defs` |
976
- | Deploy a backend service | `onex_deploy` (platform MCP) |
977
- | Check service health | `onex_status` (platform MCP) |
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
  ```