@base316/react 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +15 -15
  3. package/dist/index.cjs +4150 -160
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +48 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4136 -165
  8. package/dist/index.js.map +1 -1
  9. package/dist/manifest.json +1161 -0
  10. package/dist/src/AcceptInvitation.d.ts +27 -0
  11. package/dist/src/AcceptInvitation.d.ts.map +1 -0
  12. package/dist/src/ActiveSessions.d.ts +21 -0
  13. package/dist/src/ActiveSessions.d.ts.map +1 -0
  14. package/dist/src/ApiKeyManager.d.ts +22 -0
  15. package/dist/src/ApiKeyManager.d.ts.map +1 -0
  16. package/dist/src/Chart.d.ts +42 -0
  17. package/dist/src/Chart.d.ts.map +1 -0
  18. package/dist/src/DataTable.d.ts +56 -0
  19. package/dist/src/DataTable.d.ts.map +1 -0
  20. package/dist/src/DeviceApproval.d.ts +30 -0
  21. package/dist/src/DeviceApproval.d.ts.map +1 -0
  22. package/dist/src/ForgotPassword.d.ts +24 -0
  23. package/dist/src/ForgotPassword.d.ts.map +1 -0
  24. package/dist/src/Invoices.d.ts.map +1 -1
  25. package/dist/src/Kanban.d.ts +40 -0
  26. package/dist/src/Kanban.d.ts.map +1 -0
  27. package/dist/src/PasskeyManager.d.ts.map +1 -1
  28. package/dist/src/PayInvoice.d.ts +41 -0
  29. package/dist/src/PayInvoice.d.ts.map +1 -0
  30. package/dist/src/PricingTable.d.ts +43 -11
  31. package/dist/src/PricingTable.d.ts.map +1 -1
  32. package/dist/src/PrivacyControls.d.ts +28 -0
  33. package/dist/src/PrivacyControls.d.ts.map +1 -0
  34. package/dist/src/RecordDetail.d.ts +31 -0
  35. package/dist/src/RecordDetail.d.ts.map +1 -0
  36. package/dist/src/RecordForm.d.ts +32 -0
  37. package/dist/src/RecordForm.d.ts.map +1 -0
  38. package/dist/src/RelationPicker.d.ts +23 -0
  39. package/dist/src/RelationPicker.d.ts.map +1 -0
  40. package/dist/src/ResetPassword.d.ts +24 -0
  41. package/dist/src/ResetPassword.d.ts.map +1 -0
  42. package/dist/src/SignIn.d.ts +11 -1
  43. package/dist/src/SignIn.d.ts.map +1 -1
  44. package/dist/src/SignUp.d.ts.map +1 -1
  45. package/dist/src/SubscriptionManager.d.ts.map +1 -1
  46. package/dist/src/TeamMembers.d.ts +49 -0
  47. package/dist/src/TeamMembers.d.ts.map +1 -0
  48. package/dist/src/TwoFactorManager.d.ts +29 -0
  49. package/dist/src/TwoFactorManager.d.ts.map +1 -0
  50. package/dist/src/VerifyEmail.d.ts +24 -0
  51. package/dist/src/VerifyEmail.d.ts.map +1 -0
  52. package/dist/src/appearance.d.ts +2 -2
  53. package/dist/src/appearance.d.ts.map +1 -1
  54. package/dist/src/confirm.d.ts +32 -0
  55. package/dist/src/confirm.d.ts.map +1 -0
  56. package/dist/src/fields.d.ts +44 -0
  57. package/dist/src/fields.d.ts.map +1 -0
  58. package/dist/src/manifest.d.ts +84 -0
  59. package/dist/src/manifest.d.ts.map +1 -0
  60. package/dist/src/test/harness.d.ts +204 -0
  61. package/dist/src/test/harness.d.ts.map +1 -0
  62. package/dist/src/test/setup.d.ts +2 -0
  63. package/dist/src/test/setup.d.ts.map +1 -0
  64. package/dist/src/useCollection.d.ts +79 -0
  65. package/dist/src/useCollection.d.ts.map +1 -0
  66. package/dist/src/usePaymentLink.d.ts +41 -0
  67. package/dist/src/usePaymentLink.d.ts.map +1 -0
  68. package/dist/src/usePlans.d.ts +66 -0
  69. package/dist/src/usePlans.d.ts.map +1 -0
  70. package/package.json +36 -28
package/dist/index.js CHANGED
@@ -27,13 +27,13 @@ var VARIABLES = {
27
27
  fontSize: "--b316-font-size",
28
28
  spacing: "--b316-spacing"
29
29
  };
30
- function appearanceToStyle(appearance) {
31
- if (!appearance) {
30
+ function appearanceToStyle(appearance2) {
31
+ if (!appearance2) {
32
32
  return {};
33
33
  }
34
34
  const style = {};
35
35
  for (const [key, variable] of Object.entries(VARIABLES)) {
36
- const value = appearance[key];
36
+ const value = appearance2[key];
37
37
  if (value !== void 0) {
38
38
  style[variable] = value;
39
39
  }
@@ -119,6 +119,18 @@ var STYLESHEET = `
119
119
  text-decoration: underline;
120
120
  }
121
121
 
122
+ /*
123
+ * An armed confirmation: the question and its way out, side by side.
124
+ *
125
+ * Inline rather than a dialog, because a sandboxed iframe does not get dialogs
126
+ * and these components are meant to be embedded in one.
127
+ */
128
+ .b316-confirm {
129
+ display: inline-flex;
130
+ gap: .4rem;
131
+ align-items: center;
132
+ }
133
+
122
134
  .b316-error {
123
135
  background: var(--b316-color-danger-bg);
124
136
  color: var(--b316-color-danger);
@@ -227,11 +239,88 @@ var STYLESHEET = `
227
239
  }
228
240
 
229
241
  .b316-row { display: flex; justify-content: space-between; gap: 16px; align-items: center; }
242
+
243
+ /* Credential lists. A row is a thing plus the actions on it, and the actions
244
+ stay put as names vary in length, so the eye tracks one column not two. */
245
+ .b316-list { list-style: none; margin: 0; padding: 0; }
246
+
247
+ .b316-list-item {
248
+ display: flex;
249
+ justify-content: space-between;
250
+ align-items: flex-start;
251
+ gap: 16px;
252
+ padding: 12px 0;
253
+ border-top: 1px solid var(--b316-color-border);
254
+ }
255
+
256
+ .b316-list-item:first-child { border-top: none; }
257
+
258
+ /* Carries a secret shown exactly once, so it has to read as different from the
259
+ rest of the card rather than as another paragraph. */
260
+ .b316-notice {
261
+ border: 1px solid var(--b316-color-border);
262
+ border-radius: var(--b316-radius);
263
+ background: var(--b316-color-subtle, rgba(0,0,0,0.03));
264
+ padding: 12px;
265
+ font-size: 0.93em;
266
+ }
267
+
268
+ .b316-checkbox {
269
+ display: flex;
270
+ align-items: baseline;
271
+ gap: 8px;
272
+ padding: 4px 0;
273
+ cursor: pointer;
274
+ }
275
+
276
+ .b316-checkbox input[disabled] { cursor: default; }
277
+ .b316-checkbox.b316-muted { cursor: default; }
230
278
  `;
231
279
  function cls(base, extra) {
232
280
  return extra ? `${base} ${extra}` : base;
233
281
  }
234
282
  var STYLESHEET_EXTRA = `
283
+ .b316-pay__element { margin-bottom: var(--b316-spacing); min-height: 40px; }
284
+
285
+ .b316-pay__submit {
286
+ width: 100%;
287
+ padding: 11px 14px;
288
+ border: 0;
289
+ border-radius: calc(var(--b316-radius) / 1.6);
290
+ background: var(--b316-color-primary);
291
+ color: #fff;
292
+ font: inherit;
293
+ font-weight: 600;
294
+ cursor: pointer;
295
+ }
296
+
297
+ .b316-pay__submit[disabled] { opacity: 0.55; cursor: progress; }
298
+
299
+ .b316-pay__error {
300
+ background: var(--b316-color-danger-bg);
301
+ color: var(--b316-color-danger);
302
+ padding: 8px 10px;
303
+ border-radius: calc(var(--b316-radius) / 2);
304
+ font-size: 0.9em;
305
+ margin-bottom: var(--b316-spacing);
306
+ }
307
+
308
+ .b316-pay__done {
309
+ background: var(--b316-color-success-bg, #f0fdf4);
310
+ color: var(--b316-color-success);
311
+ padding: 12px 14px;
312
+ border-radius: calc(var(--b316-radius) / 2);
313
+ text-align: center;
314
+ margin: 0;
315
+ }
316
+
317
+ .b316-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid var(--b316-color-border); }
318
+ .b316-tab {
319
+ flex: 1; padding: 8px 4px; background: none; border: 0; cursor: pointer;
320
+ font: inherit; font-size: .95em; color: var(--b316-color-muted);
321
+ border-bottom: 2px solid transparent; margin-bottom: -1px;
322
+ }
323
+ .b316-tab-on { color: var(--b316-color-text); border-bottom-color: var(--b316-color-primary); }
235
324
  .b316-branding { display: flex; flex-direction: column; gap: 4px; }
236
325
  .b316-branding-logo { max-height: 40px; width: auto; align-self: flex-start; }
237
326
  .b316-branding-name { font-size: 1.5em; font-weight: 650; letter-spacing: -.01em; }
@@ -267,12 +356,24 @@ var STYLESHEET_EXTRA = `
267
356
  .b316-invoice-totals { display: flex; flex-direction: column; gap: 6px; margin-left: auto; min-width: 240px; }
268
357
  .b316-invoice-total { font-weight: 650; border-top: 1px solid var(--b316-color-border); padding-top: 6px; }
269
358
 
270
- /* Printing emits the invoice alone. Without this the browser would paginate the
271
- whole application around it, which is not something anyone wants to file. */
359
+ /* Printing an invoice emits the invoice alone, and printing anything else is
360
+ none of this package's business.
361
+
362
+ This used to start with a bare "body * { visibility: hidden }", which is the
363
+ usual recipe for printing one element \u2014 and it is scoped to the document,
364
+ not to us. The stylesheet is injected into the host page's head the first
365
+ time any Base316 component mounts, so from that moment every print in the
366
+ host application came out blank unless the content happened to sit inside
367
+ .b316-invoice-doc. A customer printing their own page had no way to connect
368
+ it back to us.
369
+
370
+ Scoped to a root the invoice sets on <html> while it is being printed
371
+ instead, so nothing applies unless an invoice asked for it. */
272
372
  @media print {
273
- body * { visibility: hidden; }
274
- .b316-invoice-doc, .b316-invoice-doc * { visibility: visible; }
275
- .b316-invoice-doc {
373
+ [data-b316-printing] body * { visibility: hidden; }
374
+ [data-b316-printing] .b316-invoice-doc,
375
+ [data-b316-printing] .b316-invoice-doc * { visibility: visible; }
376
+ [data-b316-printing] .b316-invoice-doc {
276
377
  position: absolute;
277
378
  inset: 0 auto auto 0;
278
379
  width: 100%;
@@ -280,6 +381,208 @@ var STYLESHEET_EXTRA = `
280
381
  }
281
382
  .b316-invoice-actions { display: none; }
282
383
  }
384
+
385
+ /* --- Collections: tables, forms and detail views ------------------------- */
386
+
387
+ .b316-table-wrap { display: flex; flex-direction: column; gap: var(--b316-spacing); }
388
+ .b316-table-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--b316-spacing); flex-wrap: wrap; }
389
+ .b316-table-actions { display: flex; align-items: center; gap: var(--b316-spacing); }
390
+
391
+ /* The table scrolls inside its own box rather than pushing the page sideways:
392
+ a collection with twenty fields is normal, and a horizontally scrolling page
393
+ makes every other component on it unreachable. */
394
+ .b316-table-scroll { overflow-x: auto; border: 1px solid var(--b316-color-border); border-radius: var(--b316-radius); }
395
+ .b316-table { width: 100%; border-collapse: collapse; font-size: 0.95em; }
396
+ .b316-table th, .b316-table td { padding: 10px 12px; text-align: left; vertical-align: top; white-space: nowrap; }
397
+ .b316-table thead th { background: var(--b316-color-surface-muted, rgba(0,0,0,.03)); font-weight: 600; font-size: 0.87em; color: var(--b316-color-muted); border-bottom: 1px solid var(--b316-color-border); }
398
+ .b316-table tbody tr + tr td { border-top: 1px solid var(--b316-color-border); }
399
+ .b316-row-clickable { cursor: pointer; }
400
+ .b316-row-clickable:hover td { background: var(--b316-color-surface-muted, rgba(0,0,0,.02)); }
401
+ .b316-table-rowactions { text-align: right; white-space: nowrap; }
402
+ .b316-table-more { display: flex; justify-content: center; }
403
+
404
+ .b316-sort { background: none; border: 0; padding: 0; font: inherit; color: inherit; cursor: pointer; }
405
+ .b316-sort:focus-visible { outline: 2px solid var(--b316-color-primary); outline-offset: 2px; }
406
+
407
+ .b316-empty { padding: calc(var(--b316-spacing) * 2); text-align: center; color: var(--b316-color-muted); }
408
+
409
+ .b316-button-quiet {
410
+ background: none;
411
+ border: 0;
412
+ padding: 4px 6px;
413
+ font: inherit;
414
+ font-size: 0.9em;
415
+ color: var(--b316-color-primary);
416
+ cursor: pointer;
417
+ border-radius: calc(var(--b316-radius) / 2);
418
+ }
419
+ .b316-button-quiet:hover:not([disabled]) { background: var(--b316-color-surface-muted, rgba(0,0,0,.05)); }
420
+ .b316-button-quiet:focus-visible { outline: 2px solid var(--b316-color-primary); outline-offset: 1px; }
421
+ .b316-button-quiet[disabled] { opacity: .55; cursor: not-allowed; }
422
+ .b316-danger { color: var(--b316-color-danger, #b42318); }
423
+
424
+ .b316-chip {
425
+ display: inline-block;
426
+ padding: 2px 8px;
427
+ border-radius: 999px;
428
+ background: var(--b316-color-surface-muted, rgba(0,0,0,.05));
429
+ font-size: 0.87em;
430
+ }
431
+ .b316-chips { display: inline-flex; gap: 4px; flex-wrap: wrap; }
432
+
433
+ .b316-form { display: flex; flex-direction: column; gap: var(--b316-spacing); }
434
+ .b316-form-title { margin: 0; font-size: 1.05em; }
435
+ .b316-form-actions { display: flex; justify-content: flex-end; gap: 8px; }
436
+ .b316-field { display: flex; flex-direction: column; gap: 4px; }
437
+ .b316-hint { font-size: 0.82em; color: var(--b316-color-muted); }
438
+ .b316-input-invalid { border-color: var(--b316-color-danger, #b42318); }
439
+ .b316-choices { display: flex; flex-wrap: wrap; gap: 10px; }
440
+ .b316-choice { display: inline-flex; align-items: center; gap: 6px; font-size: 0.95em; }
441
+ .b316-suffixed { display: flex; align-items: center; gap: 8px; }
442
+ .b316-address { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
443
+
444
+ .b316-relation-chosen { display: flex; align-items: center; gap: 8px; }
445
+ .b316-relation-results {
446
+ list-style: none;
447
+ margin: 4px 0 0;
448
+ padding: 4px;
449
+ border: 1px solid var(--b316-color-border);
450
+ border-radius: var(--b316-radius);
451
+ max-height: 220px;
452
+ overflow-y: auto;
453
+ background: var(--b316-color-surface, #fff);
454
+ }
455
+ .b316-relation-result { display: block; width: 100%; text-align: left; background: none; border: 0; padding: 6px 8px; font: inherit; cursor: pointer; border-radius: calc(var(--b316-radius) / 2); }
456
+ .b316-relation-result:hover, .b316-relation-result:focus-visible { background: var(--b316-color-surface-muted, rgba(0,0,0,.05)); outline: none; }
457
+ .b316-relation-empty { padding: 6px 8px; }
458
+
459
+ .b316-detail { display: flex; flex-direction: column; gap: var(--b316-spacing); }
460
+ .b316-detail-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--b316-spacing); }
461
+ .b316-detail-title { margin: 0; font-size: 1.1em; }
462
+ .b316-detail-actions { display: flex; gap: 4px; }
463
+ .b316-detail-fields { display: grid; grid-template-columns: minmax(120px, 200px) 1fr; gap: 8px var(--b316-spacing); margin: 0; }
464
+ .b316-detail-row { display: contents; }
465
+ .b316-detail-fields dd { margin: 0; }
466
+
467
+ /* One column on a narrow screen. A label column of 200px beside a value leaves
468
+ nothing for the value on a phone. */
469
+ /* --- Teams --------------------------------------------------------------- */
470
+
471
+ .b316-team { display: flex; flex-direction: column; gap: var(--b316-spacing); }
472
+ .b316-team-title { margin: 0; font-size: 1em; font-weight: 600; }
473
+ .b316-team-subtitle { margin: 0; font-size: .9em; font-weight: 600; color: var(--b316-color-muted); }
474
+ .b316-team-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
475
+ .b316-team-member { display: flex; align-items: center; justify-content: space-between; gap: var(--b316-spacing); }
476
+ .b316-team-member-side { display: flex; align-items: center; gap: 8px; }
477
+ .b316-team-invite { display: flex; flex-direction: column; gap: 4px; }
478
+ .b316-team-invite-row { display: flex; gap: 8px; }
479
+ .b316-team-invite-row input { flex: 1; }
480
+ /* Selectable and wrapping: this is copied by hand and pasted into an email. */
481
+ .b316-team-link { display: flex; flex-direction: column; gap: 4px; }
482
+ .b316-team-link code { word-break: break-all; user-select: all; padding: 8px; border: 1px solid var(--b316-color-border); border-radius: var(--b316-radius); }
483
+
484
+ .b316-invitation { display: flex; flex-direction: column; gap: var(--b316-spacing); }
485
+ .b316-invitation-actions { display: flex; align-items: center; gap: var(--b316-spacing); }
486
+
487
+ /* --- Sessions, two-factor and the password flows ------------------------- */
488
+
489
+ .b316-sessions { display: flex; flex-direction: column; gap: var(--b316-spacing); }
490
+ .b316-sessions-head { display: flex; align-items: center; justify-content: space-between; gap: var(--b316-spacing); flex-wrap: wrap; }
491
+ .b316-sessions-title, .b316-mfa-title { margin: 0; font-size: 1em; font-weight: 600; }
492
+ .b316-sessions-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
493
+ .b316-session { display: flex; flex-direction: column; gap: 2px; }
494
+ .b316-session-device { display: flex; align-items: center; gap: 6px; font-size: 0.95em; }
495
+
496
+ .b316-device { display: flex; flex-direction: column; gap: var(--b316-spacing); }
497
+ .b316-device-title { margin: 0; font-size: 1em; font-weight: 600; }
498
+ /* What is being granted, listed rather than run together: somebody is deciding
499
+ on this, and a comma-separated line is read as one thing. */
500
+ .b316-device-scopes { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
501
+ .b316-device-scopes li { padding: 2px 8px; border-radius: 999px; font-size: 0.85em; background: var(--b316-color-surface-muted, rgba(0,0,0,.05)); }
502
+ .b316-device-actions { display: flex; gap: 8px; }
503
+
504
+ .b316-privacy { display: flex; flex-direction: column; gap: var(--b316-spacing); }
505
+ .b316-privacy-title { margin: 0; font-size: 1em; font-weight: 600; }
506
+ /* Wraps rather than squeezes: the explanation beside each control is the part
507
+ that matters, and on a narrow screen it belongs above the button. */
508
+ .b316-privacy-row { display: flex; flex-wrap: wrap; align-items: start; justify-content: space-between; gap: 10px; }
509
+ .b316-privacy-row p { margin: 0; }
510
+ .b316-privacy-confirm { display: flex; flex-direction: column; gap: 8px; }
511
+ .b316-privacy-actions { display: flex; justify-content: flex-end; gap: 8px; }
512
+
513
+ .b316-mfa { display: flex; flex-direction: column; gap: var(--b316-spacing); }
514
+ .b316-mfa-enrol, .b316-mfa-codes, .b316-mfa-on, .b316-mfa-off { display: flex; flex-direction: column; gap: 10px; }
515
+ .b316-mfa-actions { display: flex; justify-content: flex-end; gap: 8px; }
516
+ /* Selectable and wrapping: this is copied by hand on the device that cannot
517
+ scan its own screen. */
518
+ .b316-mfa-secret code {
519
+ display: block;
520
+ padding: 10px;
521
+ border: 1px solid var(--b316-color-border);
522
+ border-radius: var(--b316-radius);
523
+ font-size: 1.05em;
524
+ letter-spacing: .08em;
525
+ word-break: break-all;
526
+ user-select: all;
527
+ }
528
+ .b316-mfa-uri { font-size: 0.9em; }
529
+ .b316-mfa-code-list { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
530
+ .b316-mfa-code-list code { user-select: all; }
531
+
532
+ @media (max-width: 560px) {
533
+ .b316-mfa-code-list { grid-template-columns: 1fr; }
534
+ .b316-detail-fields { grid-template-columns: 1fr; }
535
+ .b316-detail-row { display: block; margin-bottom: 10px; }
536
+ .b316-address { grid-template-columns: 1fr; }
537
+ }
538
+
539
+ /* --- Board --------------------------------------------------------------- */
540
+
541
+ .b316-board-wrap { display: flex; flex-direction: column; gap: var(--b316-spacing); }
542
+ /* The board scrolls sideways inside itself. Six columns is normal and a page
543
+ that scrolls horizontally puts everything else on it out of reach. */
544
+ .b316-board { display: flex; gap: var(--b316-spacing); overflow-x: auto; align-items: flex-start; padding-bottom: 4px; }
545
+ .b316-board-column {
546
+ flex: 0 0 260px;
547
+ background: var(--b316-color-surface-muted, rgba(0,0,0,.03));
548
+ border: 1px solid transparent;
549
+ border-radius: var(--b316-radius);
550
+ padding: 10px;
551
+ display: flex;
552
+ flex-direction: column;
553
+ gap: 8px;
554
+ }
555
+ .b316-board-over { border-color: var(--b316-color-primary); }
556
+ .b316-board-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
557
+ .b316-board-title { font-weight: 600; font-size: 0.92em; }
558
+ .b316-board-cards { display: flex; flex-direction: column; gap: 8px; min-height: 40px; }
559
+ .b316-board-card {
560
+ background: var(--b316-color-surface, #fff);
561
+ border: 1px solid var(--b316-color-border);
562
+ border-radius: calc(var(--b316-radius) * .8);
563
+ padding: 10px;
564
+ display: flex;
565
+ flex-direction: column;
566
+ gap: 6px;
567
+ font-size: 0.9em;
568
+ }
569
+ .b316-board-card[draggable="true"] { cursor: grab; }
570
+ .b316-board-card-title { font-weight: 600; }
571
+ .b316-board-card-field { display: flex; justify-content: space-between; gap: 8px; }
572
+ .b316-board-move { display: flex; align-items: center; gap: 6px; margin-top: 2px; }
573
+ .b316-board-move select { flex: 1; font-size: 0.9em; padding: 2px 4px; }
574
+ .b316-board-empty { color: var(--b316-color-muted); font-size: 0.87em; padding: 6px 2px; }
575
+
576
+ /* --- Chart --------------------------------------------------------------- */
577
+
578
+ .b316-chart { display: flex; flex-direction: column; gap: var(--b316-spacing); }
579
+ .b316-chart-title { font-weight: 600; font-size: 0.95em; }
580
+ .b316-chart-plot { display: flex; align-items: flex-end; gap: 4px; overflow-x: auto; }
581
+ .b316-chart-col { flex: 1 0 24px; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%; gap: 4px; }
582
+ .b316-chart-bar { width: 100%; background: var(--b316-color-primary); border-radius: 3px 3px 0 0; min-height: 2px; }
583
+ .b316-chart-label { font-size: 0.7em; color: var(--b316-color-muted); white-space: nowrap; transform: rotate(-45deg); transform-origin: top left; height: 1.6em; }
584
+ .b316-chart-data summary { cursor: pointer; font-size: 0.87em; color: var(--b316-color-muted); }
585
+ .b316-chart-data table { margin-top: 8px; }
283
586
  `;
284
587
 
285
588
  // src/provider.tsx
@@ -308,7 +611,7 @@ function Base316Provider({
308
611
  baseUrl,
309
612
  token: controlledToken,
310
613
  onTokenChange,
311
- appearance,
614
+ appearance: appearance2,
312
615
  className,
313
616
  children
314
617
  }) {
@@ -406,7 +709,7 @@ function Base316Provider({
406
709
  colorPrimary: branding.primaryColor,
407
710
  colorBackground: branding.backgroundColor
408
711
  } : {},
409
- ...appearance
712
+ ...appearance2
410
713
  }),
411
714
  children
412
715
  }
@@ -494,6 +797,9 @@ function Invoices({ className, style }) {
494
797
  },
495
798
  [client, token]
496
799
  );
800
+ if (error) {
801
+ return /* @__PURE__ */ jsx3("div", { className: cls("b316-card", className), style, children: /* @__PURE__ */ jsx3("div", { className: "b316-error", role: "alert", children: error }) });
802
+ }
497
803
  if (sessionLoading || invoices === null) {
498
804
  return /* @__PURE__ */ jsx3("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
499
805
  }
@@ -504,7 +810,7 @@ function Invoices({ className, style }) {
504
810
  return /* @__PURE__ */ jsx3("div", { className: cls("b316-card", className), style, children: "No invoices yet." });
505
811
  }
506
812
  return /* @__PURE__ */ jsxs2("div", { className: cls("b316-card b316-invoices", className), style, children: [
507
- error ? /* @__PURE__ */ jsx3("div", { className: "b316-error", children: error }) : null,
813
+ error ? /* @__PURE__ */ jsx3("div", { className: "b316-error", role: "alert", children: error }) : null,
508
814
  /* @__PURE__ */ jsxs2("table", { className: "b316-invoice-table", children: [
509
815
  /* @__PURE__ */ jsx3("thead", { children: /* @__PURE__ */ jsxs2("tr", { children: [
510
816
  /* @__PURE__ */ jsx3("th", { children: "Invoice" }),
@@ -545,7 +851,15 @@ function InvoiceView({ invoice, onClose }) {
545
851
  {
546
852
  type: "button",
547
853
  className: "b316-button b316-button-secondary",
548
- onClick: () => window.print(),
854
+ onClick: () => {
855
+ const root = document.documentElement;
856
+ root.setAttribute("data-b316-printing", "");
857
+ try {
858
+ window.print();
859
+ } finally {
860
+ root.removeAttribute("data-b316-printing");
861
+ }
862
+ },
549
863
  children: "Download PDF"
550
864
  }
551
865
  )
@@ -627,16 +941,13 @@ function InvoiceView({ invoice, onClose }) {
627
941
  }
628
942
 
629
943
  // src/SignIn.tsx
630
- import {
631
- isPasskeyAutofillAvailable,
632
- isPasskeySupported,
633
- needsSecondFactor
634
- } from "@base316/sdk";
944
+ import { isPasskeyAutofillAvailable, isPasskeySupported, needsSecondFactor } from "@base316/sdk";
635
945
  import { useEffect as useEffect3, useRef, useState as useState3 } from "react";
636
946
  import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
637
947
  function SignIn({
638
948
  onSignedIn,
639
949
  passkeys = true,
950
+ magicLinkRedirectUrl,
640
951
  className,
641
952
  style
642
953
  }) {
@@ -644,10 +955,12 @@ function SignIn({
644
955
  const [email, setEmail] = useState3("");
645
956
  const [password, setPassword] = useState3("");
646
957
  const [code, setCode] = useState3("");
647
- const [pendingUserId, setPendingUserId] = useState3(null);
958
+ const [pendingChallenge, setPendingChallenge] = useState3(null);
648
959
  const [error, setError] = useState3(null);
649
960
  const [busy, setBusy] = useState3(false);
650
961
  const [passkeyReady, setPasskeyReady] = useState3(false);
962
+ const [mode, setMode] = useState3("password");
963
+ const [linkSent, setLinkSent] = useState3(false);
651
964
  const abortRef = useRef(null);
652
965
  const signInWithPasskey = async (conditional, signal) => {
653
966
  if (!conditional) {
@@ -673,7 +986,7 @@ function SignIn({
673
986
  }
674
987
  };
675
988
  useEffect3(() => {
676
- if (!passkeys || !isPasskeySupported() || pendingUserId) {
989
+ if (!passkeys || !isPasskeySupported() || pendingChallenge) {
677
990
  return;
678
991
  }
679
992
  setPasskeyReady(true);
@@ -687,21 +1000,71 @@ function SignIn({
687
1000
  return () => {
688
1001
  controller.abort();
689
1002
  };
690
- }, [passkeys, pendingUserId]);
1003
+ }, [passkeys, pendingChallenge]);
1004
+ useEffect3(() => {
1005
+ if (typeof window === "undefined") {
1006
+ return;
1007
+ }
1008
+ const url = new URL(window.location.href);
1009
+ const token = url.searchParams.get("token");
1010
+ if (!token) {
1011
+ return;
1012
+ }
1013
+ url.searchParams.delete("token");
1014
+ window.history.replaceState({}, "", url.toString());
1015
+ let cancelled = false;
1016
+ void client.auth.magicLink.verify({ token }).then((result) => {
1017
+ if (cancelled) {
1018
+ return;
1019
+ }
1020
+ setToken(result.token);
1021
+ onSignedIn?.();
1022
+ }).catch(() => {
1023
+ if (!cancelled) {
1024
+ setError("That sign-in link is no longer valid. Ask for a new one.");
1025
+ }
1026
+ });
1027
+ return () => {
1028
+ cancelled = true;
1029
+ };
1030
+ }, []);
1031
+ async function sendLink() {
1032
+ if (!magicLinkRedirectUrl) {
1033
+ return;
1034
+ }
1035
+ setBusy(true);
1036
+ setError(null);
1037
+ try {
1038
+ await client.auth.magicLink.request({ email, redirectUrl: magicLinkRedirectUrl });
1039
+ setLinkSent(true);
1040
+ } catch (cause) {
1041
+ console.error("[base316] magic link request failed", cause);
1042
+ setError("We could not send that link. Try again.");
1043
+ } finally {
1044
+ setBusy(false);
1045
+ }
1046
+ }
691
1047
  async function submit(event) {
692
1048
  event.preventDefault();
693
1049
  setBusy(true);
694
1050
  setError(null);
695
1051
  try {
696
- if (pendingUserId) {
697
- const verified = await client.auth.verifyMfa({ userId: pendingUserId, code });
1052
+ if (mode === "magic" && !pendingChallenge) {
1053
+ await sendLink();
1054
+ return;
1055
+ }
1056
+ if (pendingChallenge) {
1057
+ const verified = await client.auth.verifyMfa({
1058
+ challengeToken: pendingChallenge,
1059
+ code
1060
+ });
698
1061
  setToken(verified.token);
699
1062
  onSignedIn?.();
700
1063
  return;
701
1064
  }
702
1065
  const result = await client.auth.signIn({ email, password });
703
1066
  if (needsSecondFactor(result)) {
704
- setPendingUserId(result.userId);
1067
+ setPendingChallenge(result.challengeToken);
705
1068
  return;
706
1069
  }
707
1070
  setToken(result.token);
@@ -709,15 +1072,56 @@ function SignIn({
709
1072
  } catch (cause) {
710
1073
  console.error("[base316] sign-in failed", cause);
711
1074
  setError(
712
- pendingUserId ? "That code was not accepted." : "That email and password did not match."
1075
+ pendingChallenge ? "That code was not accepted." : "That email and password did not match."
713
1076
  );
714
1077
  } finally {
715
1078
  setBusy(false);
716
1079
  }
717
1080
  }
1081
+ const magicAvailable = Boolean(magicLinkRedirectUrl) && !pendingChallenge;
1082
+ if (linkSent) {
1083
+ return /* @__PURE__ */ jsxs3("div", { className: cls("b316-card", className), style, children: [
1084
+ /* @__PURE__ */ jsx4("div", { style: { fontWeight: 600, marginBottom: 6 }, children: "Check your email" }),
1085
+ /* @__PURE__ */ jsxs3("div", { className: "b316-muted", children: [
1086
+ "If an account exists for ",
1087
+ email,
1088
+ ", a sign-in link is on its way. It expires shortly and can be used once."
1089
+ ] }),
1090
+ /* @__PURE__ */ jsx4(
1091
+ "button",
1092
+ {
1093
+ type: "button",
1094
+ className: "b316-button b316-button-secondary",
1095
+ style: { marginTop: 16 },
1096
+ onClick: () => setLinkSent(false),
1097
+ children: "Back to sign in"
1098
+ }
1099
+ )
1100
+ ] });
1101
+ }
718
1102
  return /* @__PURE__ */ jsxs3("form", { onSubmit: submit, className: cls("b316-card", className), style, children: [
719
- error ? /* @__PURE__ */ jsx4("div", { className: "b316-error", children: error }) : null,
720
- pendingUserId ? /* @__PURE__ */ jsxs3("div", { className: "b316-field", children: [
1103
+ error ? /* @__PURE__ */ jsx4("div", { className: "b316-error", role: "alert", children: error }) : null,
1104
+ magicAvailable ? /* @__PURE__ */ jsxs3("div", { className: "b316-tabs", children: [
1105
+ /* @__PURE__ */ jsx4(
1106
+ "button",
1107
+ {
1108
+ type: "button",
1109
+ className: cls("b316-tab", mode === "password" ? "b316-tab-on" : void 0),
1110
+ onClick: () => setMode("password"),
1111
+ children: "Password"
1112
+ }
1113
+ ),
1114
+ /* @__PURE__ */ jsx4(
1115
+ "button",
1116
+ {
1117
+ type: "button",
1118
+ className: cls("b316-tab", mode === "magic" ? "b316-tab-on" : void 0),
1119
+ onClick: () => setMode("magic"),
1120
+ children: "Magic link"
1121
+ }
1122
+ )
1123
+ ] }) : null,
1124
+ pendingChallenge ? /* @__PURE__ */ jsxs3("div", { className: "b316-field", children: [
721
1125
  /* @__PURE__ */ jsx4("label", { className: "b316-label", htmlFor: "b316-code", children: "Authentication code" }),
722
1126
  /* @__PURE__ */ jsx4(
723
1127
  "input",
@@ -748,7 +1152,7 @@ function SignIn({
748
1152
  }
749
1153
  )
750
1154
  ] }),
751
- /* @__PURE__ */ jsxs3("div", { className: "b316-field", children: [
1155
+ mode === "password" ? /* @__PURE__ */ jsxs3("div", { className: "b316-field", children: [
752
1156
  /* @__PURE__ */ jsx4("label", { className: "b316-label", htmlFor: "b316-password", children: "Password" }),
753
1157
  /* @__PURE__ */ jsx4(
754
1158
  "input",
@@ -762,10 +1166,10 @@ function SignIn({
762
1166
  required: true
763
1167
  }
764
1168
  )
765
- ] })
1169
+ ] }) : null
766
1170
  ] }),
767
- /* @__PURE__ */ jsx4("button", { className: "b316-button", type: "submit", disabled: busy, children: busy ? "Working\u2026" : pendingUserId ? "Verify" : "Sign in" }),
768
- passkeyReady && !pendingUserId ? /* @__PURE__ */ jsxs3(Fragment, { children: [
1171
+ /* @__PURE__ */ jsx4("button", { className: "b316-button", type: "submit", disabled: busy, children: busy ? "Working\u2026" : pendingChallenge ? "Verify" : mode === "magic" ? "Email me a link" : "Sign in" }),
1172
+ passkeyReady && !pendingChallenge ? /* @__PURE__ */ jsxs3(Fragment, { children: [
769
1173
  /* @__PURE__ */ jsx4("div", { className: "b316-muted", style: { textAlign: "center", margin: "10px 0" }, children: "or" }),
770
1174
  /* @__PURE__ */ jsx4(
771
1175
  "button",
@@ -817,7 +1221,7 @@ function SignUp({
817
1221
  }
818
1222
  }
819
1223
  return /* @__PURE__ */ jsxs4("form", { onSubmit: submit, className: cls("b316-card", className), style, children: [
820
- error ? /* @__PURE__ */ jsx5("div", { className: "b316-error", children: error }) : null,
1224
+ error ? /* @__PURE__ */ jsx5("div", { className: "b316-error", role: "alert", children: error }) : null,
821
1225
  /* @__PURE__ */ jsxs4("div", { className: "b316-field", children: [
822
1226
  /* @__PURE__ */ jsx5("label", { className: "b316-label", htmlFor: "b316-name", children: "Name" }),
823
1227
  /* @__PURE__ */ jsx5(
@@ -898,19 +1302,86 @@ function UserProfile({ className, style }) {
898
1302
 
899
1303
  // src/PasskeyManager.tsx
900
1304
  import { isPasskeySupported as isPasskeySupported2 } from "@base316/sdk";
901
- import { useCallback as useCallback3, useEffect as useEffect4, useState as useState5 } from "react";
1305
+ import { useCallback as useCallback3, useEffect as useEffect5, useState as useState6 } from "react";
1306
+
1307
+ // src/confirm.tsx
1308
+ import { useEffect as useEffect4, useRef as useRef2, useState as useState5 } from "react";
902
1309
  import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
1310
+ var DEFAULT_TIMEOUT_MS = 5e3;
1311
+ function ConfirmButton({
1312
+ children,
1313
+ confirmLabel,
1314
+ onConfirm,
1315
+ className,
1316
+ disabled,
1317
+ timeoutMs = DEFAULT_TIMEOUT_MS
1318
+ }) {
1319
+ const [armed, setArmed] = useState5(false);
1320
+ const confirmRef = useRef2(null);
1321
+ useEffect4(() => {
1322
+ if (!armed) {
1323
+ return;
1324
+ }
1325
+ confirmRef.current?.focus();
1326
+ const timer = setTimeout(() => setArmed(false), timeoutMs);
1327
+ return () => clearTimeout(timer);
1328
+ }, [armed, timeoutMs]);
1329
+ if (!armed) {
1330
+ return /* @__PURE__ */ jsx7(
1331
+ "button",
1332
+ {
1333
+ type: "button",
1334
+ className,
1335
+ disabled,
1336
+ onClick: () => setArmed(true),
1337
+ children
1338
+ }
1339
+ );
1340
+ }
1341
+ return (
1342
+ // No `role="group"`: both buttons already say what they do, and the
1343
+ // grouping would only repeat the question a screen reader has just read.
1344
+ /* @__PURE__ */ jsxs6("span", { className: "b316-confirm", children: [
1345
+ /* @__PURE__ */ jsx7(
1346
+ "button",
1347
+ {
1348
+ ref: confirmRef,
1349
+ type: "button",
1350
+ className,
1351
+ disabled,
1352
+ onClick: async () => {
1353
+ setArmed(false);
1354
+ await onConfirm();
1355
+ },
1356
+ children: confirmLabel
1357
+ }
1358
+ ),
1359
+ /* @__PURE__ */ jsx7(
1360
+ "button",
1361
+ {
1362
+ type: "button",
1363
+ className: "b316-button b316-button-secondary",
1364
+ onClick: () => setArmed(false),
1365
+ children: "Cancel"
1366
+ }
1367
+ )
1368
+ ] })
1369
+ );
1370
+ }
1371
+
1372
+ // src/PasskeyManager.tsx
1373
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
903
1374
  function PasskeyManager({
904
1375
  className,
905
1376
  style,
906
1377
  title = "Passkeys"
907
1378
  }) {
908
1379
  const { client, token, session } = useBase316();
909
- const [passkeys, setPasskeys] = useState5(null);
910
- const [busy, setBusy] = useState5(false);
911
- const [error, setError] = useState5(null);
912
- const [name, setName] = useState5("");
913
- const [supported, setSupported] = useState5(false);
1380
+ const [passkeys, setPasskeys] = useState6(null);
1381
+ const [busy, setBusy] = useState6(false);
1382
+ const [error, setError] = useState6(null);
1383
+ const [name, setName] = useState6("");
1384
+ const [supported, setSupported] = useState6(false);
914
1385
  const load = useCallback3(async () => {
915
1386
  if (!token) {
916
1387
  return;
@@ -918,11 +1389,13 @@ function PasskeyManager({
918
1389
  try {
919
1390
  const result = await client.auth.passkeys.list(token);
920
1391
  setPasskeys(result.data);
921
- } catch {
922
- setPasskeys([]);
1392
+ } catch (cause) {
1393
+ console.error("[base316] could not list passkeys", cause);
1394
+ setError("Could not load your passkeys. They have not been changed.");
1395
+ setPasskeys(null);
923
1396
  }
924
1397
  }, [client, token]);
925
- useEffect4(() => {
1398
+ useEffect5(() => {
926
1399
  if (!isPasskeySupported2()) {
927
1400
  return;
928
1401
  }
@@ -958,30 +1431,30 @@ function PasskeyManager({
958
1431
  setError(caught instanceof Error ? caught.message : "That did not work.");
959
1432
  }
960
1433
  };
961
- return /* @__PURE__ */ jsxs6("div", { className: cls("b316-card", className), style, children: [
962
- /* @__PURE__ */ jsx7("div", { style: { fontWeight: 600, marginBottom: 4 }, children: title }),
963
- /* @__PURE__ */ jsx7("div", { className: "b316-muted", style: { marginBottom: 12 }, children: "Sign in with your fingerprint, face or a security key \u2014 nothing to type and nothing to remember." }),
964
- passkeys === null ? /* @__PURE__ */ jsx7("div", { className: "b316-muted", children: "Loading\u2026" }) : passkeys.length === 0 ? /* @__PURE__ */ jsx7("div", { className: "b316-muted", children: "No passkeys yet." }) : /* @__PURE__ */ jsx7("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: passkeys.map((passkey) => /* @__PURE__ */ jsxs6("li", { className: "b316-row", style: { padding: "8px 0" }, children: [
965
- /* @__PURE__ */ jsxs6("div", { children: [
966
- /* @__PURE__ */ jsx7("div", { children: passkey.name || "Passkey" }),
967
- /* @__PURE__ */ jsxs6("div", { className: "b316-muted", children: [
1434
+ return /* @__PURE__ */ jsxs7("div", { className: cls("b316-card", className), style, children: [
1435
+ /* @__PURE__ */ jsx8("div", { style: { fontWeight: 600, marginBottom: 4 }, children: title }),
1436
+ /* @__PURE__ */ jsx8("div", { className: "b316-muted", style: { marginBottom: 12 }, children: "Sign in with your fingerprint, face or a security key \u2014 nothing to type and nothing to remember." }),
1437
+ passkeys === null ? /* @__PURE__ */ jsx8("div", { className: "b316-muted", children: "Loading\u2026" }) : passkeys.length === 0 ? /* @__PURE__ */ jsx8("div", { className: "b316-muted", children: "No passkeys yet." }) : /* @__PURE__ */ jsx8("ul", { style: { listStyle: "none", margin: 0, padding: 0 }, children: passkeys.map((passkey) => /* @__PURE__ */ jsxs7("li", { className: "b316-row", style: { padding: "8px 0" }, children: [
1438
+ /* @__PURE__ */ jsxs7("div", { children: [
1439
+ /* @__PURE__ */ jsx8("div", { children: passkey.name || "Passkey" }),
1440
+ /* @__PURE__ */ jsxs7("div", { className: "b316-muted", children: [
968
1441
  passkey.lastUsedAt ? `Last used ${formatDate(passkey.lastUsedAt)}` : `Added ${formatDate(passkey.createdAt)}`,
969
1442
  passkey.backedUp ? "" : " \xB7 this device only"
970
1443
  ] })
971
1444
  ] }),
972
- /* @__PURE__ */ jsx7(
973
- "button",
1445
+ /* @__PURE__ */ jsx8(
1446
+ ConfirmButton,
974
1447
  {
975
- type: "button",
976
1448
  className: "b316-button b316-button-secondary",
977
- onClick: () => void remove(passkey.id),
1449
+ confirmLabel: `Remove ${passkey.name ?? "this passkey"} for good`,
1450
+ onConfirm: () => remove(passkey.id),
978
1451
  children: "Remove"
979
1452
  }
980
1453
  )
981
1454
  ] }, passkey.id)) }),
982
- error ? /* @__PURE__ */ jsx7("div", { className: "b316-error", style: { marginTop: 12 }, children: error }) : null,
983
- /* @__PURE__ */ jsxs6("div", { className: "b316-row", style: { marginTop: 12, gap: 8 }, children: [
984
- /* @__PURE__ */ jsx7(
1455
+ error ? /* @__PURE__ */ jsx8("div", { className: "b316-error", role: "alert", style: { marginTop: 12 }, children: error }) : null,
1456
+ /* @__PURE__ */ jsxs7("div", { className: "b316-row", style: { marginTop: 12, gap: 8 }, children: [
1457
+ /* @__PURE__ */ jsx8(
985
1458
  "input",
986
1459
  {
987
1460
  className: "b316-input",
@@ -990,16 +1463,7 @@ function PasskeyManager({
990
1463
  onChange: (event) => setName(event.target.value)
991
1464
  }
992
1465
  ),
993
- /* @__PURE__ */ jsx7(
994
- "button",
995
- {
996
- type: "button",
997
- className: "b316-button",
998
- disabled: busy,
999
- onClick: () => void add(),
1000
- children: busy ? "Waiting\u2026" : "Add a passkey"
1001
- }
1002
- )
1466
+ /* @__PURE__ */ jsx8("button", { type: "button", className: "b316-button", disabled: busy, onClick: () => void add(), children: busy ? "Waiting\u2026" : "Add a passkey" })
1003
1467
  ] })
1004
1468
  ] });
1005
1469
  }
@@ -1008,111 +1472,2697 @@ function formatDate(value) {
1008
1472
  return Number.isNaN(date.getTime()) ? "" : date.toLocaleDateString();
1009
1473
  }
1010
1474
 
1011
- // src/PricingTable.tsx
1012
- import { useEffect as useEffect5, useState as useState6 } from "react";
1013
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1014
- function formatAmount(amount, currency) {
1015
- if (amount === null) return "Free";
1016
- const value = Number(amount);
1017
- if (!Number.isFinite(value)) return "Free";
1018
- try {
1019
- return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(value);
1020
- } catch {
1021
- return `${value} ${currency}`;
1475
+ // src/ActiveSessions.tsx
1476
+ import { useCallback as useCallback4, useEffect as useEffect6, useState as useState7 } from "react";
1477
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1478
+ function describeDevice(userAgent) {
1479
+ if (!userAgent) {
1480
+ return null;
1022
1481
  }
1023
- }
1024
- function cadence(plan, amount) {
1025
- if (amount === null) {
1026
- return "\xA0";
1482
+ const browser = /Edg\//.test(userAgent) ? "Edge" : /OPR\//.test(userAgent) ? "Opera" : /Chrome\//.test(userAgent) ? "Chrome" : /Safari\//.test(userAgent) ? "Safari" : /Firefox\//.test(userAgent) ? "Firefox" : null;
1483
+ const platform = /iPhone|iPad/.test(userAgent) ? "iOS" : /Android/.test(userAgent) ? "Android" : /Mac OS X/.test(userAgent) ? "macOS" : /Windows/.test(userAgent) ? "Windows" : /Linux/.test(userAgent) ? "Linux" : null;
1484
+ if (!browser && !platform) {
1485
+ return userAgent.slice(0, 60);
1027
1486
  }
1028
- const unit = plan.interval.toLowerCase();
1029
- return plan.intervalCount === 1 ? `per ${unit}` : `every ${plan.intervalCount} ${unit}s`;
1487
+ return [browser, platform].filter(Boolean).join(" on ");
1030
1488
  }
1031
- function PricingTable({
1032
- onSelectPlan,
1033
- currentPlanVersionId,
1034
- featuredPlanKey,
1035
- className,
1036
- style
1037
- }) {
1038
- const { client } = useBase316();
1039
- const [plans, setPlans] = useState6(null);
1040
- const [error, setError] = useState6(false);
1041
- const [busy, setBusy] = useState6(null);
1042
- useEffect5(() => {
1043
- let cancelled = false;
1044
- client.billing.plans().then((result) => {
1045
- if (!cancelled) setPlans(result.data);
1489
+ function when(value) {
1490
+ const date = new Date(value);
1491
+ return Number.isNaN(date.getTime()) ? "" : date.toLocaleString();
1492
+ }
1493
+ function ActiveSessions({ onRevoked, className, style }) {
1494
+ const { client, token } = useBase316();
1495
+ const [sessions, setSessions] = useState7(null);
1496
+ const [error, setError] = useState7(null);
1497
+ const [busy, setBusy] = useState7(false);
1498
+ const load = useCallback4(() => {
1499
+ if (!token) {
1500
+ return;
1501
+ }
1502
+ client.auth.sessions(token).then((result) => {
1503
+ setSessions(result.data.filter((session) => !session.revokedAt));
1504
+ setError(null);
1046
1505
  }).catch((cause) => {
1047
- console.error("[base316] could not load plans", cause);
1048
- if (!cancelled) setError(true);
1506
+ setError(cause instanceof Error ? cause.message : "Could not load your sessions.");
1049
1507
  });
1050
- return () => {
1051
- cancelled = true;
1052
- };
1053
- }, [client]);
1054
- if (error) {
1055
- return /* @__PURE__ */ jsx8("div", { className: cls("b316-card", className), style, children: "Plans are unavailable right now." });
1508
+ }, [client, token]);
1509
+ useEffect6(load, [load]);
1510
+ if (!token) {
1511
+ return null;
1056
1512
  }
1057
- if (!plans) {
1058
- return /* @__PURE__ */ jsx8("div", { className: cls("b316-card", className), style, children: "Loading plans\u2026" });
1513
+ if (error) {
1514
+ return /* @__PURE__ */ jsx9("div", { className: cls("b316-card", className), style, children: /* @__PURE__ */ jsx9("div", { className: "b316-error", role: "alert", children: error }) });
1059
1515
  }
1060
- if (plans.length === 0) {
1061
- return /* @__PURE__ */ jsx8("div", { className: cls("b316-card", className), style, children: "No plans are published yet." });
1516
+ if (!sessions) {
1517
+ return /* @__PURE__ */ jsx9("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
1062
1518
  }
1063
- const anyTrial = plans.some((plan) => Boolean(plan.trialDays));
1064
- const anyDescription = plans.some((plan) => Boolean(plan.description));
1519
+ async function revokeOthers() {
1520
+ setBusy(true);
1521
+ try {
1522
+ const result = await client.auth.revokeOtherSessions(token);
1523
+ onRevoked?.(result.revoked);
1524
+ load();
1525
+ } catch (cause) {
1526
+ setError(cause instanceof Error ? cause.message : "Could not end those sessions.");
1527
+ } finally {
1528
+ setBusy(false);
1529
+ }
1530
+ }
1531
+ return /* @__PURE__ */ jsxs8("div", { className: cls("b316-card b316-sessions", className), style, children: [
1532
+ /* @__PURE__ */ jsxs8("div", { className: "b316-sessions-head", children: [
1533
+ /* @__PURE__ */ jsx9("h3", { className: "b316-sessions-title", children: "Where you are signed in" }),
1534
+ sessions.length > 1 ? /* @__PURE__ */ jsx9("button", { type: "button", className: "b316-button", disabled: busy, onClick: revokeOthers, children: busy ? "Ending\u2026" : "Sign out other devices" }) : null
1535
+ ] }),
1536
+ /* @__PURE__ */ jsx9("ul", { className: "b316-sessions-list", children: sessions.map((session) => /* @__PURE__ */ jsxs8("li", { className: "b316-session", children: [
1537
+ /* @__PURE__ */ jsxs8("div", { className: "b316-session-device", children: [
1538
+ describeDevice(session.userAgent) ?? "Unknown device",
1539
+ session.impersonatedBy ? (
1540
+ // Worth saying loudly: somebody is acting as this account.
1541
+ /* @__PURE__ */ jsx9("span", { className: "b316-chip", children: " Support access" })
1542
+ ) : null
1543
+ ] }),
1544
+ /* @__PURE__ */ jsxs8("div", { className: "b316-muted", children: [
1545
+ session.ipAddress ? `${session.ipAddress} \xB7 ` : "",
1546
+ "last used ",
1547
+ when(session.lastSeenAt)
1548
+ ] })
1549
+ ] }, session.id)) }),
1550
+ sessions.length === 0 ? /* @__PURE__ */ jsx9("div", { className: "b316-muted", children: "No active sessions." }) : null
1551
+ ] });
1552
+ }
1553
+
1554
+ // src/DeviceApproval.tsx
1555
+ import { useState as useState8 } from "react";
1556
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1557
+ function DeviceApproval({
1558
+ userCode = "",
1559
+ onDecided,
1560
+ className,
1561
+ style
1562
+ }) {
1563
+ const { client, token } = useBase316();
1564
+ const [code, setCode] = useState8(userCode);
1565
+ const [request, setRequest] = useState8(null);
1566
+ const [decided, setDecided] = useState8(null);
1567
+ const [error, setError] = useState8(null);
1568
+ const [busy, setBusy] = useState8(false);
1569
+ if (!token) {
1570
+ return null;
1571
+ }
1572
+ async function lookUp(event) {
1573
+ event.preventDefault();
1574
+ setBusy(true);
1575
+ setError(null);
1576
+ try {
1577
+ setRequest(await client.device.lookup(code));
1578
+ } catch (cause) {
1579
+ setError(cause instanceof Error ? cause.message : "We could not check that code.");
1580
+ } finally {
1581
+ setBusy(false);
1582
+ }
1583
+ }
1584
+ async function decide(approve) {
1585
+ if (!request) {
1586
+ return;
1587
+ }
1588
+ setBusy(true);
1589
+ setError(null);
1590
+ try {
1591
+ await client.device.decide(token, { id: request.id, approve });
1592
+ setDecided(approve);
1593
+ onDecided?.(approve);
1594
+ } catch (cause) {
1595
+ setError(cause instanceof Error ? cause.message : "We could not record that.");
1596
+ } finally {
1597
+ setBusy(false);
1598
+ }
1599
+ }
1600
+ if (decided !== null) {
1601
+ return /* @__PURE__ */ jsxs9("div", { className: cls("b316-card b316-device", className), style, children: [
1602
+ /* @__PURE__ */ jsx10("h3", { className: "b316-device-title", children: decided ? "You're all set" : "Not approved" }),
1603
+ /* @__PURE__ */ jsx10("p", { className: "b316-muted", children: decided ? "You can close this page and go back to your device." : "Nothing was granted. You can close this page." })
1604
+ ] });
1605
+ }
1606
+ if (request) {
1607
+ return /* @__PURE__ */ jsxs9("div", { className: cls("b316-card b316-device", className), style, children: [
1608
+ /* @__PURE__ */ jsxs9("h3", { className: "b316-device-title", children: [
1609
+ "Sign in to ",
1610
+ request.clientName
1611
+ ] }),
1612
+ /* @__PURE__ */ jsxs9("p", { className: "b316-muted", children: [
1613
+ request.clientName,
1614
+ " is asking to sign in as you."
1615
+ ] }),
1616
+ request.scopes.length > 0 ? /* @__PURE__ */ jsx10("ul", { className: "b316-device-scopes", children: request.scopes.map((scope) => /* @__PURE__ */ jsx10("li", { children: scope }, scope)) }) : null,
1617
+ error ? /* @__PURE__ */ jsx10("div", { className: "b316-error", role: "alert", children: error }) : null,
1618
+ /* @__PURE__ */ jsxs9("div", { className: "b316-device-actions", children: [
1619
+ /* @__PURE__ */ jsx10(
1620
+ "button",
1621
+ {
1622
+ type: "button",
1623
+ className: "b316-button",
1624
+ disabled: busy,
1625
+ onClick: () => decide(true),
1626
+ children: busy ? "Approving\u2026" : "Approve"
1627
+ }
1628
+ ),
1629
+ /* @__PURE__ */ jsx10(
1630
+ "button",
1631
+ {
1632
+ type: "button",
1633
+ className: "b316-button b316-button-quiet",
1634
+ disabled: busy,
1635
+ onClick: () => decide(false),
1636
+ children: "Deny"
1637
+ }
1638
+ )
1639
+ ] })
1640
+ ] });
1641
+ }
1642
+ return /* @__PURE__ */ jsxs9("form", { className: cls("b316-card b316-device", className), style, onSubmit: lookUp, children: [
1643
+ /* @__PURE__ */ jsx10("h3", { className: "b316-device-title", children: "Connect your device" }),
1644
+ /* @__PURE__ */ jsx10("p", { className: "b316-muted", children: "Enter the code shown on your device." }),
1645
+ error ? /* @__PURE__ */ jsx10("div", { className: "b316-error", role: "alert", children: error }) : null,
1646
+ /* @__PURE__ */ jsx10(
1647
+ "input",
1648
+ {
1649
+ className: "b316-input",
1650
+ value: code,
1651
+ onChange: (event) => setCode(event.target.value),
1652
+ autoComplete: "off",
1653
+ autoCapitalize: "characters",
1654
+ spellCheck: false,
1655
+ placeholder: "XXXX-XXXX",
1656
+ "aria-label": "Device code"
1657
+ }
1658
+ ),
1659
+ /* @__PURE__ */ jsx10("button", { type: "submit", className: "b316-button", disabled: busy || code.trim().length < 4, children: busy ? "Checking\u2026" : "Continue" })
1660
+ ] });
1661
+ }
1662
+
1663
+ // src/PrivacyControls.tsx
1664
+ import { useState as useState9 } from "react";
1665
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1666
+ function PrivacyControls({
1667
+ confirmWord = "DELETE",
1668
+ onErased,
1669
+ className,
1670
+ style
1671
+ }) {
1672
+ const { client, token } = useBase316();
1673
+ const [confirming, setConfirming] = useState9(false);
1674
+ const [typed, setTyped] = useState9("");
1675
+ const [error, setError] = useState9(null);
1676
+ const [busy, setBusy] = useState9(false);
1677
+ if (!token) {
1678
+ return null;
1679
+ }
1680
+ async function download() {
1681
+ setBusy(true);
1682
+ setError(null);
1683
+ try {
1684
+ const data = await client.auth.privacy.export(token);
1685
+ const url = URL.createObjectURL(
1686
+ new Blob([JSON.stringify(data, null, 2)], { type: "application/json" })
1687
+ );
1688
+ const link = document.createElement("a");
1689
+ link.href = url;
1690
+ link.download = "my-data.json";
1691
+ link.click();
1692
+ URL.revokeObjectURL(url);
1693
+ } catch (cause) {
1694
+ setError(cause instanceof Error ? cause.message : "Could not build your export.");
1695
+ } finally {
1696
+ setBusy(false);
1697
+ }
1698
+ }
1699
+ async function erase() {
1700
+ setBusy(true);
1701
+ setError(null);
1702
+ try {
1703
+ await client.auth.privacy.erase(token);
1704
+ onErased?.();
1705
+ } catch (cause) {
1706
+ setError(cause instanceof Error ? cause.message : "Could not delete your account.");
1707
+ } finally {
1708
+ setBusy(false);
1709
+ }
1710
+ }
1711
+ return /* @__PURE__ */ jsxs10("div", { className: cls("b316-card b316-privacy", className), style, children: [
1712
+ /* @__PURE__ */ jsx11("h3", { className: "b316-privacy-title", children: "Your data" }),
1713
+ error ? /* @__PURE__ */ jsx11("div", { className: "b316-error", role: "alert", children: error }) : null,
1714
+ /* @__PURE__ */ jsxs10("div", { className: "b316-privacy-row", children: [
1715
+ /* @__PURE__ */ jsxs10("div", { children: [
1716
+ /* @__PURE__ */ jsx11("p", { children: "Download a copy" }),
1717
+ /* @__PURE__ */ jsx11("p", { className: "b316-muted", children: "Everything we hold about you, as a file. Passwords and security keys are not included \u2014 they are what protect the account rather than anything you can use." })
1718
+ ] }),
1719
+ /* @__PURE__ */ jsx11("button", { type: "button", className: "b316-button-quiet", disabled: busy, onClick: download, children: busy ? "Preparing\u2026" : "Download" })
1720
+ ] }),
1721
+ /* @__PURE__ */ jsxs10("div", { className: "b316-privacy-row", children: [
1722
+ /* @__PURE__ */ jsxs10("div", { children: [
1723
+ /* @__PURE__ */ jsx11("p", { children: "Delete your account" }),
1724
+ /* @__PURE__ */ jsx11("p", { className: "b316-muted", children: "Permanent. Invoices already raised are kept, because the law requires it; everything that identifies you is removed from them." })
1725
+ ] }),
1726
+ confirming ? null : /* @__PURE__ */ jsx11(
1727
+ "button",
1728
+ {
1729
+ type: "button",
1730
+ className: "b316-button-quiet b316-danger",
1731
+ disabled: busy,
1732
+ onClick: () => setConfirming(true),
1733
+ children: "Delete"
1734
+ }
1735
+ )
1736
+ ] }),
1737
+ confirming ? /* @__PURE__ */ jsxs10("div", { className: "b316-privacy-confirm", children: [
1738
+ /* @__PURE__ */ jsxs10("label", { className: "b316-label", htmlFor: "b316-privacy-confirm", children: [
1739
+ "Type ",
1740
+ confirmWord,
1741
+ " to confirm"
1742
+ ] }),
1743
+ /* @__PURE__ */ jsx11(
1744
+ "input",
1745
+ {
1746
+ id: "b316-privacy-confirm",
1747
+ className: "b316-input",
1748
+ value: typed,
1749
+ autoComplete: "off",
1750
+ onChange: (event) => setTyped(event.target.value)
1751
+ }
1752
+ ),
1753
+ /* @__PURE__ */ jsxs10("div", { className: "b316-privacy-actions", children: [
1754
+ /* @__PURE__ */ jsx11(
1755
+ "button",
1756
+ {
1757
+ type: "button",
1758
+ className: "b316-button-quiet",
1759
+ onClick: () => {
1760
+ setConfirming(false);
1761
+ setTyped("");
1762
+ },
1763
+ children: "Cancel"
1764
+ }
1765
+ ),
1766
+ /* @__PURE__ */ jsx11(
1767
+ "button",
1768
+ {
1769
+ type: "button",
1770
+ className: "b316-button b316-danger",
1771
+ disabled: busy || typed.trim() !== confirmWord,
1772
+ onClick: erase,
1773
+ children: busy ? "Deleting\u2026" : "Delete my account"
1774
+ }
1775
+ )
1776
+ ] })
1777
+ ] }) : null
1778
+ ] });
1779
+ }
1780
+
1781
+ // src/TwoFactorManager.tsx
1782
+ import { useState as useState10 } from "react";
1783
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1784
+ function TwoFactorManager({
1785
+ enabled = false,
1786
+ onEnabled,
1787
+ onDisabled,
1788
+ className,
1789
+ style
1790
+ }) {
1791
+ const { client, token } = useBase316();
1792
+ const [stage, setStage] = useState10({ name: "idle" });
1793
+ const [on, setOn] = useState10(enabled);
1794
+ const [code, setCode] = useState10("");
1795
+ const [error, setError] = useState10(null);
1796
+ const [busy, setBusy] = useState10(false);
1797
+ if (!token) {
1798
+ return null;
1799
+ }
1800
+ async function begin() {
1801
+ setBusy(true);
1802
+ setError(null);
1803
+ try {
1804
+ const enrollment = await client.auth.mfa.enroll(token);
1805
+ setStage({ name: "enrolling", ...enrollment });
1806
+ } catch (cause) {
1807
+ setError(cause instanceof Error ? cause.message : "Could not start setup.");
1808
+ } finally {
1809
+ setBusy(false);
1810
+ }
1811
+ }
1812
+ async function beginEmail() {
1813
+ setBusy(true);
1814
+ setError(null);
1815
+ try {
1816
+ const enrollment = await client.auth.mfa.enrollEmail(token);
1817
+ setStage({ name: "enrollingEmail", ...enrollment });
1818
+ } catch (cause) {
1819
+ setError(cause instanceof Error ? cause.message : "Could not send a code.");
1820
+ } finally {
1821
+ setBusy(false);
1822
+ }
1823
+ }
1824
+ async function confirm() {
1825
+ if (stage.name !== "enrolling" && stage.name !== "enrollingEmail") {
1826
+ return;
1827
+ }
1828
+ setBusy(true);
1829
+ setError(null);
1830
+ try {
1831
+ const result = await client.auth.mfa.confirm(token, {
1832
+ factorId: stage.factorId,
1833
+ code: code.trim()
1834
+ });
1835
+ setStage({ name: "saved", recoveryCodes: result.recoveryCodes });
1836
+ setOn(true);
1837
+ setCode("");
1838
+ onEnabled?.();
1839
+ } catch (cause) {
1840
+ setError(cause instanceof Error ? cause.message : "That code did not match.");
1841
+ } finally {
1842
+ setBusy(false);
1843
+ }
1844
+ }
1845
+ async function disable() {
1846
+ setBusy(true);
1847
+ setError(null);
1848
+ try {
1849
+ await client.auth.mfa.disable(token);
1850
+ setOn(false);
1851
+ setStage({ name: "idle" });
1852
+ onDisabled?.();
1853
+ } catch (cause) {
1854
+ setError(cause instanceof Error ? cause.message : "Could not turn it off.");
1855
+ } finally {
1856
+ setBusy(false);
1857
+ }
1858
+ }
1859
+ return /* @__PURE__ */ jsxs11("div", { className: cls("b316-card b316-mfa", className), style, children: [
1860
+ /* @__PURE__ */ jsx12("h3", { className: "b316-mfa-title", children: "Two-factor authentication" }),
1861
+ error ? /* @__PURE__ */ jsx12("div", { className: "b316-error", role: "alert", children: error }) : null,
1862
+ stage.name === "saved" ? /* @__PURE__ */ jsxs11("div", { className: "b316-mfa-codes", children: [
1863
+ /* @__PURE__ */ jsx12("p", { children: "Save these recovery codes somewhere safe. Each one signs you in once if you lose your authenticator, and they are not shown again." }),
1864
+ /* @__PURE__ */ jsx12("ul", { className: "b316-mfa-code-list", children: stage.recoveryCodes.map((recovery) => /* @__PURE__ */ jsx12("li", { children: /* @__PURE__ */ jsx12("code", { children: recovery }) }, recovery)) }),
1865
+ /* @__PURE__ */ jsx12("button", { type: "button", className: "b316-button", onClick: () => setStage({ name: "idle" }), children: "I have saved them" })
1866
+ ] }) : stage.name === "enrollingEmail" ? /* @__PURE__ */ jsxs11("div", { className: "b316-mfa-enrol", children: [
1867
+ /* @__PURE__ */ jsxs11("p", { children: [
1868
+ "We sent a code to ",
1869
+ stage.destinationHint,
1870
+ ". Enter it below to turn this on."
1871
+ ] }),
1872
+ /* @__PURE__ */ jsx12("label", { className: "b316-label", htmlFor: "b316-mfa-code", children: "Code" }),
1873
+ /* @__PURE__ */ jsx12(
1874
+ "input",
1875
+ {
1876
+ id: "b316-mfa-code",
1877
+ className: "b316-input",
1878
+ inputMode: "numeric",
1879
+ autoComplete: "one-time-code",
1880
+ value: code,
1881
+ onChange: (event) => setCode(event.target.value)
1882
+ }
1883
+ ),
1884
+ /* @__PURE__ */ jsxs11("div", { className: "b316-mfa-actions", children: [
1885
+ /* @__PURE__ */ jsx12(
1886
+ "button",
1887
+ {
1888
+ type: "button",
1889
+ className: "b316-button-quiet",
1890
+ onClick: () => setStage({ name: "idle" }),
1891
+ children: "Cancel"
1892
+ }
1893
+ ),
1894
+ /* @__PURE__ */ jsx12(
1895
+ "button",
1896
+ {
1897
+ type: "button",
1898
+ className: "b316-button",
1899
+ disabled: busy || !code,
1900
+ onClick: confirm,
1901
+ children: busy ? "Checking\u2026" : "Turn on"
1902
+ }
1903
+ )
1904
+ ] })
1905
+ ] }) : stage.name === "enrolling" ? /* @__PURE__ */ jsxs11("div", { className: "b316-mfa-enrol", children: [
1906
+ /* @__PURE__ */ jsx12("p", { children: "Add this to your authenticator app, then enter the six-digit code it shows." }),
1907
+ /* @__PURE__ */ jsx12("div", { className: "b316-mfa-secret", children: /* @__PURE__ */ jsx12("code", { children: stage.secret }) }),
1908
+ /* @__PURE__ */ jsx12("a", { className: "b316-mfa-uri", href: stage.uri, children: "Open in an authenticator app" }),
1909
+ /* @__PURE__ */ jsx12("label", { className: "b316-label", htmlFor: "b316-mfa-code", children: "Code" }),
1910
+ /* @__PURE__ */ jsx12(
1911
+ "input",
1912
+ {
1913
+ id: "b316-mfa-code",
1914
+ className: "b316-input",
1915
+ inputMode: "numeric",
1916
+ autoComplete: "one-time-code",
1917
+ value: code,
1918
+ onChange: (event) => setCode(event.target.value)
1919
+ }
1920
+ ),
1921
+ /* @__PURE__ */ jsxs11("div", { className: "b316-mfa-actions", children: [
1922
+ /* @__PURE__ */ jsx12(
1923
+ "button",
1924
+ {
1925
+ type: "button",
1926
+ className: "b316-button-quiet",
1927
+ onClick: () => setStage({ name: "idle" }),
1928
+ children: "Cancel"
1929
+ }
1930
+ ),
1931
+ /* @__PURE__ */ jsx12(
1932
+ "button",
1933
+ {
1934
+ type: "button",
1935
+ className: "b316-button",
1936
+ disabled: busy || !code,
1937
+ onClick: confirm,
1938
+ children: busy ? "Checking\u2026" : "Turn on"
1939
+ }
1940
+ )
1941
+ ] })
1942
+ ] }) : on ? /* @__PURE__ */ jsxs11("div", { className: "b316-mfa-on", children: [
1943
+ /* @__PURE__ */ jsx12("p", { className: "b316-muted", children: "On. You will be asked for a code when you sign in." }),
1944
+ /* @__PURE__ */ jsx12(
1945
+ ConfirmButton,
1946
+ {
1947
+ className: "b316-button-quiet b316-danger",
1948
+ disabled: busy,
1949
+ confirmLabel: "Turn off two-factor authentication",
1950
+ onConfirm: disable,
1951
+ children: "Turn off"
1952
+ }
1953
+ )
1954
+ ] }) : /* @__PURE__ */ jsxs11("div", { className: "b316-mfa-off", children: [
1955
+ /* @__PURE__ */ jsx12("p", { className: "b316-muted", children: "Off. Turning it on means a code as well as your password, every time you sign in." }),
1956
+ /* @__PURE__ */ jsxs11("div", { className: "b316-mfa-actions", children: [
1957
+ /* @__PURE__ */ jsx12("button", { type: "button", className: "b316-button", disabled: busy, onClick: begin, children: busy ? "Starting\u2026" : "Use an authenticator app" }),
1958
+ /* @__PURE__ */ jsx12(
1959
+ "button",
1960
+ {
1961
+ type: "button",
1962
+ className: "b316-button-quiet",
1963
+ disabled: busy,
1964
+ onClick: beginEmail,
1965
+ children: "Email me a code"
1966
+ }
1967
+ )
1968
+ ] })
1969
+ ] })
1970
+ ] });
1971
+ }
1972
+
1973
+ // src/ForgotPassword.tsx
1974
+ import { useState as useState11 } from "react";
1975
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
1976
+ function ForgotPassword({ onSent, signInUrl, className, style }) {
1977
+ const { client } = useBase316();
1978
+ const [email, setEmail] = useState11("");
1979
+ const [sent, setSent] = useState11(false);
1980
+ const [busy, setBusy] = useState11(false);
1981
+ const [error, setError] = useState11(null);
1982
+ async function submit(event) {
1983
+ event.preventDefault();
1984
+ setBusy(true);
1985
+ setError(null);
1986
+ try {
1987
+ await client.auth.requestPasswordReset({ email: email.trim() });
1988
+ setSent(true);
1989
+ onSent?.(email.trim());
1990
+ } catch (cause) {
1991
+ setError(cause instanceof Error ? cause.message : "Could not send the link.");
1992
+ } finally {
1993
+ setBusy(false);
1994
+ }
1995
+ }
1996
+ if (sent) {
1997
+ return /* @__PURE__ */ jsxs12("div", { className: cls("b316-card", className), style, children: [
1998
+ /* @__PURE__ */ jsx13("h3", { className: "b316-form-title", children: "Check your email" }),
1999
+ /* @__PURE__ */ jsxs12("p", { className: "b316-muted", children: [
2000
+ "If there is an account for ",
2001
+ email.trim(),
2002
+ ", a link to set a new password is on its way."
2003
+ ] }),
2004
+ signInUrl ? /* @__PURE__ */ jsx13("a", { className: "b316-button-link", href: signInUrl, children: "Back to sign in" }) : null
2005
+ ] });
2006
+ }
2007
+ return /* @__PURE__ */ jsxs12("form", { className: cls("b316-card b316-form", className), style, onSubmit: submit, children: [
2008
+ /* @__PURE__ */ jsx13("h3", { className: "b316-form-title", children: "Reset your password" }),
2009
+ error ? /* @__PURE__ */ jsx13("div", { className: "b316-error", role: "alert", children: error }) : null,
2010
+ /* @__PURE__ */ jsxs12("div", { className: "b316-field", children: [
2011
+ /* @__PURE__ */ jsx13("label", { className: "b316-label", htmlFor: "b316-forgot-email", children: "Email" }),
2012
+ /* @__PURE__ */ jsx13(
2013
+ "input",
2014
+ {
2015
+ id: "b316-forgot-email",
2016
+ className: "b316-input",
2017
+ type: "email",
2018
+ autoComplete: "email",
2019
+ required: true,
2020
+ value: email,
2021
+ onChange: (event) => setEmail(event.target.value)
2022
+ }
2023
+ )
2024
+ ] }),
2025
+ /* @__PURE__ */ jsx13("button", { type: "submit", className: "b316-button", disabled: busy || !email, children: busy ? "Sending\u2026" : "Send reset link" }),
2026
+ signInUrl ? /* @__PURE__ */ jsx13("a", { className: "b316-button-link", href: signInUrl, children: "Back to sign in" }) : null
2027
+ ] });
2028
+ }
2029
+
2030
+ // src/ResetPassword.tsx
2031
+ import { useEffect as useEffect7, useState as useState12 } from "react";
2032
+ import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2033
+ function ResetPassword({
2034
+ token: supplied,
2035
+ onReset,
2036
+ signInUrl,
2037
+ className,
2038
+ style
2039
+ }) {
2040
+ const { client } = useBase316();
2041
+ const [token, setToken] = useState12(supplied ?? null);
2042
+ const [password, setPassword] = useState12("");
2043
+ const [confirmation, setConfirmation] = useState12("");
2044
+ const [done, setDone] = useState12(false);
2045
+ const [busy, setBusy] = useState12(false);
2046
+ const [error, setError] = useState12(null);
2047
+ useEffect7(() => {
2048
+ if (supplied || typeof window === "undefined") {
2049
+ return;
2050
+ }
2051
+ const url = new URL(window.location.href);
2052
+ const found = url.searchParams.get("token");
2053
+ if (!found) {
2054
+ return;
2055
+ }
2056
+ url.searchParams.delete("token");
2057
+ window.history.replaceState({}, "", url.toString());
2058
+ setToken(found);
2059
+ }, [supplied]);
2060
+ async function submit(event) {
2061
+ event.preventDefault();
2062
+ if (password !== confirmation) {
2063
+ setError("Those do not match.");
2064
+ return;
2065
+ }
2066
+ if (!token) {
2067
+ return;
2068
+ }
2069
+ setBusy(true);
2070
+ setError(null);
2071
+ try {
2072
+ await client.auth.completePasswordReset({ token, password });
2073
+ setDone(true);
2074
+ onReset?.();
2075
+ } catch (cause) {
2076
+ setError(cause instanceof Error ? cause.message : "That link is no longer valid.");
2077
+ } finally {
2078
+ setBusy(false);
2079
+ }
2080
+ }
2081
+ if (done) {
2082
+ return /* @__PURE__ */ jsxs13("div", { className: cls("b316-card", className), style, children: [
2083
+ /* @__PURE__ */ jsx14("h3", { className: "b316-form-title", children: "Password changed" }),
2084
+ /* @__PURE__ */ jsx14("p", { className: "b316-muted", children: "You can sign in with your new password." }),
2085
+ signInUrl ? /* @__PURE__ */ jsx14("a", { className: "b316-button-link", href: signInUrl, children: "Sign in" }) : null
2086
+ ] });
2087
+ }
2088
+ if (!token) {
2089
+ return /* @__PURE__ */ jsxs13("div", { className: cls("b316-card", className), style, children: [
2090
+ /* @__PURE__ */ jsx14("h3", { className: "b316-form-title", children: "Nothing to reset" }),
2091
+ /* @__PURE__ */ jsx14("p", { className: "b316-muted", children: "This page needs the link from your email. Request a new one if yours has expired." })
2092
+ ] });
2093
+ }
2094
+ return /* @__PURE__ */ jsxs13("form", { className: cls("b316-card b316-form", className), style, onSubmit: submit, children: [
2095
+ /* @__PURE__ */ jsx14("h3", { className: "b316-form-title", children: "Choose a new password" }),
2096
+ error ? /* @__PURE__ */ jsx14("div", { className: "b316-error", role: "alert", children: error }) : null,
2097
+ /* @__PURE__ */ jsxs13("div", { className: "b316-field", children: [
2098
+ /* @__PURE__ */ jsx14("label", { className: "b316-label", htmlFor: "b316-reset-password", children: "New password" }),
2099
+ /* @__PURE__ */ jsx14(
2100
+ "input",
2101
+ {
2102
+ id: "b316-reset-password",
2103
+ className: "b316-input",
2104
+ type: "password",
2105
+ autoComplete: "new-password",
2106
+ required: true,
2107
+ value: password,
2108
+ onChange: (event) => setPassword(event.target.value)
2109
+ }
2110
+ )
2111
+ ] }),
2112
+ /* @__PURE__ */ jsxs13("div", { className: "b316-field", children: [
2113
+ /* @__PURE__ */ jsx14("label", { className: "b316-label", htmlFor: "b316-reset-confirm", children: "Repeat it" }),
2114
+ /* @__PURE__ */ jsx14(
2115
+ "input",
2116
+ {
2117
+ id: "b316-reset-confirm",
2118
+ className: "b316-input",
2119
+ type: "password",
2120
+ autoComplete: "new-password",
2121
+ required: true,
2122
+ value: confirmation,
2123
+ onChange: (event) => setConfirmation(event.target.value)
2124
+ }
2125
+ )
2126
+ ] }),
2127
+ /* @__PURE__ */ jsx14("button", { type: "submit", className: "b316-button", disabled: busy || !password, children: busy ? "Saving\u2026" : "Set password" })
2128
+ ] });
2129
+ }
2130
+
2131
+ // src/VerifyEmail.tsx
2132
+ import { useEffect as useEffect8, useRef as useRef3, useState as useState13 } from "react";
2133
+ import { Fragment as Fragment2, jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
2134
+ function VerifyEmail({
2135
+ token: supplied,
2136
+ onVerified,
2137
+ continueUrl,
2138
+ className,
2139
+ style
2140
+ }) {
2141
+ const { client } = useBase316();
2142
+ const [state, setState] = useState13("waiting");
2143
+ const spent = useRef3(false);
2144
+ useEffect8(() => {
2145
+ if (spent.current) {
2146
+ return;
2147
+ }
2148
+ let token = supplied ?? null;
2149
+ if (!token && typeof window !== "undefined") {
2150
+ const url = new URL(window.location.href);
2151
+ token = url.searchParams.get("token");
2152
+ if (token) {
2153
+ url.searchParams.delete("token");
2154
+ window.history.replaceState({}, "", url.toString());
2155
+ }
2156
+ }
2157
+ if (!token) {
2158
+ setState("missing");
2159
+ return;
2160
+ }
2161
+ spent.current = true;
2162
+ setState("verifying");
2163
+ client.auth.verifyEmail({ token }).then(() => {
2164
+ setState("verified");
2165
+ onVerified?.();
2166
+ }).catch(() => {
2167
+ setState("failed");
2168
+ });
2169
+ }, [client, supplied, onVerified]);
2170
+ return /* @__PURE__ */ jsx15("div", { className: cls("b316-card", className), style, children: state === "verified" ? /* @__PURE__ */ jsxs14(Fragment2, { children: [
2171
+ /* @__PURE__ */ jsx15("h3", { className: "b316-form-title", children: "Email confirmed" }),
2172
+ /* @__PURE__ */ jsx15("p", { className: "b316-muted", children: "Thanks \u2014 your address is verified." }),
2173
+ continueUrl ? /* @__PURE__ */ jsx15("a", { className: "b316-button-link", href: continueUrl, children: "Continue" }) : null
2174
+ ] }) : state === "failed" ? /* @__PURE__ */ jsxs14(Fragment2, { children: [
2175
+ /* @__PURE__ */ jsx15("h3", { className: "b316-form-title", children: "That link did not work" }),
2176
+ /* @__PURE__ */ jsx15("p", { className: "b316-muted", children: "It may have expired or already been used. Ask for a new one and try again." })
2177
+ ] }) : state === "missing" ? /* @__PURE__ */ jsxs14(Fragment2, { children: [
2178
+ /* @__PURE__ */ jsx15("h3", { className: "b316-form-title", children: "Nothing to confirm" }),
2179
+ /* @__PURE__ */ jsx15("p", { className: "b316-muted", children: "This page needs the link from your email." })
2180
+ ] }) : /* @__PURE__ */ jsx15("p", { className: "b316-muted", children: "Confirming your email\u2026" }) });
2181
+ }
2182
+
2183
+ // src/TeamMembers.tsx
2184
+ import { useCallback as useCallback5, useEffect as useEffect9, useState as useState14 } from "react";
2185
+ import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
2186
+ function TeamMembers({
2187
+ organizationId,
2188
+ onInvited,
2189
+ acceptUrl,
2190
+ onLeft,
2191
+ className,
2192
+ style
2193
+ }) {
2194
+ const { client, token, session } = useBase316();
2195
+ const [teams, setTeams] = useState14(null);
2196
+ const [members, setMembers] = useState14(null);
2197
+ const [invitations, setInvitations] = useState14(null);
2198
+ const [email, setEmail] = useState14("");
2199
+ const [busy, setBusy] = useState14(false);
2200
+ const [error, setError] = useState14(null);
2201
+ const [copied, setCopied] = useState14(null);
2202
+ const chosen = organizationId ?? (teams?.length === 1 ? teams[0].id : null);
2203
+ const team = teams?.find((candidate) => candidate.id === chosen) ?? null;
2204
+ const load = useCallback5(() => {
2205
+ if (!token) {
2206
+ return;
2207
+ }
2208
+ client.teams.list(token).then((result) => setTeams(result.data)).catch(
2209
+ (cause) => setError(cause instanceof Error ? cause.message : "Could not load your teams.")
2210
+ );
2211
+ }, [client, token]);
2212
+ useEffect9(load, [load]);
2213
+ useEffect9(() => {
2214
+ if (!token || !chosen) {
2215
+ return;
2216
+ }
2217
+ let cancelled = false;
2218
+ client.teams.members(token, chosen).then((result) => {
2219
+ if (!cancelled) {
2220
+ setMembers(result.data);
2221
+ }
2222
+ }).catch((cause) => {
2223
+ if (!cancelled) {
2224
+ setError(cause instanceof Error ? cause.message : "Could not load the members.");
2225
+ }
2226
+ });
2227
+ client.teams.invitations(token, chosen).then((result) => {
2228
+ if (!cancelled) {
2229
+ setInvitations(result.data);
2230
+ }
2231
+ }).catch(() => {
2232
+ if (!cancelled) {
2233
+ setInvitations(null);
2234
+ }
2235
+ });
2236
+ return () => {
2237
+ cancelled = true;
2238
+ };
2239
+ }, [client, token, chosen]);
2240
+ if (!token) {
2241
+ return null;
2242
+ }
2243
+ if (!teams) {
2244
+ return /* @__PURE__ */ jsx16("div", { className: cls("b316-card", className), style, children: error ?? "Loading\u2026" });
2245
+ }
2246
+ if (!chosen) {
2247
+ return /* @__PURE__ */ jsx16("div", { className: cls("b316-card", className), style, children: teams.length === 0 ? "You are not in a team yet." : "You are in more than one team. Name which with the organizationId prop." });
2248
+ }
2249
+ const canManage = invitations !== null;
2250
+ async function invite(event) {
2251
+ event.preventDefault();
2252
+ setBusy(true);
2253
+ setError(null);
2254
+ try {
2255
+ const result = await client.teams.invite(token, chosen, {
2256
+ email: email.trim()
2257
+ });
2258
+ const url = acceptUrl ? `${acceptUrl}${acceptUrl.includes("?") ? "&" : "?"}invitation=${encodeURIComponent(result.token)}` : void 0;
2259
+ onInvited?.({ email: email.trim(), token: result.token, url });
2260
+ if (!onInvited) {
2261
+ setCopied(url ?? result.token);
2262
+ }
2263
+ setEmail("");
2264
+ setInvitations((current) => [result.invitation, ...current ?? []]);
2265
+ } catch (cause) {
2266
+ setError(cause instanceof Error ? cause.message : "That invitation could not be sent.");
2267
+ } finally {
2268
+ setBusy(false);
2269
+ }
2270
+ }
2271
+ async function remove(userId) {
2272
+ const leaving = userId === session?.userId;
2273
+ setBusy(true);
2274
+ setError(null);
2275
+ try {
2276
+ await client.teams.removeMember(token, chosen, userId);
2277
+ if (leaving) {
2278
+ onLeft?.();
2279
+ load();
2280
+ } else {
2281
+ setMembers((current) => (current ?? []).filter((member) => member.user.id !== userId));
2282
+ }
2283
+ } catch (cause) {
2284
+ setError(cause instanceof Error ? cause.message : "That did not work.");
2285
+ } finally {
2286
+ setBusy(false);
2287
+ }
2288
+ }
2289
+ async function withdraw(invitationId) {
2290
+ try {
2291
+ await client.teams.revokeInvitation(token, chosen, invitationId);
2292
+ setInvitations((current) => (current ?? []).filter((one) => one.id !== invitationId));
2293
+ } catch (cause) {
2294
+ setError(cause instanceof Error ? cause.message : "That invitation could not be withdrawn.");
2295
+ }
2296
+ }
2297
+ return /* @__PURE__ */ jsxs15("div", { className: cls("b316-card b316-team", className), style, children: [
2298
+ /* @__PURE__ */ jsx16("h3", { className: "b316-team-title", children: team?.name ?? "Team" }),
2299
+ error ? /* @__PURE__ */ jsx16("div", { className: "b316-error", role: "alert", children: error }) : null,
2300
+ /* @__PURE__ */ jsx16("ul", { className: "b316-team-list", children: (members ?? []).map((member) => /* @__PURE__ */ jsxs15("li", { className: "b316-team-member", children: [
2301
+ /* @__PURE__ */ jsxs15("div", { children: [
2302
+ /* @__PURE__ */ jsx16("div", { children: member.user.name ?? member.user.email ?? "Someone" }),
2303
+ /* @__PURE__ */ jsxs15("div", { className: "b316-muted", children: [
2304
+ member.user.email,
2305
+ member.user.id === session?.userId ? " \xB7 you" : ""
2306
+ ] })
2307
+ ] }),
2308
+ /* @__PURE__ */ jsxs15("div", { className: "b316-team-member-side", children: [
2309
+ /* @__PURE__ */ jsx16("span", { className: "b316-chip", children: member.role.name }),
2310
+ canManage || member.user.id === session?.userId ? /* @__PURE__ */ jsx16(
2311
+ ConfirmButton,
2312
+ {
2313
+ className: "b316-button-quiet b316-danger",
2314
+ disabled: busy,
2315
+ confirmLabel: member.user.id === session?.userId ? "Leave this team" : `Remove ${member.user.name ?? member.user.email ?? "them"}`,
2316
+ onConfirm: () => remove(member.user.id),
2317
+ children: member.user.id === session?.userId ? "Leave" : "Remove"
2318
+ }
2319
+ ) : null
2320
+ ] })
2321
+ ] }, member.id)) }),
2322
+ canManage && (invitations ?? []).length > 0 ? /* @__PURE__ */ jsxs15(Fragment3, { children: [
2323
+ /* @__PURE__ */ jsx16("h4", { className: "b316-team-subtitle", children: "Invited" }),
2324
+ /* @__PURE__ */ jsx16("ul", { className: "b316-team-list", children: (invitations ?? []).map((invitation) => /* @__PURE__ */ jsxs15("li", { className: "b316-team-member", children: [
2325
+ /* @__PURE__ */ jsxs15("div", { children: [
2326
+ /* @__PURE__ */ jsx16("div", { children: invitation.email }),
2327
+ /* @__PURE__ */ jsxs15("div", { className: "b316-muted", children: [
2328
+ invitation.role.name,
2329
+ " \xB7 expires",
2330
+ " ",
2331
+ new Date(invitation.expiresAt).toLocaleDateString()
2332
+ ] })
2333
+ ] }),
2334
+ /* @__PURE__ */ jsx16(
2335
+ "button",
2336
+ {
2337
+ type: "button",
2338
+ className: "b316-button-quiet",
2339
+ onClick: () => void withdraw(invitation.id),
2340
+ children: "Withdraw"
2341
+ }
2342
+ )
2343
+ ] }, invitation.id)) })
2344
+ ] }) : null,
2345
+ canManage ? /* @__PURE__ */ jsxs15("form", { className: "b316-team-invite", onSubmit: invite, children: [
2346
+ /* @__PURE__ */ jsx16("label", { className: "b316-label", htmlFor: "b316-invite-email", children: "Invite by email" }),
2347
+ /* @__PURE__ */ jsxs15("div", { className: "b316-team-invite-row", children: [
2348
+ /* @__PURE__ */ jsx16(
2349
+ "input",
2350
+ {
2351
+ id: "b316-invite-email",
2352
+ className: "b316-input",
2353
+ type: "email",
2354
+ required: true,
2355
+ placeholder: "colleague@example.com",
2356
+ value: email,
2357
+ onChange: (event) => setEmail(event.target.value)
2358
+ }
2359
+ ),
2360
+ /* @__PURE__ */ jsx16("button", { type: "submit", className: "b316-button", disabled: busy || !email, children: busy ? "Sending\u2026" : "Invite" })
2361
+ ] })
2362
+ ] }) : null,
2363
+ copied ? /* @__PURE__ */ jsxs15("div", { className: "b316-team-link", children: [
2364
+ /* @__PURE__ */ jsx16("div", { className: "b316-muted", children: "Send them this link. It is shown once and cannot be fetched again." }),
2365
+ /* @__PURE__ */ jsx16("code", { children: copied })
2366
+ ] }) : null
2367
+ ] });
2368
+ }
2369
+
2370
+ // src/AcceptInvitation.tsx
2371
+ import { useEffect as useEffect10, useState as useState15 } from "react";
2372
+ import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
2373
+ function AcceptInvitation({
2374
+ token: supplied,
2375
+ onAccepted,
2376
+ signUpUrl,
2377
+ signInUrl,
2378
+ className,
2379
+ style
2380
+ }) {
2381
+ const { client, token: session, session: user } = useBase316();
2382
+ const [invitationToken, setInvitationToken] = useState15(supplied ?? null);
2383
+ const [preview, setPreview] = useState15(null);
2384
+ const [state, setState] = useState15("loading");
2385
+ const [error, setError] = useState15(null);
2386
+ const [busy, setBusy] = useState15(false);
2387
+ useEffect10(() => {
2388
+ if (supplied || typeof window === "undefined") {
2389
+ return;
2390
+ }
2391
+ setInvitationToken(new URL(window.location.href).searchParams.get("invitation"));
2392
+ }, [supplied]);
2393
+ useEffect10(() => {
2394
+ if (!invitationToken) {
2395
+ setState(supplied === void 0 ? "loading" : "invalid");
2396
+ return;
2397
+ }
2398
+ let cancelled = false;
2399
+ client.teams.preview(invitationToken).then((result) => {
2400
+ if (!cancelled) {
2401
+ setPreview(result);
2402
+ setState("ready");
2403
+ }
2404
+ }).catch(() => {
2405
+ if (!cancelled) {
2406
+ setState("invalid");
2407
+ }
2408
+ });
2409
+ return () => {
2410
+ cancelled = true;
2411
+ };
2412
+ }, [client, invitationToken, supplied]);
2413
+ async function accept() {
2414
+ if (!session || !invitationToken) {
2415
+ return;
2416
+ }
2417
+ setBusy(true);
2418
+ setError(null);
2419
+ try {
2420
+ const result = await client.teams.accept(session, invitationToken);
2421
+ setState("accepted");
2422
+ onAccepted?.(result.organizationId);
2423
+ } catch (cause) {
2424
+ setError(cause instanceof Error ? cause.message : "That invitation could not be accepted.");
2425
+ } finally {
2426
+ setBusy(false);
2427
+ }
2428
+ }
2429
+ if (state === "loading") {
2430
+ return /* @__PURE__ */ jsx17("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
2431
+ }
2432
+ if (state === "invalid") {
2433
+ return /* @__PURE__ */ jsxs16("div", { className: cls("b316-card", className), style, children: [
2434
+ /* @__PURE__ */ jsx17("h3", { className: "b316-form-title", children: "That invitation is no longer valid" }),
2435
+ /* @__PURE__ */ jsx17("p", { className: "b316-muted", children: "It may have expired, been withdrawn, or already been used. Ask for a new one." })
2436
+ ] });
2437
+ }
2438
+ if (state === "accepted") {
2439
+ return /* @__PURE__ */ jsx17("div", { className: cls("b316-card", className), style, children: /* @__PURE__ */ jsxs16("h3", { className: "b316-form-title", children: [
2440
+ "You have joined ",
2441
+ preview?.organizationName
2442
+ ] }) });
2443
+ }
2444
+ const matches = !user || (user.user.email ?? "").toLowerCase() === preview?.email.toLowerCase();
2445
+ return /* @__PURE__ */ jsxs16("div", { className: cls("b316-card b316-invitation", className), style, children: [
2446
+ /* @__PURE__ */ jsxs16("h3", { className: "b316-form-title", children: [
2447
+ "Join ",
2448
+ preview?.organizationName
2449
+ ] }),
2450
+ /* @__PURE__ */ jsxs16("p", { className: "b316-muted", children: [
2451
+ "Invited as ",
2452
+ preview?.email,
2453
+ " \xB7 ",
2454
+ preview?.roleName
2455
+ ] }),
2456
+ error ? /* @__PURE__ */ jsx17("div", { className: "b316-error", role: "alert", children: error }) : null,
2457
+ !session ? /* @__PURE__ */ jsxs16(Fragment4, { children: [
2458
+ /* @__PURE__ */ jsxs16("p", { children: [
2459
+ "Sign in as ",
2460
+ preview?.email,
2461
+ " to accept."
2462
+ ] }),
2463
+ /* @__PURE__ */ jsxs16("div", { className: "b316-invitation-actions", children: [
2464
+ signInUrl ? /* @__PURE__ */ jsx17("a", { className: "b316-button", href: signInUrl, children: "Sign in" }) : null,
2465
+ signUpUrl ? /* @__PURE__ */ jsx17("a", { className: "b316-button-link", href: signUpUrl, children: "Create an account" }) : null
2466
+ ] })
2467
+ ] }) : !matches ? (
2468
+ // Said before they press anything. The server refuses this, but
2469
+ // finding out after a click reads as a broken invitation rather than
2470
+ // as being signed in as the wrong person.
2471
+ /* @__PURE__ */ jsxs16("p", { children: [
2472
+ "You are signed in as ",
2473
+ user?.user.email,
2474
+ ". This invitation was sent to ",
2475
+ preview?.email,
2476
+ " \u2014 sign in as that address to accept it."
2477
+ ] })
2478
+ ) : /* @__PURE__ */ jsx17("button", { type: "button", className: "b316-button", disabled: busy, onClick: accept, children: busy ? "Joining\u2026" : `Join ${preview?.organizationName}` })
2479
+ ] });
2480
+ }
2481
+
2482
+ // src/PayInvoice.tsx
2483
+ import { useEffect as useEffect11, useRef as useRef4, useState as useState16 } from "react";
2484
+ import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
2485
+ var STRIPE_SRC = "https://js.stripe.com/v3/";
2486
+ function loadStripeScript() {
2487
+ if (typeof window === "undefined") {
2488
+ return Promise.reject(new Error("Stripe.js cannot load during server rendering."));
2489
+ }
2490
+ if (window.Stripe) {
2491
+ return Promise.resolve();
2492
+ }
2493
+ return new Promise((resolve, reject) => {
2494
+ const existing = document.querySelector(`script[src="${STRIPE_SRC}"]`);
2495
+ if (existing) {
2496
+ existing.addEventListener("load", () => resolve());
2497
+ existing.addEventListener("error", () => reject(new Error("Stripe.js failed to load.")));
2498
+ return;
2499
+ }
2500
+ const script = document.createElement("script");
2501
+ script.src = STRIPE_SRC;
2502
+ script.async = true;
2503
+ script.onload = () => resolve();
2504
+ script.onerror = () => reject(new Error("Stripe.js failed to load."));
2505
+ document.head.appendChild(script);
2506
+ });
2507
+ }
2508
+ function PayInvoice({
2509
+ token,
2510
+ environmentId,
2511
+ onPaid,
2512
+ className,
2513
+ style
2514
+ }) {
2515
+ const { client } = useBase316();
2516
+ const mountRef = useRef4(null);
2517
+ const stripeRef = useRef4(null);
2518
+ const elementsRef = useRef4(null);
2519
+ const [ready, setReady] = useState16(false);
2520
+ const [paying, setPaying] = useState16(false);
2521
+ const [done, setDone] = useState16(false);
2522
+ const [error, setError] = useState16(null);
2523
+ useEffect11(() => {
2524
+ let cancelled = false;
2525
+ async function start() {
2526
+ const base = `${client.baseUrl}/pay/${encodeURIComponent(environmentId)}/${encodeURIComponent(token)}`;
2527
+ const response = await fetch(`${base}/intent`, { method: "POST" });
2528
+ if (!response.ok) {
2529
+ throw new Error("This payment link is no longer valid.");
2530
+ }
2531
+ const intent = await response.json();
2532
+ await loadStripeScript();
2533
+ if (cancelled || !window.Stripe || !mountRef.current) {
2534
+ return;
2535
+ }
2536
+ const stripe = window.Stripe(
2537
+ intent.publishableKey,
2538
+ intent.accountId ? { stripeAccount: intent.accountId } : void 0
2539
+ );
2540
+ const elements = stripe.elements({ clientSecret: intent.clientSecret });
2541
+ elements.create("payment").mount(mountRef.current);
2542
+ stripeRef.current = stripe;
2543
+ elementsRef.current = elements;
2544
+ setReady(true);
2545
+ }
2546
+ start().catch((cause) => {
2547
+ if (!cancelled) {
2548
+ setError(cause instanceof Error ? cause.message : "Could not prepare this payment.");
2549
+ }
2550
+ });
2551
+ return () => {
2552
+ cancelled = true;
2553
+ };
2554
+ }, [client, environmentId, token]);
2555
+ const submit = async (event) => {
2556
+ event.preventDefault();
2557
+ if (!stripeRef.current || !elementsRef.current) {
2558
+ return;
2559
+ }
2560
+ setPaying(true);
2561
+ setError(null);
2562
+ const result = await stripeRef.current.confirmPayment({
2563
+ elements: elementsRef.current,
2564
+ confirmParams: { return_url: window.location.href },
2565
+ redirect: "if_required"
2566
+ });
2567
+ if (result.error) {
2568
+ setError(result.error.message ?? "That payment could not be completed.");
2569
+ setPaying(false);
2570
+ return;
2571
+ }
2572
+ setDone(true);
2573
+ onPaid?.();
2574
+ };
2575
+ if (done) {
2576
+ return /* @__PURE__ */ jsx18("div", { className: cls("b316-pay", className), style, children: /* @__PURE__ */ jsx18("p", { className: "b316-pay__done", children: "Paid \u2014 thank you." }) });
2577
+ }
2578
+ return /* @__PURE__ */ jsxs17("form", { className: cls("b316-pay", className), style, onSubmit: submit, children: [
2579
+ error ? /* @__PURE__ */ jsx18("p", { className: "b316-pay__error", role: "alert", children: error }) : null,
2580
+ /* @__PURE__ */ jsx18("div", { ref: mountRef, className: "b316-pay__element" }),
2581
+ /* @__PURE__ */ jsx18("button", { className: "b316-pay__submit", type: "submit", disabled: !ready || paying, children: paying ? "Paying\u2026" : "Pay" })
2582
+ ] });
2583
+ }
2584
+
2585
+ // src/usePaymentLink.ts
2586
+ import { useEffect as useEffect12, useRef as useRef5, useState as useState17 } from "react";
2587
+ function usePaymentLink({
2588
+ token,
2589
+ environmentId,
2590
+ intervalMs = 2e3,
2591
+ onPaid
2592
+ }) {
2593
+ const { client } = useBase316();
2594
+ const [status, setStatus] = useState17({ state: "loading" });
2595
+ const onPaidRef = useRef5(onPaid);
2596
+ onPaidRef.current = onPaid;
2597
+ useEffect12(() => {
2598
+ let stopped = false;
2599
+ let timer;
2600
+ const statusUrl = `${client.baseUrl}/pay/${encodeURIComponent(environmentId)}/${encodeURIComponent(token)}/status`;
2601
+ async function check() {
2602
+ try {
2603
+ const response = await fetch(statusUrl, { cache: "no-store" });
2604
+ if (stopped) {
2605
+ return;
2606
+ }
2607
+ if (response.status === 404) {
2608
+ setStatus({ state: "invalid" });
2609
+ return;
2610
+ }
2611
+ if (response.ok) {
2612
+ const body = await response.json();
2613
+ if (stopped) {
2614
+ return;
2615
+ }
2616
+ if (body.state === "settled") {
2617
+ setStatus({ state: "settled", number: body.number });
2618
+ onPaidRef.current?.();
2619
+ return;
2620
+ }
2621
+ setStatus({
2622
+ state: "payable",
2623
+ number: body.number,
2624
+ due: body.due ?? "0",
2625
+ currency: body.currency ?? "USD"
2626
+ });
2627
+ }
2628
+ } catch {
2629
+ }
2630
+ if (!stopped) {
2631
+ timer = setTimeout(check, intervalMs);
2632
+ }
2633
+ }
2634
+ void check();
2635
+ return () => {
2636
+ stopped = true;
2637
+ if (timer) {
2638
+ clearTimeout(timer);
2639
+ }
2640
+ };
2641
+ }, [client, environmentId, token, intervalMs]);
2642
+ return status;
2643
+ }
2644
+
2645
+ // src/usePlans.ts
2646
+ import { useCallback as useCallback6, useEffect as useEffect13, useState as useState18 } from "react";
2647
+ var CHECKOUT_STATUSES = /* @__PURE__ */ new Set([402, 409]);
2648
+ function usePlans(options = {}) {
2649
+ const { client, token } = useBase316();
2650
+ const [plans, setPlans] = useState18(null);
2651
+ const [error, setError] = useState18(false);
2652
+ const [busyPlanVersionId, setBusy] = useState18(null);
2653
+ const [lastError, setLastError] = useState18(null);
2654
+ const [ownPlanVersionId, setOwnPlanVersionId] = useState18(null);
2655
+ const [reloadCount, setReloadCount] = useState18(0);
2656
+ const supplied = options.currentPlanVersionId;
2657
+ const discover = supplied === void 0;
2658
+ useEffect13(() => {
2659
+ let cancelled = false;
2660
+ client.billing.plans().then((result) => {
2661
+ if (!cancelled) {
2662
+ setPlans(result.data);
2663
+ }
2664
+ }).catch((cause) => {
2665
+ console.error("[base316] could not load plans", cause);
2666
+ if (!cancelled) {
2667
+ setError(true);
2668
+ }
2669
+ });
2670
+ return () => {
2671
+ cancelled = true;
2672
+ };
2673
+ }, [client, reloadCount]);
2674
+ useEffect13(() => {
2675
+ if (!discover || !token) {
2676
+ return;
2677
+ }
2678
+ let cancelled = false;
2679
+ client.billing.subscription(token).then((result) => {
2680
+ if (!cancelled) {
2681
+ setOwnPlanVersionId(result.subscription?.planVersion.id ?? null);
2682
+ }
2683
+ }).catch(() => null);
2684
+ return () => {
2685
+ cancelled = true;
2686
+ };
2687
+ }, [client, token, discover, reloadCount]);
2688
+ const reload = useCallback6(() => {
2689
+ setError(false);
2690
+ setReloadCount((count) => count + 1);
2691
+ }, []);
2692
+ const selectPlan = useCallback6(
2693
+ async (plan) => {
2694
+ setLastError(null);
2695
+ if (!token) {
2696
+ return { status: "needs-sign-in" };
2697
+ }
2698
+ setBusy(plan.planVersionId);
2699
+ const go = (url) => {
2700
+ const navigate = options.navigate ?? true;
2701
+ if (typeof navigate === "function") {
2702
+ navigate(url);
2703
+ } else if (navigate && typeof window !== "undefined") {
2704
+ window.location.assign(url);
2705
+ }
2706
+ };
2707
+ try {
2708
+ await client.billing.changePlan(token, plan.planVersionId);
2709
+ setOwnPlanVersionId(plan.planVersionId);
2710
+ return { status: "changed", planVersionId: plan.planVersionId };
2711
+ } catch (cause) {
2712
+ const status = cause.status ?? null;
2713
+ if (status === null || !CHECKOUT_STATUSES.has(status)) {
2714
+ const reason = cause instanceof Error ? cause.message : "Could not change plan.";
2715
+ setLastError(reason);
2716
+ return { status: "failed", reason };
2717
+ }
2718
+ try {
2719
+ const session = await client.billing.checkout(token, plan.planVersionId);
2720
+ go(session.url);
2721
+ return { status: "checkout", url: session.url };
2722
+ } catch (checkoutCause) {
2723
+ const reason = checkoutCause instanceof Error ? checkoutCause.message : "Could not start checkout.";
2724
+ setLastError(reason);
2725
+ return { status: "failed", reason };
2726
+ }
2727
+ } finally {
2728
+ setBusy(null);
2729
+ }
2730
+ },
2731
+ [client, token, options.navigate]
2732
+ );
2733
+ return {
2734
+ plans,
2735
+ loading: plans === null && !error,
2736
+ error,
2737
+ currentPlanVersionId: discover ? ownPlanVersionId : supplied ?? null,
2738
+ busyPlanVersionId,
2739
+ lastError,
2740
+ signedIn: token !== null,
2741
+ selectPlan,
2742
+ reload
2743
+ };
2744
+ }
2745
+
2746
+ // src/PricingTable.tsx
2747
+ import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
2748
+ function formatAmount(amount, currency) {
2749
+ if (amount === null) return "Free";
2750
+ const value = Number(amount);
2751
+ if (!Number.isFinite(value)) return "Free";
2752
+ try {
2753
+ return new Intl.NumberFormat(void 0, { style: "currency", currency }).format(value);
2754
+ } catch {
2755
+ return `${value} ${currency}`;
2756
+ }
2757
+ }
2758
+ function cadence(plan, amount) {
2759
+ if (amount === null) {
2760
+ return "\xA0";
2761
+ }
2762
+ const unit = plan.interval.toLowerCase();
2763
+ return plan.intervalCount === 1 ? `per ${unit}` : `every ${plan.intervalCount} ${unit}s`;
2764
+ }
2765
+ function PricingTable({
2766
+ onSelectPlan,
2767
+ currentPlanVersionId,
2768
+ featuredPlanKey,
2769
+ signInUrl,
2770
+ className,
2771
+ style
2772
+ }) {
2773
+ const {
2774
+ plans,
2775
+ error,
2776
+ currentPlanVersionId: current,
2777
+ busyPlanVersionId: busy,
2778
+ lastError,
2779
+ signedIn,
2780
+ selectPlan
2781
+ } = usePlans({ currentPlanVersionId });
2782
+ async function choose(plan) {
2783
+ if (onSelectPlan) {
2784
+ await onSelectPlan(plan.planVersionId, plan);
2785
+ return;
2786
+ }
2787
+ const outcome = await selectPlan(plan);
2788
+ if (outcome.status === "needs-sign-in" && signInUrl && typeof window !== "undefined") {
2789
+ window.location.assign(signInUrl);
2790
+ }
2791
+ }
2792
+ if (error) {
2793
+ return /* @__PURE__ */ jsx19("div", { className: cls("b316-card", className), style, children: "Plans are unavailable right now." });
2794
+ }
2795
+ if (!plans) {
2796
+ return /* @__PURE__ */ jsx19("div", { className: cls("b316-card", className), style, children: "Loading plans\u2026" });
2797
+ }
2798
+ if (plans.length === 0) {
2799
+ return /* @__PURE__ */ jsx19("div", { className: cls("b316-card", className), style, children: "No plans are published yet." });
2800
+ }
2801
+ const anyTrial = plans.some((plan) => Boolean(plan.trialDays));
2802
+ const anyDescription = plans.some((plan) => Boolean(plan.description));
1065
2803
  const anyFeatured = plans.some((plan) => plan.key === featuredPlanKey);
1066
- return /* @__PURE__ */ jsx8("div", { className: cls("b316-plans", className), style, children: plans.map((plan) => {
1067
- const base = plan.prices.find((price) => price.featureId === null) ?? plan.prices[0];
1068
- const current = currentPlanVersionId === plan.planVersionId;
1069
- const featured = featuredPlanKey === plan.key;
1070
- return /* @__PURE__ */ jsxs7(
1071
- "div",
2804
+ return /* @__PURE__ */ jsxs18("div", { className: cls("b316-plans", className), style, children: [
2805
+ lastError ? /* @__PURE__ */ jsx19("div", { className: "b316-plans-error", role: "alert", children: lastError }) : null,
2806
+ plans.map((plan) => {
2807
+ const base = plan.prices.find((price) => price.featureId === null) ?? plan.prices[0];
2808
+ const isCurrent = current === plan.planVersionId;
2809
+ const featured = featuredPlanKey === plan.key;
2810
+ return /* @__PURE__ */ jsxs18(
2811
+ "div",
2812
+ {
2813
+ className: featured ? "b316-card b316-plan b316-plan-featured" : "b316-card b316-plan",
2814
+ children: [
2815
+ /* @__PURE__ */ jsxs18("div", { className: "b316-plan-head", children: [
2816
+ anyFeatured ? /* @__PURE__ */ jsx19("div", { className: featured ? "b316-plan-badge" : "b316-plan-badge-spacer", children: featured ? "Recommended" : "\xA0" }) : null,
2817
+ /* @__PURE__ */ jsx19("div", { className: "b316-plan-name", children: plan.name }),
2818
+ anyDescription ? /* @__PURE__ */ jsx19("div", { className: "b316-plan-desc", children: plan.description ?? "\xA0" }) : null
2819
+ ] }),
2820
+ /* @__PURE__ */ jsxs18("div", { className: "b316-plan-pricing", children: [
2821
+ /* @__PURE__ */ jsx19("div", { className: "b316-plan-price", children: formatAmount(base?.amount ?? null, plan.currency) }),
2822
+ /* @__PURE__ */ jsx19("div", { className: "b316-plan-cadence", children: cadence(plan, base?.amount ?? null) }),
2823
+ anyTrial ? /* @__PURE__ */ jsx19("div", { className: "b316-plan-trial", children: plan.trialDays ? `${plan.trialDays}-day free trial` : "\xA0" }) : null
2824
+ ] }),
2825
+ /* @__PURE__ */ jsx19("ul", { className: "b316-plan-features", children: plan.entitlements.filter((entitlement) => entitlement.enabled).map((entitlement) => /* @__PURE__ */ jsxs18("li", { className: "b316-plan-feature", children: [
2826
+ /* @__PURE__ */ jsx19("span", { children: entitlement.featureName }),
2827
+ /* @__PURE__ */ jsx19("span", { className: "b316-plan-feature-value", children: entitlement.limit === null ? "Unlimited" : entitlement.limit })
2828
+ ] }, entitlement.featureId)) }),
2829
+ /* @__PURE__ */ jsx19("div", { className: "b316-plan-action", children: /* @__PURE__ */ jsx19(
2830
+ "button",
2831
+ {
2832
+ type: "button",
2833
+ className: isCurrent ? "b316-button b316-button-current" : "b316-button",
2834
+ disabled: isCurrent || busy !== null || !signedIn && !signInUrl && !onSelectPlan,
2835
+ onClick: () => {
2836
+ void choose(plan);
2837
+ },
2838
+ children: isCurrent ? "Current plan" : busy === plan.planVersionId ? "Working\u2026" : !signedIn && !signInUrl && !onSelectPlan ? "Sign in to choose" : "Choose"
2839
+ }
2840
+ ) })
2841
+ ]
2842
+ },
2843
+ plan.id
2844
+ );
2845
+ })
2846
+ ] });
2847
+ }
2848
+
2849
+ // src/DataTable.tsx
2850
+ import { specFor as specFor3 } from "@base316/sdk";
2851
+ import { useState as useState22 } from "react";
2852
+
2853
+ // src/fields.tsx
2854
+ import {
2855
+ formatAddress,
2856
+ specFor
2857
+ } from "@base316/sdk";
2858
+ import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
2859
+ function formatNumber(value, fraction = 0) {
2860
+ const number = Number(value);
2861
+ return Number.isFinite(number) ? new Intl.NumberFormat(void 0, {
2862
+ minimumFractionDigits: fraction,
2863
+ maximumFractionDigits: 2
2864
+ }).format(number) : "";
2865
+ }
2866
+ function formatDuration(value) {
2867
+ const total = Number(value);
2868
+ if (!Number.isFinite(total)) {
2869
+ return "";
2870
+ }
2871
+ const hours = Math.floor(total / 3600);
2872
+ const minutes = Math.round(total % 3600 / 60);
2873
+ if (hours === 0) {
2874
+ return `${minutes}m`;
2875
+ }
2876
+ return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`;
2877
+ }
2878
+ function FieldCell({ value, field, related }) {
2879
+ if (field.type === "RELATION") {
2880
+ return related?.title ?? (related?.id ? /* @__PURE__ */ jsx20("code", { className: "b316-muted", children: related.id }) : "\u2014");
2881
+ }
2882
+ if (value === null || value === void 0 || value === "") {
2883
+ return /* @__PURE__ */ jsx20("span", { className: "b316-muted", children: "\u2014" });
2884
+ }
2885
+ switch (field.type) {
2886
+ case "BOOLEAN":
2887
+ return value ? "Yes" : "No";
2888
+ case "MONEY":
2889
+ return formatNumber(value, 2);
2890
+ case "PERCENT":
2891
+ return `${formatNumber(value)}%`;
2892
+ case "DURATION":
2893
+ return formatDuration(value);
2894
+ case "RATING": {
2895
+ const stars = Math.round(Number(value));
2896
+ return /* @__PURE__ */ jsxs19("span", { "aria-label": `${stars} out of 5`, children: [
2897
+ "\u2605".repeat(stars),
2898
+ /* @__PURE__ */ jsx20("span", { className: "b316-muted", children: "\u2605".repeat(Math.max(0, 5 - stars)) })
2899
+ ] });
2900
+ }
2901
+ case "DATE":
2902
+ return String(value).split("T")[0];
2903
+ case "DATETIME":
2904
+ return new Date(String(value)).toLocaleString();
2905
+ case "SELECT":
2906
+ return /* @__PURE__ */ jsx20("span", { className: "b316-chip", children: String(value) });
2907
+ case "MULTISELECT":
2908
+ return /* @__PURE__ */ jsx20("span", { className: "b316-chips", children: (Array.isArray(value) ? value : [value]).map((item) => /* @__PURE__ */ jsx20("span", { className: "b316-chip", children: String(item) }, String(item))) });
2909
+ case "URL":
2910
+ return /* @__PURE__ */ jsx20("a", { href: String(value), target: "_blank", rel: "noreferrer noopener", children: String(value) });
2911
+ case "EMAIL":
2912
+ return /* @__PURE__ */ jsx20("a", { href: `mailto:${String(value)}`, children: String(value) });
2913
+ case "PHONE":
2914
+ return /* @__PURE__ */ jsx20("a", { href: `tel:${String(value)}`, children: String(value) });
2915
+ case "ADDRESS":
2916
+ return formatAddress(value);
2917
+ case "ATTACHMENT": {
2918
+ const files = Array.isArray(value) ? value : [value];
2919
+ return `${files.length} file${files.length === 1 ? "" : "s"}`;
2920
+ }
2921
+ case "LONGTEXT": {
2922
+ const text = String(value);
2923
+ return text.length > 80 ? `${text.slice(0, 80)}\u2026` : text;
2924
+ }
2925
+ case "JSON":
2926
+ return /* @__PURE__ */ jsx20("code", { className: "b316-muted", children: JSON.stringify(value).slice(0, 60) });
2927
+ default:
2928
+ return String(value);
2929
+ }
2930
+ }
2931
+ function FieldInput({ value, field, onChange, error, disabled }) {
2932
+ if (specFor(field).computed) {
2933
+ return null;
2934
+ }
2935
+ const id = `b316-field-${field.key}`;
2936
+ const described = error ? `${id}-error` : void 0;
2937
+ const common = {
2938
+ id,
2939
+ disabled,
2940
+ "aria-invalid": error ? true : void 0,
2941
+ "aria-describedby": described,
2942
+ className: error ? "b316-input b316-input-invalid" : "b316-input"
2943
+ };
2944
+ function control() {
2945
+ switch (field.type) {
2946
+ case "LONGTEXT":
2947
+ return /* @__PURE__ */ jsx20(
2948
+ "textarea",
2949
+ {
2950
+ ...common,
2951
+ rows: 4,
2952
+ value: value === null || value === void 0 ? "" : String(value),
2953
+ onChange: (event) => onChange(event.target.value)
2954
+ }
2955
+ );
2956
+ case "BOOLEAN":
2957
+ return /* @__PURE__ */ jsx20(
2958
+ "input",
2959
+ {
2960
+ ...common,
2961
+ type: "checkbox",
2962
+ className: "b316-checkbox",
2963
+ checked: Boolean(value),
2964
+ onChange: (event) => onChange(event.target.checked)
2965
+ }
2966
+ );
2967
+ case "SELECT":
2968
+ return /* @__PURE__ */ jsxs19(
2969
+ "select",
2970
+ {
2971
+ ...common,
2972
+ value: value === null || value === void 0 ? "" : String(value),
2973
+ onChange: (event) => onChange(event.target.value === "" ? null : event.target.value),
2974
+ children: [
2975
+ field.required ? null : /* @__PURE__ */ jsx20("option", { value: "", children: "\u2014" }),
2976
+ (field.options ?? []).map((option) => /* @__PURE__ */ jsx20("option", { value: option, children: option }, option))
2977
+ ]
2978
+ }
2979
+ );
2980
+ case "MULTISELECT": {
2981
+ const selected = new Set(Array.isArray(value) ? value.map(String) : []);
2982
+ return /* @__PURE__ */ jsx20("div", { className: "b316-choices", children: (field.options ?? []).map((option) => /* @__PURE__ */ jsxs19("label", { className: "b316-choice", children: [
2983
+ /* @__PURE__ */ jsx20(
2984
+ "input",
2985
+ {
2986
+ type: "checkbox",
2987
+ disabled,
2988
+ checked: selected.has(option),
2989
+ onChange: (event) => {
2990
+ const next = new Set(selected);
2991
+ if (event.target.checked) {
2992
+ next.add(option);
2993
+ } else {
2994
+ next.delete(option);
2995
+ }
2996
+ onChange([...next]);
2997
+ }
2998
+ }
2999
+ ),
3000
+ option
3001
+ ] }, option)) });
3002
+ }
3003
+ case "NUMBER":
3004
+ case "MONEY":
3005
+ case "PERCENT":
3006
+ return /* @__PURE__ */ jsx20(
3007
+ "input",
3008
+ {
3009
+ ...common,
3010
+ type: "number",
3011
+ step: field.type === "NUMBER" ? 1 : 0.01,
3012
+ value: value === null || value === void 0 ? "" : String(value),
3013
+ onChange: (event) => onChange(event.target.value === "" ? null : Number(event.target.value))
3014
+ }
3015
+ );
3016
+ case "RATING":
3017
+ return /* @__PURE__ */ jsx20(
3018
+ "input",
3019
+ {
3020
+ ...common,
3021
+ type: "number",
3022
+ min: 0,
3023
+ max: 5,
3024
+ step: 1,
3025
+ value: value === null || value === void 0 ? "" : String(value),
3026
+ onChange: (event) => onChange(event.target.value === "" ? null : Number(event.target.value))
3027
+ }
3028
+ );
3029
+ case "DURATION": {
3030
+ const minutes = value === null || value === void 0 ? "" : String(Number(value) / 60);
3031
+ return /* @__PURE__ */ jsxs19("div", { className: "b316-suffixed", children: [
3032
+ /* @__PURE__ */ jsx20(
3033
+ "input",
3034
+ {
3035
+ ...common,
3036
+ type: "number",
3037
+ min: 0,
3038
+ value: minutes,
3039
+ onChange: (event) => onChange(event.target.value === "" ? null : Number(event.target.value) * 60)
3040
+ }
3041
+ ),
3042
+ /* @__PURE__ */ jsx20("span", { className: "b316-muted", children: "minutes" })
3043
+ ] });
3044
+ }
3045
+ case "DATE":
3046
+ return /* @__PURE__ */ jsx20(
3047
+ "input",
3048
+ {
3049
+ ...common,
3050
+ type: "date",
3051
+ value: value === null || value === void 0 ? "" : String(value).slice(0, 10),
3052
+ onChange: (event) => onChange(event.target.value === "" ? null : event.target.value)
3053
+ }
3054
+ );
3055
+ case "DATETIME":
3056
+ return /* @__PURE__ */ jsx20(
3057
+ "input",
3058
+ {
3059
+ ...common,
3060
+ type: "datetime-local",
3061
+ value: value === null || value === void 0 ? "" : String(value).slice(0, 16),
3062
+ onChange: (event) => onChange(
3063
+ event.target.value === "" ? null : new Date(event.target.value).toISOString()
3064
+ )
3065
+ }
3066
+ );
3067
+ case "ADDRESS": {
3068
+ const address = value ?? {};
3069
+ const part = (key, label) => /* @__PURE__ */ jsx20(
3070
+ "input",
3071
+ {
3072
+ className: "b316-input",
3073
+ disabled,
3074
+ placeholder: label,
3075
+ "aria-label": label,
3076
+ value: address[key] ?? "",
3077
+ onChange: (event) => onChange({ ...address, [key]: event.target.value })
3078
+ },
3079
+ key
3080
+ );
3081
+ return /* @__PURE__ */ jsxs19("div", { className: "b316-address", children: [
3082
+ part("line1", "Address line 1"),
3083
+ part("line2", "Address line 2"),
3084
+ part("city", "City"),
3085
+ part("region", "Region"),
3086
+ part("postalCode", "Postal code"),
3087
+ part("country", "Country")
3088
+ ] });
3089
+ }
3090
+ case "EMAIL":
3091
+ return /* @__PURE__ */ jsx20(
3092
+ "input",
3093
+ {
3094
+ ...common,
3095
+ type: "email",
3096
+ value: value === null || value === void 0 ? "" : String(value),
3097
+ onChange: (event) => onChange(event.target.value)
3098
+ }
3099
+ );
3100
+ case "URL":
3101
+ return /* @__PURE__ */ jsx20(
3102
+ "input",
3103
+ {
3104
+ ...common,
3105
+ type: "url",
3106
+ value: value === null || value === void 0 ? "" : String(value),
3107
+ onChange: (event) => onChange(event.target.value)
3108
+ }
3109
+ );
3110
+ case "PHONE":
3111
+ return /* @__PURE__ */ jsx20(
3112
+ "input",
3113
+ {
3114
+ ...common,
3115
+ type: "tel",
3116
+ value: value === null || value === void 0 ? "" : String(value),
3117
+ onChange: (event) => onChange(event.target.value)
3118
+ }
3119
+ );
3120
+ default:
3121
+ return /* @__PURE__ */ jsx20(
3122
+ "input",
3123
+ {
3124
+ ...common,
3125
+ type: "text",
3126
+ value: value === null || value === void 0 ? "" : String(value),
3127
+ onChange: (event) => onChange(event.target.value)
3128
+ }
3129
+ );
3130
+ }
3131
+ }
3132
+ return /* @__PURE__ */ jsxs19("div", { className: "b316-field", children: [
3133
+ /* @__PURE__ */ jsxs19("label", { className: "b316-label", htmlFor: id, children: [
3134
+ field.label,
3135
+ field.required ? /* @__PURE__ */ jsx20("span", { "aria-hidden": "true", children: " *" }) : null
3136
+ ] }),
3137
+ control(),
3138
+ field.description ? /* @__PURE__ */ jsx20("div", { className: "b316-hint", children: field.description }) : null,
3139
+ error ? /* @__PURE__ */ jsx20("div", { className: "b316-error", id: described, role: "alert", children: error }) : null
3140
+ ] });
3141
+ }
3142
+
3143
+ // src/RecordForm.tsx
3144
+ import { specFor as specFor2 } from "@base316/sdk";
3145
+ import { useEffect as useEffect16, useState as useState21 } from "react";
3146
+
3147
+ // src/RelationPicker.tsx
3148
+ import { useEffect as useEffect14, useRef as useRef6, useState as useState19 } from "react";
3149
+ import { Fragment as Fragment5, jsx as jsx21, jsxs as jsxs20 } from "react/jsx-runtime";
3150
+ function RelationPicker({
3151
+ field,
3152
+ value,
3153
+ onChange,
3154
+ error,
3155
+ disabled,
3156
+ tenant
3157
+ }) {
3158
+ const { client, token } = useBase316();
3159
+ const [query, setQuery] = useState19("");
3160
+ const [results, setResults] = useState19([]);
3161
+ const [chosenTitle, setChosenTitle] = useState19(null);
3162
+ const [open, setOpen] = useState19(false);
3163
+ const id = `b316-field-${field.key}`;
3164
+ const box = useRef6(null);
3165
+ useEffect14(() => {
3166
+ if (!token || !value || !field.relationTo) {
3167
+ setChosenTitle(null);
3168
+ return;
3169
+ }
3170
+ let cancelled = false;
3171
+ client.collections.titles(token, field.relationTo, [value], tenant ? { tenant } : void 0).then((result) => {
3172
+ if (!cancelled) {
3173
+ setChosenTitle(result.titles[value] ?? null);
3174
+ }
3175
+ }).catch(() => null);
3176
+ return () => {
3177
+ cancelled = true;
3178
+ };
3179
+ }, [client, token, value, field.relationTo, tenant]);
3180
+ useEffect14(() => {
3181
+ if (!token || !field.relationTo || !open) {
3182
+ return;
3183
+ }
3184
+ let cancelled = false;
3185
+ const timer = setTimeout(() => {
3186
+ client.collections.list(
3187
+ token,
3188
+ field.relationTo,
3189
+ { search: query || void 0, limit: 10 },
3190
+ tenant ? { tenant } : void 0
3191
+ ).then((page) => {
3192
+ if (!cancelled) {
3193
+ setResults(page.data.map((record) => ({ id: record.id, title: record.title })));
3194
+ }
3195
+ }).catch(() => {
3196
+ if (!cancelled) {
3197
+ setResults([]);
3198
+ }
3199
+ });
3200
+ }, 200);
3201
+ return () => {
3202
+ cancelled = true;
3203
+ clearTimeout(timer);
3204
+ };
3205
+ }, [client, token, field.relationTo, query, open, tenant]);
3206
+ useEffect14(() => {
3207
+ if (!open || typeof document === "undefined") {
3208
+ return;
3209
+ }
3210
+ const onDocumentClick = (event) => {
3211
+ if (box.current && !box.current.contains(event.target)) {
3212
+ setOpen(false);
3213
+ }
3214
+ };
3215
+ document.addEventListener("mousedown", onDocumentClick);
3216
+ return () => {
3217
+ document.removeEventListener("mousedown", onDocumentClick);
3218
+ };
3219
+ }, [open]);
3220
+ return /* @__PURE__ */ jsxs20("div", { className: "b316-field", ref: box, children: [
3221
+ /* @__PURE__ */ jsxs20("label", { className: "b316-label", htmlFor: id, children: [
3222
+ field.label,
3223
+ field.required ? /* @__PURE__ */ jsx21("span", { "aria-hidden": "true", children: " *" }) : null
3224
+ ] }),
3225
+ value && !open ? /* @__PURE__ */ jsxs20("div", { className: "b316-relation-chosen", children: [
3226
+ /* @__PURE__ */ jsx21("span", { children: chosenTitle ?? value }),
3227
+ disabled ? null : /* @__PURE__ */ jsx21(
3228
+ "button",
3229
+ {
3230
+ type: "button",
3231
+ className: "b316-button-quiet",
3232
+ onClick: () => {
3233
+ setQuery("");
3234
+ setOpen(true);
3235
+ },
3236
+ children: "Change"
3237
+ }
3238
+ ),
3239
+ disabled || field.required ? null : /* @__PURE__ */ jsx21(
3240
+ "button",
3241
+ {
3242
+ type: "button",
3243
+ className: "b316-button-quiet",
3244
+ onClick: () => onChange(null),
3245
+ "aria-label": `Clear ${field.label}`,
3246
+ children: "Clear"
3247
+ }
3248
+ )
3249
+ ] }) : /* @__PURE__ */ jsxs20(Fragment5, { children: [
3250
+ /* @__PURE__ */ jsx21(
3251
+ "input",
3252
+ {
3253
+ id,
3254
+ type: "text",
3255
+ role: "combobox",
3256
+ "aria-expanded": open,
3257
+ "aria-controls": `${id}-results`,
3258
+ "aria-autocomplete": "list",
3259
+ autoComplete: "off",
3260
+ disabled,
3261
+ className: error ? "b316-input b316-input-invalid" : "b316-input",
3262
+ placeholder: `Search ${field.label.toLowerCase()}`,
3263
+ value: query,
3264
+ onFocus: () => setOpen(true),
3265
+ onChange: (event) => {
3266
+ setQuery(event.target.value);
3267
+ setOpen(true);
3268
+ }
3269
+ }
3270
+ ),
3271
+ open ? /* @__PURE__ */ jsx21("ul", { className: "b316-relation-results", id: `${id}-results`, role: "listbox", children: results.length === 0 ? /* @__PURE__ */ jsx21("li", { className: "b316-muted b316-relation-empty", children: "No matches" }) : results.map((result) => /* @__PURE__ */ jsx21("li", { children: /* @__PURE__ */ jsx21(
3272
+ "button",
3273
+ {
3274
+ type: "button",
3275
+ role: "option",
3276
+ "aria-selected": result.id === value,
3277
+ className: "b316-relation-result",
3278
+ onClick: () => {
3279
+ onChange(result.id);
3280
+ setChosenTitle(result.title);
3281
+ setOpen(false);
3282
+ },
3283
+ children: result.title ?? result.id
3284
+ }
3285
+ ) }, result.id)) }) : null
3286
+ ] }),
3287
+ field.description ? /* @__PURE__ */ jsx21("div", { className: "b316-hint", children: field.description }) : null,
3288
+ error ? /* @__PURE__ */ jsx21("div", { className: "b316-error", role: "alert", children: error }) : null
3289
+ ] });
3290
+ }
3291
+
3292
+ // src/useCollection.ts
3293
+ import { useCallback as useCallback7, useEffect as useEffect15, useMemo as useMemo2, useState as useState20 } from "react";
3294
+ function fieldErrorFrom(cause) {
3295
+ const param = cause?.param;
3296
+ const message = cause instanceof Error ? cause.message : "That value was not accepted.";
3297
+ return param ? { [param]: message } : { _: message };
3298
+ }
3299
+ function useCollection(collection, options = {}) {
3300
+ const { client, token } = useBase316();
3301
+ const [schema, setSchema] = useState20(null);
3302
+ const [records, setRecords] = useState20([]);
3303
+ const [error, setError] = useState20(null);
3304
+ const [loading, setLoading] = useState20(true);
3305
+ const [total, setTotal] = useState20(0);
3306
+ const [cursor, setCursor] = useState20(null);
3307
+ const [reloadCount, setReloadCount] = useState20(0);
3308
+ const [search, setSearch] = useState20("");
3309
+ const [filters, setFilters] = useState20([]);
3310
+ const [sort, setSort] = useState20(
3311
+ options.sort ?? null
3312
+ );
3313
+ const [fieldErrors, setFieldErrors] = useState20({});
3314
+ const [saving, setSaving] = useState20(false);
3315
+ const { tenant, limit, recordId } = options;
3316
+ const fixedFilters = JSON.stringify(options.filters ?? []);
3317
+ const scope = useMemo2(() => tenant ? { tenant } : void 0, [tenant]);
3318
+ useEffect15(() => {
3319
+ if (!token) {
3320
+ return;
3321
+ }
3322
+ let cancelled = false;
3323
+ client.collections.schema(token, collection, scope).then((result) => {
3324
+ if (!cancelled) {
3325
+ setSchema(result);
3326
+ }
3327
+ }).catch((cause) => {
3328
+ if (!cancelled) {
3329
+ setError(cause instanceof Error ? cause.message : "This collection is unavailable.");
3330
+ setLoading(false);
3331
+ }
3332
+ });
3333
+ return () => {
3334
+ cancelled = true;
3335
+ };
3336
+ }, [client, token, collection, scope]);
3337
+ useEffect15(() => {
3338
+ if (!token || !schema) {
3339
+ return;
3340
+ }
3341
+ let cancelled = false;
3342
+ setLoading(true);
3343
+ if (recordId) {
3344
+ client.collections.get(token, collection, recordId, scope).then((record) => {
3345
+ if (cancelled) {
3346
+ return;
3347
+ }
3348
+ setRecords([record]);
3349
+ setCursor(null);
3350
+ setTotal(1);
3351
+ setError(null);
3352
+ setLoading(false);
3353
+ }).catch((cause) => {
3354
+ if (!cancelled) {
3355
+ setRecords([]);
3356
+ setTotal(0);
3357
+ setError(cause instanceof Error ? cause.message : "That record is not available.");
3358
+ setLoading(false);
3359
+ }
3360
+ });
3361
+ return () => {
3362
+ cancelled = true;
3363
+ };
3364
+ }
3365
+ client.collections.list(
3366
+ token,
3367
+ collection,
1072
3368
  {
1073
- className: featured ? "b316-card b316-plan b316-plan-featured" : "b316-card b316-plan",
1074
- children: [
1075
- /* @__PURE__ */ jsxs7("div", { className: "b316-plan-head", children: [
1076
- anyFeatured ? /* @__PURE__ */ jsx8("div", { className: featured ? "b316-plan-badge" : "b316-plan-badge-spacer", children: featured ? "Recommended" : "\xA0" }) : null,
1077
- /* @__PURE__ */ jsx8("div", { className: "b316-plan-name", children: plan.name }),
1078
- anyDescription ? /* @__PURE__ */ jsx8("div", { className: "b316-plan-desc", children: plan.description ?? "\xA0" }) : null
1079
- ] }),
1080
- /* @__PURE__ */ jsxs7("div", { className: "b316-plan-pricing", children: [
1081
- /* @__PURE__ */ jsx8("div", { className: "b316-plan-price", children: formatAmount(base?.amount ?? null, plan.currency) }),
1082
- /* @__PURE__ */ jsx8("div", { className: "b316-plan-cadence", children: cadence(plan, base?.amount ?? null) }),
1083
- anyTrial ? /* @__PURE__ */ jsx8("div", { className: "b316-plan-trial", children: plan.trialDays ? `${plan.trialDays}-day free trial` : "\xA0" }) : null
1084
- ] }),
1085
- /* @__PURE__ */ jsx8("ul", { className: "b316-plan-features", children: plan.entitlements.filter((entitlement) => entitlement.enabled).map((entitlement) => /* @__PURE__ */ jsxs7("li", { className: "b316-plan-feature", children: [
1086
- /* @__PURE__ */ jsx8("span", { children: entitlement.featureName }),
1087
- /* @__PURE__ */ jsx8("span", { className: "b316-plan-feature-value", children: entitlement.limit === null ? "Unlimited" : entitlement.limit })
1088
- ] }, entitlement.featureId)) }),
1089
- /* @__PURE__ */ jsx8("div", { className: "b316-plan-action", children: /* @__PURE__ */ jsx8(
3369
+ filters: [...JSON.parse(fixedFilters), ...filters],
3370
+ search: search || void 0,
3371
+ sort: sort ?? void 0,
3372
+ limit
3373
+ },
3374
+ scope
3375
+ ).then((page) => {
3376
+ if (cancelled) {
3377
+ return;
3378
+ }
3379
+ setRecords(page.data);
3380
+ setCursor(page.cursor);
3381
+ setTotal(page.total);
3382
+ setError(null);
3383
+ setLoading(false);
3384
+ }).catch((cause) => {
3385
+ if (!cancelled) {
3386
+ setError(cause instanceof Error ? cause.message : "Could not load these records.");
3387
+ setLoading(false);
3388
+ }
3389
+ });
3390
+ return () => {
3391
+ cancelled = true;
3392
+ };
3393
+ }, [
3394
+ client,
3395
+ token,
3396
+ collection,
3397
+ schema,
3398
+ recordId,
3399
+ fixedFilters,
3400
+ filters,
3401
+ search,
3402
+ sort,
3403
+ limit,
3404
+ scope,
3405
+ reloadCount
3406
+ ]);
3407
+ const reload = useCallback7(() => {
3408
+ setReloadCount((count) => count + 1);
3409
+ }, []);
3410
+ const loadMore = useCallback7(() => {
3411
+ if (!token || !cursor) {
3412
+ return;
3413
+ }
3414
+ setLoading(true);
3415
+ client.collections.list(
3416
+ token,
3417
+ collection,
3418
+ {
3419
+ filters: [...JSON.parse(fixedFilters), ...filters],
3420
+ search: search || void 0,
3421
+ sort: sort ?? void 0,
3422
+ limit,
3423
+ cursor
3424
+ },
3425
+ scope
3426
+ ).then((page) => {
3427
+ setRecords((existing) => [...existing, ...page.data]);
3428
+ setCursor(page.cursor);
3429
+ setLoading(false);
3430
+ }).catch(() => {
3431
+ setLoading(false);
3432
+ });
3433
+ }, [client, token, collection, cursor, fixedFilters, filters, search, sort, limit, scope]);
3434
+ const toggleSort = useCallback7((field) => {
3435
+ setSort((current) => {
3436
+ if (current?.field !== field) {
3437
+ return { field, direction: "asc" };
3438
+ }
3439
+ return current.direction === "asc" ? { field, direction: "desc" } : null;
3440
+ });
3441
+ }, []);
3442
+ const create = useCallback7(
3443
+ async (fields) => {
3444
+ if (!token) {
3445
+ return null;
3446
+ }
3447
+ setSaving(true);
3448
+ setFieldErrors({});
3449
+ try {
3450
+ const record = await client.collections.create(token, collection, fields, scope);
3451
+ reload();
3452
+ return record;
3453
+ } catch (cause) {
3454
+ setFieldErrors(fieldErrorFrom(cause));
3455
+ return null;
3456
+ } finally {
3457
+ setSaving(false);
3458
+ }
3459
+ },
3460
+ [client, token, collection, scope, reload]
3461
+ );
3462
+ const update = useCallback7(
3463
+ async (id, fields) => {
3464
+ if (!token) {
3465
+ return null;
3466
+ }
3467
+ setSaving(true);
3468
+ setFieldErrors({});
3469
+ try {
3470
+ const record = await client.collections.update(token, collection, id, fields, scope);
3471
+ setRecords(
3472
+ (existing) => existing.map((candidate) => candidate.id === id ? record : candidate)
3473
+ );
3474
+ return record;
3475
+ } catch (cause) {
3476
+ setFieldErrors(fieldErrorFrom(cause));
3477
+ return null;
3478
+ } finally {
3479
+ setSaving(false);
3480
+ }
3481
+ },
3482
+ [client, token, collection, scope]
3483
+ );
3484
+ const remove = useCallback7(
3485
+ async (id) => {
3486
+ if (!token) {
3487
+ return false;
3488
+ }
3489
+ setSaving(true);
3490
+ try {
3491
+ await client.collections.remove(token, collection, id, scope);
3492
+ setRecords((existing) => existing.filter((candidate) => candidate.id !== id));
3493
+ setTotal((count) => Math.max(0, count - 1));
3494
+ return true;
3495
+ } catch (cause) {
3496
+ setFieldErrors(fieldErrorFrom(cause));
3497
+ return false;
3498
+ } finally {
3499
+ setSaving(false);
3500
+ }
3501
+ },
3502
+ [client, token, collection, scope]
3503
+ );
3504
+ return {
3505
+ schema,
3506
+ records,
3507
+ loading,
3508
+ error,
3509
+ total,
3510
+ hasMore: cursor !== null,
3511
+ canWrite: schema?.canWrite ?? false,
3512
+ search,
3513
+ setSearch,
3514
+ filters,
3515
+ setFilters,
3516
+ sort,
3517
+ toggleSort,
3518
+ loadMore,
3519
+ reload,
3520
+ fieldErrors,
3521
+ saving,
3522
+ create,
3523
+ update,
3524
+ remove
3525
+ };
3526
+ }
3527
+
3528
+ // src/RecordForm.tsx
3529
+ import { jsx as jsx22, jsxs as jsxs21 } from "react/jsx-runtime";
3530
+ function RecordForm({
3531
+ collection,
3532
+ recordId,
3533
+ tenant,
3534
+ onSaved,
3535
+ onCancel,
3536
+ defaults,
3537
+ className,
3538
+ style
3539
+ }) {
3540
+ const { schema, records, create, update, saving, fieldErrors, error } = useCollection(
3541
+ collection,
3542
+ {
3543
+ tenant,
3544
+ // Fetched by id, not filtered for: `id` is not a schema field, so a filter
3545
+ // naming it is dropped and the form would quietly load the first page and
3546
+ // hope the record was on it.
3547
+ recordId
3548
+ }
3549
+ );
3550
+ const [values, setValues] = useState21(defaults ?? {});
3551
+ const [loaded, setLoaded] = useState21(!recordId);
3552
+ const existing = recordId ? records.find((record) => record.id === recordId) : void 0;
3553
+ useEffect16(() => {
3554
+ if (existing && !loaded) {
3555
+ setValues(existing.fields);
3556
+ setLoaded(true);
3557
+ }
3558
+ }, [existing, loaded]);
3559
+ if (error) {
3560
+ return /* @__PURE__ */ jsx22("div", { className: cls("b316-card", className), style, children: /* @__PURE__ */ jsx22("div", { className: "b316-error", role: "alert", children: error }) });
3561
+ }
3562
+ if (!schema || !loaded) {
3563
+ return /* @__PURE__ */ jsx22("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
3564
+ }
3565
+ const editable = schema.fields.filter((field) => !specFor2(field).computed);
3566
+ async function submit(event) {
3567
+ event.preventDefault();
3568
+ const saved = recordId ? await update(recordId, values) : await create(values);
3569
+ if (saved) {
3570
+ onSaved?.(saved.id);
3571
+ }
3572
+ }
3573
+ return /* @__PURE__ */ jsxs21("form", { className: cls("b316-card b316-form", className), style, onSubmit: submit, children: [
3574
+ /* @__PURE__ */ jsx22("h3", { className: "b316-form-title", children: recordId ? `Edit ${schema.name.toLowerCase()}` : `New ${schema.name.toLowerCase()}` }),
3575
+ fieldErrors._ ? /* @__PURE__ */ jsx22("div", { className: "b316-error", role: "alert", children: fieldErrors._ }) : null,
3576
+ editable.map(
3577
+ (field) => field.type === "RELATION" ? /* @__PURE__ */ jsx22(
3578
+ RelationPicker,
3579
+ {
3580
+ field,
3581
+ tenant,
3582
+ disabled: saving,
3583
+ error: fieldErrors[field.key],
3584
+ value: values[field.key] ?? null,
3585
+ onChange: (next) => setValues((current) => ({ ...current, [field.key]: next }))
3586
+ },
3587
+ field.key
3588
+ ) : /* @__PURE__ */ jsx22(
3589
+ FieldInput,
3590
+ {
3591
+ field,
3592
+ disabled: saving,
3593
+ error: fieldErrors[field.key],
3594
+ value: values[field.key] ?? null,
3595
+ onChange: (next) => setValues((current) => ({ ...current, [field.key]: next }))
3596
+ },
3597
+ field.key
3598
+ )
3599
+ ),
3600
+ /* @__PURE__ */ jsxs21("div", { className: "b316-form-actions", children: [
3601
+ onCancel ? /* @__PURE__ */ jsx22("button", { type: "button", className: "b316-button-quiet", onClick: onCancel, disabled: saving, children: "Cancel" }) : null,
3602
+ /* @__PURE__ */ jsx22("button", { type: "submit", className: "b316-button", disabled: saving, children: saving ? "Saving\u2026" : "Save" })
3603
+ ] })
3604
+ ] });
3605
+ }
3606
+
3607
+ // src/DataTable.tsx
3608
+ import { jsx as jsx23, jsxs as jsxs22 } from "react/jsx-runtime";
3609
+ function DataTable({
3610
+ collection,
3611
+ columns,
3612
+ renderCell,
3613
+ onSelect,
3614
+ onChanged,
3615
+ hideSearch,
3616
+ readOnly,
3617
+ emptyState,
3618
+ className,
3619
+ style,
3620
+ ...options
3621
+ }) {
3622
+ const {
3623
+ schema,
3624
+ records,
3625
+ loading,
3626
+ error,
3627
+ total,
3628
+ hasMore,
3629
+ canWrite,
3630
+ search,
3631
+ setSearch,
3632
+ sort,
3633
+ toggleSort,
3634
+ loadMore,
3635
+ remove
3636
+ } = useCollection(collection, options);
3637
+ const [editing, setEditing] = useState22(null);
3638
+ const [removeFailed, setRemoveFailed] = useState22(null);
3639
+ if (error) {
3640
+ return /* @__PURE__ */ jsx23("div", { className: cls("b316-card", className), style, children: /* @__PURE__ */ jsx23("div", { className: "b316-error", role: "alert", children: error }) });
3641
+ }
3642
+ if (!schema) {
3643
+ return /* @__PURE__ */ jsx23("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
3644
+ }
3645
+ const shown = columns ? (
3646
+ // Ordered by what was asked for, not by the schema: a caller naming
3647
+ // columns means that order.
3648
+ columns.map((key) => schema.fields.find((field) => field.key === key)).filter((field) => Boolean(field))
3649
+ ) : schema.fields;
3650
+ const searchable = schema.fields.some((field) => specFor3(field).capabilities.searchable);
3651
+ const writable = canWrite && !readOnly;
3652
+ return /* @__PURE__ */ jsxs22("div", { className: cls("b316-table-wrap", className), style, children: [
3653
+ removeFailed ? /* @__PURE__ */ jsx23("div", { className: "b316-error", role: "alert", children: removeFailed }) : null,
3654
+ /* @__PURE__ */ jsxs22("div", { className: "b316-table-bar", children: [
3655
+ searchable && !hideSearch ? /* @__PURE__ */ jsx23(
3656
+ "input",
3657
+ {
3658
+ type: "search",
3659
+ className: "b316-input",
3660
+ placeholder: `Search ${schema.namePlural.toLowerCase()}`,
3661
+ "aria-label": `Search ${schema.namePlural}`,
3662
+ value: search,
3663
+ onChange: (event) => setSearch(event.target.value)
3664
+ }
3665
+ ) : /* @__PURE__ */ jsx23("span", {}),
3666
+ /* @__PURE__ */ jsxs22("div", { className: "b316-table-actions", children: [
3667
+ /* @__PURE__ */ jsxs22("span", { className: "b316-muted", children: [
3668
+ total,
3669
+ " ",
3670
+ total === 1 ? schema.name.toLowerCase() : schema.namePlural.toLowerCase()
3671
+ ] }),
3672
+ writable ? /* @__PURE__ */ jsxs22("button", { type: "button", className: "b316-button", onClick: () => setEditing("new"), children: [
3673
+ "New ",
3674
+ schema.name.toLowerCase()
3675
+ ] }) : null
3676
+ ] })
3677
+ ] }),
3678
+ /* @__PURE__ */ jsx23("div", { className: "b316-table-scroll", children: /* @__PURE__ */ jsxs22("table", { className: "b316-table", children: [
3679
+ /* @__PURE__ */ jsx23("thead", { children: /* @__PURE__ */ jsxs22("tr", { children: [
3680
+ shown.map((field) => {
3681
+ const sortable = specFor3(field).capabilities.sortable;
3682
+ const active = sort?.field === field.key;
3683
+ return /* @__PURE__ */ jsx23("th", { scope: "col", "aria-sort": ariaSort(active, sort?.direction), children: sortable ? /* @__PURE__ */ jsxs22(
1090
3684
  "button",
1091
3685
  {
1092
3686
  type: "button",
1093
- className: current ? "b316-button b316-button-current" : "b316-button",
1094
- disabled: current || busy !== null,
1095
- onClick: async () => {
1096
- setBusy(plan.planVersionId);
1097
- try {
1098
- await onSelectPlan(plan.planVersionId, plan);
1099
- } finally {
1100
- setBusy(null);
3687
+ className: "b316-sort",
3688
+ onClick: () => toggleSort(field.key),
3689
+ children: [
3690
+ field.label,
3691
+ /* @__PURE__ */ jsx23("span", { "aria-hidden": "true", children: active ? sort?.direction === "desc" ? " \u2193" : " \u2191" : " \u2195" })
3692
+ ]
3693
+ }
3694
+ ) : field.label }, field.key);
3695
+ }),
3696
+ writable ? /* @__PURE__ */ jsx23("th", { scope: "col", className: "b316-table-rowactions" }) : null
3697
+ ] }) }),
3698
+ /* @__PURE__ */ jsx23("tbody", { children: records.map((record) => /* @__PURE__ */ jsxs22(
3699
+ "tr",
3700
+ {
3701
+ className: onSelect ? "b316-row-clickable" : void 0,
3702
+ onClick: onSelect ? () => onSelect(record) : void 0,
3703
+ children: [
3704
+ shown.map((field) => /* @__PURE__ */ jsx23("td", { children: renderCell?.(record, field) ?? /* @__PURE__ */ jsx23(
3705
+ FieldCell,
3706
+ {
3707
+ value: record.fields[field.key] ?? null,
3708
+ field,
3709
+ related: record.related[field.key]
3710
+ }
3711
+ ) }, field.key)),
3712
+ writable ? /* @__PURE__ */ jsxs22("td", { className: "b316-table-rowactions", children: [
3713
+ /* @__PURE__ */ jsx23(
3714
+ "button",
3715
+ {
3716
+ type: "button",
3717
+ className: "b316-button-quiet",
3718
+ onClick: (event) => {
3719
+ event.stopPropagation();
3720
+ setEditing(record);
3721
+ },
3722
+ children: "Edit"
1101
3723
  }
1102
- },
1103
- children: current ? "Current plan" : busy === plan.planVersionId ? "Working\u2026" : "Choose"
3724
+ ),
3725
+ /* @__PURE__ */ jsx23(
3726
+ ConfirmButton,
3727
+ {
3728
+ className: "b316-button-quiet b316-danger",
3729
+ confirmLabel: `Delete ${record.title ?? "this record"} for good`,
3730
+ onConfirm: () => remove(record.id).then((ok) => {
3731
+ if (ok) {
3732
+ setRemoveFailed(null);
3733
+ onChanged?.();
3734
+ return;
3735
+ }
3736
+ setRemoveFailed(
3737
+ `${record.title ?? "That record"} could not be deleted. It is still here.`
3738
+ );
3739
+ }),
3740
+ children: "Delete"
3741
+ }
3742
+ )
3743
+ ] }) : null
3744
+ ]
3745
+ },
3746
+ record.id
3747
+ )) })
3748
+ ] }) }),
3749
+ records.length === 0 && !loading ? /* @__PURE__ */ jsx23("div", { className: "b316-empty", children: emptyState ?? `No ${schema.namePlural.toLowerCase()} yet.` }) : null,
3750
+ loading ? /* @__PURE__ */ jsx23("div", { className: "b316-empty", children: "Loading\u2026" }) : null,
3751
+ hasMore ? /* @__PURE__ */ jsx23("div", { className: "b316-table-more", children: /* @__PURE__ */ jsx23("button", { type: "button", className: "b316-button-quiet", onClick: loadMore, disabled: loading, children: "Load more" }) }) : null,
3752
+ editing ? /* @__PURE__ */ jsx23(
3753
+ RecordForm,
3754
+ {
3755
+ collection,
3756
+ recordId: editing === "new" ? void 0 : editing.id,
3757
+ tenant: options.tenant,
3758
+ onSaved: () => {
3759
+ setEditing(null);
3760
+ onChanged?.();
3761
+ },
3762
+ onCancel: () => setEditing(null)
3763
+ }
3764
+ ) : null
3765
+ ] });
3766
+ }
3767
+ function ariaSort(active, direction) {
3768
+ if (!active) {
3769
+ return void 0;
3770
+ }
3771
+ return direction === "desc" ? "descending" : "ascending";
3772
+ }
3773
+
3774
+ // src/Kanban.tsx
3775
+ import {
3776
+ defaultGroupFieldOf,
3777
+ specFor as specFor4,
3778
+ titleFieldOf
3779
+ } from "@base316/sdk";
3780
+ import { useState as useState23 } from "react";
3781
+ import { jsx as jsx24, jsxs as jsxs23 } from "react/jsx-runtime";
3782
+ function Kanban({
3783
+ collection,
3784
+ groupBy,
3785
+ cardFields,
3786
+ onSelect,
3787
+ emptyState,
3788
+ className,
3789
+ style,
3790
+ ...options
3791
+ }) {
3792
+ const { schema, records, loading, error, canWrite, update, total, hasMore, loadMore } = useCollection(collection, {
3793
+ // A board shows everything at once by its nature — a column holding the
3794
+ // first twenty-five of two hundred is misleading rather than partial.
3795
+ limit: options.limit ?? 100,
3796
+ ...options
3797
+ });
3798
+ const [dragging, setDragging] = useState23(null);
3799
+ const [over, setOver] = useState23(null);
3800
+ if (error) {
3801
+ return /* @__PURE__ */ jsx24("div", { className: cls("b316-card", className), style, children: /* @__PURE__ */ jsx24("div", { className: "b316-error", role: "alert", children: error }) });
3802
+ }
3803
+ if (!schema) {
3804
+ return /* @__PURE__ */ jsx24("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
3805
+ }
3806
+ const field = groupBy ? schema.fields.find((candidate) => candidate.key === groupBy) ?? null : defaultGroupFieldOf(schema.fields);
3807
+ if (!field || !specFor4(field).capabilities.groupable) {
3808
+ return /* @__PURE__ */ jsx24("div", { className: cls("b316-card", className), style, children: groupBy ? `"${groupBy}" cannot make columns. A board groups by a choice, a yes/no or a date.` : `${schema.namePlural} have no field to group by. Add a choice field \u2014 a status or a stage \u2014 and this board will use it.` });
3809
+ }
3810
+ const columns = columnsFor(field);
3811
+ const shown = cardFields ? cardFields.map((key) => schema.fields.find((candidate) => candidate.key === key)).filter((candidate) => Boolean(candidate)) : (
3812
+ // The first few fields that are neither the grouping one nor the title.
3813
+ // The grouping field is already the column heading and the title is
3814
+ // already the card heading, so both would be printed twice on every card.
3815
+ schema.fields.filter(
3816
+ (candidate) => candidate.key !== field.key && candidate.key !== titleFieldOf(schema.fields)?.key
3817
+ ).slice(0, 3)
3818
+ );
3819
+ async function move(record, value) {
3820
+ if (!canWrite || record.fields[field.key] === value) {
3821
+ return;
3822
+ }
3823
+ await update(record.id, { [field.key]: value });
3824
+ }
3825
+ return /* @__PURE__ */ jsxs23("div", { className: cls("b316-board-wrap", className), style, children: [
3826
+ /* @__PURE__ */ jsx24("div", { className: "b316-board", role: "list", children: columns.map((column) => {
3827
+ const inColumn = records.filter(
3828
+ (record) => String(record.fields[field.key] ?? "") === String(column.value ?? "")
3829
+ );
3830
+ return /* @__PURE__ */ jsxs23(
3831
+ "section",
3832
+ {
3833
+ className: over === String(column.value) ? "b316-board-column b316-board-over" : "b316-board-column",
3834
+ role: "listitem",
3835
+ "aria-label": column.label,
3836
+ onDragOver: (event) => {
3837
+ if (!canWrite || !dragging) {
3838
+ return;
1104
3839
  }
1105
- ) })
1106
- ]
1107
- },
1108
- plan.id
3840
+ event.preventDefault();
3841
+ setOver(String(column.value));
3842
+ },
3843
+ onDragLeave: () => setOver(null),
3844
+ onDrop: (event) => {
3845
+ event.preventDefault();
3846
+ setOver(null);
3847
+ const record = records.find((candidate) => candidate.id === dragging);
3848
+ if (record) {
3849
+ void move(record, column.value);
3850
+ }
3851
+ setDragging(null);
3852
+ },
3853
+ children: [
3854
+ /* @__PURE__ */ jsxs23("header", { className: "b316-board-head", children: [
3855
+ /* @__PURE__ */ jsx24("span", { className: "b316-board-title", children: column.label }),
3856
+ /* @__PURE__ */ jsx24("span", { className: "b316-muted", children: inColumn.length })
3857
+ ] }),
3858
+ /* @__PURE__ */ jsxs23("div", { className: "b316-board-cards", children: [
3859
+ inColumn.map((record) => /* @__PURE__ */ jsxs23(
3860
+ "article",
3861
+ {
3862
+ className: "b316-board-card",
3863
+ draggable: canWrite,
3864
+ onDragStart: () => setDragging(record.id),
3865
+ onDragEnd: () => {
3866
+ setDragging(null);
3867
+ setOver(null);
3868
+ },
3869
+ onClick: onSelect ? () => onSelect(record) : void 0,
3870
+ children: [
3871
+ /* @__PURE__ */ jsx24("div", { className: "b316-board-card-title", children: record.title ?? "Untitled" }),
3872
+ shown.map((candidate) => /* @__PURE__ */ jsxs23("div", { className: "b316-board-card-field", children: [
3873
+ /* @__PURE__ */ jsx24("span", { className: "b316-muted", children: candidate.label }),
3874
+ /* @__PURE__ */ jsx24(
3875
+ FieldCell,
3876
+ {
3877
+ value: record.fields[candidate.key] ?? null,
3878
+ field: candidate,
3879
+ related: record.related[candidate.key]
3880
+ }
3881
+ )
3882
+ ] }, candidate.key)),
3883
+ canWrite ? (
3884
+ // Dragging is not reachable by keyboard and is awkward on a
3885
+ // phone. This is the same action by other means, not a
3886
+ // lesser fallback.
3887
+ /* @__PURE__ */ jsxs23("label", { className: "b316-board-move", children: [
3888
+ /* @__PURE__ */ jsx24("span", { className: "b316-muted", children: "Move to" }),
3889
+ /* @__PURE__ */ jsx24(
3890
+ "select",
3891
+ {
3892
+ className: "b316-input",
3893
+ value: String(record.fields[field.key] ?? ""),
3894
+ onClick: (event) => event.stopPropagation(),
3895
+ onChange: (event) => {
3896
+ const chosen = columns.find(
3897
+ (candidate) => String(candidate.value) === event.target.value
3898
+ );
3899
+ void move(record, chosen?.value ?? null);
3900
+ },
3901
+ children: columns.map((candidate) => /* @__PURE__ */ jsx24("option", { value: String(candidate.value), children: candidate.label }, String(candidate.value)))
3902
+ }
3903
+ )
3904
+ ] })
3905
+ ) : null
3906
+ ]
3907
+ },
3908
+ record.id
3909
+ )),
3910
+ inColumn.length === 0 ? /* @__PURE__ */ jsx24("div", { className: "b316-board-empty", children: emptyState ?? "Nothing here" }) : null
3911
+ ] })
3912
+ ]
3913
+ },
3914
+ String(column.value)
3915
+ );
3916
+ }) }),
3917
+ hasMore ? /* @__PURE__ */ jsx24("div", { className: "b316-table-more", children: /* @__PURE__ */ jsxs23("button", { type: "button", className: "b316-button-quiet", onClick: loadMore, disabled: loading, children: [
3918
+ "Showing ",
3919
+ records.length,
3920
+ " of ",
3921
+ total,
3922
+ " \u2014 load more"
3923
+ ] }) }) : null
3924
+ ] });
3925
+ }
3926
+ function columnsFor(field) {
3927
+ if (field.type === "BOOLEAN") {
3928
+ return [
3929
+ { value: true, label: "Yes" },
3930
+ { value: false, label: "No" }
3931
+ ];
3932
+ }
3933
+ const options = (field.options ?? []).map((option) => ({ value: option, label: option }));
3934
+ return [...options, { value: null, label: "None" }];
3935
+ }
3936
+
3937
+ // src/Chart.tsx
3938
+ import {
3939
+ fieldsAggregatableBy,
3940
+ fieldsFor,
3941
+ specFor as specFor5
3942
+ } from "@base316/sdk";
3943
+ import { useMemo as useMemo3 } from "react";
3944
+ import { jsx as jsx25, jsxs as jsxs24 } from "react/jsx-runtime";
3945
+ function Chart({
3946
+ collection,
3947
+ x,
3948
+ y,
3949
+ aggregate = "sum",
3950
+ bucket = "day",
3951
+ title,
3952
+ height = 220,
3953
+ className,
3954
+ style,
3955
+ ...options
3956
+ }) {
3957
+ const { schema, records, error, loading } = useCollection(collection, {
3958
+ // A chart of one page is a chart of an arbitrary subset, which is worse
3959
+ // than no chart: it looks like an answer.
3960
+ limit: options.limit ?? 200,
3961
+ ...options
3962
+ });
3963
+ const axis = useMemo3(() => {
3964
+ if (!schema) {
3965
+ return null;
3966
+ }
3967
+ const timeFields = fieldsFor(schema.fields, "timeAxis");
3968
+ const chosen = x ? schema.fields.find((field) => field.key === x) ?? null : timeFields[0] ?? null;
3969
+ return chosen && specFor5(chosen).capabilities.timeAxis ? chosen : null;
3970
+ }, [schema, x]);
3971
+ const measure = useMemo3(() => {
3972
+ if (!schema || !y) {
3973
+ return null;
3974
+ }
3975
+ const candidates = fieldsAggregatableBy(schema.fields, aggregate);
3976
+ return candidates.find((field) => field.key === y) ?? null;
3977
+ }, [schema, y, aggregate]);
3978
+ const buckets = useMemo3(
3979
+ () => axis ? bucketise(records, axis, measure, y ? aggregate : "count", bucket) : [],
3980
+ [records, axis, measure, aggregate, bucket, y]
3981
+ );
3982
+ if (error) {
3983
+ return /* @__PURE__ */ jsx25("div", { className: cls("b316-card", className), style, children: /* @__PURE__ */ jsx25("div", { className: "b316-error", role: "alert", children: error }) });
3984
+ }
3985
+ if (!schema || loading) {
3986
+ return /* @__PURE__ */ jsx25("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
3987
+ }
3988
+ if (!axis) {
3989
+ return /* @__PURE__ */ jsx25("div", { className: cls("b316-card", className), style, children: x ? `"${x}" is not a date, so it cannot be a time axis.` : `${schema.namePlural} have no date field to chart against. Add one and this chart will use it.` });
3990
+ }
3991
+ if (y && !measure) {
3992
+ return /* @__PURE__ */ jsx25("div", { className: cls("b316-card", className), style, children: `"${y}" cannot be ${aggregate === "avg" ? "averaged" : `combined with ${aggregate}`}.` });
3993
+ }
3994
+ const peak = Math.max(...buckets.map((entry) => entry.value), 0);
3995
+ const heading = title ?? `${measure ? `${aggregate} of ${measure.label}` : schema.namePlural} by ${axis.label}`;
3996
+ return /* @__PURE__ */ jsxs24("figure", { className: cls("b316-card b316-chart", className), style, children: [
3997
+ /* @__PURE__ */ jsx25("figcaption", { className: "b316-chart-title", children: heading }),
3998
+ buckets.length === 0 || peak === 0 ? /* @__PURE__ */ jsx25("div", { className: "b316-empty", children: "Nothing to chart yet." }) : /* @__PURE__ */ jsx25("div", { className: "b316-chart-plot", style: { height }, children: buckets.map((entry) => /* @__PURE__ */ jsxs24("div", { className: "b316-chart-col", children: [
3999
+ /* @__PURE__ */ jsx25(
4000
+ "div",
4001
+ {
4002
+ className: "b316-chart-bar",
4003
+ style: { height: `${Math.max(2, entry.value / peak * 100)}%` },
4004
+ title: `${entry.label}: ${format(entry.value)}`
4005
+ }
4006
+ ),
4007
+ /* @__PURE__ */ jsx25("span", { className: "b316-chart-label", children: entry.label })
4008
+ ] }, entry.key)) }),
4009
+ /* @__PURE__ */ jsxs24("details", { className: "b316-chart-data", children: [
4010
+ /* @__PURE__ */ jsx25("summary", { children: "Values" }),
4011
+ /* @__PURE__ */ jsxs24("table", { className: "b316-table", children: [
4012
+ /* @__PURE__ */ jsx25("thead", { children: /* @__PURE__ */ jsxs24("tr", { children: [
4013
+ /* @__PURE__ */ jsx25("th", { scope: "col", children: axis.label }),
4014
+ /* @__PURE__ */ jsx25("th", { scope: "col", children: measure ? measure.label : "Count" })
4015
+ ] }) }),
4016
+ /* @__PURE__ */ jsx25("tbody", { children: buckets.map((entry) => /* @__PURE__ */ jsxs24("tr", { children: [
4017
+ /* @__PURE__ */ jsx25("td", { children: entry.label }),
4018
+ /* @__PURE__ */ jsx25("td", { children: format(entry.value) })
4019
+ ] }, entry.key)) })
4020
+ ] })
4021
+ ] })
4022
+ ] });
4023
+ }
4024
+ function format(value) {
4025
+ return new Intl.NumberFormat(void 0, { maximumFractionDigits: 2 }).format(value);
4026
+ }
4027
+ function keyFor(date, bucket) {
4028
+ if (bucket === "month") {
4029
+ return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}`;
4030
+ }
4031
+ if (bucket === "week") {
4032
+ const monday = new Date(date);
4033
+ const offset = (date.getUTCDay() + 6) % 7;
4034
+ monday.setUTCDate(date.getUTCDate() - offset);
4035
+ return monday.toISOString().slice(0, 10);
4036
+ }
4037
+ return date.toISOString().slice(0, 10);
4038
+ }
4039
+ function bucketise(records, axis, measure, aggregate, bucket) {
4040
+ const groups = /* @__PURE__ */ new Map();
4041
+ for (const record of records) {
4042
+ const raw = record.fields[axis.key];
4043
+ if (raw === null || raw === void 0 || raw === "") {
4044
+ continue;
4045
+ }
4046
+ const date = new Date(String(raw));
4047
+ if (Number.isNaN(date.getTime())) {
4048
+ continue;
4049
+ }
4050
+ const key = keyFor(date, bucket);
4051
+ const value = measure ? Number(record.fields[measure.key]) : 1;
4052
+ groups.set(key, [...groups.get(key) ?? [], Number.isFinite(value) ? value : 0]);
4053
+ }
4054
+ return [...groups.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([key, values]) => ({
4055
+ key,
4056
+ label: key,
4057
+ value: combine(values, aggregate)
4058
+ }));
4059
+ }
4060
+ function combine(values, aggregate) {
4061
+ switch (aggregate) {
4062
+ case "count":
4063
+ return values.length;
4064
+ case "avg":
4065
+ return values.length === 0 ? 0 : values.reduce((total, value) => total + value, 0) / values.length;
4066
+ case "min":
4067
+ return values.length === 0 ? 0 : Math.min(...values);
4068
+ case "max":
4069
+ return values.length === 0 ? 0 : Math.max(...values);
4070
+ default:
4071
+ return values.reduce((total, value) => total + value, 0);
4072
+ }
4073
+ }
4074
+
4075
+ // src/RecordDetail.tsx
4076
+ import { specFor as specFor6 } from "@base316/sdk";
4077
+ import { useState as useState24 } from "react";
4078
+ import { jsx as jsx26, jsxs as jsxs25 } from "react/jsx-runtime";
4079
+ function RecordDetail({
4080
+ collection,
4081
+ recordId,
4082
+ tenant,
4083
+ readOnly,
4084
+ onDeleted,
4085
+ onClose,
4086
+ children,
4087
+ className,
4088
+ style
4089
+ }) {
4090
+ const { schema, records, canWrite, remove, loading, error } = useCollection(collection, {
4091
+ tenant,
4092
+ recordId
4093
+ });
4094
+ const [editing, setEditing] = useState24(false);
4095
+ const record = records.find((candidate) => candidate.id === recordId);
4096
+ if (error) {
4097
+ return /* @__PURE__ */ jsx26("div", { className: cls("b316-card", className), style, children: /* @__PURE__ */ jsx26("div", { className: "b316-error", role: "alert", children: error }) });
4098
+ }
4099
+ if (!schema || loading) {
4100
+ return /* @__PURE__ */ jsx26("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
4101
+ }
4102
+ if (!record) {
4103
+ return /* @__PURE__ */ jsxs25("div", { className: cls("b316-card", className), style, children: [
4104
+ "That ",
4105
+ schema.name.toLowerCase(),
4106
+ " is not available."
4107
+ ] });
4108
+ }
4109
+ if (editing) {
4110
+ return /* @__PURE__ */ jsx26(
4111
+ RecordForm,
4112
+ {
4113
+ collection,
4114
+ recordId,
4115
+ tenant,
4116
+ className,
4117
+ style,
4118
+ onSaved: () => setEditing(false),
4119
+ onCancel: () => setEditing(false)
4120
+ }
1109
4121
  );
1110
- }) });
4122
+ }
4123
+ const writable = canWrite && !readOnly;
4124
+ return /* @__PURE__ */ jsxs25("div", { className: cls("b316-card b316-detail", className), style, children: [
4125
+ /* @__PURE__ */ jsxs25("div", { className: "b316-detail-head", children: [
4126
+ /* @__PURE__ */ jsx26("h3", { className: "b316-detail-title", children: record.title ?? schema.name }),
4127
+ /* @__PURE__ */ jsxs25("div", { className: "b316-detail-actions", children: [
4128
+ writable ? /* @__PURE__ */ jsx26("button", { type: "button", className: "b316-button-quiet", onClick: () => setEditing(true), children: "Edit" }) : null,
4129
+ writable ? /* @__PURE__ */ jsx26(
4130
+ ConfirmButton,
4131
+ {
4132
+ className: "b316-button-quiet b316-danger",
4133
+ confirmLabel: `Delete ${record.title ?? "this record"} for good`,
4134
+ onConfirm: async () => {
4135
+ if (await remove(recordId)) {
4136
+ onDeleted?.();
4137
+ }
4138
+ },
4139
+ children: "Delete"
4140
+ }
4141
+ ) : null,
4142
+ onClose ? /* @__PURE__ */ jsx26("button", { type: "button", className: "b316-button-quiet", onClick: onClose, children: "Close" }) : null
4143
+ ] })
4144
+ ] }),
4145
+ /* @__PURE__ */ jsx26("dl", { className: "b316-detail-fields", children: schema.fields.map((field) => /* @__PURE__ */ jsxs25("div", { className: "b316-detail-row", children: [
4146
+ /* @__PURE__ */ jsxs25("dt", { className: "b316-label", children: [
4147
+ field.label,
4148
+ specFor6(field).computed ? /* @__PURE__ */ jsx26("span", { className: "b316-muted", children: " (calculated)" }) : null
4149
+ ] }),
4150
+ /* @__PURE__ */ jsx26("dd", { children: /* @__PURE__ */ jsx26(
4151
+ FieldCell,
4152
+ {
4153
+ value: record.fields[field.key] ?? null,
4154
+ field,
4155
+ related: record.related[field.key]
4156
+ }
4157
+ ) })
4158
+ ] }, field.key)) }),
4159
+ children
4160
+ ] });
1111
4161
  }
1112
4162
 
1113
4163
  // src/SubscriptionManager.tsx
1114
- import { useCallback as useCallback4, useEffect as useEffect6, useState as useState7 } from "react";
1115
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
4164
+ import { useCallback as useCallback8, useEffect as useEffect17, useState as useState25 } from "react";
4165
+ import { jsx as jsx27, jsxs as jsxs26 } from "react/jsx-runtime";
1116
4166
  function statusLabel(status) {
1117
4167
  const words = status.toLowerCase().split("_");
1118
4168
  return words.map((w, i) => i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w).join(" ");
@@ -1122,11 +4172,11 @@ function SubscriptionManager({
1122
4172
  style
1123
4173
  }) {
1124
4174
  const { client, token, loading: sessionLoading } = useBase316();
1125
- const [subscription, setSubscription] = useState7(null);
1126
- const [loading, setLoading] = useState7(true);
1127
- const [busy, setBusy] = useState7(false);
1128
- const [error, setError] = useState7(null);
1129
- const load = useCallback4(async () => {
4175
+ const [subscription, setSubscription] = useState25(null);
4176
+ const [loading, setLoading] = useState25(true);
4177
+ const [busy, setBusy] = useState25(false);
4178
+ const [error, setError] = useState25(null);
4179
+ const load = useCallback8(async () => {
1130
4180
  if (!token) {
1131
4181
  setSubscription(null);
1132
4182
  setLoading(false);
@@ -1142,32 +4192,35 @@ function SubscriptionManager({
1142
4192
  setLoading(false);
1143
4193
  }
1144
4194
  }, [client, token]);
1145
- useEffect6(() => {
4195
+ useEffect17(() => {
1146
4196
  void load();
1147
4197
  }, [load]);
1148
4198
  if (sessionLoading || loading) {
1149
- return /* @__PURE__ */ jsx9("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
4199
+ return /* @__PURE__ */ jsx27("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
1150
4200
  }
1151
4201
  if (!token) {
1152
- return /* @__PURE__ */ jsx9("div", { className: cls("b316-card", className), style, children: "Sign in to manage your subscription." });
4202
+ return /* @__PURE__ */ jsx27("div", { className: cls("b316-card", className), style, children: "Sign in to manage your subscription." });
4203
+ }
4204
+ if (error) {
4205
+ return /* @__PURE__ */ jsx27("div", { className: cls("b316-card", className), style, children: /* @__PURE__ */ jsx27("div", { className: "b316-error", role: "alert", children: error }) });
1153
4206
  }
1154
4207
  if (!subscription) {
1155
- return /* @__PURE__ */ jsx9("div", { className: cls("b316-card", className), style, children: "You are not subscribed to a plan." });
4208
+ return /* @__PURE__ */ jsx27("div", { className: cls("b316-card", className), style, children: "You are not subscribed to a plan." });
1156
4209
  }
1157
4210
  const renews = new Date(subscription.currentPeriodEnd).toLocaleDateString();
1158
- return /* @__PURE__ */ jsxs8("div", { className: cls("b316-card", className), style, children: [
1159
- error ? /* @__PURE__ */ jsx9("div", { className: "b316-error", children: error }) : null,
1160
- /* @__PURE__ */ jsx9("div", { className: "b316-plan-name", children: subscription.planVersion.plan.name }),
1161
- /* @__PURE__ */ jsxs8("div", { className: "b316-muted", children: [
4211
+ return /* @__PURE__ */ jsxs26("div", { className: cls("b316-card", className), style, children: [
4212
+ error ? /* @__PURE__ */ jsx27("div", { className: "b316-error", role: "alert", children: error }) : null,
4213
+ /* @__PURE__ */ jsx27("div", { className: "b316-plan-name", children: subscription.planVersion.plan.name }),
4214
+ /* @__PURE__ */ jsxs26("div", { className: "b316-muted", children: [
1162
4215
  statusLabel(subscription.status),
1163
4216
  " \xB7 ",
1164
4217
  subscription.cancelAtPeriodEnd ? `Ends ${renews}` : `Renews ${renews}`
1165
4218
  ] }),
1166
- subscription.cancelAtPeriodEnd ? /* @__PURE__ */ jsxs8("p", { className: "b316-muted", children: [
4219
+ subscription.cancelAtPeriodEnd ? /* @__PURE__ */ jsxs26("p", { className: "b316-muted", children: [
1167
4220
  "This subscription will not renew. You keep access until ",
1168
4221
  renews,
1169
4222
  "."
1170
- ] }) : /* @__PURE__ */ jsx9(
4223
+ ] }) : /* @__PURE__ */ jsx27(
1171
4224
  "button",
1172
4225
  {
1173
4226
  type: "button",
@@ -1192,17 +4245,935 @@ function SubscriptionManager({
1192
4245
  )
1193
4246
  ] });
1194
4247
  }
4248
+
4249
+ // src/ApiKeyManager.tsx
4250
+ import { useCallback as useCallback9, useEffect as useEffect18, useState as useState26 } from "react";
4251
+ import { jsx as jsx28, jsxs as jsxs27 } from "react/jsx-runtime";
4252
+ function duration(seconds) {
4253
+ if (seconds % 3600 === 0) {
4254
+ return `${seconds / 3600}h`;
4255
+ }
4256
+ if (seconds % 60 === 0) {
4257
+ return `${seconds / 60}m`;
4258
+ }
4259
+ return `${seconds}s`;
4260
+ }
4261
+ function ApiKeyManager({
4262
+ className,
4263
+ style,
4264
+ /** Restrict to one API. Omit to show every one the account can reach. */
4265
+ audience
4266
+ }) {
4267
+ const { client, token, loading: sessionLoading } = useBase316();
4268
+ const [resources, setResources] = useState26([]);
4269
+ const [credentials, setCredentials] = useState26([]);
4270
+ const [loading, setLoading] = useState26(true);
4271
+ const [busy, setBusy] = useState26(false);
4272
+ const [error, setError] = useState26(null);
4273
+ const [creating, setCreating] = useState26(false);
4274
+ const [name, setName] = useState26("");
4275
+ const [chosen, setChosen] = useState26([]);
4276
+ const [resourceId, setResourceId] = useState26(null);
4277
+ const [issued, setIssued] = useState26(null);
4278
+ const load = useCallback9(async () => {
4279
+ if (!token) {
4280
+ setLoading(false);
4281
+ return;
4282
+ }
4283
+ try {
4284
+ const [available, held] = await Promise.all([
4285
+ client.apiCredentials.resources(token),
4286
+ client.apiCredentials.list(token)
4287
+ ]);
4288
+ const visible = audience ? available.data.filter((resource2) => resource2.audience === audience) : available.data;
4289
+ setResources(visible);
4290
+ setCredentials(audience ? held.data.filter((item) => item.audience === audience) : held.data);
4291
+ setResourceId((current) => current ?? visible[0]?.id ?? null);
4292
+ } catch (cause) {
4293
+ console.error("[base316] could not load API credentials", cause);
4294
+ setError("Could not load your API keys.");
4295
+ } finally {
4296
+ setLoading(false);
4297
+ }
4298
+ }, [client, token, audience]);
4299
+ useEffect18(() => {
4300
+ void load();
4301
+ }, [load]);
4302
+ if (sessionLoading || loading) {
4303
+ return /* @__PURE__ */ jsx28("div", { className: cls("b316-card", className), style, children: "Loading\u2026" });
4304
+ }
4305
+ if (!token) {
4306
+ return /* @__PURE__ */ jsx28("div", { className: cls("b316-card", className), style, children: "Sign in to manage your API keys." });
4307
+ }
4308
+ if (error) {
4309
+ return /* @__PURE__ */ jsx28("div", { className: cls("b316-card", className), style, children: /* @__PURE__ */ jsx28("div", { className: "b316-error", role: "alert", children: error }) });
4310
+ }
4311
+ if (resources.length === 0) {
4312
+ return /* @__PURE__ */ jsx28("div", { className: cls("b316-card", className), style, children: "There are no APIs available to your account." });
4313
+ }
4314
+ const resource = resources.find((item) => item.id === resourceId) ?? resources[0];
4315
+ const reset = () => {
4316
+ setCreating(false);
4317
+ setName("");
4318
+ setChosen([]);
4319
+ setIssued(null);
4320
+ };
4321
+ return /* @__PURE__ */ jsxs27("div", { className: cls("b316-card", className), style, children: [
4322
+ error ? /* @__PURE__ */ jsx28("div", { className: "b316-error", role: "alert", children: error }) : null,
4323
+ resources.length > 1 ? /* @__PURE__ */ jsx28(
4324
+ "select",
4325
+ {
4326
+ className: "b316-input",
4327
+ value: resource.id,
4328
+ onChange: (event) => setResourceId(event.target.value),
4329
+ style: { marginBottom: 14 },
4330
+ children: resources.map((item) => /* @__PURE__ */ jsx28("option", { value: item.id, children: item.name }, item.id))
4331
+ }
4332
+ ) : /* @__PURE__ */ jsx28("div", { className: "b316-plan-name", children: resource.name }),
4333
+ resource.rateLimit ? /* @__PURE__ */ jsxs27("div", { className: "b316-muted", children: [
4334
+ resource.rateLimit.requestsPerWindow,
4335
+ " requests per",
4336
+ " ",
4337
+ duration(resource.rateLimit.windowSeconds),
4338
+ resource.rateLimit.burst > resource.rateLimit.requestsPerWindow ? `, bursting to ${resource.rateLimit.burst}` : null
4339
+ ] }) : /* @__PURE__ */ jsx28("div", { className: "b316-muted", children: "No rate limit." }),
4340
+ credentials.length === 0 ? /* @__PURE__ */ jsx28("p", { className: "b316-muted", style: { marginTop: 14 }, children: "You have no API keys yet." }) : /* @__PURE__ */ jsx28("ul", { className: "b316-list", style: { marginTop: 14 }, children: credentials.map((credential) => /* @__PURE__ */ jsxs27("li", { className: "b316-list-item", children: [
4341
+ /* @__PURE__ */ jsxs27("div", { children: [
4342
+ /* @__PURE__ */ jsx28("div", { className: "b316-plan-name", children: credential.name }),
4343
+ /* @__PURE__ */ jsxs27("div", { className: "b316-muted", children: [
4344
+ /* @__PURE__ */ jsx28("code", { children: credential.clientId }),
4345
+ credential.secretLastFour ? ` \xB7 \u2026${credential.secretLastFour}` : null,
4346
+ credential.disabled ? " \xB7 Disabled" : null
4347
+ ] }),
4348
+ /* @__PURE__ */ jsx28("div", { className: "b316-muted", children: credential.scopes.join(", ") })
4349
+ ] }),
4350
+ /* @__PURE__ */ jsxs27("div", { style: { display: "flex", gap: 8 }, children: [
4351
+ /* @__PURE__ */ jsx28(
4352
+ "button",
4353
+ {
4354
+ type: "button",
4355
+ className: "b316-button b316-button-secondary",
4356
+ disabled: busy,
4357
+ onClick: async () => {
4358
+ setBusy(true);
4359
+ setError(null);
4360
+ try {
4361
+ const rotated = await client.apiCredentials.rotate(
4362
+ token,
4363
+ credential.clientId
4364
+ );
4365
+ setIssued({
4366
+ clientId: credential.clientId,
4367
+ secret: rotated.issuedSecret
4368
+ });
4369
+ await load();
4370
+ } catch (cause) {
4371
+ console.error("[base316] rotate failed", cause);
4372
+ setError("Could not rotate that key.");
4373
+ } finally {
4374
+ setBusy(false);
4375
+ }
4376
+ },
4377
+ children: "Rotate"
4378
+ }
4379
+ ),
4380
+ /* @__PURE__ */ jsx28(
4381
+ ConfirmButton,
4382
+ {
4383
+ className: "b316-button b316-button-secondary",
4384
+ disabled: busy,
4385
+ confirmLabel: `Revoke ${credential.name} for good`,
4386
+ onConfirm: async () => {
4387
+ setBusy(true);
4388
+ setError(null);
4389
+ try {
4390
+ await client.apiCredentials.revoke(token, credential.clientId);
4391
+ await load();
4392
+ } catch (cause) {
4393
+ console.error("[base316] revoke failed", cause);
4394
+ setError("Could not revoke that key.");
4395
+ } finally {
4396
+ setBusy(false);
4397
+ }
4398
+ },
4399
+ children: "Revoke"
4400
+ }
4401
+ )
4402
+ ] })
4403
+ ] }, credential.clientId)) }),
4404
+ issued ? /* @__PURE__ */ jsxs27("div", { className: "b316-notice", style: { marginTop: 14 }, children: [
4405
+ /* @__PURE__ */ jsx28("strong", { children: "Copy this now." }),
4406
+ " It is shown once and cannot be retrieved again.",
4407
+ /* @__PURE__ */ jsx28("div", { style: { marginTop: 8 }, children: /* @__PURE__ */ jsx28("code", { style: { wordBreak: "break-all" }, children: issued.secret }) }),
4408
+ /* @__PURE__ */ jsx28(
4409
+ "button",
4410
+ {
4411
+ type: "button",
4412
+ className: "b316-button b316-button-secondary",
4413
+ style: { marginTop: 10 },
4414
+ onClick: () => {
4415
+ void navigator.clipboard?.writeText(issued.secret).catch(() => {
4416
+ });
4417
+ },
4418
+ children: "Copy"
4419
+ }
4420
+ ),
4421
+ /* @__PURE__ */ jsx28(
4422
+ "button",
4423
+ {
4424
+ type: "button",
4425
+ className: "b316-button b316-button-secondary",
4426
+ style: { marginTop: 10, marginLeft: 8 },
4427
+ onClick: reset,
4428
+ children: "Done"
4429
+ }
4430
+ )
4431
+ ] }) : null,
4432
+ creating && !issued ? /* @__PURE__ */ jsxs27("div", { style: { marginTop: 14 }, children: [
4433
+ /* @__PURE__ */ jsx28(
4434
+ "input",
4435
+ {
4436
+ className: "b316-input",
4437
+ placeholder: "What is this key for?",
4438
+ value: name,
4439
+ onChange: (event) => setName(event.target.value)
4440
+ }
4441
+ ),
4442
+ /* @__PURE__ */ jsxs27("div", { style: { marginTop: 10 }, children: [
4443
+ resource.availableScopes.map((scope) => /* @__PURE__ */ jsxs27("label", { className: "b316-checkbox", children: [
4444
+ /* @__PURE__ */ jsx28(
4445
+ "input",
4446
+ {
4447
+ type: "checkbox",
4448
+ checked: chosen.includes(scope.key),
4449
+ onChange: (event) => setChosen(
4450
+ (current) => event.target.checked ? [...current, scope.key] : current.filter((key) => key !== scope.key)
4451
+ )
4452
+ }
4453
+ ),
4454
+ /* @__PURE__ */ jsx28("code", { children: scope.key }),
4455
+ scope.description ? /* @__PURE__ */ jsxs27("span", { className: "b316-muted", children: [
4456
+ " \u2014 ",
4457
+ scope.description
4458
+ ] }) : null
4459
+ ] }, scope.key)),
4460
+ resource.lockedScopes.map((scope) => /* @__PURE__ */ jsxs27("label", { className: "b316-checkbox b316-muted", children: [
4461
+ /* @__PURE__ */ jsx28("input", { type: "checkbox", disabled: true }),
4462
+ /* @__PURE__ */ jsx28("code", { children: scope.key }),
4463
+ /* @__PURE__ */ jsx28("span", { children: " \u2014 not included in your plan" })
4464
+ ] }, scope.key))
4465
+ ] }),
4466
+ /* @__PURE__ */ jsx28(
4467
+ "button",
4468
+ {
4469
+ type: "button",
4470
+ className: "b316-button",
4471
+ style: { marginTop: 12 },
4472
+ disabled: busy || !name.trim() || chosen.length === 0,
4473
+ onClick: async () => {
4474
+ setBusy(true);
4475
+ setError(null);
4476
+ try {
4477
+ const created = await client.apiCredentials.create(token, {
4478
+ apiResourceId: resource.id,
4479
+ name: name.trim(),
4480
+ scopes: chosen
4481
+ });
4482
+ if (created.issuedSecret) {
4483
+ setIssued({ clientId: created.clientId, secret: created.issuedSecret });
4484
+ }
4485
+ setCreating(false);
4486
+ setName("");
4487
+ setChosen([]);
4488
+ await load();
4489
+ } catch (cause) {
4490
+ const message = cause instanceof Error ? cause.message : "Could not create that key.";
4491
+ console.error("[base316] create failed", cause);
4492
+ setError(message);
4493
+ } finally {
4494
+ setBusy(false);
4495
+ }
4496
+ },
4497
+ children: "Create key"
4498
+ }
4499
+ ),
4500
+ /* @__PURE__ */ jsx28(
4501
+ "button",
4502
+ {
4503
+ type: "button",
4504
+ className: "b316-button b316-button-secondary",
4505
+ style: { marginTop: 12, marginLeft: 8 },
4506
+ onClick: reset,
4507
+ children: "Cancel"
4508
+ }
4509
+ )
4510
+ ] }) : null,
4511
+ !creating && !issued ? /* @__PURE__ */ jsx28(
4512
+ "button",
4513
+ {
4514
+ type: "button",
4515
+ className: "b316-button",
4516
+ style: { marginTop: 14 },
4517
+ disabled: resource.availableScopes.length === 0,
4518
+ onClick: () => setCreating(true),
4519
+ children: resource.availableScopes.length === 0 ? "No scopes available on your plan" : "New API key"
4520
+ }
4521
+ ) : null
4522
+ ] });
4523
+ }
4524
+
4525
+ // src/manifest.ts
4526
+ var appearance = [
4527
+ { name: "className", summary: "Your own classes on the wrapper.", required: false },
4528
+ { name: "style", summary: "Inline styles on the wrapper.", required: false }
4529
+ ];
4530
+ var MANIFEST = [
4531
+ // --- Setup ---------------------------------------------------------------
4532
+ {
4533
+ name: "Base316Provider",
4534
+ kind: "provider",
4535
+ summary: "Holds the client, the session and the theme. Everything else must be inside it.",
4536
+ useWhen: "Once, around the part of the application that uses any other export here.",
4537
+ requires: [],
4538
+ props: [
4539
+ { name: "publishableKey", summary: "The environment's publishable key.", required: true },
4540
+ { name: "baseUrl", summary: "Where the API lives. Defaults to production.", required: false },
4541
+ {
4542
+ name: "token",
4543
+ summary: "Supply to own session storage yourself. Left out, the session is kept in localStorage.",
4544
+ required: false
4545
+ },
4546
+ {
4547
+ name: "onTokenChange",
4548
+ summary: "Called when the session changes. Pair with `token` for an httpOnly cookie.",
4549
+ required: false
4550
+ },
4551
+ {
4552
+ name: "appearance",
4553
+ summary: "Colours, radius and font as tokens. Restyles everything at once.",
4554
+ required: false
4555
+ },
4556
+ ...appearance
4557
+ ],
4558
+ seeAlso: ["useBase316", "useSession"]
4559
+ },
4560
+ {
4561
+ name: "useBase316",
4562
+ kind: "hook",
4563
+ summary: "The client, the session token and the branding, from inside the provider.",
4564
+ useWhen: "Writing a component of your own that needs to call the API directly.",
4565
+ requires: [],
4566
+ props: [],
4567
+ seeAlso: ["Base316Provider"]
4568
+ },
4569
+ {
4570
+ name: "useSession",
4571
+ kind: "hook",
4572
+ summary: "The resolved session, or null. Carries `userId`, `user` (id, email, name, image, and the customer's own declared fields) and `tenants` \u2014 every team this person is in, each with their role.",
4573
+ useWhen: "Deciding what to render for a signed-in versus a signed-out visitor.",
4574
+ requires: [],
4575
+ props: []
4576
+ },
4577
+ // --- Signing in ----------------------------------------------------------
4578
+ {
4579
+ name: "SignIn",
4580
+ kind: "component",
4581
+ summary: "Password, passkey and magic-link sign-in in one form.",
4582
+ useWhen: "The application renders its own sign-in rather than redirecting to the hosted page.",
4583
+ requires: [],
4584
+ props: [
4585
+ { name: "onSignedIn", summary: "Called once a session exists.", required: false },
4586
+ {
4587
+ name: "passkeys",
4588
+ summary: "Offer passkeys where supported. On by default.",
4589
+ required: false
4590
+ },
4591
+ {
4592
+ name: "magicLinkRedirectUrl",
4593
+ summary: "Where an emailed link lands, which must be registered on the environment. Without it, magic link is not offered at all.",
4594
+ required: false
4595
+ },
4596
+ ...appearance
4597
+ ],
4598
+ seeAlso: ["SignUp", "ForgotPassword"]
4599
+ },
4600
+ {
4601
+ name: "SignUp",
4602
+ kind: "component",
4603
+ summary: "Create an account with an email and a password.",
4604
+ useWhen: "Alongside SignIn, on a page that registers new users.",
4605
+ requires: [],
4606
+ props: [
4607
+ { name: "onSignedUp", summary: "Called after the account is created.", required: false },
4608
+ ...appearance
4609
+ ],
4610
+ seeAlso: ["SignIn", "VerifyEmail"]
4611
+ },
4612
+ {
4613
+ name: "ForgotPassword",
4614
+ kind: "component",
4615
+ summary: "Ask for a reset link by email.",
4616
+ useWhen: "Linked from a sign-in form.",
4617
+ requires: [],
4618
+ props: [
4619
+ { name: "onSent", summary: "Called with the address after the request.", required: false },
4620
+ { name: "signInUrl", summary: "Renders a link back to sign-in.", required: false },
4621
+ ...appearance
4622
+ ],
4623
+ seeAlso: ["ResetPassword"]
4624
+ },
4625
+ {
4626
+ name: "ResetPassword",
4627
+ kind: "component",
4628
+ summary: "Set a new password from an emailed link.",
4629
+ useWhen: "On the page the reset link lands on.",
4630
+ requires: [],
4631
+ props: [
4632
+ {
4633
+ name: "token",
4634
+ summary: "Read from `?token=` when omitted, and removed from the URL.",
4635
+ required: false
4636
+ },
4637
+ { name: "onReset", summary: "Called once the password is changed.", required: false },
4638
+ { name: "signInUrl", summary: "Renders a link onward to sign-in.", required: false },
4639
+ ...appearance
4640
+ ],
4641
+ whenUnmet: "With no token in the URL or props, explains that the link is needed.",
4642
+ seeAlso: ["ForgotPassword"]
4643
+ },
4644
+ {
4645
+ name: "VerifyEmail",
4646
+ kind: "component",
4647
+ summary: "Confirm an address from an emailed link.",
4648
+ useWhen: "On the page the verification link lands on.",
4649
+ requires: [],
4650
+ props: [
4651
+ {
4652
+ name: "token",
4653
+ summary: "Read from `?token=` when omitted, and removed from the URL.",
4654
+ required: false
4655
+ },
4656
+ { name: "onVerified", summary: "Called once confirmed.", required: false },
4657
+ { name: "continueUrl", summary: "Renders a link onward.", required: false },
4658
+ ...appearance
4659
+ ],
4660
+ seeAlso: ["SignUp"]
4661
+ },
4662
+ // --- The account ---------------------------------------------------------
4663
+ {
4664
+ name: "UserProfile",
4665
+ kind: "component",
4666
+ summary: "Name, email and the customer's own declared fields.",
4667
+ useWhen: "An account settings page.",
4668
+ requires: ["session"],
4669
+ props: [],
4670
+ whenUnmet: "Renders nothing without a session.",
4671
+ seeAlso: ["ActiveSessions", "TwoFactorManager", "PasskeyManager"]
4672
+ },
4673
+ {
4674
+ name: "ActiveSessions",
4675
+ kind: "component",
4676
+ summary: "Where the account is signed in, and a button to end the other devices.",
4677
+ useWhen: "A security settings page.",
4678
+ requires: ["session"],
4679
+ props: [
4680
+ { name: "onRevoked", summary: "Called with the number of sessions ended.", required: false },
4681
+ ...appearance
4682
+ ],
4683
+ whenUnmet: "Renders nothing without a session.",
4684
+ seeAlso: ["TwoFactorManager"]
4685
+ },
4686
+ {
4687
+ name: "DeviceApproval",
4688
+ kind: "component",
4689
+ summary: "Approve a sign-in that started on a CLI, a TV, or anything with no browser.",
4690
+ useWhen: "You point a device's verification URI at a page in your own app. Put this on that page.",
4691
+ requires: ["session"],
4692
+ props: [
4693
+ {
4694
+ name: "userCode",
4695
+ summary: "The code, when the URL already carries it. Skips the entry step; without it the person types the code.",
4696
+ required: false
4697
+ },
4698
+ { name: "onDecided", summary: "Called with whether they approved.", required: false },
4699
+ ...appearance
4700
+ ],
4701
+ whenUnmet: "Renders nothing without a session \u2014 there is nobody to approve as, and collecting a code first would fail at the step that matters.",
4702
+ seeAlso: ["SignIn", "ActiveSessions"]
4703
+ },
4704
+ {
4705
+ name: "PrivacyControls",
4706
+ kind: "component",
4707
+ summary: "Download a copy of everything held about you, or delete your account.",
4708
+ useWhen: "An account settings page. These are the two things a privacy notice promises and most products never build.",
4709
+ requires: ["session"],
4710
+ props: [
4711
+ {
4712
+ name: "confirmWord",
4713
+ summary: "What must be typed to confirm deletion. Defaults to the English word, which is wrong in every other language.",
4714
+ required: false
4715
+ },
4716
+ {
4717
+ name: "onErased",
4718
+ summary: "Called once the account is gone. Sign them out and send them somewhere.",
4719
+ required: false
4720
+ },
4721
+ ...appearance
4722
+ ],
4723
+ whenUnmet: "Renders nothing without a session.",
4724
+ seeAlso: ["UserProfile", "ActiveSessions"]
4725
+ },
4726
+ {
4727
+ name: "TwoFactorManager",
4728
+ kind: "component",
4729
+ summary: "Turn two-factor authentication on or off, by authenticator app or a code to the inbox.",
4730
+ useWhen: "A security settings page. Offers both factors; the app is stronger, the emailed code is the one people who will not install an app actually use.",
4731
+ requires: ["session"],
4732
+ props: [
4733
+ {
4734
+ name: "enabled",
4735
+ summary: "Whether a factor already exists, which decides which side is shown.",
4736
+ required: false
4737
+ },
4738
+ { name: "onEnabled", summary: "Called once a factor is confirmed.", required: false },
4739
+ { name: "onDisabled", summary: "Called once it is removed.", required: false },
4740
+ ...appearance
4741
+ ],
4742
+ whenUnmet: "Renders nothing without a session.",
4743
+ seeAlso: ["ActiveSessions", "PasskeyManager"]
4744
+ },
4745
+ {
4746
+ name: "PasskeyManager",
4747
+ kind: "component",
4748
+ summary: "Add and remove passkeys.",
4749
+ useWhen: "A security settings page, beside two-factor.",
4750
+ requires: ["session"],
4751
+ props: [{ name: "title", summary: "Heading text.", required: false }, ...appearance],
4752
+ whenUnmet: "Renders nothing without a session.",
4753
+ seeAlso: ["TwoFactorManager"]
4754
+ },
4755
+ {
4756
+ name: "Branding",
4757
+ kind: "component",
4758
+ summary: "The environment's logo, name and tagline.",
4759
+ useWhen: "Above a sign-in form, so your pages match the hosted ones.",
4760
+ requires: [],
4761
+ props: [
4762
+ { name: "showTagline", summary: "Include the tagline under the name.", required: false },
4763
+ ...appearance
4764
+ ]
4765
+ },
4766
+ // --- Teams ---------------------------------------------------------------
4767
+ {
4768
+ name: "TeamMembers",
4769
+ kind: "component",
4770
+ summary: "Who is in a team, who has been invited, and the way in and out.",
4771
+ useWhen: "A Team or Members settings page. One component covers the list, the pending invitations, the invite box and leaving.",
4772
+ requires: ["session"],
4773
+ props: [
4774
+ {
4775
+ name: "organizationId",
4776
+ summary: "Which team. Defaults to the only one this person is in; with several it says so rather than guessing.",
4777
+ required: false
4778
+ },
4779
+ {
4780
+ name: "onInvited",
4781
+ summary: "Called with `{ email, token, url }` \u2014 the address invited, the raw token, and the accept URL when `acceptUrl` was given. This is where your application sends the email. Without a handler the link is shown on screen instead, because the token is returned once and cannot be fetched again.",
4782
+ required: false
4783
+ },
4784
+ {
4785
+ name: "acceptUrl",
4786
+ summary: "The page hosting AcceptInvitation. The component appends `?invitation=<token>` and hands the finished link back as `url` on onInvited; without it, onInvited gets the raw token and you build the link yourself.",
4787
+ required: false
4788
+ },
4789
+ { name: "onLeft", summary: "Called after the viewer leaves the team.", required: false },
4790
+ ...appearance
4791
+ ],
4792
+ whenUnmet: "Renders nothing without a session. Inviting and removing appear only for somebody whose role grants team.manage.",
4793
+ seeAlso: ["AcceptInvitation"]
4794
+ },
4795
+ {
4796
+ name: "AcceptInvitation",
4797
+ kind: "component",
4798
+ summary: "The other end of an invitation: what it is for, and joining.",
4799
+ useWhen: "On the page an invitation link lands on.",
4800
+ requires: [],
4801
+ props: [
4802
+ {
4803
+ name: "token",
4804
+ summary: "Read from `?invitation=` when omitted.",
4805
+ required: false
4806
+ },
4807
+ {
4808
+ name: "onAccepted",
4809
+ summary: "Called with the organizationId once they have joined.",
4810
+ required: false
4811
+ },
4812
+ { name: "signUpUrl", summary: "For somebody with no account yet.", required: false },
4813
+ { name: "signInUrl", summary: "For somebody who has one.", required: false },
4814
+ ...appearance
4815
+ ],
4816
+ whenUnmet: "Says what the invitation is for without a session, since the point is that they may have no account. Names the address to sign in as when the signed-in one does not match.",
4817
+ seeAlso: ["TeamMembers", "SignUp"]
4818
+ },
4819
+ // --- Money ---------------------------------------------------------------
4820
+ {
4821
+ name: "PricingTable",
4822
+ kind: "component",
4823
+ summary: "The published plans, and completing the purchase.",
4824
+ useWhen: "A pricing page, or an upgrade screen.",
4825
+ requires: ["plans", "paymentAccount"],
4826
+ props: [
4827
+ {
4828
+ name: "onSelectPlan",
4829
+ summary: "Take over what Choose does. Left out, the component changes the plan or starts a checkout itself.",
4830
+ required: false
4831
+ },
4832
+ {
4833
+ name: "currentPlanVersionId",
4834
+ summary: "Marks the current plan. Discovered from the session when omitted.",
4835
+ required: false
4836
+ },
4837
+ {
4838
+ name: "featuredPlanKey",
4839
+ summary: "Which plan to present as the default.",
4840
+ required: false
4841
+ },
4842
+ {
4843
+ name: "signInUrl",
4844
+ summary: "Where a signed-out visitor goes on choosing. Without it the button says so rather than failing.",
4845
+ required: false
4846
+ },
4847
+ ...appearance
4848
+ ],
4849
+ whenUnmet: "Says plans are unavailable when the catalog is empty.",
4850
+ seeAlso: ["SubscriptionManager", "usePlans"]
4851
+ },
4852
+ {
4853
+ name: "usePlans",
4854
+ kind: "hook",
4855
+ summary: "The plans, the current one, and the prorate-or-checkout branch \u2014 without markup.",
4856
+ useWhen: "You want your own pricing table but not your own billing logic.",
4857
+ requires: ["plans"],
4858
+ props: [],
4859
+ seeAlso: ["PricingTable"]
4860
+ },
4861
+ {
4862
+ name: "SubscriptionManager",
4863
+ kind: "component",
4864
+ summary: "The current plan, its renewal date, and cancelling.",
4865
+ useWhen: "A billing settings page.",
4866
+ requires: ["session"],
4867
+ props: [],
4868
+ whenUnmet: "Renders nothing without a session.",
4869
+ seeAlso: ["PricingTable", "Invoices"]
4870
+ },
4871
+ {
4872
+ name: "Invoices",
4873
+ kind: "component",
4874
+ summary: "The signed-in user's invoices, newest first.",
4875
+ useWhen: "A billing settings page.",
4876
+ requires: ["session"],
4877
+ props: [],
4878
+ whenUnmet: "Renders nothing without a session.",
4879
+ seeAlso: ["SubscriptionManager"]
4880
+ },
4881
+ {
4882
+ name: "PayInvoice",
4883
+ kind: "component",
4884
+ summary: "Pay one invoice from a link, with no account needed.",
4885
+ useWhen: "A page reached from a payment link or a QR code \u2014 a walk-up customer.",
4886
+ requires: ["paymentAccount"],
4887
+ props: [
4888
+ { name: "token", summary: "The link token, from the URL.", required: false },
4889
+ { name: "environmentId", summary: "Which environment the link belongs to.", required: false },
4890
+ { name: "onPaid", summary: "Called once payment succeeds.", required: false },
4891
+ ...appearance
4892
+ ],
4893
+ seeAlso: ["usePaymentLink"]
4894
+ },
4895
+ {
4896
+ name: "usePaymentLink",
4897
+ kind: "hook",
4898
+ summary: "Watches a payment link and reports when it is paid.",
4899
+ useWhen: "Showing your own waiting screen beside a QR code.",
4900
+ requires: [],
4901
+ props: [],
4902
+ seeAlso: ["PayInvoice"]
4903
+ },
4904
+ {
4905
+ name: "ApiKeyManager",
4906
+ kind: "component",
4907
+ summary: "A customer managing their own API credentials and seeing their rate limits.",
4908
+ useWhen: "A developer-settings page inside your application.",
4909
+ requires: ["session", "apiResource"],
4910
+ props: [
4911
+ {
4912
+ name: "audience",
4913
+ summary: "Limit to one protected API. All of them when omitted.",
4914
+ required: false
4915
+ },
4916
+ ...appearance
4917
+ ],
4918
+ whenUnmet: "Renders nothing without a session; says so when no API resource is defined."
4919
+ },
4920
+ // --- The application's own data ------------------------------------------
4921
+ {
4922
+ name: "DataTable",
4923
+ kind: "component",
4924
+ summary: "A table of a collection's records, with columns derived from its fields.",
4925
+ useWhen: "Listing records. Reach for Kanban instead to group them by a status, or Chart to count or total them over time.",
4926
+ requires: ["session", "collection"],
4927
+ bindsToCollection: true,
4928
+ props: [
4929
+ { name: "collection", summary: "The collection key. Required in practice.", required: false },
4930
+ {
4931
+ name: "columns",
4932
+ summary: "Which fields to show, in order. Every field when omitted.",
4933
+ required: false
4934
+ },
4935
+ { name: "renderCell", summary: "Replace the cell for one field.", required: false },
4936
+ { name: "onSelect", summary: "Row click. Rows are inert without it.", required: false },
4937
+ {
4938
+ name: "onChanged",
4939
+ summary: "Called after this table creates, edits or deletes a record. The table refreshes itself; use this to refresh anything else on the page bound to the same collection, such as a Chart.",
4940
+ required: false
4941
+ },
4942
+ { name: "hideSearch", summary: "Hide the search box.", required: false },
4943
+ {
4944
+ name: "readOnly",
4945
+ summary: "Hide New, Edit and Delete even for a writer.",
4946
+ required: false
4947
+ },
4948
+ { name: "emptyState", summary: "Shown when there is nothing to list.", required: false },
4949
+ ...appearance
4950
+ ],
4951
+ whenUnmet: "Without the read permission it says which permission is missing, rather than showing an empty table.",
4952
+ seeAlso: ["Kanban", "Chart", "RecordDetail", "RecordForm", "useCollection"]
4953
+ },
4954
+ {
4955
+ name: "Kanban",
4956
+ kind: "component",
4957
+ summary: "A board, grouped by a choice field it finds on its own.",
4958
+ useWhen: "The records move through stages \u2014 a status, a pipeline.",
4959
+ requires: ["session", "collection"],
4960
+ bindsToCollection: true,
4961
+ props: [
4962
+ { name: "collection", summary: "The collection key. Required in practice.", required: false },
4963
+ {
4964
+ name: "groupBy",
4965
+ summary: "Which field makes the columns. Discovered when omitted.",
4966
+ required: false
4967
+ },
4968
+ { name: "cardFields", summary: "What to show on a card.", required: false },
4969
+ { name: "onSelect", summary: "Card click.", required: false },
4970
+ { name: "emptyState", summary: "Shown in a column with nothing in it.", required: false },
4971
+ ...appearance
4972
+ ],
4973
+ whenUnmet: "A collection with no groupable field says to add a choice field, rather than drawing one column.",
4974
+ seeAlso: ["DataTable", "Chart"]
4975
+ },
4976
+ {
4977
+ name: "Chart",
4978
+ kind: "component",
4979
+ summary: "Records over time, counted or totalled.",
4980
+ useWhen: "The question is how many or how much, by day, week or month.",
4981
+ requires: ["session", "collection"],
4982
+ bindsToCollection: true,
4983
+ props: [
4984
+ { name: "collection", summary: "The collection key. Required in practice.", required: false },
4985
+ { name: "x", summary: "The date field. Discovered when omitted.", required: false },
4986
+ {
4987
+ name: "y",
4988
+ summary: "What to measure. Counts the records when omitted.",
4989
+ required: false
4990
+ },
4991
+ { name: "aggregate", summary: "sum, avg, min or max. Defaults to sum.", required: false },
4992
+ { name: "bucket", summary: "day, week or month. Defaults to day.", required: false },
4993
+ { name: "title", summary: "Overrides the derived caption.", required: false },
4994
+ { name: "height", summary: "Plot height in pixels.", required: false },
4995
+ ...appearance
4996
+ ],
4997
+ whenUnmet: "A collection with no date field says so. A field that cannot be summed is refused rather than quietly counted.",
4998
+ seeAlso: ["DataTable", "Kanban"]
4999
+ },
5000
+ {
5001
+ name: "RecordDetail",
5002
+ kind: "component",
5003
+ summary: "One record in full, including the fields a table truncates.",
5004
+ useWhen: "After a row or card is clicked.",
5005
+ requires: ["session", "collection"],
5006
+ bindsToCollection: true,
5007
+ props: [
5008
+ { name: "collection", summary: "The collection key. Required in practice.", required: false },
5009
+ { name: "recordId", summary: "Which record. Required in practice.", required: false },
5010
+ { name: "tenant", summary: "Only when the user is in several.", required: false },
5011
+ { name: "readOnly", summary: "Hide Edit and Delete.", required: false },
5012
+ { name: "onDeleted", summary: "Called after deletion.", required: false },
5013
+ { name: "onClose", summary: "Renders a Close button when given.", required: false },
5014
+ { name: "children", summary: "Rendered under the fields.", required: false },
5015
+ ...appearance
5016
+ ],
5017
+ seeAlso: ["DataTable", "RecordForm"]
5018
+ },
5019
+ {
5020
+ name: "RecordForm",
5021
+ kind: "component",
5022
+ summary: "Create or edit a record, with inputs derived from the field types.",
5023
+ useWhen: "Adding a record, or editing one outside the detail view.",
5024
+ requires: ["session", "collection"],
5025
+ bindsToCollection: true,
5026
+ props: [
5027
+ { name: "collection", summary: "The collection key. Required in practice.", required: false },
5028
+ { name: "recordId", summary: "Omit to create, supply to edit.", required: false },
5029
+ { name: "tenant", summary: "Only when the user is in several.", required: false },
5030
+ {
5031
+ name: "onSaved",
5032
+ summary: "Called with the record id. Does not refresh a sibling table \u2014 see the composition rules.",
5033
+ required: false
5034
+ },
5035
+ { name: "onCancel", summary: "Renders a Cancel button when given.", required: false },
5036
+ { name: "defaults", summary: "Values to start with. Ignored when editing.", required: false },
5037
+ ...appearance
5038
+ ],
5039
+ whenUnmet: "Computed fields get no input at all, because the server produces them.",
5040
+ seeAlso: ["RecordDetail", "RelationPicker"]
5041
+ },
5042
+ {
5043
+ name: "RelationPicker",
5044
+ kind: "component",
5045
+ summary: "Choose a record in another collection by typing its name.",
5046
+ useWhen: "Building your own form. RecordForm uses this for relation fields already.",
5047
+ requires: ["session", "collection"],
5048
+ props: [
5049
+ { name: "field", summary: "The relation field descriptor.", required: true },
5050
+ { name: "value", summary: "The chosen record id, or null.", required: true },
5051
+ { name: "onChange", summary: "Called with the new id.", required: true },
5052
+ { name: "error", summary: "Message shown under the input.", required: false },
5053
+ { name: "disabled", summary: "Read-only.", required: false },
5054
+ { name: "tenant", summary: "Only when the user is in several.", required: false }
5055
+ ],
5056
+ seeAlso: ["RecordForm"]
5057
+ },
5058
+ {
5059
+ name: "useCollection",
5060
+ kind: "hook",
5061
+ summary: "A collection's schema, records, filters, paging, permissions and writes \u2014 without markup.",
5062
+ useWhen: "You want your own table, board or form but not your own data layer.",
5063
+ requires: ["session", "collection"],
5064
+ bindsToCollection: true,
5065
+ props: [],
5066
+ seeAlso: ["DataTable", "FieldCell", "FieldInput"]
5067
+ },
5068
+ {
5069
+ name: "FieldCell",
5070
+ kind: "component",
5071
+ summary: "One field value, rendered the way the tables render it.",
5072
+ useWhen: "Building your own table over useCollection.",
5073
+ requires: [],
5074
+ props: [
5075
+ { name: "value", summary: "The stored value.", required: true },
5076
+ { name: "field", summary: "The field descriptor.", required: true },
5077
+ { name: "related", summary: "A relation's resolved label.", required: false }
5078
+ ],
5079
+ seeAlso: ["FieldInput", "useCollection"]
5080
+ },
5081
+ {
5082
+ name: "FieldInput",
5083
+ kind: "component",
5084
+ summary: "The input for one field, chosen by its type.",
5085
+ useWhen: "Building your own form over useCollection.",
5086
+ requires: [],
5087
+ props: [
5088
+ { name: "value", summary: "The current value.", required: true },
5089
+ { name: "field", summary: "The field descriptor.", required: true },
5090
+ { name: "onChange", summary: "Called with the new value.", required: true },
5091
+ { name: "error", summary: "Message shown under the input.", required: false },
5092
+ { name: "disabled", summary: "Read-only.", required: false }
5093
+ ],
5094
+ whenUnmet: "Renders nothing for a computed field \u2014 the server produces those.",
5095
+ seeAlso: ["FieldCell", "RecordForm"]
5096
+ }
5097
+ ];
5098
+ var COMPOSITION = {
5099
+ rules: [
5100
+ "Everything must be inside Base316Provider. Outside it, hooks throw.",
5101
+ "Most components need a signed-in user. Gate on useSession and render SignIn when there is none \u2014 see `requires: session` on each entry.",
5102
+ "A collection must exist before anything can bind to it. Define one in the dashboard under Collections, or through POST /v1/collections with a secret key.",
5103
+ "No component takes a prop you must supply to make it behave. Naming the subject is different: a collection component needs `collection` and a detail view needs `recordId`, because those say *what* to show rather than patch *how* it works. Everything else has a default.",
5104
+ "Each component bound to a collection holds its own data \u2014 DataTable, Kanban, Chart, RecordForm, RecordDetail, every one. None of them share a cache, so a write in one leaves the others showing what they already had. Put the form inside the table, where New and Edit refresh it; pass `onChanged` to refresh a sibling such as a Chart; or lift `useCollection` and render your own, which is what the hook is for.",
5105
+ "Next.js App Router: put Base316Provider in a Client Component wrapper used by the root layout. Every component here is a Client Component; none of them run on the server."
5106
+ ],
5107
+ /** The shape almost every application ends up with. */
5108
+ example: `
5109
+ import { Base316Provider, DataTable, RecordForm, SignIn, useSession } from "@base316/react";
5110
+
5111
+ function Jobs() {
5112
+ const session = useSession();
5113
+
5114
+ // Without this the table renders nothing and looks broken.
5115
+ if (!session) return <SignIn />;
5116
+
5117
+ return (
5118
+ <>
5119
+ <DataTable collection="jobs" onSelect={(record) => console.log(record.id)} />
5120
+ <RecordForm collection="jobs" />
5121
+ </>
5122
+ );
5123
+ }
5124
+
5125
+ export function App() {
5126
+ return (
5127
+ <Base316Provider publishableKey={process.env.NEXT_PUBLIC_BASE316_PUBLISHABLE_KEY!}>
5128
+ <Jobs />
5129
+ </Base316Provider>
5130
+ );
5131
+ }
5132
+ `.trim()
5133
+ };
5134
+ function describeComponent(name) {
5135
+ return MANIFEST.find((entry) => entry.name === name);
5136
+ }
5137
+ function componentsRequiring(requirement) {
5138
+ return MANIFEST.filter((entry) => entry.requires.includes(requirement));
5139
+ }
1195
5140
  export {
5141
+ AcceptInvitation,
5142
+ ActiveSessions,
5143
+ ApiKeyManager,
1196
5144
  Base316Provider,
1197
5145
  Branding,
5146
+ COMPOSITION,
5147
+ Chart,
5148
+ DataTable,
5149
+ DeviceApproval,
5150
+ FieldCell,
5151
+ FieldInput,
5152
+ ForgotPassword,
1198
5153
  Invoices,
5154
+ Kanban,
5155
+ MANIFEST,
1199
5156
  PasskeyManager,
5157
+ PayInvoice,
1200
5158
  PricingTable,
5159
+ PrivacyControls,
5160
+ RecordDetail,
5161
+ RecordForm,
5162
+ RelationPicker,
5163
+ ResetPassword,
1201
5164
  SignIn,
1202
5165
  SignUp,
1203
5166
  SubscriptionManager,
5167
+ TeamMembers,
5168
+ TwoFactorManager,
1204
5169
  UserProfile,
5170
+ VerifyEmail,
5171
+ componentsRequiring,
5172
+ describeComponent,
1205
5173
  useBase316,
5174
+ useCollection,
5175
+ usePaymentLink,
5176
+ usePlans,
1206
5177
  useSession
1207
5178
  };
1208
5179
  //# sourceMappingURL=index.js.map