@cimplify/sdk 0.46.3 → 0.48.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 (39) hide show
  1. package/dist/advanced.d.mts +1 -1
  2. package/dist/advanced.d.ts +1 -1
  3. package/dist/advanced.js +20 -20
  4. package/dist/advanced.mjs +1 -1
  5. package/dist/{chunk-6HYKWYUF.mjs → chunk-24FK7VFL.mjs} +1 -1
  6. package/dist/{chunk-Z2MLAIID.js → chunk-CYGLTD7D.js} +35 -35
  7. package/dist/{chunk-TW4OFRWV.js → chunk-D22UVSFN.js} +2 -2
  8. package/dist/{chunk-7ZACMER7.js → chunk-DR4UPU6P.js} +0 -1
  9. package/dist/{chunk-WUIERJ6J.mjs → chunk-MBR2DBEN.mjs} +1 -1
  10. package/dist/{chunk-CKRMA5F7.mjs → chunk-OFNVLUH4.mjs} +0 -1
  11. package/dist/{client-QVINYu1X.d.ts → client-B8tJnOde.d.ts} +0 -1
  12. package/dist/{client-G2WCoxv2.d.mts → client-BZZK1txR.d.mts} +0 -1
  13. package/dist/{client-B76ZNW5r.d.ts → client-BdbvMtOU.d.ts} +1 -1
  14. package/dist/{client-CT9NwIDX.d.mts → client-BqCAm5vI.d.mts} +1 -1
  15. package/dist/index.d.mts +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +61 -61
  18. package/dist/index.mjs +2 -2
  19. package/dist/react.d.mts +113 -10
  20. package/dist/react.d.ts +113 -10
  21. package/dist/react.js +420 -248
  22. package/dist/react.mjs +405 -235
  23. package/dist/server.d.mts +2 -2
  24. package/dist/server.d.ts +2 -2
  25. package/dist/server.js +3 -3
  26. package/dist/server.mjs +2 -2
  27. package/dist/styles.css +1 -1
  28. package/dist/testing/suite.d.mts +2 -2
  29. package/dist/testing/suite.d.ts +2 -2
  30. package/dist/testing/suite.js +22 -22
  31. package/dist/testing/suite.mjs +3 -3
  32. package/dist/testing.d.mts +2 -2
  33. package/dist/testing.d.ts +2 -2
  34. package/dist/testing.js +78 -78
  35. package/dist/testing.mjs +4 -4
  36. package/package.json +3 -1
  37. package/registry/customer-input-fields.json +1 -1
  38. package/registry/date-slot-picker.json +1 -1
  39. package/registry/slot-picker.json +1 -1
package/dist/testing.js CHANGED
@@ -1,28 +1,28 @@
1
1
  'use strict';
2
2
 
3
- var chunkTW4OFRWV_js = require('./chunk-TW4OFRWV.js');
4
- require('./chunk-Z2MLAIID.js');
5
- require('./chunk-7ZACMER7.js');
3
+ var chunkD22UVSFN_js = require('./chunk-D22UVSFN.js');
4
+ require('./chunk-CYGLTD7D.js');
5
+ require('./chunk-DR4UPU6P.js');
6
6
  require('./chunk-6RP6OPYO.js');
7
7
  require('./chunk-GEWFWQYK.js');
8
8
  require('./chunk-TKOTACKZ.js');
9
9
  var zod = require('zod');
10
10
 
11
11
  var PUBLIC_SCHEMAS = {
12
- Brand: chunkTW4OFRWV_js.BrandSchema,
13
- AddItemPayload: chunkTW4OFRWV_js.AddItemPayloadSchema,
14
- Cart: chunkTW4OFRWV_js.CartSchema,
15
- CartItem: chunkTW4OFRWV_js.CartItemSchema,
16
- CartPricing: chunkTW4OFRWV_js.CartPricingSchema,
17
- CheckoutBody: chunkTW4OFRWV_js.CheckoutBodySchema,
18
- CheckoutResponse: chunkTW4OFRWV_js.CheckoutResponseSchema,
19
- VariantInfo: chunkTW4OFRWV_js.VariantInfoSchema
12
+ Brand: chunkD22UVSFN_js.BrandSchema,
13
+ AddItemPayload: chunkD22UVSFN_js.AddItemPayloadSchema,
14
+ Cart: chunkD22UVSFN_js.CartSchema,
15
+ CartItem: chunkD22UVSFN_js.CartItemSchema,
16
+ CartPricing: chunkD22UVSFN_js.CartPricingSchema,
17
+ CheckoutBody: chunkD22UVSFN_js.CheckoutBodySchema,
18
+ CheckoutResponse: chunkD22UVSFN_js.CheckoutResponseSchema,
19
+ VariantInfo: chunkD22UVSFN_js.VariantInfoSchema
20
20
  };
21
21
  function exportJsonSchemas() {
22
22
  const out = {};
23
23
  for (const [name, schema] of Object.entries(PUBLIC_SCHEMAS)) {
24
- const meta = chunkTW4OFRWV_js.cimplifyRegistry.get(schema);
25
- const json = zod.z.toJSONSchema(schema, { metadata: chunkTW4OFRWV_js.cimplifyRegistry });
24
+ const meta = chunkD22UVSFN_js.cimplifyRegistry.get(schema);
25
+ const json = zod.z.toJSONSchema(schema, { metadata: chunkD22UVSFN_js.cimplifyRegistry });
26
26
  if (meta) {
27
27
  json.$id = meta.id;
28
28
  json["x-cimplify-version"] = meta.version;
@@ -34,16 +34,16 @@ function exportJsonSchemas() {
34
34
  function listRegisteredSchemas() {
35
35
  const entries = [];
36
36
  for (const schema of [
37
- chunkTW4OFRWV_js.BrandSchema,
38
- chunkTW4OFRWV_js.AddItemPayloadSchema,
39
- chunkTW4OFRWV_js.CartSchema,
40
- chunkTW4OFRWV_js.CartItemSchema,
41
- chunkTW4OFRWV_js.CartPricingSchema,
42
- chunkTW4OFRWV_js.CheckoutBodySchema,
43
- chunkTW4OFRWV_js.CheckoutResponseSchema,
44
- chunkTW4OFRWV_js.VariantInfoSchema
37
+ chunkD22UVSFN_js.BrandSchema,
38
+ chunkD22UVSFN_js.AddItemPayloadSchema,
39
+ chunkD22UVSFN_js.CartSchema,
40
+ chunkD22UVSFN_js.CartItemSchema,
41
+ chunkD22UVSFN_js.CartPricingSchema,
42
+ chunkD22UVSFN_js.CheckoutBodySchema,
43
+ chunkD22UVSFN_js.CheckoutResponseSchema,
44
+ chunkD22UVSFN_js.VariantInfoSchema
45
45
  ]) {
46
- const meta = chunkTW4OFRWV_js.cimplifyRegistry.get(schema);
46
+ const meta = chunkD22UVSFN_js.cimplifyRegistry.get(schema);
47
47
  if (meta) entries.push({ schema, meta });
48
48
  }
49
49
  return entries;
@@ -51,227 +51,227 @@ function listRegisteredSchemas() {
51
51
 
52
52
  Object.defineProperty(exports, "AddItemPayloadSchema", {
53
53
  enumerable: true,
54
- get: function () { return chunkTW4OFRWV_js.AddItemPayloadSchema; }
54
+ get: function () { return chunkD22UVSFN_js.AddItemPayloadSchema; }
55
55
  });
56
56
  Object.defineProperty(exports, "AddOnDetailsSchema", {
57
57
  enumerable: true,
58
- get: function () { return chunkTW4OFRWV_js.AddOnDetailsSchema; }
58
+ get: function () { return chunkD22UVSFN_js.AddOnDetailsSchema; }
59
59
  });
60
60
  Object.defineProperty(exports, "AddOnGroupDetailsSchema", {
61
61
  enumerable: true,
62
- get: function () { return chunkTW4OFRWV_js.AddOnGroupDetailsSchema; }
62
+ get: function () { return chunkD22UVSFN_js.AddOnGroupDetailsSchema; }
63
63
  });
64
64
  Object.defineProperty(exports, "AddOnInDetailsSchema", {
65
65
  enumerable: true,
66
- get: function () { return chunkTW4OFRWV_js.AddOnInDetailsSchema; }
66
+ get: function () { return chunkD22UVSFN_js.AddOnInDetailsSchema; }
67
67
  });
68
68
  Object.defineProperty(exports, "AddOnOptionDetailsSchema", {
69
69
  enumerable: true,
70
- get: function () { return chunkTW4OFRWV_js.AddOnOptionDetailsSchema; }
70
+ get: function () { return chunkD22UVSFN_js.AddOnOptionDetailsSchema; }
71
71
  });
72
72
  Object.defineProperty(exports, "AuthorizationTypeSchema", {
73
73
  enumerable: true,
74
- get: function () { return chunkTW4OFRWV_js.AuthorizationTypeSchema; }
74
+ get: function () { return chunkD22UVSFN_js.AuthorizationTypeSchema; }
75
75
  });
76
76
  Object.defineProperty(exports, "BrandContactSchema", {
77
77
  enumerable: true,
78
- get: function () { return chunkTW4OFRWV_js.BrandContactSchema; }
78
+ get: function () { return chunkD22UVSFN_js.BrandContactSchema; }
79
79
  });
80
80
  Object.defineProperty(exports, "BrandFaqSchema", {
81
81
  enumerable: true,
82
- get: function () { return chunkTW4OFRWV_js.BrandFaqSchema; }
82
+ get: function () { return chunkD22UVSFN_js.BrandFaqSchema; }
83
83
  });
84
84
  Object.defineProperty(exports, "BrandHeroSchema", {
85
85
  enumerable: true,
86
- get: function () { return chunkTW4OFRWV_js.BrandHeroSchema; }
86
+ get: function () { return chunkD22UVSFN_js.BrandHeroSchema; }
87
87
  });
88
88
  Object.defineProperty(exports, "BrandNavLinkSchema", {
89
89
  enumerable: true,
90
- get: function () { return chunkTW4OFRWV_js.BrandNavLinkSchema; }
90
+ get: function () { return chunkD22UVSFN_js.BrandNavLinkSchema; }
91
91
  });
92
92
  Object.defineProperty(exports, "BrandPolicyPageSchema", {
93
93
  enumerable: true,
94
- get: function () { return chunkTW4OFRWV_js.BrandPolicyPageSchema; }
94
+ get: function () { return chunkD22UVSFN_js.BrandPolicyPageSchema; }
95
95
  });
96
96
  Object.defineProperty(exports, "BrandPolicySectionSchema", {
97
97
  enumerable: true,
98
- get: function () { return chunkTW4OFRWV_js.BrandPolicySectionSchema; }
98
+ get: function () { return chunkD22UVSFN_js.BrandPolicySectionSchema; }
99
99
  });
100
100
  Object.defineProperty(exports, "BrandSchema", {
101
101
  enumerable: true,
102
- get: function () { return chunkTW4OFRWV_js.BrandSchema; }
102
+ get: function () { return chunkD22UVSFN_js.BrandSchema; }
103
103
  });
104
104
  Object.defineProperty(exports, "BrandSocialSchema", {
105
105
  enumerable: true,
106
- get: function () { return chunkTW4OFRWV_js.BrandSocialSchema; }
106
+ get: function () { return chunkD22UVSFN_js.BrandSocialSchema; }
107
107
  });
108
108
  Object.defineProperty(exports, "BundleResolvedSchema", {
109
109
  enumerable: true,
110
- get: function () { return chunkTW4OFRWV_js.BundleResolvedSchema; }
110
+ get: function () { return chunkD22UVSFN_js.BundleResolvedSchema; }
111
111
  });
112
112
  Object.defineProperty(exports, "BundleSelectionInputSchema", {
113
113
  enumerable: true,
114
- get: function () { return chunkTW4OFRWV_js.BundleSelectionInputSchema; }
114
+ get: function () { return chunkD22UVSFN_js.BundleSelectionInputSchema; }
115
115
  });
116
116
  Object.defineProperty(exports, "CartItemSchema", {
117
117
  enumerable: true,
118
- get: function () { return chunkTW4OFRWV_js.CartItemSchema; }
118
+ get: function () { return chunkD22UVSFN_js.CartItemSchema; }
119
119
  });
120
120
  Object.defineProperty(exports, "CartPricingSchema", {
121
121
  enumerable: true,
122
- get: function () { return chunkTW4OFRWV_js.CartPricingSchema; }
122
+ get: function () { return chunkD22UVSFN_js.CartPricingSchema; }
123
123
  });
124
124
  Object.defineProperty(exports, "CartSchema", {
125
125
  enumerable: true,
126
- get: function () { return chunkTW4OFRWV_js.CartSchema; }
126
+ get: function () { return chunkD22UVSFN_js.CartSchema; }
127
127
  });
128
128
  Object.defineProperty(exports, "CheckoutBodySchema", {
129
129
  enumerable: true,
130
- get: function () { return chunkTW4OFRWV_js.CheckoutBodySchema; }
130
+ get: function () { return chunkD22UVSFN_js.CheckoutBodySchema; }
131
131
  });
132
132
  Object.defineProperty(exports, "CheckoutCustomerSchema", {
133
133
  enumerable: true,
134
- get: function () { return chunkTW4OFRWV_js.CheckoutCustomerSchema; }
134
+ get: function () { return chunkD22UVSFN_js.CheckoutCustomerSchema; }
135
135
  });
136
136
  Object.defineProperty(exports, "CheckoutResponseSchema", {
137
137
  enumerable: true,
138
- get: function () { return chunkTW4OFRWV_js.CheckoutResponseSchema; }
138
+ get: function () { return chunkD22UVSFN_js.CheckoutResponseSchema; }
139
139
  });
140
140
  Object.defineProperty(exports, "ChosenPriceSchema", {
141
141
  enumerable: true,
142
- get: function () { return chunkTW4OFRWV_js.ChosenPriceSchema; }
142
+ get: function () { return chunkD22UVSFN_js.ChosenPriceSchema; }
143
143
  });
144
144
  Object.defineProperty(exports, "CompositeResolvedSchema", {
145
145
  enumerable: true,
146
- get: function () { return chunkTW4OFRWV_js.CompositeResolvedSchema; }
146
+ get: function () { return chunkD22UVSFN_js.CompositeResolvedSchema; }
147
147
  });
148
148
  Object.defineProperty(exports, "CompositeSelectionInputSchema", {
149
149
  enumerable: true,
150
- get: function () { return chunkTW4OFRWV_js.CompositeSelectionInputSchema; }
150
+ get: function () { return chunkD22UVSFN_js.CompositeSelectionInputSchema; }
151
151
  });
152
152
  Object.defineProperty(exports, "CountryCodeSchema", {
153
153
  enumerable: true,
154
- get: function () { return chunkTW4OFRWV_js.CountryCodeSchema; }
154
+ get: function () { return chunkD22UVSFN_js.CountryCodeSchema; }
155
155
  });
156
156
  Object.defineProperty(exports, "CurrencyCodeSchema", {
157
157
  enumerable: true,
158
- get: function () { return chunkTW4OFRWV_js.CurrencyCodeSchema; }
158
+ get: function () { return chunkD22UVSFN_js.CurrencyCodeSchema; }
159
159
  });
160
160
  Object.defineProperty(exports, "CustomerInputValueSchema", {
161
161
  enumerable: true,
162
- get: function () { return chunkTW4OFRWV_js.CustomerInputValueSchema; }
162
+ get: function () { return chunkD22UVSFN_js.CustomerInputValueSchema; }
163
163
  });
164
164
  Object.defineProperty(exports, "DiscountDetailsSchema", {
165
165
  enumerable: true,
166
- get: function () { return chunkTW4OFRWV_js.DiscountDetailsSchema; }
166
+ get: function () { return chunkD22UVSFN_js.DiscountDetailsSchema; }
167
167
  });
168
168
  Object.defineProperty(exports, "IsoDateTimeSchema", {
169
169
  enumerable: true,
170
- get: function () { return chunkTW4OFRWV_js.IsoDateTimeSchema; }
170
+ get: function () { return chunkD22UVSFN_js.IsoDateTimeSchema; }
171
171
  });
172
172
  Object.defineProperty(exports, "LineTypeSchema", {
173
173
  enumerable: true,
174
- get: function () { return chunkTW4OFRWV_js.LineTypeSchema; }
174
+ get: function () { return chunkD22UVSFN_js.LineTypeSchema; }
175
175
  });
176
176
  Object.defineProperty(exports, "LocaleSchema", {
177
177
  enumerable: true,
178
- get: function () { return chunkTW4OFRWV_js.LocaleSchema; }
178
+ get: function () { return chunkD22UVSFN_js.LocaleSchema; }
179
179
  });
180
180
  Object.defineProperty(exports, "MoneyLooseSchema", {
181
181
  enumerable: true,
182
- get: function () { return chunkTW4OFRWV_js.MoneyLooseSchema; }
182
+ get: function () { return chunkD22UVSFN_js.MoneyLooseSchema; }
183
183
  });
184
184
  Object.defineProperty(exports, "MoneySchema", {
185
185
  enumerable: true,
186
- get: function () { return chunkTW4OFRWV_js.MoneySchema; }
186
+ get: function () { return chunkD22UVSFN_js.MoneySchema; }
187
187
  });
188
188
  Object.defineProperty(exports, "NextActionSchema", {
189
189
  enumerable: true,
190
- get: function () { return chunkTW4OFRWV_js.NextActionSchema; }
190
+ get: function () { return chunkD22UVSFN_js.NextActionSchema; }
191
191
  });
192
192
  Object.defineProperty(exports, "PhoneE164Schema", {
193
193
  enumerable: true,
194
- get: function () { return chunkTW4OFRWV_js.PhoneE164Schema; }
194
+ get: function () { return chunkD22UVSFN_js.PhoneE164Schema; }
195
195
  });
196
196
  Object.defineProperty(exports, "ProductTypeSchema", {
197
197
  enumerable: true,
198
- get: function () { return chunkTW4OFRWV_js.ProductTypeSchema; }
198
+ get: function () { return chunkD22UVSFN_js.ProductTypeSchema; }
199
199
  });
200
200
  Object.defineProperty(exports, "SchemaViolationError", {
201
201
  enumerable: true,
202
- get: function () { return chunkTW4OFRWV_js.SchemaViolationError; }
202
+ get: function () { return chunkD22UVSFN_js.SchemaViolationError; }
203
203
  });
204
204
  Object.defineProperty(exports, "SeedNameSchema", {
205
205
  enumerable: true,
206
- get: function () { return chunkTW4OFRWV_js.SeedNameSchema; }
206
+ get: function () { return chunkD22UVSFN_js.SeedNameSchema; }
207
207
  });
208
208
  Object.defineProperty(exports, "SelectedAddOnOptionSchema", {
209
209
  enumerable: true,
210
- get: function () { return chunkTW4OFRWV_js.SelectedAddOnOptionSchema; }
210
+ get: function () { return chunkD22UVSFN_js.SelectedAddOnOptionSchema; }
211
211
  });
212
212
  Object.defineProperty(exports, "ServiceStatusSchema", {
213
213
  enumerable: true,
214
- get: function () { return chunkTW4OFRWV_js.ServiceStatusSchema; }
214
+ get: function () { return chunkD22UVSFN_js.ServiceStatusSchema; }
215
215
  });
216
216
  Object.defineProperty(exports, "TimestampSchema", {
217
217
  enumerable: true,
218
- get: function () { return chunkTW4OFRWV_js.TimestampSchema; }
218
+ get: function () { return chunkD22UVSFN_js.TimestampSchema; }
219
219
  });
220
220
  Object.defineProperty(exports, "VariantDetailsSchema", {
221
221
  enumerable: true,
222
- get: function () { return chunkTW4OFRWV_js.VariantDetailsSchema; }
222
+ get: function () { return chunkD22UVSFN_js.VariantDetailsSchema; }
223
223
  });
224
224
  Object.defineProperty(exports, "VariantDisplayAttributeSchema", {
225
225
  enumerable: true,
226
- get: function () { return chunkTW4OFRWV_js.VariantDisplayAttributeSchema; }
226
+ get: function () { return chunkD22UVSFN_js.VariantDisplayAttributeSchema; }
227
227
  });
228
228
  Object.defineProperty(exports, "VariantInfoSchema", {
229
229
  enumerable: true,
230
- get: function () { return chunkTW4OFRWV_js.VariantInfoSchema; }
230
+ get: function () { return chunkD22UVSFN_js.VariantInfoSchema; }
231
231
  });
232
232
  Object.defineProperty(exports, "assertAddItemPayload", {
233
233
  enumerable: true,
234
- get: function () { return chunkTW4OFRWV_js.assertAddItemPayload; }
234
+ get: function () { return chunkD22UVSFN_js.assertAddItemPayload; }
235
235
  });
236
236
  Object.defineProperty(exports, "assertBrand", {
237
237
  enumerable: true,
238
- get: function () { return chunkTW4OFRWV_js.assertBrand; }
238
+ get: function () { return chunkD22UVSFN_js.assertBrand; }
239
239
  });
240
240
  Object.defineProperty(exports, "assertCart", {
241
241
  enumerable: true,
242
- get: function () { return chunkTW4OFRWV_js.assertCart; }
242
+ get: function () { return chunkD22UVSFN_js.assertCart; }
243
243
  });
244
244
  Object.defineProperty(exports, "assertCartItem", {
245
245
  enumerable: true,
246
- get: function () { return chunkTW4OFRWV_js.assertCartItem; }
246
+ get: function () { return chunkD22UVSFN_js.assertCartItem; }
247
247
  });
248
248
  Object.defineProperty(exports, "assertCheckoutBody", {
249
249
  enumerable: true,
250
- get: function () { return chunkTW4OFRWV_js.assertCheckoutBody; }
250
+ get: function () { return chunkD22UVSFN_js.assertCheckoutBody; }
251
251
  });
252
252
  Object.defineProperty(exports, "assertCheckoutResponse", {
253
253
  enumerable: true,
254
- get: function () { return chunkTW4OFRWV_js.assertCheckoutResponse; }
254
+ get: function () { return chunkD22UVSFN_js.assertCheckoutResponse; }
255
255
  });
256
256
  Object.defineProperty(exports, "cimplifyRegistry", {
257
257
  enumerable: true,
258
- get: function () { return chunkTW4OFRWV_js.cimplifyRegistry; }
258
+ get: function () { return chunkD22UVSFN_js.cimplifyRegistry; }
259
259
  });
260
260
  Object.defineProperty(exports, "createTestClient", {
261
261
  enumerable: true,
262
- get: function () { return chunkTW4OFRWV_js.createTestClient; }
262
+ get: function () { return chunkD22UVSFN_js.createTestClient; }
263
263
  });
264
264
  Object.defineProperty(exports, "fixtures", {
265
265
  enumerable: true,
266
- get: function () { return chunkTW4OFRWV_js.fixtures; }
266
+ get: function () { return chunkD22UVSFN_js.fixtures; }
267
267
  });
268
268
  Object.defineProperty(exports, "makeAssertion", {
269
269
  enumerable: true,
270
- get: function () { return chunkTW4OFRWV_js.makeAssertion; }
270
+ get: function () { return chunkD22UVSFN_js.makeAssertion; }
271
271
  });
272
272
  Object.defineProperty(exports, "registerSchema", {
273
273
  enumerable: true,
274
- get: function () { return chunkTW4OFRWV_js.registerSchema; }
274
+ get: function () { return chunkD22UVSFN_js.registerSchema; }
275
275
  });
276
276
  exports.exportJsonSchemas = exportJsonSchemas;
277
277
  exports.listRegisteredSchemas = listRegisteredSchemas;
package/dist/testing.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { VariantInfoSchema, CheckoutResponseSchema, CheckoutBodySchema, CartPricingSchema, CartItemSchema, CartSchema, AddItemPayloadSchema, BrandSchema, cimplifyRegistry } from './chunk-6HYKWYUF.mjs';
2
- export { AddItemPayloadSchema, AddOnDetailsSchema, AddOnGroupDetailsSchema, AddOnInDetailsSchema, AddOnOptionDetailsSchema, AuthorizationTypeSchema, BrandContactSchema, BrandFaqSchema, BrandHeroSchema, BrandNavLinkSchema, BrandPolicyPageSchema, BrandPolicySectionSchema, BrandSchema, BrandSocialSchema, BundleResolvedSchema, BundleSelectionInputSchema, CartItemSchema, CartPricingSchema, CartSchema, CheckoutBodySchema, CheckoutCustomerSchema, CheckoutResponseSchema, ChosenPriceSchema, CompositeResolvedSchema, CompositeSelectionInputSchema, CountryCodeSchema, CurrencyCodeSchema, CustomerInputValueSchema, DiscountDetailsSchema, IsoDateTimeSchema, LineTypeSchema, LocaleSchema, MoneyLooseSchema, MoneySchema, NextActionSchema, PhoneE164Schema, ProductTypeSchema, SchemaViolationError, SeedNameSchema, SelectedAddOnOptionSchema, ServiceStatusSchema, TimestampSchema, VariantDetailsSchema, VariantDisplayAttributeSchema, VariantInfoSchema, assertAddItemPayload, assertBrand, assertCart, assertCartItem, assertCheckoutBody, assertCheckoutResponse, cimplifyRegistry, createTestClient, fixtures, makeAssertion, registerSchema } from './chunk-6HYKWYUF.mjs';
3
- import './chunk-WUIERJ6J.mjs';
4
- import './chunk-CKRMA5F7.mjs';
1
+ import { VariantInfoSchema, CheckoutResponseSchema, CheckoutBodySchema, CartPricingSchema, CartItemSchema, CartSchema, AddItemPayloadSchema, BrandSchema, cimplifyRegistry } from './chunk-24FK7VFL.mjs';
2
+ export { AddItemPayloadSchema, AddOnDetailsSchema, AddOnGroupDetailsSchema, AddOnInDetailsSchema, AddOnOptionDetailsSchema, AuthorizationTypeSchema, BrandContactSchema, BrandFaqSchema, BrandHeroSchema, BrandNavLinkSchema, BrandPolicyPageSchema, BrandPolicySectionSchema, BrandSchema, BrandSocialSchema, BundleResolvedSchema, BundleSelectionInputSchema, CartItemSchema, CartPricingSchema, CartSchema, CheckoutBodySchema, CheckoutCustomerSchema, CheckoutResponseSchema, ChosenPriceSchema, CompositeResolvedSchema, CompositeSelectionInputSchema, CountryCodeSchema, CurrencyCodeSchema, CustomerInputValueSchema, DiscountDetailsSchema, IsoDateTimeSchema, LineTypeSchema, LocaleSchema, MoneyLooseSchema, MoneySchema, NextActionSchema, PhoneE164Schema, ProductTypeSchema, SchemaViolationError, SeedNameSchema, SelectedAddOnOptionSchema, ServiceStatusSchema, TimestampSchema, VariantDetailsSchema, VariantDisplayAttributeSchema, VariantInfoSchema, assertAddItemPayload, assertBrand, assertCart, assertCartItem, assertCheckoutBody, assertCheckoutResponse, cimplifyRegistry, createTestClient, fixtures, makeAssertion, registerSchema } from './chunk-24FK7VFL.mjs';
3
+ import './chunk-MBR2DBEN.mjs';
4
+ import './chunk-OFNVLUH4.mjs';
5
5
  import './chunk-XY2DFX5K.mjs';
6
6
  import './chunk-632JEJUS.mjs';
7
7
  import './chunk-Z2AYLZDF.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/sdk",
3
- "version": "0.46.3",
3
+ "version": "0.48.0",
4
4
  "description": "Cimplify Commerce SDK for storefronts",
5
5
  "keywords": [
6
6
  "cimplify",
@@ -138,7 +138,9 @@
138
138
  "dependencies": {
139
139
  "@base-ui/react": "^1.4.1",
140
140
  "clsx": "^2.1.1",
141
+ "date-fns": "^4.1.0",
141
142
  "libphonenumber-js": "1.12.41",
143
+ "react-day-picker": "^9.14.0",
142
144
  "tailwind-merge": "^3.5.0",
143
145
  "zod": "^4.4.3"
144
146
  }
@@ -9,7 +9,7 @@
9
9
  "files": [
10
10
  {
11
11
  "path": "customer-input-fields.tsx",
12
- "content": "\"use client\";\n\nimport React, { useCallback } from \"react\";\nimport { INPUT_FIELD_TYPE, type AddressValue, type DateRangeValue, type LocationValue, type PhoneValue, type ProductInputField, type SignatureValue } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { useOptionalCimplifyClient } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface CustomerInputFieldsClassNames {\n root?: string;\n field?: string;\n label?: string;\n required?: string;\n helpText?: string;\n input?: string;\n select?: string;\n textarea?: string;\n fileInput?: string;\n colorInput?: string;\n radioGroup?: string;\n radioOption?: string;\n checkboxLabel?: string;\n priceAdjustment?: string;\n addressInput?: string;\n phoneInput?: string;\n signatureCanvas?: string;\n multiSelectGroup?: string;\n multiSelectOption?: string;\n dateRangeInput?: string;\n locationInput?: string;\n}\n\nexport interface CustomerInputFieldsProps {\n fields: ProductInputField[];\n values: Record<string, unknown>;\n onChange: (values: Record<string, unknown>) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n className?: string;\n classNames?: CustomerInputFieldsClassNames;\n}\n\nexport function CustomerInputFields({\n fields,\n values,\n onChange,\n onFileUpload,\n className,\n classNames,\n}: CustomerInputFieldsProps): React.ReactElement | null {\n const clientContext = useOptionalCimplifyClient();\n\n if (fields.length === 0) return null;\n\n const sorted = [...fields].sort((a, b) => a.display_order - b.display_order);\n\n const setValue = useCallback(\n (fieldId: string, value: unknown) => {\n onChange({ ...values, [fieldId]: value });\n },\n [values, onChange],\n );\n\n const defaultFileUpload = useCallback(\n async (file: File): Promise<string> => {\n if (onFileUpload) return onFileUpload(file, {} as ProductInputField);\n if (!clientContext?.client) throw new Error(\"No upload provider available\");\n const result = await clientContext.client.uploads.upload(file);\n if (!result.ok) throw new Error(result.error.message);\n return result.value.url;\n },\n [onFileUpload, clientContext],\n );\n\n return (\n <div data-cimplify-customer-inputs className={cn(\"space-y-4\", className, classNames?.root)}>\n {sorted.map((field) => (\n <InputField\n key={field.id}\n field={field}\n value={values[field.id]}\n onValueChange={(value) => setValue(field.id, value)}\n onFileUpload={onFileUpload ?? (clientContext?.client ? (_file, _field) => defaultFileUpload(_file) : undefined)}\n classNames={classNames}\n />\n ))}\n </div>\n );\n}\n\nfunction InputField({\n field,\n value,\n onValueChange,\n onFileUpload,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const hasPriceAdjustment = field.price_adjustment != null && parsePrice(field.price_adjustment) > 0;\n\n return (\n <div data-cimplify-customer-input data-field-type={field.field_type} className={classNames?.field}>\n <div className=\"flex items-center gap-2 mb-1.5\">\n <label className={cn(\"text-sm font-semibold\", classNames?.label)}>\n {field.name}\n </label>\n {field.is_required && (\n <span className={cn(\"text-xs text-destructive\", classNames?.required)}>Required</span>\n )}\n {hasPriceAdjustment && (\n <span className={cn(\"text-xs text-muted-foreground\", classNames?.priceAdjustment)}>\n +<Price amount={field.price_adjustment!} />\n </span>\n )}\n </div>\n\n {field.help_text && (\n <p className={cn(\"text-xs text-muted-foreground mb-1.5\", classNames?.helpText)}>\n {field.help_text}\n </p>\n )}\n\n <FieldInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n onFileUpload={onFileUpload}\n classNames={classNames}\n />\n </div>\n );\n}\n\nfunction FieldInput({\n field,\n value,\n onValueChange,\n onFileUpload,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const inputClass = cn(\n \"w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring\",\n classNames?.input,\n );\n\n switch (field.field_type) {\n case INPUT_FIELD_TYPE.Text:\n case INPUT_FIELD_TYPE.Url:\n return (\n <input\n type={field.field_type === INPUT_FIELD_TYPE.Url ? \"url\" : \"text\"}\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder}\n required={field.is_required}\n maxLength={field.validation?.max_length ?? undefined}\n className={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Textarea:\n return (\n <textarea\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder}\n required={field.is_required}\n maxLength={field.validation?.max_length ?? undefined}\n rows={3}\n className={cn(inputClass, \"resize-none\", classNames?.textarea)}\n />\n );\n\n case INPUT_FIELD_TYPE.Number:\n return (\n <input\n type=\"number\"\n value={typeof value === \"number\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value ? parseFloat(e.target.value) : undefined)}\n placeholder={field.placeholder}\n required={field.is_required}\n min={field.validation?.min_value ?? undefined}\n max={field.validation?.max_value ?? undefined}\n className={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Select:\n return (\n <select\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value || undefined)}\n required={field.is_required}\n className={cn(inputClass, classNames?.select)}\n >\n <option value=\"\">{field.placeholder || \"Select...\"}</option>\n {(field.options || []).map((opt) => (\n <option key={opt} value={opt}>{opt}</option>\n ))}\n </select>\n );\n\n case INPUT_FIELD_TYPE.Radio:\n return (\n <div className={cn(\"space-y-2\", classNames?.radioGroup)}>\n {(field.options || []).map((opt) => (\n <label\n key={opt}\n className={cn(\n \"flex items-center gap-2 text-sm cursor-pointer\",\n classNames?.radioOption,\n )}\n >\n <input\n type=\"radio\"\n name={field.id}\n value={opt}\n checked={value === opt}\n onChange={() => onValueChange(opt)}\n className=\"accent-primary\"\n />\n {opt}\n </label>\n ))}\n </div>\n );\n\n case INPUT_FIELD_TYPE.Checkbox:\n return (\n <label className={cn(\"flex items-center gap-2 text-sm cursor-pointer\", classNames?.checkboxLabel)}>\n <input\n type=\"checkbox\"\n checked={value === true}\n onChange={(e) => onValueChange(e.target.checked)}\n className=\"accent-primary w-4 h-4\"\n />\n {field.placeholder || field.name}\n </label>\n );\n\n case INPUT_FIELD_TYPE.Color:\n return (\n <input\n type=\"color\"\n value={typeof value === \"string\" ? value : \"#000000\"}\n onChange={(e) => onValueChange(e.target.value)}\n className={cn(\"w-12 h-10 rounded-md border border-input cursor-pointer\", classNames?.colorInput)}\n />\n );\n\n case INPUT_FIELD_TYPE.Date:\n return (\n <input\n type=\"date\"\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value || undefined)}\n required={field.is_required}\n className={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.File:\n case INPUT_FIELD_TYPE.Image:\n return (\n <FileUploadInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n onFileUpload={onFileUpload}\n classNames={classNames}\n />\n );\n\n case INPUT_FIELD_TYPE.Email:\n return (\n <input\n type=\"email\"\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder || \"email@example.com\"}\n required={field.is_required}\n className={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.DateTime: {\n const dtLocalValue = typeof value === \"string\" && value.includes(\"T\")\n ? value.slice(0, 16)\n : typeof value === \"string\" ? value : \"\";\n return (\n <input\n type=\"datetime-local\"\n value={dtLocalValue}\n onChange={(e) => {\n if (!e.target.value) { onValueChange(undefined); return; }\n const date = new Date(e.target.value);\n onValueChange(Number.isNaN(date.getTime()) ? e.target.value : date.toISOString());\n }}\n required={field.is_required}\n className={inputClass}\n />\n );\n }\n\n case INPUT_FIELD_TYPE.Time:\n return (\n <input\n type=\"time\"\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value || undefined)}\n required={field.is_required}\n className={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.MultiSelect:\n return (\n <MultiSelectInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n />\n );\n\n case INPUT_FIELD_TYPE.DateRange:\n return (\n <DateRangeInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n inputClass={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Address:\n return (\n <AddressInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n inputClass={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Location:\n return (\n <LocationInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n inputClass={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Phone:\n return (\n <PhoneInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n inputClass={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Signature:\n return (\n <SignatureInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n />\n );\n\n default:\n return (\n <input\n type=\"text\"\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder}\n className={inputClass}\n />\n );\n }\n}\n\nfunction FileUploadInput({\n field,\n value,\n onValueChange,\n onFileUpload,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const [isUploading, setIsUploading] = React.useState(false);\n const fileUrl = typeof value === \"string\" ? value : undefined;\n const acceptedFormats = field.validation?.accepted_formats;\n const accept = acceptedFormats\n ? acceptedFormats.map((f) => `.${f}`).join(\",\")\n : field.field_type === INPUT_FIELD_TYPE.Image\n ? \"image/*\"\n : undefined;\n\n const handleFile = async (file: File) => {\n if (!onFileUpload) return;\n\n if (field.validation?.max_size_mb) {\n const maxBytes = field.validation.max_size_mb * 1024 * 1024;\n if (file.size > maxBytes) {\n return;\n }\n }\n\n setIsUploading(true);\n try {\n const url = await onFileUpload(file, field);\n onValueChange(url);\n } finally {\n setIsUploading(false);\n }\n };\n\n return (\n <div className={cn(\"space-y-2\", classNames?.fileInput)}>\n {fileUrl ? (\n <div className=\"flex items-center gap-3\">\n {field.field_type === INPUT_FIELD_TYPE.Image && (\n <img src={fileUrl} alt=\"Uploaded\" className=\"w-16 h-16 object-cover rounded-md border border-border\" />\n )}\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm text-foreground truncate\">{fileUrl.split(\"/\").pop()}</p>\n </div>\n <button\n type=\"button\"\n onClick={() => onValueChange(undefined)}\n className=\"text-xs text-muted-foreground hover:text-destructive transition-colors\"\n >\n Remove\n </button>\n </div>\n ) : (\n <label className=\"flex flex-col items-center justify-center w-full h-24 border-2 border-dashed border-border rounded-lg cursor-pointer hover:border-primary/40 transition-colors\">\n <input\n type=\"file\"\n accept={accept}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleFile(file);\n }}\n className=\"hidden\"\n disabled={isUploading}\n />\n {isUploading ? (\n <span className=\"text-sm text-muted-foreground\">Uploading...</span>\n ) : (\n <>\n <span className=\"text-sm text-muted-foreground\">\n {field.placeholder || `Upload ${field.field_type === INPUT_FIELD_TYPE.Image ? \"image\" : \"file\"}`}\n </span>\n {acceptedFormats && (\n <span className=\"text-xs text-muted-foreground/60 mt-1\">\n {acceptedFormats.map((f) => f.toUpperCase()).join(\", \")}\n {field.validation?.max_size_mb && ` · Max ${field.validation.max_size_mb}MB`}\n </span>\n )}\n </>\n )}\n </label>\n )}\n </div>\n );\n}\n\nfunction MultiSelectInput({\n field,\n value,\n onValueChange,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const selected = Array.isArray(value) ? (value as string[]) : [];\n const options = field.options || [];\n const maxSelections = field.validation?.max_selections;\n\n const toggle = (opt: string) => {\n const next = selected.includes(opt)\n ? selected.filter((s) => s !== opt)\n : maxSelections && selected.length >= maxSelections\n ? selected\n : [...selected, opt];\n onValueChange(next.length > 0 ? next : undefined);\n };\n\n return (\n <div className={cn(\"space-y-2\", classNames?.multiSelectGroup)}>\n {options.map((opt) => (\n <label\n key={opt}\n className={cn(\n \"flex items-center gap-2 text-sm cursor-pointer\",\n classNames?.multiSelectOption,\n )}\n >\n <input\n type=\"checkbox\"\n checked={selected.includes(opt)}\n onChange={() => toggle(opt)}\n className=\"accent-primary w-4 h-4\"\n />\n {opt}\n </label>\n ))}\n {maxSelections && (\n <p className=\"text-xs text-muted-foreground\">\n {selected.length}/{maxSelections} selected\n </p>\n )}\n </div>\n );\n}\n\nfunction DateRangeInput({\n field,\n value,\n onValueChange,\n classNames,\n inputClass,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n inputClass: string;\n}): React.ReactElement {\n const range = (value && typeof value === \"object\" ? value : {}) as DateRangeValue;\n\n const update = (key: \"start\" | \"end\", v: string) => {\n const next = { ...range, [key]: v };\n onValueChange(next.start || next.end ? next : undefined);\n };\n\n return (\n <div className={cn(\"grid grid-cols-2 gap-3\", classNames?.dateRangeInput)}>\n <div>\n <label className=\"text-xs text-muted-foreground mb-1 block\">Start</label>\n <input\n type=\"date\"\n value={range.start || \"\"}\n onChange={(e) => update(\"start\", e.target.value)}\n required={field.is_required}\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"text-xs text-muted-foreground mb-1 block\">End</label>\n <input\n type=\"date\"\n value={range.end || \"\"}\n onChange={(e) => update(\"end\", e.target.value)}\n required={field.is_required}\n className={inputClass}\n />\n </div>\n </div>\n );\n}\n\nfunction AddressInput({\n field,\n value,\n onValueChange,\n classNames,\n inputClass,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n inputClass: string;\n}): React.ReactElement {\n const clientContext = useOptionalCimplifyClient();\n const addr = (value && typeof value === \"object\" ? value : null) as AddressValue | null;\n const [inputText, setInputText] = React.useState(addr?.formatted_address ?? \"\");\n const [suggestions, setSuggestions] = React.useState<Array<{ description: string; place_id: string }>>([]);\n const [isOpen, setIsOpen] = React.useState(false);\n const [highlightedIndex, setHighlightedIndex] = React.useState(-1);\n const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n const seqRef = React.useRef(0);\n const sessionTokenRef = React.useRef(createSessionToken());\n const blurTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n React.useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n seqRef.current += 1;\n };\n }, []);\n\n const fetchSuggestions = React.useCallback(async (query: string) => {\n const client = clientContext?.client;\n if (!client || query.length < 3) {\n setSuggestions([]);\n setIsOpen(false);\n return;\n }\n\n const seq = ++seqRef.current;\n const result = await client.places.autocomplete(query, sessionTokenRef.current);\n if (seqRef.current !== seq) return;\n\n if (result.ok && result.value.predictions.length > 0) {\n setSuggestions(result.value.predictions);\n setHighlightedIndex(0);\n setIsOpen(true);\n } else {\n setSuggestions([]);\n setIsOpen(false);\n }\n }, [clientContext]);\n\n const handleInputChange = (nextValue: string) => {\n setInputText(nextValue);\n if (debounceRef.current) clearTimeout(debounceRef.current);\n debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);\n };\n\n const selectSuggestion = React.useCallback(async (suggestion: { description: string; place_id: string }) => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n\n const fallback = suggestion.description.split(\",\")[0]?.trim() || suggestion.description;\n setInputText(fallback);\n setSuggestions([]);\n setIsOpen(false);\n\n const client = clientContext?.client;\n if (!client) return;\n\n const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);\n sessionTokenRef.current = createSessionToken();\n\n if (result.ok) {\n const d = result.value;\n const addressValue: AddressValue = {\n formatted_address: d.formatted_address || fallback,\n street_address: d.street_address,\n apartment: d.apartment,\n city: d.city,\n region: d.region,\n postal_code: d.postal_code,\n country: d.country,\n latitude: d.latitude,\n longitude: d.longitude,\n place_id: d.place_id,\n };\n setInputText(addressValue.formatted_address);\n onValueChange(addressValue);\n }\n }, [clientContext, onValueChange]);\n\n return (\n <div className={cn(\"space-y-2 relative\", classNames?.addressInput)}>\n <input\n type=\"text\"\n value={inputText}\n onChange={(e) => handleInputChange(e.target.value)}\n onFocus={() => { if (suggestions.length > 0) setIsOpen(true); }}\n onBlur={() => { blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120); }}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") { setIsOpen(false); return; }\n if (!isOpen || suggestions.length === 0) return;\n if (e.key === \"ArrowDown\") { e.preventDefault(); setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0); }\n else if (e.key === \"ArrowUp\") { e.preventDefault(); setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1); }\n else if (e.key === \"Enter\" && highlightedIndex >= 0) { e.preventDefault(); void selectSuggestion(suggestions[highlightedIndex]); }\n }}\n placeholder={field.placeholder || \"Search for an address...\"}\n required={field.is_required}\n autoComplete=\"off\"\n aria-autocomplete=\"list\"\n aria-expanded={isOpen && suggestions.length > 0}\n className={inputClass}\n />\n {isOpen && suggestions.length > 0 && (\n <div role=\"listbox\" className=\"absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg\">\n {suggestions.map((s, i) => (\n <button\n key={s.place_id}\n type=\"button\"\n role=\"option\"\n aria-selected={i === highlightedIndex}\n onMouseDown={(e) => { e.preventDefault(); void selectSuggestion(s); }}\n className={cn(\"block w-full px-3 py-2.5 text-left text-sm transition-colors\", i === highlightedIndex ? \"bg-muted text-foreground\" : \"text-foreground hover:bg-muted/70\")}\n >\n {s.description}\n </button>\n ))}\n </div>\n )}\n {addr && (\n <input\n type=\"text\"\n value={addr.apartment || \"\"}\n onChange={(e) => onValueChange({ ...addr, apartment: e.target.value || undefined })}\n placeholder=\"Apt, suite, unit (optional)\"\n className={inputClass}\n />\n )}\n </div>\n );\n}\n\nfunction LocationInput({\n field,\n value,\n onValueChange,\n classNames,\n inputClass,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n inputClass: string;\n}): React.ReactElement {\n const clientContext = useOptionalCimplifyClient();\n const loc = (value && typeof value === \"object\" ? value : null) as LocationValue | null;\n const [inputText, setInputText] = React.useState(loc?.label ?? \"\");\n const [suggestions, setSuggestions] = React.useState<Array<{ description: string; place_id: string }>>([]);\n const [isOpen, setIsOpen] = React.useState(false);\n const [highlightedIndex, setHighlightedIndex] = React.useState(-1);\n const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n const seqRef = React.useRef(0);\n const sessionTokenRef = React.useRef(createSessionToken());\n const blurTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n React.useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n seqRef.current += 1;\n };\n }, []);\n\n const fetchSuggestions = React.useCallback(async (query: string) => {\n const client = clientContext?.client;\n if (!client || query.length < 3) {\n setSuggestions([]);\n setIsOpen(false);\n return;\n }\n\n const seq = ++seqRef.current;\n const result = await client.places.autocomplete(query, sessionTokenRef.current);\n if (seqRef.current !== seq) return;\n\n if (result.ok && result.value.predictions.length > 0) {\n setSuggestions(result.value.predictions);\n setHighlightedIndex(0);\n setIsOpen(true);\n } else {\n setSuggestions([]);\n setIsOpen(false);\n }\n }, [clientContext]);\n\n const handleInputChange = (nextValue: string) => {\n setInputText(nextValue);\n if (debounceRef.current) clearTimeout(debounceRef.current);\n debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);\n };\n\n const selectSuggestion = React.useCallback(async (suggestion: { description: string; place_id: string }) => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n\n const label = suggestion.description.split(\",\")[0]?.trim() || suggestion.description;\n setInputText(label);\n setSuggestions([]);\n setIsOpen(false);\n\n const client = clientContext?.client;\n if (!client) return;\n\n const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);\n sessionTokenRef.current = createSessionToken();\n\n if (result.ok) {\n const d = result.value;\n onValueChange({\n latitude: d.latitude,\n longitude: d.longitude,\n label: d.formatted_address || label,\n } satisfies LocationValue);\n setInputText(d.formatted_address || label);\n }\n }, [clientContext, onValueChange]);\n\n return (\n <div className={cn(\"relative\", classNames?.locationInput)}>\n <input\n type=\"text\"\n value={inputText}\n onChange={(e) => handleInputChange(e.target.value)}\n onFocus={() => { if (suggestions.length > 0) setIsOpen(true); }}\n onBlur={() => { blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120); }}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") { setIsOpen(false); return; }\n if (!isOpen || suggestions.length === 0) return;\n if (e.key === \"ArrowDown\") { e.preventDefault(); setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0); }\n else if (e.key === \"ArrowUp\") { e.preventDefault(); setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1); }\n else if (e.key === \"Enter\" && highlightedIndex >= 0) { e.preventDefault(); void selectSuggestion(suggestions[highlightedIndex]); }\n }}\n placeholder={field.placeholder || \"Search for a location...\"}\n required={field.is_required}\n autoComplete=\"off\"\n aria-autocomplete=\"list\"\n aria-expanded={isOpen && suggestions.length > 0}\n className={inputClass}\n />\n {isOpen && suggestions.length > 0 && (\n <div role=\"listbox\" className=\"absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg\">\n {suggestions.map((s, i) => (\n <button\n key={s.place_id}\n type=\"button\"\n role=\"option\"\n aria-selected={i === highlightedIndex}\n onMouseDown={(e) => { e.preventDefault(); void selectSuggestion(s); }}\n className={cn(\"block w-full px-3 py-2.5 text-left text-sm transition-colors\", i === highlightedIndex ? \"bg-muted text-foreground\" : \"text-foreground hover:bg-muted/70\")}\n >\n {s.description}\n </button>\n ))}\n </div>\n )}\n {loc && (\n <p className=\"text-xs text-muted-foreground mt-1\">\n {loc.latitude.toFixed(4)}, {loc.longitude.toFixed(4)}\n </p>\n )}\n </div>\n );\n}\n\nfunction PhoneInput({\n field,\n value,\n onValueChange,\n classNames,\n inputClass,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n inputClass: string;\n}): React.ReactElement {\n const phone = (value && typeof value === \"object\" ? value : null) as PhoneValue | null;\n const [code, setCode] = React.useState(phone?.country_code ?? \"+1\");\n const [number, setNumber] = React.useState(phone?.number ?? \"\");\n\n const update = (nextCode: string, nextNumber: string) => {\n setCode(nextCode);\n setNumber(nextNumber);\n if (nextNumber) {\n onValueChange({\n country_code: nextCode,\n number: nextNumber,\n formatted: `${nextCode} ${nextNumber}`,\n } satisfies PhoneValue);\n } else {\n onValueChange(undefined);\n }\n };\n\n return (\n <div className={cn(\"flex gap-2\", classNames?.phoneInput)}>\n <input\n type=\"text\"\n value={code}\n onChange={(e) => update(e.target.value, number)}\n placeholder=\"+1\"\n className={cn(inputClass, \"w-20 shrink-0\")}\n />\n <input\n type=\"tel\"\n value={number}\n onChange={(e) => update(code, e.target.value)}\n placeholder={field.placeholder || \"Phone number\"}\n required={field.is_required}\n className={inputClass}\n />\n </div>\n );\n}\n\nfunction SignatureInput({\n field,\n value,\n onValueChange,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const sig = (value && typeof value === \"object\" ? value : null) as SignatureValue | null;\n const canvasRef = React.useRef<HTMLCanvasElement>(null);\n const isDrawing = React.useRef(false);\n\n const getCtx = () => canvasRef.current?.getContext(\"2d\") ?? null;\n\n const startDraw = (e: React.PointerEvent) => {\n const ctx = getCtx();\n if (!ctx) return;\n isDrawing.current = true;\n const rect = canvasRef.current!.getBoundingClientRect();\n ctx.beginPath();\n ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);\n canvasRef.current!.setPointerCapture(e.pointerId);\n };\n\n const draw = (e: React.PointerEvent) => {\n if (!isDrawing.current) return;\n const ctx = getCtx();\n if (!ctx) return;\n const rect = canvasRef.current!.getBoundingClientRect();\n ctx.lineWidth = 2;\n ctx.lineCap = \"round\";\n const style = getComputedStyle(canvasRef.current!);\n ctx.strokeStyle = style.color || \"#000\";\n ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);\n ctx.stroke();\n };\n\n const endDraw = () => {\n if (!isDrawing.current) return;\n isDrawing.current = false;\n const canvas = canvasRef.current;\n if (!canvas) return;\n onValueChange({\n data_url: canvas.toDataURL(\"image/png\"),\n signer_name: sig?.signer_name,\n } satisfies SignatureValue);\n };\n\n const clear = () => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const ctx = canvas.getContext(\"2d\");\n if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);\n onValueChange(undefined);\n };\n\n return (\n <div className={cn(\"space-y-2\", classNames?.signatureCanvas)}>\n <div className=\"relative rounded-md border border-input overflow-hidden\">\n <canvas\n ref={canvasRef}\n width={400}\n height={150}\n className=\"w-full touch-none cursor-crosshair bg-background text-foreground\"\n onPointerDown={startDraw}\n onPointerMove={draw}\n onPointerUp={endDraw}\n onPointerLeave={endDraw}\n />\n {sig?.data_url && (\n <button\n type=\"button\"\n onClick={clear}\n className=\"absolute top-1.5 right-1.5 text-xs text-muted-foreground hover:text-destructive transition-colors\"\n >\n Clear\n </button>\n )}\n </div>\n {!sig?.data_url && (\n <p className=\"text-xs text-muted-foreground\">\n {field.placeholder || \"Draw your signature above\"}\n </p>\n )}\n </div>\n );\n}\n\nfunction createSessionToken(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `places_${Date.now()}_${Math.random().toString(16).slice(2)}`;\n}\n"
12
+ "content": "\"use client\";\n\nimport React, { useCallback } from \"react\";\nimport { INPUT_FIELD_TYPE, type AddressValue, type DateRangeValue, type LocationValue, type PhoneValue, type ProductInputField, type SignatureValue } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { useOptionalCimplifyClient } from \"@cimplify/sdk/react\";\nimport { DatePicker } from \"./date-picker\";\nimport { TimePicker } from \"./time-picker\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface CustomerInputFieldsClassNames {\n root?: string;\n field?: string;\n label?: string;\n required?: string;\n helpText?: string;\n input?: string;\n select?: string;\n textarea?: string;\n fileInput?: string;\n colorInput?: string;\n radioGroup?: string;\n radioOption?: string;\n checkboxLabel?: string;\n priceAdjustment?: string;\n addressInput?: string;\n phoneInput?: string;\n signatureCanvas?: string;\n multiSelectGroup?: string;\n multiSelectOption?: string;\n dateRangeInput?: string;\n locationInput?: string;\n}\n\nexport interface CustomerInputFieldsProps {\n fields: ProductInputField[];\n values: Record<string, unknown>;\n onChange: (values: Record<string, unknown>) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n className?: string;\n classNames?: CustomerInputFieldsClassNames;\n}\n\nexport function CustomerInputFields({\n fields,\n values,\n onChange,\n onFileUpload,\n className,\n classNames,\n}: CustomerInputFieldsProps): React.ReactElement | null {\n const clientContext = useOptionalCimplifyClient();\n\n if (fields.length === 0) return null;\n\n const sorted = [...fields].sort((a, b) => a.display_order - b.display_order);\n\n const setValue = useCallback(\n (fieldId: string, value: unknown) => {\n onChange({ ...values, [fieldId]: value });\n },\n [values, onChange],\n );\n\n const defaultFileUpload = useCallback(\n async (file: File): Promise<string> => {\n if (onFileUpload) return onFileUpload(file, {} as ProductInputField);\n if (!clientContext?.client) throw new Error(\"No upload provider available\");\n const result = await clientContext.client.uploads.upload(file);\n if (!result.ok) throw new Error(result.error.message);\n return result.value.url;\n },\n [onFileUpload, clientContext],\n );\n\n return (\n <div data-cimplify-customer-inputs className={cn(\"space-y-4\", className, classNames?.root)}>\n {sorted.map((field) => (\n <InputField\n key={field.id}\n field={field}\n value={values[field.id]}\n onValueChange={(value) => setValue(field.id, value)}\n onFileUpload={onFileUpload ?? (clientContext?.client ? (_file, _field) => defaultFileUpload(_file) : undefined)}\n classNames={classNames}\n />\n ))}\n </div>\n );\n}\n\nfunction InputField({\n field,\n value,\n onValueChange,\n onFileUpload,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const hasPriceAdjustment = field.price_adjustment != null && parsePrice(field.price_adjustment) > 0;\n\n return (\n <div data-cimplify-customer-input data-field-type={field.field_type} className={classNames?.field}>\n <div className=\"flex items-center gap-2 mb-1.5\">\n <label className={cn(\"text-sm font-semibold\", classNames?.label)}>\n {field.name}\n </label>\n {field.is_required && (\n <span className={cn(\"text-xs text-destructive\", classNames?.required)}>Required</span>\n )}\n {hasPriceAdjustment && (\n <span className={cn(\"text-xs text-muted-foreground\", classNames?.priceAdjustment)}>\n +<Price amount={field.price_adjustment!} />\n </span>\n )}\n </div>\n\n {field.help_text && (\n <p className={cn(\"text-xs text-muted-foreground mb-1.5\", classNames?.helpText)}>\n {field.help_text}\n </p>\n )}\n\n <FieldInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n onFileUpload={onFileUpload}\n classNames={classNames}\n />\n </div>\n );\n}\n\nfunction FieldInput({\n field,\n value,\n onValueChange,\n onFileUpload,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const inputClass = cn(\n \"w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring\",\n classNames?.input,\n );\n\n switch (field.field_type) {\n case INPUT_FIELD_TYPE.Text:\n case INPUT_FIELD_TYPE.Url:\n return (\n <input\n type={field.field_type === INPUT_FIELD_TYPE.Url ? \"url\" : \"text\"}\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder}\n required={field.is_required}\n maxLength={field.validation?.max_length ?? undefined}\n className={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Textarea:\n return (\n <textarea\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder}\n required={field.is_required}\n maxLength={field.validation?.max_length ?? undefined}\n rows={3}\n className={cn(inputClass, \"resize-none\", classNames?.textarea)}\n />\n );\n\n case INPUT_FIELD_TYPE.Number:\n return (\n <input\n type=\"number\"\n value={typeof value === \"number\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value ? parseFloat(e.target.value) : undefined)}\n placeholder={field.placeholder}\n required={field.is_required}\n min={field.validation?.min_value ?? undefined}\n max={field.validation?.max_value ?? undefined}\n className={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Select:\n return (\n <select\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value || undefined)}\n required={field.is_required}\n className={cn(inputClass, classNames?.select)}\n >\n <option value=\"\">{field.placeholder || \"Select...\"}</option>\n {(field.options || []).map((opt) => (\n <option key={opt} value={opt}>{opt}</option>\n ))}\n </select>\n );\n\n case INPUT_FIELD_TYPE.Radio:\n return (\n <div className={cn(\"space-y-2\", classNames?.radioGroup)}>\n {(field.options || []).map((opt) => (\n <label\n key={opt}\n className={cn(\n \"flex items-center gap-2 text-sm cursor-pointer\",\n classNames?.radioOption,\n )}\n >\n <input\n type=\"radio\"\n name={field.id}\n value={opt}\n checked={value === opt}\n onChange={() => onValueChange(opt)}\n className=\"accent-primary\"\n />\n {opt}\n </label>\n ))}\n </div>\n );\n\n case INPUT_FIELD_TYPE.Checkbox:\n return (\n <label className={cn(\"flex items-center gap-2 text-sm cursor-pointer\", classNames?.checkboxLabel)}>\n <input\n type=\"checkbox\"\n checked={value === true}\n onChange={(e) => onValueChange(e.target.checked)}\n className=\"accent-primary w-4 h-4\"\n />\n {field.placeholder || field.name}\n </label>\n );\n\n case INPUT_FIELD_TYPE.Color:\n return (\n <input\n type=\"color\"\n value={typeof value === \"string\" ? value : \"#000000\"}\n onChange={(e) => onValueChange(e.target.value)}\n className={cn(\"w-12 h-10 rounded-md border border-input cursor-pointer\", classNames?.colorInput)}\n />\n );\n\n case INPUT_FIELD_TYPE.Date:\n return (\n <DatePicker\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(next) => onValueChange(next || undefined)}\n required={field.is_required}\n placeholder={field.placeholder ?? \"Select a date\"}\n aria-label={field.name}\n />\n );\n\n case INPUT_FIELD_TYPE.File:\n case INPUT_FIELD_TYPE.Image:\n return (\n <FileUploadInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n onFileUpload={onFileUpload}\n classNames={classNames}\n />\n );\n\n case INPUT_FIELD_TYPE.Email:\n return (\n <input\n type=\"email\"\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder || \"email@example.com\"}\n required={field.is_required}\n className={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.DateTime: {\n const stringValue = typeof value === \"string\" ? value : \"\";\n const [datePart, timePartRaw] = stringValue.includes(\"T\")\n ? stringValue.split(\"T\", 2)\n : [stringValue, \"\"];\n const timePart = (timePartRaw ?? \"\").slice(0, 5);\n const commit = (nextDate: string, nextTime: string): void => {\n if (!nextDate && !nextTime) {\n onValueChange(undefined);\n return;\n }\n if (!nextDate) {\n onValueChange(`${nextTime}`);\n return;\n }\n const combined = `${nextDate}T${nextTime || \"00:00\"}`;\n const parsed = new Date(combined);\n onValueChange(Number.isNaN(parsed.getTime()) ? combined : parsed.toISOString());\n };\n return (\n <div className=\"grid grid-cols-1 sm:grid-cols-[1fr_auto] gap-2\">\n <DatePicker\n value={datePart ?? \"\"}\n onChange={(next) => commit(next, timePart)}\n placeholder={field.placeholder ?? \"Select a date\"}\n aria-label={`${field.name} date`}\n required={field.is_required}\n />\n <TimePicker\n value={timePart}\n onChange={(next) => commit(datePart ?? \"\", next)}\n placeholder=\"Time\"\n aria-label={`${field.name} time`}\n />\n </div>\n );\n }\n\n case INPUT_FIELD_TYPE.Time:\n return (\n <TimePicker\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(next) => onValueChange(next || undefined)}\n required={field.is_required}\n placeholder={field.placeholder ?? \"Select a time\"}\n aria-label={field.name}\n />\n );\n\n case INPUT_FIELD_TYPE.MultiSelect:\n return (\n <MultiSelectInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n />\n );\n\n case INPUT_FIELD_TYPE.DateRange:\n return (\n <DateRangeInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n />\n );\n\n case INPUT_FIELD_TYPE.Address:\n return (\n <AddressInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n inputClass={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Location:\n return (\n <LocationInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n inputClass={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Phone:\n return (\n <PhoneInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n inputClass={inputClass}\n />\n );\n\n case INPUT_FIELD_TYPE.Signature:\n return (\n <SignatureInput\n field={field}\n value={value}\n onValueChange={onValueChange}\n classNames={classNames}\n />\n );\n\n default:\n return (\n <input\n type=\"text\"\n value={typeof value === \"string\" ? value : \"\"}\n onChange={(e) => onValueChange(e.target.value)}\n placeholder={field.placeholder}\n className={inputClass}\n />\n );\n }\n}\n\nfunction FileUploadInput({\n field,\n value,\n onValueChange,\n onFileUpload,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n onFileUpload?: (file: File, field: ProductInputField) => Promise<string>;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const [isUploading, setIsUploading] = React.useState(false);\n const fileUrl = typeof value === \"string\" ? value : undefined;\n const acceptedFormats = field.validation?.accepted_formats;\n const accept = acceptedFormats\n ? acceptedFormats.map((f) => `.${f}`).join(\",\")\n : field.field_type === INPUT_FIELD_TYPE.Image\n ? \"image/*\"\n : undefined;\n\n const handleFile = async (file: File) => {\n if (!onFileUpload) return;\n\n if (field.validation?.max_size_mb) {\n const maxBytes = field.validation.max_size_mb * 1024 * 1024;\n if (file.size > maxBytes) {\n return;\n }\n }\n\n setIsUploading(true);\n try {\n const url = await onFileUpload(file, field);\n onValueChange(url);\n } finally {\n setIsUploading(false);\n }\n };\n\n return (\n <div className={cn(\"space-y-2\", classNames?.fileInput)}>\n {fileUrl ? (\n <div className=\"flex items-center gap-3\">\n {field.field_type === INPUT_FIELD_TYPE.Image && (\n <img src={fileUrl} alt=\"Uploaded\" className=\"w-16 h-16 object-cover rounded-md border border-border\" />\n )}\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm text-foreground truncate\">{fileUrl.split(\"/\").pop()}</p>\n </div>\n <button\n type=\"button\"\n onClick={() => onValueChange(undefined)}\n className=\"text-xs text-muted-foreground hover:text-destructive transition-colors\"\n >\n Remove\n </button>\n </div>\n ) : (\n <label className=\"flex flex-col items-center justify-center w-full h-24 border-2 border-dashed border-border rounded-lg cursor-pointer hover:border-primary/40 transition-colors\">\n <input\n type=\"file\"\n accept={accept}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleFile(file);\n }}\n className=\"hidden\"\n disabled={isUploading}\n />\n {isUploading ? (\n <span className=\"text-sm text-muted-foreground\">Uploading...</span>\n ) : (\n <>\n <span className=\"text-sm text-muted-foreground\">\n {field.placeholder || `Upload ${field.field_type === INPUT_FIELD_TYPE.Image ? \"image\" : \"file\"}`}\n </span>\n {acceptedFormats && (\n <span className=\"text-xs text-muted-foreground/60 mt-1\">\n {acceptedFormats.map((f) => f.toUpperCase()).join(\", \")}\n {field.validation?.max_size_mb && ` · Max ${field.validation.max_size_mb}MB`}\n </span>\n )}\n </>\n )}\n </label>\n )}\n </div>\n );\n}\n\nfunction MultiSelectInput({\n field,\n value,\n onValueChange,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const selected = Array.isArray(value) ? (value as string[]) : [];\n const options = field.options || [];\n const maxSelections = field.validation?.max_selections;\n\n const toggle = (opt: string) => {\n const next = selected.includes(opt)\n ? selected.filter((s) => s !== opt)\n : maxSelections && selected.length >= maxSelections\n ? selected\n : [...selected, opt];\n onValueChange(next.length > 0 ? next : undefined);\n };\n\n return (\n <div className={cn(\"space-y-2\", classNames?.multiSelectGroup)}>\n {options.map((opt) => (\n <label\n key={opt}\n className={cn(\n \"flex items-center gap-2 text-sm cursor-pointer\",\n classNames?.multiSelectOption,\n )}\n >\n <input\n type=\"checkbox\"\n checked={selected.includes(opt)}\n onChange={() => toggle(opt)}\n className=\"accent-primary w-4 h-4\"\n />\n {opt}\n </label>\n ))}\n {maxSelections && (\n <p className=\"text-xs text-muted-foreground\">\n {selected.length}/{maxSelections} selected\n </p>\n )}\n </div>\n );\n}\n\nfunction DateRangeInput({\n field,\n value,\n onValueChange,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const range = (value && typeof value === \"object\" ? value : {}) as DateRangeValue;\n\n const update = (key: \"start\" | \"end\", v: string) => {\n const next = { ...range, [key]: v };\n onValueChange(next.start || next.end ? next : undefined);\n };\n\n return (\n <div className={cn(\"grid grid-cols-1 sm:grid-cols-2 gap-3\", classNames?.dateRangeInput)}>\n <div>\n <label className=\"text-xs text-muted-foreground mb-1 block\">Start</label>\n <DatePicker\n value={range.start || \"\"}\n onChange={(next) => update(\"start\", next)}\n required={field.is_required}\n placeholder=\"Start date\"\n aria-label={`${field.name} start date`}\n maxDate={range.end ? new Date(`${range.end}T00:00`) : undefined}\n />\n </div>\n <div>\n <label className=\"text-xs text-muted-foreground mb-1 block\">End</label>\n <DatePicker\n value={range.end || \"\"}\n onChange={(next) => update(\"end\", next)}\n required={field.is_required}\n placeholder=\"End date\"\n aria-label={`${field.name} end date`}\n minDate={range.start ? new Date(`${range.start}T00:00`) : undefined}\n />\n </div>\n </div>\n );\n}\n\nfunction AddressInput({\n field,\n value,\n onValueChange,\n classNames,\n inputClass,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n inputClass: string;\n}): React.ReactElement {\n const clientContext = useOptionalCimplifyClient();\n const addr = (value && typeof value === \"object\" ? value : null) as AddressValue | null;\n const [inputText, setInputText] = React.useState(addr?.formatted_address ?? \"\");\n const [suggestions, setSuggestions] = React.useState<Array<{ description: string; place_id: string }>>([]);\n const [isOpen, setIsOpen] = React.useState(false);\n const [highlightedIndex, setHighlightedIndex] = React.useState(-1);\n const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n const seqRef = React.useRef(0);\n const sessionTokenRef = React.useRef(createSessionToken());\n const blurTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n React.useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n seqRef.current += 1;\n };\n }, []);\n\n const fetchSuggestions = React.useCallback(async (query: string) => {\n const client = clientContext?.client;\n if (!client || query.length < 3) {\n setSuggestions([]);\n setIsOpen(false);\n return;\n }\n\n const seq = ++seqRef.current;\n const result = await client.places.autocomplete(query, sessionTokenRef.current);\n if (seqRef.current !== seq) return;\n\n if (result.ok && result.value.predictions.length > 0) {\n setSuggestions(result.value.predictions);\n setHighlightedIndex(0);\n setIsOpen(true);\n } else {\n setSuggestions([]);\n setIsOpen(false);\n }\n }, [clientContext]);\n\n const handleInputChange = (nextValue: string) => {\n setInputText(nextValue);\n if (debounceRef.current) clearTimeout(debounceRef.current);\n debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);\n };\n\n const selectSuggestion = React.useCallback(async (suggestion: { description: string; place_id: string }) => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n\n const fallback = suggestion.description.split(\",\")[0]?.trim() || suggestion.description;\n setInputText(fallback);\n setSuggestions([]);\n setIsOpen(false);\n\n const client = clientContext?.client;\n if (!client) return;\n\n const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);\n sessionTokenRef.current = createSessionToken();\n\n if (result.ok) {\n const d = result.value;\n const addressValue: AddressValue = {\n formatted_address: d.formatted_address || fallback,\n street_address: d.street_address,\n apartment: d.apartment,\n city: d.city,\n region: d.region,\n postal_code: d.postal_code,\n country: d.country,\n latitude: d.latitude,\n longitude: d.longitude,\n place_id: d.place_id,\n };\n setInputText(addressValue.formatted_address);\n onValueChange(addressValue);\n }\n }, [clientContext, onValueChange]);\n\n return (\n <div className={cn(\"space-y-2 relative\", classNames?.addressInput)}>\n <input\n type=\"text\"\n value={inputText}\n onChange={(e) => handleInputChange(e.target.value)}\n onFocus={() => { if (suggestions.length > 0) setIsOpen(true); }}\n onBlur={() => { blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120); }}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") { setIsOpen(false); return; }\n if (!isOpen || suggestions.length === 0) return;\n if (e.key === \"ArrowDown\") { e.preventDefault(); setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0); }\n else if (e.key === \"ArrowUp\") { e.preventDefault(); setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1); }\n else if (e.key === \"Enter\" && highlightedIndex >= 0) { e.preventDefault(); void selectSuggestion(suggestions[highlightedIndex]); }\n }}\n placeholder={field.placeholder || \"Search for an address...\"}\n required={field.is_required}\n autoComplete=\"off\"\n aria-autocomplete=\"list\"\n aria-expanded={isOpen && suggestions.length > 0}\n className={inputClass}\n />\n {isOpen && suggestions.length > 0 && (\n <div role=\"listbox\" className=\"absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg\">\n {suggestions.map((s, i) => (\n <button\n key={s.place_id}\n type=\"button\"\n role=\"option\"\n aria-selected={i === highlightedIndex}\n onMouseDown={(e) => { e.preventDefault(); void selectSuggestion(s); }}\n className={cn(\"block w-full px-3 py-2.5 text-left text-sm transition-colors\", i === highlightedIndex ? \"bg-muted text-foreground\" : \"text-foreground hover:bg-muted/70\")}\n >\n {s.description}\n </button>\n ))}\n </div>\n )}\n {addr && (\n <input\n type=\"text\"\n value={addr.apartment || \"\"}\n onChange={(e) => onValueChange({ ...addr, apartment: e.target.value || undefined })}\n placeholder=\"Apt, suite, unit (optional)\"\n className={inputClass}\n />\n )}\n </div>\n );\n}\n\nfunction LocationInput({\n field,\n value,\n onValueChange,\n classNames,\n inputClass,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n inputClass: string;\n}): React.ReactElement {\n const clientContext = useOptionalCimplifyClient();\n const loc = (value && typeof value === \"object\" ? value : null) as LocationValue | null;\n const [inputText, setInputText] = React.useState(loc?.label ?? \"\");\n const [suggestions, setSuggestions] = React.useState<Array<{ description: string; place_id: string }>>([]);\n const [isOpen, setIsOpen] = React.useState(false);\n const [highlightedIndex, setHighlightedIndex] = React.useState(-1);\n const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n const seqRef = React.useRef(0);\n const sessionTokenRef = React.useRef(createSessionToken());\n const blurTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n React.useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n seqRef.current += 1;\n };\n }, []);\n\n const fetchSuggestions = React.useCallback(async (query: string) => {\n const client = clientContext?.client;\n if (!client || query.length < 3) {\n setSuggestions([]);\n setIsOpen(false);\n return;\n }\n\n const seq = ++seqRef.current;\n const result = await client.places.autocomplete(query, sessionTokenRef.current);\n if (seqRef.current !== seq) return;\n\n if (result.ok && result.value.predictions.length > 0) {\n setSuggestions(result.value.predictions);\n setHighlightedIndex(0);\n setIsOpen(true);\n } else {\n setSuggestions([]);\n setIsOpen(false);\n }\n }, [clientContext]);\n\n const handleInputChange = (nextValue: string) => {\n setInputText(nextValue);\n if (debounceRef.current) clearTimeout(debounceRef.current);\n debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);\n };\n\n const selectSuggestion = React.useCallback(async (suggestion: { description: string; place_id: string }) => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);\n\n const label = suggestion.description.split(\",\")[0]?.trim() || suggestion.description;\n setInputText(label);\n setSuggestions([]);\n setIsOpen(false);\n\n const client = clientContext?.client;\n if (!client) return;\n\n const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);\n sessionTokenRef.current = createSessionToken();\n\n if (result.ok) {\n const d = result.value;\n onValueChange({\n latitude: d.latitude,\n longitude: d.longitude,\n label: d.formatted_address || label,\n } satisfies LocationValue);\n setInputText(d.formatted_address || label);\n }\n }, [clientContext, onValueChange]);\n\n return (\n <div className={cn(\"relative\", classNames?.locationInput)}>\n <input\n type=\"text\"\n value={inputText}\n onChange={(e) => handleInputChange(e.target.value)}\n onFocus={() => { if (suggestions.length > 0) setIsOpen(true); }}\n onBlur={() => { blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120); }}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") { setIsOpen(false); return; }\n if (!isOpen || suggestions.length === 0) return;\n if (e.key === \"ArrowDown\") { e.preventDefault(); setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0); }\n else if (e.key === \"ArrowUp\") { e.preventDefault(); setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1); }\n else if (e.key === \"Enter\" && highlightedIndex >= 0) { e.preventDefault(); void selectSuggestion(suggestions[highlightedIndex]); }\n }}\n placeholder={field.placeholder || \"Search for a location...\"}\n required={field.is_required}\n autoComplete=\"off\"\n aria-autocomplete=\"list\"\n aria-expanded={isOpen && suggestions.length > 0}\n className={inputClass}\n />\n {isOpen && suggestions.length > 0 && (\n <div role=\"listbox\" className=\"absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg\">\n {suggestions.map((s, i) => (\n <button\n key={s.place_id}\n type=\"button\"\n role=\"option\"\n aria-selected={i === highlightedIndex}\n onMouseDown={(e) => { e.preventDefault(); void selectSuggestion(s); }}\n className={cn(\"block w-full px-3 py-2.5 text-left text-sm transition-colors\", i === highlightedIndex ? \"bg-muted text-foreground\" : \"text-foreground hover:bg-muted/70\")}\n >\n {s.description}\n </button>\n ))}\n </div>\n )}\n {loc && (\n <p className=\"text-xs text-muted-foreground mt-1\">\n {loc.latitude.toFixed(4)}, {loc.longitude.toFixed(4)}\n </p>\n )}\n </div>\n );\n}\n\nfunction PhoneInput({\n field,\n value,\n onValueChange,\n classNames,\n inputClass,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n inputClass: string;\n}): React.ReactElement {\n const phone = (value && typeof value === \"object\" ? value : null) as PhoneValue | null;\n const [code, setCode] = React.useState(phone?.country_code ?? \"+1\");\n const [number, setNumber] = React.useState(phone?.number ?? \"\");\n\n const update = (nextCode: string, nextNumber: string) => {\n setCode(nextCode);\n setNumber(nextNumber);\n if (nextNumber) {\n onValueChange({\n country_code: nextCode,\n number: nextNumber,\n formatted: `${nextCode} ${nextNumber}`,\n } satisfies PhoneValue);\n } else {\n onValueChange(undefined);\n }\n };\n\n return (\n <div className={cn(\"flex gap-2\", classNames?.phoneInput)}>\n <input\n type=\"text\"\n value={code}\n onChange={(e) => update(e.target.value, number)}\n placeholder=\"+1\"\n className={cn(inputClass, \"w-20 shrink-0\")}\n />\n <input\n type=\"tel\"\n value={number}\n onChange={(e) => update(code, e.target.value)}\n placeholder={field.placeholder || \"Phone number\"}\n required={field.is_required}\n className={inputClass}\n />\n </div>\n );\n}\n\nfunction SignatureInput({\n field,\n value,\n onValueChange,\n classNames,\n}: {\n field: ProductInputField;\n value: unknown;\n onValueChange: (value: unknown) => void;\n classNames?: CustomerInputFieldsClassNames;\n}): React.ReactElement {\n const sig = (value && typeof value === \"object\" ? value : null) as SignatureValue | null;\n const canvasRef = React.useRef<HTMLCanvasElement>(null);\n const isDrawing = React.useRef(false);\n\n const getCtx = () => canvasRef.current?.getContext(\"2d\") ?? null;\n\n const startDraw = (e: React.PointerEvent) => {\n const ctx = getCtx();\n if (!ctx) return;\n isDrawing.current = true;\n const rect = canvasRef.current!.getBoundingClientRect();\n ctx.beginPath();\n ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);\n canvasRef.current!.setPointerCapture(e.pointerId);\n };\n\n const draw = (e: React.PointerEvent) => {\n if (!isDrawing.current) return;\n const ctx = getCtx();\n if (!ctx) return;\n const rect = canvasRef.current!.getBoundingClientRect();\n ctx.lineWidth = 2;\n ctx.lineCap = \"round\";\n const style = getComputedStyle(canvasRef.current!);\n ctx.strokeStyle = style.color || \"#000\";\n ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);\n ctx.stroke();\n };\n\n const endDraw = () => {\n if (!isDrawing.current) return;\n isDrawing.current = false;\n const canvas = canvasRef.current;\n if (!canvas) return;\n onValueChange({\n data_url: canvas.toDataURL(\"image/png\"),\n signer_name: sig?.signer_name,\n } satisfies SignatureValue);\n };\n\n const clear = () => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n const ctx = canvas.getContext(\"2d\");\n if (ctx) ctx.clearRect(0, 0, canvas.width, canvas.height);\n onValueChange(undefined);\n };\n\n return (\n <div className={cn(\"space-y-2\", classNames?.signatureCanvas)}>\n <div className=\"relative rounded-md border border-input overflow-hidden\">\n <canvas\n ref={canvasRef}\n width={400}\n height={150}\n className=\"w-full touch-none cursor-crosshair bg-background text-foreground\"\n onPointerDown={startDraw}\n onPointerMove={draw}\n onPointerUp={endDraw}\n onPointerLeave={endDraw}\n />\n {sig?.data_url && (\n <button\n type=\"button\"\n onClick={clear}\n className=\"absolute top-1.5 right-1.5 text-xs text-muted-foreground hover:text-destructive transition-colors\"\n >\n Clear\n </button>\n )}\n </div>\n {!sig?.data_url && (\n <p className=\"text-xs text-muted-foreground\">\n {field.placeholder || \"Draw your signature above\"}\n </p>\n )}\n </div>\n );\n}\n\nfunction createSessionToken(): string {\n if (typeof crypto !== \"undefined\" && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return `places_${Date.now()}_${Math.random().toString(16).slice(2)}`;\n}\n"
13
13
  }
14
14
  ]
15
15
  }
@@ -10,7 +10,7 @@
10
10
  "files": [
11
11
  {
12
12
  "path": "date-slot-picker.tsx",
13
- "content": "\"use client\";\n\nimport React, { useState, useMemo, useCallback } from \"react\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport type { AvailableSlot, DayAvailability } from \"@cimplify/sdk\";\nimport { useServiceAvailability } from \"@cimplify/sdk/react\";\nimport { SlotPicker } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface DateSlotPickerClassNames {\n root?: string;\n dateStrip?: string;\n dateButton?: string;\n nav?: string;\n navButton?: string;\n slots?: string;\n loading?: string;\n}\n\nexport interface DateSlotPickerProps {\n /** Service ID to fetch availability and slots for. */\n serviceId: string;\n /** Number of days to show in the date strip. Default: 7. */\n daysToShow?: number;\n /** Number of participants. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot, date: string) => void;\n /** Pre-fetched availability data (skips fetch). */\n availability?: DayAvailability[];\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n className?: string;\n classNames?: DateSlotPickerClassNames;\n}\n\nfunction formatDate(dateStr: string): string {\n const date = new Date(dateStr + \"T00:00:00\");\n return date.toLocaleDateString(undefined, { weekday: \"short\", month: \"short\", day: \"numeric\" });\n}\n\nfunction toDateString(date: Date): string {\n return date.toISOString().split(\"T\")[0];\n}\n\nfunction addDays(date: Date, days: number): Date {\n const result = new Date(date);\n result.setDate(result.getDate() + days);\n return result;\n}\n\nexport function DateSlotPicker({\n serviceId,\n daysToShow = 7,\n participantCount,\n selectedSlot,\n onSlotSelect,\n availability: availabilityProp,\n showPrice = true,\n className,\n classNames,\n}: DateSlotPickerProps): React.ReactElement {\n const [offset, setOffset] = useState(0);\n const [selectedDate, setSelectedDate] = useState<string>(toDateString(new Date()));\n\n const dateRange = useMemo(() => {\n const today = new Date();\n const start = addDays(today, offset);\n const dates: string[] = [];\n for (let i = 0; i < daysToShow; i++) {\n dates.push(toDateString(addDays(start, i)));\n }\n return {\n dates,\n startDate: dates[0],\n endDate: dates[dates.length - 1],\n };\n }, [offset, daysToShow]);\n\n const { days: fetchedDays, isLoading: availabilityLoading } = useServiceAvailability(\n serviceId,\n dateRange.startDate,\n dateRange.endDate,\n {\n participantCount,\n enabled: availabilityProp === undefined,\n },\n );\n\n const days = availabilityProp ?? fetchedDays;\n\n const availabilityMap = useMemo(() => {\n const map = new Map<string, DayAvailability>();\n for (const day of days) {\n map.set(day.date, day);\n }\n return map;\n }, [days]);\n\n const handlePrev = useCallback(() => {\n setOffset((prev) => Math.max(0, prev - daysToShow));\n }, [daysToShow]);\n\n const handleNext = useCallback(() => {\n setOffset((prev) => prev + daysToShow);\n }, [daysToShow]);\n\n const handleDateChange = useCallback((value: string | number | null) => {\n if (typeof value === \"string\") {\n setSelectedDate(value);\n }\n }, []);\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot) => {\n onSlotSelect?.(slot, selectedDate);\n },\n [onSlotSelect, selectedDate],\n );\n\n return (\n <Tabs.Root\n value={selectedDate}\n onValueChange={handleDateChange}\n data-cimplify-date-slot-picker\n className={cn(className, classNames?.root)}\n >\n <div data-cimplify-date-nav className={classNames?.nav}>\n <button\n type=\"button\"\n onClick={handlePrev}\n disabled={offset === 0}\n data-cimplify-date-nav-prev\n className={classNames?.navButton}\n >\n &larr;\n </button>\n <button\n type=\"button\"\n onClick={handleNext}\n data-cimplify-date-nav-next\n className={classNames?.navButton}\n >\n &rarr;\n </button>\n </div>\n\n <Tabs.List data-cimplify-date-strip className={classNames?.dateStrip}>\n {dateRange.dates.map((date) => {\n const dayInfo = availabilityMap.get(date);\n const hasAvailability = dayInfo?.has_availability !== false;\n const isSelected = selectedDate === date;\n return (\n <Tabs.Tab\n key={date}\n value={date}\n data-cimplify-date-button\n data-selected={isSelected || undefined}\n data-available={hasAvailability || undefined}\n data-fully-booked={(!hasAvailability) || undefined}\n className={classNames?.dateButton}\n >\n {formatDate(date)}\n </Tabs.Tab>\n );\n })}\n </Tabs.List>\n\n {availabilityLoading && (\n <div\n data-cimplify-date-slot-loading\n aria-busy=\"true\"\n className={classNames?.loading}\n />\n )}\n\n <div data-cimplify-date-slots className={classNames?.slots}>\n <SlotPicker\n serviceId={serviceId}\n date={selectedDate}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n />\n </div>\n </Tabs.Root>\n );\n}\n"
13
+ "content": "\"use client\";\n\nimport React, { useState, useMemo, useCallback } from \"react\";\nimport { Tabs } from \"@base-ui/react/tabs\";\nimport type { AvailableSlot, DayAvailability } from \"@cimplify/sdk\";\nimport type { DurationUnit, SchedulingMode } from \"@cimplify/sdk\";\nimport { useServiceAvailability } from \"@cimplify/sdk/react\";\nimport { SlotPicker } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface DateSlotPickerClassNames {\n root?: string;\n dateStrip?: string;\n dateButton?: string;\n nav?: string;\n navButton?: string;\n slots?: string;\n loading?: string;\n}\n\nexport interface DateSlotPickerProps {\n /** Service ID to fetch availability and slots for. */\n serviceId: string;\n /** Number of days to show in the date strip. Default: 7. */\n daysToShow?: number;\n /** Number of participants. */\n participantCount?: number;\n /** Currently selected slot. */\n selectedSlot?: AvailableSlot | null;\n /** Called when a slot is selected. */\n onSlotSelect?: (slot: AvailableSlot, date: string) => void;\n /** Pre-fetched availability data (skips fetch). */\n availability?: DayAvailability[];\n /** Show price on slots. Default: true. */\n showPrice?: boolean;\n /** Forwarded to `<SlotPicker>` to render multi-day stay labels. */\n schedulingMode?: SchedulingMode;\n /** Forwarded to `<SlotPicker>` — unit for the stay summary in multi-day mode. */\n durationUnit?: DurationUnit;\n /** Forwarded to `<SlotPicker>` — value for the stay summary in multi-day mode. */\n durationValue?: number;\n className?: string;\n classNames?: DateSlotPickerClassNames;\n}\n\nfunction formatDate(dateStr: string): string {\n const date = new Date(dateStr + \"T00:00:00\");\n return date.toLocaleDateString(undefined, { weekday: \"short\", month: \"short\", day: \"numeric\" });\n}\n\nfunction toDateString(date: Date): string {\n return date.toISOString().split(\"T\")[0];\n}\n\nfunction addDays(date: Date, days: number): Date {\n const result = new Date(date);\n result.setDate(result.getDate() + days);\n return result;\n}\n\nexport function DateSlotPicker({\n serviceId,\n daysToShow = 7,\n participantCount,\n selectedSlot,\n onSlotSelect,\n availability: availabilityProp,\n showPrice = true,\n schedulingMode,\n durationUnit,\n durationValue,\n className,\n classNames,\n}: DateSlotPickerProps): React.ReactElement {\n const [offset, setOffset] = useState(0);\n const [selectedDate, setSelectedDate] = useState<string>(toDateString(new Date()));\n\n const dateRange = useMemo(() => {\n const today = new Date();\n const start = addDays(today, offset);\n const dates: string[] = [];\n for (let i = 0; i < daysToShow; i++) {\n dates.push(toDateString(addDays(start, i)));\n }\n return {\n dates,\n startDate: dates[0],\n endDate: dates[dates.length - 1],\n };\n }, [offset, daysToShow]);\n\n const { days: fetchedDays, isLoading: availabilityLoading } = useServiceAvailability(\n serviceId,\n dateRange.startDate,\n dateRange.endDate,\n {\n participantCount,\n enabled: availabilityProp === undefined,\n },\n );\n\n const days = availabilityProp ?? fetchedDays;\n\n const availabilityMap = useMemo(() => {\n const map = new Map<string, DayAvailability>();\n for (const day of days) {\n map.set(day.date, day);\n }\n return map;\n }, [days]);\n\n const handlePrev = useCallback(() => {\n setOffset((prev) => Math.max(0, prev - daysToShow));\n }, [daysToShow]);\n\n const handleNext = useCallback(() => {\n setOffset((prev) => prev + daysToShow);\n }, [daysToShow]);\n\n const handleDateChange = useCallback((value: string | number | null) => {\n if (typeof value === \"string\") {\n setSelectedDate(value);\n }\n }, []);\n\n const handleSlotSelect = useCallback(\n (slot: AvailableSlot) => {\n onSlotSelect?.(slot, selectedDate);\n },\n [onSlotSelect, selectedDate],\n );\n\n return (\n <Tabs.Root\n value={selectedDate}\n onValueChange={handleDateChange}\n data-cimplify-date-slot-picker\n className={cn(className, classNames?.root)}\n >\n <div data-cimplify-date-nav className={classNames?.nav}>\n <button\n type=\"button\"\n onClick={handlePrev}\n disabled={offset === 0}\n data-cimplify-date-nav-prev\n className={classNames?.navButton}\n >\n &larr;\n </button>\n <button\n type=\"button\"\n onClick={handleNext}\n data-cimplify-date-nav-next\n className={classNames?.navButton}\n >\n &rarr;\n </button>\n </div>\n\n <Tabs.List data-cimplify-date-strip className={classNames?.dateStrip}>\n {dateRange.dates.map((date) => {\n const dayInfo = availabilityMap.get(date);\n const hasAvailability = dayInfo?.has_availability !== false;\n const isSelected = selectedDate === date;\n return (\n <Tabs.Tab\n key={date}\n value={date}\n data-cimplify-date-button\n data-selected={isSelected || undefined}\n data-available={hasAvailability || undefined}\n data-fully-booked={(!hasAvailability) || undefined}\n className={classNames?.dateButton}\n >\n {formatDate(date)}\n </Tabs.Tab>\n );\n })}\n </Tabs.List>\n\n {availabilityLoading && (\n <div\n data-cimplify-date-slot-loading\n aria-busy=\"true\"\n className={classNames?.loading}\n />\n )}\n\n <div data-cimplify-date-slots className={classNames?.slots}>\n <SlotPicker\n serviceId={serviceId}\n date={selectedDate}\n participantCount={participantCount}\n selectedSlot={selectedSlot}\n onSlotSelect={handleSlotSelect}\n showPrice={showPrice}\n schedulingMode={schedulingMode}\n durationUnit={durationUnit}\n durationValue={durationValue}\n />\n </div>\n </Tabs.Root>\n );\n}\n"
14
14
  }
15
15
  ]
16
16
  }