@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
@@ -0,0 +1,230 @@
1
+ import type { ComponentType } from "react";
2
+ import {
3
+ defineThemeEntry,
4
+ Section,
5
+ useDirection,
6
+ type ThemeSettingsV3,
7
+ } from "@numueg/theme-sdk";
8
+ import manifest from "../theme.json";
9
+
10
+ import Header from "./sections/Header";
11
+ import Footer from "./sections/Footer";
12
+ import Hero from "./sections/hero";
13
+ import Marquee from "./sections/marquee";
14
+ import FeaturedCollection from "./sections/featured_collection";
15
+ import Categories from "./sections/categories";
16
+ import PromoBanner from "./sections/promo_banner";
17
+ import Testimonials from "./sections/testimonials";
18
+ import Newsletter from "./sections/newsletter";
19
+ import ProductDetails from "./sections/product_details";
20
+ import SizeChart from "./sections/size_chart";
21
+ import FrequentlyBought from "./sections/frequently_bought";
22
+ import ProductGrid from "./sections/product_grid";
23
+ import CartSummary from "./sections/cart_summary";
24
+ import OrderConfirmation from "./sections/order_confirmation";
25
+ import ImageWithText from "./sections/image_with_text";
26
+ import RichTextSection from "./sections/rich_text";
27
+ import NotFound from "./sections/not_found";
28
+ import SearchResults from "./sections/search_results";
29
+ import Account from "./sections/account";
30
+ import AboutSection from "./sections/about_section";
31
+
32
+ const SECTION_REGISTRY: Record<string, ComponentType<any>> = {
33
+ header: Header,
34
+ footer: Footer,
35
+ hero: Hero,
36
+ marquee: Marquee,
37
+ featured_collection: FeaturedCollection,
38
+ categories: Categories,
39
+ promo_banner: PromoBanner,
40
+ testimonials: Testimonials,
41
+ newsletter: Newsletter,
42
+ product_details: ProductDetails,
43
+ size_chart: SizeChart,
44
+ frequently_bought: FrequentlyBought,
45
+ product_grid: ProductGrid,
46
+ cart_summary: CartSummary,
47
+ order_confirmation: OrderConfirmation,
48
+ image_with_text: ImageWithText,
49
+ rich_text: RichTextSection,
50
+ not_found: NotFound,
51
+ search_results: SearchResults,
52
+ account: Account,
53
+ about_section: AboutSection,
54
+ };
55
+
56
+ const isKnown = (type: string) => Boolean(SECTION_REGISTRY[type]);
57
+
58
+ interface SectionLike {
59
+ type: string;
60
+ settings?: Record<string, any>;
61
+ blocks?: Record<string, any> | any[];
62
+ block_order?: string[];
63
+ disabled?: boolean;
64
+ }
65
+ interface GroupLike {
66
+ sections?: Record<string, SectionLike> | SectionLike[];
67
+ order?: string[];
68
+ }
69
+
70
+ // The theme's own default templates/groups, baked into the bundle. The host
71
+ // passes EMPTY templates for a marketplace PREVIEW (before install / before the
72
+ // merchant customizes), expecting the bundle to fall back to these.
73
+ const PRESETS = (manifest as any).presets ?? {};
74
+ const BUILTIN_TEMPLATES: Record<string, GroupLike> = PRESETS.templates ?? {};
75
+ const BUILTIN_GROUPS: Record<string, GroupLike> = PRESETS.section_groups ?? {};
76
+
77
+ /** Normalise a section instance so blocks are always `{map}` + `order[]`
78
+ * (presets store blocks as an array; resolved host data stores a map). */
79
+ function normaliseInstance(instance: SectionLike): SectionLike {
80
+ if (Array.isArray(instance.blocks)) {
81
+ const map: Record<string, any> = {};
82
+ const order: string[] = [];
83
+ instance.blocks.forEach((b: any, i: number) => {
84
+ const id = `${b?.type ?? "block"}-${i}`;
85
+ map[id] = b;
86
+ order.push(id);
87
+ });
88
+ return { ...instance, blocks: map, block_order: order };
89
+ }
90
+ return instance;
91
+ }
92
+
93
+ /** Normalise a template/group (array OR map+order) → ordered instance list. */
94
+ function resolveSections(
95
+ group: GroupLike | undefined,
96
+ ): Array<{ id: string; instance: SectionLike }> {
97
+ if (!group || !group.sections) return [];
98
+ if (Array.isArray(group.sections)) {
99
+ return group.sections.map((instance, idx) => ({
100
+ id: `${instance.type}-${idx}`,
101
+ instance: normaliseInstance(instance),
102
+ }));
103
+ }
104
+ const map = group.sections as Record<string, SectionLike>;
105
+ const order = group.order ?? Object.keys(map);
106
+ const out: Array<{ id: string; instance: SectionLike }> = [];
107
+ for (const id of order) {
108
+ const instance = map[id];
109
+ if (instance) out.push({ id, instance: normaliseInstance(instance) });
110
+ }
111
+ return out;
112
+ }
113
+
114
+ /** Prefer the host's customisation; fall back to bundled presets (preview). */
115
+ function selectSections(
116
+ host: GroupLike | undefined,
117
+ builtin: GroupLike | undefined,
118
+ ): Array<{ id: string; instance: SectionLike }> {
119
+ const hostList = resolveSections(host).filter((s) => isKnown(s.instance.type));
120
+ if (hostList.length > 0) return hostList;
121
+ return resolveSections(builtin).filter((s) => isKnown(s.instance.type));
122
+ }
123
+
124
+ function styleVars(global: Record<string, any>): React.CSSProperties {
125
+ const vars: Record<string, string> = {};
126
+ if (global.accent_color) vars["--nt-accent"] = global.accent_color;
127
+ if (global.foreground_color) vars["--nt-fg"] = global.foreground_color;
128
+ if (global.background_color) vars["--nt-bg"] = global.background_color;
129
+ if (global.font_family) {
130
+ const stack = `"${global.font_family}", "Inter", system-ui, sans-serif`;
131
+ vars["--nt-font-body"] = stack;
132
+ vars["--nt-font-display"] = stack;
133
+ }
134
+ return vars as React.CSSProperties;
135
+ }
136
+
137
+ function renderList(
138
+ list: Array<{ id: string; instance: SectionLike }>,
139
+ keyPrefix: string,
140
+ groupId?: string,
141
+ extra?: Record<string, unknown>,
142
+ ) {
143
+ return list.map(({ id, instance }) => {
144
+ if (instance.disabled) return null;
145
+ const Component = SECTION_REGISTRY[instance.type];
146
+ if (!Component) return null;
147
+ // <Section> emits the data-section-id the customizer's PreviewBridge reads
148
+ // for click-to-select; passing the id down lets each component wire
149
+ // <EditableText>/<EditableImage> for inline field editing.
150
+ return (
151
+ <Section
152
+ key={`${keyPrefix}-${id}`}
153
+ id={id}
154
+ type={instance.type}
155
+ groupId={groupId}
156
+ >
157
+ <Component
158
+ id={id}
159
+ type={instance.type}
160
+ settings={instance.settings}
161
+ blocks={instance.blocks}
162
+ blockOrder={instance.block_order}
163
+ {...extra}
164
+ />
165
+ </Section>
166
+ );
167
+ });
168
+ }
169
+
170
+ interface ThemeProps {
171
+ themeSettings: ThemeSettingsV3;
172
+ currentTemplate: string;
173
+ }
174
+
175
+ export default function Theme({ themeSettings, currentTemplate }: ThemeProps) {
176
+ const pageType = currentTemplate || "home";
177
+ const global = themeSettings.global_settings || {};
178
+ const dir = useDirection();
179
+
180
+ const hostTemplates = (themeSettings.templates ?? {}) as Record<
181
+ string,
182
+ GroupLike
183
+ >;
184
+ const hostTemplate =
185
+ hostTemplates[pageType] ?? hostTemplates.page ?? hostTemplates.home;
186
+ const builtinTemplate =
187
+ BUILTIN_TEMPLATES[pageType] ??
188
+ BUILTIN_TEMPLATES.page ??
189
+ BUILTIN_TEMPLATES.home;
190
+
191
+ const hostGroups = (themeSettings.section_groups ?? {}) as Record<
192
+ string,
193
+ GroupLike
194
+ >;
195
+ const headerSections = selectSections(hostGroups.header, BUILTIN_GROUPS.header);
196
+ const footerSections = selectSections(hostGroups.footer, BUILTIN_GROUPS.footer);
197
+ const bodySections = selectSections(hostTemplate, builtinTemplate);
198
+
199
+ // The header is fixed/overlay; pages that don't open with a full-bleed hero
200
+ // need top padding so their first section clears it.
201
+ const firstType = bodySections[0]?.instance.type;
202
+ const bleedTop = firstType === "hero";
203
+
204
+ return (
205
+ <div className="nt" dir={dir} style={styleVars(global)}>
206
+ {renderList(headerSections, "hg", "header", { solidHeader: !bleedTop })}
207
+ {!bleedTop && headerSections.length > 0 ? (
208
+ <div className="nt-spacer-top" aria-hidden="true" />
209
+ ) : null}
210
+ {bodySections.length > 0 ? (
211
+ renderList(bodySections, pageType)
212
+ ) : (
213
+ <section className="nt-page nt-container">
214
+ <p className="nt-placeholder">
215
+ No template configured for "{pageType}".
216
+ </p>
217
+ </section>
218
+ )}
219
+ {renderList(footerSections, "fg", "footer")}
220
+ </div>
221
+ );
222
+ }
223
+
224
+ // ── Entry: ONE definition → mount (client/hydrate) + createApp (SSR) ────────
225
+ const entry = defineThemeEntry(({ themeSettings, currentTemplate }) => (
226
+ <Theme themeSettings={themeSettings} currentTemplate={currentTemplate} />
227
+ ));
228
+
229
+ export const mount = entry.mount;
230
+ export const createApp = entry.createApp;
@@ -0,0 +1,161 @@
1
+ import {
2
+ useEffect,
3
+ useState } from "react";
4
+ import { useShop,
5
+ useCollections,
6
+ } from "@numueg/theme-sdk";
7
+ import { EditableText } from "../lib/EditableText";
8
+ import type { EmpSectionProps } from "../lib/section";
9
+ import { useT } from "../lib/i18n";
10
+
11
+ interface FooterSettings {
12
+ brand_name?: string;
13
+ description?: string;
14
+ ticker_text?: string;
15
+ instagram?: string;
16
+ facebook?: string;
17
+ twitter?: string;
18
+ copyright?: string;
19
+ }
20
+
21
+ export default function Footer({ id, settings }: EmpSectionProps) {
22
+ const s = settings as FooterSettings;
23
+ const t = useT();
24
+ const shop = useShop();
25
+ const { collections } = useCollections({ limit: 5 });
26
+ // Prefer the real store name; the theme placeholder ("STORE") is treated as
27
+ // unset so it never overrides the store name baked in at activation.
28
+ const brand =
29
+ s.brand_name && s.brand_name !== "STORE"
30
+ ? s.brand_name
31
+ : shop?.name || s.brand_name || "STORE";
32
+ const ticker = s.ticker_text || "100% مستقل";
33
+
34
+ // Year is computed client-side to keep the SSR render path deterministic.
35
+ const [year, setYear] = useState<number | null>(null);
36
+ useEffect(() => setYear(new Date().getFullYear()), []);
37
+
38
+ const socials = [
39
+ { name: "Instagram", url: s.instagram, icon: <IconInstagram /> },
40
+ { name: "X", url: s.twitter, icon: <IconX /> },
41
+ { name: "Facebook", url: s.facebook, icon: <IconFacebook /> },
42
+ ].filter((x) => x.url);
43
+
44
+ const tickerItems = Array.from({ length: 10 });
45
+
46
+ return (
47
+ <footer className="nt-footer">
48
+ {/* Ticker */}
49
+ <div className="nt-footer__ticker">
50
+ <div className="nt-marquee__track">
51
+ {tickerItems.concat(tickerItems).map((_, i) => (
52
+ <span className="nt-marquee__item" key={i}>
53
+ <span className="nt-marquee__text">{ticker}</span>
54
+ <span className="nt-marquee__dot">●</span>
55
+ <span className="nt-marquee__sub">{brand}</span>
56
+ <span className="nt-marquee__dot">●</span>
57
+ </span>
58
+ ))}
59
+ </div>
60
+ </div>
61
+
62
+ <div className="nt-container">
63
+ <div className="nt-footer__grid">
64
+ {/* Brand */}
65
+ <div className="nt-footer__brand">
66
+ <EditableText
67
+ as="h3"
68
+ sectionId={id}
69
+ settingId="brand_name"
70
+ value={brand}
71
+ />
72
+ <EditableText
73
+ as="p"
74
+ className="nt-footer__desc"
75
+ sectionId={id}
76
+ settingId="description"
77
+ value={
78
+ s.description ||
79
+ "متجر مستقل يقدّم تشكيلة مختارة بعناية. تصميم نظيف، جودة تدوم."
80
+ }
81
+ />
82
+ <div className="nt-footer__social">
83
+ {socials.length > 0
84
+ ? socials.map((so) => (
85
+ <a
86
+ key={so.name}
87
+ href={so.url}
88
+ target="_blank"
89
+ rel="noopener noreferrer"
90
+ aria-label={so.name}
91
+ >
92
+ {so.icon}
93
+ </a>
94
+ ))
95
+ : [<IconInstagram key="i" />, <IconX key="x" />, <IconFacebook key="f" />].map(
96
+ (ic, i) => (
97
+ <span key={i} style={{ opacity: 0.3 }}>
98
+ {ic}
99
+ </span>
100
+ ),
101
+ )}
102
+ </div>
103
+ </div>
104
+
105
+ {/* Shop links */}
106
+ <div>
107
+ <p className="nt-footer__heading">{t("Shop", "المتجر")}</p>
108
+ <div className="nt-footer__links">
109
+ <a href="/products">{t("All products", "كل المنتجات")}</a>
110
+ {collections.slice(0, 5).map((c) => (
111
+ <a key={c.id} href={`/collections/${c.slug}`}>
112
+ {c.name}
113
+ </a>
114
+ ))}
115
+ </div>
116
+ </div>
117
+
118
+ {/* Help links */}
119
+ <div>
120
+ <p className="nt-footer__heading">{t("Help", "المساعدة")}</p>
121
+ <div className="nt-footer__links">
122
+ <a href="/pages/contact">{t("Contact us", "تواصل معنا")}</a>
123
+ <a href="/pages/shipping">{t("Shipping", "الشحن")}</a>
124
+ <a href="/pages/returns">{t("Returns", "الإرجاع")}</a>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <div className="nt-footer__bottom">
130
+ <p className="nt-footer__copy">
131
+ {s.copyright || `© ${year ?? ""} ${brand}`.trim()}
132
+ </p>
133
+ <div className="nt-paybadges">
134
+ <span className="nt-paybadge">VISA</span>
135
+ <span className="nt-paybadge">Mastercard</span>
136
+ <span className="nt-paybadge">mada</span>
137
+ <span className="nt-paybadge">Apple Pay</span>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </footer>
142
+ );
143
+ }
144
+
145
+ const IconInstagram = () => (
146
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
147
+ <rect x="2" y="2" width="20" height="20" rx="5" />
148
+ <circle cx="12" cy="12" r="5" />
149
+ <circle cx="17.5" cy="6.5" r="1.5" fill="currentColor" stroke="none" />
150
+ </svg>
151
+ );
152
+ const IconX = () => (
153
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
154
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
155
+ </svg>
156
+ );
157
+ const IconFacebook = () => (
158
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
159
+ <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
160
+ </svg>
161
+ );