@cimplify/sdk 0.9.9 → 0.10.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/dist/{ads-MkGm5l1T.d.mts → ads-BxbWrwqp.d.mts} +0 -8
  2. package/dist/{ads-MkGm5l1T.d.ts → ads-BxbWrwqp.d.ts} +0 -8
  3. package/dist/advanced.d.mts +2 -2
  4. package/dist/advanced.d.ts +2 -2
  5. package/dist/advanced.js +93 -80
  6. package/dist/advanced.mjs +93 -80
  7. package/dist/cli.js +184 -0
  8. package/dist/{client-CYRL8s1S.d.ts → client-BSrq89H1.d.mts} +42 -374
  9. package/dist/{client-D4EiM667.d.mts → client-xBhdHLq4.d.ts} +42 -374
  10. package/dist/index.d.mts +6 -10
  11. package/dist/index.d.ts +6 -10
  12. package/dist/index.js +98 -126
  13. package/dist/index.mjs +98 -126
  14. package/dist/{payment-D58dS_E9.d.mts → payment-CrNyrc-D.d.mts} +145 -94
  15. package/dist/{payment-D58dS_E9.d.ts → payment-CrNyrc-D.d.ts} +145 -94
  16. package/dist/price-C9Z-hr49.d.mts +21 -0
  17. package/dist/price-RKKoTz-9.d.ts +21 -0
  18. package/dist/react.d.mts +1285 -35
  19. package/dist/react.d.ts +1285 -35
  20. package/dist/react.js +6782 -2781
  21. package/dist/react.mjs +6736 -2783
  22. package/dist/utils.d.mts +55 -2
  23. package/dist/utils.d.ts +55 -2
  24. package/dist/utils.js +23 -20
  25. package/dist/utils.mjs +23 -20
  26. package/package.json +13 -3
  27. package/registry/add-on-selector.json +15 -0
  28. package/registry/availability-badge.json +15 -0
  29. package/registry/booking-card.json +16 -0
  30. package/registry/booking-list.json +16 -0
  31. package/registry/booking-page.json +18 -0
  32. package/registry/bookings-page.json +17 -0
  33. package/registry/bundle-selector.json +15 -0
  34. package/registry/cart-page.json +17 -0
  35. package/registry/cart-summary.json +16 -0
  36. package/registry/catalogue-page.json +18 -0
  37. package/registry/category-filter.json +15 -0
  38. package/registry/category-grid.json +15 -0
  39. package/registry/checkout-page.json +15 -0
  40. package/registry/cn.json +13 -0
  41. package/registry/collection-page.json +16 -0
  42. package/registry/composite-selector.json +15 -0
  43. package/registry/date-slot-picker.json +16 -0
  44. package/registry/deal-banner.json +16 -0
  45. package/registry/deals-page.json +19 -0
  46. package/registry/discount-input.json +16 -0
  47. package/registry/index.json +411 -0
  48. package/registry/order-detail-page.json +16 -0
  49. package/registry/order-history-page.json +17 -0
  50. package/registry/order-history.json +16 -0
  51. package/registry/order-summary.json +16 -0
  52. package/registry/price.json +13 -0
  53. package/registry/product-card.json +17 -0
  54. package/registry/product-customizer.json +20 -0
  55. package/registry/product-grid.json +16 -0
  56. package/registry/product-image-gallery.json +13 -0
  57. package/registry/product-page.json +19 -0
  58. package/registry/product-sheet.json +18 -0
  59. package/registry/quantity-selector.json +13 -0
  60. package/registry/sale-badge.json +16 -0
  61. package/registry/search-input.json +15 -0
  62. package/registry/search-page.json +16 -0
  63. package/registry/service-card.json +16 -0
  64. package/registry/service-grid.json +16 -0
  65. package/registry/slot-picker.json +16 -0
  66. package/registry/staff-picker.json +15 -0
  67. package/registry/store-nav.json +15 -0
  68. package/registry/variant-selector.json +15 -0
  69. package/dist/index-DN_qtwG0.d.mts +0 -320
  70. package/dist/index-jUGW3oGR.d.ts +0 -320
@@ -0,0 +1,411 @@
1
+ {
2
+ "components": [
3
+ {
4
+ "name": "price",
5
+ "title": "Price",
6
+ "description": "Renders a formatted price in the display currency.",
7
+ "type": "component",
8
+ "registryDependencies": []
9
+ },
10
+ {
11
+ "name": "quantity-selector",
12
+ "title": "QuantitySelector",
13
+ "description": "Controlled increment/decrement quantity input.",
14
+ "type": "component",
15
+ "registryDependencies": []
16
+ },
17
+ {
18
+ "name": "variant-selector",
19
+ "title": "VariantSelector",
20
+ "description": "Select product variants via axis chips or direct list.",
21
+ "type": "component",
22
+ "registryDependencies": [
23
+ "price"
24
+ ]
25
+ },
26
+ {
27
+ "name": "add-on-selector",
28
+ "title": "AddOnSelector",
29
+ "description": "Modifier groups with single-select or multi-select options.",
30
+ "type": "component",
31
+ "registryDependencies": [
32
+ "price"
33
+ ]
34
+ },
35
+ {
36
+ "name": "bundle-selector",
37
+ "title": "BundleSelector",
38
+ "description": "Bundle component picker with variant choices and price summary.",
39
+ "type": "component",
40
+ "registryDependencies": [
41
+ "price"
42
+ ]
43
+ },
44
+ {
45
+ "name": "composite-selector",
46
+ "title": "CompositeSelector",
47
+ "description": "Composite product builder with group constraints and live pricing.",
48
+ "type": "component",
49
+ "registryDependencies": [
50
+ "price"
51
+ ]
52
+ },
53
+ {
54
+ "name": "product-customizer",
55
+ "title": "ProductCustomizer",
56
+ "description": "Full product configuration with variants, add-ons, and add-to-cart.",
57
+ "type": "component",
58
+ "registryDependencies": [
59
+ "price",
60
+ "quantity-selector",
61
+ "variant-selector",
62
+ "add-on-selector",
63
+ "composite-selector",
64
+ "bundle-selector"
65
+ ]
66
+ },
67
+ {
68
+ "name": "product-image-gallery",
69
+ "title": "ProductImageGallery",
70
+ "description": "Main image with thumbnail strip for product images.",
71
+ "type": "component",
72
+ "registryDependencies": []
73
+ },
74
+ {
75
+ "name": "cart-summary",
76
+ "title": "CartSummary",
77
+ "description": "Cart line items with quantity controls and totals.",
78
+ "type": "component",
79
+ "registryDependencies": [
80
+ "price",
81
+ "quantity-selector"
82
+ ]
83
+ },
84
+ {
85
+ "name": "availability-badge",
86
+ "title": "AvailabilityBadge",
87
+ "description": "Displays in-stock / out-of-stock status for tracked products.",
88
+ "type": "component",
89
+ "registryDependencies": [
90
+ "cn"
91
+ ]
92
+ },
93
+ {
94
+ "name": "sale-badge",
95
+ "title": "SaleBadge",
96
+ "description": "Sale/discount indicator with percentage and original price.",
97
+ "type": "component",
98
+ "registryDependencies": [
99
+ "price",
100
+ "cn"
101
+ ]
102
+ },
103
+ {
104
+ "name": "product-sheet",
105
+ "title": "ProductSheet",
106
+ "description": "Full product detail view with gallery, header, and customizer.",
107
+ "type": "component",
108
+ "registryDependencies": [
109
+ "price",
110
+ "product-image-gallery",
111
+ "product-customizer",
112
+ "cn"
113
+ ]
114
+ },
115
+ {
116
+ "name": "product-card",
117
+ "title": "ProductCard",
118
+ "description": "Product display card with modal or link mode.",
119
+ "type": "component",
120
+ "registryDependencies": [
121
+ "price",
122
+ "product-sheet",
123
+ "cn"
124
+ ]
125
+ },
126
+ {
127
+ "name": "product-grid",
128
+ "title": "ProductGrid",
129
+ "description": "Responsive CSS grid that renders ProductCards.",
130
+ "type": "component",
131
+ "registryDependencies": [
132
+ "product-card",
133
+ "cn"
134
+ ]
135
+ },
136
+ {
137
+ "name": "cn",
138
+ "title": "cn",
139
+ "description": "Utility that merges Tailwind CSS classes with clsx + tailwind-merge.",
140
+ "type": "utility",
141
+ "registryDependencies": []
142
+ },
143
+ {
144
+ "name": "search-input",
145
+ "title": "SearchInput",
146
+ "description": "Search bar with debounced results dropdown.",
147
+ "type": "component",
148
+ "registryDependencies": [
149
+ "cn"
150
+ ]
151
+ },
152
+ {
153
+ "name": "category-filter",
154
+ "title": "CategoryFilter",
155
+ "description": "Selectable category chips for filtering products.",
156
+ "type": "component",
157
+ "registryDependencies": [
158
+ "cn"
159
+ ]
160
+ },
161
+ {
162
+ "name": "discount-input",
163
+ "title": "DiscountInput",
164
+ "description": "Discount code input with inline validation.",
165
+ "type": "component",
166
+ "registryDependencies": [
167
+ "price",
168
+ "cn"
169
+ ]
170
+ },
171
+ {
172
+ "name": "category-grid",
173
+ "title": "CategoryGrid",
174
+ "description": "Responsive grid of category cards.",
175
+ "type": "component",
176
+ "registryDependencies": [
177
+ "cn"
178
+ ]
179
+ },
180
+ {
181
+ "name": "deal-banner",
182
+ "title": "DealBanner",
183
+ "description": "Displays active deals and promotions.",
184
+ "type": "component",
185
+ "registryDependencies": [
186
+ "price",
187
+ "cn"
188
+ ]
189
+ },
190
+ {
191
+ "name": "order-summary",
192
+ "title": "OrderSummary",
193
+ "description": "Single order detail view with line items and totals.",
194
+ "type": "component",
195
+ "registryDependencies": [
196
+ "price",
197
+ "cn"
198
+ ]
199
+ },
200
+ {
201
+ "name": "order-history",
202
+ "title": "OrderHistory",
203
+ "description": "List of past orders with status and totals.",
204
+ "type": "component",
205
+ "registryDependencies": [
206
+ "price",
207
+ "cn"
208
+ ]
209
+ },
210
+ {
211
+ "name": "store-nav",
212
+ "title": "StoreNav",
213
+ "description": "Top navigation bar with brand, categories, cart badge, and search.",
214
+ "type": "component",
215
+ "registryDependencies": [
216
+ "cn"
217
+ ]
218
+ },
219
+ {
220
+ "name": "catalogue-page",
221
+ "title": "CataloguePage",
222
+ "description": "Browse all products with category filtering and search.",
223
+ "type": "component",
224
+ "registryDependencies": [
225
+ "product-grid",
226
+ "category-filter",
227
+ "search-input",
228
+ "cn"
229
+ ]
230
+ },
231
+ {
232
+ "name": "product-page",
233
+ "title": "ProductPage",
234
+ "description": "Full product detail page with badges and related products.",
235
+ "type": "component",
236
+ "registryDependencies": [
237
+ "product-sheet",
238
+ "availability-badge",
239
+ "sale-badge",
240
+ "product-grid",
241
+ "cn"
242
+ ]
243
+ },
244
+ {
245
+ "name": "cart-page",
246
+ "title": "CartPage",
247
+ "description": "Full-page cart with summary, discount input, and checkout.",
248
+ "type": "component",
249
+ "registryDependencies": [
250
+ "cart-summary",
251
+ "discount-input",
252
+ "cn"
253
+ ]
254
+ },
255
+ {
256
+ "name": "checkout-page",
257
+ "title": "CheckoutPage",
258
+ "description": "Multi-step checkout with auth, address, and payment.",
259
+ "type": "component",
260
+ "registryDependencies": [
261
+ "cn"
262
+ ]
263
+ },
264
+ {
265
+ "name": "collection-page",
266
+ "title": "CollectionPage",
267
+ "description": "Curated product collection with header and grid.",
268
+ "type": "component",
269
+ "registryDependencies": [
270
+ "product-grid",
271
+ "cn"
272
+ ]
273
+ },
274
+ {
275
+ "name": "order-detail-page",
276
+ "title": "OrderDetailPage",
277
+ "description": "Single order detail view with live status polling.",
278
+ "type": "component",
279
+ "registryDependencies": [
280
+ "order-summary",
281
+ "cn"
282
+ ]
283
+ },
284
+ {
285
+ "name": "order-history-page",
286
+ "title": "OrderHistoryPage",
287
+ "description": "Order list with status filtering and inline detail view.",
288
+ "type": "component",
289
+ "registryDependencies": [
290
+ "order-history",
291
+ "order-summary",
292
+ "cn"
293
+ ]
294
+ },
295
+ {
296
+ "name": "search-page",
297
+ "title": "SearchPage",
298
+ "description": "Dedicated search page with input and results grid.",
299
+ "type": "component",
300
+ "registryDependencies": [
301
+ "product-grid",
302
+ "cn"
303
+ ]
304
+ },
305
+ {
306
+ "name": "deals-page",
307
+ "title": "DealsPage",
308
+ "description": "Promotions landing page with deal banners and on-sale products.",
309
+ "type": "component",
310
+ "registryDependencies": [
311
+ "deal-banner",
312
+ "product-grid",
313
+ "sale-badge",
314
+ "product-card",
315
+ "cn"
316
+ ]
317
+ },
318
+ {
319
+ "name": "slot-picker",
320
+ "title": "SlotPicker",
321
+ "description": "Time slot grid for a single day with morning/afternoon/evening grouping.",
322
+ "type": "component",
323
+ "registryDependencies": [
324
+ "price",
325
+ "cn"
326
+ ]
327
+ },
328
+ {
329
+ "name": "date-slot-picker",
330
+ "title": "DateSlotPicker",
331
+ "description": "Horizontal date strip with slot picker for service scheduling.",
332
+ "type": "component",
333
+ "registryDependencies": [
334
+ "slot-picker",
335
+ "cn"
336
+ ]
337
+ },
338
+ {
339
+ "name": "staff-picker",
340
+ "title": "StaffPicker",
341
+ "description": "Staff member selection list with avatar and bio.",
342
+ "type": "component",
343
+ "registryDependencies": [
344
+ "cn"
345
+ ]
346
+ },
347
+ {
348
+ "name": "booking-card",
349
+ "title": "BookingCard",
350
+ "description": "Single booking display with status, time, and action buttons.",
351
+ "type": "component",
352
+ "registryDependencies": [
353
+ "price",
354
+ "cn"
355
+ ]
356
+ },
357
+ {
358
+ "name": "booking-list",
359
+ "title": "BookingList",
360
+ "description": "List of booking cards with optional self-fetching.",
361
+ "type": "component",
362
+ "registryDependencies": [
363
+ "booking-card",
364
+ "cn"
365
+ ]
366
+ },
367
+ {
368
+ "name": "booking-page",
369
+ "title": "BookingPage",
370
+ "description": "Full booking flow with date/slot picker, staff selection, and cart integration.",
371
+ "type": "component",
372
+ "registryDependencies": [
373
+ "date-slot-picker",
374
+ "staff-picker",
375
+ "price",
376
+ "cn"
377
+ ]
378
+ },
379
+ {
380
+ "name": "bookings-page",
381
+ "title": "BookingsPage",
382
+ "description": "Booking history with filter tabs and inline detail view.",
383
+ "type": "component",
384
+ "registryDependencies": [
385
+ "booking-list",
386
+ "booking-card",
387
+ "cn"
388
+ ]
389
+ },
390
+ {
391
+ "name": "service-card",
392
+ "title": "ServiceCard",
393
+ "description": "Service display card with image, duration, price, and book action.",
394
+ "type": "component",
395
+ "registryDependencies": [
396
+ "price",
397
+ "cn"
398
+ ]
399
+ },
400
+ {
401
+ "name": "service-grid",
402
+ "title": "ServiceGrid",
403
+ "description": "Responsive grid of bookable services with self-fetching.",
404
+ "type": "component",
405
+ "registryDependencies": [
406
+ "service-card",
407
+ "cn"
408
+ ]
409
+ }
410
+ ]
411
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "order-detail-page",
3
+ "title": "OrderDetailPage",
4
+ "description": "Single order detail view with live status polling.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "order-summary",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "order-detail-page.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Order, LineItem } from \"@cimplify/sdk\";\nimport { OrderSummary } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderDetailPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n backButton?: string;\n summary?: string;\n}\n\nexport interface OrderDetailPageProps {\n /** Order ID to display. */\n orderId: string;\n /** Pre-fetched order for SSR. */\n order?: Order;\n /** Poll for status updates. Default: true. */\n poll?: boolean;\n /** Called when back button is clicked. */\n onBack?: () => void;\n /** Custom line item renderer. */\n renderLineItem?: (item: LineItem) => React.ReactNode;\n /** Back button label. */\n backLabel?: string;\n className?: string;\n classNames?: OrderDetailPageClassNames;\n}\n\n/**\n * OrderDetailPage — single order detail view with live status polling.\n *\n * SSR-friendly: pass `order` prop for server rendering.\n */\nexport function OrderDetailPage({\n orderId,\n order,\n poll = true,\n onBack,\n renderLineItem,\n backLabel = \"Back to orders\",\n className,\n classNames,\n}: OrderDetailPageProps): React.ReactElement {\n return (\n <div data-cimplify-order-detail-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-detail-header className={classNames?.header}>\n {onBack && (\n <button\n type=\"button\"\n onClick={onBack}\n data-cimplify-order-detail-back\n className={classNames?.backButton}\n >\n {backLabel}\n </button>\n )}\n <h1 data-cimplify-order-detail-title className={classNames?.title}>\n Order Details\n </h1>\n </div>\n\n {/* Order summary */}\n <div data-cimplify-order-detail-content className={classNames?.summary}>\n <OrderSummary\n order={order}\n orderId={order ? undefined : orderId}\n poll={poll}\n renderLineItem={renderLineItem}\n />\n </div>\n </div>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "order-history-page",
3
+ "title": "OrderHistoryPage",
4
+ "description": "Order list with status filtering and inline detail view.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "order-history",
8
+ "order-summary",
9
+ "cn"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "order-history-page.tsx",
14
+ "content": "\"use client\";\n\nimport React, { useState, useCallback } from \"react\";\nimport type { Order, OrderStatus } from \"@cimplify/sdk\";\nimport { OrderHistory } from \"@cimplify/sdk/react\";\nimport { OrderSummary } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderHistoryPageClassNames {\n root?: string;\n header?: string;\n title?: string;\n filters?: string;\n filterButton?: string;\n list?: string;\n detail?: string;\n backButton?: string;\n}\n\nexport interface OrderHistoryPageProps {\n /** Page title. */\n title?: string;\n /** Pre-fetched orders for SSR. */\n orders?: Order[];\n /** Max orders to show per page. */\n limit?: number;\n /** Called when navigating to an order detail (e.g. for routing). */\n onOrderNavigate?: (order: Order) => void;\n /** Show status filter tabs. Default: true. */\n showFilters?: boolean;\n /** Custom order row renderer. */\n renderOrder?: (order: Order) => React.ReactNode;\n className?: string;\n classNames?: OrderHistoryPageClassNames;\n}\n\nconst STATUS_FILTERS: { label: string; value: OrderStatus | undefined }[] = [\n { label: \"All\", value: undefined },\n { label: \"Active\", value: \"confirmed\" },\n { label: \"Completed\", value: \"completed\" },\n { label: \"Cancelled\", value: \"cancelled\" },\n];\n\n/**\n * OrderHistoryPage — order list with inline detail view and status filtering.\n *\n * SSR-friendly: pass `orders` prop for server rendering.\n * Supports both inline detail view and external navigation via `onOrderNavigate`.\n */\nexport function OrderHistoryPage({\n title = \"Order History\",\n orders: ordersProp,\n limit = 20,\n onOrderNavigate,\n showFilters = true,\n renderOrder,\n className,\n classNames,\n}: OrderHistoryPageProps): React.ReactElement {\n const [statusFilter, setStatusFilter] = useState<OrderStatus | undefined>(undefined);\n const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);\n\n const handleOrderClick = useCallback(\n (order: Order) => {\n if (onOrderNavigate) {\n onOrderNavigate(order);\n } else {\n setSelectedOrder(order);\n }\n },\n [onOrderNavigate],\n );\n\n const handleBack = useCallback(() => {\n setSelectedOrder(null);\n }, []);\n\n // Inline detail view\n if (selectedOrder && !onOrderNavigate) {\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n <div data-cimplify-order-history-detail className={classNames?.detail}>\n <button\n type=\"button\"\n onClick={handleBack}\n data-cimplify-order-history-back\n className={classNames?.backButton}\n >\n Back to orders\n </button>\n <OrderSummary order={selectedOrder} />\n </div>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-history-page className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-history-header className={classNames?.header}>\n <h1 data-cimplify-order-history-title className={classNames?.title}>\n {title}\n </h1>\n </div>\n\n {/* Status filter */}\n {showFilters && (\n <div data-cimplify-order-history-filters className={classNames?.filters} role=\"tablist\">\n {STATUS_FILTERS.map((filter) => (\n <button\n key={filter.label}\n type=\"button\"\n role=\"tab\"\n aria-selected={statusFilter === filter.value}\n onClick={() => setStatusFilter(filter.value)}\n data-cimplify-order-filter\n data-selected={statusFilter === filter.value || undefined}\n className={classNames?.filterButton}\n >\n {filter.label}\n </button>\n ))}\n </div>\n )}\n\n {/* Order list */}\n <div data-cimplify-order-history-list className={classNames?.list}>\n <OrderHistory\n orders={ordersProp}\n status={statusFilter}\n limit={limit}\n onOrderClick={handleOrderClick}\n renderOrder={renderOrder}\n />\n </div>\n </div>\n );\n}\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "order-history",
3
+ "title": "OrderHistory",
4
+ "description": "List of past orders with status and totals.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "order-history.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Order, OrderStatus } from \"@cimplify/sdk\";\nimport { useOrders } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderHistoryClassNames {\n root?: string;\n item?: string;\n orderId?: string;\n status?: string;\n date?: string;\n total?: string;\n empty?: string;\n loading?: string;\n}\n\nexport interface OrderHistoryProps {\n /** Override orders (skips fetch). For SSR, pass pre-fetched orders. */\n orders?: Order[];\n /** Filter by status. */\n status?: OrderStatus;\n /** Max orders to display. */\n limit?: number;\n /** Called when an order row is clicked. */\n onOrderClick?: (order: Order) => void;\n /** Custom order row renderer. */\n renderOrder?: (order: Order) => React.ReactNode;\n /** Text shown when empty. */\n emptyMessage?: string;\n className?: string;\n classNames?: OrderHistoryClassNames;\n}\n\nconst STATUS_LABELS: Record<OrderStatus, string> = {\n pending: \"Pending\",\n created: \"Created\",\n confirmed: \"Confirmed\",\n in_preparation: \"In Preparation\",\n ready_to_serve: \"Ready\",\n partially_served: \"Partially Served\",\n served: \"Served\",\n delivered: \"Delivered\",\n picked_up: \"Picked Up\",\n completed: \"Completed\",\n cancelled: \"Cancelled\",\n refunded: \"Refunded\",\n};\n\n/**\n * OrderHistory — list of past orders with status and totals.\n *\n * Fetches via `useOrders` unless pre-loaded orders are passed.\n */\nexport function OrderHistory({\n orders: ordersProp,\n status,\n limit,\n onOrderClick,\n renderOrder,\n emptyMessage = \"No orders yet\",\n className,\n classNames,\n}: OrderHistoryProps): React.ReactElement {\n const { orders: fetched, isLoading } = useOrders({\n status,\n limit,\n enabled: ordersProp === undefined,\n });\n\n const orders = ordersProp ?? fetched;\n\n if (isLoading && orders.length === 0) {\n return (\n <div\n data-cimplify-order-history\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n />\n );\n }\n\n if (orders.length === 0) {\n return (\n <div\n data-cimplify-order-history\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage}</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-history className={cn(className, classNames?.root)}>\n {orders.map((order) => (\n <button\n key={order.id}\n type=\"button\"\n onClick={() => onOrderClick?.(order)}\n data-cimplify-order-history-item\n data-status={order.status}\n className={classNames?.item}\n >\n {renderOrder ? (\n renderOrder(order)\n ) : (\n <>\n <div data-cimplify-order-history-main>\n <span data-cimplify-order-history-id className={classNames?.orderId}>\n #{order.user_friendly_id}\n </span>\n <span\n data-cimplify-order-history-status\n data-status={order.status}\n className={classNames?.status}\n >\n {STATUS_LABELS[order.status] ?? order.status}\n </span>\n </div>\n <div data-cimplify-order-history-details>\n <time\n data-cimplify-order-history-date\n dateTime={order.created_at}\n className={classNames?.date}\n >\n {new Date(order.created_at).toLocaleDateString()}\n </time>\n <span data-cimplify-order-history-items>\n {order.total_quantity} {order.total_quantity === 1 ? \"item\" : \"items\"}\n </span>\n <span data-cimplify-order-history-total className={classNames?.total}>\n <Price amount={order.total_price} />\n </span>\n </div>\n </>\n )}\n </button>\n ))}\n </div>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "order-summary",
3
+ "title": "OrderSummary",
4
+ "description": "Single order detail view with line items and totals.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "order-summary.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Order, LineItem, OrderStatus } from \"@cimplify/sdk\";\nimport { useOrder } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface OrderSummaryClassNames {\n root?: string;\n header?: string;\n orderId?: string;\n status?: string;\n items?: string;\n lineItem?: string;\n totals?: string;\n customer?: string;\n loading?: string;\n}\n\nexport interface OrderSummaryProps {\n /** Pass an Order object directly (skips fetch). */\n order?: Order;\n /** Or pass an order ID to fetch via useOrder. */\n orderId?: string;\n /** Poll for status updates. */\n poll?: boolean;\n /** Custom line item renderer. */\n renderLineItem?: (item: LineItem) => React.ReactNode;\n className?: string;\n classNames?: OrderSummaryClassNames;\n}\n\nconst STATUS_LABELS: Record<OrderStatus, string> = {\n pending: \"Pending\",\n created: \"Created\",\n confirmed: \"Confirmed\",\n in_preparation: \"In Preparation\",\n ready_to_serve: \"Ready\",\n partially_served: \"Partially Served\",\n served: \"Served\",\n delivered: \"Delivered\",\n picked_up: \"Picked Up\",\n completed: \"Completed\",\n cancelled: \"Cancelled\",\n refunded: \"Refunded\",\n};\n\n/**\n * OrderSummary — displays a single order's details, line items, and totals.\n *\n * Accepts either a pre-loaded `order` object or an `orderId` to fetch.\n * Supports polling for live status updates.\n */\nexport function OrderSummary({\n order: orderProp,\n orderId,\n poll = false,\n renderLineItem,\n className,\n classNames,\n}: OrderSummaryProps): React.ReactElement {\n const { order: fetched, isLoading } = useOrder(orderProp ? null : orderId, {\n enabled: !orderProp && !!orderId,\n poll,\n });\n\n const order = orderProp ?? fetched;\n\n if (isLoading && !order) {\n return (\n <div\n data-cimplify-order-summary\n aria-busy=\"true\"\n className={cn(className, classNames?.root, classNames?.loading)}\n >\n <div data-cimplify-order-summary-skeleton />\n </div>\n );\n }\n\n if (!order) {\n return (\n <div data-cimplify-order-summary className={cn(className, classNames?.root)}>\n <p>Order not found.</p>\n </div>\n );\n }\n\n return (\n <div data-cimplify-order-summary className={cn(className, classNames?.root)}>\n {/* Header */}\n <div data-cimplify-order-header className={classNames?.header}>\n <span data-cimplify-order-id className={classNames?.orderId}>\n Order #{order.user_friendly_id}\n </span>\n <span\n data-cimplify-order-status\n data-status={order.status}\n className={classNames?.status}\n >\n {STATUS_LABELS[order.status] ?? order.status}\n </span>\n </div>\n\n {/* Date */}\n <time data-cimplify-order-date dateTime={order.created_at}>\n {new Date(order.created_at).toLocaleDateString(undefined, {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n })}\n </time>\n\n {/* Customer info */}\n {(order.customer_name || order.customer_email) && (\n <div data-cimplify-order-customer className={classNames?.customer}>\n {order.customer_name && (\n <span data-cimplify-order-customer-name>{order.customer_name}</span>\n )}\n {order.customer_email && (\n <span data-cimplify-order-customer-email>{order.customer_email}</span>\n )}\n </div>\n )}\n\n {/* Line items */}\n <div data-cimplify-order-items className={classNames?.items}>\n {order.items.map((item) =>\n renderLineItem ? (\n <React.Fragment key={item.id}>{renderLineItem(item)}</React.Fragment>\n ) : (\n <div key={item.id} data-cimplify-order-line-item className={classNames?.lineItem}>\n <div data-cimplify-order-line-info>\n <span data-cimplify-order-line-qty>{item.quantity}&times;</span>\n <span data-cimplify-order-line-key>{item.line_key}</span>\n </div>\n <Price amount={item.price} />\n </div>\n ),\n )}\n </div>\n\n {/* Totals */}\n <div data-cimplify-order-totals className={classNames?.totals}>\n {order.total_discount != null && order.total_discount !== 0 && (\n <div data-cimplify-order-discount>\n <span>Discount</span>\n <Price amount={order.total_discount} prefix=\"-\" />\n </div>\n )}\n {order.service_charge != null && order.service_charge !== 0 && (\n <div data-cimplify-order-service-charge>\n <span>Service charge</span>\n <Price amount={order.service_charge} />\n </div>\n )}\n {order.tax != null && order.tax !== 0 && (\n <div data-cimplify-order-tax>\n <span>Tax</span>\n <Price amount={order.tax} />\n </div>\n )}\n <div data-cimplify-order-total>\n <span>Total</span>\n <Price amount={order.total_price} />\n </div>\n </div>\n\n {/* Tracking */}\n {order.tracking_link && (\n <a\n href={order.tracking_link}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n data-cimplify-order-tracking\n >\n Track your order\n </a>\n )}\n </div>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "price",
3
+ "title": "Price",
4
+ "description": "Renders a formatted price in the display currency.",
5
+ "type": "component",
6
+ "registryDependencies": [],
7
+ "files": [
8
+ {
9
+ "path": "price.tsx",
10
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport { formatPrice } from \"@cimplify/sdk\";\nimport type { CurrencyCode } from \"@cimplify/sdk\";\nimport { useCimplify } from \"@cimplify/sdk/react\";\n\nexport interface PriceProps {\n /** The amount in base (business) currency. */\n amount: number | string;\n /** Optional CSS class name for the wrapping span. */\n className?: string;\n /** Optional prefix rendered before the formatted price (e.g. \"+\"). */\n prefix?: string;\n}\n\n/**\n * Price — renders a single price value in the user's chosen display currency.\n *\n * Reads `displayCurrency` and `convertPrice` from CimplifyProvider context\n * so every price on the page stays consistent with the selected currency.\n */\nexport function Price({ amount, className, prefix }: PriceProps): React.ReactElement {\n const { displayCurrency, convertPrice } = useCimplify();\n return (\n <span className={className}>\n {prefix}\n {formatPrice(convertPrice(amount), displayCurrency as CurrencyCode)}\n </span>\n );\n}\n"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "product-card",
3
+ "title": "ProductCard",
4
+ "description": "Product display card with modal or link mode.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "product-sheet",
9
+ "cn"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "product-card.tsx",
14
+ "content": "\"use client\";\n\nimport React, { useCallback, useRef, useState } from \"react\";\nimport type { Product, ProductWithDetails } from \"@cimplify/sdk\";\nimport { useProduct } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { ProductSheet } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\nexport interface ProductCardClassNames {\n root?: string;\n imageContainer?: string;\n image?: string;\n body?: string;\n name?: string;\n description?: string;\n price?: string;\n badges?: string;\n badge?: string;\n modal?: string;\n modalOverlay?: string;\n}\n\nexport interface ProductCardProps {\n /** The product to display. */\n product: Product;\n /** Display mode: \"card\" opens a modal, \"page\" renders as a link. Auto-detected from product.display_mode. */\n displayMode?: \"card\" | \"page\";\n /** Link href for page mode. Default: `/menu/${product.slug}` */\n href?: string;\n /** Custom modal content renderer. Receives the fully-loaded product. */\n renderModal?: (product: ProductWithDetails) => React.ReactNode;\n /** Custom image renderer (e.g. Next.js Image). */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Replace the entire default card body. */\n children?: React.ReactNode;\n /** Image aspect ratio. Default: \"4/3\". */\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n classNames?: ProductCardClassNames;\n}\n\n/**\n * ProductCard — a product display card with two modes:\n *\n * - **card** (default): clickable button that opens a native `<dialog>` modal with a ProductSheet\n * - **page**: a plain `<a>` link for SEO-friendly product pages\n */\nexport function ProductCard({\n product,\n displayMode,\n href,\n renderModal,\n renderImage,\n children,\n aspectRatio = \"4/3\",\n className,\n classNames,\n}: ProductCardProps): React.ReactElement {\n const mode = displayMode ?? product.display_mode ?? \"card\";\n const [isOpen, setIsOpen] = useState(false);\n const dialogRef = useRef<HTMLDialogElement>(null);\n\n // Lazy-fetch product details only when modal is open\n const { product: productDetails } = useProduct(\n product.slug ?? product.id,\n { enabled: isOpen },\n );\n\n const handleOpen = useCallback(() => {\n setIsOpen(true);\n dialogRef.current?.showModal();\n }, []);\n\n const handleClose = useCallback(() => {\n dialogRef.current?.close();\n setIsOpen(false);\n }, []);\n\n const handleCancel = useCallback(() => {\n setIsOpen(false);\n }, []);\n\n const handleBackdropClick = useCallback(\n (e: React.MouseEvent<HTMLDialogElement>) => {\n if (e.target === dialogRef.current) {\n handleClose();\n }\n },\n [handleClose],\n );\n\n const imageUrl = product.image_url || product.images?.[0];\n\n const cardBody = children ?? (\n <>\n {/* Image */}\n {imageUrl && (\n <div\n data-cimplify-product-card-image-container\n className={classNames?.imageContainer}\n style={{\n overflow: \"hidden\",\n ...ASPECT_STYLES[aspectRatio],\n }}\n >\n {renderImage ? (\n renderImage({\n src: imageUrl,\n alt: product.name,\n className: classNames?.image,\n })\n ) : (\n <img\n src={imageUrl}\n alt={product.name}\n className={classNames?.image}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n data-cimplify-product-card-image\n />\n )}\n </div>\n )}\n\n {/* Body */}\n <div\n data-cimplify-product-card-body\n className={classNames?.body}\n >\n <span\n data-cimplify-product-card-name\n className={classNames?.name}\n >\n {product.name}\n </span>\n {product.description && (\n <span\n data-cimplify-product-card-description\n className={classNames?.description}\n style={{\n display: \"-webkit-box\",\n WebkitLineClamp: 2,\n WebkitBoxOrient: \"vertical\",\n overflow: \"hidden\",\n }}\n >\n {product.description}\n </span>\n )}\n <Price\n amount={product.default_price}\n className={classNames?.price}\n />\n </div>\n </>\n );\n\n // Page mode — render as a link\n if (mode === \"page\") {\n return (\n <a\n href={href ?? `/menu/${product.slug}`}\n data-cimplify-product-card\n data-display-mode=\"page\"\n className={cn(className, classNames?.root)}\n style={{ display: \"block\", textDecoration: \"none\", color: \"inherit\" }}\n >\n {cardBody}\n </a>\n );\n }\n\n // Card mode — render as button + native dialog\n return (\n <>\n <button\n type=\"button\"\n aria-haspopup=\"dialog\"\n onClick={handleOpen}\n data-cimplify-product-card\n data-display-mode=\"card\"\n className={cn(className, classNames?.root)}\n style={{\n display: \"block\",\n width: \"100%\",\n textAlign: \"inherit\",\n background: \"none\",\n border: \"none\",\n padding: 0,\n cursor: \"pointer\",\n font: \"inherit\",\n color: \"inherit\",\n }}\n >\n {cardBody}\n </button>\n\n <dialog\n ref={dialogRef}\n onCancel={handleCancel}\n onClick={handleBackdropClick}\n data-cimplify-product-card-modal\n className={classNames?.modal}\n style={{\n border: \"none\",\n borderRadius: \"0.75rem\",\n padding: \"1.5rem\",\n maxWidth: \"32rem\",\n width: \"100%\",\n maxHeight: \"90vh\",\n overflow: \"auto\",\n }}\n >\n {isOpen && (\n productDetails ? (\n renderModal ? (\n renderModal(productDetails)\n ) : (\n <ProductSheet\n product={productDetails}\n onClose={handleClose}\n renderImage={renderImage}\n />\n )\n ) : (\n <div\n data-cimplify-product-card-modal-loading\n aria-busy=\"true\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"1rem\",\n }}\n >\n <div\n style={{\n aspectRatio: \"4/3\",\n backgroundColor: \"rgba(0,0,0,0.06)\",\n borderRadius: \"0.5rem\",\n }}\n />\n <div\n style={{\n height: \"1.5rem\",\n width: \"60%\",\n backgroundColor: \"rgba(0,0,0,0.06)\",\n borderRadius: \"0.25rem\",\n }}\n />\n </div>\n )\n )}\n </dialog>\n </>\n );\n}\n"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "product-customizer",
3
+ "title": "ProductCustomizer",
4
+ "description": "Full product configuration with variants, add-ons, and add-to-cart.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "price",
8
+ "quantity-selector",
9
+ "variant-selector",
10
+ "add-on-selector",
11
+ "composite-selector",
12
+ "bundle-selector"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "product-customizer.tsx",
17
+ "content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect } from \"react\";\nimport type {\n ProductWithDetails,\n ProductVariant,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { useCart, useQuote } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { VariantSelector } from \"@cimplify/sdk/react\";\nimport { AddOnSelector } from \"@cimplify/sdk/react\";\nimport { CompositeSelector } from \"@cimplify/sdk/react\";\nimport { BundleSelector } from \"@cimplify/sdk/react\";\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n className?: string;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n className,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<ProductVariant | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n\n const cart = useCart();\n\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n }, [product.id]);\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const quoteEnabled = isComposite\n ? compositeReady\n : isBundle\n ? bundleReady\n : requiredAddOnsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n const quotedTotal =\n quote.quoted_total_price_info?.final_price ?? quote.final_price_info.final_price;\n return quotedTotal === undefined || quotedTotal === null ? undefined : parsePrice(quotedTotal);\n }, [quote]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: ProductVariant | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n },\n [],\n );\n\n const handleAddToCart = async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || \"\", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n setTimeout(() => {\n setIsAdded(false);\n setQuantity(1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div data-cimplify-customizer className={className}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n />\n )}\n\n <div data-cimplify-customizer-actions>\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={1}\n />\n\n <button\n type=\"button\"\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? \"cimplify-customizer-validation\" : undefined}\n data-cimplify-customizer-submit\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart &middot; <Price amount={displayTotalPrice} />\n </>\n )}\n </button>\n </div>\n\n {!quoteEnabled && (\n <p id=\"cimplify-customizer-validation\" data-cimplify-customizer-validation>Please select all required options</p>\n )}\n </div>\n );\n}\n"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "product-grid",
3
+ "title": "ProductGrid",
4
+ "description": "Responsive CSS grid that renders ProductCards.",
5
+ "type": "component",
6
+ "registryDependencies": [
7
+ "product-card",
8
+ "cn"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "product-grid.tsx",
13
+ "content": "\"use client\";\n\nimport React from \"react\";\nimport type { Product } from \"@cimplify/sdk\";\nimport { ProductCard } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductGridClassNames {\n root?: string;\n item?: string;\n empty?: string;\n}\n\nexport interface ProductGridProps {\n /** Products to display in the grid. */\n products: Product[];\n /** Responsive column counts at each breakpoint. */\n columns?: { sm?: number; md?: number; lg?: number; xl?: number };\n /** Custom card renderer per product. */\n renderCard?: (product: Product) => React.ReactNode;\n /** Custom image renderer passed to default ProductCards. */\n renderImage?: (props: {\n src: string;\n alt: string;\n className?: string;\n }) => React.ReactNode;\n /** Text shown when `products` is empty. */\n emptyMessage?: string;\n className?: string;\n classNames?: ProductGridClassNames;\n}\n\n/**\n * ProductGrid — responsive CSS grid that renders ProductCards.\n *\n * Injects an inline `<style>` tag with media queries for responsive columns.\n * Uses `React.useId()` for a hydration-safe, collision-free CSS selector.\n */\nexport function ProductGrid({\n products,\n columns,\n renderCard,\n renderImage,\n emptyMessage,\n className,\n classNames,\n}: ProductGridProps): React.ReactElement {\n const rawId = React.useId();\n // CSS selectors can't contain colons, so strip them from the React-generated ID\n const gridId = `cimplify-grid-${rawId.replace(/:/g, \"\")}`;\n\n const sm = columns?.sm ?? 1;\n const md = columns?.md ?? 2;\n const lg = columns?.lg ?? 3;\n const xl = columns?.xl ?? 4;\n\n if (products.length === 0) {\n return (\n <div\n data-cimplify-product-grid\n data-empty\n className={cn(className, classNames?.root, classNames?.empty)}\n >\n <p>{emptyMessage ?? \"No products found\"}</p>\n </div>\n );\n }\n\n const css = [\n `#${gridId}{display:grid;grid-template-columns:repeat(${sm},1fr);gap:1rem}`,\n `@media(min-width:768px){#${gridId}{grid-template-columns:repeat(${md},1fr)}}`,\n `@media(min-width:1024px){#${gridId}{grid-template-columns:repeat(${lg},1fr)}}`,\n `@media(min-width:1280px){#${gridId}{grid-template-columns:repeat(${xl},1fr)}}`,\n ].join(\"\");\n\n return (\n <>\n <style dangerouslySetInnerHTML={{ __html: css }} />\n <div\n id={gridId}\n data-cimplify-product-grid\n className={cn(className, classNames?.root)}\n >\n {products.map((product) => (\n <div\n key={product.id}\n data-cimplify-product-grid-item\n className={classNames?.item}\n >\n {renderCard\n ? renderCard(product)\n : (\n <ProductCard\n product={product}\n renderImage={renderImage}\n />\n )}\n </div>\n ))}\n </div>\n </>\n );\n}\n"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "product-image-gallery",
3
+ "title": "ProductImageGallery",
4
+ "description": "Main image with thumbnail strip for product images.",
5
+ "type": "component",
6
+ "registryDependencies": [],
7
+ "files": [
8
+ {
9
+ "path": "product-image-gallery.tsx",
10
+ "content": "\"use client\";\n\nimport React, { useEffect, useMemo, useState } from \"react\";\n\nexport interface ProductImageGalleryProps {\n images: string[];\n productName: string;\n aspectRatio?: \"square\" | \"4/3\" | \"16/10\" | \"3/4\";\n className?: string;\n}\n\nconst ASPECT_STYLES: Record<string, React.CSSProperties> = {\n square: { aspectRatio: \"1/1\" },\n \"4/3\": { aspectRatio: \"4/3\" },\n \"16/10\": { aspectRatio: \"16/10\" },\n \"3/4\": { aspectRatio: \"3/4\" },\n};\n\n/**\n * ProductImageGallery — main image + thumbnail strip.\n *\n * Uses plain `<img>` for framework-agnostic rendering (not Next.js Image).\n */\nexport function ProductImageGallery({\n images,\n productName,\n aspectRatio = \"4/3\",\n className,\n}: ProductImageGalleryProps): React.ReactElement | null {\n const normalizedImages = useMemo(\n () =>\n images.filter(\n (image): image is string =>\n typeof image === \"string\" && image.trim().length > 0,\n ),\n [images],\n );\n\n const [selectedImage, setSelectedImage] = useState(0);\n\n useEffect(() => {\n setSelectedImage(0);\n }, [normalizedImages.length, productName]);\n\n if (normalizedImages.length === 0) {\n return null;\n }\n\n const activeImage = normalizedImages[selectedImage] || normalizedImages[0];\n\n return (\n <div data-cimplify-image-gallery className={className}>\n <div\n data-cimplify-image-gallery-main\n style={{ position: \"relative\", overflow: \"hidden\", ...ASPECT_STYLES[aspectRatio] }}\n >\n <img\n src={activeImage}\n alt={productName}\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n data-cimplify-image-gallery-active\n />\n </div>\n\n {normalizedImages.length > 1 && (\n <div data-cimplify-image-gallery-thumbnails style={{ display: \"flex\", gap: \"0.5rem\", marginTop: \"0.75rem\" }}>\n {normalizedImages.map((image, index) => (\n <button\n key={`${image}-${index}`}\n type=\"button\"\n onClick={() => setSelectedImage(index)}\n aria-selected={selectedImage === index}\n data-cimplify-image-gallery-thumb\n data-selected={selectedImage === index || undefined}\n style={{\n width: \"4rem\",\n height: \"4rem\",\n overflow: \"hidden\",\n padding: 0,\n border: \"none\",\n cursor: \"pointer\",\n }}\n >\n <img\n src={image}\n alt=\"\"\n style={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n />\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}\n"
11
+ }
12
+ ]
13
+ }