easyorders 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cli.js CHANGED
@@ -87,7 +87,7 @@ async function createTheme() {
87
87
  console.log("");
88
88
  console.log(` ${DIM}Next steps:${RESET}`);
89
89
  console.log(` ${CYAN}cd ${projectName}${RESET}`);
90
- console.log(` ${CYAN}npx easyorders start${RESET}`);
90
+ console.log(` ${CYAN} easyorders start${RESET}`);
91
91
  console.log("");
92
92
  }
93
93
  async function startDev() {
@@ -103,15 +103,36 @@ async function startDev() {
103
103
  const serverDir = path.join(CLI_ROOT, "server");
104
104
  const indexScript = path.join(serverDir, "index.js");
105
105
  const tunnelScript = path.join(serverDir, "tunnel.js");
106
- const cmd = `node "${tunnelScript}" & node "${indexScript}"`;
107
- const child = spawn(cmd, {
106
+ const childEnv = { ...process.env, THEME_DIR: themeDir };
107
+ // Spawn both processes independently for cross-platform support
108
+ // (Using shell `&` only works on Unix; on Windows cmd.exe it runs sequentially and hangs)
109
+ const tunnel = spawn("node", [tunnelScript], {
108
110
  stdio: "inherit",
109
111
  cwd,
110
- env: { ...process.env, THEME_DIR: themeDir },
111
- shell: true,
112
+ env: childEnv,
112
113
  });
113
- child.on("exit", (code) => {
114
- process.exit(code ?? 0);
114
+ const server = spawn("node", [indexScript], {
115
+ stdio: "inherit",
116
+ cwd,
117
+ env: childEnv,
118
+ });
119
+ let exitCode = 0;
120
+ function cleanup(code) {
121
+ exitCode = code;
122
+ tunnel.kill();
123
+ server.kill();
124
+ }
125
+ tunnel.on("exit", (code) => cleanup(code ?? 1));
126
+ server.on("exit", (code) => cleanup(code ?? 1));
127
+ process.on("SIGINT", () => {
128
+ tunnel.kill();
129
+ server.kill();
130
+ process.exit(exitCode);
131
+ });
132
+ process.on("SIGTERM", () => {
133
+ tunnel.kill();
134
+ server.kill();
135
+ process.exit(exitCode);
115
136
  });
116
137
  }
117
138
  function showMenu() {
@@ -23,6 +23,7 @@ const port = Number(process.env.PORT) || 4000;
23
23
  const URL_FILE = path.resolve(process.cwd(), ".tunnel-url");
24
24
  const THEME_DIR = process.env.THEME_DIR || path.resolve(process.cwd(), "theme");
25
25
  const SECTIONS_DIR = path.join(THEME_DIR, "sections");
26
+ const HOME_SECTIONS_DIR = path.join(THEME_DIR, "home-sections");
26
27
  const AUTH_READY_FILE = path.resolve(process.cwd(), ".auth-ready");
27
28
  app.use(express.json());
28
29
  function getBaseUrl(req) {
@@ -49,6 +50,26 @@ function buildSections() {
49
50
  template: fs.readFileSync(path.join(SECTIONS_DIR, file), "utf-8"),
50
51
  }));
51
52
  }
53
+ function buildHomeSections() {
54
+ if (!fs.existsSync(HOME_SECTIONS_DIR))
55
+ return [];
56
+ const result = [];
57
+ const entries = fs.readdirSync(HOME_SECTIONS_DIR, { withFileTypes: true });
58
+ for (const entry of entries) {
59
+ if (!entry.isDirectory())
60
+ continue;
61
+ const key = entry.name.replace(/-/g, "_");
62
+ const folderPath = path.join(HOME_SECTIONS_DIR, entry.name);
63
+ const configPath = path.join(folderPath, "config.json");
64
+ const templatePath = path.join(folderPath, "template.liquid");
65
+ if (!fs.existsSync(configPath) || !fs.existsSync(templatePath))
66
+ continue;
67
+ const config = readJsonFile(configPath);
68
+ const template = fs.readFileSync(templatePath, "utf-8");
69
+ result.push({ key, ...config, template });
70
+ }
71
+ return result;
72
+ }
52
73
  function extractSubdomain(input) {
53
74
  const trimmed = input.trim();
54
75
  try {
@@ -211,6 +232,7 @@ app.get("/theme", (req, res) => {
211
232
  const baseUrl = getBaseUrl(req);
212
233
  res.json({
213
234
  sections: buildSections(),
235
+ home_sections: buildHomeSections(),
214
236
  theme_data: readJsonFile(path.join(THEME_DIR, "theme-data.json")),
215
237
  config: readJsonFile(path.join(THEME_DIR, "config.json")),
216
238
  style: `${baseUrl}/style.css`,
@@ -0,0 +1,66 @@
1
+ {
2
+ "icon": "https://api.iconify.design/lucide:grid-3x3.svg",
3
+ "label": "Category mosaic",
4
+ "section_schema": [
5
+ {
6
+ "name": "title",
7
+ "type": "string",
8
+ "default": "Shop by mood",
9
+ "description": "Section heading"
10
+ },
11
+ {
12
+ "name": "subtitle",
13
+ "type": "string",
14
+ "default": "Curated drops for work, weekend, and everything between.",
15
+ "description": "Subheading under the title"
16
+ },
17
+ {
18
+ "name": "columns",
19
+ "type": "select",
20
+ "default": "3",
21
+ "description": "Columns on large screens",
22
+ "options": [
23
+ { "label": "Two columns", "value": "2" },
24
+ { "label": "Three columns", "value": "3" },
25
+ { "label": "Four columns", "value": "4" }
26
+ ]
27
+ },
28
+ {
29
+ "name": "gap_tight",
30
+ "type": "boolean",
31
+ "default": false,
32
+ "description": "Use tighter spacing between tiles"
33
+ },
34
+ {
35
+ "name": "tiles",
36
+ "type": "object_array",
37
+ "description": "Category or collection tiles",
38
+ "fields": [
39
+ {
40
+ "name": "image",
41
+ "type": "string",
42
+ "default": "",
43
+ "description": "Tile image URL"
44
+ },
45
+ {
46
+ "name": "label",
47
+ "type": "string",
48
+ "default": "Dresses",
49
+ "description": "Tile title"
50
+ },
51
+ {
52
+ "name": "tagline",
53
+ "type": "string",
54
+ "default": "Fluid lines",
55
+ "description": "Short line under the title"
56
+ },
57
+ {
58
+ "name": "url",
59
+ "type": "string",
60
+ "default": "/products",
61
+ "description": "Tile link URL"
62
+ }
63
+ ]
64
+ }
65
+ ]
66
+ }
@@ -0,0 +1,151 @@
1
+ <section
2
+ class="category-mosaic{% if section_data.gap_tight %} category-mosaic--tight{% endif %}"
3
+ style="--mosaic-cols: {{ section_data.columns | default: '3' }};"
4
+ >
5
+ <div class="category-mosaic__head">
6
+ {% if section_data.title != blank %}
7
+ <h2 class="category-mosaic__title">{{ section_data.title }}</h2>
8
+ {% endif %}
9
+ {% if section_data.subtitle != blank %}
10
+ <p class="category-mosaic__subtitle">{{ section_data.subtitle }}</p>
11
+ {% endif %}
12
+ </div>
13
+
14
+ {% if section_data.tiles and section_data.tiles.size > 0 %}
15
+ <div class="category-mosaic__grid">
16
+ {% for tile in section_data.tiles %}
17
+ <a class="category-mosaic__tile" href="{{ tile.url | default: '#' }}">
18
+ <div class="category-mosaic__visual">
19
+ {% if tile.image != blank %}
20
+ <img
21
+ class="category-mosaic__img"
22
+ src="{{ tile.image }}"
23
+ alt="{{ tile.label }}"
24
+ loading="lazy"
25
+ width="640"
26
+ height="800"
27
+ />
28
+ {% else %}
29
+ <div class="category-mosaic__img-fallback" aria-hidden="true"></div>
30
+ {% endif %}
31
+ <div class="category-mosaic__shade" aria-hidden="true"></div>
32
+ </div>
33
+ <div class="category-mosaic__meta">
34
+ {% if tile.label != blank %}
35
+ <span class="category-mosaic__label">{{ tile.label }}</span>
36
+ {% endif %}
37
+ {% if tile.tagline != blank %}
38
+ <span class="category-mosaic__tagline">{{ tile.tagline }}</span>
39
+ {% endif %}
40
+ </div>
41
+ </a>
42
+ {% endfor %}
43
+ </div>
44
+ {% endif %}
45
+ </section>
46
+
47
+ <style>
48
+ .category-mosaic {
49
+ padding: clamp(2.5rem, 5vw, 4rem) 1.25rem 3rem;
50
+ background: #fff;
51
+ color: #141218;
52
+ }
53
+ .category-mosaic--tight .category-mosaic__grid {
54
+ gap: 0.75rem;
55
+ }
56
+ .category-mosaic__head {
57
+ max-width: 720px;
58
+ margin: 0 auto 2rem;
59
+ text-align: center;
60
+ }
61
+ .category-mosaic__title {
62
+ margin: 0 0 0.5rem;
63
+ font-family: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
64
+ font-weight: 500;
65
+ font-size: clamp(1.75rem, 4vw, 2.35rem);
66
+ }
67
+ .category-mosaic__subtitle {
68
+ margin: 0;
69
+ font-size: 1rem;
70
+ line-height: 1.6;
71
+ color: #5c5852;
72
+ }
73
+ .category-mosaic__grid {
74
+ max-width: 1200px;
75
+ margin: 0 auto;
76
+ display: grid;
77
+ grid-template-columns: repeat(var(--mosaic-cols, 3), minmax(0, 1fr));
78
+ gap: 1.25rem;
79
+ }
80
+ @media (max-width: 899px) {
81
+ .category-mosaic__grid {
82
+ grid-template-columns: repeat(2, minmax(0, 1fr));
83
+ }
84
+ }
85
+ @media (max-width: 520px) {
86
+ .category-mosaic__grid {
87
+ grid-template-columns: 1fr;
88
+ }
89
+ }
90
+ .category-mosaic__tile {
91
+ position: relative;
92
+ display: block;
93
+ text-decoration: none;
94
+ color: inherit;
95
+ border-radius: 6px;
96
+ overflow: hidden;
97
+ background: #ece8e1;
98
+ transition: transform 0.25s ease, box-shadow 0.25s ease;
99
+ }
100
+ .category-mosaic__tile:hover {
101
+ transform: translateY(-4px);
102
+ box-shadow: 0 22px 50px rgba(20, 18, 24, 0.18);
103
+ }
104
+ .category-mosaic__visual {
105
+ position: relative;
106
+ aspect-ratio: 3 / 4;
107
+ overflow: hidden;
108
+ }
109
+ .category-mosaic__img,
110
+ .category-mosaic__img-fallback {
111
+ width: 100%;
112
+ height: 100%;
113
+ object-fit: cover;
114
+ display: block;
115
+ transition: transform 0.45s ease;
116
+ }
117
+ .category-mosaic__img-fallback {
118
+ background: linear-gradient(160deg, #d4cec4, #a39a8f);
119
+ }
120
+ .category-mosaic__tile:hover .category-mosaic__img {
121
+ transform: scale(1.04);
122
+ }
123
+ .category-mosaic__shade {
124
+ position: absolute;
125
+ inset: 0;
126
+ background: linear-gradient(to top, rgba(12, 10, 14, 0.72), transparent 55%);
127
+ pointer-events: none;
128
+ }
129
+ .category-mosaic__meta {
130
+ position: absolute;
131
+ left: 0;
132
+ right: 0;
133
+ bottom: 0;
134
+ padding: 1.25rem 1.1rem 1.1rem;
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: 0.2rem;
138
+ color: #fff;
139
+ }
140
+ .category-mosaic__label {
141
+ font-size: 1.05rem;
142
+ font-weight: 600;
143
+ letter-spacing: 0.02em;
144
+ }
145
+ .category-mosaic__tagline {
146
+ font-size: 0.82rem;
147
+ opacity: 0.88;
148
+ letter-spacing: 0.04em;
149
+ text-transform: uppercase;
150
+ }
151
+ </style>
@@ -0,0 +1,68 @@
1
+ {
2
+ "icon": "https://api.iconify.design/lucide:layout-template.svg",
3
+ "label": "Editorial feature",
4
+ "section_schema": [
5
+ {
6
+ "name": "image",
7
+ "type": "string",
8
+ "default": "",
9
+ "description": "Feature image URL"
10
+ },
11
+ {
12
+ "name": "eyebrow",
13
+ "type": "string",
14
+ "default": "Inside the atelier",
15
+ "description": "Small label above the title"
16
+ },
17
+ {
18
+ "name": "title",
19
+ "type": "string",
20
+ "default": "Tailoring that moves with you",
21
+ "description": "Headline"
22
+ },
23
+ {
24
+ "name": "body",
25
+ "type": "string",
26
+ "default": "Precision cuts and fluid fabrics—designed for city days and slow evenings. Discover the pieces our stylists reach for first.",
27
+ "description": "Body copy"
28
+ },
29
+ {
30
+ "name": "cta_label",
31
+ "type": "string",
32
+ "default": "Explore the story",
33
+ "description": "Link label"
34
+ },
35
+ {
36
+ "name": "cta_url",
37
+ "type": "string",
38
+ "default": "/pages/about",
39
+ "description": "Link URL"
40
+ },
41
+ {
42
+ "name": "image_position",
43
+ "type": "select",
44
+ "default": "left",
45
+ "description": "Image side on desktop",
46
+ "options": [
47
+ { "label": "Image left", "value": "left" },
48
+ { "label": "Image right", "value": "right" }
49
+ ]
50
+ },
51
+ {
52
+ "name": "surface",
53
+ "type": "select",
54
+ "default": "light",
55
+ "description": "Color mood",
56
+ "options": [
57
+ { "label": "Light editorial", "value": "light" },
58
+ { "label": "Dark editorial", "value": "dark" }
59
+ ]
60
+ },
61
+ {
62
+ "name": "accent_color",
63
+ "type": "color",
64
+ "default": "#8B7355",
65
+ "description": "Eyebrow and link accent"
66
+ }
67
+ ]
68
+ }
@@ -0,0 +1,145 @@
1
+ <section
2
+ class="editorial-feature editorial-feature--{{ section_data.surface | default: 'light' }} editorial-feature--img-{{ section_data.image_position | default: 'left' }}"
3
+ style="--ed-accent: {{ section_data.accent_color | default: '#8B7355' }};"
4
+ >
5
+ <div class="editorial-feature__grid">
6
+ <div class="editorial-feature__media">
7
+ {% if section_data.image != blank %}
8
+ <img
9
+ class="editorial-feature__img"
10
+ src="{{ section_data.image }}"
11
+ alt="{{ section_data.title | default: 'Feature' }}"
12
+ loading="lazy"
13
+ width="900"
14
+ height="1100"
15
+ />
16
+ {% else %}
17
+ <div class="editorial-feature__placeholder" role="img" aria-label=""></div>
18
+ {% endif %}
19
+ </div>
20
+ <div class="editorial-feature__copy">
21
+ {% if section_data.eyebrow != blank %}
22
+ <p class="editorial-feature__eyebrow">{{ section_data.eyebrow }}</p>
23
+ {% endif %}
24
+ {% if section_data.title != blank %}
25
+ <h2 class="editorial-feature__title">{{ section_data.title }}</h2>
26
+ {% endif %}
27
+ {% if section_data.body != blank %}
28
+ <p class="editorial-feature__body">{{ section_data.body }}</p>
29
+ {% endif %}
30
+ {% if section_data.cta_label != blank %}
31
+ <a class="editorial-feature__link" href="{{ section_data.cta_url | default: '#' }}">
32
+ {{ section_data.cta_label }}
33
+ <span class="editorial-feature__link-arrow" aria-hidden="true">→</span>
34
+ </a>
35
+ {% endif %}
36
+ </div>
37
+ </div>
38
+ </section>
39
+
40
+ <style>
41
+ .editorial-feature {
42
+ padding: clamp(2.5rem, 5vw, 4.5rem) 1.25rem;
43
+ }
44
+ .editorial-feature--light {
45
+ background: #f6f3ee;
46
+ color: #1c1a17;
47
+ }
48
+ .editorial-feature--dark {
49
+ background: #121015;
50
+ color: #f4eee6;
51
+ }
52
+ .editorial-feature__grid {
53
+ max-width: 1120px;
54
+ margin: 0 auto;
55
+ display: grid;
56
+ gap: clamp(1.75rem, 4vw, 3rem);
57
+ align-items: center;
58
+ }
59
+ @media (min-width: 900px) {
60
+ .editorial-feature__grid {
61
+ grid-template-columns: 1.05fr 1fr;
62
+ }
63
+ .editorial-feature--img-right .editorial-feature__media {
64
+ order: 2;
65
+ }
66
+ .editorial-feature--img-right .editorial-feature__copy {
67
+ order: 1;
68
+ }
69
+ }
70
+ .editorial-feature__media {
71
+ position: relative;
72
+ }
73
+ .editorial-feature__img {
74
+ width: 100%;
75
+ display: block;
76
+ border-radius: 4px;
77
+ object-fit: cover;
78
+ aspect-ratio: 4 / 5;
79
+ box-shadow: 0 28px 80px rgba(0, 0, 0, 0.18);
80
+ }
81
+ .editorial-feature--dark .editorial-feature__img {
82
+ box-shadow: 0 28px 80px rgba(0, 0, 0, 0.55);
83
+ }
84
+ .editorial-feature__placeholder {
85
+ width: 100%;
86
+ aspect-ratio: 4 / 5;
87
+ border-radius: 4px;
88
+ background: linear-gradient(145deg, rgba(139, 115, 85, 0.35), rgba(28, 26, 23, 0.08));
89
+ }
90
+ .editorial-feature--dark .editorial-feature__placeholder {
91
+ background: linear-gradient(145deg, rgba(201, 169, 98, 0.25), rgba(0, 0, 0, 0.4));
92
+ }
93
+ .editorial-feature__copy {
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: 1rem;
97
+ padding: 0.25rem 0;
98
+ }
99
+ .editorial-feature__eyebrow {
100
+ margin: 0;
101
+ font-size: 0.72rem;
102
+ letter-spacing: 0.28em;
103
+ text-transform: uppercase;
104
+ font-weight: 600;
105
+ color: var(--ed-accent, #8b7355);
106
+ }
107
+ .editorial-feature__title {
108
+ margin: 0;
109
+ font-family: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
110
+ font-weight: 500;
111
+ font-size: clamp(1.85rem, 4vw, 2.65rem);
112
+ line-height: 1.15;
113
+ }
114
+ .editorial-feature__body {
115
+ margin: 0;
116
+ font-size: 1.02rem;
117
+ line-height: 1.75;
118
+ opacity: 0.88;
119
+ max-width: 34rem;
120
+ }
121
+ .editorial-feature__link {
122
+ margin-top: 0.25rem;
123
+ display: inline-flex;
124
+ align-items: center;
125
+ gap: 0.5rem;
126
+ font-size: 0.82rem;
127
+ font-weight: 600;
128
+ letter-spacing: 0.18em;
129
+ text-transform: uppercase;
130
+ text-decoration: none;
131
+ color: var(--ed-accent, #8b7355);
132
+ border-bottom: 1px solid currentColor;
133
+ padding-bottom: 2px;
134
+ width: fit-content;
135
+ transition: gap 0.2s ease, opacity 0.2s ease;
136
+ }
137
+ .editorial-feature__link:hover {
138
+ gap: 0.65rem;
139
+ opacity: 0.85;
140
+ }
141
+ .editorial-feature__link-arrow {
142
+ font-size: 1rem;
143
+ line-height: 1;
144
+ }
145
+ </style>
@@ -0,0 +1,66 @@
1
+ {
2
+ "icon": "https://api.iconify.design/lucide:mail.svg",
3
+ "label": "Newsletter — luxe",
4
+ "section_schema": [
5
+ {
6
+ "name": "headline",
7
+ "type": "string",
8
+ "default": "Notes from the studio",
9
+ "description": "Heading"
10
+ },
11
+ {
12
+ "name": "body",
13
+ "type": "string",
14
+ "default": "Private previews, styling tips, and early access to drops—sent sparingly, never noisy.",
15
+ "description": "Supporting text"
16
+ },
17
+ {
18
+ "name": "placeholder",
19
+ "type": "string",
20
+ "default": "Email address",
21
+ "description": "Input placeholder"
22
+ },
23
+ {
24
+ "name": "button_label",
25
+ "type": "string",
26
+ "default": "Join the list",
27
+ "description": "Submit button label"
28
+ },
29
+ {
30
+ "name": "form_action",
31
+ "type": "string",
32
+ "default": "/pages/contact",
33
+ "description": "Form action URL (your ESP or contact page)"
34
+ },
35
+ {
36
+ "name": "footnote",
37
+ "type": "string",
38
+ "default": "By subscribing you agree to receive marketing emails. Unsubscribe anytime.",
39
+ "description": "Fine print under the form"
40
+ },
41
+ {
42
+ "name": "show_footnote",
43
+ "type": "boolean",
44
+ "default": true,
45
+ "description": "Show the fine print"
46
+ },
47
+ {
48
+ "name": "bg_color",
49
+ "type": "color",
50
+ "default": "#1A1814",
51
+ "description": "Section background"
52
+ },
53
+ {
54
+ "name": "text_color",
55
+ "type": "color",
56
+ "default": "#F4EEE6",
57
+ "description": "Section text color"
58
+ },
59
+ {
60
+ "name": "button_color",
61
+ "type": "color",
62
+ "default": "#C9A962",
63
+ "description": "Button background"
64
+ }
65
+ ]
66
+ }