@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontera-sdk/core",
3
- "version": "1.53.2",
3
+ "version": "1.53.4",
4
4
  "description": "Frontera app runtime: the platform bridge client, app bootstrap and typed platform client.",
5
5
  "keywords": [
6
6
  "frontera",
@@ -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-cube{0%,70%,100%{transform:scale3d(.5,.5,1)}35%{transform:scale3d(0,0,1)}}' +
162
- '@keyframes frontera-boot-shimmer{0%{background-position:100% 0}100%{background-position:-100% 0}}' +
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 spinner shown while the session is being established.
210
- *
211
- * The platform's own `GridSpinner` — a 3×3 of pulsing cubes — rebuilt here in
212
- * inline styles, because this package has neither Tailwind nor the stylesheet
213
- * that rule lives in. Matching it matters more than the few lines it costs: an
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 bootSpinner(): ReactNode {
204
+ function bootLoading(): ReactNode {
225
205
  return (
226
206
  <div
227
- aria-hidden="true"
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
- display: 'inline-grid',
230
- gridTemplateColumns: 'repeat(3, 1fr)',
231
- gap: '0.08em',
232
- width: '1em',
233
- height: '1em',
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
- {BOOT_CUBE_DELAYS.map((delay, index) => (
241
- <span
242
- key={index}
243
- data-frontera-boot-cube=""
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
- backgroundColor: 'currentColor',
246
- transform: 'scale3d(0.5, 0.5, 1)',
247
- animation: `frontera-boot-cube 1.3s ${delay}s infinite ease-in-out`,
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 screens shown before an app can render: connecting, and the two ways
257
- * starting can fail.
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 paints BEFORE `applyHostTheme` has run and the values have to hold up
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, variant: 'loading' | 'error' = 'error'): ReactNode {
264
- const loading = variant === 'loading'
254
+ function diagnostic(title: string, detail: string): ReactNode {
265
255
  return (
266
256
  <div
267
- // A loading screen is a status, not an alert: `alert` is assertive and
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
- // indicator down to them. `gap` sets the smaller one for every pair
291
- // and the indicator adds the remainder below itself, so the two
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
- {loading ? (
299
- bootSpinner()
300
- ) : (
301
- <svg
302
- aria-hidden="true"
303
- viewBox="0 0 24 24"
304
- width={30}
305
- height={30}
306
- fill="none"
307
- stroke={BOOT_PALETTE.destructive}
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
- {title}
361
- </div>
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
- ...(loading
370
- ? {}
371
- : {
372
- fontFamily: 'var(--font-mono, ui-monospace, monospace)',
373
- textAlign: 'left' as const,
374
- background: BOOT_PALETTE.surface,
375
- border: `1px solid ${BOOT_PALETTE.border}`,
376
- borderRadius: 'calc(var(--radius, 8px) - 2px)',
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`'s
420
- * error variant (no alert icon, no monospaced message) because nothing is
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 = diagnostic('Connecting to Frontera', 'Establishing an authenticated App session.', 'loading'),
526
+ loading = bootLoading(),
594
527
  errorFallback,
595
528
  accessDenied = accessRefusedScreen,
596
529
  queryClient: providedQueryClient,