@asksable/site-connector 0.1.6 → 0.3.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.
package/README.md CHANGED
@@ -40,9 +40,97 @@ import { BookingWidgetPanel, useSableSiteProfile } from '@asksable/site-connecto
40
40
  - `useSableSiteProfile`
41
41
  - `useSableSiteClient`
42
42
  - `useSableSiteConfig`
43
+ - `useSableLocale`
44
+ - `useTranslation`
43
45
  - `BookingWidgetPanel`
44
46
  - `getResolvedSiteProfile`
45
- - types for public site and booking payloads
47
+ - `createTranslator`, `pickLocaleField`, `localeToIntl`, `TRANSLATIONS`, `DEFAULT_LOCALE`
48
+ - types: `SableSiteConfig`, `Locale`, `TranslationKey`, `TranslationOverrides`, plus public site / booking payloads
49
+
50
+ ## Multi-language support
51
+
52
+ The booking widget translates its own UI chrome (form labels, buttons, summaries, error messages, date/time formatting) to match the host site's language. **Every Sable customer website MUST declare its current language to the widget** so the customer never sees mismatched copy (e.g. an English "Confirm Booking" button on a Spanish-language site).
53
+
54
+ Supported locales: `'en'` (default), `'es'`. Adding a locale requires a package version bump.
55
+
56
+ ### Three usage modes
57
+
58
+ | Site type | Pattern |
59
+ | --- | --- |
60
+ | **Single-language site (English only)** | Omit `language` entirely or pass `'en'`. Widget defaults to English. |
61
+ | **Single-language site (Spanish only)** | Pass `language: 'es'` once at provider mount. |
62
+ | **Multi-language site with a toggle** | Pass a reactive value that updates when the toggle changes. The provider re-renders, the widget re-renders with the new locale. |
63
+
64
+ ### Multi-language example
65
+
66
+ ```tsx
67
+ import { SableSiteProvider } from '@asksable/site-connector'
68
+ import { useLanguage } from './your-i18n-context'
69
+
70
+ function App() {
71
+ const { lang } = useLanguage() // your toggle owns this state
72
+ return (
73
+ <SableSiteProvider
74
+ config={{
75
+ apiUrl: import.meta.env.VITE_SABLE_PUBLIC_API_URL,
76
+ siteSlug: import.meta.env.VITE_SABLE_SITE_SLUG,
77
+ language: lang,
78
+ }}
79
+ >
80
+ <Routes />
81
+ </SableSiteProvider>
82
+ )
83
+ }
84
+ ```
85
+
86
+ The widget responds instantly to language changes — your toggle component flips both the host site's text and the widget by sharing the same `language` state.
87
+
88
+ ### Override individual strings (rare)
89
+
90
+ When a specific client needs different brand voice (e.g. "Reserva mi entrega" instead of the canonical "Confirmar reserva"), pass `translationOverrides`:
91
+
92
+ ```tsx
93
+ <SableSiteProvider
94
+ config={{
95
+ apiUrl,
96
+ siteSlug,
97
+ language: lang,
98
+ translationOverrides: {
99
+ es: { btnConfirmBooking: 'Reserva mi entrega' },
100
+ },
101
+ }}
102
+ >
103
+ <App />
104
+ </SableSiteProvider>
105
+ ```
106
+
107
+ Use overrides sparingly. If a change would benefit all customers, extend the canonical dictionary in `translations.ts` and bump the package version instead.
108
+
109
+ ### What the widget translates
110
+
111
+ Form labels, button text, mobile step labels, helper text, success/cancelled state copy, error messages, ARIA labels, date/time formatting (via `Intl.DateTimeFormat(locale)`), and currency formatting.
112
+
113
+ ### What the widget does NOT translate
114
+
115
+ - **Service names, descriptions, category names** — these come from the Sable workspace. The widget reads `nameEn` / `nameEs` (or any `${field}En` / `${field}Es`) fields when available, falling back to the base field. If the workspace only entered one locale, that text renders regardless of UI language. (Future workstream: dashboard support for entering both locales.)
116
+ - **Customer-typed input** — names, notes, etc.
117
+
118
+ ### Detecting locale in custom components
119
+
120
+ If you build something inside `SableSiteProvider` that needs locale awareness, use the exposed hooks:
121
+
122
+ ```tsx
123
+ import { useTranslation, useSableLocale } from '@asksable/site-connector'
124
+
125
+ function MyComponent() {
126
+ const { t, locale } = useTranslation()
127
+ // t('contactFullName') → "Nombre completo" when locale is 'es'
128
+ }
129
+ ```
130
+
131
+ ### For template builders
132
+
133
+ Every Sable website template should include the `language` prop wiring as part of the boilerplate. If the template supports a toggle, the toggle component must flip both the host site's text and the widget by sharing the same language state. **Never let the widget and host site drift to different locales** — pass a single reactive `language` value into `SableSiteConfig` and the widget stays in sync automatically.
46
134
 
47
135
  ## Public API Contract
48
136
 
@@ -0,0 +1,495 @@
1
+ {
2
+ "breakpoints": {
3
+ "375": {
4
+ "name": "booking-widget",
5
+ "viewportWidth": 375,
6
+ "width": 375,
7
+ "height": 738,
8
+ "bones": [
9
+ [
10
+ 0,
11
+ 0,
12
+ 100,
13
+ 738,
14
+ 8,
15
+ true
16
+ ],
17
+ [
18
+ 5.3333,
19
+ 20,
20
+ 89.3333,
21
+ 24,
22
+ 8
23
+ ],
24
+ [
25
+ 5.3333,
26
+ 77,
27
+ 89.3333,
28
+ 130,
29
+ 8
30
+ ],
31
+ [
32
+ 5.3333,
33
+ 207,
34
+ 89.3333,
35
+ 1,
36
+ 8
37
+ ],
38
+ [
39
+ 5.3333,
40
+ 210,
41
+ 89.3333,
42
+ 152,
43
+ 8
44
+ ],
45
+ [
46
+ 5.3333,
47
+ 362,
48
+ 89.3333,
49
+ 1,
50
+ 8
51
+ ],
52
+ [
53
+ 5.3333,
54
+ 662,
55
+ 89.3333,
56
+ 48,
57
+ 16
58
+ ]
59
+ ]
60
+ },
61
+ "768": {
62
+ "name": "booking-widget",
63
+ "viewportWidth": 768,
64
+ "width": 768,
65
+ "height": 738,
66
+ "bones": [
67
+ [
68
+ 0,
69
+ 0,
70
+ 100,
71
+ 738,
72
+ 8,
73
+ true
74
+ ],
75
+ [
76
+ 3.125,
77
+ 20,
78
+ 93.75,
79
+ 31,
80
+ 8
81
+ ],
82
+ [
83
+ 3.125,
84
+ 88,
85
+ 93.75,
86
+ 107,
87
+ 8
88
+ ],
89
+ [
90
+ 3.125,
91
+ 195,
92
+ 93.75,
93
+ 1,
94
+ 8
95
+ ],
96
+ [
97
+ 3.125,
98
+ 198,
99
+ 93.75,
100
+ 107,
101
+ 8
102
+ ],
103
+ [
104
+ 3.125,
105
+ 305,
106
+ 93.75,
107
+ 1,
108
+ 8
109
+ ],
110
+ [
111
+ 3.125,
112
+ 666,
113
+ 93.75,
114
+ 48,
115
+ 16
116
+ ]
117
+ ]
118
+ },
119
+ "1280": {
120
+ "name": "booking-widget",
121
+ "viewportWidth": 1280,
122
+ "width": 1280,
123
+ "height": 738,
124
+ "bones": [
125
+ [
126
+ 0,
127
+ 0,
128
+ 100,
129
+ 738,
130
+ 8,
131
+ true
132
+ ],
133
+ [
134
+ 3.75,
135
+ 48,
136
+ 92.5,
137
+ 31,
138
+ 8
139
+ ],
140
+ [
141
+ 3.75,
142
+ 111,
143
+ 25.8997,
144
+ 21,
145
+ 8
146
+ ],
147
+ [
148
+ 3.75,
149
+ 158,
150
+ 25.8997,
151
+ 121,
152
+ 12
153
+ ],
154
+ [
155
+ 3.75,
156
+ 280,
157
+ 25.8997,
158
+ 121,
159
+ 12
160
+ ],
161
+ [
162
+ 32.1497,
163
+ 111,
164
+ 41.2512,
165
+ 579,
166
+ 16,
167
+ true
168
+ ],
169
+ [
170
+ 34.7278,
171
+ 136,
172
+ 9.2981,
173
+ 31,
174
+ 6
175
+ ],
176
+ [
177
+ 66.1353,
178
+ 137,
179
+ 2.1875,
180
+ 28,
181
+ 6
182
+ ],
183
+ [
184
+ 68.6353,
185
+ 137,
186
+ 2.1875,
187
+ 28,
188
+ 6
189
+ ],
190
+ [
191
+ 34.7278,
192
+ 190,
193
+ 4.7546,
194
+ 29,
195
+ 8
196
+ ],
197
+ [
198
+ 39.9512,
199
+ 190,
200
+ 4.7546,
201
+ 29,
202
+ 8
203
+ ],
204
+ [
205
+ 45.1746,
206
+ 190,
207
+ 4.7546,
208
+ 29,
209
+ 8
210
+ ],
211
+ [
212
+ 50.3979,
213
+ 190,
214
+ 4.7546,
215
+ 29,
216
+ 8
217
+ ],
218
+ [
219
+ 55.6213,
220
+ 190,
221
+ 4.7546,
222
+ 29,
223
+ 8
224
+ ],
225
+ [
226
+ 60.8447,
227
+ 190,
228
+ 4.7546,
229
+ 29,
230
+ 8
231
+ ],
232
+ [
233
+ 66.0681,
234
+ 190,
235
+ 4.7546,
236
+ 29,
237
+ 8
238
+ ],
239
+ [
240
+ 60.8447,
241
+ 247,
242
+ 4.7546,
243
+ 48,
244
+ 8
245
+ ],
246
+ [
247
+ 66.0681,
248
+ 247,
249
+ 4.7546,
250
+ 48,
251
+ 8
252
+ ],
253
+ [
254
+ 34.7278,
255
+ 301,
256
+ 4.7546,
257
+ 48,
258
+ 8
259
+ ],
260
+ [
261
+ 39.9512,
262
+ 301,
263
+ 4.7546,
264
+ 48,
265
+ 8
266
+ ],
267
+ [
268
+ 45.1746,
269
+ 301,
270
+ 4.7546,
271
+ 48,
272
+ 8
273
+ ],
274
+ [
275
+ 50.3979,
276
+ 301,
277
+ 4.7546,
278
+ 48,
279
+ 8
280
+ ],
281
+ [
282
+ 55.6213,
283
+ 301,
284
+ 4.7546,
285
+ 48,
286
+ 8
287
+ ],
288
+ [
289
+ 60.8447,
290
+ 301,
291
+ 4.7546,
292
+ 48,
293
+ 8
294
+ ],
295
+ [
296
+ 66.0681,
297
+ 301,
298
+ 4.7546,
299
+ 48,
300
+ 8
301
+ ],
302
+ [
303
+ 34.7278,
304
+ 355,
305
+ 4.7546,
306
+ 48,
307
+ 8
308
+ ],
309
+ [
310
+ 39.9512,
311
+ 355,
312
+ 4.7546,
313
+ 48,
314
+ 8
315
+ ],
316
+ [
317
+ 45.1746,
318
+ 355,
319
+ 4.7546,
320
+ 48,
321
+ 8
322
+ ],
323
+ [
324
+ 50.3979,
325
+ 355,
326
+ 4.7546,
327
+ 48,
328
+ 8
329
+ ],
330
+ [
331
+ 55.6213,
332
+ 355,
333
+ 4.7546,
334
+ 48,
335
+ 8
336
+ ],
337
+ [
338
+ 60.8447,
339
+ 355,
340
+ 4.7546,
341
+ 48,
342
+ 8
343
+ ],
344
+ [
345
+ 66.0681,
346
+ 355,
347
+ 4.7546,
348
+ 48,
349
+ 8
350
+ ],
351
+ [
352
+ 34.7278,
353
+ 409,
354
+ 4.7546,
355
+ 48,
356
+ 8
357
+ ],
358
+ [
359
+ 39.9512,
360
+ 409,
361
+ 4.7546,
362
+ 48,
363
+ 8
364
+ ],
365
+ [
366
+ 45.1746,
367
+ 409,
368
+ 4.7546,
369
+ 48,
370
+ 8
371
+ ],
372
+ [
373
+ 50.3979,
374
+ 409,
375
+ 4.7546,
376
+ 48,
377
+ 8
378
+ ],
379
+ [
380
+ 55.6213,
381
+ 409,
382
+ 4.7546,
383
+ 48,
384
+ 8
385
+ ],
386
+ [
387
+ 60.8447,
388
+ 409,
389
+ 4.7546,
390
+ 48,
391
+ 8
392
+ ],
393
+ [
394
+ 66.0681,
395
+ 409,
396
+ 4.7546,
397
+ 48,
398
+ 8
399
+ ],
400
+ [
401
+ 34.7278,
402
+ 463,
403
+ 4.7546,
404
+ 48,
405
+ 8
406
+ ],
407
+ [
408
+ 39.9512,
409
+ 463,
410
+ 4.7546,
411
+ 48,
412
+ 8
413
+ ],
414
+ [
415
+ 45.1746,
416
+ 463,
417
+ 4.7546,
418
+ 48,
419
+ 8
420
+ ],
421
+ [
422
+ 50.3979,
423
+ 463,
424
+ 4.7546,
425
+ 48,
426
+ 8
427
+ ],
428
+ [
429
+ 55.6213,
430
+ 463,
431
+ 4.7546,
432
+ 48,
433
+ 8
434
+ ],
435
+ [
436
+ 60.8447,
437
+ 463,
438
+ 4.7546,
439
+ 48,
440
+ 8
441
+ ],
442
+ [
443
+ 66.0681,
444
+ 463,
445
+ 4.7546,
446
+ 48,
447
+ 8
448
+ ],
449
+ [
450
+ 34.7278,
451
+ 517,
452
+ 4.7546,
453
+ 48,
454
+ 8
455
+ ],
456
+ [
457
+ 34.7278,
458
+ 589,
459
+ 36.095,
460
+ 26,
461
+ 8
462
+ ],
463
+ [
464
+ 48.0713,
465
+ 640,
466
+ 1.25,
467
+ 16,
468
+ "50%"
469
+ ],
470
+ [
471
+ 49.79,
472
+ 639,
473
+ 7.688,
474
+ 18,
475
+ 8
476
+ ],
477
+ [
478
+ 75.9009,
479
+ 111,
480
+ 10.7581,
481
+ 21,
482
+ 8
483
+ ],
484
+ [
485
+ 75.9009,
486
+ 156,
487
+ 19.8804,
488
+ 120,
489
+ 8
490
+ ]
491
+ ]
492
+ }
493
+ },
494
+ "_hash": "020501954315e528d46c293201a85b5d"
495
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/bones/registry.ts"],"names":[],"mappings":""}
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ // Auto-generated by `npx boneyard-js build` — do not edit
3
+ import { registerBones } from 'boneyard-js';
4
+ import { configureBoneyard } from 'boneyard-js/react';
5
+ import _booking_widget from './booking-widget.bones.json';
6
+ configureBoneyard({ "color": "#efede7", "animate": "pulse" });
7
+ registerBones({
8
+ "booking-widget": _booking_widget,
9
+ });
10
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/bones/registry.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AACZ,0DAA0D;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAErD,OAAO,eAAe,MAAM,6BAA6B,CAAA;AAEzD,iBAAiB,CAAC,EAAC,OAAO,EAAC,SAAS,EAAC,SAAS,EAAC,OAAO,EAAC,CAAC,CAAA;AAExD,aAAa,CAAC;IACZ,gBAAgB,EAAE,eAAe;CAClC,CAAC,CAAA"}
@@ -1,9 +1,84 @@
1
1
  import { type ReactNode } from 'react';
2
+ import './bones/registry.js';
3
+ /**
4
+ * Pre-existing context required by reschedule mode. The host (or the
5
+ * customer arriving via magic link) is moving an existing booking, so
6
+ * the widget needs to know what booking is being moved and any data
7
+ * that should pre-fill or lock down the form.
8
+ */
9
+ export type BookingRescheduleContext = {
10
+ appointmentId: string;
11
+ /** Original startTime (ms epoch) of the booking being moved. Shown
12
+ * as crossed-out in the sidebar so the user knows what they're
13
+ * giving up. */
14
+ formerStartTime: number;
15
+ formerEndTime: number;
16
+ /** Locked service for the reschedule — cannot be changed without
17
+ * cancelling and re-booking. */
18
+ serviceId: string;
19
+ /** Optional staff lock. When set, the staff picker is pre-filled
20
+ * to this member; when omitted the widget falls back to "any
21
+ * available". */
22
+ staffMemberId?: string;
23
+ /** Pre-fill values for the contact form, sourced from the original
24
+ * appointment's customer record. */
25
+ customerName?: string;
26
+ customerEmail?: string;
27
+ customerPhone?: string;
28
+ /** Display label for the sidebar's "Reschedule by" line, sourced
29
+ * from the original appointment record (host display name). */
30
+ rescheduledByName?: string;
31
+ };
2
32
  type BookingWidgetProps = {
3
33
  title?: string;
4
34
  description?: string;
5
35
  mobileHeader?: ReactNode;
36
+ /**
37
+ * Widget mode. 'create' (default) is the original behavior. In
38
+ * 'reschedule' mode the widget pre-fills service/staff/customer
39
+ * from `rescheduleContext`, swaps the CTA copy, and routes the
40
+ * submit through `onRescheduleSubmit` instead of the public-create
41
+ * mutation. Adding this as an opt-in prop keeps the npm package
42
+ * fully backward compatible — every existing consumer that omits
43
+ * `mode` continues to behave exactly as before.
44
+ */
45
+ mode?: 'create' | 'reschedule';
46
+ rescheduleContext?: BookingRescheduleContext;
47
+ /**
48
+ * Submit handler for reschedule mode. Receives the new slot the
49
+ * user picked. The host pages this through `api.appointments
50
+ * .rescheduleAppointment` (admin path) or a magic-link-protected
51
+ * public mutation (customer path). Required when `mode` is
52
+ * 'reschedule'.
53
+ */
54
+ onRescheduleSubmit?: (args: {
55
+ newStartTime: number;
56
+ newEndTime: number;
57
+ }) => Promise<void>;
58
+ /**
59
+ * Destination for the success-state CTA after a new booking is submitted.
60
+ * Defaults to the host site's home page so embedded sites don't trap the
61
+ * customer back on `/booking` after the thank-you state.
62
+ */
63
+ successRedirectHref?: string;
64
+ /**
65
+ * Dev-only: forces the widget into a terminal state without
66
+ * calling any backend mutation. Used by the dev preview page to
67
+ * iterate on success/cancel visuals without spinning through the
68
+ * form each time. Strip from prod surfaces — these never call any
69
+ * backend.
70
+ *
71
+ * - `'success'`: renders the "You're All Set!" view (or the
72
+ * reschedule variant when `mode === 'reschedule'`).
73
+ * - `'cancelled'`: renders the "Booking cancelled" view (preview
74
+ * only — the customer-side cancel mutation isn't built yet).
75
+ *
76
+ * The legacy `__devForceSuccess` boolean is still accepted for
77
+ * back-compat with surfaces that use the old prop.
78
+ */
79
+ __devForceState?: 'success' | 'cancelled' | 'payment-full' | 'payment-deposit';
80
+ __devForceSuccess?: boolean;
6
81
  };
7
- export declare function BookingWidgetPanel({ title, description, mobileHeader, }: BookingWidgetProps): import("react/jsx-runtime").JSX.Element;
82
+ export declare function BookingWidgetPanel({ title, description, mobileHeader, mode, rescheduleContext, onRescheduleSubmit, successRedirectHref, __devForceState, __devForceSuccess, }: BookingWidgetProps): import("react/jsx-runtime").JSX.Element;
8
83
  export {};
9
84
  //# sourceMappingURL=booking-widget.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"booking-widget.d.ts","sourceRoot":"","sources":["../src/booking-widget.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA+D,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAOnG,KAAK,kBAAkB,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,SAAS,CAAA;CACzB,CAAA;AAID,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,WAAW,EACX,YAAY,GACb,EAAE,kBAAkB,2CA+gCpB"}
1
+ {"version":3,"file":"booking-widget.d.ts","sourceRoot":"","sources":["../src/booking-widget.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqH,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AASzJ,OAAO,qBAAqB,CAAA;AA4C5B;;;;;GAKG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,aAAa,EAAE,MAAM,CAAA;IACrB;;qBAEiB;IACjB,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,EAAE,MAAM,CAAA;IACrB;qCACiC;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB;;sBAEkB;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;yCACqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;oEACgE;IAChE,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,SAAS,CAAA;IACxB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAA;IAC9B,iBAAiB,CAAC,EAAE,wBAAwB,CAAA;IAC5C;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC1B,YAAY,EAAE,MAAM,CAAA;QACpB,UAAU,EAAE,MAAM,CAAA;KACnB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACnB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;;;;;;;;;;OAcG;IACH,eAAe,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,cAAc,GAAG,iBAAiB,CAAA;IAC9E,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AAKD,wBAAgB,kBAAkB,CAAC,EACjC,KAAK,EACL,WAAW,EACX,YAAY,EACZ,IAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,mBAAyB,EACzB,eAAe,EACf,iBAAiB,GAClB,EAAE,kBAAkB,2CA2uEpB"}