@declarion/react 0.26.3 → 0.27.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.
@@ -0,0 +1,72 @@
1
+ import { type ReturnTarget, type TabItem } from "../stores/tabs";
2
+ /** Drop a leading `?` so a search string matches the canonical `TabItem.search`
3
+ * / `ReturnTarget.search` form (stored WITHOUT `?`). Empty in -> empty out. */
4
+ export declare function stripLeadingQuestion(search: string | undefined | null): string;
5
+ /** Re-attach the leading `?` for the router/`navigate` form, or `undefined`
6
+ * when there is no query. The inverse of `stripLeadingQuestion`. */
7
+ export declare function toRouterSearch(search: string | undefined): string | undefined;
8
+ /** The current location at navigate time (canonical: `search` may carry `?`,
9
+ * `hash` carries `#`). Sourced from `window.location` at the click. */
10
+ export interface CaptureLocation {
11
+ pathname: string;
12
+ search?: string;
13
+ hash?: string;
14
+ }
15
+ /**
16
+ * Build the return target to stamp on the DESTINATION tab when navigating from
17
+ * `current` to `destinationPathname`. Returns `undefined` (no capture) when:
18
+ * - the current path is not a same-origin in-app route,
19
+ * - the current path is an excluded (auth) route,
20
+ * - the destination shares the current PATHNAME (self guard, Invariant 7) -
21
+ * a same-screen, different-query link must not self-stamp a Back.
22
+ *
23
+ * `originTab` supplies metadata (id/title/icon/screen) for switching to or
24
+ * reopening the origin; only the current location drives pathname/search/hash.
25
+ */
26
+ export declare function captureReturnTarget(current: CaptureLocation, destinationPathname: string, originTab?: Pick<TabItem, "id" | "title" | "icon" | "screen">): ReturnTarget | undefined;
27
+ /** The Back visibility intent, derived from a tab's `returnTo` alone (no tab
28
+ * list needed). `hasOriginBack` drives an origin Back affordance;
29
+ * `allowFallbackBack` permits a screen's static/parent fallback (false ONLY
30
+ * for a menu-cleared `null`). */
31
+ export interface BackVisibility {
32
+ hasOriginBack: boolean;
33
+ allowFallbackBack: boolean;
34
+ }
35
+ /**
36
+ * Interpret the tri-state `returnTo` for Back VISIBILITY:
37
+ * - embed enabled -> never an origin Back; fallback allowed (embed has no tabs),
38
+ * - `null` (menu-cleared) -> no origin Back AND no fallback (a menu-opened
39
+ * screen has nowhere to return),
40
+ * - a valid origin object -> origin Back,
41
+ * - `undefined` / invalid object (deep-link) -> no origin Back, fallback allowed.
42
+ */
43
+ export declare function resolveBackVisibility(returnTo: ReturnTarget | null | undefined, embedEnabled: boolean): BackVisibility;
44
+ /** The resolved Back destination. `live` = the origin tab is still open (switch
45
+ * to it); `reopen` = the origin tab was closed (reopen it). Both navigate to
46
+ * the CAPTURED snapshot (pathname+search+hash). */
47
+ export interface ResolvedBack {
48
+ kind: "live" | "reopen";
49
+ tabId?: string;
50
+ pathname: string;
51
+ search?: string;
52
+ hash?: string;
53
+ title?: string;
54
+ icon?: string;
55
+ screen?: string;
56
+ }
57
+ /**
58
+ * Resolve the Back DESTINATION from a tab's `returnTo` and the live tab list.
59
+ * Returns `null` when there is no valid origin (the caller then uses its
60
+ * fallback). The origin tab (live by `tabId`, else by `pathname`) decides only
61
+ * SWITCH vs REOPEN; the destination URL is always the CAPTURED snapshot. Embed
62
+ * yields `null` (Back is fallback-only in embed; no tab ops).
63
+ *
64
+ * Divergence from the plan's Invariant 5 (reasoned): the plan preferred the
65
+ * live tab's CURRENT search over the captured one, premised on every origin
66
+ * screen mirroring its URL into the tab store. That premise is false for custom
67
+ * screens (Compare, dashboards) with no per-page mirror, so preferring live
68
+ * search loses URL-encoded state (Business Case 3). The captured `returnTo` is
69
+ * exact at drill time ("where I came from"), so it is the single source of
70
+ * truth for the destination URL - no per-screen search mirror needed.
71
+ */
72
+ export declare function resolveBackTarget(returnTo: ReturnTarget | null | undefined, tabs: TabItem[], embedEnabled: boolean): ResolvedBack | null;
@@ -1,5 +1,26 @@
1
1
  export type TabMode = "view" | "edit" | "create";
2
2
  export type TabCloseScope = "current" | "others" | "right" | "all";
3
+ /**
4
+ * Origin-aware Back return target. The CANONICAL stored shape: `pathname` is an
5
+ * in-app path (no origin, starts `/` not `//`), `search` is the query string
6
+ * WITHOUT a leading `?` (matches `TabItem.search`), `hash` carries its leading
7
+ * `#`. Converted to the `?`-prefixed router form only at the navigate boundary.
8
+ *
9
+ * `hash` lives here but NOT on `TabItem` (which has no `hash` field): a closed
10
+ * origin tab with a hash is reopened via pathname+search, and the hash is
11
+ * applied only in the `navigate()` call. `tabId`/`title`/`icon`/`screen` are
12
+ * hints for switching to (or reopening) the origin tab. Never stores secrets,
13
+ * external URLs, or row data. See docs/plans/2026-06-29-origin-aware-back-navigation.md.
14
+ */
15
+ export interface ReturnTarget {
16
+ pathname: string;
17
+ search?: string;
18
+ hash?: string;
19
+ tabId?: string;
20
+ title?: string;
21
+ icon?: string;
22
+ screen?: string;
23
+ }
3
24
  export interface TabItem {
4
25
  id: string;
5
26
  pathname: string;
@@ -14,6 +35,7 @@ export interface TabItem {
14
35
  record?: Record<string, unknown>;
15
36
  mode?: TabMode;
16
37
  dirty?: boolean;
38
+ returnTo?: ReturnTarget | null;
17
39
  }
18
40
  interface TabState {
19
41
  tabs: TabItem[];
@@ -26,12 +48,24 @@ interface TabState {
26
48
  findTabByPathname: (pathname: string) => TabItem | undefined;
27
49
  updateTabTitle: (id: string, title: string) => void;
28
50
  updateTabSearch: (id: string, search: string) => void;
51
+ setReturnTarget: (id: string, rt: ReturnTarget | null) => void;
52
+ commitTabNavigation: (id: string, patch: {
53
+ search: string;
54
+ returnTo?: ReturnTarget | null;
55
+ }) => void;
29
56
  setDirty: (id: string, dirty: boolean) => void;
30
57
  setMode: (id: string, mode: TabMode) => void;
31
58
  updateTab: (id: string, patch: Partial<TabItem>) => void;
32
59
  setScope: (tenantId: string | null, userId: string | null) => void;
33
60
  hydrate: () => void;
34
61
  }
62
+ /** True for an auth/chrome route that must never create a tab or be captured
63
+ * as a Back origin. Shared by `openTab` and the origin-capture path. */
64
+ export declare function isExcludedRoute(pathname: string): boolean;
65
+ /** A same-origin in-app path: starts `/` but not `//` (the latter is a
66
+ * protocol-relative external URL). The single guard reused on capture, on
67
+ * snapshot load (localStorage is hostile), and in the tenant-landing peek. */
68
+ export declare function isSameOriginPath(pathname: unknown): pathname is string;
35
69
  export declare function tabIdsForCloseScope(tabs: TabItem[], activeTabId: string | null, scope: TabCloseScope, targetId?: string): string[];
36
70
  export declare function peekTenantLandingPath(tenantId: string, userId: string): string | null;
37
71
  export declare const useTabStore: import("zustand").UseBoundStore<import("zustand").StoreApi<TabState>>;
@@ -1,4 +1,4 @@
1
- import { J as e, V as t, X as n, Y as r, ft as i, t as a } from "./ActionRunner-B2e3Igpd.js";
1
+ import { J as e, V as t, X as n, Y as r, ft as i, t as a } from "./ActionRunner-D9fJRkNV.js";
2
2
  import { useState as o } from "react";
3
3
  import { MemoryRouter as s } from "react-router-dom";
4
4
  import { QueryClient as c, QueryClientProvider as l } from "@tanstack/react-query";
@@ -1,68 +1,11 @@
1
- import { Lt as e, Nt as t, Pt as n } from "./ActionRunner-B2e3Igpd.js";
1
+ import { Lt as e, Nt as t, Pt as n } from "./ActionRunner-D9fJRkNV.js";
2
2
  import { useCallback as r, useEffect as i, useRef as a, useState as o } from "react";
3
- //#region src/embed/file-src.ts
4
- var s = "/api/files";
5
- function c(e) {
6
- if (typeof e != "object" || !e) return !1;
7
- let t = e;
8
- return typeof t.url == "string" && typeof t.proxy == "boolean";
9
- }
10
- function l(e, t) {
11
- let n = encodeURIComponent(e);
12
- return typeof t == "object" ? `${s}/${n}/derived/${encodeURIComponent(t.derivation)}/url` : `${s}/${n}/url`;
13
- }
14
- var u = class extends Error {
15
- fileId;
16
- key;
17
- constructor(e, t) {
18
- super(`derivation "${t}" of file ${e} is still generating`), this.fileId = e, this.key = t, this.name = "DerivationPendingError";
19
- }
20
- };
21
- function d(e, t) {
22
- return {
23
- method: "GET",
24
- credentials: "omit",
25
- headers: { Authorization: `Bearer ${e}` },
26
- signal: t
27
- };
28
- }
29
- function f(e) {
30
- return `${e}${e.includes("?") ? "&" : "?"}disposition=attachment`;
31
- }
32
- async function p(e, r) {
33
- let i = await t(), a = await fetch(e, d(i, r));
34
- if (a.status !== 401) return a;
35
- n();
36
- let o = await t();
37
- return fetch(e, d(o, r));
38
- }
39
- async function m(t, n, r) {
40
- if (!e()) throw Error("resolveEmbedFileSrc called outside embed token mode");
41
- let i = await p(l(t, n), r);
42
- if (i.status === 202 && typeof n == "object") throw new u(t, n.derivation);
43
- if (!i.ok) throw Error(`embed file url request failed: ${i.status} ${i.statusText}`);
44
- let a = await i.json();
45
- if (!c(a)) throw Error("embed file url response is malformed");
46
- if (!a.proxy) return {
47
- url: a.url,
48
- kind: "direct"
49
- };
50
- if (!a.url.startsWith("/api/files/")) throw Error(`embed file proxy: refusing non-files-route URL ${JSON.stringify(a.url)}`);
51
- let o = await p(n === "download" ? f(a.url) : a.url, r);
52
- if (!o.ok) throw Error(`embed file proxy fetch failed: ${o.status} ${o.statusText}`);
53
- let s = await o.blob();
54
- return {
55
- url: URL.createObjectURL(s),
56
- kind: "blob"
57
- };
58
- }
59
- //#endregion
60
3
  //#region src/components/file-widgets/helpers.ts
61
- var h = ["thumb_200", "thumb_300"];
62
- function g() {
4
+ var s = ["thumb_200", "thumb_300"];
5
+ function c() {
63
6
  return typeof navigator > "u" ? ["en"] : (navigator.languages?.length ? navigator.languages : [navigator.language]).filter((e) => typeof e == "string" && e.length > 0);
64
7
  }
65
- function _(e, t) {
8
+ function l(e, t) {
66
9
  for (let n of t) {
67
10
  if (e[n]) return e[n];
68
11
  let t = n.split("-")[0];
@@ -70,17 +13,17 @@ function _(e, t) {
70
13
  }
71
14
  for (let t of Object.values(e)) if (typeof t == "string" && t.length > 0) return t;
72
15
  }
73
- function v(e, t, n) {
16
+ function u(e, t, n) {
74
17
  if (t) return t;
75
18
  let r = e?.metadata?.alt_text;
76
19
  if (typeof r == "string" && r.length > 0) return r;
77
20
  if (r && typeof r == "object" && !Array.isArray(r)) {
78
- let e = _(Object.fromEntries(Object.entries(r).filter((e) => e[0].length > 0 && typeof e[1] == "string")), g());
21
+ let e = l(Object.fromEntries(Object.entries(r).filter((e) => e[0].length > 0 && typeof e[1] == "string")), c());
79
22
  if (e) return e;
80
23
  }
81
24
  return n;
82
25
  }
83
- var y = new Set([
26
+ var d = /* @__PURE__ */ new Set([
84
27
  "image/png",
85
28
  "image/jpeg",
86
29
  "image/webp",
@@ -88,30 +31,30 @@ var y = new Set([
88
31
  "image/avif",
89
32
  "application/pdf"
90
33
  ]);
91
- function b(e) {
34
+ function f(e) {
92
35
  let t = e.content_type?.toLowerCase().split(";")[0]?.trim() ?? "";
93
- return y.has(t);
36
+ return d.has(t);
94
37
  }
95
- function x(e) {
38
+ function p(e) {
96
39
  return e.url ?? e.presigned_url;
97
40
  }
98
- function S(e) {
99
- return e.url ? T(e.url, "attachment") : e.presigned_url;
41
+ function m(e) {
42
+ return e.url ? _(e.url, "attachment") : e.presigned_url;
100
43
  }
101
- function C(e, t) {
44
+ function h(e, t) {
102
45
  if (!e) return;
103
46
  let n = e.derivations ?? {};
104
47
  if (t && n[t]) return n[t];
105
- for (let e of h) if (n[e]) return n[e];
48
+ for (let e of s) if (n[e]) return n[e];
106
49
  return e.url ?? e.presigned_url;
107
50
  }
108
- function w(e, t) {
51
+ function g(e, t) {
109
52
  let n = e?.derivations ?? {};
110
53
  if (t && n[t]) return { derivation: t };
111
- for (let e of h) if (n[e]) return { derivation: e };
54
+ for (let e of s) if (n[e]) return { derivation: e };
112
55
  return "preview";
113
56
  }
114
- function T(e, t) {
57
+ function _(e, t) {
115
58
  try {
116
59
  let n = new URL(e, typeof window < "u" ? window.location.origin : "http://localhost");
117
60
  return n.searchParams.set("disposition", t), /^https?:\/\//i.test(e) ? n.toString() : `${n.pathname}${n.search}${n.hash}`;
@@ -119,7 +62,7 @@ function T(e, t) {
119
62
  return `${e}${e.includes("?") ? "&" : "?"}disposition=${t}`;
120
63
  }
121
64
  }
122
- function E(e) {
65
+ function v(e) {
123
66
  let t = e?.metadata?.focal_point;
124
67
  if (!t || typeof t != "object" || Array.isArray(t)) return null;
125
68
  let n = t, r = typeof n.x == "number" ? n.x : NaN, i = typeof n.y == "number" ? n.y : NaN;
@@ -128,10 +71,10 @@ function E(e) {
128
71
  y: i
129
72
  };
130
73
  }
131
- function D(e) {
74
+ function y(e) {
132
75
  if (e) return `${e.x * 100}% ${e.y * 100}%`;
133
76
  }
134
- function O(e, t) {
77
+ function b(e, t) {
135
78
  return {
136
79
  ...e,
137
80
  metadata: {
@@ -141,6 +84,63 @@ function O(e, t) {
141
84
  };
142
85
  }
143
86
  //#endregion
87
+ //#region src/embed/file-src.ts
88
+ var x = "/api/files";
89
+ function S(e) {
90
+ if (typeof e != "object" || !e) return !1;
91
+ let t = e;
92
+ return typeof t.url == "string" && typeof t.proxy == "boolean";
93
+ }
94
+ function C(e, t) {
95
+ let n = encodeURIComponent(e);
96
+ return typeof t == "object" ? `${x}/${n}/derived/${encodeURIComponent(t.derivation)}/url` : `${x}/${n}/url`;
97
+ }
98
+ var w = class extends Error {
99
+ fileId;
100
+ key;
101
+ constructor(e, t) {
102
+ super(`derivation "${t}" of file ${e} is still generating`), this.fileId = e, this.key = t, this.name = "DerivationPendingError";
103
+ }
104
+ };
105
+ function T(e, t) {
106
+ return {
107
+ method: "GET",
108
+ credentials: "omit",
109
+ headers: { Authorization: `Bearer ${e}` },
110
+ signal: t
111
+ };
112
+ }
113
+ function E(e) {
114
+ return `${e}${e.includes("?") ? "&" : "?"}disposition=attachment`;
115
+ }
116
+ async function D(e, r) {
117
+ let i = await t(), a = await fetch(e, T(i, r));
118
+ if (a.status !== 401) return a;
119
+ n();
120
+ let o = await t();
121
+ return fetch(e, T(o, r));
122
+ }
123
+ async function O(t, n, r) {
124
+ if (!e()) throw Error("resolveEmbedFileSrc called outside embed token mode");
125
+ let i = await D(C(t, n), r);
126
+ if (i.status === 202 && typeof n == "object") throw new w(t, n.derivation);
127
+ if (!i.ok) throw Error(`embed file url request failed: ${i.status} ${i.statusText}`);
128
+ let a = await i.json();
129
+ if (!S(a)) throw Error("embed file url response is malformed");
130
+ if (!a.proxy) return {
131
+ url: a.url,
132
+ kind: "direct"
133
+ };
134
+ if (!a.url.startsWith("/api/files/")) throw Error(`embed file proxy: refusing non-files-route URL ${JSON.stringify(a.url)}`);
135
+ let o = await D(n === "download" ? E(a.url) : a.url, r);
136
+ if (!o.ok) throw Error(`embed file proxy fetch failed: ${o.status} ${o.statusText}`);
137
+ let s = await o.blob();
138
+ return {
139
+ url: URL.createObjectURL(s),
140
+ kind: "blob"
141
+ };
142
+ }
143
+ //#endregion
144
144
  //#region src/components/file-widgets/useEmbedFileSrc.ts
145
145
  function k(e) {
146
146
  return typeof e == "object" ? `derivation:${e.derivation}` : e;
@@ -159,7 +159,7 @@ function j(t, n, r) {
159
159
  return;
160
160
  }
161
161
  let n = null, r = !1, i = new AbortController();
162
- return c(void 0), m(t, A(a), i.signal).then((e) => {
162
+ return c(void 0), O(t, A(a), i.signal).then((e) => {
163
163
  if (r) {
164
164
  e.kind === "blob" && URL.revokeObjectURL(e.url);
165
165
  return;
@@ -167,7 +167,7 @@ function j(t, n, r) {
167
167
  e.kind === "blob" && (n = e.url), c(e.url);
168
168
  }).catch((e) => {
169
169
  if (!r) {
170
- if (e instanceof u) {
170
+ if (e instanceof w) {
171
171
  c(void 0);
172
172
  return;
173
173
  }
@@ -188,10 +188,10 @@ function M() {
188
188
  i && window.open(i, "_blank", "noopener");
189
189
  return;
190
190
  }
191
- n && m(n, r).then((e) => {
191
+ n && O(n, r).then((e) => {
192
192
  e.kind === "blob" && t.current.push(e.url), window.open(e.url, "_blank", "noopener");
193
193
  }).catch((e) => {
194
- e instanceof u || console.error("[declarion-embed] file open failed:", e);
194
+ e instanceof w || console.error("[declarion-embed] file open failed:", e);
195
195
  });
196
196
  }, []) };
197
197
  }
@@ -207,4 +207,4 @@ function F(e) {
207
207
  return e ? Array.isArray(e) ? e.filter(N) : N(e) ? [e] : [] : [];
208
208
  }
209
209
  //#endregion
210
- export { b as a, C as c, E as d, v as f, j as i, w as l, P as n, S as o, O as p, M as r, D as s, F as t, x as u };
210
+ export { f as a, h as c, v as d, u as f, j as i, g as l, P as n, m as o, b as p, M as r, y as s, F as t, p as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@declarion/react",
3
- "version": "0.26.3",
3
+ "version": "0.27.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "React SDK for Declarion, the schema-driven business apps platform.",
@@ -65,19 +65,19 @@
65
65
  "@radix-ui/react-select": "^2.3.1",
66
66
  "@radix-ui/react-separator": "^1.1.10",
67
67
  "@radix-ui/react-slot": "^1.3.0",
68
- "@tanstack/react-query": "^5.101.0",
68
+ "@tanstack/react-query": "^5.101.2",
69
69
  "@tanstack/react-table": "^8.21.3",
70
- "@tanstack/react-virtual": "^3.14.3",
70
+ "@tanstack/react-virtual": "^3.14.4",
71
71
  "class-variance-authority": "^0.7.1",
72
72
  "clsx": "^2.1.1",
73
73
  "date-fns": "^4.4.0",
74
- "lucide-react": "^1.21.0",
74
+ "lucide-react": "^1.22.0",
75
75
  "mdast-util-to-string": "^4.0.0",
76
76
  "monaco-editor": "^0.55.1",
77
77
  "react-markdown": "^10.1.0",
78
- "react-router-dom": "^7.18.0",
78
+ "react-router-dom": "^7.18.1",
79
79
  "react-textarea-autosize": "^8.5.9",
80
- "recharts": "^3.8.1",
80
+ "recharts": "^3.9.0",
81
81
  "remark-breaks": "^4.0.0",
82
82
  "remark-gfm": "^4.0.1",
83
83
  "remark-parse": "^11.0.0",
@@ -88,30 +88,30 @@
88
88
  "zustand": "^5.0.14"
89
89
  },
90
90
  "devDependencies": {
91
- "@axe-core/react": "^4.11.3",
91
+ "@axe-core/react": "^4.12.1",
92
92
  "@tailwindcss/typography": "^0.5.20",
93
- "@tailwindcss/vite": "^4.3.1",
93
+ "@tailwindcss/vite": "^4.3.2",
94
94
  "@testing-library/dom": "^10.4.1",
95
95
  "@testing-library/jest-dom": "^6.9.1",
96
96
  "@testing-library/react": "^16.3.2",
97
97
  "@testing-library/user-event": "^14.6.1",
98
98
  "@types/react": "^19.2.17",
99
99
  "@types/react-dom": "^19.2.3",
100
- "@vitejs/plugin-react": "^6.0.2",
100
+ "@vitejs/plugin-react": "^6.0.3",
101
101
  "@vitest/coverage-v8": "^4.1.9",
102
102
  "axe-core": "^4.12.1",
103
- "eslint": "^10.5.0",
103
+ "eslint": "^10.6.0",
104
104
  "eslint-plugin-react-hooks": "^7.1.1",
105
105
  "jsdom": "^29.1.1",
106
106
  "msw": "^2.14.6",
107
107
  "publint": "^0.3.0",
108
108
  "react": "^19.2.7",
109
109
  "react-dom": "^19.2.7",
110
- "tailwindcss": "^4.3.1",
110
+ "tailwindcss": "^4.3.2",
111
111
  "tsc-alias": "^1.8.17",
112
112
  "typescript": "^6.0.3",
113
- "typescript-eslint": "^8.62.0",
114
- "vite": "^8.0.16",
113
+ "typescript-eslint": "^8.62.1",
114
+ "vite": "^8.1.0",
115
115
  "vitest": "^4.1.9"
116
116
  },
117
117
  "scripts": {