@cimplify/sdk 0.90.0 → 0.92.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 (67) hide show
  1. package/dist/advanced.d.mts +3 -3
  2. package/dist/advanced.d.ts +3 -3
  3. package/dist/advanced.js +20 -20
  4. package/dist/advanced.mjs +1 -1
  5. package/dist/{chunk-JVWFIFQ7.js → chunk-3MBMIEWT.js} +4 -4
  6. package/dist/{chunk-2UDEWILL.mjs → chunk-3XD2FVP4.mjs} +117 -35
  7. package/dist/{chunk-NGE37REW.mjs → chunk-54H2YYIW.mjs} +2 -2
  8. package/dist/{chunk-BN4V4ONE.js → chunk-DCRN67NO.js} +158 -73
  9. package/dist/{chunk-T7Z25EY4.js → chunk-HKN3YG76.js} +84 -0
  10. package/dist/{chunk-GHFDYM5K.js → chunk-IVJEUGWF.js} +10 -1
  11. package/dist/{chunk-KJP4U6L3.js → chunk-OAHSF542.js} +12 -0
  12. package/dist/{chunk-NNGKC3MM.mjs → chunk-PYBFTMHI.mjs} +12 -0
  13. package/dist/{chunk-OZDDHSIB.mjs → chunk-RRBY35TJ.mjs} +80 -1
  14. package/dist/{chunk-ZU4ZRYUR.mjs → chunk-T7LDBZZZ.mjs} +10 -1
  15. package/dist/{client-YiSelM7x.d.mts → client-BQbSKlHS.d.mts} +90 -11
  16. package/dist/{client-DMttx1xk.d.mts → client-Bw4HtLP1.d.mts} +2 -2
  17. package/dist/{client-CR1f94wY.d.ts → client-CeW3acve.d.ts} +90 -11
  18. package/dist/{client-CqpOcbin.d.ts → client-Otzv_rgP.d.ts} +2 -2
  19. package/dist/{index-DD9z396M.d.mts → index-CmIboQjl.d.mts} +3 -3
  20. package/dist/{index-CxTVu0HZ.d.ts → index-DgoRwz8l.d.ts} +3 -3
  21. package/dist/index.d.mts +49 -7
  22. package/dist/index.d.ts +49 -7
  23. package/dist/index.js +103 -83
  24. package/dist/index.mjs +5 -5
  25. package/dist/mock/cli.mjs +12 -0
  26. package/dist/mock/library.js +12 -0
  27. package/dist/mock/library.mjs +12 -0
  28. package/dist/mock/msw.js +12 -0
  29. package/dist/mock/msw.mjs +12 -0
  30. package/dist/{payment-5d8FTA1q.d.mts → payment-BFtLqiG2.d.mts} +1 -1
  31. package/dist/{payment-BX7ccIKY.d.ts → payment-Cq8FT1B6.d.ts} +1 -1
  32. package/dist/{price-DtfP_Xfq.d.ts → price-DaFXT-Lr.d.ts} +1 -1
  33. package/dist/{price-arl_4Rzn.d.mts → price-DleDWk_s.d.mts} +1 -1
  34. package/dist/{product-DZX4QuKS.d.ts → product-bZ8G5Z8D.d.mts} +38 -0
  35. package/dist/{product-DZX4QuKS.d.mts → product-bZ8G5Z8D.d.ts} +38 -0
  36. package/dist/react.d.mts +71 -6
  37. package/dist/react.d.ts +71 -6
  38. package/dist/react.js +466 -337
  39. package/dist/react.mjs +398 -272
  40. package/dist/{server-BceZMxcK.d.mts → server-D-iK-bNt.d.mts} +1 -1
  41. package/dist/{server-DoJGSC7L.d.ts → server-DwsY9hlX.d.ts} +1 -1
  42. package/dist/server.d.mts +4 -4
  43. package/dist/server.d.ts +4 -4
  44. package/dist/server.js +7 -7
  45. package/dist/server.mjs +2 -2
  46. package/dist/styles.css +1 -1
  47. package/dist/testing/msw.d.mts +2 -2
  48. package/dist/testing/msw.d.ts +2 -2
  49. package/dist/testing/msw.js +2 -2
  50. package/dist/testing/msw.mjs +1 -1
  51. package/dist/testing/suite.d.mts +6 -6
  52. package/dist/testing/suite.d.ts +6 -6
  53. package/dist/testing/suite.js +23 -23
  54. package/dist/testing/suite.mjs +4 -4
  55. package/dist/testing.d.mts +5 -5
  56. package/dist/testing.d.ts +5 -5
  57. package/dist/testing.js +79 -79
  58. package/dist/testing.mjs +5 -5
  59. package/dist/utils.d.mts +4 -4
  60. package/dist/utils.d.ts +4 -4
  61. package/package.json +1 -1
  62. package/registry/booking-page.json +1 -1
  63. package/registry/chat-widget.json +1 -1
  64. package/registry/customer-input-fields.json +1 -1
  65. package/registry/date-slot-picker.json +1 -1
  66. package/registry/product-customizer.json +1 -1
  67. package/registry/rental-service-card.json +1 -1
package/dist/react.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import { DURATION_UNIT, getVariantDisplayName, INPUT_FIELD_TYPE, PRODUCT_TYPE, BOUNDARY_KIND, resolveGallery, RENDER_HINT, DEFAULT_CDN_BASE_URL } from './chunk-OZDDHSIB.mjs';
3
- export { getVariantDisplayName } from './chunk-OZDDHSIB.mjs';
2
+ import { DURATION_UNIT, getVariantDisplayName, INPUT_FIELD_TYPE, isMultiDayService, serviceBillingMultiplier, PRODUCT_TYPE, BOUNDARY_KIND, resolveGallery, RENDER_HINT, DEFAULT_CDN_BASE_URL } from './chunk-RRBY35TJ.mjs';
3
+ export { getVariantDisplayName } from './chunk-RRBY35TJ.mjs';
4
4
  import { messagePreview } from './chunk-U4MW6PYU.mjs';
5
5
  export { messagePreview, splitGenUIContent } from './chunk-U4MW6PYU.mjs';
6
- import { createCimplifyClient } from './chunk-2UDEWILL.mjs';
7
- import { ELEMENT_TYPES, EVENT_TYPES, startSignIn } from './chunk-ZU4ZRYUR.mjs';
6
+ import { SESSION_CLAIMED, SESSION_CLEARED, createCimplifyClient, MUTATION_ERROR } from './chunk-3XD2FVP4.mjs';
7
+ import { ELEMENT_TYPES, EVENT_TYPES, startSignIn } from './chunk-T7LDBZZZ.mjs';
8
8
  import { parsePrice, formatMoney, formatPrice, getProductCurrency, getUnitPriceAtQuantity, isOnSale, getDiscountPercentage, getBasePrice, getPriceRange, formatPriceRange } from './chunk-KFLGQGMC.mjs';
9
9
  import { isSupportedCurrency } from './chunk-K5EN2W2Q.mjs';
10
10
  import './chunk-3G6RQLXK.mjs';
@@ -723,6 +723,7 @@ function mapServerCart(serverCart, fallbackCurrency) {
723
723
  scheduledStart: item.scheduled_start || void 0,
724
724
  scheduledEnd: item.scheduled_end || void 0,
725
725
  staffId: item.staff_id || void 0,
726
+ units: item.units || void 0,
726
727
  lineType: item.line_type,
727
728
  confirmationCode: item.confirmation_code || void 0,
728
729
  serviceStatus: item.service_status || void 0,
@@ -781,7 +782,7 @@ async function syncFromServer(entry) {
781
782
  return next;
782
783
  }
783
784
  function emitMutationError(entry, operation, opId, idempotencyKey, error) {
784
- entry.client.events.emit("mutation:error", {
785
+ entry.client.events.emit(MUTATION_ERROR, {
785
786
  service: "cart",
786
787
  operation,
787
788
  opId,
@@ -821,6 +822,12 @@ function getOrCreateStore(params) {
821
822
  syncTail: Promise.resolve(),
822
823
  failedOpRemovers: /* @__PURE__ */ new Set()
823
824
  };
825
+ client.events.on(SESSION_CLAIMED, () => {
826
+ if (entry.initialized) void syncFromServer(entry);
827
+ });
828
+ client.events.on(SESSION_CLEARED, () => {
829
+ if (entry.initialized) void syncFromServer(entry);
830
+ });
824
831
  cartStores.set(storeKey, entry);
825
832
  return entry;
826
833
  }
@@ -879,7 +886,8 @@ async function performAddItem(entry, product, quantity, options) {
879
886
  specialInstructions: normalizedOptions.specialInstructions,
880
887
  scheduledStart: normalizedOptions.scheduledStart,
881
888
  scheduledEnd: normalizedOptions.scheduledEnd,
882
- staffId: normalizedOptions.staffId
889
+ staffId: normalizedOptions.staffId,
890
+ units: normalizedOptions.units
883
891
  });
884
892
  const idempotencyKey = `idem_${typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`}`;
885
893
  const patch = { kind: "add_item", tempItem, lineKey };
@@ -901,6 +909,7 @@ async function performAddItem(entry, product, quantity, options) {
901
909
  scheduled_end: normalizedOptions.scheduledEnd,
902
910
  staff_id: normalizedOptions.staffId,
903
911
  resource_id: normalizedOptions.resourceId,
912
+ units: normalizedOptions.units,
904
913
  customer_inputs: normalizedOptions.customerInputs
905
914
  },
906
915
  { idempotencyKey }
@@ -5454,14 +5463,24 @@ function useServices(options = {}) {
5454
5463
  }
5455
5464
 
5456
5465
  // src/react/hooks/use-available-slots.ts
5466
+ function narrowingUnits(units) {
5467
+ return units != null && units > 1 ? units : void 0;
5468
+ }
5457
5469
  var slotsQuery = createCachedQuery({
5458
5470
  name: "useAvailableSlots",
5459
- key: (client, options) => cacheKey(client.getPublicKey(), options.serviceId, options.date, options.participantCount),
5471
+ key: (client, options) => cacheKey(
5472
+ client.getPublicKey(),
5473
+ options.serviceId,
5474
+ options.date,
5475
+ options.participantCount,
5476
+ narrowingUnits(options.units)
5477
+ ),
5460
5478
  fetch: async (client, options) => {
5461
5479
  const result = await client.scheduling.getAvailableSlots({
5462
5480
  service_id: options.serviceId,
5463
5481
  date: options.date,
5464
- participant_count: options.participantCount
5482
+ participant_count: options.participantCount,
5483
+ units: narrowingUnits(options.units)
5465
5484
  });
5466
5485
  if (!result.ok) throw result.error;
5467
5486
  return result.value;
@@ -5478,6 +5497,9 @@ function useAvailableSlots(serviceId, date, options = {}) {
5478
5497
  }
5479
5498
 
5480
5499
  // src/react/hooks/use-service-availability.ts
5500
+ function narrowingUnits2(units) {
5501
+ return units != null && units > 1 ? units : void 0;
5502
+ }
5481
5503
  var serviceAvailabilityQuery = createCachedQuery({
5482
5504
  name: "useServiceAvailability",
5483
5505
  key: (client, options) => cacheKey(
@@ -5485,7 +5507,8 @@ var serviceAvailabilityQuery = createCachedQuery({
5485
5507
  options.serviceId,
5486
5508
  options.startDate,
5487
5509
  options.endDate,
5488
- options.participantCount
5510
+ options.participantCount,
5511
+ narrowingUnits2(options.units)
5489
5512
  ),
5490
5513
  fetch: async (client, options) => {
5491
5514
  const result = await client.scheduling.getServiceAvailability({
@@ -5493,7 +5516,8 @@ var serviceAvailabilityQuery = createCachedQuery({
5493
5516
  start_date: options.startDate,
5494
5517
  end_date: options.endDate,
5495
5518
  location_id: options.locationId,
5496
- participant_count: options.participantCount
5519
+ participant_count: options.participantCount,
5520
+ units: narrowingUnits2(options.units)
5497
5521
  });
5498
5522
  if (!result.ok) throw result.error;
5499
5523
  return result.value.availability;
@@ -5877,6 +5901,9 @@ function useChat(options = {}) {
5877
5901
  const [awaitingReply, setAwaitingReply] = useState(false);
5878
5902
  const [seenAt, setSeenAt] = useState(null);
5879
5903
  const [mayHaveOlder, setMayHaveOlder] = useState(false);
5904
+ const [claimedAs, setClaimedAs] = useState(
5905
+ () => client?.support.claimedCustomerName ?? null
5906
+ );
5880
5907
  const pollRef = useRef(null);
5881
5908
  const lastActivityRef = useRef(Date.now());
5882
5909
  const openedRef = useRef(false);
@@ -5888,6 +5915,7 @@ function useChat(options = {}) {
5888
5915
  const aiEnabledRef = useRef(false);
5889
5916
  const humanRequestedRef = useRef(false);
5890
5917
  const awaitReplyTimerRef = useRef(null);
5918
+ const claimUnsubRef = useRef(null);
5891
5919
  messagesRef.current = messages;
5892
5920
  clientRef.current = client;
5893
5921
  humanRequestedRef.current = humanRequested;
@@ -5948,6 +5976,17 @@ function useChat(options = {}) {
5948
5976
  pollRef.current = null;
5949
5977
  }
5950
5978
  }, []);
5979
+ const resyncConversation = useCallback(async () => {
5980
+ const c = clientRef.current;
5981
+ if (!c || !openedRef.current) return;
5982
+ const result = await c.support.openConversation();
5983
+ if (!result.ok) return;
5984
+ setMessages(result.value.messages);
5985
+ aiEnabledRef.current = result.value.conversation.ai_enabled;
5986
+ setHumanRequested(result.value.conversation.needs_human_review ?? false);
5987
+ setSeenAt(result.value.conversation.team_read_at ?? null);
5988
+ setMayHaveOlder(result.value.messages.length >= 50);
5989
+ }, []);
5951
5990
  const connectWebSocket = useCallback(() => {
5952
5991
  const c = clientRef.current;
5953
5992
  if (!c || typeof WebSocket === "undefined") {
@@ -5982,6 +6021,8 @@ function useChat(options = {}) {
5982
6021
  if (latest && (!last || new Date(last.created_at).getTime() < new Date(latest).getTime())) {
5983
6022
  fetchNewMessages();
5984
6023
  }
6024
+ } else if (envelope.type === "rekey") {
6025
+ void resyncConversation();
5985
6026
  }
5986
6027
  } catch {
5987
6028
  }
@@ -6001,12 +6042,57 @@ function useChat(options = {}) {
6001
6042
  ws.onerror = () => {
6002
6043
  };
6003
6044
  wsRef.current = ws;
6004
- }, [appendMessages, startPolling, stopPolling, fetchNewMessages]);
6045
+ }, [appendMessages, startPolling, stopPolling, fetchNewMessages, resyncConversation]);
6046
+ const resetConversation = useCallback(() => {
6047
+ stopPolling();
6048
+ if (wsReconnectTimerRef.current) {
6049
+ clearTimeout(wsReconnectTimerRef.current);
6050
+ wsReconnectTimerRef.current = null;
6051
+ }
6052
+ if (wsRef.current) {
6053
+ wsRef.current.onclose = null;
6054
+ wsRef.current.close();
6055
+ wsRef.current = null;
6056
+ }
6057
+ wsReconnectAttemptRef.current = 0;
6058
+ openedRef.current = false;
6059
+ stopAwaitingReply();
6060
+ setMessages([]);
6061
+ setConversationOpened(false);
6062
+ setHumanRequested(false);
6063
+ setSeenAt(null);
6064
+ setMayHaveOlder(false);
6065
+ setError(null);
6066
+ setClaimedAs(null);
6067
+ }, [stopPolling, stopAwaitingReply]);
6068
+ const watchSessionClaims = useCallback(() => {
6069
+ const c = clientRef.current;
6070
+ if (!c || claimUnsubRef.current) return;
6071
+ const unsubClaimed = c.events.on(SESSION_CLAIMED, (event) => {
6072
+ setClaimedAs(event.customerName || null);
6073
+ if (!openedRef.current) return;
6074
+ connectWebSocket();
6075
+ void resyncConversation();
6076
+ });
6077
+ const unsubCleared = c.events.on(SESSION_CLEARED, resetConversation);
6078
+ claimUnsubRef.current = () => {
6079
+ unsubClaimed();
6080
+ unsubCleared();
6081
+ };
6082
+ }, [connectWebSocket, resyncConversation, resetConversation]);
6083
+ const signOut = useCallback(async () => {
6084
+ const c = clientRef.current;
6085
+ if (!c) return false;
6086
+ const result = await c.link.logout();
6087
+ return result.ok;
6088
+ }, []);
6005
6089
  useEffect(() => {
6006
6090
  return () => {
6007
6091
  if (pollRef.current) clearInterval(pollRef.current);
6008
6092
  if (wsReconnectTimerRef.current) clearTimeout(wsReconnectTimerRef.current);
6009
6093
  if (awaitReplyTimerRef.current) clearTimeout(awaitReplyTimerRef.current);
6094
+ claimUnsubRef.current?.();
6095
+ claimUnsubRef.current = null;
6010
6096
  if (wsRef.current) {
6011
6097
  wsRef.current.onclose = null;
6012
6098
  wsRef.current.close();
@@ -6026,13 +6112,15 @@ function useChat(options = {}) {
6026
6112
  setHumanRequested(result.value.conversation.needs_human_review ?? false);
6027
6113
  setSeenAt(result.value.conversation.team_read_at ?? null);
6028
6114
  setMayHaveOlder(result.value.messages.length >= 50);
6115
+ setClaimedAs(client.support.claimedCustomerName);
6029
6116
  connectWebSocket();
6117
+ watchSessionClaims();
6030
6118
  } else {
6031
6119
  openedRef.current = false;
6032
6120
  setError(result.error);
6033
6121
  }
6034
6122
  setIsLoading(false);
6035
- }, [client, connectWebSocket]);
6123
+ }, [client, connectWebSocket, watchSessionClaims]);
6036
6124
  const send = useCallback(
6037
6125
  async (content, opts) => {
6038
6126
  const attachments = opts?.attachments ?? [];
@@ -6152,6 +6240,8 @@ function useChat(options = {}) {
6152
6240
  awaitingReply,
6153
6241
  seenAt,
6154
6242
  mayHaveOlder,
6243
+ claimedAs,
6244
+ signOut,
6155
6245
  send,
6156
6246
  startConversation,
6157
6247
  react,
@@ -7886,6 +7976,177 @@ function VolumePricing({
7886
7976
  }
7887
7977
  );
7888
7978
  }
7979
+ function createPlacesSessionToken() {
7980
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
7981
+ return crypto.randomUUID();
7982
+ }
7983
+ return `places_${Date.now()}_${Math.random().toString(16).slice(2)}`;
7984
+ }
7985
+ var MIN_QUERY_LENGTH = 3;
7986
+ var DEBOUNCE_MS = 300;
7987
+ var BLUR_CLOSE_MS = 120;
7988
+ function AddressAutocomplete({
7989
+ value,
7990
+ onSelect,
7991
+ onDirty,
7992
+ placeholder,
7993
+ required,
7994
+ autoFocus,
7995
+ id,
7996
+ className,
7997
+ classNames
7998
+ }) {
7999
+ const clientContext = useOptionalCimplifyClient();
8000
+ const [inputText, setInputText] = React14.useState(value?.formatted_address ?? "");
8001
+ const [suggestions, setSuggestions] = React14.useState([]);
8002
+ const [isOpen, setIsOpen] = React14.useState(false);
8003
+ const [highlightedIndex, setHighlightedIndex] = React14.useState(-1);
8004
+ const debounceRef = React14.useRef(null);
8005
+ const seqRef = React14.useRef(0);
8006
+ const sessionTokenRef = React14.useRef(createPlacesSessionToken());
8007
+ const blurTimeoutRef = React14.useRef(null);
8008
+ React14.useEffect(() => {
8009
+ return () => {
8010
+ if (debounceRef.current) clearTimeout(debounceRef.current);
8011
+ if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8012
+ seqRef.current += 1;
8013
+ };
8014
+ }, []);
8015
+ const fetchSuggestions = React14.useCallback(
8016
+ async (query) => {
8017
+ const client = clientContext?.client;
8018
+ if (!client || query.length < MIN_QUERY_LENGTH) {
8019
+ setSuggestions([]);
8020
+ setIsOpen(false);
8021
+ return;
8022
+ }
8023
+ const seq = ++seqRef.current;
8024
+ const result = await client.places.autocomplete(query, sessionTokenRef.current);
8025
+ if (seqRef.current !== seq) return;
8026
+ if (result.ok && result.value.predictions.length > 0) {
8027
+ setSuggestions(result.value.predictions);
8028
+ setHighlightedIndex(0);
8029
+ setIsOpen(true);
8030
+ } else {
8031
+ setSuggestions([]);
8032
+ setIsOpen(false);
8033
+ }
8034
+ },
8035
+ [clientContext]
8036
+ );
8037
+ const handleInputChange = (nextValue) => {
8038
+ setInputText(nextValue);
8039
+ onDirty?.();
8040
+ if (debounceRef.current) clearTimeout(debounceRef.current);
8041
+ debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), DEBOUNCE_MS);
8042
+ };
8043
+ const selectSuggestion = React14.useCallback(
8044
+ async (suggestion) => {
8045
+ if (debounceRef.current) clearTimeout(debounceRef.current);
8046
+ if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8047
+ const fallback = suggestion.description.split(",")[0]?.trim() || suggestion.description;
8048
+ setInputText(fallback);
8049
+ setSuggestions([]);
8050
+ setIsOpen(false);
8051
+ const client = clientContext?.client;
8052
+ if (!client) return;
8053
+ const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);
8054
+ sessionTokenRef.current = createPlacesSessionToken();
8055
+ if (result.ok) {
8056
+ const d = result.value;
8057
+ const address = {
8058
+ formatted_address: d.formatted_address || fallback,
8059
+ street_address: d.street_address,
8060
+ apartment: d.apartment,
8061
+ city: d.city,
8062
+ region: d.region,
8063
+ postal_code: d.postal_code,
8064
+ country: d.country,
8065
+ latitude: d.latitude,
8066
+ longitude: d.longitude,
8067
+ place_id: d.place_id
8068
+ };
8069
+ setInputText(address.formatted_address);
8070
+ onSelect(address);
8071
+ }
8072
+ },
8073
+ [clientContext, onSelect]
8074
+ );
8075
+ return /* @__PURE__ */ jsxs("div", { "data-cimplify-address-autocomplete": true, className: cn("relative", className, classNames?.root), children: [
8076
+ /* @__PURE__ */ jsx(
8077
+ "input",
8078
+ {
8079
+ id,
8080
+ type: "text",
8081
+ value: inputText,
8082
+ onChange: (e) => handleInputChange(e.target.value),
8083
+ onFocus: () => {
8084
+ if (suggestions.length > 0) setIsOpen(true);
8085
+ },
8086
+ onBlur: () => {
8087
+ blurTimeoutRef.current = setTimeout(() => setIsOpen(false), BLUR_CLOSE_MS);
8088
+ },
8089
+ onKeyDown: (e) => {
8090
+ if (e.key === "Escape") {
8091
+ setIsOpen(false);
8092
+ return;
8093
+ }
8094
+ if (!isOpen || suggestions.length === 0) return;
8095
+ if (e.key === "ArrowDown") {
8096
+ e.preventDefault();
8097
+ setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0);
8098
+ } else if (e.key === "ArrowUp") {
8099
+ e.preventDefault();
8100
+ setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1);
8101
+ } else if (e.key === "Enter" && highlightedIndex >= 0) {
8102
+ e.preventDefault();
8103
+ void selectSuggestion(suggestions[highlightedIndex]);
8104
+ }
8105
+ },
8106
+ placeholder: placeholder ?? "Search for an address...",
8107
+ required,
8108
+ autoFocus,
8109
+ autoComplete: "off",
8110
+ "aria-autocomplete": "list",
8111
+ "aria-expanded": isOpen && suggestions.length > 0,
8112
+ "data-cimplify-address-input": true,
8113
+ className: classNames?.input
8114
+ }
8115
+ ),
8116
+ isOpen && suggestions.length > 0 && /* @__PURE__ */ jsx(
8117
+ "div",
8118
+ {
8119
+ role: "listbox",
8120
+ "data-cimplify-address-list": true,
8121
+ className: cn(
8122
+ "absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg",
8123
+ classNames?.list
8124
+ ),
8125
+ children: suggestions.map((s, i) => /* @__PURE__ */ jsx(
8126
+ "button",
8127
+ {
8128
+ type: "button",
8129
+ role: "option",
8130
+ "aria-selected": i === highlightedIndex,
8131
+ onMouseDown: (e) => {
8132
+ e.preventDefault();
8133
+ void selectSuggestion(s);
8134
+ },
8135
+ "data-cimplify-address-option": true,
8136
+ className: cn(
8137
+ "block w-full px-3 py-2.5 text-left text-sm transition-colors",
8138
+ i === highlightedIndex ? "bg-muted text-foreground" : "text-foreground hover:bg-muted/70",
8139
+ classNames?.option,
8140
+ i === highlightedIndex && classNames?.activeOption
8141
+ ),
8142
+ children: s.description
8143
+ },
8144
+ s.place_id
8145
+ ))
8146
+ }
8147
+ )
8148
+ ] });
8149
+ }
7889
8150
  function parseValue(value) {
7890
8151
  if (!value) return void 0;
7891
8152
  const date = parseISO(value);
@@ -8646,129 +8907,18 @@ function AddressInput({
8646
8907
  classNames,
8647
8908
  inputClass
8648
8909
  }) {
8649
- const clientContext = useOptionalCimplifyClient();
8650
8910
  const addr = value && typeof value === "object" ? value : null;
8651
- const [inputText, setInputText] = React14.useState(addr?.formatted_address ?? "");
8652
- const [suggestions, setSuggestions] = React14.useState([]);
8653
- const [isOpen, setIsOpen] = React14.useState(false);
8654
- const [highlightedIndex, setHighlightedIndex] = React14.useState(-1);
8655
- const debounceRef = React14.useRef(null);
8656
- const seqRef = React14.useRef(0);
8657
- const sessionTokenRef = React14.useRef(createSessionToken());
8658
- const blurTimeoutRef = React14.useRef(null);
8659
- React14.useEffect(() => {
8660
- return () => {
8661
- if (debounceRef.current) clearTimeout(debounceRef.current);
8662
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8663
- seqRef.current += 1;
8664
- };
8665
- }, []);
8666
- const fetchSuggestions = React14.useCallback(async (query) => {
8667
- const client = clientContext?.client;
8668
- if (!client || query.length < 3) {
8669
- setSuggestions([]);
8670
- setIsOpen(false);
8671
- return;
8672
- }
8673
- const seq = ++seqRef.current;
8674
- const result = await client.places.autocomplete(query, sessionTokenRef.current);
8675
- if (seqRef.current !== seq) return;
8676
- if (result.ok && result.value.predictions.length > 0) {
8677
- setSuggestions(result.value.predictions);
8678
- setHighlightedIndex(0);
8679
- setIsOpen(true);
8680
- } else {
8681
- setSuggestions([]);
8682
- setIsOpen(false);
8683
- }
8684
- }, [clientContext]);
8685
- const handleInputChange = (nextValue) => {
8686
- setInputText(nextValue);
8687
- if (debounceRef.current) clearTimeout(debounceRef.current);
8688
- debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);
8689
- };
8690
- const selectSuggestion = React14.useCallback(async (suggestion) => {
8691
- if (debounceRef.current) clearTimeout(debounceRef.current);
8692
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8693
- const fallback = suggestion.description.split(",")[0]?.trim() || suggestion.description;
8694
- setInputText(fallback);
8695
- setSuggestions([]);
8696
- setIsOpen(false);
8697
- const client = clientContext?.client;
8698
- if (!client) return;
8699
- const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);
8700
- sessionTokenRef.current = createSessionToken();
8701
- if (result.ok) {
8702
- const d = result.value;
8703
- const addressValue = {
8704
- formatted_address: d.formatted_address || fallback,
8705
- street_address: d.street_address,
8706
- apartment: d.apartment,
8707
- city: d.city,
8708
- region: d.region,
8709
- postal_code: d.postal_code,
8710
- country: d.country,
8711
- latitude: d.latitude,
8712
- longitude: d.longitude,
8713
- place_id: d.place_id
8714
- };
8715
- setInputText(addressValue.formatted_address);
8716
- onValueChange(addressValue);
8717
- }
8718
- }, [clientContext, onValueChange]);
8719
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2 relative", classNames?.addressInput), children: [
8911
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2", classNames?.addressInput), children: [
8720
8912
  /* @__PURE__ */ jsx(
8721
- "input",
8913
+ AddressAutocomplete,
8722
8914
  {
8723
- type: "text",
8724
- value: inputText,
8725
- onChange: (e) => handleInputChange(e.target.value),
8726
- onFocus: () => {
8727
- if (suggestions.length > 0) setIsOpen(true);
8728
- },
8729
- onBlur: () => {
8730
- blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120);
8731
- },
8732
- onKeyDown: (e) => {
8733
- if (e.key === "Escape") {
8734
- setIsOpen(false);
8735
- return;
8736
- }
8737
- if (!isOpen || suggestions.length === 0) return;
8738
- if (e.key === "ArrowDown") {
8739
- e.preventDefault();
8740
- setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0);
8741
- } else if (e.key === "ArrowUp") {
8742
- e.preventDefault();
8743
- setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1);
8744
- } else if (e.key === "Enter" && highlightedIndex >= 0) {
8745
- e.preventDefault();
8746
- void selectSuggestion(suggestions[highlightedIndex]);
8747
- }
8748
- },
8915
+ value: addr,
8916
+ onSelect: onValueChange,
8749
8917
  placeholder: field.placeholder || "Search for an address...",
8750
8918
  required: field.is_required,
8751
- autoComplete: "off",
8752
- "aria-autocomplete": "list",
8753
- "aria-expanded": isOpen && suggestions.length > 0,
8754
- className: inputClass
8919
+ classNames: { input: inputClass }
8755
8920
  }
8756
8921
  ),
8757
- isOpen && suggestions.length > 0 && /* @__PURE__ */ jsx("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", children: suggestions.map((s, i) => /* @__PURE__ */ jsx(
8758
- "button",
8759
- {
8760
- type: "button",
8761
- role: "option",
8762
- "aria-selected": i === highlightedIndex,
8763
- onMouseDown: (e) => {
8764
- e.preventDefault();
8765
- void selectSuggestion(s);
8766
- },
8767
- 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"),
8768
- children: s.description
8769
- },
8770
- s.place_id
8771
- )) }),
8772
8922
  addr && /* @__PURE__ */ jsx(
8773
8923
  "input",
8774
8924
  {
@@ -8788,121 +8938,22 @@ function LocationInput({
8788
8938
  classNames,
8789
8939
  inputClass
8790
8940
  }) {
8791
- const clientContext = useOptionalCimplifyClient();
8792
8941
  const loc = value && typeof value === "object" ? value : null;
8793
- const [inputText, setInputText] = React14.useState(loc?.label ?? "");
8794
- const [suggestions, setSuggestions] = React14.useState([]);
8795
- const [isOpen, setIsOpen] = React14.useState(false);
8796
- const [highlightedIndex, setHighlightedIndex] = React14.useState(-1);
8797
- const debounceRef = React14.useRef(null);
8798
- const seqRef = React14.useRef(0);
8799
- const sessionTokenRef = React14.useRef(createSessionToken());
8800
- const blurTimeoutRef = React14.useRef(null);
8801
- React14.useEffect(() => {
8802
- return () => {
8803
- if (debounceRef.current) clearTimeout(debounceRef.current);
8804
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8805
- seqRef.current += 1;
8806
- };
8807
- }, []);
8808
- const fetchSuggestions = React14.useCallback(async (query) => {
8809
- const client = clientContext?.client;
8810
- if (!client || query.length < 3) {
8811
- setSuggestions([]);
8812
- setIsOpen(false);
8813
- return;
8814
- }
8815
- const seq = ++seqRef.current;
8816
- const result = await client.places.autocomplete(query, sessionTokenRef.current);
8817
- if (seqRef.current !== seq) return;
8818
- if (result.ok && result.value.predictions.length > 0) {
8819
- setSuggestions(result.value.predictions);
8820
- setHighlightedIndex(0);
8821
- setIsOpen(true);
8822
- } else {
8823
- setSuggestions([]);
8824
- setIsOpen(false);
8825
- }
8826
- }, [clientContext]);
8827
- const handleInputChange = (nextValue) => {
8828
- setInputText(nextValue);
8829
- if (debounceRef.current) clearTimeout(debounceRef.current);
8830
- debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);
8831
- };
8832
- const selectSuggestion = React14.useCallback(async (suggestion) => {
8833
- if (debounceRef.current) clearTimeout(debounceRef.current);
8834
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8835
- const label = suggestion.description.split(",")[0]?.trim() || suggestion.description;
8836
- setInputText(label);
8837
- setSuggestions([]);
8838
- setIsOpen(false);
8839
- const client = clientContext?.client;
8840
- if (!client) return;
8841
- const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);
8842
- sessionTokenRef.current = createSessionToken();
8843
- if (result.ok) {
8844
- const d = result.value;
8845
- onValueChange({
8846
- latitude: d.latitude,
8847
- longitude: d.longitude,
8848
- label: d.formatted_address || label
8849
- });
8850
- setInputText(d.formatted_address || label);
8851
- }
8852
- }, [clientContext, onValueChange]);
8853
- return /* @__PURE__ */ jsxs("div", { className: cn("relative", classNames?.locationInput), children: [
8942
+ return /* @__PURE__ */ jsxs("div", { className: cn(classNames?.locationInput), children: [
8854
8943
  /* @__PURE__ */ jsx(
8855
- "input",
8944
+ AddressAutocomplete,
8856
8945
  {
8857
- type: "text",
8858
- value: inputText,
8859
- onChange: (e) => handleInputChange(e.target.value),
8860
- onFocus: () => {
8861
- if (suggestions.length > 0) setIsOpen(true);
8862
- },
8863
- onBlur: () => {
8864
- blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120);
8865
- },
8866
- onKeyDown: (e) => {
8867
- if (e.key === "Escape") {
8868
- setIsOpen(false);
8869
- return;
8870
- }
8871
- if (!isOpen || suggestions.length === 0) return;
8872
- if (e.key === "ArrowDown") {
8873
- e.preventDefault();
8874
- setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0);
8875
- } else if (e.key === "ArrowUp") {
8876
- e.preventDefault();
8877
- setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1);
8878
- } else if (e.key === "Enter" && highlightedIndex >= 0) {
8879
- e.preventDefault();
8880
- void selectSuggestion(suggestions[highlightedIndex]);
8881
- }
8882
- },
8946
+ value: loc ? { formatted_address: loc.label ?? "", latitude: loc.latitude, longitude: loc.longitude } : null,
8947
+ onSelect: (address) => onValueChange({
8948
+ latitude: address.latitude,
8949
+ longitude: address.longitude,
8950
+ label: address.formatted_address
8951
+ }),
8883
8952
  placeholder: field.placeholder || "Search for a location...",
8884
8953
  required: field.is_required,
8885
- autoComplete: "off",
8886
- "aria-autocomplete": "list",
8887
- "aria-expanded": isOpen && suggestions.length > 0,
8888
- className: inputClass
8954
+ classNames: { input: inputClass }
8889
8955
  }
8890
8956
  ),
8891
- isOpen && suggestions.length > 0 && /* @__PURE__ */ jsx("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", children: suggestions.map((s, i) => /* @__PURE__ */ jsx(
8892
- "button",
8893
- {
8894
- type: "button",
8895
- role: "option",
8896
- "aria-selected": i === highlightedIndex,
8897
- onMouseDown: (e) => {
8898
- e.preventDefault();
8899
- void selectSuggestion(s);
8900
- },
8901
- 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"),
8902
- children: s.description
8903
- },
8904
- s.place_id
8905
- )) }),
8906
8957
  loc && /* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: [
8907
8958
  loc.latitude.toFixed(4),
8908
8959
  ", ",
@@ -9033,12 +9084,6 @@ function SignatureInput({
9033
9084
  !sig?.data_url && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: field.placeholder || "Draw your signature above" })
9034
9085
  ] });
9035
9086
  }
9036
- function createSessionToken() {
9037
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
9038
- return crypto.randomUUID();
9039
- }
9040
- return `places_${Date.now()}_${Math.random().toString(16).slice(2)}`;
9041
- }
9042
9087
  function getTimeOfDay(timeStr) {
9043
9088
  const hour = parseInt(timeStr.split("T").pop()?.split(":")[0] ?? timeStr.split(":")[0], 10);
9044
9089
  if (hour < 12) return "morning";
@@ -9284,6 +9329,7 @@ function DateSlotPicker({
9284
9329
  serviceId,
9285
9330
  daysToShow = 7,
9286
9331
  participantCount,
9332
+ units,
9287
9333
  selectedSlot,
9288
9334
  onSlotSelect,
9289
9335
  availability: availabilityProp,
@@ -9318,6 +9364,7 @@ function DateSlotPicker({
9318
9364
  dateRange.endDate,
9319
9365
  {
9320
9366
  participantCount,
9367
+ units,
9321
9368
  enabled: availabilityProp === void 0
9322
9369
  }
9323
9370
  );
@@ -9475,6 +9522,7 @@ function ProductCustomizer({
9475
9522
  const [specialInstructions, setSpecialInstructions] = useState("");
9476
9523
  const [selectedSlot, setSelectedSlot] = useState(null);
9477
9524
  const [selectedStaffId, setSelectedStaffId] = useState(null);
9525
+ const [unitsCount, setUnitsCount] = useState(1);
9478
9526
  const cart = useCart();
9479
9527
  const currency = getProductCurrency(product);
9480
9528
  const productType = product.type || "product";
@@ -9482,6 +9530,7 @@ function ProductCustomizer({
9482
9530
  const isBundle = productType === "bundle";
9483
9531
  const isDigital = productType === "digital";
9484
9532
  const isService = productType === "service";
9533
+ const offersUnits = isService && isMultiDayService(product);
9485
9534
  const isStandard = !isComposite && !isBundle;
9486
9535
  const hasVariants = isStandard && product.variants && product.variants.length > 0;
9487
9536
  const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;
@@ -9503,6 +9552,7 @@ function ProductCustomizer({
9503
9552
  setSpecialInstructions("");
9504
9553
  setSelectedSlot(null);
9505
9554
  setSelectedStaffId(null);
9555
+ setUnitsCount(1);
9506
9556
  }, [product.id, product.min_order_quantity]);
9507
9557
  const slotStaff = useMemo(
9508
9558
  () => (selectedSlot?.available_staff ?? []).map((member) => ({
@@ -9592,7 +9642,17 @@ function ProductCustomizer({
9592
9642
  if (perUnit === void 0 || perUnit === null) return void 0;
9593
9643
  return parsePrice(perUnit) * quantity;
9594
9644
  }, [quote, quantity]);
9595
- const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;
9645
+ const displayTotalPrice = useMemo(() => {
9646
+ const total = quotedTotalPrice ?? localTotalPrice;
9647
+ const multiplier = serviceBillingMultiplier(product, {
9648
+ quantity,
9649
+ units: unitsCount,
9650
+ scheduledStart: selectedSlot?.start_time,
9651
+ scheduledEnd: selectedSlot?.end_time
9652
+ });
9653
+ if (multiplier === null) return total;
9654
+ return total / Math.max(1, quantity) * multiplier;
9655
+ }, [quotedTotalPrice, localTotalPrice, product, quantity, unitsCount, selectedSlot]);
9596
9656
  const handleVariantChange = useCallback(
9597
9657
  (variantId, variant) => {
9598
9658
  setSelectedVariantId(variantId);
@@ -9624,7 +9684,8 @@ function ProductCustomizer({
9624
9684
  scheduledStart: selectedSlot.start_time,
9625
9685
  scheduledEnd: selectedSlot.end_time,
9626
9686
  staffId: selectedStaffId ?? selectedSlot.available_staff?.[0]?.staff_id,
9627
- resourceId: selectedSlot.available_resources?.[0]?.resource_id
9687
+ resourceId: selectedSlot.available_resources?.[0]?.resource_id,
9688
+ units: unitsCount > 1 ? unitsCount : void 0
9628
9689
  } : {},
9629
9690
  customerInputs: Object.keys(customerInputValues).length > 0 ? Object.entries(customerInputValues).filter(([, v]) => v !== void 0 && v !== "").map(([fieldId, value]) => ({ field_id: fieldId, value })) : void 0,
9630
9691
  specialInstructions: specialInstructions.trim() || void 0
@@ -9645,7 +9706,7 @@ function ProductCustomizer({
9645
9706
  } finally {
9646
9707
  setIsSubmitting(false);
9647
9708
  }
9648
- }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, selectedStaffId, isSubmitting, onAddToCart, cart]);
9709
+ }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, selectedStaffId, unitsCount, isSubmitting, onAddToCart, cart]);
9649
9710
  return /* @__PURE__ */ jsxs("div", { "data-cimplify-customizer": true, className: cn("space-y-6", className, classNames?.root), children: [
9650
9711
  isComposite && product.groups && product.composite_id && /* @__PURE__ */ jsx(
9651
9712
  CompositeSelector,
@@ -9682,9 +9743,17 @@ function ProductCustomizer({
9682
9743
  setSelectedStaffId(null);
9683
9744
  },
9684
9745
  participantCount: quantity,
9685
- currency
9746
+ units: offersUnits ? unitsCount : void 0,
9747
+ currency,
9748
+ schedulingMode: product.scheduling_mode ?? (offersUnits ? "multi_day" : "intraday"),
9749
+ durationUnit: product.duration_unit,
9750
+ durationValue: product.duration_value
9686
9751
  }
9687
9752
  ),
9753
+ offersUnits && /* @__PURE__ */ jsxs("div", { "data-cimplify-customizer-units": true, className: classNames?.units, children: [
9754
+ /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3", children: "Units" }),
9755
+ /* @__PURE__ */ jsx(QuantitySelector, { value: unitsCount, onChange: setUnitsCount, min: 1 })
9756
+ ] }),
9688
9757
  isService && slotStaff.length > 0 && /* @__PURE__ */ jsxs("div", { "data-cimplify-customizer-staff": true, children: [
9689
9758
  /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3", children: "Choose your specialist" }),
9690
9759
  /* @__PURE__ */ jsx(
@@ -11738,18 +11807,18 @@ function RentalServiceCard({
11738
11807
  const currency = getProductCurrency(product);
11739
11808
  const hasDeposit = product.deposit_type && product.deposit_type !== "none" && product.deposit_amount;
11740
11809
  const unit = product.duration_unit ? UNIT_LABELS2[product.duration_unit] || product.duration_unit : "day";
11810
+ const priceUnit = product.price_basis === "per_person" ? "guest" : product.price_basis === "flat" ? "" : unit;
11741
11811
  const status = product.inventory_status;
11742
11812
  return /* @__PURE__ */ jsxs(CardShell, { product, renderLink, className, children: [
11743
11813
  /* @__PURE__ */ jsxs(CardImage, { src: image, alt: product.name, aspectRatio: "16/9", renderImage, children: [
11744
11814
  /* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" }),
11745
- /* @__PURE__ */ jsxs("span", { className: "absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm", children: [
11815
+ priceUnit && /* @__PURE__ */ jsxs("span", { className: "absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm", children: [
11746
11816
  "Per ",
11747
- unit
11817
+ priceUnit
11748
11818
  ] }),
11749
11819
  /* @__PURE__ */ jsxs("span", { className: "absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm", children: [
11750
11820
  /* @__PURE__ */ jsx(Price, { amount: product.default_price, currency }),
11751
- "/",
11752
- unit
11821
+ priceUnit && `/${priceUnit}`
11753
11822
  ] }),
11754
11823
  product.general_service_capacity != null && product.general_service_capacity > 1 && /* @__PURE__ */ jsxs("span", { className: "absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-blue-50/90 text-blue-700 border border-blue-200/50 backdrop-blur px-2 py-0.5 rounded-md", children: [
11755
11824
  "Up to ",
@@ -12343,6 +12412,15 @@ function ProductGrid({
12343
12412
  ] });
12344
12413
  }
12345
12414
 
12415
+ // src/react/utils/attachment.ts
12416
+ var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".svg"];
12417
+ function isImageAttachment(attachment) {
12418
+ if (attachment.mime_type?.startsWith("image/")) return true;
12419
+ if (attachment.mime_type) return false;
12420
+ const path = attachment.url.split("?")[0].split("#")[0].toLowerCase();
12421
+ return IMAGE_EXTENSIONS.some((extension) => path.endsWith(extension));
12422
+ }
12423
+
12346
12424
  // src/react/utils/boundary-labels.ts
12347
12425
  var PRESET_LABELS = {
12348
12426
  check_in_out: { start: "Check-in", end: "Check-out" },
@@ -13524,6 +13602,7 @@ function BookingPage({
13524
13602
  const [selectedDate, setSelectedDate] = useState(null);
13525
13603
  const [selectedStaffId, setSelectedStaffId] = useState(null);
13526
13604
  const [selectedResourceId, setSelectedResourceId] = useState(null);
13605
+ const [bookingUnits, setBookingUnits] = useState(1);
13527
13606
  const [isSubmitting, setIsSubmitting] = useState(false);
13528
13607
  const [error, setError] = useState(null);
13529
13608
  const hasResourceStep = resources && resources.length > 0;
@@ -13617,7 +13696,8 @@ function BookingPage({
13617
13696
  scheduledStart: selectedSlot.start_time,
13618
13697
  scheduledEnd: selectedSlot.end_time,
13619
13698
  staffId: selectedStaffId || void 0,
13620
- resourceId: selectedResourceId || void 0
13699
+ resourceId: selectedResourceId || void 0,
13700
+ units: bookingUnits > 1 ? bookingUnits : void 0
13621
13701
  });
13622
13702
  onBooked?.(selectedSlot, selectedStaffId);
13623
13703
  }
@@ -13713,6 +13793,31 @@ function BookingPage({
13713
13793
  ] }),
13714
13794
  step === STEP.CONFIRM && selectedSlot && /* @__PURE__ */ jsxs("div", { "data-cimplify-booking-step": "confirm", className: classNames?.step, children: [
13715
13795
  /* @__PURE__ */ jsx("h2", { "data-cimplify-booking-step-title": true, className: classNames?.stepTitle, children: "Confirm your booking" }),
13796
+ /* @__PURE__ */ jsxs("div", { "data-cimplify-booking-units": true, className: classNames?.summaryRow, children: [
13797
+ /* @__PURE__ */ jsx("span", { children: "Units" }),
13798
+ /* @__PURE__ */ jsxs("div", { "data-cimplify-booking-units-stepper": true, children: [
13799
+ /* @__PURE__ */ jsx(
13800
+ "button",
13801
+ {
13802
+ type: "button",
13803
+ "data-cimplify-booking-units-decrement": true,
13804
+ onClick: () => setBookingUnits((current) => Math.max(1, current - 1)),
13805
+ disabled: bookingUnits <= 1,
13806
+ children: "\u2212"
13807
+ }
13808
+ ),
13809
+ /* @__PURE__ */ jsx("span", { "data-cimplify-booking-units-value": true, children: bookingUnits }),
13810
+ /* @__PURE__ */ jsx(
13811
+ "button",
13812
+ {
13813
+ type: "button",
13814
+ "data-cimplify-booking-units-increment": true,
13815
+ onClick: () => setBookingUnits((current) => current + 1),
13816
+ children: "+"
13817
+ }
13818
+ )
13819
+ ] })
13820
+ ] }),
13716
13821
  /* @__PURE__ */ jsxs("div", { "data-cimplify-booking-summary": true, className: classNames?.summary, children: [
13717
13822
  /* @__PURE__ */ jsxs("div", { "data-cimplify-booking-summary-row": true, className: classNames?.summaryRow, children: [
13718
13823
  /* @__PURE__ */ jsx("span", { children: "Service" }),
@@ -16971,7 +17076,7 @@ function ChatWidget({
16971
17076
  const context = useOptionalCimplifyClient();
16972
17077
  const client = clientProp ?? context?.client;
16973
17078
  const [open, setOpen] = useState(defaultOpen);
16974
- const [unread, setUnread] = useState(0);
17079
+ const [readCount, setReadCount] = useState(0);
16975
17080
  const {
16976
17081
  messages,
16977
17082
  isLoading,
@@ -16980,6 +17085,8 @@ function ChatWidget({
16980
17085
  seenAt,
16981
17086
  error,
16982
17087
  isActive,
17088
+ claimedAs,
17089
+ signOut,
16983
17090
  send,
16984
17091
  react,
16985
17092
  startConversation
@@ -17001,22 +17108,14 @@ function ChatWidget({
17001
17108
  },
17002
17109
  [client, send]
17003
17110
  );
17004
- const prevCountRef = useRef(messages.length);
17005
- useEffect(() => {
17006
- if (!open && messages.length > prevCountRef.current) {
17007
- const newMessages = messages.slice(prevCountRef.current);
17008
- const incomingCount = newMessages.filter(
17009
- (m) => m.sender_type !== "customer"
17010
- ).length;
17011
- if (incomingCount > 0) setUnread((n) => n + incomingCount);
17012
- }
17013
- prevCountRef.current = messages.length;
17014
- }, [messages, open]);
17111
+ const unread = open ? 0 : messages.slice(readCount).filter((m) => m.sender_type !== "customer").length;
17015
17112
  const handleOpen = useCallback(() => {
17016
17113
  setOpen(true);
17017
- setUnread(0);
17018
17114
  }, []);
17019
- const handleClose = useCallback(() => setOpen(false), []);
17115
+ const handleClose = useCallback(() => {
17116
+ setReadCount(messages.length);
17117
+ setOpen(false);
17118
+ }, [messages.length]);
17020
17119
  const isLeft = position === "bottom-left";
17021
17120
  return /* @__PURE__ */ jsxs(
17022
17121
  "div",
@@ -17052,6 +17151,8 @@ function ChatWidget({
17052
17151
  error,
17053
17152
  isActive,
17054
17153
  isLeft,
17154
+ claimedAs,
17155
+ onSignOut: () => void signOut(),
17055
17156
  onClose: handleClose,
17056
17157
  onSend: send,
17057
17158
  onReact: react,
@@ -17113,6 +17214,8 @@ function Panel({
17113
17214
  error,
17114
17215
  isActive,
17115
17216
  isLeft,
17217
+ claimedAs,
17218
+ onSignOut,
17116
17219
  onClose,
17117
17220
  onSend,
17118
17221
  onReact,
@@ -17136,6 +17239,8 @@ function Panel({
17136
17239
  Header,
17137
17240
  {
17138
17241
  businessName,
17242
+ claimedAs,
17243
+ onSignOut,
17139
17244
  onClose,
17140
17245
  className: classNames?.header
17141
17246
  }
@@ -17182,6 +17287,8 @@ function Panel({
17182
17287
  }
17183
17288
  function Header({
17184
17289
  businessName,
17290
+ claimedAs,
17291
+ onSignOut,
17185
17292
  onClose,
17186
17293
  className
17187
17294
  }) {
@@ -17196,7 +17303,22 @@ function Header({
17196
17303
  /* @__PURE__ */ jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] bg-white/15", children: /* @__PURE__ */ jsx(ChatIcon, { className: "h-5 w-5" }) }),
17197
17304
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
17198
17305
  /* @__PURE__ */ jsx("div", { className: "truncate text-[15px] font-semibold leading-tight", children: businessName }),
17199
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-white/60", children: [
17306
+ claimedAs ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 truncate text-xs text-white/60", children: [
17307
+ /* @__PURE__ */ jsxs("span", { className: "truncate", children: [
17308
+ "Chatting as ",
17309
+ claimedAs
17310
+ ] }),
17311
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
17312
+ /* @__PURE__ */ jsx(
17313
+ "button",
17314
+ {
17315
+ type: "button",
17316
+ onClick: onSignOut,
17317
+ className: "flex-shrink-0 underline underline-offset-2 hover:text-white",
17318
+ children: "Not you?"
17319
+ }
17320
+ )
17321
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs text-white/60", children: [
17200
17322
  /* @__PURE__ */ jsx("span", { className: "h-[7px] w-[7px] flex-shrink-0 rounded-full bg-emerald-400" }),
17201
17323
  "Online"
17202
17324
  ] })
@@ -17280,10 +17402,6 @@ function MessageList({
17280
17402
  onReact,
17281
17403
  className
17282
17404
  }) {
17283
- const bottomRef = useRef(null);
17284
- useEffect(() => {
17285
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
17286
- }, [messages.length, isSending]);
17287
17405
  const { bubbles, reactionsByTarget } = partitionMessages(messages);
17288
17406
  const seenMessageId = lastSeenCustomerMessageId(bubbles, seenAt);
17289
17407
  return /* @__PURE__ */ jsxs(
@@ -17403,7 +17521,15 @@ function MessageList({
17403
17521
  ] }, msg.id);
17404
17522
  }),
17405
17523
  isSending && /* @__PURE__ */ jsx("div", { className: "mr-auto mt-1", children: /* @__PURE__ */ jsx(TypingDots, {}) }),
17406
- /* @__PURE__ */ jsx("div", { ref: bottomRef })
17524
+ /* @__PURE__ */ jsx(
17525
+ "div",
17526
+ {
17527
+ ref: (node) => {
17528
+ node?.scrollIntoView({ behavior: "smooth" });
17529
+ }
17530
+ },
17531
+ `${messages.length}:${isSending}`
17532
+ )
17407
17533
  ]
17408
17534
  }
17409
17535
  );
@@ -19450,4 +19576,4 @@ async function genUIFingerprint(manifest) {
19450
19576
  return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("").slice(0, 16);
19451
19577
  }
19452
19578
 
19453
- export { AccommodationCard, AccountContent, AccountDashboardPage, AccountHero, AccountIdentityStrip, AccountLineItem, AccountLineItems, AccountNav, AccountOrderDetailPage, AccountOrderView, AccountOrdersPage, AccountShell, AccountSidebar, AccountSignedOutPrompt, AccountSubscriptionDetailPage, AccountSubscriptionsPage, Ad, AdProvider, AddOnSelector, AddressElement, AuthElement, AvailabilityBadge, BackLink, BillingPlanSelector, BookingCard, BookingList, BookingPage, BookingSummary, BookingsPage, BoxIcon24, BundleProductCard, BundleProductLayout, BundleSelector, Button, CIMPLIFY_GENUI_REGISTRY, CardImage, CardShell, CardVariant, CartDrawer, CartDrawerProvider, CartPage, CartSummary, CartTemplate, CatalogueCollectionLayout, CataloguePage, CatalogueTemplate, CategoryFilter, CategoryGrid, ChatMarkdown, ChatWidget, CheckIcon, CheckoutPage, CimplifyCheckout, CimplifyProvider, CimplifySignInButton, CimplifyUserButton, CollectionPage, CollectionTemplate, CompactCartLayout, CompactCatalogueLayout, CompactSearchLayout, CompactServiceCard, CompositeProductCard, CompositeProductLayout, CompositeSelector, ContextCard, ContinuityCard, CurrencySelector, CustomAttributesTable, CustomerInputFields, DashboardIcon, DashboardIcon20, DatePicker, DateSlotPicker, DealBanner, DealsPage, DefaultCartLayout, DefaultCatalogueLayout, DefaultCollectionLayout, DefaultProductLayout, DefaultSearchLayout, DeliveryEstimate, DeliveryTracker, DeliveryTruckIcon, DigitalProductCard, DigitalProductLayout, DiscountInput, ElementsProvider, EmptyState, ErrorState, FeaturedCollectionLayout, FilterTabs, FoodProductCard, FoodProductLayout, GenUIView, HappeningCard, IdentityIcon, IdentityOfframp, InventoryBadge, ItemsCard, LeaseServiceCard, LineItemDetails, LocationPicker, MediaGallery, MetadataStringList, MobileAccountShell, MobileBackButton, MobileMoreButton, MobileTabBar, NextDeliveryCard, OrderDetailHeader, OrderDetailMetaPipe, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderList, OrderRow, OrderSummary, OrderTimeline, OrderTotals, OrdersIcon, OrdersIcon20, OvenIcon, PackageIcon, PaymentElement, Price, PriceRange, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductModel3D, ProductPage, ProductSheet, ProductTemplate, PropertiesTable, QuantitySelector, QuickAddButton, RecentlyViewed, RecommendationCarousel, RelatedProductsSection, RentalServiceCard, ResourcePicker, RetailProductCard, RichText, SaleBadge, SalePrice, SaleTeaser, ScheduleServiceCard, SearchInput, SearchPage, SearchTemplate, Section, SectionMore, ServiceProductLayout, SessionMessageBanner, SignOutIcon, Skeleton, SlotPicker, SoldOutOverlay, StaffPicker, StandardServiceCard, Status, StoreNav, StoreVideo, SubscriptionCard, SubscriptionList, SubscriptionRow, SubscriptionsIcon, SubscriptionsIcon20, TagPills, TimePicker, Toast, TwoColumnGrid, VariantSelector, VolumePricing, WelcomeCard, WholesaleProductCard, WholesaleProductLayout, WishlistButton, clearOrdersCache, cn, defaultBoundaryKind, genUICatalog, genUIFingerprint, genUIManifest, genUIManifestJson, getBoundaryLabels, isTerminalOrderStatus, parse as parseGenUIProgram, partitionMessages, prefetchProduct, prefetchProducts, resolveBoundaryKind, roomToResource, sanitizeRichTextHtml, stripHtmlToText, toAccountLineItem, toAccountLineItems, toAccountOrderView, useActivityState, useAds, useAttributeDefinitions, useAvailableSlots, useBillingPlans, useBookings, useBootstrap, useBundles, useCart, useCartDrawer, useCategories, useChat, useCheckout, useCimplify, useCimplifyClient, useCimplifySession, useCollection, useCollections, useDeals, useDeliveryFee, useDeliveryOptions, useElements, useElementsReady, useFxRate, useGenUIProgram, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, usePropertyFacets, useQuote, useRecommendations, useSearch, useServiceAvailability, useServices, useStoreCredit, useSubscription, useSubscriptions, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount, useVariantSelector };
19579
+ export { AccommodationCard, AccountContent, AccountDashboardPage, AccountHero, AccountIdentityStrip, AccountLineItem, AccountLineItems, AccountNav, AccountOrderDetailPage, AccountOrderView, AccountOrdersPage, AccountShell, AccountSidebar, AccountSignedOutPrompt, AccountSubscriptionDetailPage, AccountSubscriptionsPage, Ad, AdProvider, AddOnSelector, AddressAutocomplete, AddressElement, AuthElement, AvailabilityBadge, BackLink, BillingPlanSelector, BookingCard, BookingList, BookingPage, BookingSummary, BookingsPage, BoxIcon24, BundleProductCard, BundleProductLayout, BundleSelector, Button, CIMPLIFY_GENUI_REGISTRY, CardImage, CardShell, CardVariant, CartDrawer, CartDrawerProvider, CartPage, CartSummary, CartTemplate, CatalogueCollectionLayout, CataloguePage, CatalogueTemplate, CategoryFilter, CategoryGrid, ChatMarkdown, ChatWidget, CheckIcon, CheckoutPage, CimplifyCheckout, CimplifyProvider, CimplifySignInButton, CimplifyUserButton, CollectionPage, CollectionTemplate, CompactCartLayout, CompactCatalogueLayout, CompactSearchLayout, CompactServiceCard, CompositeProductCard, CompositeProductLayout, CompositeSelector, ContextCard, ContinuityCard, CurrencySelector, CustomAttributesTable, CustomerInputFields, DashboardIcon, DashboardIcon20, DatePicker, DateSlotPicker, DealBanner, DealsPage, DefaultCartLayout, DefaultCatalogueLayout, DefaultCollectionLayout, DefaultProductLayout, DefaultSearchLayout, DeliveryEstimate, DeliveryTracker, DeliveryTruckIcon, DigitalProductCard, DigitalProductLayout, DiscountInput, ElementsProvider, EmptyState, ErrorState, FeaturedCollectionLayout, FilterTabs, FoodProductCard, FoodProductLayout, GenUIView, HappeningCard, IdentityIcon, IdentityOfframp, InventoryBadge, ItemsCard, LeaseServiceCard, LineItemDetails, LocationPicker, MediaGallery, MetadataStringList, MobileAccountShell, MobileBackButton, MobileMoreButton, MobileTabBar, NextDeliveryCard, OrderDetailHeader, OrderDetailMetaPipe, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderList, OrderRow, OrderSummary, OrderTimeline, OrderTotals, OrdersIcon, OrdersIcon20, OvenIcon, PackageIcon, PaymentElement, Price, PriceRange, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductModel3D, ProductPage, ProductSheet, ProductTemplate, PropertiesTable, QuantitySelector, QuickAddButton, RecentlyViewed, RecommendationCarousel, RelatedProductsSection, RentalServiceCard, ResourcePicker, RetailProductCard, RichText, SaleBadge, SalePrice, SaleTeaser, ScheduleServiceCard, SearchInput, SearchPage, SearchTemplate, Section, SectionMore, ServiceProductLayout, SessionMessageBanner, SignOutIcon, Skeleton, SlotPicker, SoldOutOverlay, StaffPicker, StandardServiceCard, Status, StoreNav, StoreVideo, SubscriptionCard, SubscriptionList, SubscriptionRow, SubscriptionsIcon, SubscriptionsIcon20, TagPills, TimePicker, Toast, TwoColumnGrid, VariantSelector, VolumePricing, WelcomeCard, WholesaleProductCard, WholesaleProductLayout, WishlistButton, clearOrdersCache, cn, createPlacesSessionToken, defaultBoundaryKind, genUICatalog, genUIFingerprint, genUIManifest, genUIManifestJson, getBoundaryLabels, isImageAttachment, isTerminalOrderStatus, parse as parseGenUIProgram, partitionMessages, prefetchProduct, prefetchProducts, resolveBoundaryKind, roomToResource, sanitizeRichTextHtml, stripHtmlToText, toAccountLineItem, toAccountLineItems, toAccountOrderView, useActivityState, useAds, useAttributeDefinitions, useAvailableSlots, useBillingPlans, useBookings, useBootstrap, useBundles, useCart, useCartDrawer, useCategories, useChat, useCheckout, useCimplify, useCimplifyClient, useCimplifySession, useCollection, useCollections, useDeals, useDeliveryFee, useDeliveryOptions, useElements, useElementsReady, useFxRate, useGenUIProgram, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, usePropertyFacets, useQuote, useRecommendations, useSearch, useServiceAvailability, useServices, useStoreCredit, useSubscription, useSubscriptions, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount, useVariantSelector };