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