@stealthscale/sdk-host 0.1.0 → 0.4.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.
@@ -2,16 +2,17 @@ import { n as translatorOf, r as useProductName, t as pluginWordsOf } from "./wo
2
2
  import { DataProvider, createDataClient, defineMutation, defineQuery, findRecords, mutateOperation, operationQuery, resetData } from "@stealthscale/provider-data";
3
3
  import { HOST, NOBODY, evaluateWhen, hostContract, pluginOf, subjectOf } from "@stealthscale/sdk-core";
4
4
  import { sampledTransport } from "@stealthscale/provider-data/testing";
5
- import { Boundary, HostContext, PluginProvider, RouteDecorations, Slot, changesOf, conditionContextOf, decisionKey, decisionOf, isCommandEnabled, lazyOf, useCommands, useDocumentTitle, useNavigation, usePluginStatuses, useResolvedProduct, useSettings, useToaster } from "@stealthscale/sdk-plugin";
6
- import { Alert, EmptyState, Toast } from "@stealthscale/component-feedback";
5
+ import { Boundary, HostContext, PluginImportError, PluginProvider, RouteDecorations, Slot, asServerSnapshot, changesOf, conditionContextOf, decisionKey, decisionOf, inServerSnapshot, isCommandEnabled, lazyOf, useCommands, useDocumentTitle, useHydrating, useNavigation, usePluginStatuses, useResolvedProduct, useSettings, useToaster } from "@stealthscale/sdk-plugin";
6
+ import { Alert, EmptyState, Spinner, Toast } from "@stealthscale/component-feedback";
7
7
  import { settingKey } from "@stealthscale/settings";
8
+ import { Navigate, Outlet, RouterContextProvider, compileRoutes, createLink, declaredOf, isNotFound, notFound, redirect, routeHref, useDeclaredRoute, useNavigate, useRouteContext, useRouteMap, useRouter } from "@stealthscale/provider-router";
8
9
  import { c } from "react/compiler-runtime";
9
- import { Navigate, Outlet, RouterContextProvider, compileRoutes, createLink, declaredOf, notFound, redirect, routeHref, useDeclaredRoute, useNavigate, useRouteContext, useRouter } from "@stealthscale/provider-router";
10
10
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
- import { Component, Suspense, createElement, lazy, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
12
- import { Button } from "@stealthscale/component-actions";
11
+ import { Component, Suspense, createContext, createElement, lazy, use, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
13
12
  import { useTranslation } from "@stealthscale/provider-i18n";
13
+ import { Button } from "@stealthscale/component-actions";
14
14
  import { normalizeHotkey, useHotkey, useHotkeys } from "@stealthscale/provider-hotkeys";
15
+ import { SearchIcon, XIcon } from "lucide-react";
15
16
  import { Command, Dialog } from "@stealthscale/component-modals";
16
17
  import { loadNeeds } from "@stealthscale/provider-data/router";
17
18
  import { Stack } from "@stealthscale/component-layout";
@@ -59,7 +60,7 @@ function createEventBus({ events, report }) {
59
60
  if (event.sticky) kept.set(eventId, payload);
60
61
  depth += 1;
61
62
  try {
62
- target.dispatchEvent(new CustomEvent(eventId, { detail: payload }));
63
+ target.dispatchEvent(new CustomEvent(eventId, { detail: [payload] }));
63
64
  } finally {
64
65
  depth -= 1;
65
66
  }
@@ -84,7 +85,7 @@ function createEventBus({ events, report }) {
84
85
  * Receives the event on the target.
85
86
  */
86
87
  const listener = (event) => {
87
- deliver(event.detail);
88
+ deliver(event.detail[0]);
88
89
  };
89
90
  target.addEventListener(eventId, listener);
90
91
  if (kept.has(eventId)) deliver(kept.get(eventId));
@@ -444,26 +445,38 @@ function createPageStore() {
444
445
  //#endregion
445
446
  //#region src/stores/quarantine.ts
446
447
  /**
448
+ * Counts the renders of each route and extension that throw in a row, and quarantines a target
449
+ * that fails too many.
450
+ */
451
+ /**
447
452
  * Returns the quarantine store, with no target quarantined.
448
453
  *
449
454
  * @remarks
450
455
  * The count is per render, not per second, so a target rendered sixty times a minute and one
451
456
  * rendered once an hour follow one rule. A render that commits returns its target's count to
452
- * zero. A quarantine lasts until `retry` or a reload: no store keeps it, because a reload may
453
- * load a release that fixed the fault.
457
+ * zero. The store reports and counts an error once until a render of its target commits or a
458
+ * retry. The router's error boundary resets on every update of its match, and a boundary that
459
+ * resets renders its fallback again with the error it caught. A quarantine lasts until `retry`
460
+ * or a reload: no store keeps it, because a reload may load a release that fixed the fault. A
461
+ * `PluginImportError` is reported and does not count, because a module that failed to import
462
+ * renders only after a reload: Chromium keeps the failed import for the document's life.
454
463
  */
455
464
  function createQuarantineStore({ after, report }) {
456
465
  const failures = /* @__PURE__ */ new Map();
466
+ const last = /* @__PURE__ */ new Map();
457
467
  const state = writable(/* @__PURE__ */ new Map());
458
468
  return {
459
469
  failed: (target, error) => {
460
- const count = (failures.get(target) ?? 0) + 1;
461
- failures.set(target, count);
470
+ if (last.has(target) && last.get(target) === error) return;
471
+ last.set(target, error);
462
472
  report({
463
473
  error,
464
474
  kind: "render-failed",
465
475
  target
466
476
  });
477
+ if (error instanceof PluginImportError) return;
478
+ const count = (failures.get(target) ?? 0) + 1;
479
+ failures.set(target, count);
467
480
  if (count < after || state.get().has(target)) return;
468
481
  state.set(new Map([...state.get(), [target, {
469
482
  error,
@@ -478,9 +491,11 @@ function createQuarantineStore({ after, report }) {
478
491
  get: state.get,
479
492
  rendered: (target) => {
480
493
  failures.delete(target);
494
+ last.delete(target);
481
495
  },
482
496
  retry: (target) => {
483
497
  failures.delete(target);
498
+ last.delete(target);
484
499
  if (!state.get().has(target)) return;
485
500
  state.set(new Map([...state.get()].filter(([quarantined]) => quarantined !== target)));
486
501
  },
@@ -505,7 +520,8 @@ function createReportStore() {
505
520
  //#endregion
506
521
  //#region src/host/page.ts
507
522
  /**
508
- * Failed renders in a row after which a target is quarantined, where the product states none.
523
+ * Failed renders in a row after which a target is quarantined, where the product does not set a
524
+ * number.
509
525
  */
510
526
  const QUARANTINE_AFTER = 3;
511
527
  /**
@@ -520,9 +536,10 @@ function toConsole(entry) {
520
536
  else console.warn(`${PREFIX} ${entry.kind}`, entry);
521
537
  }
522
538
  /**
523
- * Returns the page's stores, empty, and the function that reports an entry into them.
539
+ * Returns the page's stores and the function that reports an entry into them, with each refused
540
+ * release reported.
524
541
  */
525
- function createPage({ quarantineAfter = QUARANTINE_AFTER, report: receiver = toConsole }) {
542
+ function createPage({ quarantineAfter = QUARANTINE_AFTER, refused = [], report: receiver = toConsole }) {
526
543
  const reports = createReportStore();
527
544
  /**
528
545
  * Adds an entry to the reports store and passes it to the receiver.
@@ -531,7 +548,9 @@ function createPage({ quarantineAfter = QUARANTINE_AFTER, report: receiver = toC
531
548
  reports.add(entry);
532
549
  receiver(entry);
533
550
  };
551
+ for (const entry of refused) report(entry);
534
552
  return {
553
+ loading: writable(false),
535
554
  report,
536
555
  stores: {
537
556
  mounted: createMountedStore(),
@@ -555,6 +574,9 @@ function valuesOf(record) {
555
574
  /**
556
575
  * Returns every importer a plugin's code lists: its pages, extensions, commands and sections, and
557
576
  * their fallbacks.
577
+ *
578
+ * @remarks
579
+ * The page index is left out, because the palette imports it the first time it opens.
558
580
  */
559
581
  function importersOf(code) {
560
582
  return [
@@ -756,8 +778,8 @@ function hostApiOf({ connection, data, events, product, stores, toaster }) {
756
778
  return {
757
779
  can: (permission, resourceId) => allowed(stores, product, permission.id, resourceId),
758
780
  data: hostData,
759
- emit: (event, payload) => {
760
- events.emit(pluginId, event.id, payload);
781
+ emit: (event, ...payload) => {
782
+ events.emit(pluginId, event.id, payload[0]);
761
783
  },
762
784
  flag: (flag) => stores.flags.read(flag.id) ?? flag.default ?? false,
763
785
  matched: connected?.matched().at(-1),
@@ -772,15 +794,15 @@ function hostApiOf({ connection, data, events, product, stores, toaster }) {
772
794
  //#endregion
773
795
  //#region src/recovery/stale.ts
774
796
  /**
775
- * Records the version under the key in the tab's session storage, and returns true where it did.
797
+ * Records a value under the key in the tab's session storage, and returns true where it did.
776
798
  *
777
- * @returns False where the key contains the version already, or where the browser refuses the
799
+ * @returns False where the key contains the value already, or where the browser refuses the
778
800
  * storage.
779
801
  */
780
- function recordReload(key, version) {
802
+ function recordReload(key, value) {
781
803
  try {
782
- if (window.sessionStorage.getItem(key) === version) return false;
783
- window.sessionStorage.setItem(key, version);
804
+ if (window.sessionStorage.getItem(key) === value) return false;
805
+ window.sessionStorage.setItem(key, value);
784
806
  return true;
785
807
  } catch {
786
808
  return false;
@@ -788,21 +810,24 @@ function recordReload(key, version) {
788
810
  }
789
811
  /**
790
812
  * Returns the function that reloads the page after a module failed to import, once per build
791
- * version, and returns true while the page reloads.
813
+ * version and deployment, and returns true while the page reloads.
792
814
  *
793
815
  * @remarks
794
816
  * The function records the product's version under `stealth.<productId>.reloaded` before it
795
- * reloads. For a version recorded before, it returns false, so a chunk the new build lacks as
796
- * well renders the error component instead of a second reload. A server has no page to reload,
797
- * and a page whose storage the browser refuses cannot record the version, so both return false.
798
- * @param product - The product: its id names the storage key, and its version is the build's.
799
- */
800
- function staleRecovery({ productId, version }) {
817
+ * reloads, as `<version>+<deployment>` on a page that loaded a deployment's releases. For a value
818
+ * recorded before, it returns false, so a chunk the new build lacks as well renders the error
819
+ * component instead of a second reload. A server has no page to reload, and a page whose storage
820
+ * the browser refuses cannot record the value, so both return false.
821
+ * @param product - The product: its id names the storage key, its version is the build's, and its
822
+ * deployment is the one whose releases the page loaded.
823
+ */
824
+ function staleRecovery({ deployment, productId, version }) {
801
825
  const key = `stealth.${productId}.reloaded`;
826
+ const recorded = deployment === void 0 ? version : `${version}+${deployment}`;
802
827
  let reloading = false;
803
828
  return () => {
804
829
  if (reloading) return true;
805
- if (typeof window === "undefined" || !recordReload(key, version)) return false;
830
+ if (typeof window === "undefined" || !recordReload(key, recorded)) return false;
806
831
  reloading = true;
807
832
  window.location.reload();
808
833
  return true;
@@ -817,7 +842,8 @@ function staleRecovery({ productId, version }) {
817
842
  * failed to preload and for a module that failed to import. A cancelled event makes the import
818
843
  * resolve with no module, which the host's importers keep pending while the page reloads. An
819
844
  * event the listener does not cancel rejects the import.
820
- * @param recover - Reloads the page once per build version, and returns true while it reloads.
845
+ * @param recover - Reloads the page once per build version and deployment, and returns true while
846
+ * it reloads.
821
847
  */
822
848
  function listenForStaleChunks(recover) {
823
849
  /**
@@ -858,8 +884,13 @@ function moduleOf(module, pluginId) {
858
884
  * Imports a plugin's module through its importer, measures the plugin's first import, and reloads
859
885
  * the page where the import fails.
860
886
  *
887
+ * @remarks
888
+ * Where the page does not reload, the import rejects with a `PluginImportError`, whose `cause` is
889
+ * the import's error. TanStack's lazy route component reloads the page once more for an error
890
+ * whose message names a module that failed to fetch, and the host's message names none, so the
891
+ * route's error component renders after the one reload per build version and deployment.
861
892
  * @returns The module, or a promise that never settles while the page reloads.
862
- * @throws The import's error where the page does not reload.
893
+ * @throws {@link PluginImportError} Where the import fails and the page does not reload.
863
894
  */
864
895
  async function imported(importer, pluginId, loading) {
865
896
  const start = loading.startOf(pluginId);
@@ -868,7 +899,7 @@ async function imported(importer, pluginId, loading) {
868
899
  module = moduleOf(await importer(), pluginId);
869
900
  } catch (error) {
870
901
  if (loading.recover()) return reloading();
871
- throw error;
902
+ throw new PluginImportError(pluginId, error);
872
903
  }
873
904
  if (start !== void 0) performance.measure(`stealth:load:${pluginId}`, { start });
874
905
  return module;
@@ -886,8 +917,8 @@ function mapped(record, map) {
886
917
  return record === void 0 ? void 0 : Object.fromEntries(Object.entries(record).map(([name, value]) => [name, map(value)]));
887
918
  }
888
919
  /**
889
- * Returns a plugin's code with every importer wrapped: pages, fallbacks, extensions, commands and
890
- * settings sections.
920
+ * Returns a plugin's code with every importer wrapped: routes and their fallbacks, extensions,
921
+ * commands, settings sections and the page index.
891
922
  */
892
923
  function codeOf(code, wrap) {
893
924
  return {
@@ -899,6 +930,7 @@ function codeOf(code, wrap) {
899
930
  component: wrap(entry.component),
900
931
  fallback: maybe(entry.fallback, wrap)
901
932
  })),
933
+ pages: maybe(code.pages, wrap),
902
934
  routes: mapped(code.routes, (route) => typeof route === "function" ? wrap(route) : {
903
935
  component: wrap(route.component),
904
936
  fallback: maybe(route.fallback, wrap)
@@ -929,11 +961,11 @@ function loadingOf(product) {
929
961
  * @remarks
930
962
  * A wrapped importer measures its plugin's first import as `stealth:load:<plugin id>`, from the
931
963
  * moment the import starts until the module resolves. Where an import rejects or resolves with no
932
- * module, the page reloads once per build version, and the import never settles. A second failure
933
- * for the same version rejects, and the route's error component or the extension's boundary
934
- * renders it. `createRuntime` and `createHostRoutes` both wrap the product they receive, so a
935
- * product that passes one object to `createHost` and `createHostRoutes` shares one recovery and
936
- * one measure per plugin.
964
+ * module, the page reloads once per build version and deployment, and the import never settles. A
965
+ * second failure for the same version and deployment rejects with a `PluginImportError`, and the
966
+ * route's error component or the extension's boundary renders it. `createRuntime` and
967
+ * `createHostRoutes` both wrap the product they receive, so a product that passes one object to
968
+ * `createHost` and `createHostRoutes` shares one recovery and one measure per plugin.
937
969
  * @param product - The product, with each installed plugin's manifest.
938
970
  */
939
971
  function instrumented(product) {
@@ -1221,14 +1253,27 @@ function createAccessStore({ product, report, source }) {
1221
1253
  */
1222
1254
  const ON = { on: true };
1223
1255
  /**
1256
+ * The availability of a plugin that the computation withholds.
1257
+ */
1258
+ const WITHHELD = {
1259
+ on: false,
1260
+ reason: "unavailable"
1261
+ };
1262
+ /**
1263
+ * The empty set of withheld plugins.
1264
+ */
1265
+ const NONE$1 = /* @__PURE__ */ new Set();
1266
+ /**
1224
1267
  * Returns every installed plugin's availability, in install order.
1225
1268
  *
1226
1269
  * @remarks
1227
- * A plugin is computed on first need, so it is computed after every plugin its condition names
1228
- * and every plugin it requires, whatever the install order. A plugin on a ring of conditions and
1229
- * requirements, which the build refuses, reads as off to the plugin that asks.
1270
+ * A plugin is computed on first need, so every plugin in its condition and every plugin it
1271
+ * requires is computed before it, whatever the install order. The build refuses a ring of
1272
+ * conditions and requirements. On such a ring, `on` returns false for a plugin that is still
1273
+ * being computed. A withheld plugin is off before any test runs, so a plugin that requires it is
1274
+ * off too.
1230
1275
  */
1231
- function availabilityOf({ availability, flags, plugins, session, switches }) {
1276
+ function availabilityOf({ availability, flags, plugins, session, switches, withheld }) {
1232
1277
  const installed = new Map(plugins.map((plugin) => [plugin.id, plugin]));
1233
1278
  const known = /* @__PURE__ */ new Map();
1234
1279
  const computing = /* @__PURE__ */ new Set();
@@ -1251,6 +1296,7 @@ function availabilityOf({ availability, flags, plugins, session, switches }) {
1251
1296
  * Returns a plugin's availability: the reason of the first test it fails, else on.
1252
1297
  */
1253
1298
  const tested = (plugin) => {
1299
+ if (withheld.has(plugin.id)) return WITHHELD;
1254
1300
  if (flags.read(plugin.killSwitch) === false) return {
1255
1301
  on: false,
1256
1302
  reason: "unavailable"
@@ -1284,6 +1330,24 @@ function availabilityOf({ availability, flags, plugins, session, switches }) {
1284
1330
  return plugins.map((plugin) => [plugin.id, computed(plugin)]);
1285
1331
  }
1286
1332
  /**
1333
+ * Returns the reader of the availability inside a server snapshot: computed from the flags the
1334
+ * flag store returns there, and computed again only after the flag state it returns changed.
1335
+ *
1336
+ * @param flags - The flag store, whose state the computation is kept for.
1337
+ * @param compute - Computes every plugin's availability from the stores as they read.
1338
+ */
1339
+ function servedOf(flags, compute) {
1340
+ let served;
1341
+ return () => {
1342
+ const state = flags.get();
1343
+ if (served === void 0 || served.flags !== state) served = {
1344
+ flags: state,
1345
+ value: Object.fromEntries(compute())
1346
+ };
1347
+ return served.value;
1348
+ };
1349
+ }
1350
+ /**
1287
1351
  * Returns the availability store, computed now and again whenever the session, a flag or a switch
1288
1352
  * changes.
1289
1353
  *
@@ -1292,19 +1356,24 @@ function availabilityOf({ availability, flags, plugins, session, switches }) {
1292
1356
  * on (`unavailable`), it is locked or switched on (`off`), its condition is true (`condition`),
1293
1357
  * and every plugin it requires without `optional` is on (`requirement`). A computation that
1294
1358
  * changes no plugin's state publishes nothing. A plugin whose reason changes while it remains
1295
- * off is published without a call to `changed`.
1359
+ * off is published without a call to `changed`. Inside a server snapshot the store returns the
1360
+ * availability that it computes from the server render's flags, so a boundary that hydrates late
1361
+ * renders what the server rendered. The server renders a page without the plugins of its
1362
+ * releases, so inside a server snapshot each release plugin is off with the reason `unavailable`.
1296
1363
  */
1297
1364
  function createAvailabilityStore({ changed, flags, product, session, switches }) {
1298
1365
  const availability = writable({});
1299
1366
  /**
1300
- * Returns every plugin's availability as the stores are now.
1367
+ * Returns every plugin's availability as the stores are now, with every plugin in `withheld`
1368
+ * off.
1301
1369
  */
1302
- const compute = () => availabilityOf({
1370
+ const compute = (withheld = NONE$1) => availabilityOf({
1303
1371
  availability,
1304
1372
  flags,
1305
1373
  plugins: product.plugins,
1306
1374
  session,
1307
- switches: switches.get()
1375
+ switches: switches.get(),
1376
+ withheld
1308
1377
  });
1309
1378
  const first = compute();
1310
1379
  let states = new Map(first.map(([pluginId, state]) => [pluginId, state.on]));
@@ -1330,15 +1399,92 @@ function createAvailabilityStore({ changed, flags, product, session, switches })
1330
1399
  flags.subscribe(recompute),
1331
1400
  switches.subscribe(recompute)
1332
1401
  ];
1402
+ const releases = new Set(product.releases);
1403
+ const served = servedOf(flags, () => compute(releases));
1333
1404
  return {
1334
1405
  dispose: () => {
1335
1406
  for (const stop of stops) stop();
1336
1407
  },
1337
- get: availability.get,
1408
+ get: () => inServerSnapshot() ? served() : availability.get(),
1338
1409
  subscribe: availability.subscribe
1339
1410
  };
1340
1411
  }
1341
1412
  //#endregion
1413
+ //#region src/stores/flag-overrides.ts
1414
+ /**
1415
+ * Returns true where a value fits a flag: a boolean for a release or an ops flag, one of its
1416
+ * variants for an experiment.
1417
+ */
1418
+ function fits(flag, value) {
1419
+ return flag.type === "boolean" ? typeof value === "boolean" : typeof value === "string" && flag.variants?.includes(value) === true;
1420
+ }
1421
+ /**
1422
+ * Returns true where a parsed value is an object whose members can be read by name.
1423
+ */
1424
+ function isRecord$1(value) {
1425
+ return typeof value === "object" && value !== null;
1426
+ }
1427
+ /**
1428
+ * Returns the object stored text contains, or an empty object for no text, for text that is not
1429
+ * JSON, and for JSON that is not an object.
1430
+ */
1431
+ function parsed(text) {
1432
+ if (text === null) return {};
1433
+ try {
1434
+ const value = JSON.parse(text);
1435
+ return isRecord$1(value) ? value : {};
1436
+ } catch {
1437
+ return {};
1438
+ }
1439
+ }
1440
+ /**
1441
+ * Returns the overrides the tab's storage contains that fit a declared flag, and queues
1442
+ * `flag-ignored` for each that does not fit. An override for a flag no installed plugin declares is
1443
+ * left out without a report.
1444
+ *
1445
+ * @param storage - The tab's session storage. No override is read without it.
1446
+ * @param key - The key the overrides are kept under.
1447
+ * @param declared - The declared flags, by qualified id.
1448
+ * @param queue - Receives `flag-ignored` for an override that does not fit its flag.
1449
+ */
1450
+ function storedOverrides(storage, key, declared, queue) {
1451
+ const kept = [];
1452
+ for (const [id, value] of Object.entries(parsed(storage?.getItem(key) ?? null))) {
1453
+ const flag = declared.get(id);
1454
+ if (flag === void 0) continue;
1455
+ if (fits(flag, value)) kept.push([id, value]);
1456
+ else queue({
1457
+ flag: id,
1458
+ kind: "flag-ignored",
1459
+ source: "override",
1460
+ value
1461
+ });
1462
+ }
1463
+ return Object.fromEntries(kept);
1464
+ }
1465
+ /**
1466
+ * Writes the overrides to the tab's storage, or removes the key where none is left.
1467
+ *
1468
+ * @returns False where the storage refused the write. The overrides then last until the page
1469
+ * reloads.
1470
+ */
1471
+ function written(storage, key, overrides) {
1472
+ try {
1473
+ if (Object.keys(overrides).length === 0) storage.removeItem(key);
1474
+ else storage.setItem(key, JSON.stringify(overrides));
1475
+ return true;
1476
+ } catch {
1477
+ return false;
1478
+ }
1479
+ }
1480
+ /**
1481
+ * Returns the overrides with a flag's override replaced, or removed where no value is given.
1482
+ */
1483
+ function withOverride(overrides, id, value) {
1484
+ const kept = Object.entries(overrides).filter(([flag]) => flag !== id);
1485
+ return Object.fromEntries(value === void 0 ? kept : [...kept, [id, value]]);
1486
+ }
1487
+ //#endregion
1342
1488
  //#region src/stores/flag-session.ts
1343
1489
  /**
1344
1490
  * Returns the record of the variants served, for no subject yet.
@@ -1416,13 +1562,6 @@ function identityOf(source, failed) {
1416
1562
  * variant the person saw.
1417
1563
  */
1418
1564
  /**
1419
- * Returns true where a value fits a flag: a boolean for a release or an ops flag, one of its
1420
- * variants for an experiment.
1421
- */
1422
- function fits(flag, value) {
1423
- return flag.type === "boolean" ? typeof value === "boolean" : typeof value === "string" && flag.variants?.includes(value) === true;
1424
- }
1425
- /**
1426
1565
  * Returns the flag source's value for a flag, or undefined where the source states none or throws.
1427
1566
  * A throw queues `flags-failed`.
1428
1567
  */
@@ -1525,67 +1664,6 @@ function publishedOf(report) {
1525
1664
  };
1526
1665
  }
1527
1666
  /**
1528
- * Returns true where a parsed value is an object whose members can be read by name.
1529
- */
1530
- function isRecord$1(value) {
1531
- return typeof value === "object" && value !== null;
1532
- }
1533
- /**
1534
- * Returns the object stored text contains, or an empty object for no text, for text that is not
1535
- * JSON, and for JSON that is not an object.
1536
- */
1537
- function parsed(text) {
1538
- if (text === null) return {};
1539
- try {
1540
- const value = JSON.parse(text);
1541
- return isRecord$1(value) ? value : {};
1542
- } catch {
1543
- return {};
1544
- }
1545
- }
1546
- /**
1547
- * Returns the overrides the tab's storage contains that fit a declared flag, and queues
1548
- * `flag-ignored` for each that does not fit. An override for a flag no installed plugin declares is
1549
- * left out without a report.
1550
- */
1551
- function storedOverrides(storage, key, declared, queue) {
1552
- const kept = [];
1553
- for (const [id, value] of Object.entries(parsed(storage?.getItem(key) ?? null))) {
1554
- const flag = declared.get(id);
1555
- if (flag === void 0) continue;
1556
- if (fits(flag, value)) kept.push([id, value]);
1557
- else queue({
1558
- flag: id,
1559
- kind: "flag-ignored",
1560
- source: "override",
1561
- value
1562
- });
1563
- }
1564
- return Object.fromEntries(kept);
1565
- }
1566
- /**
1567
- * Writes the overrides to the tab's storage, or removes the key where none is left.
1568
- *
1569
- * @returns False where the storage refused the write. The overrides then last until the page
1570
- * reloads.
1571
- */
1572
- function written(storage, key, overrides) {
1573
- try {
1574
- if (Object.keys(overrides).length === 0) storage.removeItem(key);
1575
- else storage.setItem(key, JSON.stringify(overrides));
1576
- return true;
1577
- } catch {
1578
- return false;
1579
- }
1580
- }
1581
- /**
1582
- * Returns the overrides with a flag's override replaced, or removed where the value is undefined.
1583
- */
1584
- function withOverride(overrides, id, value) {
1585
- const kept = Object.entries(overrides).filter(([flag]) => flag !== id);
1586
- return Object.fromEntries(value === void 0 ? kept : [...kept, [id, value]]);
1587
- }
1588
- /**
1589
1667
  * Returns the store's `read`: the tab's override, else the reading the page took, else a first
1590
1668
  * evaluation, published at the end of the task.
1591
1669
  */
@@ -1664,20 +1742,28 @@ function hydrationOf(shared, reevaluate, waiting) {
1664
1742
  const { declared, exposures, published } = shared;
1665
1743
  let active = false;
1666
1744
  let aside = {};
1745
+ let served;
1667
1746
  return {
1668
1747
  active: () => active,
1669
1748
  hydrate: (readings, subject) => {
1749
+ const kept = /* @__PURE__ */ new Map();
1670
1750
  active = true;
1671
1751
  aside = published.state.get().overrides;
1672
1752
  published.setOverrides({});
1673
1753
  for (const [id, reading] of readings) {
1674
1754
  const flag = declared.get(id);
1675
1755
  if (flag === void 0 || !fits(flag, reading.value)) continue;
1756
+ kept.set(id, reading);
1676
1757
  published.readings.set(id, reading);
1677
1758
  exposures.serve(subject, flag, reading.value);
1678
1759
  }
1760
+ served = {
1761
+ overrides: {},
1762
+ readings: kept
1763
+ };
1679
1764
  published.flush();
1680
1765
  },
1766
+ served: () => served,
1681
1767
  settle: () => {
1682
1768
  if (!active) return;
1683
1769
  active = false;
@@ -1688,6 +1774,28 @@ function hydrationOf(shared, reevaluate, waiting) {
1688
1774
  };
1689
1775
  }
1690
1776
  /**
1777
+ * Returns the store's `get` and `read`, which return what the server render read inside a server
1778
+ * snapshot after a hydration, and the published state outside one.
1779
+ *
1780
+ * @remarks
1781
+ * A boundary can hydrate after `settle`, once the store took the browser's values. React reads
1782
+ * its components' flags as a server snapshot, so the boundary hydrates against the server's HTML
1783
+ * and renders the browser's values after it. A flag the server render did not read is evaluated
1784
+ * as outside a server snapshot.
1785
+ * @param hydration - The store's hydration, which keeps the server render's state.
1786
+ * @param published - The store's published state.
1787
+ * @param read - The store's reader outside a server snapshot.
1788
+ */
1789
+ function servedReadersOf(hydration, published, read) {
1790
+ return {
1791
+ get: () => (inServerSnapshot() ? hydration.served() : void 0) ?? published.state.get(),
1792
+ read: (id) => {
1793
+ const served = inServerSnapshot() ? hydration.served()?.readings.get(id) : void 0;
1794
+ return served === void 0 ? read(id) : served.value;
1795
+ }
1796
+ };
1797
+ }
1798
+ /**
1691
1799
  * Returns the flag store over the product's flags, its flag source and the tab's storage.
1692
1800
  *
1693
1801
  * @remarks
@@ -1698,7 +1806,8 @@ function hydrationOf(shared, reevaluate, waiting) {
1698
1806
  * the flags the page has read, and the previous values remain while `identify` is pending.
1699
1807
  * `flag-exposed` is reported the first time the session's subject is served each variant of an
1700
1808
  * experiment. An override counts no exposure. A store a server render hydrated reads the
1701
- * server's readings until `settle`, and evaluates again only then.
1809
+ * server's readings until `settle`, and evaluates again only then. Inside a server snapshot it
1810
+ * returns the server's readings after `settle` too.
1702
1811
  */
1703
1812
  function createFlagStore({ overrides: storage, product, report, source }) {
1704
1813
  const published = publishedOf(report);
@@ -1723,19 +1832,21 @@ function createFlagStore({ overrides: storage, product, report, source }) {
1723
1832
  const stop = source?.subscribe(() => {
1724
1833
  if (shared.identity.trusted() !== void 0 && !hydration.active()) reevaluate();
1725
1834
  });
1835
+ const readers = servedReadersOf(hydration, published, readerOf(shared));
1726
1836
  return {
1727
1837
  dispose: () => {
1728
1838
  stop?.();
1729
1839
  shared.identity.stop();
1730
1840
  },
1731
- get: published.state.get,
1841
+ get: readers.get,
1732
1842
  hydrate: hydration.hydrate,
1733
1843
  identify: async (session) => {
1734
1844
  shared.exposures.reset(subjectOf(session));
1735
1845
  if (await shared.identity.identify(session) && !hydration.active()) reevaluate();
1736
1846
  },
1847
+ overridable: storage !== void 0,
1737
1848
  override: overriderOf(shared, storage, key),
1738
- read: readerOf(shared),
1849
+ read: readers.read,
1739
1850
  settle: hydration.settle,
1740
1851
  snapshot: () => [...published.readings],
1741
1852
  subscribe: published.state.subscribe
@@ -2249,12 +2360,33 @@ function createHost(options) {
2249
2360
  connect: connection.connect,
2250
2361
  flags,
2251
2362
  glyphs: options.glyphs,
2363
+ loading: page.loading,
2252
2364
  runtime,
2253
2365
  switches: state.switches
2254
2366
  });
2255
2367
  return host;
2256
2368
  }
2257
2369
  //#endregion
2370
+ //#region src/parts/caught.ts
2371
+ /**
2372
+ * Writes an error that a React error boundary caught to the console, and leaves the router's
2373
+ * not-found out.
2374
+ */
2375
+ /**
2376
+ * Writes an error that an error boundary caught to the console, as React's default does in a
2377
+ * production build, and returns without writing for the router's not-found.
2378
+ *
2379
+ * @remarks
2380
+ * A page throws the router's not-found for parameters it cannot resolve, and the router's
2381
+ * boundary renders the product's not-found page in the page's place. React reports every error a
2382
+ * boundary catches, so a root without this handler writes each such not-found to the console. A
2383
+ * product passes the handler to `createRoot` and `hydrateRoot` as `onCaughtError`.
2384
+ */
2385
+ function onCaughtError(error) {
2386
+ if (isNotFound(error)) return;
2387
+ console.error(error);
2388
+ }
2389
+ //#endregion
2258
2390
  //#region src/parts/content.tsx
2259
2391
  /**
2260
2392
  * Renders the root route's outlet inside the `content` region, so the region's extensions decorate
@@ -2277,89 +2409,107 @@ function HostContent() {
2277
2409
  return t0;
2278
2410
  }
2279
2411
  //#endregion
2280
- //#region src/routes/not-found.ts
2281
- /**
2282
- * Returns true for the data of a page whose plugin a switch or a kill switch stopped.
2283
- */
2284
- function isStopped(data) {
2285
- return "plugin" in data && typeof data.plugin === "string" && "reason" in data && (data.reason === "off" || data.reason === "unavailable");
2286
- }
2412
+ //#region src/host/use-internals.ts
2287
2413
  /**
2288
- * Returns true for the data of a page the host quarantined.
2414
+ * Reads the internals of the host that the router's context contains, for the host's own parts.
2289
2415
  */
2290
- function isQuarantined(data) {
2291
- return "target" in data && typeof data.target === "string" && data.target.startsWith("route:") && "reason" in data && data.reason === "quarantined";
2292
- }
2293
2416
  /**
2294
- * Reads the data of a not-found error as the host's not-found data.
2417
+ * Returns the internals of the host that the router's context contains.
2295
2418
  *
2296
- * @remarks
2297
- * Every route of the tree can throw a not-found error, so data of another shape, from a route the
2298
- * product wrote, reads as none.
2299
- * @param data - The data the router passes the not-found component.
2300
- * @returns The data the route evaluator threw, or nothing.
2419
+ * @throws {@link Error} Where `createHost` did not create the host.
2301
2420
  */
2302
- function notFoundDataOf(data) {
2303
- if (typeof data !== "object" || data === null) return void 0;
2304
- return isStopped(data) || isQuarantined(data) ? data : void 0;
2421
+ function useInternals() {
2422
+ const $ = c(3);
2423
+ let t0;
2424
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
2425
+ t0 = { strict: false };
2426
+ $[0] = t0;
2427
+ } else t0 = $[0];
2428
+ const { host } = useRouteContext(t0);
2429
+ let t1;
2430
+ if ($[1] !== host) {
2431
+ t1 = internalsOf(host);
2432
+ $[1] = host;
2433
+ $[2] = t1;
2434
+ } else t1 = $[2];
2435
+ return t1;
2305
2436
  }
2306
2437
  //#endregion
2307
- //#region src/parts/not-found.tsx
2438
+ //#region src/parts/loading.tsx
2308
2439
  /**
2309
- * Renders the root's not-found page: for a page whose plugin a switch or a kill switch stopped, for
2310
- * a page the host quarantined, and for an address no route matches.
2440
+ * Renders the host's loading page. The loading page takes the place of a page while the router
2441
+ * loads that page.
2311
2442
  */
2312
2443
  /**
2313
- * Returns the ref of the page's heading, which takes focus where the page replaced the page a
2314
- * person was on without a navigation.
2444
+ * Renders a busy panel with a spinner above a level-one heading.
2315
2445
  *
2316
2446
  * @remarks
2317
- * The router records an address as resolved only after the pages of its navigation rendered, so
2318
- * the resolved address equals the address while the page first renders only where no navigation
2319
- * led to it: a switch, a kill switch, a condition or a quarantine changed under the person.
2447
+ * The spinner does not have a role, so a screen reader announces the state from the heading. The
2448
+ * panel sets `aria-busy` while it renders. The host records the panel as on screen from its
2449
+ * commit until it unmounts, so the page that replaces it renders as the first page of the load.
2320
2450
  */
2321
- function useHeading() {
2322
- const $ = c(6);
2323
- const heading = useRef(null);
2324
- const router = useRouter();
2451
+ function HostLoading() {
2452
+ const $ = c(8);
2453
+ const { t } = useTranslation("host");
2454
+ const { loading } = useInternals();
2325
2455
  let t0;
2326
- if ($[0] !== router.state.location.href || $[1] !== router.state.resolvedLocation?.href) {
2327
- t0 = () => router.state.resolvedLocation?.href === router.state.location.href;
2328
- $[0] = router.state.location.href;
2329
- $[1] = router.state.resolvedLocation?.href;
2330
- $[2] = t0;
2331
- } else t0 = $[2];
2332
- const [replaced] = useState(t0);
2333
2456
  let t1;
2334
- let t2;
2335
- if ($[3] !== replaced) {
2336
- t1 = () => {
2337
- if (replaced) heading.current?.focus();
2457
+ if ($[0] !== loading) {
2458
+ t0 = () => {
2459
+ loading.set(true);
2460
+ return () => {
2461
+ loading.set(false);
2462
+ };
2338
2463
  };
2339
- t2 = [replaced];
2340
- $[3] = replaced;
2341
- $[4] = t1;
2342
- $[5] = t2;
2464
+ t1 = [loading];
2465
+ $[0] = loading;
2466
+ $[1] = t0;
2467
+ $[2] = t1;
2343
2468
  } else {
2344
- t1 = $[4];
2345
- t2 = $[5];
2469
+ t0 = $[1];
2470
+ t1 = $[2];
2346
2471
  }
2347
- useEffect(t1, t2);
2348
- return heading;
2472
+ useEffect(t0, t1);
2473
+ let t2;
2474
+ if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
2475
+ t2 = /* @__PURE__ */ jsx(EmptyState.Indicator, { children: /* @__PURE__ */ jsx(Spinner, { size: "2xl" }) });
2476
+ $[3] = t2;
2477
+ } else t2 = $[3];
2478
+ let t3;
2479
+ if ($[4] !== t) {
2480
+ t3 = t("page.loading.title");
2481
+ $[4] = t;
2482
+ $[5] = t3;
2483
+ } else t3 = $[5];
2484
+ let t4;
2485
+ if ($[6] !== t3) {
2486
+ t4 = /* @__PURE__ */ jsx(EmptyState.Root, {
2487
+ "aria-busy": "true",
2488
+ children: /* @__PURE__ */ jsxs(EmptyState.Content, { children: [t2, /* @__PURE__ */ jsx(EmptyState.Title, {
2489
+ as: "h1",
2490
+ children: t3
2491
+ })] })
2492
+ });
2493
+ $[6] = t3;
2494
+ $[7] = t4;
2495
+ } else t4 = $[7];
2496
+ return t4;
2349
2497
  }
2498
+ //#endregion
2499
+ //#region src/parts/not-found-page.tsx
2350
2500
  /**
2351
- * Renders the not-found page the router's data describes.
2501
+ * Renders the not-found page that the data describes.
2352
2502
  *
2353
2503
  * @remarks
2354
- * A page whose plugin a switch turned off names the plugin and offers a button that turns it on.
2355
- * A page whose plugin a kill switch stopped names the plugin and offers nothing. A quarantined
2356
- * page offers a retry that lifts the quarantine. Data of another shape, or none, renders a plain
2357
- * not-found page, which states nothing about the address. The host runs the router's
2358
- * `beforeLoad` again after a switch or a retry, and the page renders again in this one's place.
2504
+ * A page whose plugin a switch turned off shows the plugin's name and a button that turns the
2505
+ * plugin on. A page whose plugin a kill switch stopped shows the plugin's name alone. A
2506
+ * quarantined page shows a retry that lifts the quarantine. A plain not-found page does not
2507
+ * describe the address. The host runs the router's `beforeLoad` again after a switch or a retry,
2508
+ * and the page renders again in place of this one.
2359
2509
  */
2360
- function HostNotFound(t0) {
2510
+ function NotFoundPage(t0) {
2361
2511
  const $ = c(25);
2362
- const { data } = t0;
2512
+ const { found, heading } = t0;
2363
2513
  let t1;
2364
2514
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
2365
2515
  t1 = { strict: false };
@@ -2367,16 +2517,13 @@ function HostNotFound(t0) {
2367
2517
  } else t1 = $[0];
2368
2518
  const { host } = useRouteContext(t1);
2369
2519
  const { i18n, t } = useTranslation("host");
2370
- const heading = useHeading();
2520
+ const reason = found?.reason ?? "plain";
2371
2521
  let T0;
2372
2522
  let T1;
2373
- let found;
2374
2523
  let t2;
2375
2524
  let t3;
2376
2525
  let t4;
2377
- if ($[1] !== data || $[2] !== heading || $[3] !== host || $[4] !== i18n || $[5] !== t) {
2378
- found = notFoundDataOf(data);
2379
- const reason = found?.reason ?? "plain";
2526
+ if ($[1] !== found || $[2] !== heading || $[3] !== host || $[4] !== i18n || $[5] !== reason || $[6] !== t) {
2380
2527
  const plugin = found === void 0 || found.reason === "quarantined" ? "" : pluginWordsOf(i18n).t(found.plugin, "plugin.name");
2381
2528
  T1 = EmptyState.Root;
2382
2529
  T0 = EmptyState.Content;
@@ -2393,21 +2540,20 @@ function HostNotFound(t0) {
2393
2540
  },
2394
2541
  children: t("notFound.off.action", { plugin })
2395
2542
  }) : null;
2396
- $[1] = data;
2543
+ $[1] = found;
2397
2544
  $[2] = heading;
2398
2545
  $[3] = host;
2399
2546
  $[4] = i18n;
2400
- $[5] = t;
2401
- $[6] = T0;
2402
- $[7] = T1;
2403
- $[8] = found;
2547
+ $[5] = reason;
2548
+ $[6] = t;
2549
+ $[7] = T0;
2550
+ $[8] = T1;
2404
2551
  $[9] = t2;
2405
2552
  $[10] = t3;
2406
2553
  $[11] = t4;
2407
2554
  } else {
2408
- T0 = $[6];
2409
- T1 = $[7];
2410
- found = $[8];
2555
+ T0 = $[7];
2556
+ T1 = $[8];
2411
2557
  t2 = $[9];
2412
2558
  t3 = $[10];
2413
2559
  t4 = $[11];
@@ -2450,16 +2596,206 @@ function HostNotFound(t0) {
2450
2596
  return t7;
2451
2597
  }
2452
2598
  //#endregion
2453
- //#region src/parts/connection.ts
2599
+ //#region src/routes/not-found.ts
2454
2600
  /**
2455
- * Returns the qualified ids of the declared routes the router matched, outermost first.
2456
- *
2457
- * @remarks
2458
- * The root and every route the product wrote without a name state no id, so the list leaves them
2459
- * out.
2601
+ * Returns true for the data of a page whose plugin a switch or a kill switch stopped.
2460
2602
  */
2461
- function matchedOf(router) {
2462
- return router.state.matches.flatMap((match) => declaredOf(match)?.id ?? []);
2603
+ function isStopped(data) {
2604
+ return "plugin" in data && typeof data.plugin === "string" && "reason" in data && (data.reason === "off" || data.reason === "unavailable");
2605
+ }
2606
+ /**
2607
+ * Returns true for the data of a page the host quarantined.
2608
+ */
2609
+ function isQuarantined(data) {
2610
+ return "target" in data && typeof data.target === "string" && data.target.startsWith("route:") && "reason" in data && data.reason === "quarantined";
2611
+ }
2612
+ /**
2613
+ * Reads the data of a not-found error as the host's not-found data.
2614
+ *
2615
+ * @remarks
2616
+ * Every route of the tree can throw a not-found error, so data of another shape, from a route the
2617
+ * product wrote, reads as none.
2618
+ * @param data - The data the router passes the not-found component.
2619
+ * @returns The data the route evaluator threw, or nothing.
2620
+ */
2621
+ function notFoundDataOf(data) {
2622
+ if (typeof data !== "object" || data === null) return void 0;
2623
+ return isStopped(data) || isQuarantined(data) ? data : void 0;
2624
+ }
2625
+ //#endregion
2626
+ //#region src/routes/release-reload.ts
2627
+ /**
2628
+ * Loads the address again after a hydration where the page's document contains a deployment.
2629
+ *
2630
+ * @remarks
2631
+ * A server renders a page without the routes of its releases. Where only a route of a release
2632
+ * matches the address, the server renders the not-found page. The router's hydration commits that
2633
+ * page as final and does not load the address. The page loads the address again once it hydrated.
2634
+ */
2635
+ /**
2636
+ * Returns true while a plain not-found page waits for the router to load the address again with
2637
+ * the routes of the page's releases.
2638
+ *
2639
+ * @remarks
2640
+ * The wait starts where the page renders on a server or hydrates, and the page's document
2641
+ * contains a deployment. After the page hydrated, the router loads the address again. The wait
2642
+ * ends when that load ends. A route of a release that matches the address then renders in place
2643
+ * of the not-found page.
2644
+ * @param plain - True where the not-found page does not have data of the host's own.
2645
+ */
2646
+ function useReleaseReload(plain) {
2647
+ const $ = c(8);
2648
+ const { product } = useInternals().runtime;
2649
+ const router = useRouter();
2650
+ const hydrating = useHydrating();
2651
+ let t0;
2652
+ if ($[0] !== hydrating || $[1] !== plain || $[2] !== product) {
2653
+ t0 = () => plain && hydrating && product.releases !== void 0;
2654
+ $[0] = hydrating;
2655
+ $[1] = plain;
2656
+ $[2] = product;
2657
+ $[3] = t0;
2658
+ } else t0 = $[3];
2659
+ const [reloading, setReloading] = useState(t0);
2660
+ let t1;
2661
+ let t2;
2662
+ if ($[4] !== reloading || $[5] !== router) {
2663
+ t1 = () => {
2664
+ if (!reloading) return;
2665
+ const ended = () => {
2666
+ setReloading(false);
2667
+ };
2668
+ router.invalidate().then(ended, ended);
2669
+ };
2670
+ t2 = [reloading, router];
2671
+ $[4] = reloading;
2672
+ $[5] = router;
2673
+ $[6] = t1;
2674
+ $[7] = t2;
2675
+ } else {
2676
+ t1 = $[6];
2677
+ t2 = $[7];
2678
+ }
2679
+ useEffect(t1, t2);
2680
+ return reloading;
2681
+ }
2682
+ //#endregion
2683
+ //#region src/routes/replacement.ts
2684
+ /**
2685
+ * Moves focus to a page that renders in place of the page a person is on, where focus moved to the
2686
+ * document's body.
2687
+ */
2688
+ /**
2689
+ * Moves focus to the page when it renders in place of the page a person is on without a
2690
+ * navigation, and focus is on the document's body.
2691
+ *
2692
+ * @remarks
2693
+ * The router records an address as resolved only after the pages of its navigation rendered.
2694
+ * While the page first renders, the resolved address equals the address only where a switch, a
2695
+ * kill switch, a condition, a quarantine or a retry changed the page without a navigation. The
2696
+ * page takes focus only after the content region showed a page, so the first page of a load does
2697
+ * not take focus. A page that replaces the host's loading page is the first page of its load too.
2698
+ * A page does not take focus while it hydrates. Focus moves to the document's body when the
2699
+ * element that has it unmounts. The page takes focus from the body alone, so a control outside
2700
+ * the page keeps it. The element that takes focus gets a tab index of `-1` where it has none, so
2701
+ * it takes focus without a tab stop.
2702
+ * @param heading - The page's own heading. Without it, the first level-one heading of the main
2703
+ * region takes focus, else the main region.
2704
+ */
2705
+ function useReplacementFocus(heading) {
2706
+ const $ = c(11);
2707
+ let t0;
2708
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
2709
+ t0 = { strict: false };
2710
+ $[0] = t0;
2711
+ } else t0 = $[0];
2712
+ const { host } = useRouteContext(t0);
2713
+ const router = useRouter();
2714
+ const hydrating = useHydrating();
2715
+ let t1;
2716
+ if ($[1] !== host || $[2] !== hydrating || $[3] !== router) {
2717
+ t1 = () => !hydrating && !internalsOf(host).loading.get() && host.stores.mounted.get().has(hostContract.slots.content.id) && router.state.resolvedLocation?.href === router.state.location.href;
2718
+ $[1] = host;
2719
+ $[2] = hydrating;
2720
+ $[3] = router;
2721
+ $[4] = t1;
2722
+ } else t1 = $[4];
2723
+ const [replaced] = useState(t1);
2724
+ let t2;
2725
+ if ($[5] !== heading?.current || $[6] !== replaced) {
2726
+ t2 = () => {
2727
+ if (!replaced || document.activeElement !== document.body) return;
2728
+ const main = document.querySelector("main");
2729
+ const target = heading?.current ?? main?.querySelector("h1") ?? main;
2730
+ target?.setAttribute("tabindex", target.getAttribute("tabindex") ?? "-1");
2731
+ target?.focus();
2732
+ };
2733
+ $[5] = heading?.current;
2734
+ $[6] = replaced;
2735
+ $[7] = t2;
2736
+ } else t2 = $[7];
2737
+ let t3;
2738
+ if ($[8] !== heading || $[9] !== replaced) {
2739
+ t3 = [heading, replaced];
2740
+ $[8] = heading;
2741
+ $[9] = replaced;
2742
+ $[10] = t3;
2743
+ } else t3 = $[10];
2744
+ useEffect(t2, t3);
2745
+ }
2746
+ //#endregion
2747
+ //#region src/parts/not-found.tsx
2748
+ /**
2749
+ * Renders the root's not-found page: for a page whose plugin a switch or a kill switch stopped, for
2750
+ * a page the host quarantined, and for an address no route matches.
2751
+ */
2752
+ /**
2753
+ * Renders the not-found page that the router's data describes, or the loading page while the
2754
+ * router loads the address again with the routes of the page's releases.
2755
+ *
2756
+ * @remarks
2757
+ * The page treats the router's data as the host's own where the route evaluator threw it, and
2758
+ * ignores any other data. Where the page's document contains a deployment, a plain
2759
+ * not-found page renders the loading page on a server and while it hydrates. It renders the
2760
+ * loading page until the router loaded the address again. The heading takes focus where the page
2761
+ * renders in place of the page a person is on.
2762
+ */
2763
+ function HostNotFound(t0) {
2764
+ const $ = c(5);
2765
+ const { data } = t0;
2766
+ const heading = useRef(null);
2767
+ let t1;
2768
+ if ($[0] !== data) {
2769
+ t1 = notFoundDataOf(data);
2770
+ $[0] = data;
2771
+ $[1] = t1;
2772
+ } else t1 = $[1];
2773
+ const found = t1;
2774
+ const reloading = useReleaseReload(found === void 0);
2775
+ useReplacementFocus(heading);
2776
+ let t2;
2777
+ if ($[2] !== found || $[3] !== reloading) {
2778
+ t2 = reloading ? /* @__PURE__ */ jsx(HostLoading, {}) : /* @__PURE__ */ jsx(NotFoundPage, {
2779
+ found,
2780
+ heading
2781
+ });
2782
+ $[2] = found;
2783
+ $[3] = reloading;
2784
+ $[4] = t2;
2785
+ } else t2 = $[4];
2786
+ return t2;
2787
+ }
2788
+ //#endregion
2789
+ //#region src/parts/connection.ts
2790
+ /**
2791
+ * Returns the qualified ids of the declared routes the router matched, outermost first.
2792
+ *
2793
+ * @remarks
2794
+ * The root and every route the product wrote without a name state no id, so the list leaves them
2795
+ * out.
2796
+ */
2797
+ function matchedOf(router) {
2798
+ return router.state.matches.flatMap((match) => declaredOf(match)?.id ?? []);
2463
2799
  }
2464
2800
  /**
2465
2801
  * Returns the connection of a router and the catalogues' instance.
@@ -2709,7 +3045,7 @@ function HostFailed(t0) {
2709
3045
  let t6;
2710
3046
  if ($[11] !== t5) {
2711
3047
  t6 = /* @__PURE__ */ jsx(Button, {
2712
- onClick: _temp$4,
3048
+ onClick: _temp$6,
2713
3049
  children: t5
2714
3050
  });
2715
3051
  $[11] = t5;
@@ -2732,7 +3068,7 @@ function HostFailed(t0) {
2732
3068
  } else t7 = $[16];
2733
3069
  return t7;
2734
3070
  }
2735
- function _temp$4() {
3071
+ function _temp$6() {
2736
3072
  window.location.reload();
2737
3073
  }
2738
3074
  //#endregion
@@ -2938,7 +3274,7 @@ function CommandKeys() {
2938
3274
  const [hotkey, bound] = t1;
2939
3275
  return {
2940
3276
  callback: () => {
2941
- const first = bound.find(_temp$3);
3277
+ const first = bound.find(_temp$5);
2942
3278
  if (first !== void 0) runReporting(first, runtime, t("commands.failed", { label: first.label }));
2943
3279
  },
2944
3280
  hotkey
@@ -2946,20 +3282,305 @@ function CommandKeys() {
2946
3282
  }));
2947
3283
  return null;
2948
3284
  }
2949
- function _temp$3(t0) {
3285
+ function _temp$5(t0) {
2950
3286
  const { enabled } = t0;
2951
3287
  return enabled;
2952
3288
  }
2953
3289
  //#endregion
2954
- //#region src/commands/palette.tsx
3290
+ //#region src/commands/palette-state.ts
2955
3291
  /**
2956
- * Renders the command palette: the commands a key could run and the main menu's pages, in a dialog
2957
- * `Mod+K` opens.
3292
+ * Shares the command palette's open state between the palette and the controls of the frame.
2958
3293
  */
2959
3294
  /**
2960
- * The chord that opens the palette, which the build refuses to a command.
3295
+ * Provides the palette's open state to the palette and the frame. `HostRoot` provides it, and the
3296
+ * value is undefined outside `HostRoot`.
2961
3297
  */
2962
- const PALETTE = "Mod+K";
3298
+ const PaletteContext = createContext(void 0);
3299
+ /**
3300
+ * Returns the palette's open state, so a control in the frame opens the palette that `Mod+K` opens.
3301
+ *
3302
+ * @throws {@link Error} Outside `HostRoot`.
3303
+ */
3304
+ function usePalette() {
3305
+ const state = use(PaletteContext);
3306
+ if (state === void 0) throw new Error("usePalette() found no HostRoot. The frame and the palette render inside HostRoot, which provides the palette's state.");
3307
+ return state;
3308
+ }
3309
+ //#endregion
3310
+ //#region src/app/listed.ts
3311
+ /**
3312
+ * Reads which routes the person may open, with or without a menu entry, and the menus and settings
3313
+ * pages a sidebar lists from them.
3314
+ */
3315
+ /**
3316
+ * Returns the routes whose plugin is on and whose condition is true, with or without a menu entry.
3317
+ *
3318
+ * @param routes - The product's routes.
3319
+ * @param stores - The stores that a condition depends on.
3320
+ */
3321
+ function openableRoutesOf(routes, stores) {
3322
+ const context = conditionContextOf(stores);
3323
+ return routes.filter(({ plugin, when }) => context.on(plugin) && evaluateWhen(when, context));
3324
+ }
3325
+ /**
3326
+ * Returns the routes with a menu entry whose plugin is on and whose condition is true.
3327
+ *
3328
+ * @param routes - The product's routes.
3329
+ * @param stores - The stores that a condition depends on.
3330
+ */
3331
+ function listedRoutesOf(routes, stores) {
3332
+ return openableRoutesOf(routes, stores).filter(({ navigation }) => navigation !== void 0);
3333
+ }
3334
+ /**
3335
+ * Returns the ids of the routes a selection returns, and renders again when a plugin turns on or
3336
+ * off, a flag changes or the session changes.
3337
+ *
3338
+ * @remarks
3339
+ * The snapshot is the ids joined by spaces. A qualified id does not contain a space, so a
3340
+ * component does not render again after a change that selects the same routes. The server
3341
+ * snapshot runs the selection inside `asServerSnapshot`, so a component hydrates with the routes
3342
+ * that the server render selected.
3343
+ */
3344
+ function useSelected(select) {
3345
+ const $ = c(14);
3346
+ const { product, stores } = useInternals().runtime;
3347
+ const { availability, flags, session } = stores;
3348
+ let t0;
3349
+ if ($[0] !== availability || $[1] !== flags || $[2] !== session) {
3350
+ t0 = (changed) => {
3351
+ const stops = [
3352
+ availability,
3353
+ flags,
3354
+ session
3355
+ ].map((store) => store.subscribe(changed));
3356
+ return () => {
3357
+ for (const stop of stops) stop();
3358
+ };
3359
+ };
3360
+ $[0] = availability;
3361
+ $[1] = flags;
3362
+ $[2] = session;
3363
+ $[3] = t0;
3364
+ } else t0 = $[3];
3365
+ const subscribe = t0;
3366
+ let t1;
3367
+ if ($[4] !== availability || $[5] !== flags || $[6] !== product.routes || $[7] !== select || $[8] !== session) {
3368
+ t1 = () => select(product.routes, {
3369
+ availability,
3370
+ flags,
3371
+ session
3372
+ }).map(_temp$4).join(" ");
3373
+ $[4] = availability;
3374
+ $[5] = flags;
3375
+ $[6] = product.routes;
3376
+ $[7] = select;
3377
+ $[8] = session;
3378
+ $[9] = t1;
3379
+ } else t1 = $[9];
3380
+ const read = t1;
3381
+ let t2;
3382
+ if ($[10] !== read) {
3383
+ t2 = () => asServerSnapshot(read);
3384
+ $[10] = read;
3385
+ $[11] = t2;
3386
+ } else t2 = $[11];
3387
+ const t3 = useSyncExternalStore(subscribe, read, t2);
3388
+ let t4;
3389
+ if ($[12] !== t3) {
3390
+ t4 = new Set(t3.split(" "));
3391
+ $[12] = t3;
3392
+ $[13] = t4;
3393
+ } else t4 = $[13];
3394
+ return t4;
3395
+ }
3396
+ function _temp$4(t0) {
3397
+ const { id } = t0;
3398
+ return id;
3399
+ }
3400
+ /**
3401
+ * Returns the ids of the routes a menu may list, and renders again when a plugin turns on or off,
3402
+ * a flag changes or the session changes.
3403
+ */
3404
+ function useListed() {
3405
+ return useSelected(listedRoutesOf);
3406
+ }
3407
+ /**
3408
+ * Returns the ids of the routes the person may open, with or without a menu entry, and renders
3409
+ * again when a plugin turns on or off, a flag changes or the session changes.
3410
+ */
3411
+ function useOpenable() {
3412
+ return useSelected(openableRoutesOf);
3413
+ }
3414
+ /**
3415
+ * Returns true where a menu lists a route the person may open.
3416
+ */
3417
+ function lists(menu, product, listed) {
3418
+ return product.routes.some(({ id, navigation }) => {
3419
+ return navigation?.menu === menu && listed.has(id);
3420
+ });
3421
+ }
3422
+ /**
3423
+ * Returns the menus plugins declare that list a route the person may open, in install order.
3424
+ *
3425
+ * @param product - The resolved product.
3426
+ * @param listed - The ids of the routes a menu may list.
3427
+ * @param plugin - The plugin whose menus count. Without it, the menus of every plugin but the host
3428
+ * count.
3429
+ */
3430
+ function menusOf(product, listed, plugin) {
3431
+ return product.menus.filter((menu) => {
3432
+ return (plugin === void 0 ? menu.plugin !== hostContract.pluginId : menu.plugin === plugin) && lists(menu.id, product, listed);
3433
+ });
3434
+ }
3435
+ /**
3436
+ * Returns true where the settings menu lists a page the person may open.
3437
+ *
3438
+ * @param product - The resolved product.
3439
+ * @param listed - The ids of the routes a menu may list.
3440
+ * @param plugin - The plugin whose settings pages count. Without it, the pages of every plugin
3441
+ * count.
3442
+ */
3443
+ function settingsListed(product, listed, plugin) {
3444
+ return product.routes.some(({ id, navigation, plugin: owner }) => {
3445
+ return navigation?.menu === hostContract.menus.settings.id && listed.has(id) && (plugin === void 0 || owner === plugin);
3446
+ });
3447
+ }
3448
+ //#endregion
3449
+ //#region src/commands/pages.ts
3450
+ /**
3451
+ * Imports the page index of every installed plugin the first time the palette opens.
3452
+ *
3453
+ * @remarks
3454
+ * The catalogue's index lists 183 pages, so the palette imports the indexes only when a person
3455
+ * opens it. The host imports each index once per product. The host reports an index as
3456
+ * `index-failed` under its plugin when its import fails or its module does not export exactly one
3457
+ * function. The palette then lists none of that plugin's indexed pages.
3458
+ */
3459
+ /**
3460
+ * The separator i18next reads between a namespace and a key.
3461
+ */
3462
+ const SEPARATOR = ":";
3463
+ /**
3464
+ * The pages of each product's indexes, imported once per product.
3465
+ */
3466
+ const INDEXES = /* @__PURE__ */ new WeakMap();
3467
+ /**
3468
+ * Returns a key without its namespace prefix.
3469
+ *
3470
+ * @param key - A key of a plugin's catalogue, or `<namespace>:<key>` of another catalogue.
3471
+ */
3472
+ function bareKeyOf(key) {
3473
+ return key.slice(key.indexOf(SEPARATOR) + 1);
3474
+ }
3475
+ /**
3476
+ * Returns the namespaces that prefix the keys of the pages, without repeats.
3477
+ */
3478
+ function namespacesOf(pages) {
3479
+ const named = pages.flatMap(({ entry }) => [entry.label, entry.group ?? ""]).filter((key) => key.includes(SEPARATOR)).map((key) => key.slice(0, key.indexOf(SEPARATOR)));
3480
+ return [...new Set(named)];
3481
+ }
3482
+ /**
3483
+ * Returns the pages one plugin's index lists.
3484
+ *
3485
+ * @throws {@link Error} When the index's module does not export exactly one function.
3486
+ */
3487
+ async function indexOf(pluginId, load) {
3488
+ const functions = Object.values(await load()).filter((value) => typeof value === "function");
3489
+ const [list] = functions;
3490
+ if (functions.length !== 1 || list === void 0) throw new Error(`The page index of ${pluginId} exports ${String(functions.length)} functions. A page index must export exactly one function.`);
3491
+ return list().map((entry) => ({
3492
+ entry,
3493
+ plugin: pluginId
3494
+ }));
3495
+ }
3496
+ /**
3497
+ * Imports every installed plugin's index and loads the namespaces that prefix its pages' keys.
3498
+ *
3499
+ * @remarks
3500
+ * The function reports each index that fails, and lists the pages of the others.
3501
+ * @param product - The product, with each plugin's manifest.
3502
+ * @param report - The host's report.
3503
+ * @param load - Loads the catalogues of the namespaces that prefix the keys.
3504
+ * @returns The pages of every index that loaded, in install order.
3505
+ */
3506
+ async function indexedPagesOf(product, report, load) {
3507
+ const lists = Object.entries(product.manifests).map(async ([pluginId, { code }]) => {
3508
+ if (code.pages === void 0) return [];
3509
+ try {
3510
+ return await indexOf(pluginId, code.pages);
3511
+ } catch (error) {
3512
+ report({
3513
+ error,
3514
+ kind: "index-failed",
3515
+ plugin: pluginId
3516
+ });
3517
+ return [];
3518
+ }
3519
+ });
3520
+ const pages = (await Promise.all(lists)).flat();
3521
+ await load(namespacesOf(pages));
3522
+ return pages;
3523
+ }
3524
+ /**
3525
+ * Returns the pages of a product's indexes, imported on the first call for the product.
3526
+ */
3527
+ function cachedPagesOf(product, report, load) {
3528
+ const known = INDEXES.get(product) ?? indexedPagesOf(product, report, load);
3529
+ INDEXES.set(product, known);
3530
+ return known;
3531
+ }
3532
+ /**
3533
+ * Returns the pages of every installed plugin's index once the palette has opened, and none
3534
+ * before.
3535
+ *
3536
+ * @param open - True while the palette is open.
3537
+ */
3538
+ function usePageIndex(open) {
3539
+ const $ = c(10);
3540
+ const { runtime } = useInternals();
3541
+ const { i18n } = useTranslation("host");
3542
+ let t0;
3543
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
3544
+ t0 = [];
3545
+ $[0] = t0;
3546
+ } else t0 = $[0];
3547
+ const [pages, setPages] = useState(t0);
3548
+ let t1;
3549
+ if ($[1] !== i18n || $[2] !== open || $[3] !== runtime.product || $[4] !== runtime.report) {
3550
+ t1 = () => {
3551
+ let current = true;
3552
+ const listed = async () => {
3553
+ const loaded = await cachedPagesOf(runtime.product, runtime.report, (namespaces) => i18n.loadNamespaces([...namespaces]));
3554
+ if (current) setPages(loaded);
3555
+ };
3556
+ if (open) listed();
3557
+ return () => {
3558
+ current = false;
3559
+ };
3560
+ };
3561
+ $[1] = i18n;
3562
+ $[2] = open;
3563
+ $[3] = runtime.product;
3564
+ $[4] = runtime.report;
3565
+ $[5] = t1;
3566
+ } else t1 = $[5];
3567
+ let t2;
3568
+ if ($[6] !== i18n || $[7] !== open || $[8] !== runtime) {
3569
+ t2 = [
3570
+ i18n,
3571
+ open,
3572
+ runtime
3573
+ ];
3574
+ $[6] = i18n;
3575
+ $[7] = open;
3576
+ $[8] = runtime;
3577
+ $[9] = t2;
3578
+ } else t2 = $[9];
3579
+ useEffect(t1, t2);
3580
+ return pages;
3581
+ }
3582
+ //#endregion
3583
+ //#region src/commands/choices.ts
2963
3584
  /**
2964
3585
  * Returns true for a command the palette lists: it is enabled, takes no arguments and resolves
2965
3586
  * with no result.
@@ -2968,224 +3589,342 @@ function isListed({ command, enabled }) {
2968
3589
  return enabled && !command.takesArguments && !command.returnsResult;
2969
3590
  }
2970
3591
  /**
2971
- * Returns the rows the palette lists: each command a key could run under its plugin's name, then
2972
- * each page of the main menu under `Go to`.
3592
+ * Returns a word of a plugin's catalogue, or of the catalogue its key names, and the key without a
3593
+ * namespace where no catalogue states the word.
3594
+ */
3595
+ function worded(words, plugin, key) {
3596
+ return words.t(plugin, key, { defaultValue: bareKeyOf(key) });
3597
+ }
3598
+ /**
3599
+ * Returns a row per entry of every menu but the main one that the person may open: under its
3600
+ * plugin's name, or under the settings menu's heading for a settings page.
3601
+ *
3602
+ * @remarks
3603
+ * A row matches its menu's heading beside its text, and the palette lists it while a query
3604
+ * matches it.
3605
+ */
3606
+ function menuRows({ go, hrefOf, openable, product, words }) {
3607
+ return product.menus.filter(({ id }) => id !== hostContract.menus.main.id).flatMap((menu) => {
3608
+ const heading = words.t(menu.plugin, menu.label);
3609
+ return product.routes.flatMap((route) => {
3610
+ const { navigation } = route;
3611
+ if (navigation?.menu !== menu.id || !openable.has(route.id)) return [];
3612
+ const href = hrefOf(route.id);
3613
+ return [{
3614
+ action: {
3615
+ group: menu.plugin === HOST ? heading : words.t(route.plugin, "plugin.name"),
3616
+ keywords: heading,
3617
+ label: words.t(route.plugin, navigation.label),
3618
+ searchOnly: true,
3619
+ value: `route:${route.id}`
3620
+ },
3621
+ run: () => {
3622
+ go(href);
3623
+ }
3624
+ }];
3625
+ });
3626
+ });
3627
+ }
3628
+ /**
3629
+ * Returns a row per page of every plugin's index that the person may open, under its plugin's name.
3630
+ *
3631
+ * @remarks
3632
+ * A row matches its group's heading beside its title, and the palette lists it while a query
3633
+ * matches it.
3634
+ */
3635
+ function pageRows({ go, hrefOf, openable, words }, pages) {
3636
+ return pages.flatMap(({ entry, plugin }) => {
3637
+ if (!openable.has(entry.route.id)) return [];
3638
+ const href = hrefOf(entry.route.id, entry.params);
3639
+ return [{
3640
+ action: {
3641
+ group: words.t(plugin, "plugin.name"),
3642
+ keywords: entry.group === void 0 ? void 0 : worded(words, plugin, entry.group),
3643
+ label: worded(words, plugin, entry.label),
3644
+ searchOnly: true,
3645
+ value: `page:${href}`
3646
+ },
3647
+ run: () => {
3648
+ go(href);
3649
+ }
3650
+ }];
3651
+ });
3652
+ }
3653
+ /**
3654
+ * Returns the rows the palette lists: each command a key could run under its plugin's name, each
3655
+ * page of the main menu under `Go to`, then the entries of the other menus and the pages of every
3656
+ * plugin's index.
2973
3657
  *
2974
3658
  * @remarks
2975
3659
  * A command matches its catalogue's `keywords.<label>` beside its label, where the catalogue
2976
- * states it.
3660
+ * states it. The indexes are imported the first time the palette opens.
3661
+ * @param runtime - The host's runtime, which reports a command that fails.
3662
+ * @param open - True while the palette is open.
2977
3663
  */
2978
- function useChoices(runtime) {
2979
- const $ = c(10);
3664
+ function useChoices(runtime, open) {
3665
+ const $ = c(14);
2980
3666
  const commands = useCommands();
2981
3667
  const entries = useNavigation();
2982
3668
  const navigate = useNavigate();
3669
+ const map = useRouteMap();
3670
+ const openable = useOpenable();
3671
+ const pages = usePageIndex(open);
2983
3672
  const { i18n, t } = useTranslation("host");
2984
3673
  let t0;
2985
- if ($[0] !== commands || $[1] !== entries || $[2] !== i18n || $[3] !== navigate || $[4] !== runtime || $[5] !== t) {
3674
+ if ($[0] !== commands || $[1] !== entries || $[2] !== i18n || $[3] !== map || $[4] !== navigate || $[5] !== openable || $[6] !== pages || $[7] !== runtime || $[8] !== t) {
2986
3675
  const words = pluginWordsOf(i18n);
2987
3676
  let t1;
2988
- if ($[7] !== navigate || $[8] !== t) {
2989
- t1 = (t2) => {
2990
- const { href, label: label_0, routeId } = t2;
3677
+ if ($[10] !== navigate) {
3678
+ t1 = (href) => {
3679
+ navigate({ to: href });
3680
+ };
3681
+ $[10] = navigate;
3682
+ $[11] = t1;
3683
+ } else t1 = $[11];
3684
+ let t2;
3685
+ if ($[12] !== map) {
3686
+ t2 = (routeId, params) => routeHref(map, routeId, params);
3687
+ $[12] = map;
3688
+ $[13] = t2;
3689
+ } else t2 = $[13];
3690
+ const rows = {
3691
+ go: t1,
3692
+ hrefOf: t2,
3693
+ openable,
3694
+ product: runtime.product,
3695
+ words
3696
+ };
3697
+ t0 = [
3698
+ ...commands.filter(_temp$3).map((status_0) => {
3699
+ const { id, label, plugin } = status_0.command;
3700
+ const keywords = `keywords.${label}`;
3701
+ return {
3702
+ action: {
3703
+ group: words.t(plugin, "plugin.name"),
3704
+ keywords: words.exists(plugin, keywords) ? words.t(plugin, keywords) : void 0,
3705
+ label: status_0.label,
3706
+ shortcut: status_0.keys,
3707
+ value: id
3708
+ },
3709
+ run: () => {
3710
+ runReporting(status_0, runtime, t("commands.failed", { label: status_0.label }));
3711
+ }
3712
+ };
3713
+ }),
3714
+ ...entries.map((t3) => {
3715
+ const { href: href_0, label: label_0, routeId: routeId_0 } = t3;
2991
3716
  return {
2992
3717
  action: {
2993
3718
  group: t("palette.goTo"),
2994
3719
  label: label_0,
2995
- value: `route:${routeId}`
3720
+ value: `route:${routeId_0}`
2996
3721
  },
2997
3722
  run: () => {
2998
- navigate({ to: href });
3723
+ rows.go(href_0);
2999
3724
  }
3000
3725
  };
3001
- };
3002
- $[7] = navigate;
3003
- $[8] = t;
3004
- $[9] = t1;
3005
- } else t1 = $[9];
3006
- t0 = [...commands.filter(_temp$2).map((status_0) => {
3007
- const { id, label, plugin } = status_0.command;
3008
- const keywords = `keywords.${label}`;
3009
- return {
3010
- action: {
3011
- group: words.t(plugin, "plugin.name"),
3012
- keywords: words.exists(plugin, keywords) ? words.t(plugin, keywords) : void 0,
3013
- label: status_0.label,
3014
- shortcut: status_0.keys,
3015
- value: id
3016
- },
3017
- run: () => {
3018
- runReporting(status_0, runtime, t("commands.failed", { label: status_0.label }));
3019
- }
3020
- };
3021
- }), ...entries.map(t1)];
3726
+ }),
3727
+ ...menuRows(rows),
3728
+ ...pageRows(rows, pages)
3729
+ ];
3022
3730
  $[0] = commands;
3023
3731
  $[1] = entries;
3024
3732
  $[2] = i18n;
3025
- $[3] = navigate;
3026
- $[4] = runtime;
3027
- $[5] = t;
3028
- $[6] = t0;
3029
- } else t0 = $[6];
3733
+ $[3] = map;
3734
+ $[4] = navigate;
3735
+ $[5] = openable;
3736
+ $[6] = pages;
3737
+ $[7] = runtime;
3738
+ $[8] = t;
3739
+ $[9] = t0;
3740
+ } else t0 = $[9];
3030
3741
  return t0;
3031
3742
  }
3032
- function _temp$2(status) {
3743
+ function _temp$3(status) {
3033
3744
  return isListed(status);
3034
3745
  }
3746
+ //#endregion
3747
+ //#region src/commands/palette.tsx
3748
+ /**
3749
+ * The chord that opens the palette. The build refuses a command that binds it.
3750
+ */
3751
+ const PALETTE = "Mod+K";
3035
3752
  /**
3036
3753
  * Renders the palette in a dialog that `Mod+K` opens and closes.
3037
3754
  *
3038
3755
  * @remarks
3039
- * Choosing a row closes the dialog, then runs the command or navigates to the page. The dialog
3040
- * renders where the palette renders, the `overlay` region at the end of the frame, so it needs no
3041
- * portal and renders on a server while closed.
3756
+ * The query bar renders a search glyph before the field, and a clear control after it while the
3757
+ * field has a query. A chosen row closes the dialog, then runs its command or navigates to its
3758
+ * page. The palette renders in the `overlay` region at the end of the frame, so the dialog needs
3759
+ * no portal. A server renders the dialog while it is closed. `HostRoot` provides the open state,
3760
+ * so a control in the frame opens the same dialog.
3042
3761
  */
3043
3762
  function CommandPalette() {
3044
- const $ = c(38);
3045
- const [open, setOpen] = useState(false);
3763
+ const $ = c(46);
3764
+ const { open, setOpen } = usePalette();
3765
+ const choices = useChoices(useInternals().runtime, open);
3766
+ const { t } = useTranslation("host");
3046
3767
  let t0;
3047
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
3048
- t0 = { strict: false };
3049
- $[0] = t0;
3050
- } else t0 = $[0];
3051
- const { host } = useRouteContext(t0);
3768
+ if ($[0] !== setOpen) {
3769
+ t0 = () => {
3770
+ setOpen(_temp$2);
3771
+ };
3772
+ $[0] = setOpen;
3773
+ $[1] = t0;
3774
+ } else t0 = $[1];
3775
+ useHotkey(PALETTE, t0);
3052
3776
  let t1;
3053
- if ($[1] !== host) {
3054
- t1 = internalsOf(host);
3055
- $[1] = host;
3056
- $[2] = t1;
3057
- } else t1 = $[2];
3058
- const choices = useChoices(t1.runtime);
3059
- const { t } = useTranslation("host");
3777
+ if ($[2] !== t) {
3778
+ t1 = t("palette.label");
3779
+ $[2] = t;
3780
+ $[3] = t1;
3781
+ } else t1 = $[3];
3060
3782
  let t2;
3061
- if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
3062
- t2 = () => {
3063
- setOpen(_temp2);
3783
+ if ($[4] !== setOpen) {
3784
+ t2 = (t3) => {
3785
+ const { open: next } = t3;
3786
+ setOpen(next);
3064
3787
  };
3065
- $[3] = t2;
3066
- } else t2 = $[3];
3067
- useHotkey(PALETTE, t2);
3788
+ $[4] = setOpen;
3789
+ $[5] = t2;
3790
+ } else t2 = $[5];
3068
3791
  let t3;
3069
- if ($[4] !== t) {
3070
- t3 = t("palette.label");
3071
- $[4] = t;
3072
- $[5] = t3;
3073
- } else t3 = $[5];
3074
- let t4;
3075
3792
  if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
3076
- t4 = (t5) => {
3077
- const { open: next } = t5;
3078
- setOpen(next);
3079
- };
3080
- $[6] = t4;
3081
- } else t4 = $[6];
3793
+ t3 = /* @__PURE__ */ jsx(Dialog.Backdrop, {});
3794
+ $[6] = t3;
3795
+ } else t3 = $[6];
3796
+ let t4;
3797
+ if ($[7] !== choices) {
3798
+ t4 = choices.map(_temp2);
3799
+ $[7] = choices;
3800
+ $[8] = t4;
3801
+ } else t4 = $[8];
3082
3802
  let t5;
3083
- if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
3084
- t5 = /* @__PURE__ */ jsx(Dialog.Backdrop, {});
3085
- $[7] = t5;
3086
- } else t5 = $[7];
3803
+ if ($[9] !== t) {
3804
+ t5 = t("palette.commands");
3805
+ $[9] = t;
3806
+ $[10] = t5;
3807
+ } else t5 = $[10];
3087
3808
  let t6;
3088
- if ($[8] !== choices) {
3089
- t6 = choices.map(_temp3);
3090
- $[8] = choices;
3091
- $[9] = t6;
3092
- } else t6 = $[9];
3809
+ if ($[11] !== t) {
3810
+ t6 = (matches) => t("palette.count", { count: matches });
3811
+ $[11] = t;
3812
+ $[12] = t6;
3813
+ } else t6 = $[12];
3093
3814
  let t7;
3094
- if ($[10] !== t) {
3095
- t7 = t("palette.commands");
3096
- $[10] = t;
3097
- $[11] = t7;
3098
- } else t7 = $[11];
3099
- let t8;
3100
- if ($[12] !== t) {
3101
- t8 = (matches) => t("palette.count", { count: matches });
3102
- $[12] = t;
3103
- $[13] = t8;
3104
- } else t8 = $[13];
3105
- let t9;
3106
- if ($[14] !== choices) {
3107
- t9 = (value) => {
3815
+ if ($[13] !== choices || $[14] !== setOpen) {
3816
+ t7 = (value) => {
3108
3817
  setOpen(false);
3109
3818
  for (const choice of choices) if (choice.action.value === value) choice.run();
3110
3819
  };
3111
- $[14] = choices;
3112
- $[15] = t9;
3113
- } else t9 = $[15];
3114
- let t10;
3820
+ $[13] = choices;
3821
+ $[14] = setOpen;
3822
+ $[15] = t7;
3823
+ } else t7 = $[15];
3824
+ let t8;
3115
3825
  if ($[16] !== t) {
3116
- t10 = t("palette.search");
3826
+ t8 = t("palette.search");
3117
3827
  $[16] = t;
3118
- $[17] = t10;
3119
- } else t10 = $[17];
3828
+ $[17] = t8;
3829
+ } else t8 = $[17];
3830
+ let t9;
3831
+ if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
3832
+ t9 = /* @__PURE__ */ jsx(SearchIcon, { size: "100%" });
3833
+ $[18] = t9;
3834
+ } else t9 = $[18];
3835
+ let t10;
3836
+ if ($[19] !== t) {
3837
+ t10 = t("palette.search");
3838
+ $[19] = t;
3839
+ $[20] = t10;
3840
+ } else t10 = $[20];
3120
3841
  let t11;
3121
- if ($[18] !== t) {
3122
- t11 = t("palette.search");
3123
- $[18] = t;
3124
- $[19] = t11;
3125
- } else t11 = $[19];
3842
+ if ($[21] !== t) {
3843
+ t11 = t("frame.clear");
3844
+ $[21] = t;
3845
+ $[22] = t11;
3846
+ } else t11 = $[22];
3126
3847
  let t12;
3127
- if ($[20] !== t10 || $[21] !== t11) {
3128
- t12 = /* @__PURE__ */ jsx(Command.Input, {
3129
- "aria-label": t10,
3130
- placeholder: t11
3131
- });
3132
- $[20] = t10;
3133
- $[21] = t11;
3134
- $[22] = t12;
3135
- } else t12 = $[22];
3848
+ if ($[23] === Symbol.for("react.memo_cache_sentinel")) {
3849
+ t12 = /* @__PURE__ */ jsx(XIcon, { size: "100%" });
3850
+ $[23] = t12;
3851
+ } else t12 = $[23];
3136
3852
  let t13;
3137
- if ($[23] !== t) {
3138
- t13 = t("palette.none");
3139
- $[23] = t;
3140
- $[24] = t13;
3141
- } else t13 = $[24];
3142
- let t14;
3143
- if ($[25] !== t13) {
3144
- t14 = /* @__PURE__ */ jsx(Command.List, { children: /* @__PURE__ */ jsx(Command.Empty, { children: t13 }) });
3853
+ if ($[24] !== t11) {
3854
+ t13 = /* @__PURE__ */ jsx(Command.Clear, {
3855
+ "aria-label": t11,
3856
+ children: t12
3857
+ });
3858
+ $[24] = t11;
3145
3859
  $[25] = t13;
3146
- $[26] = t14;
3147
- } else t14 = $[26];
3860
+ } else t13 = $[25];
3861
+ let t14;
3862
+ if ($[26] !== t10 || $[27] !== t13 || $[28] !== t8) {
3863
+ t14 = /* @__PURE__ */ jsx(Command.Input, {
3864
+ "aria-label": t8,
3865
+ indicator: t9,
3866
+ placeholder: t10,
3867
+ children: t13
3868
+ });
3869
+ $[26] = t10;
3870
+ $[27] = t13;
3871
+ $[28] = t8;
3872
+ $[29] = t14;
3873
+ } else t14 = $[29];
3148
3874
  let t15;
3149
- if ($[27] !== t12 || $[28] !== t14 || $[29] !== t6 || $[30] !== t7 || $[31] !== t8 || $[32] !== t9) {
3150
- t15 = /* @__PURE__ */ jsx(Dialog.Positioner, { children: /* @__PURE__ */ jsx(Dialog.Content, { children: /* @__PURE__ */ jsxs(Command.Root, {
3151
- actions: t6,
3152
- "aria-label": t7,
3153
- count: t8,
3154
- onRun: t9,
3155
- children: [t12, t14]
3156
- }) }) });
3157
- $[27] = t12;
3158
- $[28] = t14;
3159
- $[29] = t6;
3160
- $[30] = t7;
3161
- $[31] = t8;
3162
- $[32] = t9;
3163
- $[33] = t15;
3164
- } else t15 = $[33];
3875
+ if ($[30] !== t) {
3876
+ t15 = t("palette.none");
3877
+ $[30] = t;
3878
+ $[31] = t15;
3879
+ } else t15 = $[31];
3165
3880
  let t16;
3166
- if ($[34] !== open || $[35] !== t15 || $[36] !== t3) {
3167
- t16 = /* @__PURE__ */ jsxs(Dialog.Root, {
3168
- "aria-label": t3,
3169
- onOpenChange: t4,
3881
+ if ($[32] !== t15) {
3882
+ t16 = /* @__PURE__ */ jsx(Command.List, { children: /* @__PURE__ */ jsx(Command.Empty, { children: t15 }) });
3883
+ $[32] = t15;
3884
+ $[33] = t16;
3885
+ } else t16 = $[33];
3886
+ let t17;
3887
+ if ($[34] !== t14 || $[35] !== t16 || $[36] !== t4 || $[37] !== t5 || $[38] !== t6 || $[39] !== t7) {
3888
+ t17 = /* @__PURE__ */ jsx(Dialog.Positioner, { children: /* @__PURE__ */ jsx(Dialog.Content, { children: /* @__PURE__ */ jsxs(Command.Root, {
3889
+ actions: t4,
3890
+ "aria-label": t5,
3891
+ count: t6,
3892
+ onRun: t7,
3893
+ children: [t14, t16]
3894
+ }) }) });
3895
+ $[34] = t14;
3896
+ $[35] = t16;
3897
+ $[36] = t4;
3898
+ $[37] = t5;
3899
+ $[38] = t6;
3900
+ $[39] = t7;
3901
+ $[40] = t17;
3902
+ } else t17 = $[40];
3903
+ let t18;
3904
+ if ($[41] !== open || $[42] !== t1 || $[43] !== t17 || $[44] !== t2) {
3905
+ t18 = /* @__PURE__ */ jsxs(Dialog.Root, {
3906
+ "aria-label": t1,
3907
+ onOpenChange: t2,
3170
3908
  open,
3171
3909
  placement: "top",
3172
3910
  scrollBehavior: "inside",
3173
3911
  size: "lg",
3174
3912
  variant: "plain",
3175
- children: [t5, t15]
3913
+ children: [t3, t17]
3176
3914
  });
3177
- $[34] = open;
3178
- $[35] = t15;
3179
- $[36] = t3;
3180
- $[37] = t16;
3181
- } else t16 = $[37];
3182
- return t16;
3183
- }
3184
- function _temp3(t0) {
3915
+ $[41] = open;
3916
+ $[42] = t1;
3917
+ $[43] = t17;
3918
+ $[44] = t2;
3919
+ $[45] = t18;
3920
+ } else t18 = $[45];
3921
+ return t18;
3922
+ }
3923
+ function _temp2(t0) {
3185
3924
  const { action } = t0;
3186
3925
  return action;
3187
3926
  }
3188
- function _temp2(shown) {
3927
+ function _temp$2(shown) {
3189
3928
  return !shown;
3190
3929
  }
3191
3930
  //#endregion
@@ -3238,66 +3977,80 @@ function _temp$1(toast) {
3238
3977
  //#endregion
3239
3978
  //#region src/parts/root.tsx
3240
3979
  /**
3980
+ * Renders the root route's component: the `layout` region around the product's frame, and the
3981
+ * `overlay` region after it with the palette and the toast region.
3982
+ */
3983
+ /**
3241
3984
  * Renders the product's frame inside the `layout` region, the `overlay` region after it, and the
3242
3985
  * keys of the product's commands.
3243
3986
  *
3244
3987
  * @remarks
3245
3988
  * The router renders the root route's component inside a catch boundary of its own, so the
3246
3989
  * component renders the host's last boundary around the frame, and an error in the frame
3247
- * renders the failure page and is reported with the target `host`.
3990
+ * renders the failure page and is reported with the target `host`. The component provides the
3991
+ * palette's open state, so a control in the frame opens the palette that `Mod+K` opens.
3248
3992
  */
3249
3993
  function HostRoot(t0) {
3250
- const $ = c(10);
3994
+ const $ = c(12);
3251
3995
  const { children } = t0;
3996
+ const { runtime } = useInternals();
3997
+ const [open, setOpen] = useState(false);
3252
3998
  let t1;
3253
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
3254
- t1 = { strict: false };
3255
- $[0] = t1;
3256
- } else t1 = $[0];
3257
- const { host } = useRouteContext(t1);
3999
+ if ($[0] !== open) {
4000
+ t1 = {
4001
+ open,
4002
+ setOpen
4003
+ };
4004
+ $[0] = open;
4005
+ $[1] = t1;
4006
+ } else t1 = $[1];
3258
4007
  let t2;
3259
- if ($[1] !== host) {
3260
- t2 = internalsOf(host);
3261
- $[1] = host;
3262
- $[2] = t2;
3263
- } else t2 = $[2];
3264
- let t3;
3265
- if ($[3] !== children) {
3266
- t3 = /* @__PURE__ */ jsx(Slot, {
4008
+ if ($[2] !== children) {
4009
+ t2 = /* @__PURE__ */ jsx(Slot, {
3267
4010
  slot: hostContract.slots.layout,
3268
4011
  children
3269
4012
  });
3270
- $[3] = children;
3271
- $[4] = t3;
3272
- } else t3 = $[4];
4013
+ $[2] = children;
4014
+ $[3] = t2;
4015
+ } else t2 = $[3];
4016
+ let t3;
3273
4017
  let t4;
3274
- let t5;
3275
- if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
3276
- t4 = /* @__PURE__ */ jsxs(Slot, {
4018
+ if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
4019
+ t3 = /* @__PURE__ */ jsxs(Slot, {
3277
4020
  slot: hostContract.slots.overlay,
3278
4021
  children: [/* @__PURE__ */ jsx(CommandPalette, {}), /* @__PURE__ */ jsx(HostToasts, {})]
3279
4022
  });
3280
- t5 = /* @__PURE__ */ jsx(CommandKeys, {});
4023
+ t4 = /* @__PURE__ */ jsx(CommandKeys, {});
4024
+ $[4] = t3;
3281
4025
  $[5] = t4;
3282
- $[6] = t5;
3283
4026
  } else {
4027
+ t3 = $[4];
3284
4028
  t4 = $[5];
3285
- t5 = $[6];
3286
4029
  }
3287
- let t6;
3288
- if ($[7] !== t2.runtime || $[8] !== t3) {
3289
- t6 = /* @__PURE__ */ jsxs(LastBoundary, {
3290
- runtime: t2.runtime,
4030
+ let t5;
4031
+ if ($[6] !== runtime || $[7] !== t2) {
4032
+ t5 = /* @__PURE__ */ jsxs(LastBoundary, {
4033
+ runtime,
3291
4034
  children: [
4035
+ t2,
3292
4036
  t3,
3293
- t4,
3294
- t5
4037
+ t4
3295
4038
  ]
3296
4039
  });
3297
- $[7] = t2.runtime;
3298
- $[8] = t3;
3299
- $[9] = t6;
3300
- } else t6 = $[9];
4040
+ $[6] = runtime;
4041
+ $[7] = t2;
4042
+ $[8] = t5;
4043
+ } else t5 = $[8];
4044
+ let t6;
4045
+ if ($[9] !== t1 || $[10] !== t5) {
4046
+ t6 = /* @__PURE__ */ jsx(PaletteContext, {
4047
+ value: t1,
4048
+ children: t5
4049
+ });
4050
+ $[9] = t1;
4051
+ $[10] = t5;
4052
+ $[11] = t6;
4053
+ } else t6 = $[11];
3301
4054
  return t6;
3302
4055
  }
3303
4056
  //#endregion
@@ -3343,16 +4096,16 @@ function componentOf(module, routeId) {
3343
4096
  * error page, and counts each error towards the page's quarantine.
3344
4097
  */
3345
4098
  /**
3346
- * Marks a route error component that has counted no error yet.
3347
- */
3348
- const UNCOUNTED = Symbol("uncounted");
3349
- /**
3350
4099
  * Returns the error component of a plugin's route.
3351
4100
  *
3352
4101
  * @remarks
3353
- * The router renders the component for an error that the page or its loader throws. Each error
3354
- * counts one failure towards the route's quarantine, once however often the component renders
3355
- * it. The fallback renders in its plugin's scope, without props.
4102
+ * The router renders the component for an error that the page or its loader throws. The component
4103
+ * passes each error to the route's quarantine, which counts the error once however often the
4104
+ * component mounts with it. The fallback renders in its plugin's scope, without props. A page
4105
+ * whose module failed to import renders the host's error page with a button that reloads the
4106
+ * page, in place of the fallback, because the fallback is a module of the same plugin and a
4107
+ * reload is the one way to import it again. The error page takes focus where it renders in place
4108
+ * of the page a person is on.
3356
4109
  * @param routeId - Qualified id of the route.
3357
4110
  * @param fallback - Imports the component the manifest renders in the page's place, where it
3358
4111
  * states one.
@@ -3367,23 +4120,31 @@ function routeErrorOf(routeId, fallback) {
3367
4120
  const { host } = useRouteContext({ strict: false });
3368
4121
  const { quarantine } = host.stores;
3369
4122
  const { t } = useTranslation("host");
3370
- const counted = useRef(UNCOUNTED);
4123
+ const unloaded = error instanceof PluginImportError;
4124
+ useReplacementFocus();
3371
4125
  useEffect(() => {
3372
- if (counted.current === error) return;
3373
- counted.current = error;
3374
4126
  quarantine.failed(target, error);
3375
4127
  }, [error, quarantine]);
3376
- if (Fallback !== void 0) return /* @__PURE__ */ jsx(PluginProvider, {
4128
+ if (Fallback !== void 0 && !unloaded) return /* @__PURE__ */ jsx(PluginProvider, {
3377
4129
  pluginId: pluginOf(routeId),
3378
4130
  children: /* @__PURE__ */ jsx(Suspense, {
3379
4131
  fallback: null,
3380
4132
  children: /* @__PURE__ */ jsx(Fallback, {})
3381
4133
  })
3382
4134
  });
3383
- return /* @__PURE__ */ jsx(EmptyState.Root, { children: /* @__PURE__ */ jsxs(EmptyState.Content, { children: [/* @__PURE__ */ jsx(EmptyState.Title, {
3384
- as: "h1",
3385
- children: t("page.failed.title")
3386
- }), /* @__PURE__ */ jsx(EmptyState.Description, { children: t("page.failed.description") })] }) });
4135
+ return /* @__PURE__ */ jsx(EmptyState.Root, { children: /* @__PURE__ */ jsxs(EmptyState.Content, { children: [
4136
+ /* @__PURE__ */ jsx(EmptyState.Title, {
4137
+ as: "h1",
4138
+ children: t("page.failed.title")
4139
+ }),
4140
+ /* @__PURE__ */ jsx(EmptyState.Description, { children: t("page.failed.description") }),
4141
+ unloaded ? /* @__PURE__ */ jsx(Button, {
4142
+ onClick: () => {
4143
+ window.location.reload();
4144
+ },
4145
+ children: t("failed.reload")
4146
+ }) : null
4147
+ ] }) });
3387
4148
  };
3388
4149
  }
3389
4150
  //#endregion
@@ -3427,18 +4188,24 @@ function requiresSignIn(when) {
3427
4188
  * context.
3428
4189
  *
3429
4190
  * @remarks
3430
- * The evaluator throws the router's not-found error where the route's plugin is not on, with the
3431
- * plugin and its reason as data where a switch or a kill switch stopped it, and where the route
3432
- * is quarantined, with the target as data. Where the condition is false for a person who is not
3433
- * signed in, the product states a sign-in route, and the condition requires sign-in at its top
3434
- * level or in a top-level `allOf`, it throws the router's redirect to the sign-in route, with the
3435
- * address the navigation enters as the `redirect` search. Otherwise it returns the condition.
4191
+ * For a person who is not signed in, where the product states a sign-in route and the condition
4192
+ * requires sign-in at its top level or in a top-level `allOf`, the evaluator throws the router's
4193
+ * redirect to the sign-in route, with the address the navigation enters as the `redirect` search.
4194
+ * The redirect comes before the plugin's availability, because an entitlement or a condition on
4195
+ * the plugin reads a session that does not exist yet. Otherwise the evaluator throws the router's
4196
+ * not-found error where the route's plugin is not on, with the plugin and its reason as data
4197
+ * where a switch or a kill switch stopped it, and where the route is quarantined, with the target
4198
+ * as data. It then returns the condition.
3436
4199
  * @param product - The installed plugins and the sign-in route.
3437
4200
  */
3438
4201
  function evaluatorOf(product) {
3439
4202
  return ({ pluginId, routeId, when }, { host, routes }, location) => {
3440
4203
  const context = conditionContextOf(host.stores);
3441
4204
  const target = `route:${routeId}`;
4205
+ if (product.signIn !== void 0 && !context.authenticated && when !== void 0 && requiresSignIn(when)) throw redirect({
4206
+ search: { redirect: location?.href },
4207
+ to: routeHref(routes, product.signIn)
4208
+ });
3442
4209
  if (!context.on(pluginId)) {
3443
4210
  const data = stoppedBy(pluginId, product.plugins, host.stores.availability.get());
3444
4211
  throw data === void 0 ? notFound() : notFound({ data });
@@ -3447,12 +4214,7 @@ function evaluatorOf(product) {
3447
4214
  reason: "quarantined",
3448
4215
  target
3449
4216
  } });
3450
- if (when === void 0 || evaluateWhen(when, context)) return true;
3451
- if (product.signIn !== void 0 && !context.authenticated && requiresSignIn(when)) throw redirect({
3452
- search: { redirect: location?.href },
3453
- to: routeHref(routes, product.signIn)
3454
- });
3455
- return false;
4217
+ return when === void 0 || evaluateWhen(when, context);
3456
4218
  };
3457
4219
  }
3458
4220
  //#endregion
@@ -3495,7 +4257,7 @@ function loaderOf(product, route) {
3495
4257
  * @remarks
3496
4258
  * The component reads the host from the router's context. A render of the page that throws
3497
4259
  * commits nothing, so the route's count of failed renders returns to zero only after a render
3498
- * that commits.
4260
+ * that commits. The page takes focus where it renders in place of the page a person is on.
3499
4261
  * @param pluginId - Id of the plugin that declares the route, or the settings page, whose scope
3500
4262
  * the page renders in.
3501
4263
  * @param routeId - The route's qualified id, under which its failed renders and the extensions
@@ -3511,6 +4273,7 @@ function hostPageOf(pluginId, routeId, page) {
3511
4273
  return function HostPage() {
3512
4274
  const { host } = useRouteContext({ strict: false });
3513
4275
  const { quarantine } = host.stores;
4276
+ useReplacementFocus();
3514
4277
  useEffect(() => {
3515
4278
  quarantine.rendered(target);
3516
4279
  });
@@ -3600,8 +4363,8 @@ function unplacedOf(sections, listed, pages) {
3600
4363
  * part under the settings route.
3601
4364
  */
3602
4365
  /**
3603
- * Returns the ids of the sections whose plugin is on and whose condition is true, joined by spaces,
3604
- * which no qualified id contains.
4366
+ * Returns the ids of the sections whose plugin is on and whose condition is true, joined by spaces.
4367
+ * A qualified id does not contain a space.
3605
4368
  */
3606
4369
  function shownOf(sections, stores) {
3607
4370
  const context = conditionContextOf(stores);
@@ -3610,9 +4373,13 @@ function shownOf(sections, stores) {
3610
4373
  /**
3611
4374
  * Returns the sections that render and the pages the settings menu lists, and renders again when a
3612
4375
  * plugin turns on or off, a flag changes or the session changes.
4376
+ *
4377
+ * @remarks
4378
+ * The server snapshot computes the sections inside `asServerSnapshot`, so a settings page
4379
+ * hydrates with the sections that the server render showed.
3613
4380
  */
3614
4381
  function useSections() {
3615
- const $ = c(22);
4382
+ const $ = c(24);
3616
4383
  let t0;
3617
4384
  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
3618
4385
  t0 = { strict: false };
@@ -3654,47 +4421,53 @@ function useSections() {
3654
4421
  $[9] = t2;
3655
4422
  } else t2 = $[9];
3656
4423
  const read = t2;
3657
- const t3 = useSyncExternalStore(subscribe, read, read);
3658
- let t4;
3659
- if ($[10] !== t3) {
3660
- t4 = new Set(t3.split(" "));
3661
- $[10] = t3;
3662
- $[11] = t4;
3663
- } else t4 = $[11];
3664
- const shown = t4;
4424
+ let t3;
4425
+ if ($[10] !== read) {
4426
+ t3 = () => asServerSnapshot(read);
4427
+ $[10] = read;
4428
+ $[11] = t3;
4429
+ } else t3 = $[11];
4430
+ const t4 = useSyncExternalStore(subscribe, read, t3);
3665
4431
  let t5;
3666
- if ($[12] !== entries) {
3667
- t5 = entries.map(_temp);
3668
- $[12] = entries;
4432
+ if ($[12] !== t4) {
4433
+ t5 = new Set(t4.split(" "));
4434
+ $[12] = t4;
3669
4435
  $[13] = t5;
3670
4436
  } else t5 = $[13];
4437
+ const shown = t5;
3671
4438
  let t6;
3672
- if ($[14] !== settings.sections || $[15] !== shown) {
3673
- let t7;
3674
- if ($[17] !== shown) {
3675
- t7 = (t8) => {
3676
- const { id } = t8;
4439
+ if ($[14] !== entries) {
4440
+ t6 = entries.map(_temp);
4441
+ $[14] = entries;
4442
+ $[15] = t6;
4443
+ } else t6 = $[15];
4444
+ let t7;
4445
+ if ($[16] !== settings.sections || $[17] !== shown) {
4446
+ let t8;
4447
+ if ($[19] !== shown) {
4448
+ t8 = (t9) => {
4449
+ const { id } = t9;
3677
4450
  return shown.has(id);
3678
4451
  };
3679
- $[17] = shown;
3680
- $[18] = t7;
3681
- } else t7 = $[18];
3682
- t6 = settings.sections.filter(t7);
3683
- $[14] = settings.sections;
3684
- $[15] = shown;
3685
- $[16] = t6;
3686
- } else t6 = $[16];
3687
- let t7;
3688
- if ($[19] !== t5 || $[20] !== t6) {
3689
- t7 = {
3690
- listed: t5,
3691
- shown: t6
4452
+ $[19] = shown;
4453
+ $[20] = t8;
4454
+ } else t8 = $[20];
4455
+ t7 = settings.sections.filter(t8);
4456
+ $[16] = settings.sections;
4457
+ $[17] = shown;
4458
+ $[18] = t7;
4459
+ } else t7 = $[18];
4460
+ let t8;
4461
+ if ($[21] !== t6 || $[22] !== t7) {
4462
+ t8 = {
4463
+ listed: t6,
4464
+ shown: t7
3692
4465
  };
3693
- $[19] = t5;
3694
- $[20] = t6;
3695
- $[21] = t7;
3696
- } else t7 = $[21];
3697
- return t7;
4466
+ $[21] = t6;
4467
+ $[22] = t7;
4468
+ $[23] = t8;
4469
+ } else t8 = $[23];
4470
+ return t8;
3698
4471
  }
3699
4472
  function _temp(t0) {
3700
4473
  const { routeId } = t0;
@@ -4710,9 +5483,11 @@ function declarationOf(product, route) {
4710
5483
  * The routes compile in one call, so the compiler sees every path at once. Each route loads its
4711
5484
  * page with the modules of the plugins in its `loads`, loads its `data` through the data client
4712
5485
  * in the router's context, validates its search with its contract's validator, and evaluates its
4713
- * condition against the host in the router's context. The settings route renders the settings
4714
- * frame, its index opens the first settings page, and each settings page renders its sections.
4715
- * The pages import through the manifests' importers as `instrumented` wraps them.
5486
+ * condition against the host in the router's context. Each route renders the root's not-found
5487
+ * page in its own place, so a page that throws the router's not-found shows it inside the frame
5488
+ * and counts no failure. The settings route renders the settings frame, its index opens the first
5489
+ * settings page, and each settings page renders its sections. The pages import through the
5490
+ * manifests' importers as `instrumented` wraps them.
4716
5491
  * @param product - The product, with each installed plugin's manifest.
4717
5492
  * @returns The function that compiles the routes under the root, for the root's `addChildren`.
4718
5493
  */
@@ -4723,8 +5498,9 @@ function createHostRoutes(product) {
4723
5498
  return (root) => compileRoutes(declarations, {
4724
5499
  errorComponent: ({ id }) => routeErrorOf(id, mappingOf(loaded, id).fallback),
4725
5500
  evaluate,
5501
+ notFoundComponent: root.options.notFoundComponent,
4726
5502
  parent: root
4727
5503
  });
4728
5504
  }
4729
5505
  //#endregion
4730
- export { HostContent as a, sampledFrom as c, HostNotFound as i, HostRoot as n, createHost as o, HostProvider as r, internalsOf as s, createHostRoutes as t };
5506
+ export { settingsListed as a, HostProvider as c, HostContent as d, onCaughtError as f, sampledFrom as h, menusOf as i, HostNotFound as l, internalsOf as m, HostRoot as n, useListed as o, createHost as p, listedRoutesOf as r, usePalette as s, createHostRoutes as t, useInternals as u };