@declarion/react 0.5.0 → 0.6.1

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.
@@ -7,9 +7,11 @@ export { classifyInboundMessage, createInboundMessageListener, type InboundClass
7
7
  export { EmbedAuthBootstrap } from "./EmbedAuthBootstrap";
8
8
  export { EmbedResizeReporter } from "./EmbedResizeReporter";
9
9
  export { EmbedDirtyReporter } from "./EmbedDirtyReporter";
10
- export { EMBED_MESSAGE_SOURCE, EMBED_PROTOCOL_VERSION, EMBED_MESSAGE_TYPES, postToParent, type EmbedMessageType, type EmbedMessage, type EmbedMessagePayloadMap, type EmbedReadyPayload, type EmbedSetTokenPayload, type EmbedTokenExpiredPayload, type EmbedReloadRequiredPayload, type EmbedResizedPayload, type EmbedNavigationPayload, type EmbedDirtyChangedPayload, type EmbedNavigatePayload, type EmbedSetThemePayload, } from "./protocol";
11
- export { resolveEmbedNavigationPayload } from "./screen-location";
12
- export { useEmbedNavigate, useEmbedScreenNavigate } from "./useEmbedNavigate";
10
+ export { EMBED_MESSAGE_SOURCE, EMBED_PROTOCOL_VERSION, EMBED_MESSAGE_TYPES, postToParent, type EmbedMessageType, type EmbedMessage, type EmbedMessagePayloadMap, type EmbedReadyPayload, type EmbedSetTokenPayload, type EmbedTokenExpiredPayload, type EmbedReloadRequiredPayload, type EmbedResizedPayload, type EmbedNavigationPayload, type EmbedDirtyChangedPayload, type EmbedNavigationFailedPayload, type EmbedNavigatePayload, type EmbedSetThemePayload, } from "./protocol";
11
+ export { resolveEmbedNavigation, resolveEmbedNavigationPayload, } from "./screen-location";
12
+ export type { EmbedNavigationResolution } from "./screen-location";
13
+ export { useEmbedNavigate, useEmbedScreenNavigate, useEmbedNavigationFailed, } from "./useEmbedNavigate";
14
+ export { useGlobalLinkInterceptor, interceptedNavigationTarget, interceptedProgrammaticTarget, NATIVE_LINK_ATTR, } from "./useGlobalLinkInterceptor";
13
15
  export { EmbedLocationReporter } from "./EmbedLocationReporter";
14
16
  export { useEmbedDirtyStore, useReportEmbedDirty } from "./embed-dirty";
15
17
  export { EmbedUnsavedGuard } from "./EmbedUnsavedGuard";
@@ -32,6 +32,9 @@ export declare const EMBED_MESSAGE_TYPES: {
32
32
  readonly navigationRequested: "navigation-requested";
33
33
  /** iframe -> host: the embedded screen's unsaved-edits state flipped. */
34
34
  readonly dirtyChanged: "dirty-changed";
35
+ /** iframe -> host: an in-frame navigation could not be handled (unknown
36
+ * route, or a 404 the host should recover from). The iframe stayed put. */
37
+ readonly navigationFailed: "navigation-failed";
35
38
  /** host -> iframe: drive the iframe to a screen (deep-linking). */
36
39
  readonly navigate: "navigate";
37
40
  /** host -> iframe: runtime theme switch. */
@@ -62,14 +65,22 @@ export interface EmbedResizedPayload {
62
65
  }
63
66
  /**
64
67
  * `navigated` / `navigation-requested` payload: the screen route and, when
65
- * resolvable, the bound entity code and record id.
68
+ * resolvable, the matched screen code, bound entity code, and record id.
66
69
  *
67
- * `entity` and `recordId` are optional: a `custom` screen has no entity, and
68
- * a list route has no record id.
70
+ * `screenCode`, `entity`, and `recordId` are optional: an unregistered path
71
+ * resolves to none of them, a `custom` screen has no entity, and a list route
72
+ * has no record id. The host needs `screenCode` to mint a scoped embed token
73
+ * for the target screen when it executes a `delegated` navigation intent.
69
74
  */
70
75
  export interface EmbedNavigationPayload {
71
76
  /** The Declarion screen route path the navigation targets. */
72
77
  readonly route: string;
78
+ /**
79
+ * The matched screen's code (its key in the schema screen map), when a
80
+ * registered screen owns the route. Absent for an unresolved path. The host
81
+ * passes this as `screen_code` to `auth.create_embed_session`.
82
+ */
83
+ readonly screenCode?: string;
73
84
  /** The bound entity code, when the route resolves to one. */
74
85
  readonly entity?: string;
75
86
  /** The record id, when the route is a detail route with an id. */
@@ -85,6 +96,21 @@ export interface EmbedDirtyChangedPayload {
85
96
  /** True when the embedded screen has unsaved edits. */
86
97
  readonly dirty: boolean;
87
98
  }
99
+ /**
100
+ * `navigation-failed` payload: an in-frame navigation the iframe refused or
101
+ * could not resolve. The iframe stayed put; the host owns recovery (show its
102
+ * own not-found, reset its sidebar selection, etc.).
103
+ *
104
+ * `reason` is a closed set, each with a real producer: `not_found` from the
105
+ * embedded 404 catch-all, `unresolved` from the navigation chokepoint when the
106
+ * target route matches no registered screen.
107
+ */
108
+ export interface EmbedNavigationFailedPayload {
109
+ /** The route that could not be navigated to. */
110
+ readonly route: string;
111
+ /** Why it failed. */
112
+ readonly reason: "not_found" | "unresolved";
113
+ }
88
114
  /** `navigate` payload: the route the host drives the iframe to. */
89
115
  export interface EmbedNavigatePayload {
90
116
  /** The Declarion screen route the host wants opened. */
@@ -108,6 +134,7 @@ export interface EmbedMessagePayloadMap {
108
134
  [EMBED_MESSAGE_TYPES.navigated]: EmbedNavigationPayload;
109
135
  [EMBED_MESSAGE_TYPES.navigationRequested]: EmbedNavigationPayload;
110
136
  [EMBED_MESSAGE_TYPES.dirtyChanged]: EmbedDirtyChangedPayload;
137
+ [EMBED_MESSAGE_TYPES.navigationFailed]: EmbedNavigationFailedPayload;
111
138
  [EMBED_MESSAGE_TYPES.navigate]: EmbedNavigatePayload;
112
139
  [EMBED_MESSAGE_TYPES.setTheme]: EmbedSetThemePayload;
113
140
  }
@@ -1,20 +1,50 @@
1
1
  import type { EmbedNavigationPayload } from "./protocol";
2
2
  import type { Screen } from "../types/schema";
3
3
  /**
4
- * Resolve the embed navigation payload for a router pathname.
4
+ * The result of resolving a router pathname against the screen schema.
5
+ *
6
+ * `matched` is the load-bearing flag for the navigation chokepoint: when a
7
+ * registered screen owns the path it is `true` and `screenCode` names that
8
+ * screen; when no screen owns the path it is `false` (the chokepoint then
9
+ * refuses an embed navigation and emits `navigation-failed`). `payload` is
10
+ * always present - it carries the matched screen route (or the raw pathname
11
+ * when unmatched) for the `navigated` / `navigation-requested` messages.
12
+ */
13
+ export interface EmbedNavigationResolution {
14
+ /** True when a registered screen route owns this pathname. */
15
+ readonly matched: boolean;
16
+ /** The matched screen's code (its key in `screens`), when matched. */
17
+ readonly screenCode?: string;
18
+ /** The matched screen's `type` (`list` / `detail` / `custom` / ...), when
19
+ * matched. The same-surface rule reads it to identify a true list/detail
20
+ * pair (vs. a same-entity custom or record-list screen). */
21
+ readonly screenType?: string;
22
+ /** The navigation payload to emit (route always set; ids when resolvable). */
23
+ readonly payload: EmbedNavigationPayload;
24
+ }
25
+ /**
26
+ * Resolve a router pathname against the screen schema, with match status.
5
27
  *
6
28
  * Matches the pathname against every registered screen route. An exact
7
29
  * (wildcard-free) route wins over a `:param` route, mirroring
8
30
  * `screenTitleForPath`, so `/cases` resolves to the list screen even though
9
31
  * the detail route `/cases/:id` also shares the prefix.
10
32
  *
11
- * - `route` is the matched screen's own route pattern, or the raw pathname
12
- * when no screen owns it (a route always exists in the payload).
13
- * - `entity` is the matched screen's bound entity code, when it has one.
14
- * - `recordId` is the last path segment for a detail (`:param`) route, when
15
- * that segment is present and non-empty.
33
+ * - `matched` is true only when a registered screen owns the path.
34
+ * - `screenCode` is the matched screen's map key (so the host can mint a
35
+ * scoped token for it).
36
+ * - `payload.route` is the matched screen's own route pattern, or the raw
37
+ * pathname when no screen owns it (a route always exists in the payload).
38
+ * - `payload.entity` is the matched screen's bound entity code, when it has one.
39
+ * - `payload.recordId` is the last path segment for a detail (`:param`) route,
40
+ * when that segment is present and non-empty.
16
41
  *
17
- * `screens` may be undefined (schema not yet loaded); the payload then
18
- * carries just the raw pathname as `route`.
42
+ * `screens` may be undefined (schema not yet loaded); the result is then
43
+ * `matched: false` with the raw pathname as `payload.route`.
44
+ */
45
+ export declare function resolveEmbedNavigation(pathname: string, screens: Record<string, Screen> | undefined): EmbedNavigationResolution;
46
+ /**
47
+ * Convenience wrapper returning just the payload, for the `navigated` and
48
+ * `navigation-requested` emitters that do not need the match status.
19
49
  */
20
50
  export declare function resolveEmbedNavigationPayload(pathname: string, screens: Record<string, Screen> | undefined): EmbedNavigationPayload;
@@ -1,4 +1,5 @@
1
1
  import { type NavigateOptions } from "react-router-dom";
2
+ import { type EmbedNavigationFailedPayload } from "./protocol";
2
3
  /**
3
4
  * The shared embed navigation policy, decoupled from React Router's hook.
4
5
  *
@@ -25,3 +26,11 @@ export declare function useEmbedScreenNavigate(): (to: string, opts?: NavigateOp
25
26
  * them.
26
27
  */
27
28
  export declare function useEmbedNavigate(): (to: string, opts?: NavigateOptions) => void;
29
+ /**
30
+ * Emit a `navigation-failed` event to the host. Used where an in-frame
31
+ * navigation cannot be handled OUTSIDE the chokepoint - chiefly the embedded
32
+ * 404 page, which the iframe reaches via a host-driven `navigate` command, a
33
+ * bad initial route, or a hard reload (none of which pass through the
34
+ * chokepoint's own guard). A no-op outside embed mode.
35
+ */
36
+ export declare function useEmbedNavigationFailed(): (route: string, reason: EmbedNavigationFailedPayload["reason"]) => void;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Attribute that opts an anchor out of interception. A react-router `<Link>`
3
+ * (or any anchor) that must keep native / router-managed click semantics
4
+ * (relative resolution, `replace`, navigation `state`) carries this and the
5
+ * interceptor leaves it to the browser / the router's own onClick.
6
+ */
7
+ export declare const NATIVE_LINK_ATTR = "data-declarion-native-link";
8
+ /**
9
+ * Decide whether a click should be intercepted for in-app navigation, and to
10
+ * where. Returns the in-app path (`pathname + search + hash`) to navigate to,
11
+ * or `null` to leave the click to the browser.
12
+ *
13
+ * Pure (no side effects, no `preventDefault`): the caller prevents default and
14
+ * routes. The guard order is cheap-first so the common "not for us" click
15
+ * exits immediately. Mirrors React Router `shouldProcessLinkClick`, Turbo
16
+ * `LinkClickObserver`, and the SvelteKit client router.
17
+ */
18
+ export declare function interceptedNavigationTarget(event: MouseEvent, loc?: {
19
+ origin: string;
20
+ pathname: string;
21
+ search: string;
22
+ }): string | null;
23
+ /**
24
+ * Minimal structural shape of the Navigation API `navigate` event - typed
25
+ * locally because `window.navigation` / `NavigateEvent` are not yet in the
26
+ * ambient DOM lib across our toolchain.
27
+ */
28
+ export interface NavigateEventLike {
29
+ readonly canIntercept: boolean;
30
+ readonly hashChange: boolean;
31
+ readonly downloadRequest: string | null;
32
+ readonly formData: unknown;
33
+ readonly cancelable: boolean;
34
+ readonly destination: {
35
+ readonly url: string;
36
+ readonly sameDocument: boolean;
37
+ };
38
+ preventDefault(): void;
39
+ }
40
+ /**
41
+ * Decide whether a Navigation API `navigate` event is a programmatic, same-
42
+ * origin, FULL-document escape that must be kept inside the SPA / embed
43
+ * boundary, and to where. Returns the in-app path, or null to leave it alone.
44
+ *
45
+ * This is the layer the click interceptor cannot cover: `location.assign`,
46
+ * `location.href = ...`, `<meta http-equiv=refresh>`, etc. - navigations not
47
+ * triggered by an anchor click. Same-document navigations (react-router's own
48
+ * `pushState` SPA transitions) are skipped via `destination.sameDocument`, so
49
+ * we never re-process or loop on our own router. Cross-origin and non-http
50
+ * destinations are left to the browser (the iframe sandbox contains them);
51
+ * downloads and hash changes are left alone. A POST form submission carries a
52
+ * body (`formData` non-null) and must reach the server, so it is left alone
53
+ * too; a GET form navigating to a same-origin route has no body and is routed
54
+ * like any internal navigation (kept in-frame, not full-loaded out of embed).
55
+ */
56
+ export declare function interceptedProgrammaticTarget(event: NavigateEventLike, loc?: {
57
+ origin: string;
58
+ }): string | null;
59
+ /**
60
+ * Mount the single global link interceptor. Call once, high in the tree that
61
+ * renders screens (the app `Layout`), so it covers every embedded and
62
+ * standalone screen. It is intentionally NOT mounted on the auth routes
63
+ * (`/login`, `/signup`, ...), which live outside `Layout`, so those pages keep
64
+ * their native react-router `<Link>` semantics.
65
+ */
66
+ export declare function useGlobalLinkInterceptor(): void;