@orderlyshop/web-components 0.1.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 (70) 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 +495 -0
  11. package/dist/browser/orderly-web-components.define.global.js +3085 -0
  12. package/dist/browser/orderly-web-components.define.global.js.map +1 -0
  13. package/dist/browser/orderly-web-components.global.js +3085 -0
  14. package/dist/browser/orderly-web-components.global.js.map +1 -0
  15. package/dist/default-shop-DWdB_MRd.d.ts +220 -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-IAQk8OmQ.d.ts +9 -0
  20. package/dist/define.d.ts +2 -0
  21. package/dist/define.js +10266 -0
  22. package/dist/define.js.map +1 -0
  23. package/dist/index.d.ts +683 -0
  24. package/dist/index.js +10589 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/navigation.d.ts +51 -0
  27. package/dist/navigation.js +818 -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-BnT1C6kG.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 +10359 -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-CCQDd6Nd.d.ts +95 -0
  47. package/docs/components/README.md +610 -0
  48. package/docs/components/product-grid.md +176 -0
  49. package/docs/components/product-rail.md +174 -0
  50. package/examples/shop/README.md +71 -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 +80 -0
  55. package/examples/shop/src/includes/body-end.html +1 -0
  56. package/examples/shop/src/includes/body-start.html +2 -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/shop-footer.html +76 -0
  62. package/examples/shop/tsconfig.json +32 -0
  63. package/examples/shop/vite.config.mjs +184 -0
  64. package/html-custom-data.json +262 -0
  65. package/package.json +118 -0
  66. package/server/README.md +111 -0
  67. package/server/apache/.htaccess +18 -0
  68. package/server/nginx/orderly-products.conf +24 -0
  69. package/server/node/product-snapshot-server.mjs +133 -0
  70. package/server/php/orderly-product.php +204 -0
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { dirname, relative, resolve } from "node:path";
4
+ import { stdin as input, stdout as output } from "node:process";
5
+ import { createInterface } from "node:readline/promises";
6
+
7
+ const cwd = process.cwd();
8
+ const args = parseArgs(process.argv.slice(2));
9
+ const defaultBackendUrl = "https://service.orderly.shop";
10
+
11
+ if (args.help || args.h) {
12
+ printHelp();
13
+ process.exit(0);
14
+ }
15
+
16
+ const target = resolve(cwd, args.path ?? "navigation.ts");
17
+ const productDumpTarget = resolve(cwd, args["product-dump-path"] ?? defaultProductDumpPath(target));
18
+
19
+ if (!args.suggest && existsSync(target) && !args.force) {
20
+ throw new Error(`${relative(cwd, target)} already exists. Re-run with --force to overwrite it.`);
21
+ }
22
+ if (args.suggest && existsSync(productDumpTarget) && !args.force) {
23
+ throw new Error(`${relative(cwd, productDumpTarget)} already exists. Re-run with --force to overwrite it.`);
24
+ }
25
+
26
+ const generated = args.suggest
27
+ ? await suggestedProductDump()
28
+ : { navigation: defaultNavigationSource() };
29
+
30
+ if (generated.navigation) {
31
+ mkdirSync(dirname(target), { recursive: true });
32
+ writeFileSync(target, generated.navigation, "utf8");
33
+ console.log(`Created ${relative(cwd, target)}.`);
34
+ }
35
+ if (generated.productDump) {
36
+ mkdirSync(dirname(productDumpTarget), { recursive: true });
37
+ writeFileSync(productDumpTarget, generated.productDump, "utf8");
38
+ console.log(`Created ${relative(cwd, productDumpTarget)}.`);
39
+ }
40
+ if (generated.nextStep) {
41
+ console.log(generated.nextStep);
42
+ }
43
+
44
+ async function suggestedProductDump() {
45
+ const accountId = await promptText({
46
+ label: "Account ID for the new shop",
47
+ value: args["account-id"],
48
+ required: true
49
+ });
50
+ const baseUrl = await promptText({
51
+ label: "Orderly backend URL",
52
+ value: args["base-url"] ?? defaultBackendUrl,
53
+ required: true
54
+ });
55
+ const maxResults = positiveInteger(args["max-results"] ?? args.limit ?? 1000, "max results");
56
+ const result = await fetchProductSample({ accountId, baseUrl, maxResults });
57
+ return {
58
+ productDump: productDumpSource({ accountId, baseUrl, maxResults, result }),
59
+ nextStep: `Use ${relative(cwd, productDumpTarget)} to create ${relative(cwd, target)} manually. Define an explicit unique slug for every category. Configure the shop scope with a SearchQuery and pass it to configureShop({ defaultShop: { defaultQuery } }). Keep navigation to at most two category levels.`
60
+ };
61
+ }
62
+
63
+ async function fetchProductSample({ accountId, baseUrl, maxResults }) {
64
+ const [
65
+ { create, clone },
66
+ { anyUnpack },
67
+ {
68
+ AccountIdSchema,
69
+ SearchObjectSchema,
70
+ SearchQuerySchema,
71
+ UUIDSchema
72
+ },
73
+ { createOrderlyNodeClient }
74
+ ] = await Promise.all([
75
+ import("@bufbuild/protobuf"),
76
+ import("@bufbuild/protobuf/wkt"),
77
+ import("@orderlyshop/core-client"),
78
+ import("@orderlyshop/core-client/node")
79
+ ]);
80
+ const client = createOrderlyNodeClient({ baseUrl, protocol: "grpc" });
81
+ const sourceAccountId = create(AccountIdSchema, {
82
+ Id: create(UUIDSchema, { Value: accountId })
83
+ });
84
+ const products = [];
85
+ let scanned = 0;
86
+ let query = create(SearchQuerySchema, {
87
+ Query: "",
88
+ SourceAccountId: sourceAccountId,
89
+ OrderBy: ["CreatedTime desc"]
90
+ });
91
+ while (products.length < maxResults) {
92
+ const result = await client.services.searchService.search(query);
93
+ for (const product of unpackProducts(result, { anyUnpack, SearchObjectSchema })) {
94
+ scanned += 1;
95
+ if (shouldSampleProduct(scanned)) {
96
+ products.push(productSummary(product));
97
+ }
98
+ if (products.length >= maxResults) {
99
+ break;
100
+ }
101
+ }
102
+ if (!result.Continuation?.Token) {
103
+ break;
104
+ }
105
+ query = clone(SearchQuerySchema, query);
106
+ query.Continuation = result.Continuation;
107
+ }
108
+ return { products, scanned };
109
+ }
110
+
111
+ function unpackProducts(result, { anyUnpack, SearchObjectSchema }) {
112
+ return result.data
113
+ .map((item) => anyUnpack(item, SearchObjectSchema))
114
+ .filter(Boolean);
115
+ }
116
+
117
+ async function promptText({ label, value, required }) {
118
+ if (value) {
119
+ return value;
120
+ }
121
+ if (!input.isTTY) {
122
+ throw new Error(`${label} is required. Pass it as an option or run in an interactive terminal.`);
123
+ }
124
+ const rl = createInterface({ input, output });
125
+ try {
126
+ const answer = (await rl.question(`${label}: `)).trim();
127
+ if (required && !answer) {
128
+ throw new Error(`${label} is required.`);
129
+ }
130
+ return answer;
131
+ } finally {
132
+ rl.close();
133
+ }
134
+ }
135
+
136
+ function parseArgs(values) {
137
+ const parsed = {};
138
+ for (let index = 0; index < values.length; index += 1) {
139
+ const value = values[index];
140
+ if (!value.startsWith("--")) {
141
+ continue;
142
+ }
143
+ const [name, inlineValue] = value.slice(2).split("=", 2);
144
+ if (inlineValue !== undefined) {
145
+ parsed[name] = inlineValue;
146
+ } else if (values[index + 1] && !values[index + 1].startsWith("--")) {
147
+ parsed[name] = values[index + 1];
148
+ index += 1;
149
+ } else {
150
+ parsed[name] = true;
151
+ }
152
+ }
153
+ return parsed;
154
+ }
155
+
156
+ function printHelp() {
157
+ console.log(`Usage: orderly-init-navigation [options]
158
+
159
+ Create a default strongly typed navigation.ts file, or use --suggest to dump product data for agent-assisted navigation design.
160
+
161
+ Options:
162
+ --path <file> Output path. Defaults to navigation.ts.
163
+ --product-dump-path <file>
164
+ Output path for sampled product information with --suggest.
165
+ Defaults to navigation-products.json next to navigation.ts.
166
+ --force Overwrite an existing output file.
167
+ --suggest Dump sampled product information through SearchService.
168
+ --account-id <id> Orderly account id to sample with --suggest.
169
+ --base-url <url> Orderly backend URL. Defaults to ${defaultBackendUrl}.
170
+ --max-results <number> Max products to write to the dump. Defaults to 1000.
171
+ --limit <number> Alias for --max-results.
172
+ --help, -h Show this help.
173
+
174
+ Agent workflow:
175
+ 1. Ask the user for the shop account id.
176
+ 2. Run: npx orderly-init-navigation --suggest --account-id <account-id>
177
+ 3. Read the generated product dump and create navigation.ts manually.
178
+ 4. Export navigationDefinitions: NavigationDefinition[] with explicit unique slug values on every category.
179
+ 5. Configure tenant scoping in the shop setup with a SearchQuery passed to configureShop({ defaultShop: { defaultQuery } }).
180
+ 6. Keep navigation to at most two category levels and avoid categories with only a few matching products.
181
+ `);
182
+ }
183
+
184
+ function defaultNavigationSource() {
185
+ return navigationSource(defaultNavigationDefinitions(), {}, target.endsWith(".ts") || target.endsWith(".tsx"));
186
+ }
187
+
188
+ function navigationSource(definitions, metadata = {}, typed = false) {
189
+ const metadataLines = [];
190
+ if (metadata.generatedFromAccountId) {
191
+ metadataLines.push(`// Generated from AccountId: ${metadata.generatedFromAccountId}`);
192
+ }
193
+ if (metadata.generatedFromProductCount !== undefined) {
194
+ metadataLines.push(`// Products sampled: ${metadata.generatedFromProductCount}`);
195
+ }
196
+ return `${metadataLines.length ? `${metadataLines.join("\n")}\n` : ""}${typed ? "import type { NavigationDefinition } from \"@orderlyshop/web-components\";\n\n" : ""}export const navigationDefinitions${typed ? ": NavigationDefinition[]" : ""} = ${formatNavigationDefinitions(definitions)};
197
+ `;
198
+ }
199
+
200
+ function formatNavigationDefinitions(definitions) {
201
+ return `[\n${definitions.map((definition) => `${formatNavigationDefinition(definition, 2)}`).join(",\n")}\n]`;
202
+ }
203
+
204
+ function formatNavigationDefinition(definition, indent) {
205
+ const pad = " ".repeat(indent);
206
+ const childPad = " ".repeat(indent + 2);
207
+ const lines = [
208
+ `${pad}{`,
209
+ `${childPad}label: ${JSON.stringify(definition.label)},`,
210
+ `${childPad}slug: ${JSON.stringify(definition.slug)},`
211
+ ];
212
+ if (definition.heroImage) {
213
+ lines.push(`${childPad}heroImage: ${JSON.stringify(definition.heroImage)},`);
214
+ }
215
+ if (definition.query) {
216
+ lines.push(`${childPad}query: { query: ${JSON.stringify(definition.query)} }`);
217
+ }
218
+ if (definition.children?.length) {
219
+ if (definition.query) {
220
+ lines[lines.length - 1] += ",";
221
+ }
222
+ lines.push(`${childPad}children: [`);
223
+ lines.push(definition.children.map((child) => formatNavigationDefinition(child, indent + 4)).join(",\n"));
224
+ lines.push(`${childPad}]`);
225
+ }
226
+ lines.push(`${pad}}`);
227
+ return lines.join("\n");
228
+ }
229
+
230
+ function productDumpSource({ accountId, baseUrl, maxResults, result }) {
231
+ return `${JSON.stringify({
232
+ generatedAt: new Date().toISOString(),
233
+ accountId,
234
+ baseUrl,
235
+ maxResults,
236
+ sourceOrder: ["CreatedTime desc"],
237
+ sampling: {
238
+ strategy: "source position 1-200: every 2nd product; 201-500: every 3rd; 501-1000: every 5th; 1001-2000: every 8th; 2001+: every 13th",
239
+ sourceProductsScanned: result.scanned,
240
+ productsWritten: result.products.length
241
+ },
242
+ products: result.products
243
+ }, null, 2)}\n`;
244
+ }
245
+
246
+ function productSummary(product) {
247
+ return {
248
+ title: product.Title ?? "",
249
+ brand: product.Brand ?? "",
250
+ tags: uniqueStrings((product.Tags ?? []).map((tag) => tag.Value).filter(Boolean))
251
+ };
252
+ }
253
+
254
+ function shouldSampleProduct(sourcePosition) {
255
+ return (sourcePosition - 1) % sampleInterval(sourcePosition) === 0;
256
+ }
257
+
258
+ function sampleInterval(sourcePosition) {
259
+ if (sourcePosition <= 200) {
260
+ return 2;
261
+ }
262
+ if (sourcePosition <= 500) {
263
+ return 3;
264
+ }
265
+ if (sourcePosition <= 1000) {
266
+ return 5;
267
+ }
268
+ if (sourcePosition <= 2000) {
269
+ return 8;
270
+ }
271
+ return 13;
272
+ }
273
+
274
+ function uniqueStrings(values) {
275
+ return [...new Set(values)];
276
+ }
277
+
278
+ function positiveInteger(value, label) {
279
+ const parsed = Number(value);
280
+ if (!Number.isInteger(parsed) || parsed <= 0) {
281
+ throw new Error(`${label} must be a positive integer.`);
282
+ }
283
+ return parsed;
284
+ }
285
+
286
+ function defaultProductDumpPath(navigationPath) {
287
+ return resolve(dirname(navigationPath), "navigation-products.json");
288
+ }
289
+
290
+ function defaultNavigationDefinitions() {
291
+ return [
292
+ {
293
+ label: "Clothing",
294
+ slug: "clothing",
295
+ heroImage: "https://images.unsplash.com/photo-1496747611176-843222e1e57c?auto=format&fit=crop&w=1800&q=80",
296
+ query: "clothing fashion",
297
+ children: [
298
+ { label: "Women", slug: "women", query: "women clothing dresses jackets" },
299
+ { label: "Men", slug: "men", query: "men clothing shirts jackets" },
300
+ { label: "Kids", slug: "kids", query: "kids clothing" },
301
+ { label: "Accessories", slug: "accessories", query: "fashion accessories" }
302
+ ]
303
+ },
304
+ {
305
+ label: "Shoes",
306
+ slug: "shoes",
307
+ heroImage: "https://images.unsplash.com/photo-1542291026-7eec264c27ff?auto=format&fit=crop&w=1800&q=80",
308
+ query: "shoes sneakers",
309
+ children: [
310
+ { label: "Women shoes", slug: "women-shoes", query: "women shoes sneakers boots" },
311
+ { label: "Men shoes", slug: "men-shoes", query: "men shoes sneakers boots" },
312
+ { label: "Sport shoes", slug: "sport-shoes", query: "sport shoes running" }
313
+ ]
314
+ },
315
+ {
316
+ label: "Home",
317
+ slug: "home",
318
+ heroImage: "https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?auto=format&fit=crop&w=1800&q=80",
319
+ query: "home interior design",
320
+ children: [
321
+ { label: "Furniture", slug: "furniture", query: "furniture sofa chair table" },
322
+ { label: "Lighting", slug: "lighting", query: "lamp lighting home" },
323
+ { label: "Kitchen", slug: "kitchen", query: "kitchen equipment" }
324
+ ]
325
+ }
326
+ ];
327
+ }