@frontera-sdk/core 1.53.2 → 1.53.4
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/package.json +1 -1
- package/src/create-frontera-app.tsx +72 -139
package/package.json
CHANGED
|
@@ -158,20 +158,9 @@ class AppErrorBoundary extends Component<BoundaryProps, { error: Error | null }>
|
|
|
158
158
|
* the build it is meant to report on.
|
|
159
159
|
*/
|
|
160
160
|
const BOOT_KEYFRAMES =
|
|
161
|
-
'@keyframes frontera-boot-
|
|
162
|
-
'@keyframes frontera-boot-
|
|
163
|
-
'@media(prefers-reduced-motion:reduce){'
|
|
164
|
-
'[data-frontera-boot-cube]{animation-duration:3.9s!important}' +
|
|
165
|
-
'[data-frontera-boot-shimmer]{animation:none!important}}'
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Per-cube animation delays, in grid order.
|
|
169
|
-
*
|
|
170
|
-
* Copied from the platform's `.spinner-cube:nth-child(n)` rules rather than
|
|
171
|
-
* re-derived, so the two read as the same object in motion. The pattern is a
|
|
172
|
-
* diagonal wave: equal delays run bottom-left to top-right.
|
|
173
|
-
*/
|
|
174
|
-
const BOOT_CUBE_DELAYS = [0.2, 0.3, 0.4, 0.1, 0.2, 0.3, 0, 0.1, 0.2]
|
|
161
|
+
'@keyframes frontera-boot-appear{to{opacity:1}}' +
|
|
162
|
+
'@keyframes frontera-boot-sweep{0%{transform:translateX(-100%)}100%{transform:translateX(250%)}}' +
|
|
163
|
+
'@media(prefers-reduced-motion:reduce){[data-frontera-boot-bar]{animation:none!important;width:100%!important;opacity:.4}}'
|
|
175
164
|
|
|
176
165
|
/**
|
|
177
166
|
* Colours for the boot screen.
|
|
@@ -206,68 +195,66 @@ const BOOT_PALETTE = {
|
|
|
206
195
|
}
|
|
207
196
|
|
|
208
197
|
/**
|
|
209
|
-
* The
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* app booting inside the platform should not announce itself with a spinner
|
|
215
|
-
* shape that appears nowhere else in the product.
|
|
216
|
-
*
|
|
217
|
-
* Not a top bar: `AppFrame` already shows `LoadingBar` outside the iframe for
|
|
218
|
-
* exactly this wait, and a second bar inside the frame would draw the same
|
|
219
|
-
* progress twice.
|
|
220
|
-
*
|
|
221
|
-
* Sized in `em` like the original, so the cubes track the font size rather
|
|
222
|
-
* than needing a second number kept in sync.
|
|
198
|
+
* The screen shown while the session is being established: the app's own
|
|
199
|
+
* surface with a thin bar sweeping along its top edge, and no words. Most
|
|
200
|
+
* boots finish before the bar fades in, so a fast start shows only the
|
|
201
|
+
* surface the app is about to paint over; a slow one shows that it is still
|
|
202
|
+
* working without asking anyone to read about sessions.
|
|
223
203
|
*/
|
|
224
|
-
function
|
|
204
|
+
function bootLoading(): ReactNode {
|
|
225
205
|
return (
|
|
226
206
|
<div
|
|
227
|
-
|
|
207
|
+
// A status, not an alert: `alert` is assertive and interrupts the
|
|
208
|
+
// screen-reader user on every single app boot.
|
|
209
|
+
role="status"
|
|
210
|
+
aria-label="Loading"
|
|
228
211
|
style={{
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
fontSize: 28,
|
|
235
|
-
color: BOOT_PALETTE.muted,
|
|
236
|
-
// Tops the container's 10 up to the 16 this sits above.
|
|
237
|
-
marginBottom: 6,
|
|
212
|
+
boxSizing: 'border-box',
|
|
213
|
+
position: 'relative',
|
|
214
|
+
minHeight: '100dvh',
|
|
215
|
+
overflow: 'hidden',
|
|
216
|
+
background: BOOT_PALETTE.background,
|
|
238
217
|
}}
|
|
239
218
|
>
|
|
240
|
-
{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
219
|
+
<style>{BOOT_KEYFRAMES}</style>
|
|
220
|
+
<div
|
|
221
|
+
aria-hidden="true"
|
|
222
|
+
style={{
|
|
223
|
+
position: 'absolute',
|
|
224
|
+
inset: '0 0 auto 0',
|
|
225
|
+
height: 2,
|
|
226
|
+
opacity: 0,
|
|
227
|
+
animation: 'frontera-boot-appear 200ms ease-out 300ms forwards',
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<div
|
|
231
|
+
data-frontera-boot-bar=""
|
|
244
232
|
style={{
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
233
|
+
width: '40%',
|
|
234
|
+
height: '100%',
|
|
235
|
+
borderRadius: 2,
|
|
236
|
+
background: BOOT_PALETTE.muted,
|
|
237
|
+
opacity: 0.6,
|
|
238
|
+
animation: 'frontera-boot-sweep 1.2s cubic-bezier(.4,0,.2,1) infinite',
|
|
248
239
|
}}
|
|
249
240
|
/>
|
|
250
|
-
|
|
241
|
+
</div>
|
|
251
242
|
</div>
|
|
252
243
|
)
|
|
253
244
|
}
|
|
254
245
|
|
|
255
246
|
/**
|
|
256
|
-
* The
|
|
257
|
-
*
|
|
247
|
+
* The screen shown when an app cannot render: it failed to start, or it
|
|
248
|
+
* threw while rendering.
|
|
258
249
|
*
|
|
259
250
|
* Colours come from `BOOT_PALETTE` rather than being written inline, because
|
|
260
|
-
* this
|
|
261
|
-
* without it.
|
|
251
|
+
* this can paint BEFORE `applyHostTheme` has run and the values have to hold
|
|
252
|
+
* up without it.
|
|
262
253
|
*/
|
|
263
|
-
function diagnostic(title: string, detail: string
|
|
264
|
-
const loading = variant === 'loading'
|
|
254
|
+
function diagnostic(title: string, detail: string): ReactNode {
|
|
265
255
|
return (
|
|
266
256
|
<div
|
|
267
|
-
|
|
268
|
-
// interrupts the screen-reader user on every single app boot.
|
|
269
|
-
role={loading ? 'status' : 'alert'}
|
|
270
|
-
aria-live={loading ? 'polite' : undefined}
|
|
257
|
+
role="alert"
|
|
271
258
|
style={{
|
|
272
259
|
boxSizing: 'border-box',
|
|
273
260
|
minHeight: '100dvh',
|
|
@@ -280,85 +267,35 @@ function diagnostic(title: string, detail: string, variant: 'loading' | 'error'
|
|
|
280
267
|
lineHeight: 1.5,
|
|
281
268
|
}}
|
|
282
269
|
>
|
|
283
|
-
<style>{BOOT_KEYFRAMES}</style>
|
|
284
270
|
<div
|
|
285
271
|
style={{
|
|
286
272
|
display: 'flex',
|
|
287
273
|
flexDirection: 'column',
|
|
288
274
|
alignItems: 'center',
|
|
289
275
|
// The two gaps differ: 10 between the lines of text, 16 from the
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
// numbers stay readable instead of being one compromise value.
|
|
276
|
+
// icon down to them. `gap` sets the smaller one for every pair and
|
|
277
|
+
// the icon adds the remainder below itself.
|
|
293
278
|
gap: 10,
|
|
294
279
|
textAlign: 'center',
|
|
295
280
|
maxWidth: 380,
|
|
296
281
|
}}
|
|
297
282
|
>
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
strokeWidth={2}
|
|
309
|
-
strokeLinecap="round"
|
|
310
|
-
>
|
|
311
|
-
<circle cx="12" cy="12" r="9" />
|
|
312
|
-
<path d="M12 8v5" />
|
|
313
|
-
<path d="M12 16.5h.01" />
|
|
314
|
-
</svg>
|
|
315
|
-
)}
|
|
316
|
-
<div
|
|
317
|
-
data-frontera-boot-shimmer={loading ? '' : undefined}
|
|
318
|
-
style={{
|
|
319
|
-
// No font-size: `<strong>` inherited the body size on main, and
|
|
320
|
-
// matching it keeps this screen the size it has always been.
|
|
321
|
-
fontWeight: 'bold' as const,
|
|
322
|
-
// A highlight sweeping across the glyphs themselves, clipped to the
|
|
323
|
-
// text. Only while connecting: an error is not in progress, and
|
|
324
|
-
// animating it would suggest the app is still trying.
|
|
325
|
-
//
|
|
326
|
-
// The gradient rests on `currentColor`, which is the heading's own
|
|
327
|
-
// full-contrast colour — on main this was plain `--foreground`, and
|
|
328
|
-
// a muted base would leave it at the same weight as the line
|
|
329
|
-
// beneath it for most of the sweep.
|
|
330
|
-
//
|
|
331
|
-
// `currentColor` rather than the token, because a token can fail:
|
|
332
|
-
// in the platform theme `--foreground` holds a bare HSL triplet, so
|
|
333
|
-
// naming it inside `linear-gradient()` makes the whole declaration
|
|
334
|
-
// invalid. The browser then drops the gradient but KEEPS
|
|
335
|
-
// `-webkit-text-fill-color: transparent` — and the heading vanishes
|
|
336
|
-
// entirely. `currentColor` is always a valid colour, so the worst
|
|
337
|
-
// case is a shimmer that does not shimmer, never invisible text.
|
|
338
|
-
//
|
|
339
|
-
// The travelling stop is `muted-foreground`, NOT `primary`: primary
|
|
340
|
-
// is near-black in the default light theme, which is also what the
|
|
341
|
-
// heading already is, so that sweep was three identical stops and
|
|
342
|
-
// no visible motion. Muted is the one token guaranteed to differ
|
|
343
|
-
// from body text in both schemes — that is what it is for.
|
|
344
|
-
...(loading
|
|
345
|
-
? {
|
|
346
|
-
backgroundImage: `linear-gradient(90deg, currentColor 30%, ${BOOT_PALETTE.muted} 50%, currentColor 70%)`,
|
|
347
|
-
backgroundSize: '200% 100%',
|
|
348
|
-
WebkitBackgroundClip: 'text',
|
|
349
|
-
backgroundClip: 'text',
|
|
350
|
-
fontSize: 18,
|
|
351
|
-
// Transparent text is invisible if `background-clip: text` is
|
|
352
|
-
// not honoured, so the fill is set the same way and the
|
|
353
|
-
// colour underneath stays the readable one.
|
|
354
|
-
WebkitTextFillColor: 'transparent',
|
|
355
|
-
animation: 'frontera-boot-shimmer 2s linear infinite',
|
|
356
|
-
}
|
|
357
|
-
: {}),
|
|
358
|
-
}}
|
|
283
|
+
<svg
|
|
284
|
+
aria-hidden="true"
|
|
285
|
+
viewBox="0 0 24 24"
|
|
286
|
+
width={30}
|
|
287
|
+
height={30}
|
|
288
|
+
fill="none"
|
|
289
|
+
stroke={BOOT_PALETTE.destructive}
|
|
290
|
+
strokeWidth={2}
|
|
291
|
+
strokeLinecap="round"
|
|
292
|
+
style={{ marginBottom: 6 }}
|
|
359
293
|
>
|
|
360
|
-
|
|
361
|
-
|
|
294
|
+
<circle cx="12" cy="12" r="9" />
|
|
295
|
+
<path d="M12 8v5" />
|
|
296
|
+
<path d="M12 16.5h.01" />
|
|
297
|
+
</svg>
|
|
298
|
+
<div style={{ fontWeight: 'bold' as const }}>{title}</div>
|
|
362
299
|
<div
|
|
363
300
|
style={{
|
|
364
301
|
fontSize: 14,
|
|
@@ -366,18 +303,14 @@ function diagnostic(title: string, detail: string, variant: 'loading' | 'error'
|
|
|
366
303
|
// An error detail is a raw message or stack fragment: monospaced,
|
|
367
304
|
// left-aligned and boxed so a long one stays readable instead of
|
|
368
305
|
// becoming a centred wall of text.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
padding: '10px 12px',
|
|
378
|
-
maxWidth: '100%',
|
|
379
|
-
overflowWrap: 'anywhere' as const,
|
|
380
|
-
}),
|
|
306
|
+
fontFamily: 'var(--font-mono, ui-monospace, monospace)',
|
|
307
|
+
textAlign: 'left' as const,
|
|
308
|
+
background: BOOT_PALETTE.surface,
|
|
309
|
+
border: `1px solid ${BOOT_PALETTE.border}`,
|
|
310
|
+
borderRadius: 'calc(var(--radius, 8px) - 2px)',
|
|
311
|
+
padding: '10px 12px',
|
|
312
|
+
maxWidth: '100%',
|
|
313
|
+
overflowWrap: 'anywhere' as const,
|
|
381
314
|
}}
|
|
382
315
|
>
|
|
383
316
|
{detail}
|
|
@@ -416,8 +349,8 @@ const ACCESS_COPY: Record<AccessRefusalReason, { title: string; detail: string }
|
|
|
416
349
|
|
|
417
350
|
/**
|
|
418
351
|
* The default screen for a person Frontera refused to sign in: a title, one
|
|
419
|
-
* sentence on what to do, and Try again. Deliberately unlike `diagnostic`
|
|
420
|
-
*
|
|
352
|
+
* sentence on what to do, and Try again. Deliberately unlike `diagnostic`
|
|
353
|
+
* (no alert icon, no monospaced message) because nothing is
|
|
421
354
|
* broken. The service's own message stays out: it is written for the App's
|
|
422
355
|
* builder, not the person.
|
|
423
356
|
*/
|
|
@@ -590,7 +523,7 @@ export interface FronteraAppProviderProps {
|
|
|
590
523
|
export function FronteraAppProvider({
|
|
591
524
|
children,
|
|
592
525
|
providers = [],
|
|
593
|
-
loading =
|
|
526
|
+
loading = bootLoading(),
|
|
594
527
|
errorFallback,
|
|
595
528
|
accessDenied = accessRefusedScreen,
|
|
596
529
|
queryClient: providedQueryClient,
|