@declarion/react 0.1.80 → 0.1.82
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/dist-lib/{DraggableChildrenTable-CcXR8Vm6.js → DraggableChildrenTable-XKJWMb1S.js} +2 -2
- package/dist-lib/{DraggableChildrenTable-CcXR8Vm6.js.map → DraggableChildrenTable-XKJWMb1S.js.map} +1 -1
- package/dist-lib/{Grid-CseTLMLv.js → Grid-By_Ere-W.js} +2 -2
- package/dist-lib/{Grid-CseTLMLv.js.map → Grid-By_Ere-W.js.map} +1 -1
- package/dist-lib/{JsonEditor-Tew1oNqZ.js → JsonEditor-CexL5HXN.js} +17 -15
- package/dist-lib/JsonEditor-CexL5HXN.js.map +1 -0
- package/dist-lib/api/events.d.ts +5 -0
- package/dist-lib/components/fields/BoolField.d.ts +1 -1
- package/dist-lib/components/fields/EmailField.d.ts +1 -1
- package/dist-lib/components/fields/EnumField.d.ts +1 -1
- package/dist-lib/components/fields/IntArrayField.d.ts +1 -1
- package/dist-lib/components/fields/JsonEditor.d.ts +2 -1
- package/dist-lib/components/fields/JsonField.d.ts +1 -1
- package/dist-lib/components/fields/MultilangField.d.ts +1 -1
- package/dist-lib/components/fields/MultilangTextField.d.ts +1 -1
- package/dist-lib/components/fields/NumberField.d.ts +1 -1
- package/dist-lib/components/fields/PasswordField.d.ts +1 -1
- package/dist-lib/components/fields/PhoneField.d.ts +1 -1
- package/dist-lib/components/fields/RefField.d.ts +1 -1
- package/dist-lib/components/fields/SecretField.d.ts +1 -1
- package/dist-lib/components/fields/StringArrayField.d.ts +1 -1
- package/dist-lib/components/fields/StringField.d.ts +1 -1
- package/dist-lib/components/fields/TextField.d.ts +1 -1
- package/dist-lib/components/fields/TimestampField.d.ts +1 -1
- package/dist-lib/components/fields/UrlField.d.ts +1 -1
- package/dist-lib/components/fields/index.d.ts +2 -0
- package/dist-lib/components/file-widgets/host.d.ts +2 -0
- package/dist-lib/components/file-widgets/types.d.ts +1 -0
- package/dist-lib/components/pages/list/DataCell.d.ts +7 -0
- package/dist-lib/components/pages/list/ListBulkBar.d.ts +27 -0
- package/dist-lib/components/pages/list/ListHeader.d.ts +23 -0
- package/dist-lib/components/pages/list/RowActionsCell.d.ts +38 -0
- package/dist-lib/components/pages/list/SaveViewDialog.d.ts +6 -0
- package/dist-lib/components/pages/list/columns.d.ts +42 -0
- package/dist-lib/components/pages/list/filter-meta.d.ts +9 -0
- package/dist-lib/components/primitives/BoolToggle.d.ts +3 -1
- package/dist-lib/components/primitives/Fields.d.ts +12 -3
- package/dist-lib/index.js +4189 -4066
- package/dist-lib/index.js.map +1 -1
- package/dist-lib/lib/action-dispatch.d.ts +7 -0
- package/dist-lib/stores/realtime.d.ts +9 -0
- package/dist-lib/types/schema.d.ts +11 -1
- package/dist-lib/{useUserLocale-D9wc5kzQ.js → useUserLocale-CSS6fwi8.js} +1110 -1003
- package/dist-lib/useUserLocale-CSS6fwi8.js.map +1 -0
- package/dist-lib/{value-FIp50R1-.js → value-DgzPSFZM.js} +149 -196
- package/dist-lib/value-DgzPSFZM.js.map +1 -0
- package/package.json +1 -1
- package/dist-lib/JsonEditor-Tew1oNqZ.js.map +0 -1
- package/dist-lib/components/shared/RowActionItem.d.ts +0 -11
- package/dist-lib/useUserLocale-D9wc5kzQ.js.map +0 -1
- package/dist-lib/value-FIp50R1-.js.map +0 -1
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"value-FIp50R1-.js","names":[],"sources":["../src/types/api.ts","../src/api/auth-redirect.ts","../src/lib/versionTracker.ts","../src/api/client.ts","../src/components/file-widgets/upload.ts","../src/components/file-widgets/helpers.ts","../src/components/file-widgets/draft-scope.tsx","../src/components/file-widgets/value.ts"],"sourcesContent":["import type { AccentDef, LocalizedString } from \"./schema\";\nimport type { HydratedFile } from \"./files\";\n\nexport interface ListMeta {\n total: number;\n page: number;\n per_page: number;\n total_pages: number;\n}\n\nexport interface ListResponse<T = Record<string, unknown>> {\n data: T[];\n meta: ListMeta;\n $refs?: Record<string, Record<string, Record<string, unknown>>>;\n}\n\nexport interface DataResponse<T = Record<string, unknown>> {\n data: T;\n}\n\nexport interface ApiErrorBody {\n error: {\n message: string;\n code: string;\n field?: string;\n };\n /** Bulk operation item index (present on validation errors for array requests). */\n index?: number;\n}\n\nexport class ApiError extends Error {\n status: number;\n code: string;\n /** Bulk operation item index (present on validation errors for array requests). */\n index?: number;\n /** Raw JSON body for endpoints that return a non-standard error envelope (e.g. invoke:each batch). */\n raw?: unknown;\n\n constructor(status: number, message: string, code: string, index?: number, raw?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.code = code;\n this.index = index;\n this.raw = raw;\n }\n}\n\nexport type TenantEnv = \"production\" | \"staging\" | \"dev\" | string;\n\nexport interface Tenant {\n id: string;\n code: string;\n name: string;\n role: string;\n env?: TenantEnv;\n /**\n * Whether the caller has a `tenant_users` row for this tenant. Always\n * true for non-superadmins. For superadmins, `auth.list_tenants` also\n * returns non-membership tenants reachable via cross-tenant authority;\n * the switcher splits them into a separate \"All tenants\" section.\n */\n is_member?: boolean;\n}\n\nexport interface AuthUser {\n id: string;\n email: string;\n display_name: string;\n is_active: boolean;\n roles: string[];\n permissions?: string[];\n tenant?: Tenant;\n /** Populated only when the request is acting under an impersonation session. */\n impersonation?: ImpersonationInfo;\n created_at: string;\n updated_at: string;\n}\n\n/**\n * ImpersonationInfo is the read-side projection of the active session\n * surfaced by auth.me. Drives the banner, topbar chip, viewport frame,\n * and the \"Exit impersonation\" menu item.\n */\nexport interface ImpersonationInfo {\n session_id: string;\n real_user_id: string;\n real_user_email?: string;\n target_user_id: string;\n target_user_email?: string;\n reason: string;\n started_at: string;\n expires_at: string;\n}\n\nexport interface LoginResponse {\n expires_at: string;\n user: AuthUser;\n tenant?: Tenant;\n}\n\nexport interface CheckSetupResponse {\n needs_setup: boolean;\n}\n\nexport interface StatusInfo {\n group_code: string;\n status_code: string;\n status_name: LocalizedString;\n color?: string;\n set_by?: string;\n set_at: string;\n note?: string;\n}\n\nexport interface PropertyValue {\n property_code: string;\n name: LocalizedString;\n type: string;\n value: unknown;\n}\n\nexport interface PublicParam {\n code: string;\n name: LocalizedString;\n type: string;\n category: string;\n description?: LocalizedString;\n value: unknown;\n}\n\n/** Per-app legal/support URL set rendered in the auth-shell footer. */\nexport interface PublicLegalLinks {\n security_url?: string;\n privacy_url?: string;\n terms_url?: string;\n docs_url?: string;\n}\n\n/** Subset of branding exposed pre-login by /api/params/public. */\nexport interface PublicBranding {\n app_name?: string;\n app_logo?: string;\n app_initial?: string;\n accent_color?: string;\n ai_name?: string;\n wordmark?: string;\n version_label?: string;\n status_url?: string;\n auth_tagline?: LocalizedString;\n auth_subtagline?: LocalizedString;\n auth_badges?: LocalizedString[];\n legal_entity?: string;\n workspace_domain?: string;\n legal_links?: PublicLegalLinks;\n}\n\n/**\n * Deploy-time identity exposed pre-login by /api/params/public so the\n * auth shell can render real region/build values (no mockup placeholders).\n * Each field is empty when unconfigured/unset; consumers hide the pill\n * when the entire block is missing or empty.\n */\nexport interface PublicMeta {\n version?: string;\n revision?: string;\n build_time?: string;\n region?: string;\n}\n\n/** Envelope returned by GET /api/params/public. */\nexport interface PublicParamsResponse {\n parameters: PublicParam[];\n accents?: Record<string, AccentDef>;\n branding?: PublicBranding;\n meta?: PublicMeta;\n}\n\n/**\n * Wire envelope for GET /api/params/{code}.\n *\n * The server always responds with HTTP 200. `found:false` means the\n * platform has no value for the code (undeclared, restricted-category,\n * or no value at any resolution layer); the caller substitutes its own\n * default. `found:true` means `value` is authoritative - even if it is\n * null/zero (an explicit nil value is a real value, not absence).\n *\n * The default never crosses the wire; servers don't track caller\n * defaults, so two callers with different defaults always see consistent\n * server state.\n */\nexport interface ParamLookup {\n code: string;\n found: boolean;\n value?: unknown;\n source?: \"object\" | \"user\" | \"role\" | \"tenant\" | \"env\" | \"yaml\";\n type?: string;\n}\n\n/** @deprecated Prefer `ParamLookup`. Retained for callers still inspecting `source`. */\nexport interface ResolvedParam {\n code: string;\n name: LocalizedString;\n type: string;\n category?: string;\n description?: LocalizedString;\n value: unknown;\n source: \"object\" | \"user\" | \"role\" | \"tenant\" | \"env\" | \"yaml\" | \"none\";\n}\n\nexport interface ParamOverride {\n param_code: string;\n value: unknown;\n}\n\nexport interface SSOProvider {\n name: string;\n slug: string;\n}\n\nexport interface ListParams {\n page?: number;\n per_page?: number;\n sort?: string;\n search?: string;\n /** JSON filter nodes (recursive AND/OR tree). */\n filters?: FilterNodeParam[];\n include_deleted?: boolean;\n expand?: string[];\n screen?: string;\n /** Return bounded display previews for fields that declare display.list_preview. */\n preview?: boolean;\n /**\n * Field whitelist. When set, the server returns only these columns plus\n * primary-key field(s) (auto-injected server-side). Filters/search/sort\n * still operate against all fields regardless of select. Omit to fetch\n * every column (default, backwards-compatible).\n */\n select?: string[];\n}\n\n/** Minimal filter node type for API params (matches backend FilterNode). */\nexport interface FilterNodeParam {\n field?: string;\n op?: string;\n value?: unknown;\n or?: FilterNodeParam[][];\n and?: FilterNodeParam[];\n}\n\n// ---------------------------------------------------------------------------\n// v2 API types - bulk operations, cursor pagination, sub-resource shapes\n// ---------------------------------------------------------------------------\n\n/** Cursor-based pagination metadata returned by v2 List. */\nexport interface V2Meta {\n total: number;\n limit: number;\n has_more: boolean;\n cursor?: string;\n}\n\n/**\n * Refs map: keyed by entity code (or $-prefixed virtual entity code),\n * then by record ID, then field values.\n * Example: { company: { \"uuid-1\": { id: \"uuid-1\", name: \"Acme\" } } }\n */\nexport type Refs = Record<string, Record<string, Record<string, unknown>>>;\n\n/** Per-row status instance. */\nexport interface V2StatusInstance {\n group_code: string;\n status_code: string;\n status_name?: Record<string, string>;\n color?: string;\n set_at?: string;\n set_by?: string;\n note?: string;\n}\n\n/** Per-row property instance. */\nexport interface V2PropertyInstance {\n property_code: string;\n value: unknown;\n updated_at?: string;\n updated_by?: string;\n}\n\n/** Per-row parameter override. */\nexport interface V2ParamInstance {\n param_code: string;\n value: unknown;\n}\n\n/** v2 List response with cursor pagination and response-level $refs. */\nexport interface V2ListResponse<T = Record<string, unknown>> {\n data: T[];\n meta: V2Meta;\n $refs?: Refs;\n}\n\n/** v2 List query parameters. */\nexport interface V2ListParams {\n select?: string[];\n expand?: string[];\n sort?: string;\n search?: string;\n filters?: FilterNodeParam[];\n limit?: number;\n after?: string;\n include_deleted?: boolean;\n}\n\n/** Per-field resolved display unit codes. */\nexport type Units = Record<string, string>;\n\n/** A row enriched with $-prefixed sub-resources (as returned by Get or write responses). */\nexport interface EnrichedRow extends Record<string, unknown> {\n $refs?: Refs;\n $files?: Record<string, HydratedFile | HydratedFile[] | null>;\n $statuses?: V2StatusInstance[];\n $properties?: V2PropertyInstance[];\n $params?: V2ParamInstance[];\n $children?: Record<string, Record<string, unknown>[]>;\n $units?: Units;\n}\n","const REDIRECT_FALLBACK = \"/\";\nconst LOGIN_PATH = \"/login\";\nconst SIGNUP_PATH = \"/signup\";\nconst PARSE_BASE = \"https://declarion.local\";\n\nfunction isAuthRoute(path: string): boolean {\n const pathname = path.split(/[?#]/, 1)[0];\n return (\n pathname === LOGIN_PATH ||\n pathname.startsWith(`${LOGIN_PATH}/`) ||\n pathname === SIGNUP_PATH ||\n pathname.startsWith(`${SIGNUP_PATH}/`)\n );\n}\n\nexport function sanitizeRedirectPath(value: string | null | undefined): string {\n if (!value) return REDIRECT_FALLBACK;\n\n const lower = value.toLowerCase();\n if (\n !value.startsWith(\"/\") ||\n value.startsWith(\"//\") ||\n value.includes(\"\\\\\") ||\n lower.includes(\"%5c\")\n ) {\n return REDIRECT_FALLBACK;\n }\n\n try {\n const parsed = new URL(value, PARSE_BASE);\n if (parsed.origin !== PARSE_BASE) return REDIRECT_FALLBACK;\n\n const path = `${parsed.pathname}${parsed.search}${parsed.hash}`;\n if (!path.startsWith(\"/\") || path.startsWith(\"//\") || path.includes(\"\\\\\")) {\n return REDIRECT_FALLBACK;\n }\n return path || REDIRECT_FALLBACK;\n } catch {\n return REDIRECT_FALLBACK;\n }\n}\n\nexport function loginPathForRedirectPath(value: string | null | undefined): string {\n const redirectPath = sanitizeRedirectPath(value);\n if (redirectPath === REDIRECT_FALLBACK || isAuthRoute(redirectPath)) {\n return LOGIN_PATH;\n }\n return `${LOGIN_PATH}?next=${encodeURIComponent(redirectPath)}`;\n}\n\nexport function loginPathForCurrentLocation(): string {\n if (typeof window === \"undefined\") return LOGIN_PATH;\n const { pathname, search, hash } = window.location;\n return loginPathForRedirectPath(`${pathname}${search}${hash}`);\n}\n","/**\n * versionTracker — central staleness-detection state for the SPA.\n *\n * Every API response carries `X-Declarion-Version: <combined-fingerprint>`\n * (set by the server's version_header middleware). The FIRST arrival\n * in a bundle's lifetime is captured as the baseline; any later\n * response with a different value triggers drift detection — the SPA\n * either silently invalidates the schema query (if only schema differs)\n * or prompts the user to reload (if assets/binary changed).\n *\n * **State is in-memory only — no sessionStorage / localStorage.** Each\n * bundle load captures its own baseline from its first response. The\n * bundle IS its own version contract; the live server's first answer\n * defines what version the bundle should consider itself. Persisting\n * across reloads is actively harmful: a stale baseline from a previous\n * bundle could override the live answer, producing a \"click Reload →\n * page reloads → toast pops again\" infinite loop. This is the same\n * lesson behind why `index.html` is `Cache-Control: no-cache` — the\n * bootstrap manifest must always be live, never replayed.\n *\n * Defense in depth runs three sources into the same comparator:\n * 1. observe() — header read on every API response (apiFetch hook).\n * 2. fetchProbe() — explicit GET /api/version (focus / online / 60s).\n * 3. (optional) SSE `version-changed` event from useSSE.\n *\n * Cross-tab coordination via BroadcastChannel('declarion-version') —\n * runtime IPC only, not persistence. When one tab detects drift it\n * fans the decision out so all tabs reload together (avoids the\n * half-old / half-new tab anti-pattern). Each tab still bootstraps\n * its own baseline independently.\n */\n\nexport interface VersionDelta {\n /** Reason the delta was emitted. Lets subscribers decide UX. */\n kind: \"schema-only\" | \"asset-or-binary\" | \"unknown\";\n /** Latest combined fingerprint observed. Drives the next baseline. */\n observed: string;\n /** Per-component breakdown when /api/version was reached. */\n components?: VersionComponents;\n}\n\nexport interface VersionComponents {\n schema: string;\n asset: string;\n binary: string;\n}\n\nexport type VersionDeltaListener = (delta: VersionDelta) => void;\n\nconst HEADER_NAME = \"X-Declarion-Version\";\n\n/**\n * BroadcastChannel name. Browser support: Chrome 54+, Firefox 38+,\n * Safari 15.4+ — within Declarion's stated browser matrix. The\n * tracker tolerates a missing API (e.g. SSR / older Safari) by\n * skipping cross-tab fan-out; in-tab detection still works.\n */\nconst BROADCAST_CHANNEL = \"declarion-version\";\n\ninterface InternalState {\n firstSeen: string | null;\n firstComponents: VersionComponents | null;\n /** True once we've reported drift; debounces repeat events. */\n hasReportedDrift: boolean;\n listeners: Set<VersionDeltaListener>;\n channel: BroadcastChannel | null;\n}\n\nfunction makeChannel(): BroadcastChannel | null {\n if (typeof BroadcastChannel === \"undefined\") return null;\n try {\n return new BroadcastChannel(BROADCAST_CHANNEL);\n } catch {\n return null;\n }\n}\n\n// All state is in-memory. See header comment for why we deliberately\n// don't persist `firstSeen` across reloads.\nconst state: InternalState = {\n firstSeen: null,\n firstComponents: null,\n hasReportedDrift: false,\n listeners: new Set(),\n channel: null,\n};\n\n// Lazy channel init — defers cross-tab wiring until the SDK is actually\n// running in a browser tab (skipped for SSR / unit tests).\nfunction ensureChannel(): void {\n if (state.channel || typeof BroadcastChannel === \"undefined\") return;\n state.channel = makeChannel();\n if (!state.channel) return;\n state.channel.addEventListener(\"message\", (ev) => {\n const data = ev.data as { type?: string; delta?: VersionDelta } | null;\n if (!data || data.type !== \"drift\" || !data.delta) return;\n // Another tab reported drift; mirror locally so this tab also\n // reloads / invalidates. Do NOT re-broadcast (idempotency).\n if (state.hasReportedDrift) return;\n state.hasReportedDrift = true;\n for (const cb of state.listeners) cb(data.delta);\n });\n}\n\n/**\n * Observe an API response. Captures the first-seen fingerprint and\n * compares subsequent ones. Idempotent — safe to call from every\n * apiFetch round-trip; backward-compatible — no-op when the header\n * is absent (older server).\n */\nexport function observe(headers: Headers | undefined | null): void {\n ensureChannel();\n if (!headers) return;\n const value = headers.get(HEADER_NAME);\n if (!value) return;\n if (!state.firstSeen) {\n state.firstSeen = value;\n return;\n }\n if (value === state.firstSeen) return;\n if (state.hasReportedDrift) return;\n // Drift detected via header. We don't yet know the per-component\n // breakdown — fetch it from /api/version so subscribers can classify\n // schema-only vs asset/binary, then emit.\n fetchProbe().catch(() => {\n // Probe failed → still emit with kind=unknown so the SPA at\n // least prompts for reload. Better than swallowing drift.\n emitDrift({ kind: \"unknown\", observed: value });\n });\n}\n\n/**\n * Hit /api/version with If-None-Match (cheap when nothing changed —\n * 304 with no body). On any change, emit a typed delta.\n */\nexport async function fetchProbe(): Promise<void> {\n ensureChannel();\n let res: Response;\n try {\n const headers: Record<string, string> = {};\n if (state.firstSeen) {\n headers[\"If-None-Match\"] = `\"${state.firstSeen}\"`;\n }\n res = await fetch(\"/api/version\", {\n method: \"GET\",\n credentials: \"include\",\n headers,\n });\n } catch {\n // Network error — not our problem, the watcher backs off.\n throw new Error(\"version probe network error\");\n }\n\n if (res.status === 304) {\n // Server confirmed: nothing changed. Reset the drift flag so a\n // future actual change will emit (it shouldn't have been set,\n // but defense in depth).\n return;\n }\n if (!res.ok) {\n // 404 → server is older than this SDK; gracefully disable.\n // 5xx → backoff at the watcher level.\n throw new Error(`version probe ${res.status}`);\n }\n let body: {\n fingerprint?: string;\n schema_fingerprint?: string;\n asset_fingerprint?: string;\n binary_fingerprint?: string;\n };\n try {\n body = (await res.json()) as typeof body;\n } catch {\n throw new Error(\"version probe malformed body\");\n }\n\n const observed = body.fingerprint;\n if (typeof observed !== \"string\" || observed === \"\") {\n throw new Error(\"version probe missing fingerprint\");\n }\n\n const components: VersionComponents | undefined =\n typeof body.schema_fingerprint === \"string\" &&\n typeof body.asset_fingerprint === \"string\" &&\n typeof body.binary_fingerprint === \"string\"\n ? {\n schema: body.schema_fingerprint,\n asset: body.asset_fingerprint,\n binary: body.binary_fingerprint,\n }\n : undefined;\n\n // First probe in this bundle's lifetime → capture baseline (both\n // fingerprint and components). `firstSeen` may already be set by\n // an earlier observe() that ran from a regular API response — in\n // that case `firstComponents` is null and we backfill it here.\n // Without this backfill, every later drift would emit `kind:\n // \"unknown\"` because `classifyDelta(null, current)` has no\n // baseline, and the silent schema-only invalidation path never\n // fires.\n if (!state.firstSeen) {\n state.firstSeen = observed;\n }\n if (!state.firstComponents && components && observed === state.firstSeen) {\n state.firstComponents = components;\n }\n\n if (observed === state.firstSeen) return;\n\n const kind = classifyDelta(state.firstComponents, components);\n emitDrift({ kind, observed, components });\n}\n\n/**\n * Schema-only delta: schema differs, asset+binary unchanged → silent\n * React Query invalidation. Anything else (asset, binary, missing\n * baseline) is \"asset-or-binary\" → reload prompt.\n */\nfunction classifyDelta(\n baseline: VersionComponents | null,\n current: VersionComponents | undefined,\n): VersionDelta[\"kind\"] {\n if (!baseline || !current) return \"unknown\";\n const schemaChanged = baseline.schema !== current.schema;\n const assetChanged = baseline.asset !== current.asset;\n const binaryChanged = baseline.binary !== current.binary;\n if (schemaChanged && !assetChanged && !binaryChanged) return \"schema-only\";\n if (assetChanged || binaryChanged) return \"asset-or-binary\";\n // Combined differs but no component changed? Shouldn't happen with\n // a correct server; treat conservatively.\n return \"unknown\";\n}\n\nfunction emitDrift(delta: VersionDelta): void {\n if (state.hasReportedDrift) return;\n state.hasReportedDrift = true;\n for (const cb of state.listeners) cb(delta);\n if (state.channel) {\n try {\n state.channel.postMessage({ type: \"drift\", delta });\n } catch {\n /* channel may be closed during shutdown */\n }\n }\n}\n\n/**\n * Subscribe to drift events. Called once from useVersionWatcher; the\n * subscriber renders the UpdatePrompt (toast or silent invalidation).\n */\nexport function subscribe(cb: VersionDeltaListener): () => void {\n state.listeners.add(cb);\n return () => {\n state.listeners.delete(cb);\n };\n}\n\n/**\n * Test-only: reset internal state. Not exposed via package exports.\n */\nexport function __resetForTests(): void {\n state.firstSeen = null;\n state.firstComponents = null;\n state.hasReportedDrift = false;\n state.listeners.clear();\n if (state.channel) {\n state.channel.close();\n state.channel = null;\n }\n}\n","import { ApiError, type ApiErrorBody } from \"@/types/api\";\nimport { loginPathForCurrentLocation } from \"./auth-redirect\";\nimport { observe as observeVersion } from \"@/lib/versionTracker\";\n\n// The access and refresh tokens live in httpOnly cookies managed entirely by\n// the backend (__Host-declarion-access-token, __Host-declarion-refresh-token).\n// The browser attaches them automatically on every request when\n// credentials: \"include\" is set. JavaScript cannot read these cookies, which\n// is the whole point - an XSS cannot exfiltrate the session.\n//\n// The backend collapsed every auth/SSO/api-key endpoint into the generic\n// handler dispatcher at POST /api/actions/{code}. `callHandler` below is\n// the single entry point from the frontend; every wrapper in api/auth.ts\n// etc. uses it. `apiFetch` is kept for the handful of platform endpoints\n// that still live at named URLs (schema, data CRUD, params, etc.).\n\n// POST /api/actions/{code} bodies are flat: the handler's params sit at\n// the top level. Global handlers (auth.*, etc.) pass their args as-is.\nconst flatActionBody = (args: unknown) =>\n JSON.stringify(args ?? {});\n\nlet refreshPromise: Promise<void> | null = null;\n\n// refreshAccessToken stays on raw fetch (NOT apiFetch / callHandler) to\n// avoid recursion: apiFetch's 401 retry path waits on refreshPromise, so\n// if the refresh call itself went through apiFetch it would deadlock.\n/** Signals that auth.refresh failed terminally (refresh cookie missing, token\n * expired/reused, session revoked). Callers MUST stop retrying and route the\n * user back to /login — retrying produces an infinite 401/4xx storm against\n * the rate-limited refresh endpoint. Distinct from generic network errors so\n * useSSE can branch on it. */\nexport class RefreshDeadError extends Error {\n constructor(public status: number) {\n super(`refresh failed terminally (${status})`);\n this.name = \"RefreshDeadError\";\n }\n}\n\nasync function refreshAccessToken(): Promise<void> {\n const init: RequestInit = {\n method: \"POST\",\n credentials: \"include\",\n headers: { \"Content-Type\": \"application/json\" },\n body: flatActionBody({}),\n };\n const res = await fetch(\"/api/actions/auth.refresh\", init);\n\n // Grace-period replay: backend says retry shortly. ONLY 429 is retried —\n // any other non-OK status indicates the session is dead and we must escalate.\n if (res.status === 429) {\n const retryAfter = parseInt(res.headers.get(\"Retry-After\") || \"1\", 10);\n await new Promise((r) => setTimeout(r, retryAfter * 1000));\n const retryRes = await fetch(\"/api/actions/auth.refresh\", init);\n if (!retryRes.ok) {\n throw new RefreshDeadError(retryRes.status);\n }\n return;\n }\n\n if (!res.ok) {\n throw new RefreshDeadError(res.status);\n }\n}\n\nexport async function ensureRefresh(): Promise<void> {\n if (refreshPromise) return refreshPromise;\n refreshPromise = refreshAccessToken().finally(() => {\n refreshPromise = null;\n });\n return refreshPromise;\n}\n\n// Handler invoked when a persistent 401 requires sending the user back to\n// the login flow. Injected (not hardcoded) so tests can substitute a spy\n// and non-browser consumers (e.g. a worker) can route the signal their own\n// way. Default implementation assigns window.location.href — same behavior\n// as before this was extracted.\ntype PersistentAuthFailureHandler = () => void;\n\nconst defaultPersistentAuthFailureHandler: PersistentAuthFailureHandler = () => {\n if (typeof window === \"undefined\") return;\n const { pathname } = window.location;\n if (\n pathname.startsWith(\"/login\") ||\n pathname.startsWith(\"/signup\") ||\n pathname === \"/reset\" ||\n pathname === \"/forgot\"\n ) {\n return;\n }\n window.location.href = loginPathForCurrentLocation();\n};\n\nlet onPersistentAuthFailure: PersistentAuthFailureHandler =\n defaultPersistentAuthFailureHandler;\n\n/** Replace the handler invoked on persistent 401. Intended for tests\n * and non-browser consumers. Returns the previous handler so callers\n * can restore it if needed. */\nexport function setPersistentAuthFailureHandler(\n h: PersistentAuthFailureHandler,\n): PersistentAuthFailureHandler {\n const prev = onPersistentAuthFailure;\n onPersistentAuthFailure = h;\n return prev;\n}\n\n/** Returns the currently registered persistent-auth-failure handler. useSSE\n * reads this lazily so a setPersistentAuthFailureHandler() call from a test\n * is observed by the SSE retry loop without re-importing the module. */\nexport function getPersistentAuthFailureHandler(): PersistentAuthFailureHandler {\n return onPersistentAuthFailure;\n}\n\n// Structured non-2xx report. Routed through an injectable reporter so the\n// SDK does not hardcode `console.error` — production keeps the existing\n// in-browser log, tests install a no-op (clean stderr), and downstream\n// consumers can plug in observability backends (Sentry, Datadog, etc.)\n// without forking the client.\nexport type ApiErrorReport = {\n status: number;\n code: string;\n message: string;\n};\n\ntype ApiErrorReporter = (report: ApiErrorReport) => void;\n\nconst defaultApiErrorReporter: ApiErrorReporter = ({ status, code, message }) => {\n console.error(`[API ${status}] ${code}: ${message}`);\n};\n\nlet apiErrorReporter: ApiErrorReporter = defaultApiErrorReporter;\n\n/** Replace the reporter invoked for every non-2xx response. Returns the\n * previous reporter so callers can restore it. Tests should install a\n * spy or no-op via this hook instead of stubbing `console.error`. */\nexport function setApiErrorReporter(r: ApiErrorReporter): ApiErrorReporter {\n const prev = apiErrorReporter;\n apiErrorReporter = r;\n return prev;\n}\n\n// REQUEST_TIMEOUT_MS bounds every apiFetch round-trip. Prevents the\n// \"halt forever\" UX when a request is queued client-side and never\n// reaches the server — typically the HTTP/1.1 6-connection cap kicks\n// in (see docs/gotchas.md \"Dev (HTTP) halts when 6+ tabs are open\").\n// Without this, TanStack Query's spinner never resolves; with it, the\n// UI surfaces a clear \"request timed out\" error after 30s.\nconst REQUEST_TIMEOUT_MS = 30_000;\n\nexport async function apiFetch<T>(\n path: string,\n options: RequestInit = {},\n): Promise<T> {\n const headers = new Headers(options.headers);\n\n if (!headers.has(\"Content-Type\") && options.body) {\n headers.set(\"Content-Type\", \"application/json\");\n }\n\n // Compose the caller's signal (if any) with an internal timeout so\n // a stalled request fails loudly instead of spinning forever.\n const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);\n const signal = options.signal\n ? AbortSignal.any([options.signal, timeoutSignal])\n : timeoutSignal;\n\n let res: Response;\n try {\n res = await fetch(path, { ...options, headers, credentials: \"include\", signal });\n } catch (err) {\n if (err instanceof DOMException && err.name === \"TimeoutError\") {\n throw new ApiError(\n 0,\n `Request to ${path} timed out after ${REQUEST_TIMEOUT_MS / 1000}s. If you have many tabs open to the dev server, the HTTP/1.1 6-connection cap may be queueing requests in the browser. See docs/gotchas.md.`,\n \"REQUEST_TIMEOUT\",\n );\n }\n throw err;\n }\n\n // Drift detection — every API response carries the server's\n // X-Declarion-Version header (set by middleware/version_header.go).\n // observeVersion is a one-shot capture on first arrival and a\n // comparison thereafter; it no-ops when the header is absent\n // (older servers — backward compatible) and its emission flow is\n // debounced so this runs cheaply on every round-trip.\n observeVersion(res.headers);\n\n if (res.status === 204) {\n return undefined as T;\n }\n\n // 207 Multi-Status is a success status for the caller: the response body is\n // a well-formed payload (invoke:each partial). Flow through the normal parse\n // path so callers receive the body and can inspect status/errors themselves.\n if (res.status === 207) {\n return res.json() as Promise<T>;\n }\n\n if (!res.ok) {\n let message = \"Request failed\";\n let code = \"UNKNOWN\";\n let index: number | undefined;\n let raw: unknown;\n\n try {\n raw = await res.json();\n const body = raw as ApiErrorBody;\n if (body && body.error) {\n message = body.error.message;\n code = body.error.code;\n if (body.index != null) index = body.index;\n }\n } catch {\n // Ignore JSON parse errors\n }\n\n // 401 with a refresh hint: attempt exactly one silent refresh + retry.\n if (res.status === 401 && (code === \"TOKEN_REFRESH_NEEDED\" || code === \"UNAUTHORIZED\")) {\n try {\n await ensureRefresh();\n const retryHeaders = new Headers(options.headers);\n if (!retryHeaders.has(\"Content-Type\") && options.body) {\n retryHeaders.set(\"Content-Type\", \"application/json\");\n }\n const retryRes = await fetch(path, {\n ...options,\n headers: retryHeaders,\n credentials: \"include\",\n signal,\n });\n if (retryRes.status === 204) {\n return undefined as T;\n }\n if (!retryRes.ok) {\n onPersistentAuthFailure();\n const retryBody = (await retryRes\n .json()\n .catch(() => ({ error: { message: \"Request failed\", code: \"UNKNOWN\" } }))) as ApiErrorBody;\n throw new ApiError(retryRes.status, retryBody.error.message, retryBody.error.code, retryBody.index);\n }\n return retryRes.json() as Promise<T>;\n } catch (refreshErr) {\n if (refreshErr instanceof ApiError) {\n throw refreshErr;\n }\n onPersistentAuthFailure();\n throw new ApiError(401, message, code, index);\n }\n }\n\n // Session revoked or other 401 - redirect to login.\n if (res.status === 401) {\n onPersistentAuthFailure();\n }\n\n apiErrorReporter({ status: res.status, code, message });\n throw new ApiError(res.status, message, code, index, raw);\n }\n\n return res.json() as Promise<T>;\n}\n\n// Action endpoint response envelope. The actions dispatcher always wraps\n// successful handler results as { status: \"success\", result: <data>, ... }.\ninterface ActionEnvelope<R> {\n status: string;\n result: R;\n audit_operation_id?: string;\n object_count?: number;\n}\n\n// apiFetchBlob is the binary-payload sibling of apiFetch. Same auth-refresh +\n// error-envelope contract; differs only in the success path: it returns the\n// response body as a Blob and parses Content-Disposition for the filename.\n//\n// Used by /api/data/{entity}/export (CSV download). apiFetch stays JSON-only\n// so widening it would touch every call site.\n//\n// Returns:\n// - blob: the response body as a Blob (Content-Type preserved by the server,\n// e.g. text/csv; charset=utf-8).\n// - filename: parsed from Content-Disposition. RFC 5987 `filename*=UTF-8''<...>`\n// wins over the legacy `filename=\"...\"`. Empty string when neither is set.\n// - headers: the raw Response headers, in case callers need additional\n// metadata (e.g. X-Declarion-Audit-Operation-ID).\n//\n// On non-2xx: parses the same {error: {message, code}} envelope as apiFetch\n// and throws ApiError. On 401 with TOKEN_REFRESH_NEEDED / UNAUTHORIZED:\n// silent refresh + one retry, identical to apiFetch.\nexport interface BlobResponse {\n blob: Blob;\n filename: string;\n headers: Headers;\n}\n\nexport async function apiFetchBlob(\n path: string,\n options: RequestInit = {},\n): Promise<BlobResponse> {\n const headers = new Headers(options.headers);\n if (!headers.has(\"Content-Type\") && options.body) {\n headers.set(\"Content-Type\", \"application/json\");\n }\n if (!headers.has(\"Accept\")) {\n headers.set(\"Accept\", \"*/*\");\n }\n\n const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);\n const signal = options.signal\n ? AbortSignal.any([options.signal, timeoutSignal])\n : timeoutSignal;\n\n let res: Response;\n try {\n res = await fetch(path, { ...options, headers, credentials: \"include\", signal });\n } catch (err) {\n if (err instanceof DOMException && err.name === \"TimeoutError\") {\n throw new ApiError(\n 0,\n `Request to ${path} timed out after ${REQUEST_TIMEOUT_MS / 1000}s.`,\n \"REQUEST_TIMEOUT\",\n );\n }\n throw err;\n }\n\n observeVersion(res.headers);\n\n if (!res.ok) {\n let message = \"Request failed\";\n let code = \"UNKNOWN\";\n let index: number | undefined;\n let raw: unknown;\n try {\n raw = await res.json();\n const body = raw as ApiErrorBody;\n if (body && body.error) {\n message = body.error.message;\n code = body.error.code;\n if (body.index != null) index = body.index;\n }\n } catch {\n // Non-JSON body on a non-2xx — leave the defaults.\n }\n\n if (res.status === 401 && (code === \"TOKEN_REFRESH_NEEDED\" || code === \"UNAUTHORIZED\")) {\n try {\n await ensureRefresh();\n const retryHeaders = new Headers(options.headers);\n if (!retryHeaders.has(\"Content-Type\") && options.body) {\n retryHeaders.set(\"Content-Type\", \"application/json\");\n }\n if (!retryHeaders.has(\"Accept\")) {\n retryHeaders.set(\"Accept\", \"*/*\");\n }\n const retryRes = await fetch(path, {\n ...options,\n headers: retryHeaders,\n credentials: \"include\",\n signal,\n });\n if (!retryRes.ok) {\n onPersistentAuthFailure();\n const retryBody = (await retryRes\n .json()\n .catch(() => ({ error: { message: \"Request failed\", code: \"UNKNOWN\" } }))) as ApiErrorBody;\n throw new ApiError(retryRes.status, retryBody.error.message, retryBody.error.code, retryBody.index);\n }\n const blob = await retryRes.blob();\n return { blob, filename: parseContentDispositionFilename(retryRes.headers), headers: retryRes.headers };\n } catch (refreshErr) {\n if (refreshErr instanceof ApiError) {\n throw refreshErr;\n }\n onPersistentAuthFailure();\n throw new ApiError(401, message, code, index);\n }\n }\n\n if (res.status === 401) {\n onPersistentAuthFailure();\n }\n apiErrorReporter({ status: res.status, code, message });\n throw new ApiError(res.status, message, code, index, raw);\n }\n\n const blob = await res.blob();\n return { blob, filename: parseContentDispositionFilename(res.headers), headers: res.headers };\n}\n\n// parseContentDispositionFilename extracts a filename from Content-Disposition\n// per RFC 6266. The modern `filename*=UTF-8''<percent-encoded>` form wins\n// over the legacy `filename=\"...\"` so non-ASCII filenames decode correctly.\n// Returns \"\" when no filename is set so callers can supply a fallback.\nexport function parseContentDispositionFilename(headers: Headers): string {\n const cd = headers.get(\"Content-Disposition\");\n if (!cd) return \"\";\n\n const star = cd.match(/filename\\*\\s*=\\s*([^']*)'([^']*)'([^;]+)/i);\n if (star) {\n try {\n return decodeURIComponent(star[3].trim());\n } catch {\n // Malformed encoding — fall through to the legacy form.\n }\n }\n\n const legacy = cd.match(/filename\\s*=\\s*(\"([^\"]+)\"|([^;]+))/i);\n if (legacy) {\n return (legacy[2] ?? legacy[3] ?? \"\").trim();\n }\n return \"\";\n}\n\n// callHandler is the single entry point for every auth/SSO/api-key\n// operation. It POSTs to /api/actions/{code} with a flat body (handler\n// params at the top level) and unwraps the result envelope, so callers\n// see the handler's return type directly.\n//\n// Cookie side effects (login/signup/refresh/switch_tenant) are set by the\n// backend via the HTTPCustomizer interface on *auth.LoginResult; the\n// browser stores them automatically because `credentials: \"include\"` is\n// already set by apiFetch. Frontend does not need any special handling.\nexport async function callHandler<R = unknown>(\n code: string,\n args: unknown = {},\n): Promise<R> {\n const env = await apiFetch<ActionEnvelope<R>>(\n `/api/actions/${encodeURIComponent(code)}`,\n {\n method: \"POST\",\n body: flatActionBody(args),\n },\n );\n return env.result;\n}\n","import { callHandler } from \"@/api/client\";\nimport type { UploadProgress, UploadedFile } from \"./types\";\n\n// uploadFile sends a multipart/form-data POST to /api/files/upload via XHR\n// (so the widget can report progress and abort mid-flight). The single\n// \"file\" part carries the binary payload — the server's MultipartReader\n// reads it as a stream. The endpoint is mounted via roles.RegisterModuleRoute\n// per the /api/files/* exception in CLAUDE.md.\n//\n// On 4xx the server returns the standard ApiError envelope:\n// { error: { code, message }, ... }. We surface code + message so widgets\n// can render specific copy (FILE_TOO_LARGE → \"Image too large\", etc.).\nexport interface UploadOptions {\n file: File;\n entityCode?: string;\n fieldCode?: string;\n slotCode?: string;\n onProgress?: (p: UploadProgress) => void;\n signal?: AbortSignal;\n}\n\nexport class UploadError extends Error {\n code: string;\n status: number;\n constructor(status: number, code: string, message: string) {\n super(message);\n this.code = code;\n this.status = status;\n }\n}\n\nexport interface UploadEnvelope {\n status: string;\n result: UploadedFile;\n}\n\nexport function uploadFile(opts: UploadOptions): Promise<UploadedFile> {\n const { file, entityCode, fieldCode, slotCode, onProgress, signal } = opts;\n return new Promise<UploadedFile>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(\"POST\", uploadURL(entityCode, fieldCode, slotCode));\n xhr.withCredentials = true;\n\n xhr.upload.onprogress = (ev) => {\n if (!onProgress) return;\n const total = ev.lengthComputable ? ev.total : file.size;\n const pct = total > 0 ? Math.min(100, Math.round((ev.loaded / total) * 100)) : 0;\n onProgress({ loaded: ev.loaded, total, pct });\n };\n\n xhr.onload = () => {\n const txt = xhr.responseText || \"\";\n let parsed: unknown = null;\n try {\n parsed = txt ? JSON.parse(txt) : null;\n } catch {\n // tolerate non-JSON 5xx\n }\n if (xhr.status >= 200 && xhr.status < 300) {\n const out = normalizeUploadedFile(parsed);\n if (!out || !out.id) {\n reject(new UploadError(xhr.status, \"BAD_RESPONSE\", \"Upload returned no file id\"));\n return;\n }\n resolve(out);\n return;\n }\n const body = parsed as { error?: { code?: string; message?: string } } | null;\n const code = body?.error?.code ?? \"UPLOAD_FAILED\";\n const message = body?.error?.message ?? `Upload failed (${xhr.status})`;\n reject(new UploadError(xhr.status, code, message));\n };\n\n xhr.onerror = () => {\n reject(new UploadError(0, \"NETWORK_ERROR\", \"Network error during upload\"));\n };\n xhr.onabort = () => {\n reject(new UploadError(0, \"ABORTED\", \"Upload aborted\"));\n };\n\n if (signal) {\n if (signal.aborted) {\n xhr.abort();\n reject(new UploadError(0, \"ABORTED\", \"Upload aborted\"));\n return;\n }\n signal.addEventListener(\"abort\", () => xhr.abort(), { once: true });\n }\n\n const fd = new FormData();\n fd.append(\"file\", file, file.name);\n xhr.send(fd);\n });\n}\n\nfunction uploadURL(entityCode?: string, fieldCode?: string, slotCode?: string): string {\n const qs = new URLSearchParams();\n if (entityCode && fieldCode) {\n qs.set(\"entity_code\", entityCode);\n qs.set(\"field_code\", fieldCode);\n if (slotCode) qs.set(\"slot_code\", slotCode);\n }\n const suffix = qs.toString();\n return suffix ? `/api/files/upload?${suffix}` : \"/api/files/upload\";\n}\n\nfunction normalizeUploadedFile(payload: unknown): UploadedFile | null {\n if (!isRecord(payload)) return null;\n if (isRecord(payload.result)) {\n return normalizeUploadedFile(payload.result);\n }\n if (isRecord(payload.file)) {\n return normalizeRouteUpload(payload.file);\n }\n if (typeof payload.id === \"string\") {\n return payload as unknown as UploadedFile;\n }\n return null;\n}\n\nfunction normalizeRouteUpload(payload: Record<string, unknown>): UploadedFile | null {\n const id = asString(payload.file_id);\n if (!id) return null;\n const filename = asString(payload.filename_original) ?? asString(payload.filename_normalized) ?? id;\n return {\n id,\n filename,\n content_type: asString(payload.content_type) ?? \"application/octet-stream\",\n size_bytes: asNumber(payload.size_bytes) ?? 0,\n sha256: asString(payload.sha256) ?? \"\",\n dedup: asBoolean(payload.dedup),\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction asNumber(value: unknown): number | undefined {\n return typeof value === \"number\" ? value : undefined;\n}\n\nfunction asBoolean(value: unknown): boolean | undefined {\n return typeof value === \"boolean\" ? value : undefined;\n}\n\n// discardFile asks the server to drop a draft file row + its blob (if no\n// other draft references the same bytes). Bound to the `files.discard`\n// handler registered in Task 7. Errors are swallowed by callers — discard\n// is best-effort cleanup; the GC sweep eventually catches leaked drafts.\nexport async function discardFile(fileId: string): Promise<void> {\n await callHandler(\"files.discard\", { file_id: fileId });\n}\n\n// validateClientSide mirrors the server's accepts + max_size_bytes checks\n// so the widget can reject before issuing a network call. Returns null on\n// pass, an error object on fail. Wildcards in `accepts` follow the standard\n// HTTP convention: \"image/*\" matches any image type.\nexport function validateClientSide(\n file: File,\n accepts: string[] | undefined,\n maxBytes: number | undefined,\n): { code: string; message: string } | null {\n if (maxBytes && file.size > maxBytes) {\n return {\n code: \"FILE_TOO_LARGE\",\n message: `File is ${formatBytes(file.size)}, max ${formatBytes(maxBytes)}`,\n };\n }\n if (accepts && accepts.length > 0) {\n const ct = file.type || \"application/octet-stream\";\n const ok = accepts.some((acc) => mimeMatches(acc, ct));\n if (!ok) {\n return {\n code: \"MIME_NOT_ACCEPTED\",\n message: `File type ${ct} is not accepted`,\n };\n }\n }\n return null;\n}\n\nfunction mimeMatches(pattern: string, ct: string): boolean {\n if (pattern === \"*/*\" || pattern === \"*\") return true;\n if (pattern.endsWith(\"/*\")) {\n const prefix = pattern.slice(0, pattern.length - 1);\n return ct.startsWith(prefix);\n }\n return pattern === ct;\n}\n\nexport function formatBytes(n: number): string {\n if (n < 1024) return `${n} B`;\n const units = [\"KB\", \"MB\", \"GB\"];\n let v = n / 1024;\n let i = 0;\n while (v >= 1024 && i < units.length - 1) {\n v /= 1024;\n i++;\n }\n return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`;\n}\n","import type { HydratedFile } from \"./types\";\n\nexport interface FocalPoint {\n x: number;\n y: number;\n}\n\nfunction preferredLocales(): string[] {\n if (typeof navigator === \"undefined\") return [\"en\"];\n const locales = navigator.languages?.length ? navigator.languages : [navigator.language];\n return locales.filter((value): value is string => typeof value === \"string\" && value.length > 0);\n}\n\nfunction resolveLocalizedText(value: Record<string, string>, locales: string[]): string | undefined {\n for (const locale of locales) {\n if (value[locale]) return value[locale];\n const base = locale.split(\"-\")[0];\n if (base && value[base]) return value[base];\n }\n for (const entry of Object.values(value)) {\n if (typeof entry === \"string\" && entry.length > 0) return entry;\n }\n return undefined;\n}\n\nexport function resolveFileAltText(\n file: HydratedFile | null,\n alt: string | undefined,\n fallback: string,\n): string {\n if (alt) return alt;\n const meta = file?.metadata as Record<string, unknown> | undefined;\n const raw = meta?.alt_text;\n if (typeof raw === \"string\" && raw.length > 0) return raw;\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n const localized = Object.fromEntries(\n Object.entries(raw).filter(\n (entry): entry is [string, string] => entry[0].length > 0 && typeof entry[1] === \"string\",\n ),\n );\n const resolved = resolveLocalizedText(localized, preferredLocales());\n if (resolved) return resolved;\n }\n return fallback;\n}\n\n// Server inline safelist mirror. The platform's /download response only\n// honors `inline` for these types; any other type is force-downgraded\n// to attachment regardless of disposition= query. Keep this list in\n// exact sync with golang/internal/fileutil/active_content.go\n// (inlineSafeMIMEs). No prefix matching — `image/svg+xml` is XML with\n// scripting and lives on the active-content blocklist, not here.\nconst INLINE_SAFE_MIMES = new Set([\n \"image/png\",\n \"image/jpeg\",\n \"image/webp\",\n \"image/gif\",\n \"image/avif\",\n \"application/pdf\",\n]);\n\nexport function canPreviewFile(file: HydratedFile): boolean {\n const ct = file.content_type?.toLowerCase().split(\";\")[0]?.trim() ?? \"\";\n return INLINE_SAFE_MIMES.has(ct);\n}\n\nexport function previewFileURL(file: HydratedFile): string | undefined {\n return file.url ?? file.presigned_url;\n}\n\nexport function downloadFileURL(file: HydratedFile): string | undefined {\n if (file.url) return withDisposition(file.url, \"attachment\");\n return file.presigned_url;\n}\n\nexport function preferredListThumbnailURL(\n file: HydratedFile | null,\n preferredDerivationKey?: string,\n): string | undefined {\n if (!file) return undefined;\n const derivations = file.derivations ?? {};\n if (preferredDerivationKey && derivations[preferredDerivationKey]) {\n return derivations[preferredDerivationKey];\n }\n return derivations.thumb_200 ?? derivations.thumb_300 ?? file.url ?? file.presigned_url;\n}\n\nfunction withDisposition(url: string, disposition: \"attachment\"): string {\n try {\n const parsed = new URL(url, typeof window !== \"undefined\" ? window.location.origin : \"http://localhost\");\n parsed.searchParams.set(\"disposition\", disposition);\n if (/^https?:\\/\\//i.test(url)) return parsed.toString();\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n const separator = url.includes(\"?\") ? \"&\" : \"?\";\n return `${url}${separator}disposition=${disposition}`;\n }\n}\n\nexport function readFocalPoint(file: HydratedFile | null): FocalPoint | null {\n const raw = (file?.metadata as Record<string, unknown> | undefined)?.focal_point;\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return null;\n const candidate = raw as Record<string, unknown>;\n const x = typeof candidate.x === \"number\" ? candidate.x : Number.NaN;\n const y = typeof candidate.y === \"number\" ? candidate.y : Number.NaN;\n if (!Number.isFinite(x) || !Number.isFinite(y)) return null;\n if (x < 0 || x > 1 || y < 0 || y > 1) return null;\n return { x, y };\n}\n\nexport function focalPointObjectPosition(point: FocalPoint | null): string | undefined {\n if (!point) return undefined;\n return `${point.x * 100}% ${point.y * 100}%`;\n}\n\nexport function withFocalPoint(file: HydratedFile, point: FocalPoint): HydratedFile {\n return {\n ...file,\n metadata: {\n ...(file.metadata ?? {}),\n focal_point: point,\n },\n };\n}\n","import { createContext, useCallback, useContext, useMemo, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\n\n// FileDraftScope tracks pending file drafts so a parent form can discard them\n// if the user clicks Cancel before saving. Drafts are uploaded immediately\n// (so the user sees the preview) but only become committed when the form\n// save runs the entity update with the $files payload. Until then they are\n// \"draft\" rows in declarion.file (uploaded_by=actor, object_id=NULL) which\n// the GC sweep would eventually trash anyway — but explicit Cancel discards\n// trim the leakage window from hours to instant.\n//\n// Usage:\n// <FileDraftScope>\n// <Form>\n// <SquareImage ... />\n// <button onClick={() => scope.flushAllDrafts()}>Cancel</button>\n// </Form>\n// </FileDraftScope>\n//\n// Widgets self-register their pending drafts on upload-success and unregister\n// once the form save completes (the parent form invokes\n// `scope.markCommitted(fileId)` after PATCH /api/data/... succeeds).\n\nexport interface DraftEntry {\n fileId: string;\n discard: () => Promise<void> | void;\n}\n\nexport interface FileDraftScopeApi {\n registerDraft(entry: DraftEntry): void;\n unregisterDraft(fileId: string): void;\n hasDraft(fileId: string): boolean;\n // flushAllDrafts fires `files.discard` on every still-pending draft and\n // clears the scope. Safe to call multiple times.\n flushAllDrafts(): Promise<void>;\n // markCommitted removes a draft without discarding. Forms call this on\n // save success, when the file row has been adopted by the entity.\n markCommitted(fileId: string): void;\n}\n\nconst noopApi: FileDraftScopeApi = {\n registerDraft() {},\n unregisterDraft() {},\n hasDraft() { return false; },\n flushAllDrafts: async () => {},\n markCommitted() {},\n};\n\nconst FileDraftScopeContext = createContext<FileDraftScopeApi>(noopApi);\n\nexport function FileDraftScope({ children }: { children: ReactNode }) {\n const draftsRef = useRef<Map<string, DraftEntry>>(new Map());\n\n const api = useMemo<FileDraftScopeApi>(() => ({\n registerDraft(entry) {\n draftsRef.current.set(entry.fileId, entry);\n },\n unregisterDraft(fileId) {\n draftsRef.current.delete(fileId);\n },\n hasDraft(fileId) {\n return draftsRef.current.has(fileId);\n },\n markCommitted(fileId) {\n draftsRef.current.delete(fileId);\n },\n async flushAllDrafts() {\n const entries = Array.from(draftsRef.current.values());\n draftsRef.current.clear();\n // Best-effort parallel discard; ignore individual failures because\n // the GC sweep is the source of truth for orphan cleanup.\n await Promise.allSettled(entries.map((e) => Promise.resolve(e.discard())));\n },\n }), []);\n\n return (\n <FileDraftScopeContext.Provider value={api}>\n {children}\n </FileDraftScopeContext.Provider>\n );\n}\n\nexport function useFileDraftScope(): FileDraftScopeApi {\n return useContext(FileDraftScopeContext);\n}\n\n// useDraftRegistration is a convenience for widgets: returns stable\n// register/unregister callbacks scoped to the active FileDraftScope.\nexport function useDraftRegistration() {\n const scope = useFileDraftScope();\n const register = useCallback(\n (fileId: string, discard: () => Promise<void> | void) => {\n scope.registerDraft({ fileId, discard });\n },\n [scope],\n );\n const unregister = useCallback((fileId: string) => {\n scope.unregisterDraft(fileId);\n }, [scope]);\n const markCommitted = useCallback((fileId: string) => {\n scope.markCommitted(fileId);\n }, [scope]);\n const isDraft = useCallback((fileId: string) => {\n return scope.hasDraft(fileId);\n }, [scope]);\n return { register, unregister, markCommitted, isDraft };\n}\n","import type { FileFieldValue, HydratedFile } from \"./types\";\n\nexport function isHydratedFile(value: unknown): value is HydratedFile {\n return typeof value === \"object\" && value !== null && typeof (value as HydratedFile).id === \"string\";\n}\n\nexport function singleFileFromValue(value: FileFieldValue): HydratedFile | null {\n if (!value) return null;\n if (Array.isArray(value)) return value.find(isHydratedFile) ?? null;\n return isHydratedFile(value) ? value : null;\n}\n\nexport function fileArrayFromValue(value: FileFieldValue): HydratedFile[] {\n if (!value) return [];\n if (Array.isArray(value)) return value.filter(isHydratedFile);\n return isHydratedFile(value) ? [value] : [];\n}\n"],"mappings":"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"}
|