@numueg/theme-cli 0.4.1 → 0.6.0

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 (62) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/index.js +817 -405
  3. package/package.json +2 -1
  4. package/templates/scaffold/index.html +13 -0
  5. package/templates/scaffold/package.json +27 -0
  6. package/templates/scaffold/schemas/sections/about_section.json +23 -0
  7. package/templates/scaffold/schemas/sections/account.json +8 -0
  8. package/templates/scaffold/schemas/sections/cart_summary.json +12 -0
  9. package/templates/scaffold/schemas/sections/categories.json +9 -0
  10. package/templates/scaffold/schemas/sections/featured_collection.json +14 -0
  11. package/templates/scaffold/schemas/sections/footer.json +14 -0
  12. package/templates/scaffold/schemas/sections/frequently_bought.json +10 -0
  13. package/templates/scaffold/schemas/sections/header.json +14 -0
  14. package/templates/scaffold/schemas/sections/hero.json +15 -0
  15. package/templates/scaffold/schemas/sections/image_with_text.json +19 -0
  16. package/templates/scaffold/schemas/sections/marquee.json +9 -0
  17. package/templates/scaffold/schemas/sections/newsletter.json +11 -0
  18. package/templates/scaffold/schemas/sections/not_found.json +12 -0
  19. package/templates/scaffold/schemas/sections/order_confirmation.json +9 -0
  20. package/templates/scaffold/schemas/sections/product_details.json +12 -0
  21. package/templates/scaffold/schemas/sections/product_grid.json +12 -0
  22. package/templates/scaffold/schemas/sections/promo_banner.json +13 -0
  23. package/templates/scaffold/schemas/sections/rich_text.json +17 -0
  24. package/templates/scaffold/schemas/sections/search_results.json +11 -0
  25. package/templates/scaffold/schemas/sections/size_chart.json +9 -0
  26. package/templates/scaffold/schemas/sections/testimonials.json +22 -0
  27. package/templates/scaffold/settings_schema.json +35 -0
  28. package/templates/scaffold/src/dev-entry.tsx +244 -0
  29. package/templates/scaffold/src/lib/CouponForm.tsx +90 -0
  30. package/templates/scaffold/src/lib/EditableText.tsx +178 -0
  31. package/templates/scaffold/src/lib/ProductCard.tsx +99 -0
  32. package/templates/scaffold/src/lib/cartUI.ts +43 -0
  33. package/templates/scaffold/src/lib/i18n.ts +17 -0
  34. package/templates/scaffold/src/lib/section.ts +12 -0
  35. package/templates/scaffold/src/main.tsx +230 -0
  36. package/templates/scaffold/src/sections/Footer.tsx +161 -0
  37. package/templates/scaffold/src/sections/Header.tsx +453 -0
  38. package/templates/scaffold/src/sections/about_section.tsx +104 -0
  39. package/templates/scaffold/src/sections/account.tsx +422 -0
  40. package/templates/scaffold/src/sections/cart_summary.tsx +169 -0
  41. package/templates/scaffold/src/sections/categories.tsx +57 -0
  42. package/templates/scaffold/src/sections/featured_collection.tsx +109 -0
  43. package/templates/scaffold/src/sections/frequently_bought.tsx +187 -0
  44. package/templates/scaffold/src/sections/hero.tsx +133 -0
  45. package/templates/scaffold/src/sections/image_with_text.tsx +105 -0
  46. package/templates/scaffold/src/sections/marquee.tsx +45 -0
  47. package/templates/scaffold/src/sections/newsletter.tsx +79 -0
  48. package/templates/scaffold/src/sections/not_found.tsx +56 -0
  49. package/templates/scaffold/src/sections/order_confirmation.tsx +127 -0
  50. package/templates/scaffold/src/sections/product_details.tsx +517 -0
  51. package/templates/scaffold/src/sections/product_grid.tsx +147 -0
  52. package/templates/scaffold/src/sections/promo_banner.tsx +80 -0
  53. package/templates/scaffold/src/sections/rich_text.tsx +51 -0
  54. package/templates/scaffold/src/sections/search_results.tsx +93 -0
  55. package/templates/scaffold/src/sections/size_chart.tsx +109 -0
  56. package/templates/scaffold/src/sections/testimonials.tsx +112 -0
  57. package/templates/scaffold/styles.css +2404 -0
  58. package/templates/scaffold/templates/error.html +13 -0
  59. package/templates/scaffold/templates/loading.html +11 -0
  60. package/templates/scaffold/theme.json +224 -0
  61. package/templates/scaffold/tsconfig.json +22 -0
  62. package/templates/scaffold/vite.config.ts +16 -0
package/dist/index.js CHANGED
@@ -878,359 +878,80 @@ function detectAuthor() {
878
878
  if (name) return name;
879
879
  return "Theme Author";
880
880
  }
881
- var initCommand = new import_commander.Command("init").description("Scaffold a new NUMU theme project").argument("<name>", "Theme name").option("--template <template>", "Starter template", "basic").action(async (name, _options) => {
882
- const dir = path.resolve(process.cwd(), name);
883
- if (fs.existsSync(dir)) {
884
- console.error(`Directory "${name}" already exists`);
885
- process.exit(1);
881
+ function scaffoldRoot() {
882
+ const candidates = [
883
+ path.join(__dirname, "..", "templates", "scaffold"),
884
+ path.join(__dirname, "..", "..", "templates", "scaffold")
885
+ ];
886
+ for (const c of candidates) {
887
+ if (fs.existsSync(c)) return c;
886
888
  }
887
- console.log(`Creating NUMU theme: ${name}...`);
888
- for (const d of [
889
- "src/sections",
890
- "src/blocks",
891
- "schemas/sections",
892
- "schemas/blocks",
893
- "assets"
894
- ]) {
895
- fs.mkdirSync(path.join(dir, d), { recursive: true });
896
- }
897
- fs.writeFileSync(
898
- path.join(dir, "theme.json"),
899
- JSON.stringify(
900
- {
901
- id: name.toLowerCase().replace(/[^a-z0-9-]/g, "-"),
902
- name,
903
- author: detectAuthor(),
904
- version: "0.1.0",
905
- layout: "single-column",
906
- description: `${name} NUMU theme`,
907
- // Phase 7.3 — static BYOT templates for chrome that renders
908
- // outside the React tree (the streaming loading skeleton +
909
- // the client error boundary). Themes that want to fully
910
- // own those moments edit these HTML files; absent or 404
911
- // → the platform's hardcoded fallback renders.
912
- error_template: "templates/error.html",
913
- loading_template: "templates/loading.html",
914
- presets: {
915
- templates: {
916
- home: {
917
- name: "Home",
918
- sections: [
919
- {
920
- type: "hero",
921
- settings: { headline: "Welcome to our store" }
922
- }
923
- ]
924
- }
925
- }
926
- }
927
- },
928
- null,
929
- 2
930
- )
931
- );
932
- fs.mkdirSync(path.join(dir, "templates"), { recursive: true });
933
- fs.writeFileSync(
934
- path.join(dir, "templates/error.html"),
935
- `<!--
936
- Static BYOT error template \u2014 rendered by the storefront when a
937
- page throws. No JS available (the bundle might be the thing that
938
- failed). Use <button data-numu-reset> to expose a retry button \u2014
939
- the storefront wires the click for you.
940
- -->
941
- <main role="alert" style="min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem;font-family:system-ui">
942
- <div style="text-align:center;max-width:28rem">
943
- <h1 style="font-size:1.5rem;font-weight:700;color:#b91c1c">Something went wrong</h1>
944
- <p style="color:#374151;margin-top:.5rem">Please try again in a moment.</p>
945
- <button data-numu-reset type="button" style="margin-top:1.5rem;padding:.5rem 1rem;background:#1d4ed8;color:white;border-radius:.375rem;border:0;cursor:pointer">Try again</button>
946
- </div>
947
- </main>
948
- `
949
- );
950
- fs.writeFileSync(
951
- path.join(dir, "templates/loading.html"),
952
- `<!-- Static BYOT loading skeleton. -->
953
- <div role="status" aria-live="polite" aria-label="Loading" style="min-height:100vh;display:flex;align-items:center;justify-content:center;padding:1rem;font-family:system-ui">
954
- <div style="display:flex;align-items:center;gap:.75rem;color:#4b5563">
955
- <svg style="width:1.25rem;height:1.25rem;animation:spin 1s linear infinite" viewBox="0 0 24 24" fill="none" aria-hidden="true">
956
- <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-opacity=".25" stroke-width="3"></circle>
957
- <path d="M22 12a10 10 0 0 1-10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path>
958
- </svg>
959
- <span style="font-size:.875rem;font-weight:500">Loading\u2026</span>
960
- </div>
961
- <style>@keyframes spin { to { transform: rotate(360deg) } }</style>
962
- </div>
963
- `
964
- );
965
- fs.writeFileSync(
966
- path.join(dir, "settings_schema.json"),
967
- JSON.stringify(
968
- [
969
- {
970
- type: "color",
971
- id: "primary_color",
972
- label: "Primary Color",
973
- default: "#3B82F6"
974
- },
975
- {
976
- type: "color",
977
- id: "secondary_color",
978
- label: "Secondary Color",
979
- default: "#1E40AF"
980
- },
981
- {
982
- type: "select",
983
- id: "font_family",
984
- label: "Font Family",
985
- default: "Inter",
986
- options: [
987
- { value: "Inter", label: "Inter" },
988
- { value: "Roboto", label: "Roboto" },
989
- { value: "Cairo", label: "Cairo" }
990
- ]
991
- }
992
- ],
993
- null,
994
- 2
995
- )
996
- );
997
- fs.writeFileSync(
998
- path.join(dir, "styles.css"),
999
- ":root { --numu-primary: #3B82F6; }\n"
1000
- );
1001
- fs.writeFileSync(
1002
- path.join(dir, "src/sections/Hero.tsx"),
1003
- `import type { SectionProps } from "@numueg/theme-sdk";
1004
-
1005
- export default function Hero({ settings }: SectionProps) {
1006
- return (
1007
- <section className="min-h-[50vh] flex items-center justify-center bg-gray-900 text-white">
1008
- <div className="text-center">
1009
- <h1 className="text-4xl font-bold">{settings.headline as string || "Welcome"}</h1>
1010
- {settings.subtitle ? (
1011
- <p className="mt-4 text-xl">{settings.subtitle as string}</p>
1012
- ) : null}
1013
- </div>
1014
- </section>
889
+ throw new Error(
890
+ "Could not locate the scaffold templates. Reinstall @numueg/theme-cli."
1015
891
  );
1016
892
  }
1017
- `
1018
- );
1019
- fs.writeFileSync(
1020
- path.join(dir, "schemas/sections/hero.json"),
1021
- JSON.stringify(
1022
- {
1023
- type: "hero",
1024
- name: "Hero Banner",
1025
- locales: { ar: { name: "\u0628\u0627\u0646\u0631 \u0631\u0626\u064A\u0633\u064A" } },
1026
- settings: [
1027
- {
1028
- type: "text",
1029
- id: "headline",
1030
- label: "Headline",
1031
- locales: { ar: { label: "\u0627\u0644\u0639\u0646\u0648\u0627\u0646" } },
1032
- default: "Welcome"
1033
- },
1034
- {
1035
- type: "text",
1036
- id: "subtitle",
1037
- label: "Subtitle",
1038
- locales: { ar: { label: "\u0627\u0644\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0641\u0631\u0639\u064A" } }
1039
- },
1040
- {
1041
- type: "image_picker",
1042
- id: "background_image",
1043
- label: "Background Image",
1044
- locales: { ar: { label: "\u0635\u0648\u0631\u0629 \u0627\u0644\u062E\u0644\u0641\u064A\u0629" } }
1045
- }
1046
- ]
1047
- },
1048
- null,
1049
- 2
1050
- )
893
+ var SUBSTITUTED = /* @__PURE__ */ new Set([
894
+ "theme.json",
895
+ "package.json",
896
+ "index.html",
897
+ path.join("src", "dev-entry.tsx")
898
+ ]);
899
+ function applyTokens(content, tokens) {
900
+ return content.replace(
901
+ /__([A-Z_]+)__/g,
902
+ (whole, key) => key in tokens ? tokens[key] : whole
1051
903
  );
1052
- fs.writeFileSync(
1053
- path.join(dir, "src/main.tsx"),
1054
- `import type { ComponentType } from "react";
1055
- import { defineThemeEntry } from "@numueg/theme-sdk";
1056
- import type { ThemeSettingsV3 } from "@numueg/theme-sdk";
1057
- import Hero from "./sections/Hero";
1058
-
1059
- const SECTION_REGISTRY: Record<string, ComponentType<any>> = {
1060
- hero: Hero,
1061
- };
1062
-
1063
- interface ThemeProps {
1064
- themeSettings: ThemeSettingsV3;
1065
- currentTemplate: string;
1066
904
  }
1067
-
1068
- export default function Theme({ themeSettings, currentTemplate }: ThemeProps) {
1069
- const template =
1070
- themeSettings.templates?.[currentTemplate] || themeSettings.templates?.home;
1071
-
1072
- return (
1073
- <main>
1074
- {template?.order.map((sectionId) => {
1075
- const section = template.sections[sectionId];
1076
- if (!section || section.disabled) return null;
1077
- const Component = SECTION_REGISTRY[section.type];
1078
- if (!Component) return null;
1079
- return (
1080
- <Component
1081
- key={sectionId}
1082
- settings={section.settings}
1083
- blocks={section.blocks}
1084
- blockOrder={section.block_order}
1085
- />
1086
- );
1087
- })}
1088
- </main>
1089
- );
905
+ function copyTree(from, to, rel, tokens) {
906
+ const entries = fs.readdirSync(from, { withFileTypes: true });
907
+ for (const entry of entries) {
908
+ const srcPath = path.join(from, entry.name);
909
+ const dstPath = path.join(to, entry.name);
910
+ const relPath = rel ? path.join(rel, entry.name) : entry.name;
911
+ if (entry.isDirectory()) {
912
+ fs.mkdirSync(dstPath, { recursive: true });
913
+ copyTree(srcPath, dstPath, relPath, tokens);
914
+ } else {
915
+ let content = fs.readFileSync(srcPath, "utf-8");
916
+ if (SUBSTITUTED.has(relPath)) content = applyTokens(content, tokens);
917
+ fs.writeFileSync(dstPath, content);
918
+ }
919
+ }
1090
920
  }
1091
-
1092
- // SSR rule of thumb: everything rendered above must be deterministic and
1093
- // browser-free (no window/document/Date.now in the render path \u2014 put those
1094
- // in useEffect). \`numu-theme lint\` checks this for you.
1095
- const entry = defineThemeEntry(({ themeSettings, currentTemplate }) => (
1096
- <Theme themeSettings={themeSettings} currentTemplate={currentTemplate} />
1097
- ));
1098
-
1099
- export const mount = entry.mount;
1100
- export const createApp = entry.createApp;
1101
- `
1102
- );
1103
- fs.writeFileSync(
1104
- path.join(dir, "index.html"),
1105
- `<!doctype html>
1106
- <html lang="en">
1107
- <head>
1108
- <meta charset="UTF-8" />
1109
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1110
- <title>${name} \u2014 NUMU Theme Dev</title>
1111
- <link rel="stylesheet" href="/styles.css" />
1112
- </head>
1113
- <body>
1114
- <div id="root"></div>
1115
- <script type="module" src="/src/dev-entry.tsx"></script>
1116
- </body>
1117
- </html>
1118
- `
1119
- );
1120
- fs.writeFileSync(
1121
- path.join(dir, "src/dev-entry.tsx"),
1122
- `import { createRoot } from "react-dom/client";
1123
- import Theme from "./main";
1124
-
1125
- const placeholder = {
1126
- schema_version: 3 as const,
1127
- theme_id: "${name}",
1128
- global_settings: {},
1129
- templates: {
1130
- home: {
1131
- name: "Home",
1132
- sections: { hero_1: { type: "hero", settings: { headline: "Hello, theme!" } } },
1133
- order: ["hero_1"],
1134
- },
1135
- },
1136
- section_groups: {},
1137
- };
1138
-
1139
- const root = document.getElementById("root");
1140
- if (root)
1141
- createRoot(root).render(
1142
- <Theme themeSettings={placeholder as any} currentTemplate="home" />,
1143
- );
1144
- `
1145
- );
1146
- fs.writeFileSync(
1147
- path.join(dir, "vite.config.ts"),
1148
- `import { defineConfig } from "vite";
1149
- import react from "@vitejs/plugin-react";
1150
- import { numuTheme } from "@numueg/theme-plugin";
1151
-
1152
- // The @numueg/theme-plugin handles all the NUMU-specific glue:
1153
- // - validates theme.json + settings_schema.json + entry point
1154
- // - externalizes React + @numueg/theme-sdk (host-provided)
1155
- // - emits dist/manifest.json + dist/import-map.json after build
1156
- //
1157
- // You don't need to repeat \`build.lib\` / \`build.rollupOptions.external\`
1158
- // \u2014 the plugin sets sensible defaults when they're omitted.
1159
-
1160
- export default defineConfig({
1161
- plugins: [react(), numuTheme()],
1162
- server: { port: 5173 },
1163
- });
1164
- `
1165
- );
1166
- fs.writeFileSync(
1167
- path.join(dir, "tsconfig.json"),
1168
- JSON.stringify(
1169
- {
1170
- compilerOptions: {
1171
- target: "ES2022",
1172
- lib: ["ES2022", "DOM", "DOM.Iterable"],
1173
- module: "ESNext",
1174
- moduleResolution: "bundler",
1175
- jsx: "react-jsx",
1176
- strict: true,
1177
- noEmit: true,
1178
- skipLibCheck: true,
1179
- isolatedModules: true,
1180
- esModuleInterop: true,
1181
- resolveJsonModule: true
1182
- },
1183
- include: ["src"]
1184
- },
1185
- null,
1186
- 2
1187
- )
1188
- );
1189
- fs.writeFileSync(
1190
- path.join(dir, "package.json"),
1191
- JSON.stringify(
1192
- {
1193
- name: `numu-theme-${name}`,
1194
- version: "0.1.0",
1195
- private: true,
1196
- type: "module",
1197
- scripts: {
1198
- dev: "numu-theme dev",
1199
- build: "numu-theme build",
1200
- check: "numu-theme check",
1201
- // Render-verify every template against fixtures (Phase 2).
1202
- verify: "numu-theme verify"
1203
- },
1204
- dependencies: { "@numueg/theme-sdk": "^0.4.0" },
1205
- devDependencies: {
1206
- "@numueg/theme-cli": "^0.4.0",
1207
- "@numueg/theme-plugin": "^0.4.0",
1208
- "@vitejs/plugin-react": "^4.3.0",
1209
- vite: "^6.0.0",
1210
- typescript: "^5.8.0",
1211
- "@types/react": "^19.0.0",
1212
- "@types/react-dom": "^19.0.0",
1213
- react: "^19.0.0",
1214
- "react-dom": "^19.0.0"
1215
- }
1216
- },
1217
- null,
1218
- 2
1219
- )
1220
- );
1221
- fs.writeFileSync(
1222
- path.join(dir, ".gitignore"),
1223
- "node_modules\ndist\n.env\n.env.*\n.next\n"
1224
- );
921
+ var initCommand = new import_commander.Command("init").description("Scaffold a new NUMU theme project from the standard starter").argument("<name>", "Theme name").option("--template <template>", "Starter template", "basic").action(async (name, _options) => {
922
+ const dir = path.resolve(process.cwd(), name);
923
+ if (fs.existsSync(dir)) {
924
+ console.error(`Directory "${name}" already exists`);
925
+ process.exit(1);
926
+ }
927
+ const themeId = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
928
+ const tokens = {
929
+ THEME_NAME: name,
930
+ THEME_ID: themeId,
931
+ PKG_NAME: themeId,
932
+ AUTHOR: detectAuthor(),
933
+ VERSION: "0.1.0"
934
+ };
935
+ console.log(`Creating NUMU theme: ${name}...`);
936
+ let root;
937
+ try {
938
+ root = scaffoldRoot();
939
+ } catch (err) {
940
+ console.error(err.message);
941
+ process.exit(1);
942
+ }
943
+ fs.mkdirSync(dir, { recursive: true });
944
+ copyTree(root, dir, "", tokens);
1225
945
  console.log(
1226
946
  `
1227
- Theme "${name}" created.
947
+ Theme "${name}" created from the NUMU Starter.
1228
948
 
1229
949
  Next steps:
1230
950
  cd ${name}
1231
951
  npm install
1232
- npx numu-theme dev # local preview
1233
- npx numu-theme build # production build
952
+ npx numu-theme dev # local preview
953
+ npx numu-theme check # validate
954
+ npx numu-theme build # production build
1234
955
  `
1235
956
  );
1236
957
  });
@@ -3001,21 +2722,29 @@ var featuredProducts = {
3001
2722
  name: "Featured Products",
3002
2723
  description: "Grid of hand-picked products with title + price + CTA",
3003
2724
  component: `import type { SectionProps } from "@numueg/theme-sdk";
3004
- import { ProductCard, useProducts } from "@numueg/theme-sdk";
2725
+ import { ProductCard, useProducts, useLocale } from "@numueg/theme-sdk";
2726
+
2727
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
2728
+ function useT() {
2729
+ const locale = useLocale();
2730
+ const isAr =
2731
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
2732
+ return (en: string, ar: string) => (isAr ? ar : en);
2733
+ }
3005
2734
 
3006
2735
  export default function FeaturedProducts({ settings }: SectionProps) {
2736
+ const t = useT();
3007
2737
  const ids = (settings.product_ids as string[]) || [];
3008
2738
  const limit = (settings.limit as number) || 8;
3009
- const title = (settings.title as string) || "Featured products";
2739
+ const title = (settings.title as string) || t("Featured products", "\u0645\u0646\u062A\u062C\u0627\u062A \u0645\u0645\u064A\u0632\u0629");
3010
2740
 
3011
- // When specific products are picked, fetch them; otherwise fetch a
3012
- // top-N list from the store catalog.
3013
- const { products, loading } = useProducts({
3014
- ids: ids.length > 0 ? ids : undefined,
3015
- limit: ids.length > 0 ? undefined : limit,
3016
- });
2741
+ // Pull the catalog (fetching if the route didn't pre-load it), then narrow
2742
+ // to the merchant's hand-picked ids when provided, else take the top-N.
2743
+ const { products: all, loading } = useProducts({ fetchIfMissing: true, limit });
2744
+ const products =
2745
+ ids.length > 0 ? all.filter((p) => ids.includes(p.id)) : all;
3017
2746
 
3018
- if (loading) return <section className="py-16 text-center text-gray-500">Loading\u2026</section>;
2747
+ if (loading) return <section className="py-16 text-center text-gray-500">{t("Loading\u2026", "\u062C\u0627\u0631\u064D \u0627\u0644\u062A\u062D\u0645\u064A\u0644\u2026")}</section>;
3019
2748
  if (!products.length) return null;
3020
2749
 
3021
2750
  return (
@@ -3067,9 +2796,18 @@ var imageWithText = {
3067
2796
  name: "Image with Text",
3068
2797
  description: "Two-column section: image on one side, headline + body + CTA on the other",
3069
2798
  component: `import type { SectionProps } from "@numueg/theme-sdk";
3070
- import { useDirection } from "@numueg/theme-sdk";
2799
+ import { useDirection, useLocale } from "@numueg/theme-sdk";
2800
+
2801
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
2802
+ function useT() {
2803
+ const locale = useLocale();
2804
+ const isAr =
2805
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
2806
+ return (en: string, ar: string) => (isAr ? ar : en);
2807
+ }
3071
2808
 
3072
2809
  export default function ImageWithText({ settings }: SectionProps) {
2810
+ const t = useT();
3073
2811
  const dir = useDirection();
3074
2812
  const image = settings.image as string | undefined;
3075
2813
  const layout = (settings.image_position as string) || "left";
@@ -3164,13 +2902,22 @@ var newsletterSignup = {
3164
2902
  name: "Newsletter Signup",
3165
2903
  description: "Email capture form with merchant-supplied heading + consent copy",
3166
2904
  component: `import type { SectionProps } from "@numueg/theme-sdk";
3167
- import { Form } from "@numueg/theme-sdk";
2905
+ import { Form, useLocale } from "@numueg/theme-sdk";
2906
+
2907
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
2908
+ function useT() {
2909
+ const locale = useLocale();
2910
+ const isAr =
2911
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
2912
+ return (en: string, ar: string) => (isAr ? ar : en);
2913
+ }
3168
2914
 
3169
2915
  export default function NewsletterSignup({ settings }: SectionProps) {
3170
- const heading = (settings.heading as string) || "Stay in touch";
2916
+ const t = useT();
2917
+ const heading = (settings.heading as string) || t("Stay in touch", "\u0627\u0628\u0642\u064E \u0639\u0644\u0649 \u062A\u0648\u0627\u0635\u0644");
3171
2918
  const subheading = (settings.subheading as string) || "";
3172
- const placeholder = (settings.placeholder as string) || "Email address";
3173
- const buttonLabel = (settings.button_label as string) || "Subscribe";
2919
+ const placeholder = (settings.placeholder as string) || t("Email address", "\u0627\u0644\u0628\u0631\u064A\u062F \u0627\u0644\u0625\u0644\u0643\u062A\u0631\u0648\u0646\u064A");
2920
+ const buttonLabel = (settings.button_label as string) || t("Subscribe", "\u0627\u0634\u062A\u0631\u0627\u0643");
3174
2921
  const consent = (settings.consent_text as string) || "";
3175
2922
 
3176
2923
  return (
@@ -3251,10 +2998,19 @@ var multiColumn = {
3251
2998
  slug: "multi-column",
3252
2999
  name: "Multi-column",
3253
3000
  description: "2 / 3 / 4-column features grid with icon + heading + text per column",
3254
- component: `import type { SectionProps, BlockProps } from "@numueg/theme-sdk";
3255
- import { Block } from "@numueg/theme-sdk";
3001
+ component: `import type { SectionProps } from "@numueg/theme-sdk";
3002
+ import { Block, useLocale } from "@numueg/theme-sdk";
3003
+
3004
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3005
+ function useT() {
3006
+ const locale = useLocale();
3007
+ const isAr =
3008
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3009
+ return (en: string, ar: string) => (isAr ? ar : en);
3010
+ }
3256
3011
 
3257
- export default function MultiColumn({ settings, blocks }: SectionProps & { blocks?: BlockProps[] }) {
3012
+ export default function MultiColumn({ settings, blocks, blockOrder }: SectionProps) {
3013
+ const t = useT();
3258
3014
  const heading = (settings.heading as string) || "";
3259
3015
  const cols = (settings.columns as number) || 3;
3260
3016
  const gridCols =
@@ -3262,31 +3018,38 @@ export default function MultiColumn({ settings, blocks }: SectionProps & { block
3262
3018
  cols === 4 ? "md:grid-cols-2 lg:grid-cols-4" :
3263
3019
  "md:grid-cols-3";
3264
3020
 
3021
+ // Blocks arrive as a {id: instance} map + an order array \u2014 mirror the
3022
+ // section\u2192block render: walk the order and look each id up.
3023
+ const map = blocks || {};
3024
+ const order = blockOrder || Object.keys(map);
3025
+
3265
3026
  return (
3266
- <section className="py-16 px-6 max-w-7xl mx-auto">
3027
+ <section className="py-16 px-6 max-w-7xl mx-auto" aria-label={t("Features", "\u0627\u0644\u0645\u0645\u064A\u0632\u0627\u062A")}>
3267
3028
  {heading && (
3268
3029
  <h2 className="text-2xl md:text-3xl font-semibold text-center mb-12">{heading}</h2>
3269
3030
  )}
3270
3031
  <div className={\`grid grid-cols-1 \${gridCols} gap-10\`}>
3271
- {(blocks || []).map((block, i) => (
3272
- <Block key={i} block={block}>
3273
- {block.type === "column" && (
3032
+ {order.map((id) => {
3033
+ const b = map[id];
3034
+ if (!b || b.type !== "column") return null;
3035
+ return (
3036
+ <Block key={id} id={id} type={b.type}>
3274
3037
  <div className="text-center">
3275
- {block.settings.icon ? (
3038
+ {b.settings.icon ? (
3276
3039
  <img
3277
- src={block.settings.icon as string}
3040
+ src={b.settings.icon as string}
3278
3041
  alt=""
3279
3042
  className="w-12 h-12 mx-auto mb-4"
3280
3043
  />
3281
3044
  ) : null}
3282
3045
  <h3 className="text-lg font-semibold mb-2">
3283
- {(block.settings.title as string) || ""}
3046
+ {(b.settings.title as string) || ""}
3284
3047
  </h3>
3285
- <p className="text-gray-600">{(block.settings.text as string) || ""}</p>
3048
+ <p className="text-gray-600">{(b.settings.text as string) || ""}</p>
3286
3049
  </div>
3287
- )}
3288
- </Block>
3289
- ))}
3050
+ </Block>
3051
+ );
3052
+ })}
3290
3053
  </div>
3291
3054
  </section>
3292
3055
  );
@@ -3350,9 +3113,19 @@ var testimonials = {
3350
3113
  name: "Testimonials",
3351
3114
  description: "Customer reviews carousel with quote, author, role, optional photo",
3352
3115
  component: `import type { SectionProps, BlockProps } from "@numueg/theme-sdk";
3116
+ import { useLocale } from "@numueg/theme-sdk";
3117
+
3118
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3119
+ function useT() {
3120
+ const locale = useLocale();
3121
+ const isAr =
3122
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3123
+ return (en: string, ar: string) => (isAr ? ar : en);
3124
+ }
3353
3125
 
3354
3126
  export default function Testimonials({ settings, blocks }: SectionProps & { blocks?: BlockProps[] }) {
3355
- const heading = (settings.heading as string) || "What our customers say";
3127
+ const t = useT();
3128
+ const heading = (settings.heading as string) || t("What our customers say", "\u0622\u0631\u0627\u0621 \u0639\u0645\u0644\u0627\u0626\u0646\u0627");
3356
3129
  return (
3357
3130
  <section className="py-16 px-6 bg-gray-50">
3358
3131
  <div className="max-w-5xl mx-auto">
@@ -3405,9 +3178,19 @@ var faqAccordion = {
3405
3178
  name: "FAQ Accordion",
3406
3179
  description: "Collapsible question/answer pairs using native <details>/<summary>",
3407
3180
  component: `import type { SectionProps, BlockProps } from "@numueg/theme-sdk";
3181
+ import { useLocale } from "@numueg/theme-sdk";
3182
+
3183
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3184
+ function useT() {
3185
+ const locale = useLocale();
3186
+ const isAr =
3187
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3188
+ return (en: string, ar: string) => (isAr ? ar : en);
3189
+ }
3408
3190
 
3409
3191
  export default function FaqAccordion({ settings, blocks }: SectionProps & { blocks?: BlockProps[] }) {
3410
- const heading = (settings.heading as string) || "Frequently asked questions";
3192
+ const t = useT();
3193
+ const heading = (settings.heading as string) || t("Frequently asked questions", "\u0627\u0644\u0623\u0633\u0626\u0644\u0629 \u0627\u0644\u0634\u0627\u0626\u0639\u0629");
3411
3194
  return (
3412
3195
  <section className="py-16 px-6 max-w-3xl mx-auto">
3413
3196
  <h2 className="text-2xl md:text-3xl font-semibold text-center mb-10">{heading}</h2>
@@ -3453,8 +3236,18 @@ var logoCloud = {
3453
3236
  name: "Logo Cloud",
3454
3237
  description: "Row of partner / press / payment logos in grayscale with hover restore",
3455
3238
  component: `import type { SectionProps, BlockProps } from "@numueg/theme-sdk";
3239
+ import { useLocale } from "@numueg/theme-sdk";
3240
+
3241
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3242
+ function useT() {
3243
+ const locale = useLocale();
3244
+ const isAr =
3245
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3246
+ return (en: string, ar: string) => (isAr ? ar : en);
3247
+ }
3456
3248
 
3457
3249
  export default function LogoCloud({ settings, blocks }: SectionProps & { blocks?: BlockProps[] }) {
3250
+ const t = useT();
3458
3251
  const heading = (settings.heading as string) || "";
3459
3252
  return (
3460
3253
  <section className="py-12 px-6 bg-white">
@@ -3501,17 +3294,25 @@ var collectionList = {
3501
3294
  name: "Collection List",
3502
3295
  description: "Grid of collection cards (image + name) linking to each collection's PLP",
3503
3296
  component: `import type { SectionProps } from "@numueg/theme-sdk";
3504
- import { CollectionCard, useCollections } from "@numueg/theme-sdk";
3297
+ import { CollectionCard, useCollections, useLocale } from "@numueg/theme-sdk";
3298
+
3299
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3300
+ function useT() {
3301
+ const locale = useLocale();
3302
+ const isAr =
3303
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3304
+ return (en: string, ar: string) => (isAr ? ar : en);
3305
+ }
3505
3306
 
3506
3307
  export default function CollectionListSection({ settings }: SectionProps) {
3308
+ const t = useT();
3507
3309
  const ids = (settings.collection_ids as string[]) || [];
3508
3310
  const limit = (settings.limit as number) || 6;
3509
- const heading = (settings.heading as string) || "Shop by collection";
3510
- const { collections, loading } = useCollections({
3511
- ids: ids.length > 0 ? ids : undefined,
3512
- limit: ids.length > 0 ? undefined : limit,
3513
- });
3514
- if (loading) return <section className="py-16 text-center text-gray-500">Loading\u2026</section>;
3311
+ const heading = (settings.heading as string) || t("Shop by collection", "\u062A\u0633\u0648\u0651\u0642 \u062D\u0633\u0628 \u0627\u0644\u0645\u062C\u0645\u0648\u0639\u0629");
3312
+ const { collections: all, loading } = useCollections({ fetchIfMissing: true, limit });
3313
+ const collections =
3314
+ ids.length > 0 ? all.filter((c) => ids.includes(c.id)) : all;
3315
+ if (loading) return <section className="py-16 text-center text-gray-500">{t("Loading\u2026", "\u062C\u0627\u0631\u064D \u0627\u0644\u062A\u062D\u0645\u064A\u0644\u2026")}</section>;
3515
3316
  if (!collections.length) return null;
3516
3317
  return (
3517
3318
  <section className="py-16 px-6 max-w-7xl mx-auto">
@@ -3541,8 +3342,18 @@ var videoEmbed = {
3541
3342
  name: "Video Embed",
3542
3343
  description: "Responsive 16:9 video (YouTube / Vimeo / self-hosted MP4)",
3543
3344
  component: `import type { SectionProps } from "@numueg/theme-sdk";
3345
+ import { useLocale } from "@numueg/theme-sdk";
3346
+
3347
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3348
+ function useT() {
3349
+ const locale = useLocale();
3350
+ const isAr =
3351
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3352
+ return (en: string, ar: string) => (isAr ? ar : en);
3353
+ }
3544
3354
 
3545
3355
  export default function VideoEmbed({ settings }: SectionProps) {
3356
+ const t = useT();
3546
3357
  const heading = (settings.heading as string) || "";
3547
3358
  const url = (settings.video_url as string) || "";
3548
3359
  const poster = (settings.poster as string) || undefined;
@@ -3558,14 +3369,14 @@ export default function VideoEmbed({ settings }: SectionProps) {
3558
3369
  ) : (
3559
3370
  <iframe
3560
3371
  src={url}
3561
- title={heading || "Video"}
3372
+ title={heading || t("Video", "\u0641\u064A\u062F\u064A\u0648")}
3562
3373
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
3563
3374
  allowFullScreen
3564
3375
  className="w-full h-full border-0"
3565
3376
  />
3566
3377
  )
3567
3378
  ) : (
3568
- <div className="flex items-center justify-center h-full text-white/60">Set a video URL</div>
3379
+ <div className="flex items-center justify-center h-full text-white/60">{t("Set a video URL", "\u0623\u062F\u062E\u0644 \u0631\u0627\u0628\u0637 \u0627\u0644\u0641\u064A\u062F\u064A\u0648")}</div>
3569
3380
  )}
3570
3381
  </div>
3571
3382
  </section>
@@ -3590,9 +3401,18 @@ var richText = {
3590
3401
  name: "Rich Text",
3591
3402
  description: "Merchant-edited rich text block \u2014 title + paragraph(s) + optional CTA",
3592
3403
  component: `import type { SectionProps } from "@numueg/theme-sdk";
3593
- import { RichText } from "@numueg/theme-sdk";
3404
+ import { RichText, useLocale } from "@numueg/theme-sdk";
3405
+
3406
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3407
+ function useT() {
3408
+ const locale = useLocale();
3409
+ const isAr =
3410
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3411
+ return (en: string, ar: string) => (isAr ? ar : en);
3412
+ }
3594
3413
 
3595
3414
  export default function RichTextSection({ settings }: SectionProps) {
3415
+ const t = useT();
3596
3416
  const heading = (settings.heading as string) || "";
3597
3417
  const body = (settings.body as string) || "";
3598
3418
  const align = (settings.alignment as string) || "left";
@@ -3639,11 +3459,21 @@ var announcementBar = {
3639
3459
  name: "Announcement Bar",
3640
3460
  description: "Thin top-of-page bar for promos / shipping notices, optionally dismissible",
3641
3461
  component: `import type { SectionProps } from "@numueg/theme-sdk";
3462
+ import { useLocale } from "@numueg/theme-sdk";
3642
3463
  import { useEffect, useState } from "react";
3643
3464
 
3644
3465
  const DISMISS_KEY = "numu_announcement_dismissed";
3645
3466
 
3467
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3468
+ function useT() {
3469
+ const locale = useLocale();
3470
+ const isAr =
3471
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3472
+ return (en: string, ar: string) => (isAr ? ar : en);
3473
+ }
3474
+
3646
3475
  export default function AnnouncementBar({ settings }: SectionProps) {
3476
+ const t = useT();
3647
3477
  const message = (settings.message as string) || "";
3648
3478
  const linkLabel = settings.link_label as string | undefined;
3649
3479
  const linkHref = (settings.link_href as string) || "/";
@@ -3658,7 +3488,7 @@ export default function AnnouncementBar({ settings }: SectionProps) {
3658
3488
  if (!message || dismissed) return null;
3659
3489
 
3660
3490
  return (
3661
- <div role="region" aria-label="Announcement" className="bg-gray-900 text-white text-sm">
3491
+ <div role="region" aria-label={t("Announcement", "\u0625\u0639\u0644\u0627\u0646")} className="bg-gray-900 text-white text-sm">
3662
3492
  <div className="max-w-7xl mx-auto px-4 py-2 flex items-center justify-center gap-3 relative">
3663
3493
  <p>
3664
3494
  {message}
@@ -3672,7 +3502,7 @@ export default function AnnouncementBar({ settings }: SectionProps) {
3672
3502
  {dismissible && (
3673
3503
  <button
3674
3504
  type="button"
3675
- aria-label="Dismiss announcement"
3505
+ aria-label={t("Dismiss announcement", "\u0625\u063A\u0644\u0627\u0642 \u0627\u0644\u0625\u0639\u0644\u0627\u0646")}
3676
3506
  onClick={() => {
3677
3507
  setDismissed(true);
3678
3508
  try { window.sessionStorage.setItem(DISMISS_KEY, "1"); } catch {}
@@ -3706,8 +3536,17 @@ var countdownTimer = {
3706
3536
  name: "Countdown Timer",
3707
3537
  description: "Sale-end countdown \u2014 DD:HH:MM:SS ticking to a merchant-set target date",
3708
3538
  component: `import type { SectionProps } from "@numueg/theme-sdk";
3539
+ import { useLocale } from "@numueg/theme-sdk";
3709
3540
  import { useEffect, useState } from "react";
3710
3541
 
3542
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3543
+ function useT() {
3544
+ const locale = useLocale();
3545
+ const isAr =
3546
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3547
+ return (en: string, ar: string) => (isAr ? ar : en);
3548
+ }
3549
+
3711
3550
  function diff(target: number) {
3712
3551
  const ms = Math.max(0, target - Date.now());
3713
3552
  const d = Math.floor(ms / 86_400_000);
@@ -3718,14 +3557,15 @@ function diff(target: number) {
3718
3557
  }
3719
3558
 
3720
3559
  export default function CountdownTimer({ settings }: SectionProps) {
3721
- const heading = (settings.heading as string) || "Sale ends in";
3560
+ const t = useT();
3561
+ const heading = (settings.heading as string) || t("Sale ends in", "\u064A\u0646\u062A\u0647\u064A \u0627\u0644\u0639\u0631\u0636 \u062E\u0644\u0627\u0644");
3722
3562
  const targetIso = settings.end_at as string | undefined;
3723
3563
  const target = targetIso ? Date.parse(targetIso) : 0;
3724
- const [t, setT] = useState(() => diff(target));
3564
+ const [time, setTime] = useState(() => diff(target));
3725
3565
 
3726
3566
  useEffect(() => {
3727
3567
  if (!target) return;
3728
- const id = window.setInterval(() => setT(diff(target)), 1000);
3568
+ const id = window.setInterval(() => setTime(diff(target)), 1000);
3729
3569
  return () => window.clearInterval(id);
3730
3570
  }, [target]);
3731
3571
 
@@ -3734,17 +3574,17 @@ export default function CountdownTimer({ settings }: SectionProps) {
3734
3574
  return (
3735
3575
  <section className="py-12 px-6 bg-red-50 text-center">
3736
3576
  <h2 className="text-xl md:text-2xl font-semibold mb-4">{heading}</h2>
3737
- {t.done ? (
3738
- <p className="text-lg text-red-700">The sale has ended.</p>
3577
+ {time.done ? (
3578
+ <p className="text-lg text-red-700">{t("The sale has ended.", "\u0627\u0646\u062A\u0647\u0649 \u0627\u0644\u0639\u0631\u0636.")}</p>
3739
3579
  ) : (
3740
3580
  <div className="flex justify-center gap-4 md:gap-6 font-mono">
3741
3581
  {[
3742
- { label: "days", value: t.d },
3743
- { label: "hrs", value: t.h },
3744
- { label: "min", value: t.m },
3745
- { label: "sec", value: t.s },
3582
+ { key: "days", label: t("days", "\u0623\u064A\u0627\u0645"), value: time.d },
3583
+ { key: "hrs", label: t("hrs", "\u0633\u0627\u0639\u0627\u062A"), value: time.h },
3584
+ { key: "min", label: t("min", "\u062F\u0642\u0627\u0626\u0642"), value: time.m },
3585
+ { key: "sec", label: t("sec", "\u062B\u0648\u0627\u0646\u064D"), value: time.s },
3746
3586
  ].map((u) => (
3747
- <div key={u.label} className="bg-white rounded-lg p-3 min-w-[64px] shadow-sm">
3587
+ <div key={u.key} className="bg-white rounded-lg p-3 min-w-[64px] shadow-sm">
3748
3588
  <div className="text-2xl md:text-3xl font-bold">{String(u.value).padStart(2, "0")}</div>
3749
3589
  <div className="text-xs text-gray-500 uppercase">{u.label}</div>
3750
3590
  </div>
@@ -3772,6 +3612,7 @@ var featuredBlogPosts = {
3772
3612
  name: "Featured Blog Posts",
3773
3613
  description: "3-up grid of recent blog articles with cover image + title + excerpt",
3774
3614
  component: `import type { SectionProps } from "@numueg/theme-sdk";
3615
+ import { useLocale } from "@numueg/theme-sdk";
3775
3616
 
3776
3617
  interface Article {
3777
3618
  handle: string;
@@ -3781,8 +3622,17 @@ interface Article {
3781
3622
  cover_image?: string | null;
3782
3623
  }
3783
3624
 
3625
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3626
+ function useT() {
3627
+ const locale = useLocale();
3628
+ const isAr =
3629
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3630
+ return (en: string, ar: string) => (isAr ? ar : en);
3631
+ }
3632
+
3784
3633
  export default function FeaturedBlogPosts({ settings }: SectionProps) {
3785
- const heading = (settings.heading as string) || "From the blog";
3634
+ const t = useT();
3635
+ const heading = (settings.heading as string) || t("From the blog", "\u0645\u0646 \u0627\u0644\u0645\u062F\u0648\u0646\u0629");
3786
3636
  // Themes can pass articles in via page.data (the blogs route ships
3787
3637
  // them on /[domain]/blogs). On other pages, this section needs a
3788
3638
  // merchant-supplied list of links in the schema.
@@ -3826,9 +3676,19 @@ var contactMap = {
3826
3676
  name: "Contact + Map",
3827
3677
  description: "Two-column contact info (address / phone / hours) + embedded map iframe",
3828
3678
  component: `import type { SectionProps } from "@numueg/theme-sdk";
3679
+ import { useLocale } from "@numueg/theme-sdk";
3680
+
3681
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3682
+ function useT() {
3683
+ const locale = useLocale();
3684
+ const isAr =
3685
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3686
+ return (en: string, ar: string) => (isAr ? ar : en);
3687
+ }
3829
3688
 
3830
3689
  export default function ContactMap({ settings }: SectionProps) {
3831
- const heading = (settings.heading as string) || "Visit us";
3690
+ const t = useT();
3691
+ const heading = (settings.heading as string) || t("Visit us", "\u0632\u0648\u0631\u0648\u0646\u0627");
3832
3692
  const address = (settings.address as string) || "";
3833
3693
  const phone = (settings.phone as string) || "";
3834
3694
  const hours = (settings.hours as string) || "";
@@ -3841,19 +3701,19 @@ export default function ContactMap({ settings }: SectionProps) {
3841
3701
  <div className="space-y-6">
3842
3702
  {address && (
3843
3703
  <div>
3844
- <h3 className="font-semibold mb-1">Address</h3>
3704
+ <h3 className="font-semibold mb-1">{t("Address", "\u0627\u0644\u0639\u0646\u0648\u0627\u0646")}</h3>
3845
3705
  <p className="text-gray-700 whitespace-pre-line">{address}</p>
3846
3706
  </div>
3847
3707
  )}
3848
3708
  {phone && (
3849
3709
  <div>
3850
- <h3 className="font-semibold mb-1">Phone</h3>
3710
+ <h3 className="font-semibold mb-1">{t("Phone", "\u0627\u0644\u0647\u0627\u062A\u0641")}</h3>
3851
3711
  <a href={\`tel:\${phone}\`} className="text-blue-700 hover:underline">{phone}</a>
3852
3712
  </div>
3853
3713
  )}
3854
3714
  {hours && (
3855
3715
  <div>
3856
- <h3 className="font-semibold mb-1">Hours</h3>
3716
+ <h3 className="font-semibold mb-1">{t("Hours", "\u0633\u0627\u0639\u0627\u062A \u0627\u0644\u0639\u0645\u0644")}</h3>
3857
3717
  <p className="text-gray-700 whitespace-pre-line">{hours}</p>
3858
3718
  </div>
3859
3719
  )}
@@ -3862,14 +3722,14 @@ export default function ContactMap({ settings }: SectionProps) {
3862
3722
  {mapUrl ? (
3863
3723
  <iframe
3864
3724
  src={mapUrl}
3865
- title="Map"
3725
+ title={t("Map", "\u062E\u0631\u064A\u0637\u0629")}
3866
3726
  loading="lazy"
3867
3727
  className="w-full h-full border-0"
3868
3728
  referrerPolicy="no-referrer-when-downgrade"
3869
3729
  allowFullScreen
3870
3730
  />
3871
3731
  ) : (
3872
- <div className="flex items-center justify-center h-full text-gray-400">Set a map embed URL</div>
3732
+ <div className="flex items-center justify-center h-full text-gray-400">{t("Set a map embed URL", "\u0623\u0636\u0641 \u0631\u0627\u0628\u0637 \u062A\u0636\u0645\u064A\u0646 \u0627\u0644\u062E\u0631\u064A\u0637\u0629")}</div>
3873
3733
  )}
3874
3734
  </div>
3875
3735
  </div>
@@ -3891,6 +3751,547 @@ export default function ContactMap({ settings }: SectionProps) {
3891
3751
  }
3892
3752
  };
3893
3753
 
3754
+ // src/section-library/entries/product-details.ts
3755
+ var productDetails = {
3756
+ slug: "product-details",
3757
+ name: "Product Details",
3758
+ description: "PDP buy box: gallery, variant/size pickers (size-chart aware) that gate add-to-cart",
3759
+ component: `import { useState } from "react";
3760
+ import type { SectionProps } from "@numueg/theme-sdk";
3761
+ import {
3762
+ useProductOptional,
3763
+ useVariantSelection,
3764
+ useProductSizeChart,
3765
+ useCart,
3766
+ useLocalization,
3767
+ useShop,
3768
+ useLocale,
3769
+ defaultVariant,
3770
+ } from "@numueg/theme-sdk";
3771
+
3772
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
3773
+ function useT() {
3774
+ const locale = useLocale();
3775
+ const isAr =
3776
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
3777
+ return (en: string, ar: string) => (isAr ? ar : en);
3778
+ }
3779
+ // Coerce API money (often string-decimals) before any math.
3780
+ const toNum = (v: unknown) => Number(v) || 0;
3781
+
3782
+ /**
3783
+ * Product detail buy box. Renders the variant picker from real option axes
3784
+ * when present; otherwise derives required size buttons from the product's
3785
+ * size chart (common single-SKU case) and gates add-to-cart on a size pick \u2014
3786
+ * recording the chosen size on the cart note so the merchant fulfils it.
3787
+ */
3788
+ export default function ProductDetails({ settings }: SectionProps) {
3789
+ const t = useT();
3790
+ const product = useProductOptional();
3791
+ const { cart, addItem, updateNote } = useCart();
3792
+ const { formatMoney } = useLocalization();
3793
+ const shop = useShop();
3794
+ const chart = useProductSizeChart();
3795
+ const [qty, setQty] = useState(1);
3796
+ const [pickedSize, setPickedSize] = useState<string | undefined>(undefined);
3797
+ const [pending, setPending] = useState(false);
3798
+
3799
+ const variantSel = useVariantSelection(
3800
+ product ?? { options: [], variants: [] },
3801
+ { autoSelect: true },
3802
+ );
3803
+
3804
+ if (!product) {
3805
+ return (
3806
+ <section className="py-16 px-6 text-center text-gray-500">
3807
+ {t("Product not found.", "\u0644\u0645 \u064A\u062A\u0645 \u0627\u0644\u0639\u062B\u0648\u0631 \u0639\u0644\u0649 \u0627\u0644\u0645\u0646\u062A\u062C.")}
3808
+ </section>
3809
+ );
3810
+ }
3811
+
3812
+ const opts = product.options ?? [];
3813
+ const variants = product.variants ?? [];
3814
+ const hasOptions = opts.length > 0;
3815
+ const { selection, variant, select, availability, isComplete } = variantSel;
3816
+
3817
+ const chartSizes = (chart?.rows ?? [])
3818
+ .map((r) => r.size)
3819
+ .filter((sz): sz is string => Boolean(sz));
3820
+ const needsSize = !hasOptions && variants.length <= 1 && chartSizes.length > 0;
3821
+
3822
+ const activeVariant = hasOptions
3823
+ ? variant
3824
+ : (defaultVariant(product) ?? variants[0] ?? null);
3825
+ const currency = product.currency || shop?.currency;
3826
+ const price = toNum(activeVariant?.price ?? product.price);
3827
+ const images = product.images ?? [];
3828
+ const productName = product.name;
3829
+ const productId = product.id;
3830
+
3831
+ const purchasable =
3832
+ product.in_stock &&
3833
+ (activeVariant?.is_in_stock ?? true) &&
3834
+ (hasOptions ? isComplete : true) &&
3835
+ (needsSize ? Boolean(pickedSize) : true);
3836
+
3837
+ async function handleAdd() {
3838
+ if (pending || !purchasable) return;
3839
+ setPending(true);
3840
+ try {
3841
+ if (needsSize && pickedSize) {
3842
+ const prefix = "\u2022 " + productName + " \u2014 ";
3843
+ const tag = prefix + t("Size", "\u0627\u0644\u0645\u0642\u0627\u0633") + ": ";
3844
+ const prev = cart?.note ?? "";
3845
+ const kept = prev
3846
+ .split("\\n")
3847
+ .filter((line) => line && !line.startsWith(prefix))
3848
+ .join("\\n");
3849
+ await updateNote((kept ? kept + "\\n" : "") + tag + pickedSize);
3850
+ }
3851
+ await addItem(productId, activeVariant?.id, qty);
3852
+ } finally {
3853
+ setPending(false);
3854
+ }
3855
+ }
3856
+
3857
+ const box =
3858
+ "min-w-[3rem] h-12 px-3 inline-flex items-center justify-center border rounded-md text-sm font-medium";
3859
+ const boxActive = "bg-black text-white border-black";
3860
+
3861
+ return (
3862
+ <section className="py-10 px-6 max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-10">
3863
+ <div>
3864
+ {images[0] ? (
3865
+ <img
3866
+ src={images[0].url}
3867
+ alt={images[0].alt || productName}
3868
+ className="w-full aspect-square object-cover rounded-lg bg-gray-100"
3869
+ />
3870
+ ) : (
3871
+ <div className="w-full aspect-square rounded-lg bg-gray-100" />
3872
+ )}
3873
+ </div>
3874
+
3875
+ <div>
3876
+ <h1 className="text-2xl font-bold mb-2">{productName}</h1>
3877
+ <p className="text-xl font-semibold mb-6">
3878
+ {formatMoney(price, currency)}
3879
+ </p>
3880
+
3881
+ {opts.map((opt) => (
3882
+ <div key={opt.name} className="mb-5">
3883
+ <span className="block text-sm font-semibold mb-2">{opt.name}</span>
3884
+ <div className="flex flex-wrap gap-2">
3885
+ {opt.values.map((value) => {
3886
+ const sel = selection[opt.name] === value;
3887
+ const reachable = availability[opt.name]?.has(value) ?? true;
3888
+ return (
3889
+ <button
3890
+ key={value}
3891
+ type="button"
3892
+ disabled={!reachable && !sel}
3893
+ onClick={() => select(opt.name, value)}
3894
+ className={box + (sel ? " " + boxActive : "") + (!reachable && !sel ? " opacity-40 line-through" : "")}
3895
+ >
3896
+ {value}
3897
+ </button>
3898
+ );
3899
+ })}
3900
+ </div>
3901
+ </div>
3902
+ ))}
3903
+
3904
+ {needsSize ? (
3905
+ <div className="mb-5">
3906
+ <span className="block text-sm font-semibold mb-2">
3907
+ {t("Size", "\u0627\u0644\u0645\u0642\u0627\u0633")}
3908
+ </span>
3909
+ <div className="flex flex-wrap gap-2">
3910
+ {chartSizes.map((sz) => (
3911
+ <button
3912
+ key={sz}
3913
+ type="button"
3914
+ onClick={() => setPickedSize(sz)}
3915
+ className={box + (pickedSize === sz ? " " + boxActive : "")}
3916
+ >
3917
+ {sz}
3918
+ </button>
3919
+ ))}
3920
+ </div>
3921
+ {!pickedSize ? (
3922
+ <p className="text-xs text-red-600 mt-2">
3923
+ {t("Please select a size", "\u0627\u062E\u062A\u0631 \u0627\u0644\u0645\u0642\u0627\u0633 \u0645\u0646 \u0641\u0636\u0644\u0643")}
3924
+ </p>
3925
+ ) : null}
3926
+ </div>
3927
+ ) : null}
3928
+
3929
+ <div className="flex items-center gap-3 mb-5">
3930
+ <span className="text-sm font-semibold">{t("Quantity", "\u0627\u0644\u0643\u0645\u064A\u0629")}</span>
3931
+ <div className="inline-flex items-center border rounded-md">
3932
+ <button type="button" className="px-3 py-2" onClick={() => setQty((q) => Math.max(1, q - 1))}>
3933
+ \u2212
3934
+ </button>
3935
+ <span className="px-3">{qty}</span>
3936
+ <button type="button" className="px-3 py-2" onClick={() => setQty((q) => q + 1)}>
3937
+ +
3938
+ </button>
3939
+ </div>
3940
+ </div>
3941
+
3942
+ <button
3943
+ type="button"
3944
+ disabled={!purchasable || pending}
3945
+ onClick={handleAdd}
3946
+ className="w-full py-3 rounded-full bg-black text-white font-semibold disabled:opacity-50"
3947
+ >
3948
+ {pending
3949
+ ? "\u2026"
3950
+ : !product.in_stock
3951
+ ? t("Sold out", "\u0646\u0641\u0630 \u0627\u0644\u0645\u062E\u0632\u0648\u0646")
3952
+ : needsSize && !pickedSize
3953
+ ? t("Select a size", "\u0627\u062E\u062A\u0631 \u0627\u0644\u0645\u0642\u0627\u0633")
3954
+ : (settings.add_to_cart_label as string) ||
3955
+ t("Add to cart", "\u0623\u0636\u0641 \u0625\u0644\u0649 \u0627\u0644\u0633\u0644\u0629")}
3956
+ </button>
3957
+ </div>
3958
+ </section>
3959
+ );
3960
+ }
3961
+ `,
3962
+ schema: {
3963
+ type: "product-details",
3964
+ name: "Product Details",
3965
+ locales: { ar: { name: "\u062A\u0641\u0627\u0635\u064A\u0644 \u0627\u0644\u0645\u0646\u062A\u062C" } },
3966
+ settings: [
3967
+ {
3968
+ type: "text",
3969
+ id: "add_to_cart_label",
3970
+ label: "Add-to-cart label",
3971
+ locales: { ar: { label: "\u0646\u0635 \u0632\u0631 \u0627\u0644\u0625\u0636\u0627\u0641\u0629" } },
3972
+ default: "Add to cart"
3973
+ }
3974
+ ]
3975
+ }
3976
+ };
3977
+
3978
+ // src/section-library/entries/frequently-bought.ts
3979
+ var frequentlyBought = {
3980
+ slug: "frequently-bought",
3981
+ name: "Frequently Bought Together",
3982
+ description: "Bundle the current product with related items; checkboxes + one-tap add-all",
3983
+ component: `import { useMemo, useState } from "react";
3984
+ import type { SectionProps } from "@numueg/theme-sdk";
3985
+ import {
3986
+ useProductOptional,
3987
+ useRelatedProducts,
3988
+ useProducts,
3989
+ useCart,
3990
+ useLocalization,
3991
+ useShop,
3992
+ useLocale,
3993
+ type Product,
3994
+ } from "@numueg/theme-sdk";
3995
+
3996
+ // --- Standards (self-contained so the snippet stays forkable) -------------
3997
+ // Bilingual AR/EN text without a shared import.
3998
+ function useT() {
3999
+ const locale = useLocale();
4000
+ const isAr =
4001
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
4002
+ return (en: string, ar: string) => (isAr ? ar : en);
4003
+ }
4004
+ // Coerce API money (often string-decimals) so totals add instead of concat.
4005
+ const toNum = (v: unknown) => Number(v) || 0;
4006
+ // Tolerate images as {url} objects OR raw url strings.
4007
+ const imgUrl = (p?: Product) => {
4008
+ const raw = p?.images?.[0] as unknown;
4009
+ return typeof raw === "string"
4010
+ ? raw
4011
+ : (raw as { url?: string } | undefined)?.url;
4012
+ };
4013
+
4014
+ interface FbtSettings {
4015
+ title?: string;
4016
+ max_items?: number;
4017
+ }
4018
+
4019
+ /**
4020
+ * Frequently bought together \u2014 seeds the bundle with the current product and
4021
+ * appends related items, falling back to the catalog. Each row is a toggle;
4022
+ * "add all" adds every checked product in one pass. Hidden when < 2 items.
4023
+ */
4024
+ export default function FrequentlyBought({ settings }: SectionProps) {
4025
+ const s = settings as FbtSettings;
4026
+ const t = useT();
4027
+ const max = Math.max(2, Math.min(4, s.max_items ?? 3));
4028
+
4029
+ const product = useProductOptional();
4030
+ const related = useRelatedProducts(product?.id, { limit: max });
4031
+ const catalog = useProducts({ fetchIfMissing: true });
4032
+ const { addItem } = useCart();
4033
+ const { formatMoney } = useLocalization();
4034
+ const shop = useShop();
4035
+
4036
+ const bundle = useMemo(() => {
4037
+ const pool = related.items.length > 0 ? related.items : catalog.products;
4038
+ const seen = new Set<string>();
4039
+ const list: Product[] = [];
4040
+ if (product) {
4041
+ list.push(product);
4042
+ seen.add(product.id);
4043
+ }
4044
+ for (const p of pool) {
4045
+ if (list.length >= max) break;
4046
+ if (seen.has(p.id)) continue;
4047
+ seen.add(p.id);
4048
+ list.push(p);
4049
+ }
4050
+ return list;
4051
+ }, [product, related.items, catalog.products, max]);
4052
+
4053
+ const [selected, setSelected] = useState<Record<string, boolean>>({});
4054
+ const isOn = (id: string) => selected[id] !== false; // default on
4055
+ const [pending, setPending] = useState(false);
4056
+
4057
+ const currency = product?.currency || shop?.currency;
4058
+ const total = bundle
4059
+ .filter((p) => isOn(p.id))
4060
+ .reduce((sum, p) => sum + toNum(p.price), 0);
4061
+
4062
+ if (bundle.length < 2) return null;
4063
+
4064
+ async function addAll() {
4065
+ if (pending) return;
4066
+ setPending(true);
4067
+ try {
4068
+ for (const p of bundle) {
4069
+ if (!isOn(p.id)) continue;
4070
+ await addItem(p.id, p.variants?.[0]?.id, 1);
4071
+ }
4072
+ } finally {
4073
+ setPending(false);
4074
+ }
4075
+ }
4076
+
4077
+ return (
4078
+ <section className="py-12 px-6 max-w-3xl mx-auto">
4079
+ <h2 className="text-lg font-semibold mb-4">
4080
+ {s.title || t("Frequently bought together", "\u064A\u064F\u0634\u062A\u0631\u0649 \u0639\u0627\u062F\u0629\u064B \u0645\u0639\u0627\u064B")}
4081
+ </h2>
4082
+ <div className="flex flex-wrap items-stretch gap-3">
4083
+ {bundle.map((p, i) => (
4084
+ <div key={p.id} className="flex items-center gap-3">
4085
+ <label className="flex items-center gap-3 border rounded-lg p-3 cursor-pointer">
4086
+ <input
4087
+ type="checkbox"
4088
+ checked={isOn(p.id)}
4089
+ onChange={(e) =>
4090
+ setSelected((prev) => ({ ...prev, [p.id]: e.target.checked }))
4091
+ }
4092
+ />
4093
+ {imgUrl(p) ? (
4094
+ <img
4095
+ src={imgUrl(p)}
4096
+ alt={p.name}
4097
+ loading="lazy"
4098
+ className="w-12 h-12 object-cover rounded"
4099
+ />
4100
+ ) : (
4101
+ <span className="w-12 h-12 rounded bg-gray-100" />
4102
+ )}
4103
+ <span className="text-sm">
4104
+ <span className="block font-medium">{p.name}</span>
4105
+ <span className="block text-gray-500">
4106
+ {formatMoney(toNum(p.price), currency)}
4107
+ </span>
4108
+ </span>
4109
+ </label>
4110
+ {i < bundle.length - 1 ? (
4111
+ <span className="text-gray-400" aria-hidden="true">
4112
+ +
4113
+ </span>
4114
+ ) : null}
4115
+ </div>
4116
+ ))}
4117
+ </div>
4118
+ <div className="flex items-center justify-between mt-5">
4119
+ <p className="text-sm">
4120
+ {t("Total", "\u0627\u0644\u0625\u062C\u0645\u0627\u0644\u064A")}:{" "}
4121
+ <strong>{formatMoney(total, currency)}</strong>
4122
+ </p>
4123
+ <button
4124
+ type="button"
4125
+ disabled={pending || total <= 0}
4126
+ onClick={addAll}
4127
+ className="px-5 py-2.5 rounded-full bg-black text-white text-sm font-semibold disabled:opacity-50"
4128
+ >
4129
+ {pending ? "\u2026" : t("Add all to cart", "\u0623\u0636\u0641 \u0627\u0644\u0643\u0644 \u0644\u0644\u0633\u0644\u0629")}
4130
+ </button>
4131
+ </div>
4132
+ </section>
4133
+ );
4134
+ }
4135
+ `,
4136
+ schema: {
4137
+ type: "frequently-bought",
4138
+ name: "Frequently Bought Together",
4139
+ locales: { ar: { name: "\u064A\u064F\u0634\u062A\u0631\u0649 \u0645\u0639\u0627\u064B" } },
4140
+ settings: [
4141
+ {
4142
+ type: "text",
4143
+ id: "title",
4144
+ label: "Section title",
4145
+ locales: { ar: { label: "\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0642\u0633\u0645" } },
4146
+ default: "Frequently bought together"
4147
+ },
4148
+ {
4149
+ type: "range",
4150
+ id: "max_items",
4151
+ label: "Max items in bundle",
4152
+ locales: { ar: { label: "\u0623\u0642\u0635\u0649 \u0639\u062F\u062F \u0644\u0644\u0645\u0646\u062A\u062C\u0627\u062A" } },
4153
+ min: 2,
4154
+ max: 4,
4155
+ default: 3
4156
+ }
4157
+ ]
4158
+ }
4159
+ };
4160
+
4161
+ // src/section-library/entries/size-guide.ts
4162
+ var sizeGuide = {
4163
+ slug: "size-guide",
4164
+ name: "Size Guide",
4165
+ description: "Size-chart trigger + modal table, resolved from the product/store size chart",
4166
+ component: `import { useState } from "react";
4167
+ import type { SectionProps } from "@numueg/theme-sdk";
4168
+ import { useProductSizeChart, useLocale } from "@numueg/theme-sdk";
4169
+
4170
+ // Bilingual AR/EN text without a shared import (keeps the snippet forkable).
4171
+ function useT() {
4172
+ const locale = useLocale();
4173
+ const isAr =
4174
+ typeof locale === "string" && locale.toLowerCase().startsWith("ar");
4175
+ return (en: string, ar: string) => (isAr ? ar : en);
4176
+ }
4177
+
4178
+ /**
4179
+ * Size guide \u2014 renders a trigger button and a modal table. The chart is
4180
+ * resolved by the SDK from the product chart (\`product.attributes.size_chart\`)
4181
+ * with a fallback to the store-wide default. Renders nothing when there's no
4182
+ * chart to show, or when an enabled chart is still empty.
4183
+ */
4184
+ export default function SizeGuide({ settings }: SectionProps) {
4185
+ const t = useT();
4186
+ const [open, setOpen] = useState(false);
4187
+ const chart = useProductSizeChart();
4188
+
4189
+ const hasContent =
4190
+ !!chart &&
4191
+ ((chart.rows?.length ?? 0) > 0 || Boolean(chart.image_url));
4192
+ if (!hasContent || !chart) return null;
4193
+
4194
+ const label = (settings.label as string) || t("Size guide", "\u062F\u0644\u064A\u0644 \u0627\u0644\u0645\u0642\u0627\u0633\u0627\u062A");
4195
+
4196
+ return (
4197
+ <div className="py-4 px-6 max-w-3xl mx-auto">
4198
+ <button
4199
+ type="button"
4200
+ onClick={() => setOpen(true)}
4201
+ className="inline-flex items-center gap-2 text-sm font-medium underline underline-offset-4"
4202
+ >
4203
+ {label}
4204
+ </button>
4205
+
4206
+ {open ? (
4207
+ <div
4208
+ role="dialog"
4209
+ aria-modal="true"
4210
+ aria-label={label}
4211
+ className="fixed inset-0 z-50 flex items-center justify-center p-4"
4212
+ >
4213
+ <div
4214
+ className="absolute inset-0 bg-black/50"
4215
+ onClick={() => setOpen(false)}
4216
+ />
4217
+ <div className="relative bg-white rounded-xl max-w-lg w-full max-h-[85vh] overflow-auto p-6">
4218
+ <div className="flex items-center justify-between mb-4">
4219
+ <h2 className="text-lg font-semibold">
4220
+ {label}
4221
+ {chart.unit ? " (" + chart.unit + ")" : ""}
4222
+ </h2>
4223
+ <button
4224
+ type="button"
4225
+ onClick={() => setOpen(false)}
4226
+ aria-label={t("Close", "\u0625\u063A\u0644\u0627\u0642")}
4227
+ className="p-1"
4228
+ >
4229
+ \u2715
4230
+ </button>
4231
+ </div>
4232
+ {chart.image_url ? (
4233
+ <img
4234
+ src={chart.image_url}
4235
+ alt=""
4236
+ className="w-full rounded mb-4"
4237
+ />
4238
+ ) : null}
4239
+ {chart.rows?.length ? (
4240
+ <table className="w-full text-sm border-collapse">
4241
+ <thead>
4242
+ <tr>
4243
+ <th className="text-start p-2 border-b">
4244
+ {t("Size", "\u0627\u0644\u0645\u0642\u0627\u0633")}
4245
+ </th>
4246
+ {chart.column_headers.map((h, i) => (
4247
+ <th key={i} className="text-start p-2 border-b">
4248
+ {h}
4249
+ </th>
4250
+ ))}
4251
+ </tr>
4252
+ </thead>
4253
+ <tbody>
4254
+ {chart.rows.map((row, ri) => (
4255
+ <tr key={ri}>
4256
+ <th scope="row" className="text-start p-2 border-b font-medium">
4257
+ {row.size}
4258
+ </th>
4259
+ {chart.column_headers.map((_, ci) => (
4260
+ <td key={ci} className="p-2 border-b">
4261
+ {row.values[ci] ?? "\u2014"}
4262
+ </td>
4263
+ ))}
4264
+ </tr>
4265
+ ))}
4266
+ </tbody>
4267
+ </table>
4268
+ ) : null}
4269
+ {chart.notes ? (
4270
+ <p className="text-xs text-gray-500 mt-4">{chart.notes}</p>
4271
+ ) : null}
4272
+ </div>
4273
+ </div>
4274
+ ) : null}
4275
+ </div>
4276
+ );
4277
+ }
4278
+ `,
4279
+ schema: {
4280
+ type: "size-guide",
4281
+ name: "Size Guide",
4282
+ locales: { ar: { name: "\u062F\u0644\u064A\u0644 \u0627\u0644\u0645\u0642\u0627\u0633\u0627\u062A" } },
4283
+ settings: [
4284
+ {
4285
+ type: "text",
4286
+ id: "label",
4287
+ label: "Trigger label",
4288
+ locales: { ar: { label: "\u0646\u0635 \u0627\u0644\u0632\u0631" } },
4289
+ default: "Size guide"
4290
+ }
4291
+ ]
4292
+ }
4293
+ };
4294
+
3894
4295
  // src/section-library/index.ts
3895
4296
  var LIBRARY = [
3896
4297
  heroWithCta,
@@ -3907,7 +4308,10 @@ var LIBRARY = [
3907
4308
  announcementBar,
3908
4309
  countdownTimer,
3909
4310
  featuredBlogPosts,
3910
- contactMap
4311
+ contactMap,
4312
+ productDetails,
4313
+ frequentlyBought,
4314
+ sizeGuide
3911
4315
  ];
3912
4316
  function findEntry(slug) {
3913
4317
  return LIBRARY.find((e) => e.slug === slug);
@@ -3972,7 +4376,7 @@ var addSectionCommand = new import_commander13.Command("add-section").descriptio
3972
4376
  ]
3973
4377
  };
3974
4378
  }
3975
- const componentPath = path12.join(themeDir, "src/sections", `${pascal}.tsx`);
4379
+ const componentPath = path12.join(themeDir, "src/sections", `${slug}.tsx`);
3976
4380
  ensureDirOf(componentPath);
3977
4381
  if (fs12.existsSync(componentPath)) {
3978
4382
  console.error(`Already exists: ${componentPath}`);
@@ -3985,7 +4389,7 @@ var addSectionCommand = new import_commander13.Command("add-section").descriptio
3985
4389
  tryWireMain(themeDir, slug, pascal);
3986
4390
  tryAddToHomePreset(themeDir, slug);
3987
4391
  console.log(`\u2714 Created section '${slug}'.`);
3988
- console.log(` \u2022 src/sections/${pascal}.tsx`);
4392
+ console.log(` \u2022 src/sections/${slug}.tsx`);
3989
4393
  console.log(` \u2022 schemas/sections/${slug}.json`);
3990
4394
  console.log(
3991
4395
  options.fromLibrary ? `
@@ -4024,8 +4428,8 @@ function tryWireMain(themeDir, slug, pascal) {
4024
4428
  const mainPath = path12.join(themeDir, "src/main.tsx");
4025
4429
  if (!fs12.existsSync(mainPath)) return;
4026
4430
  const src = fs12.readFileSync(mainPath, "utf-8");
4027
- if (src.includes(`./sections/${pascal}`)) return;
4028
- const importLine = `import ${pascal} from "./sections/${pascal}";
4431
+ if (src.includes(`./sections/${slug}"`)) return;
4432
+ const importLine = `import ${pascal} from "./sections/${slug}";
4029
4433
  `;
4030
4434
  const lines = src.split("\n");
4031
4435
  let lastImportIdx = -1;
@@ -4037,6 +4441,14 @@ function tryWireMain(themeDir, slug, pascal) {
4037
4441
  } else {
4038
4442
  lines.unshift(importLine.trimEnd());
4039
4443
  }
4444
+ for (let i = 0; i < lines.length; i++) {
4445
+ if (/SECTION_REGISTRY[^=]*=\s*\{/.test(lines[i])) {
4446
+ const indent = (lines[i].match(/^\s*/)?.[0] ?? "") + " ";
4447
+ lines.splice(i + 1, 0, `${indent}"${slug}": ${pascal},`);
4448
+ fs12.writeFileSync(mainPath, lines.join("\n"));
4449
+ return;
4450
+ }
4451
+ }
4040
4452
  let inserted = false;
4041
4453
  for (let i = 0; i < lines.length; i++) {
4042
4454
  if (/section\.type\s*===\s*['"]/.test(lines[i])) {