AppRoot.tsx 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
  1. /**
  2. * Shell root: boot loading page → (boot settled) → real UI in one switch.
  3. * Pure kernel component with zero plugin dependencies — before settled it may
  4. * only rely on itself (the fail-loud presentation must not depend on the
  5. * system whose failure it reports; the status/signal stores are kernel-own,
  6. * web2 shell self-sufficiency rule); the real UI is produced by the
  7. * app-shell entry once every entry is active. A failed boot keeps the
  8. * loading page, lists the per-entry fiber states and the sweep report (fail
  9. * loud, no partial UI).
  10. */
  11. import { useSyncExternalStore } from 'react'
  12. import type { ReactNode } from 'react'
  13. import type { KernelSignal, LoaderStatus } from './loader-status.ts'
  14. import css from './AppRoot.module.css'
  15. /** AppRoot props: settled signal, fiber-state projection feed, boot failure report, deferred real-UI factory. */
  16. export interface AppRootProps {
  17. /** True once the boot chain settled (loader quiesced + all entries ACTIVE); the boot closure flips it. */
  18. settled: KernelSignal<boolean>
  19. /** Per-entry fiber-state projection store (drives loading/failed rendering). */
  20. status: KernelSignal<LoaderStatus>
  21. /** Boot failure report (the settle rejection message); undefined while loading or after success. */
  22. error: KernelSignal<string | undefined>
  23. /** Builds the real UI; called only after settled. */
  24. renderApp: () => ReactNode
  25. }
  26. /** Boot gate: loading page until the boot settles; failures stay here. */
  27. export function AppRoot(props: AppRootProps) {
  28. const settled = useSyncExternalStore(props.settled.subscribe, props.settled.getSnapshot)
  29. const status = useSyncExternalStore(props.status.subscribe, props.status.getSnapshot)
  30. const error = useSyncExternalStore(props.error.subscribe, props.error.getSnapshot)
  31. const failed = Object.entries(status).filter(([, s]) => s === 'failed')
  32. if (settled) return <>{props.renderApp()}</>
  33. const loud = error !== undefined || failed.length > 0
  34. return (
  35. <div className={css.boot}>
  36. <div className={css.card}>
  37. <div className={css.wordmark}>HARNESS</div>
  38. {!loud
  39. ? (
  40. <>
  41. <div className={css.spinner} />
  42. <div className={css.hint}>Loading plugins…</div>
  43. </>
  44. )
  45. : (
  46. <div className={css.failed}>
  47. <div className={css.failedTitle}>Failed to load plugins</div>
  48. {failed.map(([id]) => <div key={id} className={css.failedItem}>{id}</div>)}
  49. {error !== undefined && <div className={css.failedItem}>{error}</div>}
  50. </div>
  51. )}
  52. </div>
  53. </div>
  54. )
  55. }