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