@xemahq/ui-kernel 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -2
- package/src/index.ts +0 -17
- package/src/lib/biome-host/agent-validation.ts +0 -239
- package/src/lib/biome-host/biome-builders.ts +0 -112
- package/src/lib/biome-host/biome-mode.ts +0 -6
- package/src/lib/biome-host/biome-navigation.ts +0 -37
- package/src/lib/biome-host/biome-registry.ts +0 -299
- package/src/lib/biome-host/biome-scope-hooks.ts +0 -41
- package/src/lib/biome-host/biome-scope.tsx +0 -52
- package/src/lib/biome-host/biome-scoped-query.ts +0 -134
- package/src/lib/biome-host/create-biome-orval-config.ts +0 -82
- package/src/lib/biome-host/define-web-biome.spec.ts +0 -221
- package/src/lib/biome-host/define-web-biome.ts +0 -222
- package/src/lib/biome-host/errors.ts +0 -220
- package/src/lib/biome-host/frontend-biome.ts +0 -256
- package/src/lib/biome-host/host-bridge.ts +0 -281
- package/src/lib/biome-host/host-sources.ts +0 -54
- package/src/lib/biome-host/i18n-hooks.ts +0 -51
- package/src/lib/biome-host/i18n-port.test.ts +0 -50
- package/src/lib/biome-host/i18n-port.ts +0 -96
- package/src/lib/biome-host/index.ts +0 -35
- package/src/lib/biome-host/realtime-hooks.ts +0 -75
- package/src/lib/biome-host/realtime-port.ts +0 -115
- package/src/lib/biome-host/session-contributions.ts +0 -296
- package/src/lib/biome-host/use-mutation-with-error-toast.ts +0 -90
- package/src/lib/biome-host/use-page-state.ts +0 -248
- package/src/lib/capabilities/capability-context.ts +0 -25
- package/src/lib/capabilities/capability-provider.tsx +0 -73
- package/src/lib/capabilities/index.ts +0 -17
- package/src/lib/capabilities/types.ts +0 -69
- package/src/lib/capabilities/use-capability.ts +0 -79
- package/src/lib/contribution-registry/contribution-registry.spec.ts +0 -141
- package/src/lib/contribution-registry/contribution-registry.ts +0 -204
- package/src/lib/contribution-registry/index.ts +0 -9
- package/src/lib/contribution-registry/renderable-registry.ts +0 -142
- package/src/lib/system-bus/capability-invoke.ts +0 -92
- package/src/lib/system-bus/deeplink.ts +0 -210
- package/src/lib/system-bus/enums.ts +0 -86
- package/src/lib/system-bus/host-ports.ts +0 -93
- package/src/lib/system-bus/index.ts +0 -16
- package/src/lib/system-bus/intent-registry.ts +0 -109
- package/src/lib/system-bus/intents.ts +0 -113
- package/src/lib/system-bus/palette.ts +0 -78
- package/src/lib/system-bus/system-bus-builder.ts +0 -159
- package/src/lib/system-bus/system-bus.ts +0 -37
- package/src/lib/system-bus/windows.ts +0 -51
- package/src/offline/id.ts +0 -16
- package/src/offline/index.ts +0 -54
- package/src/offline/mutation.ts +0 -75
- package/src/offline/offline-status-badge.tsx +0 -51
- package/src/offline/online-controller.ts +0 -105
- package/src/offline/outbox.test.ts +0 -172
- package/src/offline/outbox.ts +0 -348
- package/src/offline/react-hooks.ts +0 -40
- package/src/offline/react.tsx +0 -32
- package/src/offline/storage.ts +0 -112
- package/src/offline/types.ts +0 -77
- package/src/org-db/components/DbResultTable.tsx +0 -176
- package/src/org-db/index.ts +0 -1
- package/src/pdf/index.ts +0 -11
- package/src/registry/index.ts +0 -9
- package/src/registry/lib/agent-validation-host.ts +0 -36
- package/src/registry/lib/biome-slot.tsx +0 -50
- package/src/registry/lib/biomes-enabled-context.ts +0 -20
- package/src/registry/lib/extension-points.ts +0 -134
- package/src/registry/lib/primitives/SessionEventCard.tsx +0 -164
- package/src/registry/lib/primitives/SessionEventTimeline.tsx +0 -95
- package/src/registry/lib/primitives/SessionMutationBar.tsx +0 -76
- package/src/registry/lib/primitives/SessionTabbedDrawer.tsx +0 -165
- package/src/registry/lib/primitives/index.ts +0 -18
- package/src/registry/lib/session-context-builder.ts +0 -84
- package/src/registry/lib/session-context-provider.tsx +0 -33
- package/src/registry/lib/session-context.ts +0 -21
- package/src/registry/lib/session-selectors.ts +0 -272
- package/src/session/comments/CommentRail.tsx +0 -164
- package/src/session/index.ts +0 -3
- package/src/session/lib/cn.ts +0 -11
- package/src/session/shell/SessionWorkspaceShell.tsx +0 -147
- package/src/session-kit/approvals/ApprovalButton.tsx +0 -89
- package/src/session-kit/approvals/ApprovalCard.tsx +0 -341
- package/src/session-kit/approvals/ApprovalsCenter.tsx +0 -336
- package/src/session-kit/approvals/CapabilityApprovalStickyBar.tsx +0 -118
- package/src/session-kit/approvals/Obligation.tsx +0 -111
- package/src/session-kit/approvals/ScopedBody.tsx +0 -392
- package/src/session-kit/approvals/approval-icons.ts +0 -31
- package/src/session-kit/approvals/approval-model.ts +0 -204
- package/src/session-kit/approvals/index.ts +0 -22
- package/src/session-kit/approvals/obligation-display.ts +0 -100
- package/src/session-kit/approvals/risk-accent.ts +0 -47
- package/src/session-kit/approvals/scope-icons.ts +0 -19
- package/src/session-kit/combobox/Combobox.tsx +0 -358
- package/src/session-kit/combobox/use-click-outside.ts +0 -21
- package/src/session-kit/display/ContextHeader.tsx +0 -148
- package/src/session-kit/display/FileDiffCard.tsx +0 -140
- package/src/session-kit/display/MD.tsx +0 -162
- package/src/session-kit/display/MessageTurn.tsx +0 -157
- package/src/session-kit/display/ThinkingPanel.tsx +0 -81
- package/src/session-kit/display/TodoChecklist.tsx +0 -120
- package/src/session-kit/display/TokenMeter.tsx +0 -89
- package/src/session-kit/display/ToolStrip.tsx +0 -278
- package/src/session-kit/display/TypingDots.tsx +0 -24
- package/src/session-kit/index.ts +0 -44
- package/src/session-kit/lib/enums.ts +0 -66
- package/src/session-kit/lib/portal-accent.ts +0 -30
- package/src/session-kit/lib/status-dot.ts +0 -68
- package/src/session-kit/primitives/Avatar.tsx +0 -44
- package/src/session-kit/primitives/PortalGlyph.tsx +0 -51
- package/src/session-kit/primitives/RiskPill.tsx +0 -95
- package/src/session-kit/primitives/ScopeDot.tsx +0 -47
- package/src/session-kit/primitives/Segmented.tsx +0 -71
- package/src/ui/branding/app-branding-context.ts +0 -20
- package/src/ui/branding/app-branding-provider.tsx +0 -123
- package/src/ui/branding/sanitize.ts +0 -89
- package/src/ui/breakpoints.ts +0 -14
- package/src/ui/capture/document-preview.tsx +0 -166
- package/src/ui/capture/field-spec-to-zod.test.ts +0 -81
- package/src/ui/capture/field-spec-to-zod.ts +0 -174
- package/src/ui/capture/field-spec-validate.ts +0 -236
- package/src/ui/capture/field-spec.test.ts +0 -49
- package/src/ui/capture/field-spec.ts +0 -274
- package/src/ui/capture/index.ts +0 -51
- package/src/ui/capture/schema-field-input.tsx +0 -620
- package/src/ui/capture/schema-form.tsx +0 -149
- package/src/ui/capture/submit-values.test.ts +0 -70
- package/src/ui/capture/submit-values.ts +0 -63
- package/src/ui/capture/use-prefill.ts +0 -186
- package/src/ui/capture/use-schema-validation.ts +0 -145
- package/src/ui/capture/use-wizard.ts +0 -180
- package/src/ui/capture/value-format.ts +0 -32
- package/src/ui/capture/wizard.tsx +0 -105
- package/src/ui/charts/bar-chart.tsx +0 -139
- package/src/ui/charts/chart-frame.tsx +0 -125
- package/src/ui/charts/chart-types.ts +0 -52
- package/src/ui/charts/chart-utils.ts +0 -190
- package/src/ui/charts/charts.test.tsx +0 -204
- package/src/ui/charts/donut-chart.tsx +0 -129
- package/src/ui/charts/index.ts +0 -27
- package/src/ui/charts/line-chart.tsx +0 -202
- package/src/ui/charts/sparkline.tsx +0 -109
- package/src/ui/chrome/AsyncBoundary.tsx +0 -74
- package/src/ui/chrome/EmptyState.tsx +0 -257
- package/src/ui/chrome/ErrorCard.tsx +0 -63
- package/src/ui/chrome/LoadingState.tsx +0 -61
- package/src/ui/chrome/PageHeader.tsx +0 -137
- package/src/ui/chrome/RecordCardList.tsx +0 -98
- package/src/ui/chrome/StateCard.tsx +0 -150
- package/src/ui/chrome/confirm-delete-dialog.tsx +0 -59
- package/src/ui/chrome/page-shell.spec.tsx +0 -117
- package/src/ui/chrome/page-shell.tsx +0 -221
- package/src/ui/chrome/scope-badge.tsx +0 -40
- package/src/ui/chrome/status-badge.tsx +0 -79
- package/src/ui/cn.ts +0 -32
- package/src/ui/design-tokens.ts +0 -372
- package/src/ui/hooks/use-debounced-value.ts +0 -16
- package/src/ui/hooks/use-is-mobile.ts +0 -28
- package/src/ui/index.ts +0 -110
- package/src/ui/pdf/pdf-preview.tsx +0 -122
- package/src/ui/primitives/alert-dialog.tsx +0 -104
- package/src/ui/primitives/async-combobox.tsx +0 -182
- package/src/ui/primitives/badge-variants.ts +0 -21
- package/src/ui/primitives/badge.tsx +0 -25
- package/src/ui/primitives/button-variants.ts +0 -31
- package/src/ui/primitives/button.tsx +0 -29
- package/src/ui/primitives/card.tsx +0 -43
- package/src/ui/primitives/checkbox.tsx +0 -26
- package/src/ui/primitives/collapsible.tsx +0 -9
- package/src/ui/primitives/dialog.tsx +0 -103
- package/src/ui/primitives/dropdown-menu.tsx +0 -179
- package/src/ui/primitives/form-stepper.tsx +0 -94
- package/src/ui/primitives/form-stepper.types.ts +0 -15
- package/src/ui/primitives/input.tsx +0 -22
- package/src/ui/primitives/label.tsx +0 -17
- package/src/ui/primitives/overflow-tabs.tsx +0 -331
- package/src/ui/primitives/popover.tsx +0 -33
- package/src/ui/primitives/preview-device-frame.tsx +0 -213
- package/src/ui/primitives/radio-group.tsx +0 -36
- package/src/ui/primitives/resizable.tsx +0 -67
- package/src/ui/primitives/scroll-area.tsx +0 -38
- package/src/ui/primitives/select.tsx +0 -143
- package/src/ui/primitives/separator.tsx +0 -20
- package/src/ui/primitives/sheet.tsx +0 -107
- package/src/ui/primitives/skeleton.tsx +0 -99
- package/src/ui/primitives/switch.tsx +0 -27
- package/src/ui/primitives/table.tsx +0 -72
- package/src/ui/primitives/tabs.tsx +0 -53
- package/src/ui/primitives/tag-multi-select.tsx +0 -261
- package/src/ui/primitives/textarea.tsx +0 -21
- package/src/ui/primitives/tooltip.tsx +0 -30
- package/src/ui/primitives/url-preview-chrome.tsx +0 -103
- package/src/vista/GenericCatalogueSurface.tsx +0 -179
- package/src/vista/SurfaceFallbackCard.tsx +0 -109
- package/src/vista/VistaPanel.tsx +0 -163
- package/src/vista/form-payload.spec.ts +0 -139
- package/src/vista/html/CustomUiFrame.spec.tsx +0 -127
- package/src/vista/html/CustomUiFrame.tsx +0 -206
- package/src/vista/html/custom-ui-frame-bridge.spec.ts +0 -191
- package/src/vista/html/custom-ui-frame-bridge.ts +0 -98
- package/src/vista/html/custom-ui-frame-contract.ts +0 -88
- package/src/vista/html/custom-ui-intents.spec.ts +0 -135
- package/src/vista/html/custom-ui-intents.ts +0 -155
- package/src/vista/html/custom-ui-srcdoc.spec.ts +0 -151
- package/src/vista/html/custom-ui-srcdoc.ts +0 -193
- package/src/vista/html/index.ts +0 -13
- package/src/vista/index.ts +0 -52
- package/src/vista/surface-catalogue.spec.ts +0 -140
- package/src/vista/surface-catalogue.ts +0 -90
- package/src/vista/surface-icons.ts +0 -57
- package/src/vista/surface-registry.ts +0 -56
- package/src/vista/surfaces/ChartSurface.tsx +0 -34
- package/src/vista/surfaces/FilesSurface.tsx +0 -202
- package/src/vista/surfaces/FormSurface.tsx +0 -380
- package/src/vista/surfaces/HtmlSurface.tsx +0 -43
- package/src/vista/surfaces/TableSurface.tsx +0 -100
- package/src/vista/surfaces/UrlSurface.tsx +0 -40
- package/src/vista/surfaces/blob-surfaces.tsx +0 -139
- package/src/vista/surfaces/elicitation-transport.spec.ts +0 -369
- package/src/vista/surfaces/elicitation-transport.ts +0 -367
- package/src/vista/surfaces/file-surface-utils.ts +0 -69
- package/src/vista/surfaces/index.ts +0 -24
- package/src/vista/surfaces/register-surfaces.tsx +0 -139
- package/src/vista/surfaces/text-surfaces.tsx +0 -132
- package/src/vista/use-vista-tabs.ts +0 -196
- package/src/vista/vista-code-renderer-context.ts +0 -16
- package/src/vista/vista-code-renderer.tsx +0 -35
- package/src/vista/vista-file-context.ts +0 -14
- package/src/vista/vista-file-source.tsx +0 -141
- package/src/vista/vista-form-context.ts +0 -20
- package/src/vista/vista-form-host.tsx +0 -48
- package/src/vista/vista-model.ts +0 -137
- package/src/vista/vista-open-context.tsx +0 -27
- package/src/vista/vista-open-state.ts +0 -11
- package/src/vista/vista-payloads.ts +0 -635
- package/src/vista/vista-tabs-manifest.ts +0 -138
- package/src/vista/vista-tools.ts +0 -232
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { type PaletteEntrySource } from './enums';
|
|
2
|
-
|
|
3
|
-
import type { CapabilityRef } from '@xemahq/kernel-contracts/capability';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Command-palette plane of the SystemBus.
|
|
7
|
-
*
|
|
8
|
-
* The palette is the UNION of two sources (tagged by
|
|
9
|
-
* {@link PaletteEntrySource}):
|
|
10
|
-
* 1. every capability the actor may invoke — sourced from
|
|
11
|
-
* `capability-registry-api` (the host's capability-registry client);
|
|
12
|
-
* 2. every registered cross-biome intent action — sourced from the
|
|
13
|
-
* SystemBus `intents` registry.
|
|
14
|
-
*
|
|
15
|
-
* The palette performs NO authorization. The capability source is already
|
|
16
|
-
* actor-scoped server-side (the registry returns only capabilities the
|
|
17
|
-
* subject can see); selecting an entry routes through `capability.invoke`,
|
|
18
|
-
* where the backend router makes the final allow/deny decision. The
|
|
19
|
-
* palette is pure presentation orchestration.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/** A palette entry backed by a capability descriptor. */
|
|
23
|
-
export interface CapabilityPaletteEntry {
|
|
24
|
-
readonly source: PaletteEntrySource.Capability;
|
|
25
|
-
/** Stable id (the capability ref doubles as the id). */
|
|
26
|
-
readonly id: string;
|
|
27
|
-
/** Human label rendered in the palette row. */
|
|
28
|
-
readonly label: string;
|
|
29
|
-
/** Optional one-line description from the capability descriptor. */
|
|
30
|
-
readonly summary?: string;
|
|
31
|
-
/** The capability the entry invokes when selected. */
|
|
32
|
-
readonly capabilityRef: CapabilityRef;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** A palette entry backed by a registered cross-biome intent. */
|
|
36
|
-
export interface IntentPaletteEntry {
|
|
37
|
-
readonly source: PaletteEntrySource.Intent;
|
|
38
|
-
/** Stable id, unique across registered intents. */
|
|
39
|
-
readonly id: string;
|
|
40
|
-
/** Human label rendered in the palette row. */
|
|
41
|
-
readonly label: string;
|
|
42
|
-
/** Biome that provides the intent action. */
|
|
43
|
-
readonly biomeId: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Discriminated union of palette entries. */
|
|
47
|
-
export type PaletteEntry = CapabilityPaletteEntry | IntentPaletteEntry;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Query against the palette. `text` is matched by the host's own ranker
|
|
51
|
-
* (the contract does not prescribe a fuzzy algorithm). `sources` narrows
|
|
52
|
-
* to a subset of {@link PaletteEntrySource}; omitted means "all sources".
|
|
53
|
-
*/
|
|
54
|
-
export interface PaletteQuery {
|
|
55
|
-
readonly text: string;
|
|
56
|
-
readonly sources?: readonly PaletteEntrySource[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* The `palette` surface of the SystemBus. Framework-agnostic — no React.
|
|
61
|
-
* The host wires a capability-registry client + the SystemBus intent
|
|
62
|
-
* registry behind `query`. `subscribe` lets a palette UI re-render when
|
|
63
|
-
* either source changes (a biome registers an intent, the registry
|
|
64
|
-
* refreshes).
|
|
65
|
-
*/
|
|
66
|
-
export interface PaletteBus {
|
|
67
|
-
/**
|
|
68
|
-
* Resolve palette entries matching `query`, ranked by the host. Async
|
|
69
|
-
* because the capability source may hit `capability-registry-api`.
|
|
70
|
-
*/
|
|
71
|
-
query(query: PaletteQuery): Promise<readonly PaletteEntry[]>;
|
|
72
|
-
/**
|
|
73
|
-
* Subscribe to palette-source changes. Returns an unsubscribe fn. Fires
|
|
74
|
-
* when a biome registers/unregisters an intent or the capability source
|
|
75
|
-
* is refreshed — so a palette overlay can re-query without polling.
|
|
76
|
-
*/
|
|
77
|
-
subscribe(listener: () => void): () => void;
|
|
78
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
parseDeeplink,
|
|
3
|
-
type DeeplinkBus,
|
|
4
|
-
type ParsedDeeplink,
|
|
5
|
-
} from './deeplink';
|
|
6
|
-
import { PaletteEntrySource } from './enums';
|
|
7
|
-
import {
|
|
8
|
-
type CapabilityInvoker,
|
|
9
|
-
type DeeplinkResolver,
|
|
10
|
-
type PaletteSource,
|
|
11
|
-
type WindowManager,
|
|
12
|
-
} from './host-ports';
|
|
13
|
-
import { IntentRegistry } from './intent-registry';
|
|
14
|
-
import {
|
|
15
|
-
type IntentPaletteEntry,
|
|
16
|
-
type PaletteBus,
|
|
17
|
-
type PaletteEntry,
|
|
18
|
-
type PaletteQuery,
|
|
19
|
-
} from './palette';
|
|
20
|
-
|
|
21
|
-
import type { SystemBus } from './system-bus';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Host-supplied ports needed to build a real {@link SystemBus}. The kernel
|
|
25
|
-
* ships the framework-agnostic pieces (`IntentRegistry`, `parseDeeplink`);
|
|
26
|
-
* the host supplies the concrete backend + shell behind these ports. NONE
|
|
27
|
-
* are optional — a missing port is a wiring bug and `buildSystemBus` fails
|
|
28
|
-
* fast.
|
|
29
|
-
*/
|
|
30
|
-
export interface SystemBusPorts {
|
|
31
|
-
/** Relays `capability.invoke` to the backend capability-router. */
|
|
32
|
-
readonly capability: CapabilityInvoker;
|
|
33
|
-
/** Sources the capability half of the palette from the registry. */
|
|
34
|
-
readonly palette: PaletteSource;
|
|
35
|
-
/** Realises the window plane (open / focus / close / arrange). */
|
|
36
|
-
readonly windows: WindowManager;
|
|
37
|
-
/** Navigates the running shell to a parsed deeplink target. */
|
|
38
|
-
readonly deeplink: DeeplinkResolver;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function assertPort<T>(value: T | undefined, name: string): T {
|
|
42
|
-
if (value === undefined || value === null) {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`[xema-ui-kernel] SystemBus port "${name}" was not supplied. ` +
|
|
45
|
-
'The host MUST provide every SystemBus port (capability / palette / ' +
|
|
46
|
-
'windows / deeplink) — there is no silent no-op fallback.',
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
return value;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Build the palette bus by unioning the host's capability source with the
|
|
54
|
-
* SystemBus intent registry. The capability half is already actor-scoped
|
|
55
|
-
* server-side; the intent half is flattened from every registration's
|
|
56
|
-
* provided actions. The host's `PaletteSource.queryCapabilities` applies
|
|
57
|
-
* its own ranking to the capability rows; intent rows are filtered here by
|
|
58
|
-
* a case-insensitive label/biome substring match (the contract leaves the
|
|
59
|
-
* ranker to the host — substring is the deterministic floor).
|
|
60
|
-
*
|
|
61
|
-
* Pure orchestration: no authorization. Selecting a capability entry
|
|
62
|
-
* routes through `capability.invoke` where the backend decides allow/deny.
|
|
63
|
-
*/
|
|
64
|
-
function buildPaletteBus(
|
|
65
|
-
capabilitySource: PaletteSource,
|
|
66
|
-
intents: IntentRegistry,
|
|
67
|
-
): PaletteBus {
|
|
68
|
-
function intentEntries(query: PaletteQuery): IntentPaletteEntry[] {
|
|
69
|
-
if (query.sources && !query.sources.includes(PaletteEntrySource.Intent)) {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
const needle = query.text.trim().toLowerCase();
|
|
73
|
-
const out: IntentPaletteEntry[] = [];
|
|
74
|
-
// The IntentRegistry exposes registrations only through `resolve(kind)`;
|
|
75
|
-
// for a palette we want every provided action regardless of kind, so we
|
|
76
|
-
// read the public snapshot the registry exposes via `snapshot()`.
|
|
77
|
-
for (const registration of intents.snapshot()) {
|
|
78
|
-
for (const action of registration.provides) {
|
|
79
|
-
if (
|
|
80
|
-
needle.length > 0 &&
|
|
81
|
-
!action.label.toLowerCase().includes(needle) &&
|
|
82
|
-
!registration.biomeId.toLowerCase().includes(needle)
|
|
83
|
-
) {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
out.push({
|
|
87
|
-
source: PaletteEntrySource.Intent,
|
|
88
|
-
id: `${registration.biomeId}:${action.id}`,
|
|
89
|
-
label: action.label,
|
|
90
|
-
biomeId: registration.biomeId,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return out;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
async query(query: PaletteQuery): Promise<readonly PaletteEntry[]> {
|
|
99
|
-
const wantsCapabilities =
|
|
100
|
-
!query.sources || query.sources.includes(PaletteEntrySource.Capability);
|
|
101
|
-
const capabilityEntries = wantsCapabilities
|
|
102
|
-
? await capabilitySource.queryCapabilities(query)
|
|
103
|
-
: [];
|
|
104
|
-
return [...capabilityEntries, ...intentEntries(query)];
|
|
105
|
-
},
|
|
106
|
-
subscribe(listener: () => void): () => void {
|
|
107
|
-
const offCapabilities = capabilitySource.subscribe(listener);
|
|
108
|
-
const offIntents = intents.subscribe(listener);
|
|
109
|
-
return () => {
|
|
110
|
-
offCapabilities();
|
|
111
|
-
offIntents();
|
|
112
|
-
};
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function buildDeeplinkBus(resolver: DeeplinkResolver): DeeplinkBus {
|
|
118
|
-
return {
|
|
119
|
-
parse(raw: string): ParsedDeeplink {
|
|
120
|
-
return parseDeeplink(raw);
|
|
121
|
-
},
|
|
122
|
-
async resolve(target: string | ParsedDeeplink): Promise<void> {
|
|
123
|
-
// Parse-first so a malformed deeplink fails fast in the kernel parser
|
|
124
|
-
// before the host navigation ever runs.
|
|
125
|
-
const parsed =
|
|
126
|
-
typeof target === 'string' ? parseDeeplink(target) : target;
|
|
127
|
-
await resolver.resolve(parsed);
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Construct the real {@link SystemBus} from the host's ports plus the
|
|
134
|
-
* kernel's framework-agnostic runtime (`IntentRegistry` + `parseDeeplink`).
|
|
135
|
-
* The `intents` registry is owned by the bus instance so biome
|
|
136
|
-
* registrations and palette intent rows share one source of truth.
|
|
137
|
-
*
|
|
138
|
-
* Fail-fast: every port is required. No port may be silently stubbed. This
|
|
139
|
-
* is the framework-agnostic builder — any host adapter (Next.js, React-
|
|
140
|
-
* Router, …) wires its concrete ports and calls this.
|
|
141
|
-
*/
|
|
142
|
-
export function buildSystemBus(ports: SystemBusPorts): SystemBus {
|
|
143
|
-
const capability = assertPort(ports.capability, 'capability');
|
|
144
|
-
const paletteSource = assertPort(ports.palette, 'palette');
|
|
145
|
-
const windows = assertPort(ports.windows, 'windows');
|
|
146
|
-
const deeplinkResolver = assertPort(ports.deeplink, 'deeplink');
|
|
147
|
-
|
|
148
|
-
// Concrete type (not the `IntentBus` interface): the palette builder
|
|
149
|
-
// reads `intents.snapshot()`, which is an `IntentRegistry` affordance.
|
|
150
|
-
const intents: IntentRegistry = new IntentRegistry();
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
capability,
|
|
154
|
-
intents,
|
|
155
|
-
palette: buildPaletteBus(paletteSource, intents),
|
|
156
|
-
deeplink: buildDeeplinkBus(deeplinkResolver),
|
|
157
|
-
windows,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { CapabilityBus } from './capability-invoke';
|
|
2
|
-
import type { DeeplinkBus } from './deeplink';
|
|
3
|
-
import type { IntentBus } from './intents';
|
|
4
|
-
import type { PaletteBus } from './palette';
|
|
5
|
-
import type { WindowBus } from './windows';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* `SystemBus` — the framework-agnostic OS-orchestration plane every
|
|
9
|
-
* frontend biome reaches through its {@link HostBridge}.
|
|
10
|
-
*
|
|
11
|
-
* **Pure orchestration. Zero policy.** The SystemBus routes intent,
|
|
12
|
-
* navigation, capability invocation, palette, and windowing. It NEVER
|
|
13
|
-
* authorizes, denies, or duplicates policy. Every `capability.invoke`
|
|
14
|
-
* routes through the backend capability-router, which is the single
|
|
15
|
-
* authority on auth / tenancy / policy / audit. The SystemBus's job is to
|
|
16
|
-
* carry orchestration metadata faithfully and relay the router's outcome —
|
|
17
|
-
* a denial surfaces as an observable result (`output === undefined` +
|
|
18
|
-
* `auditId`), never a silent swallow.
|
|
19
|
-
*
|
|
20
|
-
* The contract is framework-agnostic on purpose: it imports no Vite, no
|
|
21
|
-
* Next.js, no React-Router, and no generated transport client. A concrete
|
|
22
|
-
* host (Vite SPA today, Next.js tomorrow) implements each sub-bus against
|
|
23
|
-
* its router, its capability-router client, and its window manager. Biome
|
|
24
|
-
* code is identical across hosts.
|
|
25
|
-
*/
|
|
26
|
-
export interface SystemBus {
|
|
27
|
-
/** Capability invocation → backend router (backend enforces; bus adds none). */
|
|
28
|
-
readonly capability: CapabilityBus;
|
|
29
|
-
/** Cross-biome "Open with… / Send to…" intent registry. */
|
|
30
|
-
readonly intents: IntentBus;
|
|
31
|
-
/** Command palette = capabilities ∪ registered intents. */
|
|
32
|
-
readonly palette: PaletteBus;
|
|
33
|
-
/** Typed `xema://` deeplink parse + resolve. */
|
|
34
|
-
readonly deeplink: DeeplinkBus;
|
|
35
|
-
/** Host window manager (open / focus / close / arrange). */
|
|
36
|
-
readonly windows: WindowBus;
|
|
37
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { type WindowArrangement, WindowOp } from './enums';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Window-management plane of the SystemBus.
|
|
5
|
-
*
|
|
6
|
-
* A "window" is a host-managed surface hosting a biome route or object.
|
|
7
|
-
* The contract is framework-agnostic: the host (desktop shell, tiling WM,
|
|
8
|
-
* tabbed SPA) decides how a window is physically realised, but every host
|
|
9
|
-
* exposes the same closed {@link WindowOp} verb set. The SystemBus carries
|
|
10
|
-
* no authorization here — windowing is pure shell orchestration.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** Identifier of an open window, minted by the host on `open`. */
|
|
14
|
-
export type WindowId = string;
|
|
15
|
-
|
|
16
|
-
/** A request to open a new window onto a deeplink target. */
|
|
17
|
-
export interface OpenWindowRequest {
|
|
18
|
-
/** The `xema://` deeplink the new window should display. */
|
|
19
|
-
readonly target: string;
|
|
20
|
-
/** Optional human title; the host falls back to the target's own title. */
|
|
21
|
-
readonly title?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Snapshot of an open window. */
|
|
25
|
-
export interface WindowState {
|
|
26
|
-
readonly id: WindowId;
|
|
27
|
-
readonly target: string;
|
|
28
|
-
readonly title: string;
|
|
29
|
-
readonly focused: boolean;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The `windows` surface of the SystemBus. Each method maps to a closed
|
|
34
|
-
* {@link WindowOp}. Operations a host cannot honour reject fail-fast
|
|
35
|
-
* rather than silently no-op, so callers never assume a window exists
|
|
36
|
-
* when it does not.
|
|
37
|
-
*/
|
|
38
|
-
export interface WindowBus {
|
|
39
|
-
/** {@link WindowOp.Open} — open a new window; resolves with its id. */
|
|
40
|
-
open(request: OpenWindowRequest): Promise<WindowId>;
|
|
41
|
-
/** {@link WindowOp.Focus} — bring a window to the foreground. Rejects on unknown id. */
|
|
42
|
-
focus(id: WindowId): Promise<void>;
|
|
43
|
-
/** {@link WindowOp.Close} — close a window. Rejects on unknown id. */
|
|
44
|
-
close(id: WindowId): Promise<void>;
|
|
45
|
-
/** {@link WindowOp.Arrange} — apply a layout to all open windows. */
|
|
46
|
-
arrange(arrangement: WindowArrangement): Promise<void>;
|
|
47
|
-
/** Current snapshot of open windows (for a window-list UI). */
|
|
48
|
-
list(): readonly WindowState[];
|
|
49
|
-
/** Subscribe to window-set changes; returns an unsubscribe fn. */
|
|
50
|
-
subscribe(listener: () => void): () => void;
|
|
51
|
-
}
|
package/src/offline/id.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/** RFC-4122 id, using the platform crypto when available (browser + Node 18+),
|
|
2
|
-
* with a non-cryptographic fallback for exotic runtimes. */
|
|
3
|
-
export function newId(): string {
|
|
4
|
-
const c = (globalThis as { crypto?: Crypto }).crypto;
|
|
5
|
-
if (c && typeof c.randomUUID === 'function') {
|
|
6
|
-
return c.randomUUID();
|
|
7
|
-
}
|
|
8
|
-
// Fallback — good enough for an outbox entry id (not a security token).
|
|
9
|
-
return (
|
|
10
|
-
'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (ch) => {
|
|
11
|
-
const r = (Math.floor(Math.random() * 16) + Date.now()) % 16;
|
|
12
|
-
const v = ch === 'x' ? r : (r & 0x3) | 0x8;
|
|
13
|
-
return v.toString(16);
|
|
14
|
-
})
|
|
15
|
-
);
|
|
16
|
-
}
|
package/src/offline/index.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// @xemahq/ui-kernel/offline — generic offline field-capture kit
|
|
3
|
-
//
|
|
4
|
-
// A durable, provider-agnostic mutation outbox: queue mutations while
|
|
5
|
-
// disconnected (IndexedDB) and drain them on reconnect with retry, backoff, and
|
|
6
|
-
// idempotency. Pairs with the existing guided-capture kit (`../ui/capture`) and
|
|
7
|
-
// the existing mobile off-canvas shell so a form captured in the field (trade
|
|
8
|
-
// fair, no signal) is never lost. The immediate-online path is unchanged; only
|
|
9
|
-
// a failed/offline send is queued.
|
|
10
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
-
|
|
12
|
-
export { newId } from './id';
|
|
13
|
-
export {
|
|
14
|
-
OutboxEntryStatus,
|
|
15
|
-
type OutboxEntry,
|
|
16
|
-
type OutboxStorage,
|
|
17
|
-
type OutboxExecutor,
|
|
18
|
-
type OutboxExecuteContext,
|
|
19
|
-
type OutboxSnapshot,
|
|
20
|
-
} from './types';
|
|
21
|
-
export {
|
|
22
|
-
InMemoryOutboxStorage,
|
|
23
|
-
IndexedDbOutboxStorage,
|
|
24
|
-
createDefaultOutboxStorage,
|
|
25
|
-
} from './storage';
|
|
26
|
-
export {
|
|
27
|
-
OnlineController,
|
|
28
|
-
isNetworkUnavailableError,
|
|
29
|
-
type OnlineListener,
|
|
30
|
-
} from './online-controller';
|
|
31
|
-
export {
|
|
32
|
-
Outbox,
|
|
33
|
-
type OutboxOptions,
|
|
34
|
-
type OutboxListener,
|
|
35
|
-
type EnqueueOptions,
|
|
36
|
-
} from './outbox';
|
|
37
|
-
export {
|
|
38
|
-
createOfflineMutationFn,
|
|
39
|
-
type OfflineMutationResult,
|
|
40
|
-
type OfflineMutationOptions,
|
|
41
|
-
} from './mutation';
|
|
42
|
-
export {
|
|
43
|
-
OutboxProvider,
|
|
44
|
-
type OutboxProviderProps,
|
|
45
|
-
} from './react';
|
|
46
|
-
export {
|
|
47
|
-
useOutbox,
|
|
48
|
-
useOutboxSnapshot,
|
|
49
|
-
useOnlineStatus,
|
|
50
|
-
} from './react-hooks';
|
|
51
|
-
export {
|
|
52
|
-
OfflineStatusBadge,
|
|
53
|
-
type OfflineStatusBadgeProps,
|
|
54
|
-
} from './offline-status-badge';
|
package/src/offline/mutation.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { newId } from './id';
|
|
2
|
-
import { isNetworkUnavailableError } from './online-controller';
|
|
3
|
-
|
|
4
|
-
import type { Outbox } from './outbox';
|
|
5
|
-
import type { OutboxExecuteContext } from './types';
|
|
6
|
-
|
|
7
|
-
/** Outcome of an offline-aware mutation. */
|
|
8
|
-
export interface OfflineMutationResult<TResult> {
|
|
9
|
-
/** True when the mutation was queued for later delivery (offline / network
|
|
10
|
-
* failure) instead of applied immediately. */
|
|
11
|
-
queued: boolean;
|
|
12
|
-
/** The outbox entry id when queued (for optimistic UI correlation). */
|
|
13
|
-
entryId?: string;
|
|
14
|
-
/** The server result when applied immediately (`queued === false`). */
|
|
15
|
-
result?: TResult;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface OfflineMutationOptions<TPayload, TResult> {
|
|
19
|
-
outbox: Outbox;
|
|
20
|
-
/** Stable executor key for this mutation type (e.g. "app-forge:create-record"). */
|
|
21
|
-
kind: string;
|
|
22
|
-
/** Sends the mutation to the backend. Used BOTH for the immediate online path
|
|
23
|
-
* and for draining the queue. Pass `ctx.idempotencyKey` to the backend so a
|
|
24
|
-
* redelivered mutation is not double-applied. */
|
|
25
|
-
send: (payload: TPayload, ctx: OutboxExecuteContext) => Promise<TResult>;
|
|
26
|
-
/** Optional label for the pending-items UI. */
|
|
27
|
-
label?: (payload: TPayload) => string | undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Build an offline-aware `mutationFn` (drop-in for `@tanstack/react-query`'s
|
|
32
|
-
* `useMutation`). When online it sends immediately; if the send fails because
|
|
33
|
-
* the network is unavailable — or the app is already offline — the payload is
|
|
34
|
-
* durably queued and the promise RESOLVES (queued: true) so the UI can update
|
|
35
|
-
* optimistically. The outbox drains on reconnect. A real server rejection (4xx)
|
|
36
|
-
* still throws so the form shows the validation error.
|
|
37
|
-
*
|
|
38
|
-
* Registers the outbox executor for `kind` once (idempotent).
|
|
39
|
-
*/
|
|
40
|
-
export function createOfflineMutationFn<TPayload, TResult>(
|
|
41
|
-
opts: OfflineMutationOptions<TPayload, TResult>,
|
|
42
|
-
): (payload: TPayload) => Promise<OfflineMutationResult<TResult>> {
|
|
43
|
-
const { outbox, kind, send, label } = opts;
|
|
44
|
-
|
|
45
|
-
if (!outbox.hasExecutor(kind)) {
|
|
46
|
-
outbox.registerExecutor<TPayload>(kind, async (payload, ctx) => {
|
|
47
|
-
await send(payload, ctx);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return async (payload: TPayload): Promise<OfflineMutationResult<TResult>> => {
|
|
52
|
-
const idempotencyKey = newId();
|
|
53
|
-
|
|
54
|
-
const queue = async (): Promise<OfflineMutationResult<TResult>> => {
|
|
55
|
-
const entry = await outbox.enqueue(kind, payload, {
|
|
56
|
-
idempotencyKey,
|
|
57
|
-
...(label ? { label: label(payload) } : {}),
|
|
58
|
-
});
|
|
59
|
-
return { queued: true, entryId: entry.id };
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
if (!outbox.isOnline()) {
|
|
63
|
-
return queue();
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
const result = await send(payload, { idempotencyKey, attempt: 1, entryId: 'direct' });
|
|
67
|
-
return { queued: false, result };
|
|
68
|
-
} catch (err) {
|
|
69
|
-
if (isNetworkUnavailableError(err)) {
|
|
70
|
-
return queue();
|
|
71
|
-
}
|
|
72
|
-
throw err; // genuine server rejection — surface it to the caller
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { useOutboxSnapshot } from './react-hooks';
|
|
2
|
-
|
|
3
|
-
import type { Outbox } from './outbox';
|
|
4
|
-
|
|
5
|
-
export interface OfflineStatusBadgeProps {
|
|
6
|
-
/** Explicit outbox; falls back to the nearest OutboxProvider. */
|
|
7
|
-
outbox?: Outbox;
|
|
8
|
-
className?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const BASE =
|
|
12
|
-
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-body-2 font-medium';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* A compact connectivity + queue indicator for the offline field-capture kit.
|
|
16
|
-
* Renders nothing when online with an empty queue; otherwise surfaces the
|
|
17
|
-
* offline / syncing / dead-letter state so the user knows captures are safe and
|
|
18
|
-
* will sync. Self-contained (no UI-primitive deps) so it can live in the
|
|
19
|
-
* framework-level offline module.
|
|
20
|
-
*/
|
|
21
|
-
export function OfflineStatusBadge({ outbox, className }: OfflineStatusBadgeProps) {
|
|
22
|
-
const { online, pendingCount, failedCount, draining } = useOutboxSnapshot(outbox);
|
|
23
|
-
|
|
24
|
-
if (online && pendingCount === 0 && failedCount === 0) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const cls = (tone: string): string =>
|
|
29
|
-
`${BASE} ${tone}${className ? ` ${className}` : ''}`;
|
|
30
|
-
|
|
31
|
-
if (failedCount > 0) {
|
|
32
|
-
return (
|
|
33
|
-
<span className={cls('bg-destructive/10 text-destructive')} role="status">
|
|
34
|
-
⚠ {failedCount} failed to sync
|
|
35
|
-
</span>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
if (!online) {
|
|
39
|
-
return (
|
|
40
|
-
<span className={cls('bg-amber-500/10 text-amber-600')} role="status">
|
|
41
|
-
● Offline{pendingCount > 0 ? ` — ${pendingCount} queued` : ''}
|
|
42
|
-
</span>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
// Online with a non-empty queue → actively draining.
|
|
46
|
-
return (
|
|
47
|
-
<span className={cls('bg-secondary text-secondary-foreground')} role="status">
|
|
48
|
-
{draining ? '⟳' : '●'} Syncing {pendingCount}…
|
|
49
|
-
</span>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
export type OnlineListener = (online: boolean) => void;
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Tracks connectivity via `navigator.onLine` + the window `online`/`offline`
|
|
5
|
-
* events. SSR-safe: when there is no `window`/`navigator` it reports online (so
|
|
6
|
-
* server rendering never queues) and is inert. `navigator.onLine` only knows
|
|
7
|
-
* about the local link, so callers may also mark the controller offline when a
|
|
8
|
-
* request fails as network-unavailable (`reportOffline`) and online again after
|
|
9
|
-
* a successful send (`reportOnline`).
|
|
10
|
-
*/
|
|
11
|
-
export class OnlineController {
|
|
12
|
-
private online: boolean;
|
|
13
|
-
private readonly listeners = new Set<OnlineListener>();
|
|
14
|
-
private started = false;
|
|
15
|
-
|
|
16
|
-
private readonly onOnline = () => this.set(true);
|
|
17
|
-
private readonly onOffline = () => this.set(false);
|
|
18
|
-
|
|
19
|
-
constructor() {
|
|
20
|
-
this.online =
|
|
21
|
-
typeof navigator === 'undefined' || typeof navigator.onLine !== 'boolean'
|
|
22
|
-
? true
|
|
23
|
-
: navigator.onLine;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
isOnline(): boolean {
|
|
27
|
-
return this.online;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
start(): void {
|
|
31
|
-
if (this.started || typeof window === 'undefined') {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
this.started = true;
|
|
35
|
-
window.addEventListener('online', this.onOnline);
|
|
36
|
-
window.addEventListener('offline', this.onOffline);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
stop(): void {
|
|
40
|
-
if (!this.started || typeof window === 'undefined') {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
this.started = false;
|
|
44
|
-
window.removeEventListener('online', this.onOnline);
|
|
45
|
-
window.removeEventListener('offline', this.onOffline);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Force offline after a network-unavailable request failure. */
|
|
49
|
-
reportOffline(): void {
|
|
50
|
-
this.set(false);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Force online after a successful send (recovers from a stale offline flag). */
|
|
54
|
-
reportOnline(): void {
|
|
55
|
-
this.set(true);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
subscribe(listener: OnlineListener): () => void {
|
|
59
|
-
this.listeners.add(listener);
|
|
60
|
-
return () => this.listeners.delete(listener);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private set(online: boolean): void {
|
|
64
|
-
if (this.online === online) {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
this.online = online;
|
|
68
|
-
for (const l of this.listeners) {
|
|
69
|
-
l(online);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Best-effort classifier for "the request failed because the network was
|
|
76
|
-
* unavailable" (offline, DNS, connection refused) vs a real server rejection.
|
|
77
|
-
* A fetch that never reaches the server rejects with a TypeError ("Failed to
|
|
78
|
-
* fetch"); an aborted request has name AbortError. Anything with an HTTP status
|
|
79
|
-
* is a server response, NOT a network failure.
|
|
80
|
-
*/
|
|
81
|
-
export function isNetworkUnavailableError(err: unknown): boolean {
|
|
82
|
-
if (typeof navigator !== 'undefined' && navigator.onLine === false) {
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
if (!err || typeof err !== 'object') {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
const e = err as { name?: string; message?: string; status?: number; response?: unknown };
|
|
89
|
-
// A response with a status is a server answer, not a transport failure.
|
|
90
|
-
if (typeof e.status === 'number' || e.response !== undefined) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
if (e.name === 'AbortError') {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
const msg = (e.message ?? '').toLowerCase();
|
|
97
|
-
return (
|
|
98
|
-
e.name === 'TypeError' ||
|
|
99
|
-
msg.includes('failed to fetch') ||
|
|
100
|
-
msg.includes('network') ||
|
|
101
|
-
msg.includes('load failed') ||
|
|
102
|
-
msg.includes('econnrefused') ||
|
|
103
|
-
msg.includes('enotfound')
|
|
104
|
-
);
|
|
105
|
-
}
|