@numueg/theme-cli 0.5.0 → 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,80 @@
1
+ import { EditableImage } from "@numueg/theme-sdk";
2
+ import { EditableText } from "../lib/EditableText";
3
+ import type { EmpSectionProps } from "../lib/section";
4
+ import { useT } from "../lib/i18n";
5
+
6
+ interface PromoSettings {
7
+ badge_text?: string;
8
+ headline?: string;
9
+ subtitle?: string;
10
+ cta_text?: string;
11
+ cta_link?: string;
12
+ image_url?: string;
13
+ }
14
+
15
+ /**
16
+ * Full-width black promo banner — square image beside a badge + headline +
17
+ * subtitle and a light pill CTA. All text + the image are inline-editable.
18
+ */
19
+ export default function PromoBanner({ id, settings }: EmpSectionProps) {
20
+ const s = settings as PromoSettings;
21
+ const t = useT();
22
+ const badge = s.badge_text ?? t("Limited offer", "عرض محدود");
23
+ const headline = s.headline ?? t("25% off all accessories", "خصم ٢٥٪ على كل الإكسسوارات");
24
+ const subtitle = s.subtitle ?? t("Ends this month — don't miss out!", "العرض ساري لنهاية الشهر. متفوتش الفرصة!");
25
+ const ctaText = s.cta_text ?? t("Shop now", "تسوق الآن");
26
+ const ctaLink = s.cta_link ?? "/products";
27
+
28
+ return (
29
+ <section style={{ paddingBlock: "2rem" }}>
30
+ <div className="nt-container">
31
+ <div className="nt-promo">
32
+ <div className="nt-promo__img">
33
+ <EditableImage
34
+ sectionId={id}
35
+ settingId="image_url"
36
+ src={
37
+ s.image_url ||
38
+ "https://images.unsplash.com/photo-1611085583191-a3b181a88401?w=600"
39
+ }
40
+ alt=""
41
+ />
42
+ </div>
43
+ <div className="nt-promo__body">
44
+ {badge ? (
45
+ <EditableText
46
+ as="span"
47
+ className="nt-promo__badge"
48
+ sectionId={id}
49
+ settingId="badge_text"
50
+ value={badge}
51
+ />
52
+ ) : null}
53
+ <EditableText
54
+ as="h3"
55
+ className="nt-promo__title"
56
+ sectionId={id}
57
+ settingId="headline"
58
+ value={headline}
59
+ />
60
+ <EditableText
61
+ as="p"
62
+ className="nt-promo__sub"
63
+ sectionId={id}
64
+ settingId="subtitle"
65
+ value={subtitle}
66
+ />
67
+ <a className="nt-btn-light" href={ctaLink}>
68
+ <EditableText
69
+ as="span"
70
+ sectionId={id}
71
+ settingId="cta_text"
72
+ value={ctaText}
73
+ />
74
+ </a>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </section>
79
+ );
80
+ }
@@ -0,0 +1,51 @@
1
+ import { RichText } from "@numueg/theme-sdk";
2
+ import { EditableText } from "../lib/EditableText";
3
+ import type { EmpSectionProps } from "../lib/section";
4
+
5
+ interface RichTextSettings {
6
+ title?: string;
7
+ content?: string;
8
+ align?: "start" | "center";
9
+ width?: "narrow" | "wide";
10
+ }
11
+
12
+ /**
13
+ * Rich-text content block — body for the `page` template (About / Shipping /
14
+ * Returns / Terms CMS pages). The merchant's HTML lives in `content` and is
15
+ * rendered through the SDK's sanitising `<RichText>`; the optional title is
16
+ * inline-editable.
17
+ */
18
+ export default function RichTextSection({ id, settings }: EmpSectionProps) {
19
+ const s = settings as RichTextSettings;
20
+ const align = s.align === "center" ? "center" : "start";
21
+ const narrow = s.width !== "wide";
22
+
23
+ return (
24
+ <section className="nt-container" style={{ paddingBlock: "3rem" }}>
25
+ <div
26
+ className="nt-richtext"
27
+ style={{
28
+ textAlign: align,
29
+ maxWidth: narrow ? "44rem" : "64rem",
30
+ marginInline: align === "center" ? "auto" : undefined,
31
+ }}
32
+ >
33
+ {s.title ? (
34
+ <EditableText
35
+ as="h1"
36
+ className="nt-display-sm"
37
+ sectionId={id}
38
+ settingId="title"
39
+ value={s.title}
40
+ style={{ marginBottom: "1.5rem" }}
41
+ />
42
+ ) : null}
43
+ {s.content ? (
44
+ <RichText className="nt-prose" html={s.content} />
45
+ ) : (
46
+ <p className="nt-placeholder">أضف المحتوى من إعدادات القسم.</p>
47
+ )}
48
+ </div>
49
+ </section>
50
+ );
51
+ }
@@ -0,0 +1,93 @@
1
+ import {
2
+ useMemo,
3
+ useState } from "react";
4
+ import {
5
+ usePage,
6
+ useProducts,
7
+ type Product,
8
+ } from "@numueg/theme-sdk";
9
+ import { EditableText } from "../lib/EditableText";
10
+ import type { EmpSectionProps } from "../lib/section";
11
+ import { ProductCard } from "../lib/ProductCard";
12
+
13
+ interface SearchSettings {
14
+ title?: string;
15
+ search_placeholder?: string;
16
+ no_results_text?: string;
17
+ columns_desktop?: number;
18
+ }
19
+
20
+ /**
21
+ * Search results — body for the `search` template. Seeds the query from
22
+ * `page.data.query` (the storefront /search route stashes it) and lets the
23
+ * visitor refine in a live box. Uses the host's pre-fetched `page.data.results`
24
+ * when present, else filters `useProducts()` client-side (customizer preview).
25
+ */
26
+ export default function SearchResults({ id, settings }: EmpSectionProps) {
27
+ const s = settings as SearchSettings;
28
+ const cols = Math.max(2, Math.min(5, s.columns_desktop ?? 4));
29
+ const placeholder = s.search_placeholder ?? "ابحث عن المنتجات…";
30
+
31
+ const page = usePage();
32
+ const pd = page?.data as Record<string, unknown> | undefined;
33
+ const initialQuery =
34
+ (pd?.query as string | undefined) ?? (pd?.q as string | undefined) ?? "";
35
+ const preFetched = (pd?.results as Product[] | undefined) ?? [];
36
+
37
+ const { products } = useProducts({ fetchIfMissing: true });
38
+ const [query, setQuery] = useState(initialQuery);
39
+
40
+ const matches = useMemo(() => {
41
+ const needle = query.trim().toLowerCase();
42
+ if (!needle) return preFetched;
43
+ const pool = preFetched.length > 0 ? preFetched : products;
44
+ return pool.filter(
45
+ (p) =>
46
+ p.name?.toLowerCase().includes(needle) ||
47
+ p.description?.toLowerCase().includes(needle),
48
+ );
49
+ }, [query, preFetched, products]);
50
+
51
+ return (
52
+ <section className="nt-container" style={{ paddingBlock: "2.5rem" }}>
53
+ <EditableText
54
+ as="h1"
55
+ className="nt-display-sm"
56
+ sectionId={id}
57
+ settingId="title"
58
+ value={s.title ?? "البحث"}
59
+ style={{ marginBottom: "1.25rem" }}
60
+ />
61
+
62
+ <input
63
+ className="nt-input"
64
+ type="search"
65
+ value={query}
66
+ placeholder={placeholder}
67
+ onChange={(e) => setQuery(e.target.value)}
68
+ style={{ marginBottom: "1.5rem" }}
69
+ />
70
+
71
+ {query.trim() ? (
72
+ <p className="nt-label" style={{ marginBottom: "1.5rem" }}>
73
+ {matches.length} نتيجة لـ "{query.trim()}"
74
+ </p>
75
+ ) : null}
76
+
77
+ {matches.length > 0 ? (
78
+ <div className="nt-grid" style={{ ["--cols" as string]: cols }}>
79
+ {matches.map((p) => (
80
+ <ProductCard key={p.id} product={p} />
81
+ ))}
82
+ </div>
83
+ ) : (
84
+ <p className="nt-placeholder" style={{ paddingBlock: "3rem" }}>
85
+ {query.trim()
86
+ ? s.no_results_text ||
87
+ "لا توجد نتائج. جرّب كلمة أخرى أو تصفّح كل التشكيلة."
88
+ : "اكتب كلمة للبحث في المتجر."}
89
+ </p>
90
+ )}
91
+ </section>
92
+ );
93
+ }
@@ -0,0 +1,109 @@
1
+ import {
2
+ useState } from "react";
3
+ import { useProductSizeChart,
4
+ } from "@numueg/theme-sdk";
5
+ import { EditableText } from "../lib/EditableText";
6
+ import type { EmpSectionProps } from "../lib/section";
7
+
8
+ interface SizeChartSettings {
9
+ trigger_label?: string;
10
+ title?: string;
11
+ }
12
+
13
+ /**
14
+ * Size guide. Resolution (product `custom` → store `default` → `off`) is owned
15
+ * by the SDK's `useProductSizeChart()` so every theme resolves it identically
16
+ * to the merchant hub + backend validator. Renders a trigger pill + modal
17
+ * table; returns null when there's nothing to show.
18
+ */
19
+ export default function SizeChart({ id, settings }: EmpSectionProps) {
20
+ const s = settings as SizeChartSettings;
21
+ const [open, setOpen] = useState(false);
22
+ const chart = useProductSizeChart();
23
+
24
+ if (!chart) return null;
25
+ const unit = chart.unit ? ` (${chart.unit})` : "";
26
+
27
+ return (
28
+ <div className="nt-container" style={{ paddingBottom: "1rem" }}>
29
+ <button
30
+ className="nt-sizeguide__trigger"
31
+ type="button"
32
+ onClick={() => setOpen(true)}
33
+ >
34
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
35
+ <path d="M3 7h18v10H3z" />
36
+ <path d="M7 7v4M11 7v6M15 7v4M19 7v6" />
37
+ </svg>
38
+ <EditableText
39
+ as="span"
40
+ sectionId={id}
41
+ settingId="trigger_label"
42
+ value={s.trigger_label || "دليل المقاسات"}
43
+ />
44
+ </button>
45
+
46
+ {open ? (
47
+ <div
48
+ className="nt-modal"
49
+ role="dialog"
50
+ aria-modal="true"
51
+ aria-label={s.title || "دليل المقاسات"}
52
+ >
53
+ <div className="nt-modal__overlay" onClick={() => setOpen(false)} />
54
+ <div className="nt-modal__panel">
55
+ <div className="nt-modal__head">
56
+ <h2 className="nt-modal__title">
57
+ {(s.title || "دليل المقاسات") + unit}
58
+ </h2>
59
+ <button
60
+ className="nt-drawer__close"
61
+ type="button"
62
+ onClick={() => setOpen(false)}
63
+ aria-label="إغلاق"
64
+ >
65
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
66
+ <path d="M18 6 6 18M6 6l12 12" />
67
+ </svg>
68
+ </button>
69
+ </div>
70
+ <div className="nt-modal__body">
71
+ {chart.image_url ? (
72
+ <img
73
+ className="nt-sizeguide__img"
74
+ src={chart.image_url}
75
+ alt=""
76
+ />
77
+ ) : null}
78
+ <table className="nt-sizetable">
79
+ <thead>
80
+ <tr>
81
+ <th>المقاس</th>
82
+ {chart.column_headers.map((h, i) => (
83
+ <th key={i}>{h}</th>
84
+ ))}
85
+ </tr>
86
+ </thead>
87
+ <tbody>
88
+ {chart.rows.map((row, ri) => (
89
+ <tr key={ri}>
90
+ <th scope="row">{row.size}</th>
91
+ {chart.column_headers.map((_, ci) => (
92
+ <td key={ci}>{row.values[ci] ?? "—"}</td>
93
+ ))}
94
+ </tr>
95
+ ))}
96
+ </tbody>
97
+ </table>
98
+ {chart.notes ? (
99
+ <p className="nt-muted" style={{ fontSize: "0.8125rem", marginTop: "1rem" }}>
100
+ {chart.notes}
101
+ </p>
102
+ ) : null}
103
+ </div>
104
+ </div>
105
+ </div>
106
+ ) : null}
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1,112 @@
1
+ import { type BlockInstance } from "@numueg/theme-sdk";
2
+ import { EditableText } from "../lib/EditableText";
3
+ import type { EmpSectionProps } from "../lib/section";
4
+ import { useT } from "../lib/i18n";
5
+
6
+ interface TestimonialsSettings {
7
+ title?: string;
8
+ }
9
+
10
+ /** Customer reviews — three bordered cards on the off-white canvas, each with
11
+ * black star rating, quote and an avatar initial. Cards come from `review`
12
+ * blocks the merchant arranges in the customizer. */
13
+ export default function Testimonials({
14
+ id,
15
+ settings,
16
+ blocks,
17
+ blockOrder,
18
+ }: EmpSectionProps) {
19
+ const s = settings as TestimonialsSettings;
20
+ const t = useT();
21
+ const title = s.title ?? t("What our customers say", "رأي عملائنا");
22
+
23
+ const reviews = (blockOrder ?? [])
24
+ .map((bid) => ({ bid, block: blocks?.[bid] }))
25
+ .filter(
26
+ (x): x is { bid: string; block: BlockInstance } =>
27
+ !!x.block && !x.block.disabled && x.block.type === "review",
28
+ );
29
+
30
+ if (reviews.length === 0) return null;
31
+
32
+ return (
33
+ <section className="nt-section nt-bg-white">
34
+ <div className="nt-container">
35
+ <EditableText
36
+ as="h2"
37
+ className="nt-display-sm"
38
+ sectionId={id}
39
+ settingId="title"
40
+ value={title}
41
+ style={{ textAlign: "center", marginBottom: "2rem" }}
42
+ />
43
+ <div className="nt-tgrid">
44
+ {reviews.map(({ bid, block }) => {
45
+ const rating = Math.max(
46
+ 0,
47
+ Math.min(5, Number(block.settings.rating ?? 5)),
48
+ );
49
+ const author = (block.settings.author as string) || "عميل";
50
+ const city = block.settings.city as string | undefined;
51
+ const text = (block.settings.text as string) || "";
52
+ return (
53
+ <div className="nt-tcard" key={bid}>
54
+ <div className="nt-stars" aria-label={`${rating} من 5`}>
55
+ {Array.from({ length: 5 }).map((_, i) => (
56
+ <Star key={i} filled={i < rating} />
57
+ ))}
58
+ </div>
59
+ <EditableText
60
+ as="p"
61
+ className="nt-tquote"
62
+ sectionId={id}
63
+ blockId={bid}
64
+ settingId="text"
65
+ value={`"${text}"`}
66
+ />
67
+ <div className="nt-tauthor">
68
+ <span className="nt-tavatar">{author[0]}</span>
69
+ <div>
70
+ <EditableText
71
+ as="p"
72
+ className="nt-tname"
73
+ sectionId={id}
74
+ blockId={bid}
75
+ settingId="author"
76
+ value={author}
77
+ />
78
+ {city ? (
79
+ <EditableText
80
+ as="p"
81
+ className="nt-tcity"
82
+ sectionId={id}
83
+ blockId={bid}
84
+ settingId="city"
85
+ value={city}
86
+ />
87
+ ) : null}
88
+ </div>
89
+ </div>
90
+ </div>
91
+ );
92
+ })}
93
+ </div>
94
+ </div>
95
+ </section>
96
+ );
97
+ }
98
+
99
+ const Star = ({ filled }: { filled: boolean }) => (
100
+ <svg
101
+ width="14"
102
+ height="14"
103
+ viewBox="0 0 24 24"
104
+ fill={filled ? "currentColor" : "none"}
105
+ stroke="currentColor"
106
+ strokeWidth="2"
107
+ className={`nt-star${filled ? " is-filled" : ""}`}
108
+ aria-hidden="true"
109
+ >
110
+ <path d="m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
111
+ </svg>
112
+ );