@patientos/website-kit 0.2.8 → 0.2.10

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.
@@ -5,7 +5,7 @@ import {
5
5
  UnsupportedCartLineList,
6
6
  quoteSourceQuantities,
7
7
  useCartQuote
8
- } from "./chunk-IOC6QLB7.js";
8
+ } from "./chunk-LW2LLAL4.js";
9
9
  import {
10
10
  createStoreClient
11
11
  } from "./chunk-7BUDUYXN.js";
@@ -24,7 +24,8 @@ function CartClient({ storeApiOrigin }) {
24
24
  );
25
25
  const [cart, setCart] = React.useState(() => readCart());
26
26
  const [mutationUnavailable, setMutationUnavailable] = React.useState(false);
27
- const { quote, quoteSourceCart, failed } = useCartQuote(client, cart);
27
+ const { quote, quoteSourceCart, failed, pending } = useCartQuote(client, cart);
28
+ const liveQuantities = React.useMemo(() => quoteSourceQuantities(cart), [cart]);
28
29
  async function runMutation(mutation) {
29
30
  const result = await mutation;
30
31
  setMutationUnavailable(result.status === "cart_storage_unavailable");
@@ -61,6 +62,8 @@ function CartClient({ storeApiOrigin }) {
61
62
  {
62
63
  lines: quote.lines,
63
64
  sourceQuantities: quoteSourceQuantities(quoteSourceCart),
65
+ liveQuantities,
66
+ pending,
64
67
  onMutate: (m) => void runMutation(m)
65
68
  }
66
69
  ),
@@ -3,16 +3,16 @@ import {
3
3
  CertificateFunnelClient,
4
4
  PortalAccountClient,
5
5
  PortalPanelClient
6
- } from "./chunk-4GKSSM5N.js";
6
+ } from "./chunk-SYXA5WP7.js";
7
7
  import {
8
8
  mountIslandsWith
9
9
  } from "./chunk-THMT43MV.js";
10
10
  import {
11
11
  CartClient
12
- } from "./chunk-2CZFJOKK.js";
12
+ } from "./chunk-55W3KKJD.js";
13
13
  import {
14
14
  CartButtonClient
15
- } from "./chunk-ZNH6TSYP.js";
15
+ } from "./chunk-Q5DIBKWX.js";
16
16
  import {
17
17
  CheckoutClient
18
18
  } from "./chunk-S3ZQQOQT.js";
@@ -50,9 +50,11 @@ function UnsupportedCartLineList({
50
50
  function CartLineList({
51
51
  lines,
52
52
  sourceQuantities,
53
- onMutate
53
+ onMutate,
54
+ liveQuantities,
55
+ pending
54
56
  }) {
55
- return /* @__PURE__ */ jsx("ul", { className: "sk-cart__lines", children: lines.map((line) => /* @__PURE__ */ jsxs("li", { className: "sk-cart__line", children: [
57
+ return /* @__PURE__ */ jsx("ul", { className: "sk-cart__lines", "aria-busy": pending || void 0, children: lines.map((line) => /* @__PURE__ */ jsxs("li", { className: "sk-cart__line", children: [
56
58
  /* @__PURE__ */ jsx("a", { className: "sk-cart__line-title", href: `/store/${line.productHandle}`, children: line.title }),
57
59
  /* @__PURE__ */ jsxs("span", { className: "sk-cart__line-unit", children: [
58
60
  formatMoney(line.unitPrice),
@@ -75,7 +77,7 @@ function CartLineList({
75
77
  children: "\u2212"
76
78
  }
77
79
  ),
78
- /* @__PURE__ */ jsx("span", { "aria-live": "polite", children: line.quantity }),
80
+ /* @__PURE__ */ jsx("span", { className: "sk-cart__qty-value", "aria-live": "polite", children: (pending ? liveQuantities?.get(line.variantId) : void 0) ?? line.quantity }),
79
81
  /* @__PURE__ */ jsx(
80
82
  "button",
81
83
  {
@@ -125,22 +127,38 @@ function useCartQuote(client, cart, enabled = true) {
125
127
  const [state, setState] = React.useState({
126
128
  quote: null,
127
129
  quoteSourceCart: null,
128
- failed: false
130
+ failed: false,
131
+ pending: false
129
132
  });
130
133
  React.useEffect(() => {
131
134
  if (!enabled || cart.lines.length === 0) {
132
- setState({ quote: null, quoteSourceCart: null, failed: false });
135
+ setState({
136
+ quote: null,
137
+ quoteSourceCart: null,
138
+ failed: false,
139
+ pending: false
140
+ });
133
141
  return;
134
142
  }
135
143
  let alive = true;
136
- setState({ quote: null, quoteSourceCart: null, failed: false });
144
+ setState((previous) => ({ ...previous, failed: false, pending: true }));
137
145
  void client.post("/api/store/quote", { lines: cartToWireLines(cart) }).then((result) => {
138
146
  if (!alive) return;
139
147
  if (!result.ok) {
140
- setState({ quote: null, quoteSourceCart: null, failed: true });
148
+ setState({
149
+ quote: null,
150
+ quoteSourceCart: null,
151
+ failed: true,
152
+ pending: false
153
+ });
141
154
  return;
142
155
  }
143
- setState({ quote: result.data, quoteSourceCart: cart, failed: false });
156
+ setState({
157
+ quote: result.data,
158
+ quoteSourceCart: cart,
159
+ failed: false,
160
+ pending: false
161
+ });
144
162
  });
145
163
  return () => {
146
164
  alive = false;
@@ -5,7 +5,7 @@ import {
5
5
  UnsupportedCartLineList,
6
6
  quoteSourceQuantities,
7
7
  useCartQuote
8
- } from "./chunk-IOC6QLB7.js";
8
+ } from "./chunk-LW2LLAL4.js";
9
9
  import {
10
10
  createStoreClient
11
11
  } from "./chunk-7BUDUYXN.js";
@@ -87,7 +87,12 @@ function CartDrawer({
87
87
  window.addEventListener("keydown", onKeyDown);
88
88
  return () => window.removeEventListener("keydown", onKeyDown);
89
89
  }, [open, onClose]);
90
- const { quote, quoteSourceCart, failed } = useCartQuote(client, cart, open);
90
+ const { quote, quoteSourceCart, failed, pending } = useCartQuote(
91
+ client,
92
+ cart,
93
+ open
94
+ );
95
+ const liveQuantities = React.useMemo(() => quoteSourceQuantities(cart), [cart]);
91
96
  async function runMutation(mutation) {
92
97
  const result = await mutation;
93
98
  setMutationUnavailable(result.status === "cart_storage_unavailable");
@@ -144,6 +149,8 @@ function CartDrawer({
144
149
  {
145
150
  lines: quote.lines,
146
151
  sourceQuantities: quoteSourceQuantities(quoteSourceCart),
152
+ liveQuantities,
153
+ pending,
147
154
  onMutate: (m) => void runMutation(m)
148
155
  }
149
156
  ),
@@ -11062,6 +11062,23 @@ function PortalConsultPanel({
11062
11062
  ] });
11063
11063
  }
11064
11064
 
11065
+ // src/portal-account-cache.ts
11066
+ var snapshots = /* @__PURE__ */ new Map();
11067
+ function rememberPortalAccount(surface, state) {
11068
+ if (state.kind === "signed-in") {
11069
+ snapshots.set(surface, state);
11070
+ return;
11071
+ }
11072
+ if (state.kind === "loading" || state.kind === "link-sent") return;
11073
+ clearPortalAccountCache();
11074
+ }
11075
+ function recallPortalAccount(surface) {
11076
+ return snapshots.get(surface) ?? null;
11077
+ }
11078
+ function clearPortalAccountCache() {
11079
+ snapshots.clear();
11080
+ }
11081
+
11065
11082
  // src/portal-account.client.tsx
11066
11083
  import { Fragment as Fragment11, jsx as jsx26, jsxs as jsxs23 } from "react/jsx-runtime";
11067
11084
  var APPOINTMENTS_URL = "/portal/api/appointments";
@@ -11220,21 +11237,36 @@ function PortalAccountSurfaceClient({
11220
11237
  const portalClient = usePortalClient();
11221
11238
  const fetchImpl = React21.useMemo(() => portalFetchForClient(portalClient), [portalClient]);
11222
11239
  const [state, setState] = React21.useState(
11223
- // `readPortalSignedInHint` is in the initialiser (not an effect) on purpose: the
11240
+ // STALE-WHILE-REVALIDATE (PAT-949). Three ways to start, in falling order of how
11241
+ // much we already know:
11242
+ // 1. the caller drove it (tests, a future SSR seed);
11243
+ // 2. this document already resolved this surface — render that body NOW and
11244
+ // revalidate behind it, so returning to a surface is not a redraw. In-memory
11245
+ // only; see `./portal-account-cache` for the PHI rules;
11246
+ // 3. genuinely cold — a skeleton, shaped like the rows about to land.
11247
+ // `readPortalSignedInHint` stays in the initialiser (not an effect) on purpose: the
11224
11248
  // whole point is that the FIRST paint already knows, so there is no placeholder
11225
11249
  // swap. It reads one boolean out of sessionStorage — no PHI, no network.
11226
- () => initialState ?? { kind: "loading", hinted: readPortalSignedInHint() }
11250
+ () => initialState ?? recallPortalAccount(surface) ?? { kind: "loading", hinted: readPortalSignedInHint() }
11227
11251
  );
11228
11252
  const [signingOut, setSigningOut] = React21.useState(false);
11229
- const commit = React21.useCallback((next) => {
11230
- writePortalSignedInHint(next.kind === "signed-in" || next.kind === "not-available");
11231
- setState(next);
11232
- }, []);
11253
+ const [revalidating, setRevalidating] = React21.useState(false);
11254
+ const commit = React21.useCallback(
11255
+ (next) => {
11256
+ writePortalSignedInHint(next.kind === "signed-in" || next.kind === "not-available");
11257
+ rememberPortalAccount(surface, next);
11258
+ setState(next);
11259
+ },
11260
+ [surface]
11261
+ );
11233
11262
  React21.useEffect(() => {
11234
11263
  if (initialState) return;
11235
11264
  let live = true;
11265
+ setRevalidating(true);
11236
11266
  void loadPortalAccount(surface, fetchImpl).then((next) => {
11237
- if (live) commit(next);
11267
+ if (!live) return;
11268
+ setRevalidating(false);
11269
+ commit(next);
11238
11270
  });
11239
11271
  return () => {
11240
11272
  live = false;
@@ -11246,6 +11278,7 @@ function PortalAccountSurfaceClient({
11246
11278
  async function onSignOut() {
11247
11279
  if (signingOut) return;
11248
11280
  setSigningOut(true);
11281
+ clearPortalAccountCache();
11249
11282
  await signOutOfPortal(fetchImpl);
11250
11283
  const next = await loadPortalAccount(surface, fetchImpl);
11251
11284
  setSigningOut(false);
@@ -11253,91 +11286,119 @@ function PortalAccountSurfaceClient({
11253
11286
  }
11254
11287
  function replaceData(data) {
11255
11288
  const refetched = refetchedGivenName(data);
11256
- setState(
11257
- (prev) => prev.kind === "signed-in" ? { ...prev, data, givenName: refetched === void 0 ? prev.givenName : refetched } : prev
11258
- );
11289
+ setState((prev) => {
11290
+ if (prev.kind !== "signed-in") return prev;
11291
+ const next = {
11292
+ ...prev,
11293
+ data,
11294
+ givenName: refetched === void 0 ? prev.givenName : refetched
11295
+ };
11296
+ rememberPortalAccount(surface, next);
11297
+ return next;
11298
+ });
11259
11299
  }
11260
- return /* @__PURE__ */ jsxs23("section", { className: "sk-portal-account__body", "aria-busy": state.kind === "loading", children: [
11261
- /* @__PURE__ */ jsx26("h2", { className: "sk-portal-account__heading", children: heading2 }),
11262
- state.kind === "loading" && !state.hinted ? /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__placeholder", children: "Checking your sign-in\u2026" }) : null,
11263
- state.kind === "loading" && state.hinted ? /* @__PURE__ */ jsx26(PortalAccountSkeleton, {}) : null,
11264
- state.kind === "link-out" ? /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__placeholder", children: /* @__PURE__ */ jsx26("a", { className: "sk-portal-link", href: base2, children: "Sign in to your patient portal" }) }) : null,
11265
- state.kind === "signed-out" ? /* @__PURE__ */ jsx26(
11266
- PortalSignInRamp,
11267
- {
11268
- idSuffix: `account-${surface}`,
11269
- submit: (email) => portalClient.requestMagicLink(email).then((ok) => ({ ok })),
11270
- onResult: ({ ok, email }) => setState(ok ? { kind: "link-sent", email } : { kind: "link-out" })
11271
- }
11272
- ) : null,
11273
- state.kind === "link-sent" ? /* @__PURE__ */ jsx26(PortalLinkSentNotice, { email: state.email }) : null,
11274
- state.kind === "not-available" || state.kind === "signed-in" ? /* @__PURE__ */ jsxs23("div", { className: "sk-portal-account__header", children: [
11275
- /* @__PURE__ */ jsxs23("span", { className: "sk-portal-account__who", children: [
11276
- "Signed in as ",
11277
- state.givenName ?? "you"
11278
- ] }),
11279
- /* @__PURE__ */ jsx26(
11280
- "button",
11281
- {
11282
- type: "button",
11283
- className: "sk-portal-button sk-portal-button--ghost",
11284
- onClick: () => void onSignOut(),
11285
- disabled: signingOut,
11286
- children: signingOut ? "Signing out\u2026" : "Sign out"
11287
- }
11288
- )
11289
- ] }) : null,
11290
- state.kind === "not-available" ? /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__note", role: "note", children: "This isn't available online at this clinic." }) : null,
11291
- state.kind === "signed-in" && surface === "appointments" ? /* @__PURE__ */ jsx26(
11292
- PortalAccountAppointments,
11293
- {
11294
- data: state.data,
11295
- base: base2,
11296
- bookHref: book,
11297
- fetchImpl,
11298
- onReloaded: replaceData
11299
- }
11300
- ) : null,
11301
- state.kind === "signed-in" && surface === "addresses" ? /* @__PURE__ */ jsx26(
11302
- PortalAccountAddresses,
11303
- {
11304
- data: state.data,
11305
- client: portalClient,
11306
- fetchImpl,
11307
- onReloaded: replaceData
11308
- }
11309
- ) : null,
11310
- state.kind === "signed-in" && surface === "documents" ? /* @__PURE__ */ jsx26(
11311
- PortalAccountDocuments,
11312
- {
11313
- data: state.data,
11314
- fetchImpl,
11315
- openUrl
11316
- }
11317
- ) : null,
11318
- state.kind === "signed-in" && surface === "orders" ? /* @__PURE__ */ jsx26(
11319
- PortalAccountOrders,
11320
- {
11321
- data: state.data,
11322
- fetchImpl,
11323
- openUrl
11324
- }
11325
- ) : null,
11326
- state.kind === "signed-in" && surface === "profile" ? /* @__PURE__ */ jsx26(
11327
- PortalAccountProfile,
11328
- {
11329
- data: state.data,
11330
- fetchImpl,
11331
- onSaved: replaceData
11332
- }
11333
- ) : null
11334
- ] });
11300
+ return /* @__PURE__ */ jsxs23(
11301
+ "section",
11302
+ {
11303
+ className: "sk-portal-account__body",
11304
+ "aria-busy": state.kind === "loading" || revalidating,
11305
+ children: [
11306
+ /* @__PURE__ */ jsx26("h2", { className: "sk-portal-account__heading", children: heading2 }),
11307
+ state.kind === "loading" && !state.hinted ? /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__placeholder", role: "status", children: "Checking your sign-in\u2026" }) : null,
11308
+ state.kind === "loading" && state.hinted ? /* @__PURE__ */ jsx26(PortalAccountSkeleton, { surface }) : null,
11309
+ state.kind === "link-out" ? /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__placeholder", children: /* @__PURE__ */ jsx26("a", { className: "sk-portal-link", href: base2, children: "Sign in to your patient portal" }) }) : null,
11310
+ state.kind === "signed-out" ? /* @__PURE__ */ jsx26(
11311
+ PortalSignInRamp,
11312
+ {
11313
+ idSuffix: `account-${surface}`,
11314
+ submit: (email) => portalClient.requestMagicLink(email).then((ok) => ({ ok })),
11315
+ onResult: ({ ok, email }) => setState(ok ? { kind: "link-sent", email } : { kind: "link-out" })
11316
+ }
11317
+ ) : null,
11318
+ state.kind === "link-sent" ? /* @__PURE__ */ jsx26(PortalLinkSentNotice, { email: state.email }) : null,
11319
+ state.kind === "not-available" || state.kind === "signed-in" ? /* @__PURE__ */ jsxs23("div", { className: "sk-portal-account__header", children: [
11320
+ /* @__PURE__ */ jsxs23("span", { className: "sk-portal-account__who", children: [
11321
+ "Signed in as ",
11322
+ state.givenName ?? "you"
11323
+ ] }),
11324
+ /* @__PURE__ */ jsx26(
11325
+ "button",
11326
+ {
11327
+ type: "button",
11328
+ className: "sk-portal-button sk-portal-button--ghost",
11329
+ onClick: () => void onSignOut(),
11330
+ disabled: signingOut,
11331
+ children: signingOut ? "Signing out\u2026" : "Sign out"
11332
+ }
11333
+ )
11334
+ ] }) : null,
11335
+ state.kind === "not-available" ? /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__note", role: "note", children: "This isn't available online at this clinic." }) : null,
11336
+ state.kind === "signed-in" && surface === "appointments" ? /* @__PURE__ */ jsx26(
11337
+ PortalAccountAppointments,
11338
+ {
11339
+ data: state.data,
11340
+ base: base2,
11341
+ bookHref: book,
11342
+ fetchImpl,
11343
+ onReloaded: replaceData
11344
+ }
11345
+ ) : null,
11346
+ state.kind === "signed-in" && surface === "addresses" ? /* @__PURE__ */ jsx26(
11347
+ PortalAccountAddresses,
11348
+ {
11349
+ data: state.data,
11350
+ client: portalClient,
11351
+ fetchImpl,
11352
+ onReloaded: replaceData
11353
+ }
11354
+ ) : null,
11355
+ state.kind === "signed-in" && surface === "documents" ? /* @__PURE__ */ jsx26(
11356
+ PortalAccountDocuments,
11357
+ {
11358
+ data: state.data,
11359
+ fetchImpl,
11360
+ openUrl
11361
+ }
11362
+ ) : null,
11363
+ state.kind === "signed-in" && surface === "orders" ? /* @__PURE__ */ jsx26(
11364
+ PortalAccountOrders,
11365
+ {
11366
+ data: state.data,
11367
+ fetchImpl,
11368
+ openUrl
11369
+ }
11370
+ ) : null,
11371
+ state.kind === "signed-in" && surface === "profile" ? /* @__PURE__ */ jsx26(
11372
+ PortalAccountProfile,
11373
+ {
11374
+ data: state.data,
11375
+ fetchImpl,
11376
+ onSaved: replaceData
11377
+ }
11378
+ ) : null
11379
+ ]
11380
+ }
11381
+ );
11335
11382
  }
11336
- function PortalAccountSkeleton() {
11337
- return /* @__PURE__ */ jsxs23("div", { className: "sk-portal-account__skeleton", "aria-hidden": "true", children: [
11338
- /* @__PURE__ */ jsx26("span", { className: "sk-portal-account__skeleton-row" }),
11339
- /* @__PURE__ */ jsx26("span", { className: "sk-portal-account__skeleton-row" }),
11340
- /* @__PURE__ */ jsx26("span", { className: "sk-portal-account__skeleton-row" })
11383
+ var SKELETON_ROWS = 3;
11384
+ function PortalAccountSkeleton({
11385
+ surface
11386
+ }) {
11387
+ const grouped = surface === "appointments";
11388
+ return /* @__PURE__ */ jsxs23(Fragment11, { children: [
11389
+ /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__placeholder sk-portal-account__placeholder--sr", role: "status", children: "Loading your account\u2026" }),
11390
+ /* @__PURE__ */ jsxs23("div", { className: "sk-portal-account__skeleton", "aria-hidden": "true", children: [
11391
+ /* @__PURE__ */ jsxs23("div", { className: "sk-portal-account__header sk-portal-account__header--skeleton", children: [
11392
+ /* @__PURE__ */ jsx26("span", { className: "sk-portal-account__skeleton-line sk-portal-account__skeleton-line--who" }),
11393
+ /* @__PURE__ */ jsx26("span", { className: "sk-portal-account__skeleton-line sk-portal-account__skeleton-line--button" })
11394
+ ] }),
11395
+ grouped ? /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__group-title sk-portal-account__group-title--skeleton", children: /* @__PURE__ */ jsx26("span", { className: "sk-portal-account__skeleton-line sk-portal-account__skeleton-line--group" }) }) : null,
11396
+ /* @__PURE__ */ jsx26("ul", { className: "sk-portal-account__list", children: Array.from({ length: SKELETON_ROWS }, (_, index) => /* @__PURE__ */ jsxs23("li", { className: "sk-portal-account__item sk-portal-account__item--skeleton", children: [
11397
+ /* @__PURE__ */ jsx26("span", { className: "sk-portal-account__skeleton-line sk-portal-account__skeleton-line--title" }),
11398
+ /* @__PURE__ */ jsx26("span", { className: "sk-portal-account__skeleton-line sk-portal-account__skeleton-line--meta" }),
11399
+ grouped ? /* @__PURE__ */ jsx26("span", { className: "sk-portal-account__skeleton-line sk-portal-account__skeleton-line--meta" }) : null
11400
+ ] }, index)) })
11401
+ ] })
11341
11402
  ] });
11342
11403
  }
11343
11404
  function StatusBadge({ status }) {
@@ -11598,8 +11659,11 @@ function PortalAccountOrders({
11598
11659
  initialUi
11599
11660
  }) {
11600
11661
  const [expandedId, setExpandedId] = React21.useState(initialUi?.expandedId ?? null);
11601
- const [detail, setDetail] = React21.useState(initialUi?.detail ?? null);
11602
- const [loading, setLoading] = React21.useState(false);
11662
+ const [details, setDetails] = React21.useState(
11663
+ () => initialUi?.detail && initialUi.expandedId ? { [initialUi.expandedId]: initialUi.detail } : {}
11664
+ );
11665
+ const [loadingId, setLoadingId] = React21.useState(null);
11666
+ const [failedIds, setFailedIds] = React21.useState([]);
11603
11667
  const [error2, setError] = React21.useState(initialUi?.error ?? null);
11604
11668
  async function toggle(id) {
11605
11669
  if (expandedId === id) {
@@ -11607,12 +11671,13 @@ function PortalAccountOrders({
11607
11671
  return;
11608
11672
  }
11609
11673
  setExpandedId(id);
11610
- setDetail(null);
11611
11674
  setError(null);
11612
- setLoading(true);
11675
+ if (details[id]) return;
11676
+ setLoadingId(id);
11613
11677
  const next = await loadPortalOrderDetail(id, fetchImpl);
11614
- setLoading(false);
11615
- setDetail(next);
11678
+ setLoadingId((current) => current === id ? null : current);
11679
+ if (next) setDetails((prev) => ({ ...prev, [id]: next }));
11680
+ else setFailedIds((prev) => prev.includes(id) ? prev : [...prev, id]);
11616
11681
  }
11617
11682
  async function receipt(id) {
11618
11683
  setError(null);
@@ -11626,6 +11691,9 @@ function PortalAccountOrders({
11626
11691
  /* @__PURE__ */ jsx26(ErrorNote, { message: error2 }),
11627
11692
  /* @__PURE__ */ jsx26("ul", { className: "sk-portal-account__list", children: data.items.map((order) => {
11628
11693
  const open = expandedId === order.id;
11694
+ const detail = details[order.id] ?? null;
11695
+ const loading = loadingId === order.id && !detail;
11696
+ const failed = !detail && failedIds.includes(order.id);
11629
11697
  return /* @__PURE__ */ jsxs23("li", { className: "sk-portal-account__item", children: [
11630
11698
  /* @__PURE__ */ jsxs23(
11631
11699
  "button",
@@ -11645,7 +11713,7 @@ function PortalAccountOrders({
11645
11713
  ]
11646
11714
  }
11647
11715
  ),
11648
- open ? /* @__PURE__ */ jsx26("div", { className: "sk-portal-account__detail", children: loading ? /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__placeholder", role: "status", children: "Loading this order\u2026" }) : !detail ? /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__note", role: "note", children: "We couldn't load this order just now." }) : /* @__PURE__ */ jsxs23(Fragment11, { children: [
11716
+ open ? /* @__PURE__ */ jsx26("div", { className: "sk-portal-account__detail", "aria-busy": loading, children: loading ? /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__placeholder", role: "status", children: "Loading this order\u2026" }) : failed || !detail ? /* @__PURE__ */ jsx26("p", { className: "sk-portal-account__note", role: "note", children: "We couldn't load this order just now." }) : /* @__PURE__ */ jsxs23(Fragment11, { children: [
11649
11717
  /* @__PURE__ */ jsx26("ul", { className: "sk-portal-account__lines", children: (detail.lines ?? []).map((line) => /* @__PURE__ */ jsxs23("li", { className: "sk-portal-account__line", children: [
11650
11718
  /* @__PURE__ */ jsxs23("span", { children: [
11651
11719
  line.title ?? "\u2014",
package/dist/index.d.ts CHANGED
@@ -217,6 +217,6 @@ interface PortalSession {
217
217
  */
218
218
  declare function usePortalSession(): PortalSession;
219
219
 
220
- declare const WEBSITE_KIT_VERSION = "0.2.8";
220
+ declare const WEBSITE_KIT_VERSION = "0.2.10";
221
221
 
222
222
  export { Button, type ButtonProps, type ButtonVariant, Container, type ContainerProps, FAQ, type FAQProps, type FaqItem, Hero, type HeroProps, Hours, type HoursProps, type HoursRow, MapEmbed, type MapEmbedProps, PortalClient, PortalPrefill, PortalProvider, type PortalProviderProps, type PortalSession, type PortalSessionStatus, PublicClaim, Row, type RowProps, Section, type SectionProps, type Service, ServiceGrid, type ServiceGridProps, Stack, type StackProps, TeamGrid, type TeamGridProps, type TeamMember, ThemeProvider, type ThemeProviderProps, WEBSITE_KIT_VERSION, WebsiteShell, type WebsiteShellProps, type WebsiteTheme, WhoAmI, defaultTheme, resolveTheme, themeToCssVars, usePortalClient, usePortalSession };
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  import {
12
12
  ISLANDS,
13
13
  mountIslands
14
- } from "./chunk-HTVKKLWI.js";
14
+ } from "./chunk-FW2X2SUZ.js";
15
15
  import {
16
16
  PortalAccount,
17
17
  PortalPanel,
@@ -23,7 +23,7 @@ import {
23
23
  getThemeTokens,
24
24
  usePortalClient,
25
25
  usePortalSession
26
- } from "./chunk-4GKSSM5N.js";
26
+ } from "./chunk-SYXA5WP7.js";
27
27
  import "./chunk-3T7UWKAH.js";
28
28
  import {
29
29
  evaluateShowWhen,
@@ -36,9 +36,9 @@ import {
36
36
  mountIslandsWith,
37
37
  readIslandProps
38
38
  } from "./chunk-THMT43MV.js";
39
- import "./chunk-2CZFJOKK.js";
40
- import "./chunk-ZNH6TSYP.js";
41
- import "./chunk-IOC6QLB7.js";
39
+ import "./chunk-55W3KKJD.js";
40
+ import "./chunk-Q5DIBKWX.js";
41
+ import "./chunk-LW2LLAL4.js";
42
42
  import "./chunk-S3ZQQOQT.js";
43
43
  import {
44
44
  BPOINT_SCRIPT_ORIGIN,
@@ -511,7 +511,7 @@ function Checkout({
511
511
  }
512
512
 
513
513
  // src/index.ts
514
- var WEBSITE_KIT_VERSION = "0.2.8";
514
+ var WEBSITE_KIT_VERSION = "0.2.10";
515
515
  export {
516
516
  BPOINT_SCRIPT_ORIGIN,
517
517
  BPOINT_SCRIPT_URL,
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  CartButtonClient,
3
3
  CartDrawer
4
- } from "../chunk-ZNH6TSYP.js";
5
- import "../chunk-IOC6QLB7.js";
4
+ } from "../chunk-Q5DIBKWX.js";
5
+ import "../chunk-LW2LLAL4.js";
6
6
  import "../chunk-7BUDUYXN.js";
7
7
  import "../chunk-4NF7SSIX.js";
8
8
  import "../chunk-FZ4WKWIE.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  CartClient
3
- } from "../chunk-2CZFJOKK.js";
4
- import "../chunk-IOC6QLB7.js";
3
+ } from "../chunk-55W3KKJD.js";
4
+ import "../chunk-LW2LLAL4.js";
5
5
  import "../chunk-7BUDUYXN.js";
6
6
  import "../chunk-4NF7SSIX.js";
7
7
  import "../chunk-FZ4WKWIE.js";
@@ -3,17 +3,17 @@ import {
3
3
  CertificateFunnelClient,
4
4
  PortalAccountClient,
5
5
  PortalPanelClient
6
- } from "./chunk-4GKSSM5N.js";
6
+ } from "./chunk-SYXA5WP7.js";
7
7
  import "./chunk-3T7UWKAH.js";
8
8
  import "./chunk-QDO3SJWR.js";
9
9
  import "./chunk-THMT43MV.js";
10
10
  import {
11
11
  CartClient
12
- } from "./chunk-2CZFJOKK.js";
12
+ } from "./chunk-55W3KKJD.js";
13
13
  import {
14
14
  CartButtonClient
15
- } from "./chunk-ZNH6TSYP.js";
16
- import "./chunk-IOC6QLB7.js";
15
+ } from "./chunk-Q5DIBKWX.js";
16
+ import "./chunk-LW2LLAL4.js";
17
17
  import {
18
18
  CheckoutClient
19
19
  } from "./chunk-S3ZQQOQT.js";
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  ISLANDS,
3
3
  mountIslands
4
- } from "./chunk-HTVKKLWI.js";
5
- import "./chunk-4GKSSM5N.js";
4
+ } from "./chunk-FW2X2SUZ.js";
5
+ import "./chunk-SYXA5WP7.js";
6
6
  import "./chunk-3T7UWKAH.js";
7
7
  import "./chunk-QDO3SJWR.js";
8
8
  import "./chunk-THMT43MV.js";
9
- import "./chunk-2CZFJOKK.js";
10
- import "./chunk-ZNH6TSYP.js";
11
- import "./chunk-IOC6QLB7.js";
9
+ import "./chunk-55W3KKJD.js";
10
+ import "./chunk-Q5DIBKWX.js";
11
+ import "./chunk-LW2LLAL4.js";
12
12
  import "./chunk-S3ZQQOQT.js";
13
13
  import "./chunk-ZOF22TJA.js";
14
14
  import "./chunk-UTAMB2SK.js";
@@ -2414,16 +2414,78 @@ select.sk-identity__input[data-empty] {
2414
2414
  * to earn its keep, and it buys nothing here — this is on screen for a few hundred ms.
2415
2415
  * This is NOT the no-JS fallback below; that one is real content and must stay real. */
2416
2416
  .sk-portal-account__skeleton {
2417
- display: flex;
2418
- flex-direction: column;
2419
- gap: var(--sk-space, 0.5rem);
2420
- margin-bottom: calc(var(--sk-space, 0.5rem) * 2);
2417
+ display: block;
2418
+ margin-bottom: 0;
2421
2419
  }
2422
2420
 
2423
- .sk-portal-account__skeleton-row {
2424
- height: 2.75rem;
2421
+ /* PAT-949 — the skeleton borrows `__header` and `__item` for its boxes (see the
2422
+ * component), so the only thing defined here is the INK inside them and the two
2423
+ * differences a placeholder needs: it cannot be clicked, and its header carries no
2424
+ * text to give it height, so it gets the height back explicitly. */
2425
+ .sk-portal-account__header--skeleton {
2426
+ min-height: 2.25rem;
2427
+ }
2428
+
2429
+ .sk-portal-account__item--skeleton {
2430
+ pointer-events: none;
2431
+ }
2432
+
2433
+ .sk-portal-account__skeleton-line {
2434
+ display: block;
2435
+ height: 0.875rem;
2425
2436
  border-radius: var(--sk-radius, 0.625rem);
2426
- background: color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 7%, transparent);
2437
+ background: color-mix(in srgb, var(--sk-neutral-foreground, #1b2733) 12%, transparent);
2438
+ }
2439
+
2440
+ /* Widths chosen to sit under the real strings they stand in for: "Signed in as Jane",
2441
+ * a "Sign out" ghost button, an item title and its date · clinician · amount line. */
2442
+ .sk-portal-account__skeleton-line--who {
2443
+ width: 10rem;
2444
+ }
2445
+
2446
+ .sk-portal-account__skeleton-line--button {
2447
+ width: 5.5rem;
2448
+ height: 2rem;
2449
+ }
2450
+
2451
+ .sk-portal-account__skeleton-line--title {
2452
+ width: 60%;
2453
+ }
2454
+
2455
+ .sk-portal-account__skeleton-line--meta {
2456
+ width: 40%;
2457
+ height: 0.75rem;
2458
+ }
2459
+
2460
+ /* Stands in for a group heading ("UPCOMING"), so the list does not slide up under one
2461
+ * when the real body lands. */
2462
+ .sk-portal-account__group-title--skeleton {
2463
+ display: block;
2464
+ }
2465
+
2466
+ .sk-portal-account__skeleton-line--group {
2467
+ width: 6rem;
2468
+ height: 0.75rem;
2469
+ }
2470
+
2471
+ @media (prefers-reduced-motion: no-preference) {
2472
+ .sk-portal-account__skeleton-line {
2473
+ animation: sk-skeleton-pulse 1.4s ease-in-out infinite;
2474
+ }
2475
+ }
2476
+
2477
+ /* The screen-reader-only status line the skeleton announces itself with — the bars
2478
+ * themselves are `aria-hidden`. Same recipe as the store block's (PAT-944). */
2479
+ .sk-portal-account__placeholder--sr {
2480
+ position: absolute;
2481
+ width: 1px;
2482
+ height: 1px;
2483
+ padding: 0;
2484
+ margin: -1px;
2485
+ overflow: hidden;
2486
+ clip-path: inset(50%);
2487
+ white-space: nowrap;
2488
+ border: 0;
2427
2489
  }
2428
2490
 
2429
2491
  /* The build-time no-JS fallback: REAL visible content, never a hidden skeleton. */
@@ -2887,23 +2949,75 @@ select.sk-identity__input[data-empty] {
2887
2949
  border-radius: var(--sk-radius, 0.625rem);
2888
2950
  font-size: 0.9375rem;
2889
2951
  }
2952
+ .sk-cart__lines[aria-busy='true'] {
2953
+ /* A re-quote is in flight over prices that are still correct enough to read. Dim the
2954
+ money a touch rather than tearing the list down — the teardown is what read as the
2955
+ cart reloading (PAT-949). Steppers stay live: the cart write already succeeded. */
2956
+ opacity: 0.7;
2957
+ transition: opacity 120ms ease-out;
2958
+ }
2959
+ @media (prefers-reduced-motion: reduce) {
2960
+ .sk-cart__lines[aria-busy='true'] {
2961
+ transition: none;
2962
+ }
2963
+ }
2890
2964
  .sk-cart__lines {
2891
2965
  list-style: none;
2892
2966
  margin: 0;
2893
2967
  padding: 0;
2968
+ /* The drawer is ~380px inside a wide viewport, so a VIEWPORT media query can never
2969
+ tell us the row is cramped (PAT-948). Size the row off its own container instead. */
2970
+ container-type: inline-size;
2971
+ container-name: sk-cart-lines;
2894
2972
  }
2895
2973
  .sk-cart__line {
2896
2974
  display: grid;
2897
- grid-template-columns: 1fr auto auto auto;
2975
+ /* Five children, so name five tracks. They used to run in a FOUR-column grid, which
2976
+ wrapped Remove onto its own row where it stretched the full 1fr title column and
2977
+ centred its own label — the reason it floated mid-row (PAT-948). */
2978
+ grid-template-columns: minmax(0, 1fr) auto auto auto auto;
2979
+ grid-template-areas: 'title unit qty total remove';
2898
2980
  align-items: center;
2899
2981
  gap: calc(var(--sk-space, 0.5rem) * 2);
2900
2982
  padding: calc(var(--sk-space, 0.5rem) * 2) 0;
2901
2983
  border-bottom: 1px solid color-mix(in srgb, var(--sk-muted, #5b6b7a) 30%, transparent);
2902
2984
  }
2985
+ .sk-cart__line-title {
2986
+ grid-area: title;
2987
+ }
2988
+ .sk-cart__line-unit {
2989
+ grid-area: unit;
2990
+ }
2991
+ .sk-cart__qty {
2992
+ grid-area: qty;
2993
+ }
2994
+ .sk-cart__line-total {
2995
+ grid-area: total;
2996
+ }
2997
+ .sk-cart__remove {
2998
+ grid-area: remove;
2999
+ }
3000
+ /* Narrow CONTAINER (the drawer, and a phone): two columns, three rows. */
3001
+ @container sk-cart-lines (max-width: 34rem) {
3002
+ .sk-cart__line {
3003
+ grid-template-columns: minmax(0, 1fr) auto;
3004
+ grid-template-areas:
3005
+ 'title title'
3006
+ 'unit total'
3007
+ 'qty remove';
3008
+ row-gap: calc(var(--sk-space, 0.5rem) * 1);
3009
+ }
3010
+ }
2903
3011
  .sk-cart__line-title {
2904
3012
  color: inherit;
2905
3013
  font-weight: 600;
2906
3014
  text-decoration: none;
3015
+ /* One line, clipped — a product name is not worth reflowing the row it sits in.
3016
+ min-width:0 is what lets the 1fr track actually shrink inside a grid. */
3017
+ min-width: 0;
3018
+ overflow: hidden;
3019
+ text-overflow: ellipsis;
3020
+ white-space: nowrap;
2907
3021
  }
2908
3022
  .sk-cart__line-unit {
2909
3023
  font-size: 0.875rem;
@@ -2913,6 +3027,14 @@ select.sk-identity__input[data-empty] {
2913
3027
  display: inline-flex;
2914
3028
  align-items: center;
2915
3029
  gap: calc(var(--sk-space, 0.5rem) * 1);
3030
+ justify-self: start;
3031
+ }
3032
+ /* The quantity reserves room for two digits and uses tabular figures, so stepping
3033
+ 1 -> 2 -> 10 never shifts the − and + buttons under the shopper's cursor. */
3034
+ .sk-cart__qty-value {
3035
+ min-width: 2ch;
3036
+ text-align: center;
3037
+ font-variant-numeric: tabular-nums;
2916
3038
  }
2917
3039
  .sk-cart__qty button {
2918
3040
  min-width: 2rem;
@@ -2928,6 +3050,10 @@ select.sk-identity__input[data-empty] {
2928
3050
  .sk-cart__line-total {
2929
3051
  font-variant-numeric: tabular-nums;
2930
3052
  font-weight: 600;
3053
+ /* Reserve the widest realistic total so $70.00 and $599.00 hold the same column
3054
+ and the stepper beside them does not slide between lines. */
3055
+ min-width: 6ch;
3056
+ text-align: right;
2931
3057
  }
2932
3058
  .sk-cart__remove {
2933
3059
  border: 0;
@@ -2938,6 +3064,11 @@ select.sk-identity__input[data-empty] {
2938
3064
  opacity: 0.75;
2939
3065
  cursor: pointer;
2940
3066
  text-decoration: underline;
3067
+ /* A grid item stretches by default, so this button used to fill its track and
3068
+ centre its own label. Shrink it to its text and pin it to the row's end. */
3069
+ justify-self: end;
3070
+ padding: 0;
3071
+ width: max-content;
2941
3072
  }
2942
3073
  .sk-cart__totals {
2943
3074
  margin: calc(var(--sk-space, 0.5rem) * 3) 0 0;
@@ -2993,11 +3124,8 @@ select.sk-identity__input[data-empty] {
2993
3124
  font-weight: 600;
2994
3125
  text-decoration: none;
2995
3126
  }
2996
- @media (max-width: 600px) {
2997
- .sk-cart__line {
2998
- grid-template-columns: 1fr auto;
2999
- }
3000
- }
3127
+ /* The old viewport `max-width: 600px` rule lived here. It could not see the drawer,
3128
+ which is ~380px inside a wide window; the @container rule above replaces it. */
3001
3129
 
3002
3130
 
3003
3131
  /* ── checkout (PAT-766) ──────────────────────────────────────────────────── */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patientos/website-kit",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "PatientOS clinic website components and patient-facing interactive islands.",
5
5
  "type": "module",
6
6
  "sideEffects": [