@owlmeans/web-payment 0.1.18-rc.3 → 0.1.18-rc.31

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 (231) hide show
  1. package/README.md +30 -1
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/web-payment/SKILL.md +302 -21
  4. package/build/@/components/ui/card.d.ts +10 -0
  5. package/build/@/components/ui/card.d.ts.map +1 -0
  6. package/build/@/components/ui/card.js +25 -0
  7. package/build/@/components/ui/card.js.map +1 -0
  8. package/build/@/components/ui/checkbox.d.ts +5 -0
  9. package/build/@/components/ui/checkbox.d.ts.map +1 -0
  10. package/build/@/components/ui/checkbox.js +6 -0
  11. package/build/@/components/ui/checkbox.js.map +1 -0
  12. package/build/@/components/ui/progress.d.ts +5 -0
  13. package/build/@/components/ui/progress.d.ts.map +1 -0
  14. package/build/@/components/ui/progress.js +11 -0
  15. package/build/@/components/ui/progress.js.map +1 -0
  16. package/build/@/components/ui/select.d.ts +8 -0
  17. package/build/@/components/ui/select.d.ts.map +1 -0
  18. package/build/@/components/ui/select.js +9 -0
  19. package/build/@/components/ui/select.js.map +1 -0
  20. package/build/capability-list.d.ts +7 -0
  21. package/build/capability-list.d.ts.map +1 -0
  22. package/build/capability-list.js +24 -0
  23. package/build/capability-list.js.map +1 -0
  24. package/build/checkout-limit.d.ts +11 -0
  25. package/build/checkout-limit.d.ts.map +1 -0
  26. package/build/checkout-limit.js +33 -0
  27. package/build/checkout-limit.js.map +1 -0
  28. package/build/consts.d.ts +3 -0
  29. package/build/consts.d.ts.map +1 -0
  30. package/build/consts.js +3 -0
  31. package/build/consts.js.map +1 -0
  32. package/build/consts.local.d.ts +8 -0
  33. package/build/consts.local.d.ts.map +1 -0
  34. package/build/consts.local.js +17 -0
  35. package/build/consts.local.js.map +1 -0
  36. package/build/consumer/cancellation.d.ts +20 -0
  37. package/build/consumer/cancellation.d.ts.map +1 -0
  38. package/build/consumer/cancellation.js +115 -0
  39. package/build/consumer/cancellation.js.map +1 -0
  40. package/build/consumer/consts.d.ts +3 -0
  41. package/build/consumer/consts.d.ts.map +1 -0
  42. package/build/consumer/consts.js +3 -0
  43. package/build/consumer/consts.js.map +1 -0
  44. package/build/consumer/consts.local.d.ts +7 -0
  45. package/build/consumer/consts.local.d.ts.map +1 -0
  46. package/build/consumer/consts.local.js +7 -0
  47. package/build/consumer/consts.local.js.map +1 -0
  48. package/build/consumer/copy.d.ts +21 -0
  49. package/build/consumer/copy.d.ts.map +1 -0
  50. package/build/consumer/copy.js +75 -0
  51. package/build/consumer/copy.js.map +1 -0
  52. package/build/consumer/ensure.d.ts +15 -0
  53. package/build/consumer/ensure.d.ts.map +1 -0
  54. package/build/consumer/ensure.js +71 -0
  55. package/build/consumer/ensure.js.map +1 -0
  56. package/build/consumer/fields.d.ts +12 -0
  57. package/build/consumer/fields.d.ts.map +1 -0
  58. package/build/consumer/fields.js +18 -0
  59. package/build/consumer/fields.js.map +1 -0
  60. package/build/consumer/hooks.d.ts +30 -0
  61. package/build/consumer/hooks.d.ts.map +1 -0
  62. package/build/consumer/hooks.js +225 -0
  63. package/build/consumer/hooks.js.map +1 -0
  64. package/build/consumer/index.d.ts +14 -0
  65. package/build/consumer/index.d.ts.map +1 -0
  66. package/build/consumer/index.js +12 -0
  67. package/build/consumer/index.js.map +1 -0
  68. package/build/consumer/legal.d.ts +30 -0
  69. package/build/consumer/legal.d.ts.map +1 -0
  70. package/build/consumer/legal.js +77 -0
  71. package/build/consumer/legal.js.map +1 -0
  72. package/build/consumer/performance-consent-dialog.d.ts +20 -0
  73. package/build/consumer/performance-consent-dialog.d.ts.map +1 -0
  74. package/build/consumer/performance-consent-dialog.js +75 -0
  75. package/build/consumer/performance-consent-dialog.js.map +1 -0
  76. package/build/consumer/provider.d.ts +19 -0
  77. package/build/consumer/provider.d.ts.map +1 -0
  78. package/build/consumer/provider.js +26 -0
  79. package/build/consumer/provider.js.map +1 -0
  80. package/build/consumer/refusal.d.ts +35 -0
  81. package/build/consumer/refusal.d.ts.map +1 -0
  82. package/build/consumer/refusal.js +93 -0
  83. package/build/consumer/refusal.js.map +1 -0
  84. package/build/consumer/subscription-start-dialog.d.ts +19 -0
  85. package/build/consumer/subscription-start-dialog.d.ts.map +1 -0
  86. package/build/consumer/subscription-start-dialog.js +46 -0
  87. package/build/consumer/subscription-start-dialog.js.map +1 -0
  88. package/build/consumer/types.d.ts +304 -0
  89. package/build/consumer/types.d.ts.map +1 -0
  90. package/build/consumer/types.js +2 -0
  91. package/build/consumer/types.js.map +1 -0
  92. package/build/consumer/types.local.d.ts +21 -0
  93. package/build/consumer/types.local.d.ts.map +1 -0
  94. package/build/consumer/types.local.js +2 -0
  95. package/build/consumer/types.local.js.map +1 -0
  96. package/build/consumer/withdrawal.d.ts +29 -0
  97. package/build/consumer/withdrawal.d.ts.map +1 -0
  98. package/build/consumer/withdrawal.js +140 -0
  99. package/build/consumer/withdrawal.js.map +1 -0
  100. package/build/copy.d.ts +10 -0
  101. package/build/copy.d.ts.map +1 -0
  102. package/build/copy.js +51 -0
  103. package/build/copy.js.map +1 -0
  104. package/build/dialog.d.ts +1 -1
  105. package/build/dialog.d.ts.map +1 -1
  106. package/build/dialog.js +56 -19
  107. package/build/dialog.js.map +1 -1
  108. package/build/entitlement.d.ts +25 -0
  109. package/build/entitlement.d.ts.map +1 -0
  110. package/build/entitlement.js +42 -0
  111. package/build/entitlement.js.map +1 -0
  112. package/build/estimate-cache.d.ts +14 -0
  113. package/build/estimate-cache.d.ts.map +1 -0
  114. package/build/estimate-cache.js +39 -0
  115. package/build/estimate-cache.js.map +1 -0
  116. package/build/estimate-summary.d.ts +32 -0
  117. package/build/estimate-summary.d.ts.map +1 -0
  118. package/build/estimate-summary.js +94 -0
  119. package/build/estimate-summary.js.map +1 -0
  120. package/build/estimate.d.ts +20 -0
  121. package/build/estimate.d.ts.map +1 -0
  122. package/build/estimate.js +92 -0
  123. package/build/estimate.js.map +1 -0
  124. package/build/format.d.ts +30 -0
  125. package/build/format.d.ts.map +1 -0
  126. package/build/format.js +70 -0
  127. package/build/format.js.map +1 -0
  128. package/build/hooks.d.ts +1 -6
  129. package/build/hooks.d.ts.map +1 -1
  130. package/build/hooks.js +3 -30
  131. package/build/hooks.js.map +1 -1
  132. package/build/i18n/be.json +1 -1
  133. package/build/i18n/de.json +1 -1
  134. package/build/i18n/en.json +1 -1
  135. package/build/i18n/es.json +1 -1
  136. package/build/i18n/fr.json +146 -0
  137. package/build/i18n/pl.json +1 -1
  138. package/build/i18n/ru.json +1 -1
  139. package/build/i18n/uk.json +1 -1
  140. package/build/i18n.js +10 -7
  141. package/build/i18n.js.map +1 -1
  142. package/build/index.d.ts +9 -0
  143. package/build/index.d.ts.map +1 -1
  144. package/build/index.js +9 -0
  145. package/build/index.js.map +1 -1
  146. package/build/limit-meter.d.ts +7 -0
  147. package/build/limit-meter.d.ts.map +1 -0
  148. package/build/limit-meter.js +26 -0
  149. package/build/limit-meter.js.map +1 -0
  150. package/build/plan-card.d.ts +7 -0
  151. package/build/plan-card.d.ts.map +1 -0
  152. package/build/plan-card.js +24 -0
  153. package/build/plan-card.js.map +1 -0
  154. package/build/poll.d.ts +7 -0
  155. package/build/poll.d.ts.map +1 -0
  156. package/build/poll.js +28 -0
  157. package/build/poll.js.map +1 -0
  158. package/build/selectors.d.ts +18 -0
  159. package/build/selectors.d.ts.map +1 -0
  160. package/build/selectors.js +73 -0
  161. package/build/selectors.js.map +1 -0
  162. package/build/service.d.ts +1 -1
  163. package/build/service.d.ts.map +1 -1
  164. package/build/service.js +0 -1
  165. package/build/service.js.map +1 -1
  166. package/build/types.d.ts +177 -2
  167. package/build/types.d.ts.map +1 -1
  168. package/package.json +35 -20
  169. package/src/@/components/ui/card.tsx +93 -0
  170. package/src/@/components/ui/checkbox.tsx +23 -0
  171. package/src/@/components/ui/progress.tsx +44 -0
  172. package/src/@/components/ui/select.tsx +62 -0
  173. package/src/@/globals.css +10 -0
  174. package/src/capability-list.tsx +50 -0
  175. package/src/checkout-limit.tsx +46 -0
  176. package/src/consts.local.ts +21 -0
  177. package/src/consts.ts +2 -0
  178. package/src/consumer/cancellation.tsx +213 -0
  179. package/src/consumer/consts.local.ts +9 -0
  180. package/src/consumer/consts.ts +2 -0
  181. package/src/consumer/copy.ts +87 -0
  182. package/src/consumer/ensure.ts +79 -0
  183. package/src/consumer/fields.tsx +55 -0
  184. package/src/consumer/hooks.ts +262 -0
  185. package/src/consumer/index.ts +13 -0
  186. package/src/consumer/legal.tsx +103 -0
  187. package/src/consumer/performance-consent-dialog.tsx +137 -0
  188. package/src/consumer/provider.tsx +38 -0
  189. package/src/consumer/refusal.ts +101 -0
  190. package/src/consumer/subscription-start-dialog.tsx +91 -0
  191. package/src/consumer/types.local.ts +22 -0
  192. package/src/consumer/types.ts +331 -0
  193. package/src/consumer/withdrawal.tsx +267 -0
  194. package/src/copy.tsx +57 -0
  195. package/src/dialog.tsx +95 -43
  196. package/src/entitlement.ts +55 -0
  197. package/src/estimate-cache.ts +40 -0
  198. package/src/estimate-summary.tsx +138 -0
  199. package/src/estimate.ts +103 -0
  200. package/src/format.ts +75 -0
  201. package/src/hooks.ts +5 -37
  202. package/src/i18n/be.json +1 -1
  203. package/src/i18n/de.json +1 -1
  204. package/src/i18n/en.json +1 -1
  205. package/src/i18n/es.json +1 -1
  206. package/src/i18n/fr.json +146 -0
  207. package/src/i18n/pl.json +1 -1
  208. package/src/i18n/ru.json +1 -1
  209. package/src/i18n/uk.json +1 -1
  210. package/src/i18n.ts +10 -7
  211. package/src/index.ts +9 -0
  212. package/src/limit-meter.tsx +55 -0
  213. package/src/plan-card.tsx +61 -0
  214. package/src/poll.ts +27 -0
  215. package/src/selectors.ts +86 -0
  216. package/src/service.ts +2 -1
  217. package/src/types.ts +206 -2
  218. package/tests/consent-refusal.spec.ts +83 -0
  219. package/tests/consumer-hooks.spec.ts +205 -0
  220. package/tests/consumer.spec.ts +424 -0
  221. package/tests/context.ts +9 -0
  222. package/tests/dialog.spec.ts +73 -0
  223. package/tests/entitlement.spec.ts +117 -0
  224. package/tests/estimate-cache.spec.ts +46 -0
  225. package/tests/harness/consumer-fixtures.ts +106 -0
  226. package/tests/harness/consumer.tsx +197 -0
  227. package/tests/harness/entitlement.tsx +159 -0
  228. package/tests/harness/estimate-fixtures.ts +48 -0
  229. package/tests/harness/mount.tsx +25 -3
  230. package/tests/i18n.spec.ts +78 -8
  231. package/tests/pieces.spec.ts +134 -0
package/README.md CHANGED
@@ -28,6 +28,35 @@ Checkout redirects the current window by default; pass `useCheckout('_blank')` o
28
28
  is intentional. The dialog uses the consumer's `@/components/ui/*` primitives, semantic theme
29
29
  tokens, integer minor units, and the packaged seven-language `web-payment` resource.
30
30
 
31
+ ## Entitlements
32
+
33
+ `useEntitlementView(ctx.entrypoint(account.entitlements))` polls a served `EntitlementView` and
34
+ revives its ISO dates; `null` means "not known yet", so render paid controls disabled until it
35
+ answers. Read it with `useCapability(view, param)` (`boolean | null`) and `useLimit(view, key)`
36
+ (the limit row plus `exhausted` and a `ratio` clamped to `[0, 1]`). An application that keeps the
37
+ view in its own store uses the pure selectors instead — `capabilityStateOf`, `limitStatusOf`,
38
+ `planStatusLineOf`, `promoInscriptionOf` — and never polls a second time.
39
+
40
+ ```tsx
41
+ const view = useEntitlementView(ctx.entrypoint(account.entitlements))
42
+ const seats = useLimit(view, 'seats')
43
+ const { portal, pending } = usePortal(PortalFlow.Change)
44
+
45
+ {view != null && <>
46
+ <PlanCard plan={view.plan} offer={{ sku: 'pro-monthly', title: 'Pro', priceLabel: '$20 / month' }}
47
+ actionLabel="Change plan" pending={pending}
48
+ onAction={planSku => portal(ctx.entrypoint(account.portal), { body: { planSku } })} />
49
+ {seats != null && <LimitMeter limit={seats} label="Seats" />}
50
+ <CapabilityList capabilities={view.capabilities} labels={{ 'feature:whitelabel': 'White label' }} />
51
+ </>}
52
+ ```
53
+
54
+ `usePortal(flow)` sets `body.flow` on a protocol whose body is a `PortalLinkBody` and sends the
55
+ browser to the returned URL. The pieces take data and callbacks only: titles, prices, labels and
56
+ action copy come from the application; status lines, "N of M", reset dates and promo inscriptions
57
+ come from the packaged `entitlement` strings. They import `@/components/ui/{card,progress,button}`,
58
+ so the consumer provides those primitives and `@radix-ui/react-progress`.
59
+
31
60
  <!-- owlmeans:agent-guidance:start -->
32
61
  ## Agent guidance
33
62
 
@@ -36,7 +65,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
36
65
  your project's skill store (`.agents/skills/`):
37
66
 
38
67
  ```sh
39
- npx @owlmeans/agent-skills@^0.1.18-rc.19
68
+ npx @owlmeans/agent-skills@^0.1.18-rc.48
40
69
  ```
41
70
 
42
71
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-payment",
4
- "version": "0.1.18-rc.3",
5
- "generatedAt": "2026-09-12T13:51:53.538Z",
4
+ "version": "0.1.18-rc.31",
5
+ "generatedAt": "2026-10-05T08:32:19.126Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,19 +1,24 @@
1
1
  ---
2
2
  name: web-payment
3
- description: Public React payment UI for OwlMeans web apps — protocol-bound checkout/balance hooks, same-window redirects, entitlement helpers and the themed AmountCheckoutDialog. Use when building payment UI with @owlmeans/web-payment.
3
+ description: Public React payment UI for OwlMeans web apps — protocol-bound checkout/balance hooks, same-window redirects, entitlement helpers, the themed AmountCheckoutDialog with its per-entity checkout limit, the locked-country price estimate, and the EU consumer-rights pieces of the ./consumer subpath (spend consent, subscription start, withdrawal and cancellation functions, the consent gate). Use when building payment UI with @owlmeans/web-payment.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
7
7
 
8
8
  # @owlmeans/web-payment
9
9
 
10
- **Install:** `bun add @owlmeans/web-payment@^0.1.18-rc.2`
10
+ **Install:** `bun add @owlmeans/web-payment@^0.1.18-rc.31`
11
11
 
12
12
  Public MIT web package paired with `@owlmeans/server-payment`. It incorporates checkout, balance,
13
- entitlement and shallow-auth helpers over the underlying `@owlmeans/client-payment` service.
13
+ entitlement and shallow-auth helpers over the underlying `@owlmeans/client-payment` service, plus
14
+ presentational pieces over the entitlement view of `@owlmeans/payment` (the model itself is the
15
+ `entitlements` skill), and — on the `./consumer` subpath — the consumer-rights pieces over
16
+ `@owlmeans/payment`'s consumer-rights contracts and legal copy (the `payment` skill).
17
+
18
+ ## Protocol-bound hooks
14
19
 
15
20
  Hooks accept `RegisteredEntrypoint<Request, Response>` values. Pass `ctx.entrypoint(protocol)`;
16
- never weaken a protocol to a home-grown callable type or recover its I/O with a generic. Checkout
21
+ never weaken a protocol to a home-grown callable type or recover its I/O with a generic. Call
17
22
  arguments use the protocol's `CallArguments`, so a required body cannot be omitted by the hook.
18
23
 
19
24
  ```typescript
@@ -21,30 +26,306 @@ const { checkout, pending } = useCheckout() // same-window by default
21
26
  await checkout(ctx.entrypoint(account.topUp), { body: { amountMinor } })
22
27
  ```
23
28
 
24
- `useCheckout('_blank')` is the explicit new-tab variant. `openCheckout(url)` defaults to
25
- `window.location.assign`; both are inert when no DOM exists. `usePaymentBalance` polls one protocol,
26
- `useEntitlements` extracts its list, and `useEntitlement` uses the shared payment predicate. `null`
27
- means not known and must render paid controls disabled.
29
+ - `useCheckout('_blank')` is the explicit new-tab variant. `openCheckout(url)` defaults to
30
+ `window.location.assign`; both are inert when no DOM exists.
31
+ - `usePaymentBalance(entry, intervalMs?, deps?)` polls one protocol and keeps the last good answer;
32
+ a failed poll is swallowed, because explicit actions own their errors.
33
+
34
+ ## Entitlement hooks — `null` means unknown
35
+
36
+ | Hook | Answers |
37
+ |---|---|
38
+ | `useEntitlementView(entry, intervalMs = 60_000, deps = [], ...request)` | The polled `EntitlementView`, revived (`reviveEntitlementView`) so every date is a `Date`; `null` until the first answer. Trailing `request` arguments are the protocol's `CallArguments`. |
39
+ | `useCapability(view, param)` | `boolean \| null` — the capability predicate over granted rows; `null` for a null view. |
40
+ | `useLimit(view, key)` | `LimitStatus \| null` — the `LimitView` plus `exhausted` and `ratio`; `null` for a null view or an undeclared key. |
41
+ | `usePortal(flow, target = '_self')` | `{ portal(entry, request?), pending }` — calls a protocol whose body is a `PortalLinkBody` with `body.flow = flow`, then `openCheckout(url, target)`. The caller's arguments omit `flow` (a compile error otherwise); navigation is inert without a DOM. |
42
+
43
+ - **`null` renders paid controls disabled**, never enabled-then-refused. A `false` is an answer;
44
+ `null` is the absence of one.
45
+ - **One poller per view.** An application that keeps the view in its own store (fed once, refreshed
46
+ after the actions that change it) reads it through the pure selectors, not a second
47
+ `useEntitlementView`.
48
+ - Anything read off a view fetched some other way must be revived before a date is used.
49
+
50
+ ## Pure selectors
51
+
52
+ | Selector | Returns |
53
+ |---|---|
54
+ | `capabilityStateOf(view, param)` | `boolean \| null` — what `useCapability` memoizes |
55
+ | `limitStatusOf(limitView)` | `LimitStatus \| null` — `exhausted = remaining < 1` (a `limit: 0` row is exhausted); `ratio = used / limit` clamped to `[0, 1]`, and for a `0` ceiling `1` once anything is used, else `0` |
56
+ | `planStatusLineOf(planView)` | `{ kind, tone, date? }` — first match: `created`/`canceled`/`expired`/`ended` (inactive), `blocked` (critical), `paused` (a `pausedAt`, warning), `suspended` (critical), `trial` (`trialEnd`), `past-due` (warning), `cancel-scheduled` (`periodEnd`, warning), `free`, `renews` (`periodEnd`), `active` |
57
+ | `promoInscriptionOf(promoView)` | `'free-until'` (active, not grandfathered) · `'grandfathered'` (active, grandfathered) · `'ended'` (inactive) · `null` |
58
+
59
+ Use these, plus `capabilityOf` / `limitOf` / `hasLimitRoom` from `@owlmeans/payment`, wherever the
60
+ browser decides something; the server gate uses the same predicates, so a disabled button and a
61
+ 403 cannot disagree.
62
+
63
+ ## Pieces
64
+
65
+ Pieces take data and callbacks only — no protocol calls, no product copy (titles, prices, labels
66
+ and action text come from the application). Every one accepts `className`.
67
+
68
+ | Piece | Props | Renders |
69
+ |---|---|---|
70
+ | `PlanCard` | `plan: EntitlementPlanView`, `offer?: { sku, title, priceLabel, highlight? }`, `current?`, `pending?`, `actionLabel?`, `onAction?(sku)`, `children?` | Title; the status line (with a tone dot) for the entity's own plan, or for an offer only when it is current; a "Current plan" badge; `priceLabel`; children; one action button (outline when current, disabled while `pending`). `current` defaults to `offer.sku === plan.sku`. |
71
+ | `LimitMeter` | `limit: LimitView`, `label`, `showReset? = true`, `compact?` | `used of limit` (with `unit`), a `Progress` bar, "Limit reached" when exhausted, "Resets on …" for a window limit, the promo inscription. `limit: 0` reads "Not included" and draws no bar. |
72
+ | `CapabilityList` | `capabilities: CapabilityView[]`, `labels: Record<param, string>`, `onlyGranted?` | One row per labelled param in view order (duplicate params collapse, granted wins); unlabelled rows are not rendered; ungranted rows are muted; promo inscription under the label. |
73
+ | `CheckoutLimitNote` | `limit: CheckoutLimitView \| null`, `reasonLabel?: string \| (reason) => string` | Nothing for `null` or a limit that narrowed nothing; else "You can add up to {max} right now" (or "You cannot add credits right now" when `blocked`), the reason's sentence (`per-purchase`, `window`, `total`, `hold`, any other → generic; `reasonLabel` replaces it) and "More becomes available on {resetsAt}" (UTC, to the minute). |
74
+
75
+ - **Stable selectors** for tests and styling: `[data-plan-card]` with `data-plan-sku`,
76
+ `data-plan-status` (the line kind), `data-plan-tone`, `data-current`; `[data-plan-status-line]`,
77
+ `[data-plan-current]`, `[data-plan-price]`; `[data-limit-meter]` with `data-limit-key`,
78
+ `data-limit-kind`, `data-included`, `data-exhausted`, and `[data-limit-usage]`,
79
+ `[data-limit-resets]`, `[data-limit-exhausted]`; `[data-capability-list]`, `[data-capability]`
80
+ with `data-granted`; `[data-promo]` carrying the inscription kind; `[data-checkout-limit]` with
81
+ `data-max-minor`, `data-blocked` (`"true"`/`"false"`), `data-reason`, `data-resets-at` (ISO).
82
+ - **Dates render as calendar UTC days** (`dateStyle: 'medium'`) in the current language — the
83
+ calendar limit windows count in, so a reset date names the day the counter rolls.
84
+ - Promo copy: "Free until {{date}}", "Included for your plan", "Promotion ended".
28
85
 
29
86
  ## AmountCheckoutDialog
30
87
 
31
- The dialog takes `{ open, onOpenChange, policy, pending, onConfirm }`. It keeps integer minor units,
32
- accepts locale decimal separators and at most two decimals, shows configured presets plus custom
33
- input, and reports below/above-bound errors without clamping. Confirmation is disabled when invalid
34
- or pending.
88
+ The dialog takes `{ open, onOpenChange, policy, pending, disabled?, onConfirm, estimate?, limit?, legalNote?, details? }`.
89
+ It keeps integer minor units, accepts locale decimal separators and at most two decimals, shows
90
+ configured presets plus custom input, and reports below/above-bound errors without clamping.
91
+ Confirmation is disabled when invalid or pending. The dialog body scrolls (`max-h-[90vh]
92
+ overflow-y-auto`, like the consent dialogs): the tier `details`, the estimate and the legal note can
93
+ outgrow a short screen, and the confirm button must stay reachable — never render it outside.
94
+
95
+ It shows net credit value, processing adjustment and pre-tax subtotal from `chargeAmountMinor`.
96
+ With no `estimate`, the plain note that Stripe calculates applicable tax; with one, `PriceEstimateSummary`
97
+ renders instead (see below) against the SAME `chargeMinor`, so the country picker and its numbers
98
+ track whatever the buyer has typed. The policy maximum bounds net credit, not the adjusted or
99
+ tax-inclusive debit.
100
+
101
+ - **`limit`** (`AmountPolicyView.limit`, what the entity may buy NOW) narrows `policy` through
102
+ `@owlmeans/payment`'s `narrowAmountPolicy` — the same computation the server refuses with
103
+ (`CheckoutLimitExceeded`), so the control and a refusal cannot disagree. Presets above the
104
+ maximum disappear, the default is clamped, the above-bound error names the narrowed maximum
105
+ ("The most you can add right now is …"), and a `CheckoutLimitNote` shows above the presets.
106
+ `policy` may be the plan's or one already narrowed (`AmountPolicyView.policy`) — narrowing is
107
+ idempotent. **`blocked`** disables the presets, the input and the confirm button; the pinned
108
+ policy `narrowAmountPolicy` returns for a block is never an offer of the minimum.
109
+ - **`details`** is a reusable application-owned React slot (`[data-amount-details]`), the dialog's
110
+ SIDE column, holding the `CheckoutLimitNote` ("You can add up to …") above the details. With it the
111
+ dialog is sized by the window — `w-[90vw]`, `lg:w-[50vw]` from a landscape tablet up — and every
112
+ base cap is replaced by its twin (`max-w-none`, `sm:max-w-none`). The two blocks are a WRAPPING
113
+ flex row filling the content box: purchase `flex-3 min-w-60`, side `flex-2 min-w-44` (basis 0, so
114
+ side : purchase is exactly 2 : 3), `gap-x-6` (the dialog's padding) between them. The whole side
115
+ column drops under the purchase block, above the `legalNote` and the buttons, once the dialog
116
+ cannot hold both minimums — never a viewport breakpoint — so the confirm button's right edge is
117
+ the side column's, or the purchase block's when it is alone. The preset buttons answer to their
118
+ own container (`@container` + `@sm:grid-cols-4`). No `details` keeps the single narrow column with
119
+ the limit note above the presets.
120
+ `disabled` prevents amount selection and submission while application reads are unavailable;
121
+ cancel remains usable. Applications own loading, failed-read, retry and refreshed-limit details.
122
+ - **`legalNote`** is a slot right above the buttons for the application's legal line (the EU
123
+ withdrawal note of a top-up, terms links) — `[data-legal-note]`.
124
+ - Selectors: `[data-amount-checkout]` (`data-blocked`), `[data-amount-preset="<minor>"]`,
125
+ `[data-amount-confirm]`.
126
+
127
+ ## Price estimate: a live tax/currency read for any amount
128
+
129
+ ```typescript
130
+ // self-contained (one estimate, its own picker) — the credit dialog:
131
+ const estimate = usePriceEstimate(ctx.entrypoint(account.priceEstimate), { enabled: dialogOpen })
132
+ <AmountCheckoutDialog … estimate={estimate} />
133
+ <PriceEstimateSummary control={estimate} subtotalMinor={planPriceMinor} currency="usd" />
134
+
135
+ // shared (one picker, several estimates) — a plan comparison table:
136
+ const [country, setCountry] = useState('')
137
+ const pro = usePriceEstimate(entry, { enabled, country, onCountryChange: setCountry }, { body: { planSku: 'pro' } })
138
+ const team = usePriceEstimate(entry, { enabled, country, onCountryChange: setCountry }, { body: { planSku: 'team' } })
139
+ <CountrySelect value={country} onChange={setCountry} disabled={pro.locked} /> {/* rendered ONCE */}
140
+ <PriceEstimateAmount control={pro} subtotalMinor={proPriceMinor} currency="usd" /> {/* per offer */}
141
+ <PriceEstimateAmount control={team} subtotalMinor={teamPriceMinor} currency="usd" />
142
+ ```
143
+
144
+ - **`usePriceEstimate(entry, { enabled, ttlMs?, country?, onCountryChange? }, ...request)`** calls a
145
+ `PriceEstimateBody` protocol (`@owlmeans/payment`'s `PriceEstimate` response) only while `enabled`,
146
+ and again whenever the country changes — a closed dialog costs nothing. A module-level client
147
+ cache (keyed by the entry's alias, the request body and the country; default TTL 5 min) shares an
148
+ answer across every mounted instance and never caches a rejected fetch. The caller's `request`
149
+ omits `body.country` — the hook owns it — the same way `usePortal`'s omits `flow`.
150
+ - **Uncontrolled** (no `opts.country`): the hook owns the country itself, and the first answer
151
+ whose `source` is `'customer'` preselects it, once.
152
+ - **Controlled** (`opts.country` given): the caller owns the country — several `usePriceEstimate`
153
+ calls can share ONE value (a plan comparison table, one picker, one estimate per offer); the
154
+ hook never preselects on its own, and the returned control's `onCountryChange` calls
155
+ `opts.onCountryChange` instead of an internal setter.
156
+ - **Locked** (an answer with `locked: true` or `source: 'profile'` — the entity's billing country
157
+ fixed by its first purchase): the hook adopts that country over any pick (controlled: through
158
+ `opts.onCountryChange`, once), returns `locked: true`, and ignores `onCountryChange` from then
159
+ on (`isLockedAnswer(estimate)`).
160
+ - **`CountrySelect({ value, onChange, label?, id?, className?, disabled?, note? })`** is the picker
161
+ alone — `Intl.DisplayNames` + `Intl.Collator` over `@owlmeans/payment`'s `COUNTRY_CODES`, so it
162
+ lists the buyer's language's own country names. Render it once and drive every estimate that
163
+ shares it. `disabled` (a locked country) disables it; `note` is a line under it. Selectors
164
+ `[data-country-select]` with `data-locked` and `data-country`, `[data-country-note]`.
165
+ - **`PriceEstimateAmount({ control, subtotalMinor, currency, suffix?, className? })`** is the
166
+ numbers alone, no picker: via `estimateOf` re-derived against `subtotalMinor`, a rate row for a
167
+ `taxed` status, an estimated total, and one sentence for every status that leaves no number to
168
+ trust (reverse charge, no tax, "at checkout", "choose a country"). `data-price-estimate` /
169
+ `data-status` are stable test hooks.
170
+ - When the estimate carries `local` (Adaptive Pricing found the country's currency), the tax and
171
+ total show ONLY in that currency, each marked `≈` — never both currencies at once, which would
172
+ read as two different prices for the same line. A short note explains the amount is converted
173
+ at Stripe's current rate and confirmed at checkout. With no `local`, the amounts show in
174
+ `currency` (the integration currency) with no `≈` and no note.
175
+ - **`PriceEstimateSummary`** is `CountrySelect` bound to the control's own `country`/
176
+ `onCountryChange` followed by `PriceEstimateAmount` — the one-estimate composition. A `locked`
177
+ control renders the picker disabled with the "set by your first purchase" note. Building a
178
+ shared picker composes the two pieces separately instead, one `PriceEstimateAmount` per estimate.
35
179
 
36
- It shows net credit value, processing adjustment and pre-tax subtotal from `chargeAmountMinor`, plus
37
- the note that Stripe calculates applicable tax. The policy maximum bounds net credit, not the
38
- adjusted or tax-inclusive debit.
180
+ ## `./consumer` — the EU consumer-rights pieces
39
181
 
40
- The component imports the consuming app's shadcn primitives from
41
- `@/components/ui/{dialog,input,label,button}` and uses semantic theme tokens. The package carries
42
- standalone copies under `src/@` only so its own build/tests resolve the same alias contract. A
43
- Tailwind v4 consumer scanning package source adds:
182
+ `import { … } from '@owlmeans/web-payment/consumer'` — a separate subpath, so an application that
183
+ never renders them needs neither their checkbox primitive nor its peer. Legal text comes from
184
+ `@owlmeans/payment`'s `payment-consumer-rights` copy (`consumerText`, `legalLabelsOf`,
185
+ `consentStatementOf` render the same strings the server records and mails); the interface text
186
+ comes from this package's `web-payment` resource. Neither carries product copy: the trader, the
187
+ plan title and the price line are the view's or the application's.
188
+
189
+ **The language rule.** A consent or statutory function is shown in the CONTRACT language — the
190
+ billing country's (`view.language`, `list.language`, the `language` prop) — with a toggle to the
191
+ interface language and back (`useShownLanguage`, `LanguageToggle`). The whole piece switches: the
192
+ legal copy through `legalTextOf(lng)` (a `LegalText`: `(path, values?, context?)`, the context
193
+ reading the `<path>_<context>` variant first), the interface strings through `paymentTextOf(lng)` (both
194
+ read any language without i18next and without draining a bundle — `resolveI18nResource`), dates
195
+ and amounts through `Intl` in that language. The toggle alone is phrased in the INTERFACE language
196
+ ("Show in German") — the reader who needs it reads that one. The links follow the shown language
197
+ (`links` prop: a record by language or a function such as `lng => linksOf(policy, lng)`; absent,
198
+ the view's own). What a person agrees to is what was on screen: toggling un-ticks the checkbox, and
199
+ the recorded body names the language shown.
200
+
201
+ | Piece | Props | Behaviour |
202
+ |---|---|---|
203
+ | `PerformanceConsentDialog` | `open, onOpenChange, view: PerformanceConsentView \| null, onConfirm(body: PerformanceConsentBody), onDecline?, uiLanguage?, pending?, error?, links?, onWithdraw?, formatAmount?` | Title and intro (count, last deadline), the purchases with date, amount and last withdrawal day (`performance-consent.purchase`), ONE checkbox carrying the whole statement (`performance-consent.checkbox` with `view.trader`), **unchecked by default**; a `view.context` shows the `_<context>` variants of the title, the intro and the checkbox — exactly the statement `consentStatementOf(lng, Performance, { trader, context })` records — and marks the dialog `data-consent-context`; confirm ("Start now" / "Jetzt beginnen" …) disabled until it is ticked; decline ("Not now"); links Billing Terms · Withdrawal information · the withdrawal function (a button when `onWithdraw`, else the links' `withdrawalFunction` page). `onConfirm` gets `{ purchaseIds, textVersion, language: <shown>, acknowledged: true, uiLanguage }`. |
204
+ | `SubscriptionStartDialog` | `open, onOpenChange, view: SubscriptionStartView \| null, planTitle, price?: ReactNode, onConfirm(body: SubscriptionStartBody), onDecline?, uiLanguage?, pending?, error?, links?, onWithdraw?` | The same shape before a subscription checkout; confirm is "Continue to payment" — the step before the paygate's own order button. The statement follows the plan's withdrawal arithmetic: a `view.context` (`startContextOf(plan)` on the server, `'units'`) shows the `_<context>` texts — exactly the statement `consentStatementOf(lng, SubscriptionStart, { trader, plan, context })` records — and marks the dialog `data-start-context`; without one the base (time-only) statement. |
205
+ | `WithdrawalFunctionButton` | `language, onClick, uiLanguage?, disabled?, variant? = 'outline'` | The statutory entry ("Withdraw from contract here", "Vertrag widerrufen", "Renoncer au contrat ici", "Odstąp od umowy tutaj") in the contract language; the interface language's label as `title` when they differ. |
206
+ | `WithdrawalForm` / `WithdrawalDialog` | `mode: 'in-app' \| 'public', language, onSubmit(body: WithdrawalBody), list?, defaults?, uiLanguage?, pending?, error?, receipt?, formatAmount?` (+ `open, onOpenChange` for the dialog; `onClose` for the form) | Three steps. **Form**: in-app a contract picker over `list.candidates` (bought, paid, last day, estimated refund), publicly a contract-reference field — plus name and e-mail, NOTHING else is asked (a public form also carries the honeypot). **Review**: the summary and the statutory confirm ("Confirm withdrawal", "Widerruf bestätigen", "Confirmer la rétractation", "Potwierdź odstąpienie od umowy"). **Receipt** (whenever `receipt` is set): the received sentence with date AND time in UTC, reference, status, refund. The dialog does not clear the receipt the application holds — reset it on close. |
207
+ | `CancellationForm` | `mode, language, onSubmit(body: CancellationBody), defaults?, uiLanguage?, pending?, error?, receipt?, onPrint?, onClose?` | **Form**: kind (ordinary / extraordinary — the reason becomes required), name, contract or customer reference, effective (earliest / a date from today), e-mail, and a honeypot. **Summary** with the statutory confirm ("Cancel now", "Jetzt kündigen", "Notification de la résiliation", "Wypowiedz teraz"). **Receipt**: received date and time in UTC, the effective date when known, status, a print button (`onPrint`, default `window.print()`). |
208
+
209
+ - `error` is `true` (the generic sentence in the shown language) or the application's own node — a
210
+ `WithdrawalUnavailable` phrased by the app, say.
211
+ - **The honeypot** (`Honeypot`, internal): an input named `website`, off screen, `tabIndex={-1}`,
212
+ inside `aria-hidden`; only a filled one is sent (`body.honeypot`), and the server answers a bot
213
+ exactly like a person.
214
+ - **Bodies** carry `language` = the shown language; an in-app withdrawal sends `purchaseId` and
215
+ `contractRef`, a public one `contractRef` only.
216
+
217
+ ### Hooks — protocol-bound, from `makeConsumerRightsProtocols`
218
+
219
+ ```ts
220
+ usePerformanceConsent(view: RegisteredEntrypoint<{}, PerformanceConsentView>,
221
+ record: RegisteredEntrypoint<{ body: PerformanceConsentBody }, PerformanceConsentResponse>,
222
+ opts?: { enabled?, uiLanguage?, links?, onWithdraw?, onRecorded?(response) })
223
+ → { view, required: boolean | null, ensure(opts?: { force? }): Promise<boolean>, refresh(), dialog }
224
+ PerformanceConsentProvider({ view, record, options?, children }) // ONE dialog for the app
225
+ useConsentGate() → { ensure, withConsent<T>(action): Promise<T>, required }
226
+ useSubscriptionStart(start: RegisteredEntrypoint<{ query: { planSku } }, SubscriptionStartView>,
227
+ requestStart: RegisteredEntrypoint<{ body: SubscriptionStartBody }, SubscriptionStartResponse>, opts?)
228
+ → { ensure(planSku, { planTitle, price? }): Promise<string | null>, view, dialog }
229
+ useWithdrawal(withdrawals | null, withdraw, { enabled? }) → { list, load, submit, receipt, pending, error, reset, form }
230
+ useCancellation(cancel) → { submit, receipt, pending, error, reset, form }
231
+ ```
232
+
233
+ - **`ensure()`** reads the view fresh: `true` when nothing is required, or once the consent was
234
+ confirmed AND recorded; `false` on a decline or a closed dialog. A failed read answers `true` —
235
+ the server's 428 stays the authority, and a flaky read never blocks work that needs no consent.
236
+ Concurrent calls share ONE pending answer (a burst of clicks opens one dialog). A record refused
237
+ with a 428 (a stale text version) reloads the view and keeps the dialog open.
238
+ - **`useSubscriptionStart().ensure`** answers the `startRequestId` to send with the checkout
239
+ (`CreateCheckoutBody.startRequestId`), `''` when none is needed (or the read failed — the
240
+ checkout's own refusal then decides), `null` on a decline.
241
+ - **`withConsent(action)`** runs the action; on a SPEND-consent refusal it asks with
242
+ `ensure({ force: true })` and retries exactly ONCE. A decline throws `ConsentDeclined`
243
+ (`isConsentDeclined`); a second refusal and every other failure propagate unchanged. Outside a
244
+ provider the gate passes through (`ensure` → `true`, refusals propagate).
245
+ - The asking state machine is `makeAsker` and the gate `makeConsentGate` (pure, React-free).
246
+ - `dialog` / `form` objects are spread into ONE piece each; `form.error` is a boolean, the raw
247
+ failure is `error`.
248
+
249
+ ### Recognising a consent refusal
250
+
251
+ `isConsentRefusal(e)` — `consentRefusalOf(e) != null` (the class after the registry, or its marker
252
+ in `message`/`type`) OR `httpStatusOf(e) === 428` (`@owlmeans/api/status`: a production body is only
253
+ an incident id, so the status is all that is left). Read on the error AND on what it wraps —
254
+ `cause`, `error`, `original`, `originalError`, `inner`, `reason`, an aggregate's `errors`, and
255
+ strings — five levels deep with a cycle guard, so a planning `CommitFailed`
256
+ (`planning:commit-failed:<transition>:<the refusal's text>`) is recognised without depending on
257
+ planning. `consentRefusalKindOf(e)` → `'performance' | 'subscription-start' | 'unknown'` (a bare
258
+ 428) `| null`; `isPerformanceConsentRefusal(e)` is what the gate retries on (performance or
259
+ unknown — never a subscription start).
260
+
261
+ ### Selectors
262
+
263
+ | Piece | Selectors |
264
+ |---|---|
265
+ | Spend consent | `[data-consent-dialog]` (also `[data-performance-consent]`) with `data-language` (shown), `data-contract-language`, `data-text-version`, `data-consent-context` (only when the view has a context); `[data-consent-purchase]` with `data-purchase-id`, `data-deadline` (ISO); `[data-consent-statement]`; `[data-consent-checkbox]` (Radix `data-state`); `[data-consent-confirm]`; `[data-consent-decline]`; `[data-consent-language-toggle]` (also `[data-language-toggle]` with `data-target-language`) |
266
+ | Legal links (both dialogs) | `[data-legal-links]`, `[data-legal-link="billing-terms" \| "withdrawal-information" \| "withdrawal-function"]` |
267
+ | Subscription start | `[data-start-dialog]` (also `[data-subscription-start]`) with `data-language`, `data-contract-language`, `data-plan-sku`, `data-start-context` (only when the view has a context); `[data-start-statement]`, `[data-start-price]`, `[data-start-checkbox]`, `[data-start-confirm]`, `[data-start-decline]`, `[data-start-language-toggle]` |
268
+ | Withdrawal | `[data-withdrawal-function]` (`data-language`); `[data-withdrawal-dialog]` (`data-language`, `data-step`); `[data-withdrawal-form]` with `data-withdrawal-step="form\|review\|receipt"` (also `data-step`, `data-mode`, `data-language`); `[data-withdrawal-candidate]` (`data-purchase-id`, `data-contract-ref`) and its radio `[data-withdrawal-candidate-input="<purchaseId>"]`; `[data-withdrawal-estimate]` (`data-refund-minor`); `[data-withdrawal-field="contract\|name\|email"]`; `[data-withdrawal-continue]`, `[data-withdrawal-back]`, `[data-withdrawal-confirm]`; `[data-withdrawal-receipt]` (`data-status`, `data-declaration-id`, `data-received-at`), `[data-withdrawal-reference]`, `[data-withdrawal-received-at]`, `[data-withdrawal-refund]`; `[data-withdrawal-language-toggle]` |
269
+ | Cancellation | `[data-cancellation-form]` with `data-cancellation-step="form\|review\|receipt"` (also `data-step`, `data-mode`, `data-language`); `[data-cancellation-field="kind\|reason\|name\|contract\|effective\|date\|email"]` (kind radios carry `data-kind`, effective radios `data-effective`); `[data-cancellation-continue]`, `[data-cancellation-back]`, `[data-cancellation-confirm]`, `[data-cancellation-summary]`; `[data-cancellation-receipt]` (`data-status`, `data-declaration-id`, `data-received-at`, `data-effective-at`), `[data-cancellation-effective]`, `[data-cancellation-print]`; `[data-cancellation-language-toggle]` |
270
+ | Honeypot | `[data-honeypot]` |
271
+
272
+ ## The `@/components/ui/*` contract
273
+
274
+ Components import the consuming app's shadcn primitives through the app alias:
275
+ `@/components/ui/{button,dialog,input,label,select}` for the dialog and estimate summary,
276
+ `@/components/ui/{card,progress,button}` for the pieces (`Card`, `CardHeader`, `CardTitle`,
277
+ `CardDescription`, `CardContent`, `CardFooter`, `Progress`), and — **only from `./consumer`** —
278
+ `@/components/ui/checkbox` (`export { Checkbox }` with the Radix Root props, the stock shadcn file).
279
+ The consumer therefore provides those files and the peers `@radix-ui/react-progress` /
280
+ `@radix-ui/react-select` beside the dialog's Radix ones, plus `@radix-ui/react-checkbox` when it
281
+ imports `./consumer`. The package carries standalone copies under `src/@` only so its own build and
282
+ tests resolve the same alias; they are never exported (its checkbox draws an inline SVG check, no
283
+ icon library). Styling uses semantic theme tokens only (`primary`, `muted-foreground`,
284
+ `destructive`, `border`, `card`).
285
+
286
+ A Tailwind v4 consumer scanning package source adds:
44
287
 
45
288
  ```css
46
289
  @source "../../node_modules/@owlmeans/web-payment/src";
47
290
  ```
48
291
 
49
- All visible strings live in the `web-payment` library resource and must stay present in all seven
50
- languages: `en pl ru be uk es de`.
292
+ ## Strings
293
+
294
+ All visible interface strings live in the `web-payment` library resource (`lib` namespace,
295
+ `WEB_PAYMENT_RESOURCE`) in all eight languages — `en pl ru be uk es de fr` — under
296
+ `amount-checkout` (incl. `above-limit`), `checkout-limit` (`note`, `blocked`, `resets`,
297
+ `reason.{per-purchase,window,total,hold,other}`), `entitlement` (`plan`, `status`, `limit`, `promo`,
298
+ `capability`), `estimate` (`country`, `country-locked`, `tax-type`, `rate`, `total`,
299
+ `converted-note`, `status`, `loading`, `failed`), and for `./consumer`: `consumer` (toggle, steps,
300
+ receipt labels, validation), `withdrawal` (candidate lines, estimate, kinds, `status.*`) and
301
+ `cancellation` (`effective-at`, `status.*`). The legal copy is NOT here — it is `@owlmeans/payment`'s.
302
+
303
+ - A new key lands in every language at once, with the same `{{placeholders}}`; the parity spec
304
+ walks the resources recursively and scans every language for the forbidden wording (never
305
+ "non-refundable" or a translation of it: the right of withdrawal EXPIRES for what was used, and
306
+ unused credits ARE reimbursed). Every `WithdrawalStatus`, `CancellationStatus` (incl. `review` —
307
+ extraordinary and unmatched public cancellations) and `PurchaseKind` value has its string; the
308
+ spec iterates the enums, so a new status fails it until all eight languages phrase it.
309
+ - The last withdrawal day shown anywhere is `@owlmeans/payment`'s `lastWithdrawalDayOf(deadline)`
310
+ (the deadline is exclusive), formatted as a UTC date — the rule the server's e-mails use.
311
+ - The `consumer`/`withdrawal`/`cancellation` branches take the register of the legal copy they sit
312
+ beside (de and es formal, like `payment-consumer-rights`), while `amount-checkout` and
313
+ `checkout-limit` keep the dialog's.
314
+ - An application overrides a string with `addI18nApp(lng, 'web-payment', data, { ns: LIB_NAMESPACE })`
315
+ — `paymentTextOf` reads that override too.
316
+
317
+ ## Tests
318
+
319
+ Category D plus unit specs: `bun test ./tests` runs the selector, hook, cache, consent-refusal
320
+ (`consent-refusal.spec.ts`: class, marker in message/type/string, bare 428 `ApiStatusError`,
321
+ stamped status, `cause`/`error`/aggregate nesting, a `CommitFailed`-shaped message, negatives) and
322
+ asker/gate (`consumer-hooks.spec.ts`: `makeAsker` sharing and failing open, `withConsent` retrying
323
+ exactly once, first-render hook shapes with `react-dom/server`) specs, the 8-language parity spec,
324
+ and drives chromium through the Vite harness, which picks a surface by `?case=` (none = the dialog,
325
+ `pieces`, `hook`, `shared-estimate`; `consent` (`&context=` a consent copy variant), `start` (`&context=units`), `withdrawal`, `withdrawal-public`,
326
+ `cancellation`, `limit` (`&blocked=true`), `country-locked`, `consent-gate`), with `?lng=` the
327
+ contract language and `?ui=` the interface language, and for the dialog an `?estimate=<name>`
328
+ canned `PriceEstimateControl` fixture (`tests/harness/estimate-fixtures.ts`,
329
+ `tests/harness/consumer-fixtures.ts` — no server, no Stripe). The harness pre-bundles every runtime
330
+ dependency in `optimizeDeps.include` (`@radix-ui/react-checkbox` included); one discovered
331
+ mid-navigation loads a second React and the page throws "Invalid hook call".
@@ -0,0 +1,10 @@
1
+ import * as React from 'react';
2
+ declare function Card({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
3
+ declare function CardHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
4
+ declare function CardTitle({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
5
+ declare function CardDescription({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
6
+ declare function CardAction({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
7
+ declare function CardContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
8
+ declare function CardFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
9
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
10
+ //# sourceMappingURL=card.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card.d.ts","sourceRoot":"","sources":["../../../../src/@/components/ui/card.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,iBAAS,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWjE;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWvE;AAED,iBAAS,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQtE;AAED,iBAAS,eAAe,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQ5E;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAWvE;AAED,iBAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQxE;AAED,iBAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,qBAQvE;AAED,OAAO,EACL,IAAI,EACJ,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,WAAW,GACZ,CAAA"}
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from '@/lib/utils';
3
+ function Card({ className, ...props }) {
4
+ return (_jsx("div", { "data-slot": "card", className: cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className), ...props }));
5
+ }
6
+ function CardHeader({ className, ...props }) {
7
+ return (_jsx("div", { "data-slot": "card-header", className: cn("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", className), ...props }));
8
+ }
9
+ function CardTitle({ className, ...props }) {
10
+ return (_jsx("div", { "data-slot": "card-title", className: cn("leading-none font-semibold", className), ...props }));
11
+ }
12
+ function CardDescription({ className, ...props }) {
13
+ return (_jsx("div", { "data-slot": "card-description", className: cn("text-muted-foreground text-sm", className), ...props }));
14
+ }
15
+ function CardAction({ className, ...props }) {
16
+ return (_jsx("div", { "data-slot": "card-action", className: cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className), ...props }));
17
+ }
18
+ function CardContent({ className, ...props }) {
19
+ return (_jsx("div", { "data-slot": "card-content", className: cn("px-6", className), ...props }));
20
+ }
21
+ function CardFooter({ className, ...props }) {
22
+ return (_jsx("div", { "data-slot": "card-footer", className: cn("flex items-center px-6 [.border-t]:pt-6", className), ...props }));
23
+ }
24
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
25
+ //# sourceMappingURL=card.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"card.js","sourceRoot":"","sources":["../../../../src/@/components/ui/card.tsx"],"names":[],"mappings":";AAGA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAEhC,SAAS,IAAI,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IAChE,OAAO,CACL,2BACY,MAAM,EAChB,SAAS,EAAE,EAAE,CACX,mFAAmF,EACnF,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACtE,OAAO,CACL,2BACY,aAAa,EACvB,SAAS,EAAE,EAAE,CACX,4JAA4J,EAC5J,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,SAAS,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACrE,OAAO,CACL,2BACY,YAAY,EACtB,SAAS,EAAE,EAAE,CAAC,4BAA4B,EAAE,SAAS,CAAC,KAClD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,eAAe,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IAC3E,OAAO,CACL,2BACY,kBAAkB,EAC5B,SAAS,EAAE,EAAE,CAAC,+BAA+B,EAAE,SAAS,CAAC,KACrD,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACtE,OAAO,CACL,2BACY,aAAa,EACvB,SAAS,EAAE,EAAE,CACX,gEAAgE,EAChE,SAAS,CACV,KACG,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACvE,OAAO,CACL,2BACY,cAAc,EACxB,SAAS,EAAE,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,KAC5B,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAA+B;IACtE,OAAO,CACL,2BACY,aAAa,EACvB,SAAS,EAAE,EAAE,CAAC,yCAAyC,EAAE,SAAS,CAAC,KAC/D,KAAK,GACT,CACH,CAAA;AACH,CAAC;AAED,OAAO,EACL,IAAI,EACJ,UAAU,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,WAAW,GACZ,CAAA"}
@@ -0,0 +1,5 @@
1
+ import * as React from 'react';
2
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
3
+ /** The shadcn checkbox, with an inline check mark instead of an icon library. */
4
+ export declare const Checkbox: ({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) => React.JSX.Element;
5
+ //# sourceMappingURL=checkbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkbox.d.ts","sourceRoot":"","sources":["../../../../src/@/components/ui/checkbox.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,iBAAiB,MAAM,0BAA0B,CAAA;AAG7D,iFAAiF;AACjF,eAAO,MAAM,QAAQ,4BAA6B,KAAK,CAAC,cAAc,CAAC,OAAO,iBAAiB,CAAC,IAAI,CAAC,sBAiB1E,CAAA"}
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
3
+ import { cn } from '@/lib/utils';
4
+ /** The shadcn checkbox, with an inline check mark instead of an icon library. */
5
+ export const Checkbox = ({ className, ...props }) => _jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn('peer border-input size-4 shrink-0 cursor-pointer rounded-[4px] border shadow-xs outline-none transition-shadow', 'data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "flex items-center justify-center text-current", children: _jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", className: "size-3.5", fill: "none", stroke: "currentColor", strokeWidth: 3, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M20 6 9 17l-5-5" }) }) }) });
6
+ //# sourceMappingURL=checkbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkbox.js","sourceRoot":"","sources":["../../../../src/@/components/ui/checkbox.tsx"],"names":[],"mappings":";AACA,OAAO,KAAK,iBAAiB,MAAM,0BAA0B,CAAA;AAC7D,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAEhC,iFAAiF;AACjF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAuD,EAAE,EAAE,CACvG,KAAC,iBAAiB,CAAC,IAAI,iBACX,UAAU,EACpB,SAAS,EAAE,EAAE,CACX,gHAAgH,EAChH,kHAAkH,EAClH,+HAA+H,EAC/H,SAAS,CACV,KACG,KAAK,YAET,KAAC,iBAAiB,CAAC,SAAS,iBAAW,oBAAoB,EAAC,SAAS,EAAC,+CAA+C,YACnH,cAAK,OAAO,EAAC,WAAW,iBAAa,MAAM,EAAC,SAAS,EAAC,UAAU,EAAC,IAAI,EAAC,MAAM,EAAC,MAAM,EAAC,cAAc,EAChG,WAAW,EAAE,CAAC,EAAE,aAAa,EAAC,OAAO,EAAC,cAAc,EAAC,OAAO,YAC5D,eAAM,CAAC,EAAC,iBAAiB,GAAG,GACxB,GACsB,GACP,CAAA"}
@@ -0,0 +1,5 @@
1
+ import * as React from 'react';
2
+ import * as ProgressPrimitive from '@radix-ui/react-progress';
3
+ declare function Progress({ className, value, ...props }: React.ComponentProps<typeof ProgressPrimitive.Root>): React.JSX.Element;
4
+ export { Progress };
5
+ //# sourceMappingURL=progress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.d.ts","sourceRoot":"","sources":["../../../../src/@/components/ui/progress.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,iBAAiB,MAAM,0BAA0B,CAAA;AAI7D,iBAAS,QAAQ,CAAC,EAChB,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EACT,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,iBAAiB,CAAC,IAAI,CAAC,qBA4BrD;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as ProgressPrimitive from '@radix-ui/react-progress';
3
+ import { cn } from '@/lib/utils';
4
+ function Progress({ className, value, ...props }) {
5
+ const indeterminate = value == null;
6
+ return (_jsx(ProgressPrimitive.Root, { "data-slot": "progress", className: cn("bg-primary/20 relative h-2 w-full overflow-hidden rounded-full", className), value: indeterminate ? undefined : value, ...props, children: _jsx(ProgressPrimitive.Indicator, { "data-slot": "progress-indicator", "data-state": indeterminate ? 'indeterminate' : 'determinate', className: cn("bg-primary h-full w-full flex-1 transition-all", indeterminate && "absolute inset-y-0 left-0 w-1/3 animate-[progress-indeterminate_1.5s_linear_infinite]"), style: indeterminate
7
+ ? undefined
8
+ : { transform: `translateX(-${100 - (value || 0)}%)` } }) }));
9
+ }
10
+ export { Progress };
11
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.js","sourceRoot":"","sources":["../../../../src/@/components/ui/progress.tsx"],"names":[],"mappings":";AAKA,OAAO,KAAK,iBAAiB,MAAM,0BAA0B,CAAA;AAE7D,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAEhC,SAAS,QAAQ,CAAC,EAChB,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EAC4C;IACpD,MAAM,aAAa,GAAG,KAAK,IAAI,IAAI,CAAA;IAEnC,OAAO,CACL,KAAC,iBAAiB,CAAC,IAAI,iBACX,UAAU,EACpB,SAAS,EAAE,EAAE,CACX,gEAAgE,EAChE,SAAS,CACV,EACD,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,KACpC,KAAK,YAET,KAAC,iBAAiB,CAAC,SAAS,iBAChB,oBAAoB,gBAClB,aAAa,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,aAAa,EAC3D,SAAS,EAAE,EAAE,CACX,gDAAgD,EAChD,aAAa,IAAI,uFAAuF,CACzG,EACD,KAAK,EACH,aAAa;gBACX,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,EAAE,SAAS,EAAE,eAAe,GAAG,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,EAAE,GAE1D,GACqB,CAC1B,CAAA;AACH,CAAC;AAED,OAAO,EAAE,QAAQ,EAAE,CAAA"}
@@ -0,0 +1,8 @@
1
+ import * as React from 'react';
2
+ import * as SelectPrimitive from '@radix-ui/react-select';
3
+ export declare const Select: (props: React.ComponentProps<typeof SelectPrimitive.Root>) => React.JSX.Element;
4
+ export declare const SelectValue: (props: React.ComponentProps<typeof SelectPrimitive.Value>) => React.JSX.Element;
5
+ export declare const SelectTrigger: ({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger>) => React.JSX.Element;
6
+ export declare const SelectContent: ({ className, children, position, ...props }: React.ComponentProps<typeof SelectPrimitive.Content>) => React.JSX.Element;
7
+ export declare const SelectItem: ({ className, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) => React.JSX.Element;
8
+ //# sourceMappingURL=select.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../../../src/@/components/ui/select.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAA;AAGzD,eAAO,MAAM,MAAM,UAAW,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,sBAAwC,CAAA;AACvH,eAAO,MAAM,WAAW,UAAW,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,KAAK,CAAC,sBAAyC,CAAA;AAE9H,eAAO,MAAM,aAAa,sCAAuC,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,OAAO,CAAC,sBAepH,CAAA;AAED,eAAO,MAAM,aAAa,gDAA4D,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,OAAO,CAAC,sBAsBzI,CAAA;AAED,eAAO,MAAM,UAAU,sCAAuC,KAAK,CAAC,cAAc,CAAC,OAAO,eAAe,CAAC,IAAI,CAAC,sBAa9G,CAAA"}
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as SelectPrimitive from '@radix-ui/react-select';
3
+ import { cn } from '@/lib/utils';
4
+ export const Select = (props) => _jsx(SelectPrimitive.Root, { ...props });
5
+ export const SelectValue = (props) => _jsx(SelectPrimitive.Value, { ...props });
6
+ export const SelectTrigger = ({ className, children, ...props }) => (_jsxs(SelectPrimitive.Trigger, { className: cn('border-input data-[placeholder]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50', 'flex h-9 w-full items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs', 'outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx("span", { "aria-hidden": "true", className: "text-muted-foreground shrink-0", children: "\u25BE" }) })] }));
7
+ export const SelectContent = ({ className, children, position = 'popper', ...props }) => (_jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { position: position, className: cn('bg-popover text-popover-foreground relative z-50 max-h-96 min-w-[8rem] overflow-y-auto rounded-md border shadow-md', position === 'popper' && 'data-[side=bottom]:translate-y-1 data-[side=top]:-translate-y-1', className), ...props, children: [_jsx(SelectPrimitive.ScrollUpButton, { className: "flex items-center justify-center py-1", children: _jsx("span", { "aria-hidden": "true", children: "\u25B4" }) }), _jsx(SelectPrimitive.Viewport, { className: cn('p-1', position === 'popper' && 'w-full min-w-[var(--radix-select-trigger-width)]'), children: children }), _jsx(SelectPrimitive.ScrollDownButton, { className: "flex items-center justify-center py-1", children: _jsx("span", { "aria-hidden": "true", children: "\u25BE" }) })] }) }));
8
+ export const SelectItem = ({ className, children, ...props }) => (_jsxs(SelectPrimitive.Item, { className: cn('focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', className), ...props, children: [_jsx("span", { className: "absolute right-2 flex size-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { "aria-hidden": "true", children: "\u2713" }) }), _jsx(SelectPrimitive.ItemText, { children: children })] }));
9
+ //# sourceMappingURL=select.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select.js","sourceRoot":"","sources":["../../../../src/@/components/ui/select.tsx"],"names":[],"mappings":";AACA,OAAO,KAAK,eAAe,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAEhC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,KAAwD,EAAE,EAAE,CAAC,KAAC,eAAe,CAAC,IAAI,OAAK,KAAK,GAAI,CAAA;AACvH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAyD,EAAE,EAAE,CAAC,KAAC,eAAe,CAAC,KAAK,OAAK,KAAK,GAAI,CAAA;AAE9H,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAwD,EAAE,EAAE,CAAC,CACxH,MAAC,eAAe,CAAC,OAAO,IACtB,SAAS,EAAE,EAAE,CACX,4GAA4G,EAC5G,iHAAiH,EACjH,qHAAqH,EACrH,SAAS,CACV,KACG,KAAK,aAER,QAAQ,EACT,KAAC,eAAe,CAAC,IAAI,IAAC,OAAO,kBAC3B,8BAAkB,MAAM,EAAC,SAAS,EAAC,gCAAgC,uBAAS,GACvD,IACC,CAC3B,CAAA;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,GAAG,QAAQ,EAAE,GAAG,KAAK,EAAwD,EAAE,EAAE,CAAC,CAC7I,KAAC,eAAe,CAAC,MAAM,cACrB,MAAC,eAAe,CAAC,OAAO,IACtB,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,EAAE,CACX,oHAAoH,EACpH,QAAQ,KAAK,QAAQ,IAAI,iEAAiE,EAC1F,SAAS,CACV,KACG,KAAK,aAET,KAAC,eAAe,CAAC,cAAc,IAAC,SAAS,EAAC,uCAAuC,YAC/E,8BAAkB,MAAM,uBAAS,GACF,EACjC,KAAC,eAAe,CAAC,QAAQ,IAAC,SAAS,EAAE,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,QAAQ,IAAI,kDAAkD,CAAC,YACxH,QAAQ,GACgB,EAC3B,KAAC,eAAe,CAAC,gBAAgB,IAAC,SAAS,EAAC,uCAAuC,YACjF,8BAAkB,MAAM,uBAAS,GACA,IACX,GACH,CAC1B,CAAA;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAqD,EAAE,EAAE,CAAC,CAClH,MAAC,eAAe,CAAC,IAAI,IACnB,SAAS,EAAE,EAAE,CACX,mOAAmO,EACnO,SAAS,CACV,KACG,KAAK,aAET,eAAM,SAAS,EAAC,4DAA4D,YAC1E,KAAC,eAAe,CAAC,aAAa,mBAAa,MAAM,uBAAkC,GAC9E,EACP,KAAC,eAAe,CAAC,QAAQ,cAAE,QAAQ,GAA4B,IAC1C,CACxB,CAAA"}
@@ -0,0 +1,7 @@
1
+ import type { CapabilityListProps } from './types.js';
2
+ /**
3
+ * The capabilities of a view, labelled by the application. A capability without a label is not
4
+ * rendered, so a view may carry grants a screen has nothing to say about.
5
+ */
6
+ export declare const CapabilityList: ({ capabilities, labels, onlyGranted, className }: CapabilityListProps) => import("react").JSX.Element;
7
+ //# sourceMappingURL=capability-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability-list.d.ts","sourceRoot":"","sources":["../src/capability-list.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAkBrD;;;GAGG;AACH,eAAO,MAAM,cAAc,qDAA8D,mBAAmB,gCAyB3G,CAAA"}
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PromoNote, useEntitlementCopy } from './copy.js';
3
+ const classes = (...names) => names.filter(Boolean).join(' ');
4
+ /** One row per param, in view order: granted when any row grants it, the promo of that row. */
5
+ const collapse = (capabilities) => {
6
+ const rows = new Map();
7
+ for (const capability of capabilities) {
8
+ const seen = rows.get(capability.param);
9
+ if (seen == null || (!seen.granted && capability.granted)) {
10
+ rows.set(capability.param, capability);
11
+ }
12
+ }
13
+ return [...rows.values()];
14
+ };
15
+ /**
16
+ * The capabilities of a view, labelled by the application. A capability without a label is not
17
+ * rendered, so a view may carry grants a screen has nothing to say about.
18
+ */
19
+ export const CapabilityList = ({ capabilities, labels, onlyGranted = false, className }) => {
20
+ const copy = useEntitlementCopy();
21
+ const rows = collapse(capabilities).filter(capability => (labels[capability.param] ?? '') !== '' && (!onlyGranted || capability.granted));
22
+ return _jsx("ul", { "data-capability-list": "", className: classes('grid gap-2 text-sm', className), children: rows.map(capability => _jsxs("li", { "data-capability": capability.param, "data-granted": capability.granted ? 'true' : 'false', className: "flex items-start gap-2", children: [_jsx("span", { "aria-hidden": "true", className: classes('mt-0.5 inline-flex size-4 shrink-0 items-center justify-center text-xs font-semibold', capability.granted ? 'text-primary' : 'text-muted-foreground'), children: capability.granted ? '✓' : '–' }), _jsxs("span", { className: "grid gap-0.5", children: [_jsxs("span", { className: capability.granted ? undefined : 'text-muted-foreground', children: [labels[capability.param], _jsx("span", { className: "sr-only", children: ` — ${copy.text(capability.granted ? 'capability.included' : 'capability.not-included')}` })] }), capability.promo != null && _jsx("span", { className: "text-xs", children: _jsx(PromoNote, { promo: capability.promo, copy: copy }) })] })] }, capability.param)) });
23
+ };
24
+ //# sourceMappingURL=capability-list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability-list.js","sourceRoot":"","sources":["../src/capability-list.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AAGzD,MAAM,OAAO,GAAG,CAAC,GAAG,KAA+C,EAAU,EAAE,CAC7E,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAEjC,+FAA+F;AAC/F,MAAM,QAAQ,GAAG,CAAC,YAA8B,EAAoB,EAAE;IACpE,MAAM,IAAI,GAAG,IAAI,GAAG,EAA0B,CAAA;IAC9C,KAAK,MAAM,UAAU,IAAI,YAAY,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,CAAA;QACvC,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1D,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,EAAE,UAAU,CAAC,CAAA;QACxC,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;AAC3B,CAAC,CAAA;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,KAAK,EAAE,SAAS,EAAuB,EAAE,EAAE;IAC9G,MAAM,IAAI,GAAG,kBAAkB,EAAE,CAAA;IACjC,MAAM,IAAI,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CACtD,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,WAAW,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAA;IAElF,OAAO,qCAAyB,EAAE,EAAC,SAAS,EAAE,OAAO,CAAC,oBAAoB,EAAE,SAAS,CAAC,YACnF,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,iCAEL,UAAU,CAAC,KAAK,kBACnB,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EACnD,SAAS,EAAC,wBAAwB,aAElC,8BAAkB,MAAM,EAAC,SAAS,EAAE,OAAO,CACzC,sFAAsF,EACtF,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,uBAAuB,CAC9D,YAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAQ,EAC1C,gBAAM,SAAS,EAAC,cAAc,aAC5B,gBAAM,SAAS,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,uBAAuB,aACtE,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,EACzB,eAAM,SAAS,EAAC,SAAS,YAAE,MAAM,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,yBAAyB,CAAC,EAAE,GAAQ,IACvH,EACN,UAAU,CAAC,KAAK,IAAI,IAAI,IAAI,eAAM,SAAS,EAAC,SAAS,YAAC,KAAC,SAAS,IAAC,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,GAAI,GAAO,IAC3G,KAfF,UAAU,CAAC,KAAK,CAgBlB,CAAC,GACH,CAAA;AACP,CAAC,CAAA"}