@orderlyshop/web-components 0.1.0-build.7045

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 (71) hide show
  1. package/AGENTS.md +110 -0
  2. package/README.md +685 -0
  3. package/bin/orderly-build-category-pages.mjs +160 -0
  4. package/bin/orderly-generate-category-pages.mjs +308 -0
  5. package/bin/orderly-hydrate-static-pages.mjs +595 -0
  6. package/bin/orderly-init-navigation.mjs +327 -0
  7. package/bin/orderly-init-shop.mjs +876 -0
  8. package/bin/orderly-init-taxonomy.mjs +2 -0
  9. package/bin/orderly-publish-site.mjs +342 -0
  10. package/custom-elements.json +505 -0
  11. package/dist/browser/orderly-web-components.define.global.js +3551 -0
  12. package/dist/browser/orderly-web-components.define.global.js.map +1 -0
  13. package/dist/browser/orderly-web-components.global.js +3551 -0
  14. package/dist/browser/orderly-web-components.global.js.map +1 -0
  15. package/dist/default-shop-DgX6uy10.d.ts +221 -0
  16. package/dist/default-shop.d.ts +6 -0
  17. package/dist/default-shop.js +762 -0
  18. package/dist/default-shop.js.map +1 -0
  19. package/dist/define-BNMhl19n.d.ts +9 -0
  20. package/dist/define.d.ts +2 -0
  21. package/dist/define.js +11094 -0
  22. package/dist/define.js.map +1 -0
  23. package/dist/index.d.ts +683 -0
  24. package/dist/index.js +11417 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/navigation.d.ts +61 -0
  27. package/dist/navigation.js +1125 -0
  28. package/dist/navigation.js.map +1 -0
  29. package/dist/query.d.ts +31 -0
  30. package/dist/query.js +115 -0
  31. package/dist/query.js.map +1 -0
  32. package/dist/registry-CPDecU3g.d.ts +6 -0
  33. package/dist/shop-BgQhGRzS.d.ts +173 -0
  34. package/dist/shop-query.d.ts +8 -0
  35. package/dist/shop-query.js +100 -0
  36. package/dist/shop-query.js.map +1 -0
  37. package/dist/shop.d.ts +8 -0
  38. package/dist/shop.js +11187 -0
  39. package/dist/shop.js.map +1 -0
  40. package/dist/stores.d.ts +46 -0
  41. package/dist/stores.js +145 -0
  42. package/dist/stores.js.map +1 -0
  43. package/dist/taxonomy.d.ts +35 -0
  44. package/dist/taxonomy.js +247 -0
  45. package/dist/taxonomy.js.map +1 -0
  46. package/dist/types-Bjez59Hr.d.ts +96 -0
  47. package/docs/components/README.md +708 -0
  48. package/docs/components/product-grid.md +182 -0
  49. package/docs/components/product-rail.md +174 -0
  50. package/examples/shop/README.md +72 -0
  51. package/examples/shop/package.json +28 -0
  52. package/examples/shop/src/category.html +20 -0
  53. package/examples/shop/src/checkout.html +21 -0
  54. package/examples/shop/src/forretningsbetingelser.html +81 -0
  55. package/examples/shop/src/includes/body-end.html +1 -0
  56. package/examples/shop/src/includes/body-start.html +3 -0
  57. package/examples/shop/src/includes/head.html +32 -0
  58. package/examples/shop/src/index.html +25 -0
  59. package/examples/shop/src/navigation.ts +154 -0
  60. package/examples/shop/src/product.html +24 -0
  61. package/examples/shop/src/templates/page-layouts.html +162 -0
  62. package/examples/shop/src/templates/shop-footer.html +76 -0
  63. package/examples/shop/tsconfig.json +32 -0
  64. package/examples/shop/vite.config.mjs +190 -0
  65. package/html-custom-data.json +279 -0
  66. package/package.json +118 -0
  67. package/server/README.md +111 -0
  68. package/server/apache/.htaccess +18 -0
  69. package/server/nginx/orderly-products.conf +24 -0
  70. package/server/node/product-snapshot-server.mjs +133 -0
  71. package/server/php/orderly-product.php +204 -0
@@ -0,0 +1,876 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, relative, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const cwd = process.cwd();
7
+ const args = parseArgs(process.argv.slice(2));
8
+ const packageRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
9
+ const ownPackage = readJson(resolve(packageRoot, "package.json")) ?? {};
10
+ const defaultBackendUrl = "https://service.orderly.shop";
11
+ const shopName = args.name ?? "Orderly Butik";
12
+ const targetRoot = resolve(cwd, args.path ?? ".");
13
+ const force = Boolean(args.force);
14
+ const dryRun = Boolean(args["dry-run"]);
15
+ const skipPackageJson = Boolean(args["skip-package-json"]);
16
+
17
+ if (args.help || args.h) {
18
+ printHelp();
19
+ process.exit(0);
20
+ }
21
+
22
+ const config = {
23
+ accountId: args["account-id"],
24
+ backendUrl: args["base-url"] ?? defaultBackendUrl,
25
+ packageName: args["package-name"] ?? slugPackageName(shopName),
26
+ shopName
27
+ };
28
+ const files = scaffoldFiles(config);
29
+ const packageJsonPath = resolve(targetRoot, "package.json");
30
+ const planned = [...files.map((file) => file.path)];
31
+ if (!skipPackageJson) {
32
+ planned.push("package.json");
33
+ }
34
+
35
+ const existing = files
36
+ .filter((file) => !file.skipIfExists)
37
+ .map((file) => file.path)
38
+ .filter((file) => existsSync(resolve(targetRoot, file)));
39
+ if (existing.length && !force) {
40
+ throw new Error(`Refusing to overwrite existing files: ${existing.join(", ")}. Re-run with --force to overwrite scaffold files.`);
41
+ }
42
+
43
+ if (dryRun) {
44
+ console.log(`Would initialize ${shopName} in ${relative(cwd, targetRoot) || "."}.`);
45
+ for (const file of planned) {
46
+ console.log(`- ${file}`);
47
+ }
48
+ process.exit(0);
49
+ }
50
+
51
+ mkdirSync(targetRoot, { recursive: true });
52
+ for (const file of files) {
53
+ const target = resolve(targetRoot, file.path);
54
+ if (file.skipIfExists && existsSync(target) && !force) {
55
+ console.log(`Skipped existing ${relative(cwd, target)}.`);
56
+ continue;
57
+ }
58
+ mkdirSync(dirname(target), { recursive: true });
59
+ writeFileSync(target, file.content, "utf8");
60
+ console.log(`Created ${relative(cwd, target)}.`);
61
+ }
62
+
63
+ if (!skipPackageJson) {
64
+ const packageJsonExists = existsSync(packageJsonPath);
65
+ const nextPackageJson = packageJsonSource(packageJsonPath, config);
66
+ writeFileSync(packageJsonPath, `${JSON.stringify(nextPackageJson, null, 2)}\n`, "utf8");
67
+ console.log(`${packageJsonExists ? "Updated" : "Created"} ${relative(cwd, packageJsonPath)}.`);
68
+ }
69
+
70
+ console.log(`\nNext steps:
71
+ npm install
72
+ npm run dev
73
+
74
+ Then open the local Vite URL and start customizing src/navigation.ts, src/style.css, and src/templates/*.html.`);
75
+
76
+ function scaffoldFiles(options) {
77
+ return [
78
+ { path: ".gitignore", content: gitignoreSource(), skipIfExists: true },
79
+ { path: "AGENTS.md", content: agentsSource(), skipIfExists: true },
80
+ { path: "README.md", content: readmeSource(options), skipIfExists: true },
81
+ { path: "tsconfig.json", content: tsconfigSource() },
82
+ { path: "vite.config.mjs", content: viteConfigSource() },
83
+ { path: "src/index.html", content: pageSource({ title: options.shopName, element: homeElementSource() }) },
84
+ { path: "src/category.html", content: pageSource({ title: `Kategori | ${options.shopName}`, element: "<orderly-category-page></orderly-category-page>" }) },
85
+ { path: "src/product.html", content: pageSource({ title: `Produkt | ${options.shopName}`, element: productDetailElementSource() }) },
86
+ { path: "src/checkout.html", content: pageSource({ title: `Betaling | ${options.shopName}`, element: checkoutElementSource() }) },
87
+ { path: "src/includes/head.html", content: headIncludeSource(options) },
88
+ { path: "src/includes/body-start.html", content: bodyStartSource() },
89
+ { path: "src/includes/body-end.html", content: "<!-- Add shared body-end snippets here. -->\n" },
90
+ { path: "src/navigation.ts", content: navigationSource() },
91
+ { path: "src/shop-query.ts", content: shopQuerySource(options) },
92
+ { path: "src/style.css", content: styleSource() },
93
+ { path: "src/templates/shop-footer.html", content: shopFooterTemplateSource(options) },
94
+ { path: "src/templates/product-tile.html", content: productTileTemplateSource() },
95
+ { path: "src/templates/product-page.html", content: productPageTemplateSource() },
96
+ { path: "src/templates/basket.html", content: basketTemplateSource() }
97
+ ];
98
+ }
99
+
100
+ function packageJsonSource(file, options) {
101
+ const existing = readJson(file);
102
+ const packageJson = existing ?? {
103
+ name: options.packageName,
104
+ version: "0.1.0",
105
+ private: true,
106
+ type: "module"
107
+ };
108
+ packageJson.type ??= "module";
109
+ packageJson.scripts = {
110
+ dev: "vite",
111
+ build: "orderly-build-category-pages --build-command \"tsc --noEmit && vite build\"",
112
+ preview: "vite preview",
113
+ "generate:categories": "orderly-generate-category-pages",
114
+ "clean:categories": "orderly-generate-category-pages --clean",
115
+ "build:category-pages": "orderly-build-category-pages --build-command \"tsc --noEmit && vite build\"",
116
+ ...(packageJson.scripts ?? {})
117
+ };
118
+ packageJson.dependencies = {
119
+ "@orderlyshop/core-client": ownPackage.dependencies?.["@orderlyshop/core-client"] ?? "^0.1.0",
120
+ "@orderlyshop/web-components": `^${ownPackage.version ?? "0.1.0"}`,
121
+ ...(packageJson.dependencies ?? {})
122
+ };
123
+ packageJson.devDependencies = {
124
+ typescript: ownPackage.devDependencies?.typescript ?? "^5.9.3",
125
+ vite: "^7.3.2",
126
+ ...(packageJson.devDependencies ?? {})
127
+ };
128
+ return packageJson;
129
+ }
130
+
131
+ function gitignoreSource() {
132
+ return `node_modules
133
+ dist
134
+ .env
135
+ .DS_Store
136
+ `;
137
+ }
138
+
139
+ function agentsSource() {
140
+ return `# Agent Guide
141
+
142
+ - This shop was scaffolded with \`npx orderly-init-shop\` from \`@orderlyshop/web-components\`.
143
+ - Keep storefront logic in Orderly web components. Customize navigation, content, templates, and CSS instead of replacing package components.
144
+ - Configure shop-wide backend, default query, navigation, page layout, and component registration in \`src/includes/head.html\` through \`configureShop(...)\`.
145
+ - Use \`src/navigation.ts\` for category/navigation structure. Every category needs a stable globally unique \`slug\`.
146
+ - Use \`src/shop-query.ts\` for the shop-wide Core \`SearchQuery\`, such as tenant/account scoping.
147
+ - Put shop-specific component templates in \`src/templates/*.html\` and include them from \`src/includes/body-start.html\`.
148
+ - \`npm run build\` generates real category URL files and hydrates built homepage/category pages with server-rendered fallback HTML.
149
+ - Use \`npm run generate:categories\` for local source inspection and \`npm run clean:categories\` to remove generated source pages.
150
+ `;
151
+ }
152
+
153
+ function readmeSource(options) {
154
+ return `# ${options.shopName}
155
+
156
+ Vanilla storefront scaffolded with \`@orderlyshop/web-components\`.
157
+
158
+ ## Commands
159
+
160
+ \`\`\`sh
161
+ npm install
162
+ npm run dev
163
+ npm run build
164
+ npm run preview
165
+ \`\`\`
166
+
167
+ Override the backend during local development:
168
+
169
+ \`\`\`sh
170
+ VITE_ORDERLY_BASE_URL=${options.backendUrl} npm run dev
171
+ \`\`\`
172
+
173
+ Production builds generate real category URL pages from \`src/navigation.ts\` and hydrate the built homepage/category pages with server-rendered fallback HTML:
174
+
175
+ \`\`\`sh
176
+ npm run build
177
+ \`\`\`
178
+
179
+ Use \`npm run generate:categories\` when you want to inspect generated source pages during development. Generated category source pages are removed automatically after \`npm run build\`.
180
+
181
+ ## Customize
182
+
183
+ - \`src/navigation.ts\` defines storefront navigation and category queries.
184
+ - \`src/shop-query.ts\` defines the shop-wide Core \`SearchQuery\`.
185
+ - \`src/includes/head.html\` calls \`configureShop(...)\`.
186
+ - \`src/templates/*.html\` contains global component templates.
187
+ - \`src/style.css\` owns the shop look and feel.
188
+ `;
189
+ }
190
+
191
+ function tsconfigSource() {
192
+ return `{
193
+ "compilerOptions": {
194
+ "target": "ES2022",
195
+ "useDefineForClassFields": true,
196
+ "module": "ES2022",
197
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
198
+ "types": ["vite/client"],
199
+ "skipLibCheck": true,
200
+ "moduleResolution": "bundler",
201
+ "allowImportingTsExtensions": true,
202
+ "isolatedModules": true,
203
+ "moduleDetection": "force",
204
+ "noEmit": true,
205
+ "strict": true
206
+ },
207
+ "include": ["src"]
208
+ }
209
+ `;
210
+ }
211
+
212
+ function viteConfigSource() {
213
+ return `import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
214
+ import { dirname, relative, resolve } from "node:path";
215
+ import { defineConfig } from "vite";
216
+
217
+ export default defineConfig({
218
+ server: {
219
+ host: "localhost",
220
+ port: 61677,
221
+ strictPort: true
222
+ },
223
+ preview: {
224
+ host: "localhost",
225
+ port: 61677,
226
+ strictPort: true
227
+ },
228
+ plugins: [htmlIncludes(), sourceHtmlDevServer(), rootHtmlOutput()],
229
+ build: {
230
+ rollupOptions: {
231
+ input: htmlInputs()
232
+ }
233
+ }
234
+ });
235
+
236
+ function htmlIncludes() {
237
+ return {
238
+ name: "orderly-html-includes",
239
+ transformIndexHtml: {
240
+ order: "pre",
241
+ handler(html) {
242
+ return html
243
+ .replace("<!-- orderly-head-includes -->", include("head.html"))
244
+ .replace("<!-- orderly-body-start-includes -->", include("body-start.html"))
245
+ .replace("<!-- orderly-body-end-includes -->", include("body-end.html"));
246
+ }
247
+ }
248
+ };
249
+ }
250
+
251
+ function sourceHtmlDevServer() {
252
+ return {
253
+ name: "orderly-source-html-dev-server",
254
+ configureServer(server) {
255
+ server.middlewares.use(async (request, response, next) => {
256
+ const file = sourceHtmlFileForUrl(request.url ?? "/");
257
+ if (!file) {
258
+ next();
259
+ return;
260
+ }
261
+
262
+ try {
263
+ const html = await server.transformIndexHtml(request.url ?? "/", readFileSync(file, "utf8"));
264
+ response.statusCode = 200;
265
+ response.setHeader("Content-Type", "text/html");
266
+ response.end(html);
267
+ } catch (error) {
268
+ next(error);
269
+ }
270
+ });
271
+ }
272
+ };
273
+ }
274
+
275
+ function rootHtmlOutput() {
276
+ return {
277
+ name: "orderly-root-html-output",
278
+ enforce: "post",
279
+ generateBundle(_, bundle) {
280
+ for (const [fileName, output] of Object.entries(bundle)) {
281
+ if (!fileName.startsWith("src/") || !fileName.endsWith(".html")) {
282
+ continue;
283
+ }
284
+ delete bundle[fileName];
285
+ output.fileName = fileName.slice("src/".length);
286
+ bundle[output.fileName] = output;
287
+ }
288
+ }
289
+ };
290
+ }
291
+
292
+ function include(fileName) {
293
+ return includeFile(resolve(process.cwd(), "src/includes", fileName));
294
+ }
295
+
296
+ function includeFile(filePath) {
297
+ return expandTemplateIncludes(readFileSync(filePath, "utf8").trim(), dirname(filePath));
298
+ }
299
+
300
+ function expandTemplateIncludes(html, baseDir) {
301
+ return html.replace(/<!--\\s*orderly-include-template:\\s*([^>]+?)\\s*-->/g, (_, templatePath) => {
302
+ return includeFile(resolve(baseDir, templatePath.trim()));
303
+ });
304
+ }
305
+
306
+ function htmlInputs(root = process.cwd()) {
307
+ const inputs = {};
308
+ for (const file of listHtmlFiles(root)) {
309
+ const name = htmlInputName(root, file);
310
+ inputs[name] = file;
311
+ }
312
+ return inputs;
313
+ }
314
+
315
+ function htmlInputName(root, file) {
316
+ const name = relative(root, file).replace(/\\\\/g, "/").replace(/\\.html$/, "");
317
+ return name.startsWith("src/") ? name.slice("src/".length) : name;
318
+ }
319
+
320
+ function listHtmlFiles(dir) {
321
+ const files = [];
322
+ for (const entry of readdirSync(dir)) {
323
+ if (entry === "dist" || entry === "node_modules") {
324
+ continue;
325
+ }
326
+ const fullPath = resolve(dir, entry);
327
+ if (isHtmlPartial(fullPath)) {
328
+ continue;
329
+ }
330
+ const stat = statSync(fullPath);
331
+ if (stat.isDirectory()) {
332
+ files.push(...listHtmlFiles(fullPath));
333
+ } else if (entry.endsWith(".html")) {
334
+ files.push(fullPath);
335
+ }
336
+ }
337
+ return files;
338
+ }
339
+
340
+ function isHtmlPartial(file) {
341
+ const path = relative(process.cwd(), file).replace(/\\\\/g, "/");
342
+ return path.startsWith("src/includes") || path.startsWith("src/templates");
343
+ }
344
+
345
+ function sourceHtmlFileForUrl(url) {
346
+ const requestPath = safeHtmlRequestPath(url);
347
+ if (!requestPath) {
348
+ return undefined;
349
+ }
350
+ const srcRoot = resolve(process.cwd(), "src");
351
+ const categoryTemplate = resolve(srcRoot, "category.html");
352
+ if (requestPath.startsWith("categories/") && requestPath.endsWith("index.html") && existsSync(categoryTemplate)) {
353
+ return categoryTemplate;
354
+ }
355
+ const file = resolve(srcRoot, requestPath);
356
+ if (!file.startsWith(srcRoot) || isHtmlPartial(file) || !existsSync(file)) {
357
+ return undefined;
358
+ }
359
+ return statSync(file).isFile() ? file : undefined;
360
+ }
361
+
362
+ function safeHtmlRequestPath(url) {
363
+ const [rawPath] = url.split(/[?#]/, 1);
364
+ let path;
365
+ try {
366
+ path = decodeURIComponent(rawPath || "/");
367
+ } catch {
368
+ return undefined;
369
+ }
370
+ if (path.includes("..")) {
371
+ return undefined;
372
+ }
373
+ if (path === "/") {
374
+ return "index.html";
375
+ }
376
+ if (path.endsWith("/")) {
377
+ return path.slice(1) + "index.html";
378
+ }
379
+ if (path.startsWith("/categories/") && !path.includes(".")) {
380
+ return path.slice(1) + "/index.html";
381
+ }
382
+ if (path.endsWith(".html")) {
383
+ return path.slice(1);
384
+ }
385
+ return undefined;
386
+ }
387
+ `;
388
+ }
389
+
390
+ function pageSource({ title, element }) {
391
+ return `<!doctype html>
392
+ <html lang="da">
393
+ <head>
394
+ <meta charset="UTF-8">
395
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
396
+ <title>${escapeHtml(title)}</title>
397
+ <!-- orderly-head-includes -->
398
+ </head>
399
+ <body>
400
+ <!-- orderly-body-start-includes -->
401
+ ${element}
402
+ <!-- orderly-body-end-includes -->
403
+ </body>
404
+ </html>
405
+ `;
406
+ }
407
+
408
+ function homeElementSource() {
409
+ return `<orderly-home-page
410
+ eyebrow="Nye varer"
411
+ title="Find næste favorit"
412
+ rail-cta-label="Se alle"
413
+ empty-label="Der blev ikke fundet varer."
414
+ checkout-label="Gå til betaling">
415
+ <div slot="utility" class="shop-utility-strip" aria-label="Kundefordele">
416
+ <span>Hurtig levering</span>
417
+ <span>Nem retur</span>
418
+ <span>Sikker betaling</span>
419
+ </div>
420
+ </orderly-home-page>`;
421
+ }
422
+
423
+ function productDetailElementSource() {
424
+ return `<orderly-product-detail-page
425
+ add-label="Læg i kurven"
426
+ loading-label="Indlæser produkt..."
427
+ not-found-label="Produktet blev ikke fundet."
428
+ error-label="Produktet kunne ikke indlæses.">
429
+ <div slot="utility" class="shop-utility-strip" aria-label="Kundefordele">
430
+ <span>Hurtig levering</span>
431
+ <span>Nem retur</span>
432
+ <span>Sikker betaling</span>
433
+ </div>
434
+ </orderly-product-detail-page>`;
435
+ }
436
+
437
+ function checkoutElementSource() {
438
+ return `<orderly-checkout-page>
439
+ <div slot="utility" class="shop-utility-strip" aria-label="Kundefordele">
440
+ <span>Hurtig levering</span>
441
+ <span>Nem retur</span>
442
+ <span>Sikker betaling</span>
443
+ </div>
444
+ </orderly-checkout-page>`;
445
+ }
446
+
447
+ function headIncludeSource(options) {
448
+ return `<!-- Add shared head snippets here, for example analytics, consent, or preconnect tags. -->
449
+ <link rel="icon" href="https://orderly.shop/home/App_Icon.svg" type="image/svg+xml">
450
+ <link rel="stylesheet" href="/src/style.css">
451
+ <script type="module">
452
+ import { configureShop } from "@orderlyshop/web-components";
453
+ import { navigationDefinitions } from "/src/navigation.ts";
454
+ import { defaultQuery } from "/src/shop-query.ts";
455
+
456
+ const categoryUrlMode = import.meta.env.VITE_ORDERLY_CATEGORY_URL_MODE === "hash" ? "hash" : "path";
457
+
458
+ configureShop({
459
+ uiLanguage: "DA",
460
+ defaultShop: {
461
+ baseUrl: import.meta.env.VITE_ORDERLY_BASE_URL ?? ${JSON.stringify(options.backendUrl)},
462
+ defaultQuery,
463
+ brandLabel: ${JSON.stringify(options.shopName)},
464
+ homeHref: "/",
465
+ checkoutHref: "/checkout.html",
466
+ navigationDefinitions,
467
+ categoryPageRoot: categoryUrlMode === "hash" ? "/category.html" : "/categories/",
468
+ categoryUrlMode
469
+ },
470
+ pageLayout: {
471
+ logoSrc: "https://orderly.shop/home/App_Icon.svg",
472
+ logoAlt: ${JSON.stringify(options.shopName)},
473
+ logoHref: "/"
474
+ }
475
+ });
476
+ </script>
477
+ `;
478
+ }
479
+
480
+ function bodyStartSource() {
481
+ return `<!-- Add shared body-start snippets here, for example Google Tag Manager noscript fallback. -->
482
+ <!-- orderly-include-template: ../templates/shop-footer.html -->
483
+ <!-- orderly-include-template: ../templates/product-tile.html -->
484
+ <!-- orderly-include-template: ../templates/product-page.html -->
485
+ <!-- orderly-include-template: ../templates/basket.html -->
486
+ `;
487
+ }
488
+
489
+ function navigationSource() {
490
+ return `import type { NavigationDefinition } from "@orderlyshop/web-components";
491
+
492
+ export const navigationDefinitions: NavigationDefinition[] = [
493
+ {
494
+ label: "Nyheder",
495
+ slug: "nyheder",
496
+ heroImage: "https://images.unsplash.com/photo-1496747611176-843222e1e57c?auto=format&fit=crop&w=1800&q=80",
497
+ query: { query: "nyheder" },
498
+ children: [
499
+ { label: "Dame", slug: "dame", query: { query: "dame mode" } },
500
+ { label: "Herre", slug: "herre", query: { query: "herre mode" } },
501
+ { label: "Børn", slug: "boern", query: { query: "børn baby" } }
502
+ ]
503
+ },
504
+ {
505
+ label: "Sko",
506
+ slug: "sko",
507
+ heroImage: "https://images.unsplash.com/photo-1542291026-7eec264c27ff?auto=format&fit=crop&w=1800&q=80",
508
+ query: { query: "sko sneakers" },
509
+ children: [
510
+ { label: "Damesko", slug: "damesko", query: { query: "damesko sneakers" } },
511
+ { label: "Herresko", slug: "herresko", query: { query: "herresko sneakers" } },
512
+ { label: "Sportssko", slug: "sportssko", query: { query: "sportssko løb" } }
513
+ ]
514
+ },
515
+ {
516
+ label: "Bolig",
517
+ slug: "bolig",
518
+ heroImage: "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?auto=format&fit=crop&w=1800&q=80",
519
+ query: { query: "bolig indretning" },
520
+ children: [
521
+ { label: "Møbler", slug: "moebler", query: { query: "møbler sofa stol bord" } },
522
+ { label: "Belysning", slug: "belysning", query: { query: "belysning lampe" } },
523
+ { label: "Dekoration", slug: "dekoration", query: { query: "dekoration kunst bolig" } }
524
+ ]
525
+ }
526
+ ];
527
+ `;
528
+ }
529
+
530
+ function shopQuerySource(options) {
531
+ if (!options.accountId) {
532
+ return `import type { SearchQuery } from "@orderlyshop/core-client";
533
+
534
+ // Add shop-wide SearchQuery constraints here, for example SourceAccountId.
535
+ // This query is merged into every category, rail, and product grid search.
536
+ export const defaultQuery: SearchQuery | undefined = undefined;
537
+ `;
538
+ }
539
+ return `import { create } from "@bufbuild/protobuf";
540
+ import { AccountIdSchema, SearchQuerySchema, UUIDSchema } from "@orderlyshop/core-client";
541
+
542
+ export const defaultQuery = create(SearchQuerySchema, {
543
+ SourceAccountId: create(AccountIdSchema, {
544
+ Id: create(UUIDSchema, { Value: ${JSON.stringify(options.accountId)} })
545
+ })
546
+ });
547
+ `;
548
+ }
549
+
550
+ function styleSource() {
551
+ return `* {
552
+ box-sizing: border-box;
553
+ }
554
+
555
+ :root {
556
+ --shop-accent: #ef5a24;
557
+ --shop-border: #e2e2e2;
558
+ --shop-muted: #5f5f5f;
559
+ color: #141414;
560
+ background: #eeeeee;
561
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
562
+ }
563
+
564
+ body {
565
+ min-width: 320px;
566
+ margin: 0;
567
+ overflow-x: hidden;
568
+ background: #eeeeee;
569
+ }
570
+
571
+ body.orderly-navigation-menu-open {
572
+ overflow: hidden;
573
+ }
574
+
575
+ orderly-home-page:not(:defined),
576
+ orderly-category-page:not(:defined),
577
+ orderly-product-detail-page:not(:defined),
578
+ orderly-checkout-page:not(:defined) {
579
+ display: block;
580
+ min-height: 100vh;
581
+ background: #f6f8f9;
582
+ }
583
+
584
+ orderly-home-page:not(:defined)::before,
585
+ orderly-category-page:not(:defined)::before,
586
+ orderly-product-detail-page:not(:defined)::before,
587
+ orderly-checkout-page:not(:defined)::before {
588
+ content: "";
589
+ display: block;
590
+ height: 62px;
591
+ border-bottom: 1px solid #d8dee3;
592
+ background: #ffffff;
593
+ }
594
+
595
+ button,
596
+ input,
597
+ select {
598
+ font: inherit;
599
+ }
600
+
601
+ .shop-utility-strip {
602
+ display: flex;
603
+ justify-content: center;
604
+ gap: clamp(14px, 4vw, 64px);
605
+ padding: 8px 16px;
606
+ background: #141414;
607
+ color: #ffffff;
608
+ font-size: 0.8rem;
609
+ font-weight: 700;
610
+ text-transform: uppercase;
611
+ }
612
+
613
+ .orderly-page-layout__header {
614
+ position: sticky;
615
+ top: 0;
616
+ z-index: 20;
617
+ display: flex;
618
+ align-items: center;
619
+ justify-content: space-between;
620
+ gap: 16px;
621
+ padding: 14px 24px;
622
+ border-bottom: 1px solid var(--shop-border);
623
+ background: #ffffff;
624
+ }
625
+
626
+ .orderly-page-layout__primary-nav,
627
+ .orderly-page-layout__footer {
628
+ background: #ffffff;
629
+ }
630
+
631
+ .orderly-product-grid__items,
632
+ .orderly-product-rail__items {
633
+ display: grid;
634
+ grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
635
+ gap: 18px;
636
+ }
637
+
638
+ .orderly-product-rail__items {
639
+ display: flex;
640
+ overflow-x: auto;
641
+ padding-bottom: 12px;
642
+ }
643
+
644
+ .orderly-product-rail__item {
645
+ flex: 0 0 min(240px, 70vw);
646
+ }
647
+
648
+ .orderly-product-tile {
649
+ display: grid;
650
+ grid-template-rows: auto minmax(0, 1fr);
651
+ height: 100%;
652
+ border: 1px solid var(--shop-border);
653
+ border-radius: 8px;
654
+ background: #ffffff;
655
+ overflow: hidden;
656
+ }
657
+
658
+ .orderly-product-tile__image {
659
+ width: 100%;
660
+ aspect-ratio: 1;
661
+ background: #ffffff;
662
+ }
663
+
664
+ .orderly-product-tile__body {
665
+ display: flex;
666
+ flex-direction: column;
667
+ gap: 8px;
668
+ min-width: 0;
669
+ padding: 12px;
670
+ }
671
+
672
+ .orderly-product-tile__footer {
673
+ display: flex;
674
+ align-items: center;
675
+ justify-content: space-between;
676
+ gap: 12px;
677
+ margin-top: auto;
678
+ }
679
+
680
+ .orderly-product-tile__add,
681
+ .orderly-basket-icon__button {
682
+ min-width: 44px;
683
+ min-height: 44px;
684
+ border: 1px solid #141414;
685
+ border-radius: 999px;
686
+ background: #141414;
687
+ color: #ffffff;
688
+ }
689
+
690
+ .orderly-product-tile__add-icon,
691
+ .orderly-product-tile__add-svg,
692
+ .orderly-product-tile__remove-svg {
693
+ display: block;
694
+ }
695
+
696
+ .orderly-shop-footer {
697
+ display: grid;
698
+ gap: 24px;
699
+ padding: 40px 24px;
700
+ border-top: 1px solid var(--shop-border);
701
+ }
702
+
703
+ @media (min-width: 760px) {
704
+ .orderly-shop-footer {
705
+ grid-template-columns: 2fr repeat(3, 1fr);
706
+ }
707
+ }
708
+ `;
709
+ }
710
+
711
+ function shopFooterTemplateSource(options) {
712
+ return `<template data-orderly-template="footer" data-orderly-for="shop-footer">
713
+ <div class="orderly-shop-footer">
714
+ <div class="orderly-shop-footer__brand">
715
+ <a class="orderly-shop-footer__logo" href="/" aria-label="${escapeAttribute(options.shopName)}">
716
+ <img class="orderly-shop-footer__logo-image" src="https://orderly.shop/home/App_Icon.svg" alt="${escapeAttribute(options.shopName)}">
717
+ </a>
718
+ <p class="orderly-shop-footer__about">A storefront built with Orderly web components.</p>
719
+ </div>
720
+ <section class="orderly-shop-footer__section">
721
+ <h2 class="orderly-shop-footer__heading">Contact</h2>
722
+ <p>hello@example.com</p>
723
+ <p>+45 12 34 56 78</p>
724
+ </section>
725
+ <section class="orderly-shop-footer__section">
726
+ <h2 class="orderly-shop-footer__heading">Opening hours</h2>
727
+ <p>Monday - Friday: 09.00 - 17.00</p>
728
+ <p>Saturday: 10.00 - 14.00</p>
729
+ </section>
730
+ <section class="orderly-shop-footer__section">
731
+ <h2 class="orderly-shop-footer__heading">Information</h2>
732
+ <ul class="orderly-shop-footer__list">
733
+ <li><a href="/information/terms/">Terms</a></li>
734
+ <li><a href="/information/delivery/">Delivery and returns</a></li>
735
+ <li><a href="/information/privacy/">Privacy policy</a></li>
736
+ </ul>
737
+ </section>
738
+ </div>
739
+ </template>
740
+ `;
741
+ }
742
+
743
+ function productTileTemplateSource() {
744
+ return `<template data-orderly-template="product" data-orderly-for="product-tile">
745
+ <article class="orderly-product-tile">
746
+ <orderly-stored-image class="orderly-product-tile__image" data-orderly-bind="image" fit="contain"></orderly-stored-image>
747
+ <div class="orderly-product-tile__body">
748
+ <h3 class="orderly-product-tile__title">
749
+ <a class="orderly-product-tile__link" href="/product.html" data-orderly-bind="share-url">
750
+ <span data-orderly-bind="title"></span>
751
+ </a>
752
+ </h3>
753
+ <p class="orderly-product-tile__brand" data-orderly-bind="brand"></p>
754
+ <div class="orderly-product-tile__footer">
755
+ <p class="orderly-product-tile__price" data-orderly-bind="price"></p>
756
+ <button class="orderly-product-tile__add" type="button" data-orderly-action="add-to-basket">
757
+ <span class="orderly-product-tile__add-icon" data-orderly-bind="basket-action-icon" aria-hidden="true"></span>
758
+ </button>
759
+ </div>
760
+ </div>
761
+ </article>
762
+ </template>
763
+ `;
764
+ }
765
+
766
+ function productPageTemplateSource() {
767
+ return `<template data-orderly-template="product" data-orderly-for="product-page">
768
+ <section class="orderly-product-page">
769
+ <div class="orderly-product-page__media" style="display: grid; gap: 12px; align-content: start; justify-self: center; width: 100%; max-width: 520px; min-width: 0;">
770
+ <orderly-stored-image class="orderly-product-page__image" data-orderly-bind="image" fit="contain" variant="object" style="display: block; width: 100%; max-width: 520px; min-width: 0; aspect-ratio: 1 / 1; overflow: hidden; background: #ffffff;"></orderly-stored-image>
771
+ <div class="orderly-product-page__thumbnails" data-orderly-slot="thumbnails"></div>
772
+ </div>
773
+ <div class="orderly-product-page__details" style="min-width: 0;">
774
+ <p data-orderly-bind="brand"></p>
775
+ <h1 data-orderly-bind="title"></h1>
776
+ <p data-orderly-bind="price"></p>
777
+ <p data-orderly-bind="description"></p>
778
+ <button type="button" data-orderly-action="add-to-basket">
779
+ <span data-orderly-bind="basket-action-label"></span>
780
+ </button>
781
+ </div>
782
+ </section>
783
+ </template>
784
+ `;
785
+ }
786
+
787
+ function basketTemplateSource() {
788
+ return `<template data-orderly-template="basket" data-orderly-for="basket">
789
+ <section class="orderly-basket">
790
+ <header class="orderly-basket__header">
791
+ <h2 data-orderly-bind="title"></h2>
792
+ <button type="button" data-orderly-action="clear"><span data-orderly-bind="clearLabel"></span></button>
793
+ </header>
794
+ <p class="orderly-basket__empty" data-orderly-bind="emptyLabel"></p>
795
+ <ul class="orderly-basket__lines" data-orderly-slot="lines"></ul>
796
+ <footer class="orderly-basket__summary">
797
+ <p><span data-orderly-bind="subtotalLabel"></span> <strong data-orderly-bind="subtotal"></strong></p>
798
+ <p><span data-orderly-bind="totalLabel"></span> <strong data-orderly-bind="total"></strong></p>
799
+ <a class="orderly-basket__checkout" data-orderly-action="checkout" data-orderly-bind="checkoutHref">
800
+ <span data-orderly-bind="checkoutLabel"></span>
801
+ </a>
802
+ </footer>
803
+ </section>
804
+ </template>
805
+ `;
806
+ }
807
+
808
+ function parseArgs(values) {
809
+ const parsed = {};
810
+ for (let index = 0; index < values.length; index += 1) {
811
+ const value = values[index];
812
+ if (!value.startsWith("--")) {
813
+ continue;
814
+ }
815
+ const [name, inlineValue] = value.slice(2).split("=", 2);
816
+ if (inlineValue !== undefined) {
817
+ parsed[name] = inlineValue;
818
+ } else if (values[index + 1] && !values[index + 1].startsWith("--")) {
819
+ parsed[name] = values[index + 1];
820
+ index += 1;
821
+ } else {
822
+ parsed[name] = true;
823
+ }
824
+ }
825
+ return parsed;
826
+ }
827
+
828
+ function printHelp() {
829
+ console.log(`Usage: orderly-init-shop [options]
830
+
831
+ Create a vanilla TypeScript storefront using @orderlyshop/web-components.
832
+
833
+ Options:
834
+ --path <dir> Target directory. Defaults to the current directory.
835
+ --name <name> Shop display name. Defaults to "Orderly Butik".
836
+ --package-name <name> package.json name when package.json does not exist.
837
+ --base-url <url> Orderly backend URL. Defaults to ${defaultBackendUrl}.
838
+ --account-id <id> Optional tenant/account id written into src/shop-query.ts.
839
+ --skip-package-json Do not create or update package.json.
840
+ --force Overwrite scaffold files that already exist.
841
+ --dry-run Print files that would be written without mutating the project.
842
+ --help, -h Show this help.
843
+
844
+ Agent workflow:
845
+ 1. If no shop scaffold exists, run: npx orderly-init-shop
846
+ 2. Ask the user for the shop account id. Re-run with --account-id <id> or edit src/shop-query.ts.
847
+ 3. Customize src/navigation.ts with categories and SearchQuery values.
848
+ 4. Customize src/templates/*.html and src/style.css for the shop look and feel.
849
+ `);
850
+ }
851
+
852
+ function readJson(file) {
853
+ if (!existsSync(file)) {
854
+ return undefined;
855
+ }
856
+ return JSON.parse(readFileSync(file, "utf8"));
857
+ }
858
+
859
+ function slugPackageName(value) {
860
+ const slug = value
861
+ .toLowerCase()
862
+ .replace(/[^a-z0-9]+/g, "-")
863
+ .replace(/^-+|-+$/g, "");
864
+ return slug || "orderly-shop";
865
+ }
866
+
867
+ function escapeHtml(value) {
868
+ return String(value)
869
+ .replaceAll("&", "&amp;")
870
+ .replaceAll("<", "&lt;")
871
+ .replaceAll(">", "&gt;");
872
+ }
873
+
874
+ function escapeAttribute(value) {
875
+ return escapeHtml(value).replaceAll("\"", "&quot;");
876
+ }