@adminide-stack/yantra-mobile 12.0.50-alpha.0 → 12.0.51-alpha.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.
Files changed (46) hide show
  1. package/lib/components/KeyboardComposerDock.js +76 -21
  2. package/lib/components/KeyboardComposerDock.js.map +1 -1
  3. package/lib/components/NavigationHeader/NavigationHeader.js +5 -2
  4. package/lib/components/NavigationHeader/NavigationHeader.js.map +1 -1
  5. package/lib/compute.js +14 -7
  6. package/lib/compute.js.map +1 -1
  7. package/lib/config/env-config.js +14 -2
  8. package/lib/config/env-config.js.map +1 -1
  9. package/lib/features/audio-input/NativeDictationBar.js +162 -0
  10. package/lib/features/audio-input/NativeDictationBar.js.map +1 -0
  11. package/lib/features/audio-input/nativeDictation.js +9 -0
  12. package/lib/features/audio-input/nativeDictation.js.map +1 -0
  13. package/lib/features/canvas/canvasCore.js +51 -0
  14. package/lib/features/canvas/canvasCore.js.map +1 -0
  15. package/lib/features/canvas/sessionIdentity.js +21 -0
  16. package/lib/features/canvas/sessionIdentity.js.map +1 -0
  17. package/lib/features/canvas/surfaceIndex.js +122 -1
  18. package/lib/features/canvas/surfaceIndex.js.map +1 -1
  19. package/lib/features/canvas/useCanvasSession.js +75 -29
  20. package/lib/features/canvas/useCanvasSession.js.map +1 -1
  21. package/lib/features/chat/ChatTranscript.js +35 -13
  22. package/lib/features/chat/ChatTranscript.js.map +1 -1
  23. package/lib/hooks/useCdecliChannel.js +30 -10
  24. package/lib/hooks/useCdecliChannel.js.map +1 -1
  25. package/lib/hooks/useChatApi.js +123 -41
  26. package/lib/hooks/useChatApi.js.map +1 -1
  27. package/lib/hooks/useChatStream.js +4 -2
  28. package/lib/hooks/useChatStream.js.map +1 -1
  29. package/lib/hooks/useKeyboardBottomOffset.js +22 -0
  30. package/lib/hooks/useKeyboardBottomOffset.js.map +1 -0
  31. package/lib/index.js +1 -1
  32. package/lib/index.js.map +1 -1
  33. package/lib/routes.json +14 -7
  34. package/lib/screens/Chat/index.js +39 -10
  35. package/lib/screens/Chat/index.js.map +1 -1
  36. package/lib/screens/ChatHistory/index.js +1 -1
  37. package/lib/screens/ChatHistory/index.js.map +1 -1
  38. package/lib/screens/Home/HomeScreen.js +27 -4
  39. package/lib/screens/Home/HomeScreen.js.map +1 -1
  40. package/lib/screens/Home/components/CanvasPrewarm.js +58 -0
  41. package/lib/screens/Home/components/CanvasPrewarm.js.map +1 -0
  42. package/lib/screens/Home/components/ChatHistoryLanding.js +30 -38
  43. package/lib/screens/Home/components/ChatHistoryLanding.js.map +1 -1
  44. package/lib/utils/keyboardController.js +21 -0
  45. package/lib/utils/keyboardController.js.map +1 -0
  46. package/package.json +4 -3
@@ -0,0 +1,162 @@
1
+ import {jsxs,jsx}from'react/jsx-runtime';import {useState,useRef,useCallback,useEffect}from'react';import {View,StyleSheet,ActivityIndicator,Text,Pressable}from'react-native';import {Feather}from'@expo/vector-icons';import {requireOptionalNativeModule}from'expo-modules-core';function getSpeechNative() {
2
+ try {
3
+ return requireOptionalNativeModule("ExpoSpeechRecognition");
4
+ } catch (e) {
5
+ return null;
6
+ }
7
+ }
8
+ function NativeDictationBar({
9
+ isDark,
10
+ onInterim,
11
+ onTranscriptionComplete,
12
+ onCancel,
13
+ onFallback
14
+ }) {
15
+ const [transcript, setTranscript] = useState("");
16
+ const [starting, setStarting] = useState(true);
17
+ const settledRef = useRef(false);
18
+ const transcriptRef = useRef("");
19
+ const moduleRef = useRef(null);
20
+ const finish = useCallback((mode, error) => {
21
+ var _a;
22
+ if (settledRef.current) return;
23
+ settledRef.current = true;
24
+ try {
25
+ (_a = moduleRef.current) == null ? void 0 : _a.abort();
26
+ } catch (e) {
27
+ }
28
+ if (mode === "complete") onTranscriptionComplete(transcriptRef.current.trim());
29
+ else if (mode === "cancel") onCancel();
30
+ else onFallback(error);
31
+ }, [onTranscriptionComplete, onCancel, onFallback]);
32
+ useEffect(() => {
33
+ const speech = getSpeechNative();
34
+ if (!speech) {
35
+ finish("fallback", "Speech recognition is not available.");
36
+ return void 0;
37
+ }
38
+ moduleRef.current = speech;
39
+ const resultSub = speech.addListener("result", (event) => {
40
+ var _a, _b, _c;
41
+ const text = (_c = (_b = (_a = event.results) == null ? void 0 : _a[0]) == null ? void 0 : _b.transcript) != null ? _c : "";
42
+ setStarting(false);
43
+ setTranscript(text);
44
+ transcriptRef.current = text;
45
+ onInterim(text);
46
+ });
47
+ const errorSub = speech.addListener("error", (event) => {
48
+ if (event.error === "no-speech") {
49
+ finish(transcriptRef.current.trim() ? "complete" : "cancel");
50
+ return;
51
+ }
52
+ finish("fallback", event.message || event.error || "Dictation failed.");
53
+ });
54
+ const endSub = speech.addListener("end", () => {
55
+ finish(transcriptRef.current.trim() ? "complete" : "cancel");
56
+ });
57
+ let cancelled = false;
58
+ void (async () => {
59
+ try {
60
+ const perm = await speech.requestPermissionsAsync();
61
+ if (cancelled) return;
62
+ if (!perm.granted) {
63
+ finish("fallback", "Speech recognition permission was not granted.");
64
+ return;
65
+ }
66
+ speech.start({
67
+ lang: "en-US",
68
+ interimResults: true,
69
+ continuous: true,
70
+ requiresOnDeviceRecognition: false
71
+ });
72
+ if (!cancelled) setStarting(false);
73
+ } catch (err) {
74
+ if (!cancelled) finish("fallback", err instanceof Error ? err.message : String(err));
75
+ }
76
+ })();
77
+ return () => {
78
+ cancelled = true;
79
+ resultSub.remove();
80
+ errorSub.remove();
81
+ endSub.remove();
82
+ if (!settledRef.current) {
83
+ try {
84
+ speech.abort();
85
+ } catch (e) {
86
+ }
87
+ }
88
+ };
89
+ }, [finish, onInterim]);
90
+ const done = useCallback(() => {
91
+ var _a;
92
+ try {
93
+ (_a = moduleRef.current) == null ? void 0 : _a.stop();
94
+ } catch (e) {
95
+ finish("complete");
96
+ }
97
+ }, [finish]);
98
+ return /* @__PURE__ */ jsxs(View, { style: [styles.bar, isDark && styles.barDark], accessibilityLabel: "Dictation in progress", children: [
99
+ /* @__PURE__ */ jsxs(View, { style: styles.status, children: [
100
+ starting ? /* @__PURE__ */ jsx(ActivityIndicator, { size: "small", color: isDark ? "#a5b4fc" : "#6366f1" }) : /* @__PURE__ */ jsx(View, { style: styles.dot }),
101
+ /* @__PURE__ */ jsx(Text, { style: [styles.transcript, isDark && styles.transcriptDark], numberOfLines: 2, children: transcript || (starting ? "Starting\u2026" : "Listening\u2026") })
102
+ ] }),
103
+ /* @__PURE__ */ jsxs(View, { style: styles.actions, children: [
104
+ /* @__PURE__ */ jsx(Pressable, { onPress: () => finish("cancel"), hitSlop: 10, style: styles.iconBtn, accessibilityRole: "button", accessibilityLabel: "Cancel dictation", children: /* @__PURE__ */ jsx(Feather, { name: "x", size: 20, color: isDark ? "#94a3b8" : "#64748b" }) }),
105
+ /* @__PURE__ */ jsx(Pressable, { onPress: done, hitSlop: 10, style: styles.doneBtn, accessibilityRole: "button", accessibilityLabel: "Finish dictation", children: /* @__PURE__ */ jsx(Feather, { name: "check", size: 20, color: "#ffffff" }) })
106
+ ] })
107
+ ] });
108
+ }
109
+ const styles = StyleSheet.create({
110
+ bar: {
111
+ flexDirection: "row",
112
+ alignItems: "center",
113
+ justifyContent: "space-between",
114
+ gap: 12,
115
+ paddingHorizontal: 14,
116
+ paddingVertical: 12,
117
+ borderRadius: 24,
118
+ backgroundColor: "#f1f5f9"
119
+ },
120
+ barDark: {
121
+ backgroundColor: "#1e293b"
122
+ },
123
+ status: {
124
+ flex: 1,
125
+ flexDirection: "row",
126
+ alignItems: "center",
127
+ gap: 10
128
+ },
129
+ dot: {
130
+ width: 10,
131
+ height: 10,
132
+ borderRadius: 5,
133
+ backgroundColor: "#ef4444"
134
+ },
135
+ transcript: {
136
+ flex: 1,
137
+ fontSize: 15,
138
+ color: "#0f172a"
139
+ },
140
+ transcriptDark: {
141
+ color: "#e2e8f0"
142
+ },
143
+ actions: {
144
+ flexDirection: "row",
145
+ alignItems: "center",
146
+ gap: 8
147
+ },
148
+ iconBtn: {
149
+ width: 40,
150
+ height: 40,
151
+ alignItems: "center",
152
+ justifyContent: "center"
153
+ },
154
+ doneBtn: {
155
+ width: 40,
156
+ height: 40,
157
+ borderRadius: 20,
158
+ backgroundColor: "#6366f1",
159
+ alignItems: "center",
160
+ justifyContent: "center"
161
+ }
162
+ });export{NativeDictationBar};//# sourceMappingURL=NativeDictationBar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeDictationBar.js","sources":["../../../src/features/audio-input/NativeDictationBar.tsx"],"sourcesContent":["/**\n * Live on-device dictation using Apple's SFSpeechRecognizer.\n * Talks to the native module through `requireOptionalNativeModule` so Expo Go\n * (which does not ship ExpoSpeechRecognition) never evaluates\n * `expo-speech-recognition`'s throwing `requireNativeModule` import.\n */\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\nimport { ActivityIndicator, Pressable, StyleSheet, Text, View } from 'react-native';\nimport { Feather } from '@expo/vector-icons';\nimport { requireOptionalNativeModule } from 'expo-modules-core';\n\nexport interface NativeDictationBarProps {\n isDark: boolean;\n onInterim: (text: string) => void;\n onTranscriptionComplete: (text: string) => void;\n onCancel: () => void;\n onFallback: (error?: string) => void;\n}\n\ntype SpeechNativeModule = {\n abort: () => void;\n stop: () => void;\n requestPermissionsAsync: () => Promise<{ granted: boolean }>;\n start: (options: Record<string, unknown>) => void;\n addListener: (\n event: string,\n listener: (event: { results?: Array<{ transcript?: string }>; error?: string; message?: string }) => void,\n ) => { remove: () => void };\n};\n\nfunction getSpeechNative(): SpeechNativeModule | null {\n try {\n return requireOptionalNativeModule<SpeechNativeModule>('ExpoSpeechRecognition');\n } catch {\n return null;\n }\n}\n\nexport function NativeDictationBar({\n isDark,\n onInterim,\n onTranscriptionComplete,\n onCancel,\n onFallback,\n}: NativeDictationBarProps) {\n const [transcript, setTranscript] = useState('');\n const [starting, setStarting] = useState(true);\n const settledRef = useRef(false);\n const transcriptRef = useRef('');\n const moduleRef = useRef<SpeechNativeModule | null>(null);\n\n const finish = useCallback(\n (mode: 'complete' | 'cancel' | 'fallback', error?: string) => {\n if (settledRef.current) return;\n settledRef.current = true;\n try {\n moduleRef.current?.abort();\n } catch {\n /* already stopped */\n }\n if (mode === 'complete') onTranscriptionComplete(transcriptRef.current.trim());\n else if (mode === 'cancel') onCancel();\n else onFallback(error);\n },\n [onTranscriptionComplete, onCancel, onFallback],\n );\n\n useEffect(() => {\n const speech = getSpeechNative();\n if (!speech) {\n finish('fallback', 'Speech recognition is not available.');\n return undefined;\n }\n moduleRef.current = speech;\n\n const resultSub = speech.addListener('result', (event) => {\n const text = event.results?.[0]?.transcript ?? '';\n setStarting(false);\n setTranscript(text);\n transcriptRef.current = text;\n onInterim(text);\n });\n const errorSub = speech.addListener('error', (event) => {\n if (event.error === 'no-speech') {\n finish(transcriptRef.current.trim() ? 'complete' : 'cancel');\n return;\n }\n finish('fallback', event.message || event.error || 'Dictation failed.');\n });\n const endSub = speech.addListener('end', () => {\n finish(transcriptRef.current.trim() ? 'complete' : 'cancel');\n });\n\n let cancelled = false;\n void (async () => {\n try {\n const perm = await speech.requestPermissionsAsync();\n if (cancelled) return;\n if (!perm.granted) {\n finish('fallback', 'Speech recognition permission was not granted.');\n return;\n }\n speech.start({\n lang: 'en-US',\n interimResults: true,\n continuous: true,\n requiresOnDeviceRecognition: false,\n });\n if (!cancelled) setStarting(false);\n } catch (err) {\n if (!cancelled) finish('fallback', err instanceof Error ? err.message : String(err));\n }\n })();\n\n return () => {\n cancelled = true;\n resultSub.remove();\n errorSub.remove();\n endSub.remove();\n if (!settledRef.current) {\n try {\n speech.abort();\n } catch {\n /* noop */\n }\n }\n };\n }, [finish, onInterim]);\n\n const done = useCallback(() => {\n try {\n moduleRef.current?.stop();\n } catch {\n finish('complete');\n }\n }, [finish]);\n\n return (\n <View style={[styles.bar, isDark && styles.barDark]} accessibilityLabel=\"Dictation in progress\">\n <View style={styles.status}>\n {starting ? (\n <ActivityIndicator size=\"small\" color={isDark ? '#a5b4fc' : '#6366f1'} />\n ) : (\n <View style={styles.dot} />\n )}\n <Text style={[styles.transcript, isDark && styles.transcriptDark]} numberOfLines={2}>\n {transcript || (starting ? 'Starting…' : 'Listening…')}\n </Text>\n </View>\n <View style={styles.actions}>\n <Pressable\n onPress={() => finish('cancel')}\n hitSlop={10}\n style={styles.iconBtn}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Cancel dictation\"\n >\n <Feather name=\"x\" size={20} color={isDark ? '#94a3b8' : '#64748b'} />\n </Pressable>\n <Pressable\n onPress={done}\n hitSlop={10}\n style={styles.doneBtn}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Finish dictation\"\n >\n <Feather name=\"check\" size={20} color=\"#ffffff\" />\n </Pressable>\n </View>\n </View>\n );\n}\n\nconst styles = StyleSheet.create({\n bar: {\n flexDirection: 'row',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: 12,\n paddingHorizontal: 14,\n paddingVertical: 12,\n borderRadius: 24,\n backgroundColor: '#f1f5f9',\n },\n barDark: { backgroundColor: '#1e293b' },\n status: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 10 },\n dot: { width: 10, height: 10, borderRadius: 5, backgroundColor: '#ef4444' },\n transcript: { flex: 1, fontSize: 15, color: '#0f172a' },\n transcriptDark: { color: '#e2e8f0' },\n actions: { flexDirection: 'row', alignItems: 'center', gap: 8 },\n iconBtn: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center' },\n doneBtn: {\n width: 40,\n height: 40,\n borderRadius: 20,\n backgroundColor: '#6366f1',\n alignItems: 'center',\n justifyContent: 'center',\n },\n});\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import {requireOptionalNativeModule}from'expo-modules-core';function isNativeDictationAvailable() {
2
+ try {
3
+ const mod = requireOptionalNativeModule("ExpoSpeechRecognition");
4
+ if (!mod) return false;
5
+ return typeof mod.isRecognitionAvailable === "function" ? mod.isRecognitionAvailable() : true;
6
+ } catch (e) {
7
+ return false;
8
+ }
9
+ }export{isNativeDictationAvailable};//# sourceMappingURL=nativeDictation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeDictation.js","sources":["../../../src/features/audio-input/nativeDictation.ts"],"sourcesContent":["/**\n * Availability probe for Apple's on-device speech recognition\n * (SFSpeechRecognizer via expo-speech-recognition) - the iOS-built-in mic the\n * chat composer prefers, with the record -> Whisper recorder as the fallback.\n *\n * The check deliberately goes through requireOptionalNativeModule rather than\n * importing `expo-speech-recognition`: that package calls\n * `requireNativeModule(\"ExpoSpeechRecognition\")` at module-eval time, which\n * THROWS on any binary that shipped before the native module was linked.\n * Reading the module optionally lets a stale binary answer \"unavailable\" and\n * fall back to Whisper instead of crashing - `NativeDictationBar` (which does\n * import the full JS API) is only ever mounted once this returns true.\n */\nimport { requireOptionalNativeModule } from 'expo-modules-core';\n\ninterface SpeechNativeModule {\n isRecognitionAvailable?: () => boolean;\n}\n\n/** True only when this binary can do native on-device dictation right now. */\nexport function isNativeDictationAvailable(): boolean {\n try {\n const mod = requireOptionalNativeModule<SpeechNativeModule>('ExpoSpeechRecognition');\n if (!mod) return false;\n // Newer builds expose isRecognitionAvailable(); if it is missing the\n // module is still linked, so treat presence as available.\n return typeof mod.isRecognitionAvailable === 'function' ? mod.isRecognitionAvailable() : true;\n } catch {\n return false;\n }\n}\n"],"names":[],"mappings":"4DAmBO,SAAS,0BAAsC,GAAA;AACpD,EAAI,IAAA;AACF,IAAM,MAAA,GAAA,GAAM,4BAAgD,uBAAuB,CAAA;AACnF,IAAI,IAAA,CAAC,KAAY,OAAA,KAAA;AAGjB,IAAA,OAAO,OAAO,GAAI,CAAA,sBAAA,KAA2B,UAAa,GAAA,GAAA,CAAI,wBAA2B,GAAA,IAAA;AAAA,GACnF,CAAA,OAAA,CAAA,EAAA;AACN,IAAO,OAAA,KAAA;AAAA;AAEX"}
@@ -0,0 +1,51 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __spreadValues = (a, b) => {
7
+ for (var prop in b || (b = {}))
8
+ if (__hasOwnProp.call(b, prop))
9
+ __defNormalProp(a, prop, b[prop]);
10
+ if (__getOwnPropSymbols)
11
+ for (var prop of __getOwnPropSymbols(b)) {
12
+ if (__propIsEnum.call(b, prop))
13
+ __defNormalProp(a, prop, b[prop]);
14
+ }
15
+ return a;
16
+ };
17
+ function parseBoardState(raw) {
18
+ if (!raw || typeof raw !== "object") return null;
19
+ const candidate = raw;
20
+ if (candidate.version !== 1 || !Array.isArray(candidate.items)) return null;
21
+ const items = candidate.items.filter(isRenderableItem);
22
+ return __spreadValues({
23
+ version: 1,
24
+ savedAt: typeof candidate.savedAt === "number" ? candidate.savedAt : 0,
25
+ items,
26
+ zoom: clampZoom(typeof candidate.zoom === "number" ? candidate.zoom : 1)
27
+ }, typeof candidate.title === "string" && candidate.title ? {
28
+ title: candidate.title
29
+ } : {});
30
+ }
31
+ function emptyBoardState(title) {
32
+ return __spreadValues({
33
+ version: 1,
34
+ savedAt: 0,
35
+ items: [],
36
+ zoom: 1
37
+ }, title ? {
38
+ title
39
+ } : {});
40
+ }
41
+ function isRenderableItem(item) {
42
+ if (!item || typeof item !== "object") return false;
43
+ const c = item;
44
+ return typeof c.id === "string" && (c.type === "image" || c.type === "video" || c.type === "audio") && typeof c.src === "string" && c.src.length > 0 && !c.src.startsWith("blob:") && [c.x, c.y, c.width, c.height].every((n) => typeof n === "number" && Number.isFinite(n)) && c.width > 0 && c.height > 0;
45
+ }
46
+ const MIN_ZOOM = 0.25;
47
+ const MAX_ZOOM = 4;
48
+ function clampZoom(zoom) {
49
+ if (!Number.isFinite(zoom)) return 1;
50
+ return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
51
+ }export{MAX_ZOOM,MIN_ZOOM,clampZoom,emptyBoardState,isRenderableItem,parseBoardState};//# sourceMappingURL=canvasCore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvasCore.js","sources":["../../../src/features/canvas/canvasCore.ts"],"sourcesContent":["/**\n * Platform-agnostic canvas document model — the native twin of the web\n * surface's `state/boards.ts` (`@yantra/canvas-surface`).\n *\n * The shapes are copied, not imported: the surface package builds for the DOM\n * (react-dom peer, vite, `crypto.randomUUID`), so importing it would drag the\n * web toolchain into Metro. The contract that actually matters is the persisted\n * JSON — `PersistedCanvasState` version 1 — and tests below pin this file to\n * fixtures of that JSON, so drift between the copies fails a test rather than\n * corrupting a board.\n *\n * Everything here is pure data + math so it runs identically under Metro,\n * vite, and jest: no RN imports, no DOM globals.\n */\n\nexport interface CanvasItem {\n id: string;\n type: 'image' | 'video' | 'audio';\n src: string;\n x: number;\n y: number;\n width: number;\n height: number;\n zIndex: number;\n generatedAssetName?: string;\n}\n\nexport interface PersistedCanvasState {\n version: 1;\n savedAt: number;\n items: CanvasItem[];\n zoom: number;\n title?: string;\n}\n\n/** Parse a persisted board, tolerating the unknown: bad rows drop, not throw. */\nexport function parseBoardState(raw: unknown): PersistedCanvasState | null {\n if (!raw || typeof raw !== 'object') return null;\n const candidate = raw as Partial<PersistedCanvasState>;\n if (candidate.version !== 1 || !Array.isArray(candidate.items)) return null;\n const items = candidate.items.filter(isRenderableItem);\n return {\n version: 1,\n savedAt: typeof candidate.savedAt === 'number' ? candidate.savedAt : 0,\n items,\n zoom: clampZoom(typeof candidate.zoom === 'number' ? candidate.zoom : 1),\n ...(typeof candidate.title === 'string' && candidate.title ? { title: candidate.title } : {}),\n };\n}\n\n/** Blank board used when the index row has no items yet — native still opens it. */\nexport function emptyBoardState(title?: string): PersistedCanvasState {\n return {\n version: 1,\n savedAt: 0,\n items: [],\n zoom: 1,\n ...(title ? { title } : {}),\n };\n}\n\n/**\n * Media that cannot survive a reload. Web drops `blob:` (dies with the tab);\n * native also drops device URIs (`file:`, photo-library schemes) until the host\n * has uploaded them to storage — same rule as canvas-surface `buildBoardState`.\n */\nexport function isLocalOnlySrc(src: string): boolean {\n return /^(blob:|file:|content:|ph:|assets-library:|data:)/i.test(src.trim());\n}\n\n/**\n * State to write to the index. Title is sticky once the user (or the first\n * labelled item) sets it, matching web `buildBoardState`.\n */\n/** Placeholder titles native used to stamp on a blank board. Web leaves title\n * unset so the first upload (`IMG_0002`) becomes the grid name. */\nexport function isUnsetBoardTitle(title?: string): boolean {\n const t = title?.trim();\n return !t || /^untitled$/i.test(t) || /^new canvas$/i.test(t);\n}\n\n/** Web `addMediaItem` strips the extension so `IMG_0003.jpeg` → `IMG_0003`. */\nexport function labelFromFileName(name: string): string {\n const base = name.trim().split(/[\\\\/]/).pop() || name.trim();\n const stripped = base.replace(/\\.[^.]+$/, '');\n return stripped || base;\n}\n\nexport function buildBoardState(\n items: readonly CanvasItem[],\n zoom: number,\n currentTitle?: string,\n): PersistedCanvasState {\n const persistableItems = items.filter((it) => !isLocalOnlySrc(it.src));\n const named = persistableItems.find((it) => it.generatedAssetName)?.generatedAssetName;\n return {\n version: 1,\n savedAt: Date.now(),\n items: persistableItems,\n zoom: clampZoom(zoom),\n title: isUnsetBoardTitle(currentTitle) ? named : currentTitle,\n };\n}\n\n/**\n * Comparable fingerprint of what is worth a write. Geometry and zoom ride\n * along on the next real save but are not themselves a reason to hit the index\n * (web `boardSignature`).\n */\nexport function boardSignature(state: PersistedCanvasState): string {\n return JSON.stringify({\n title: state.title ?? '',\n items: state.items.map((it) => [it.id, it.type, it.src, it.generatedAssetName ?? '']),\n });\n}\n\n/** Mint a board id. The index upserts on `{tenantId, surface, channelId}` with no chat FK. */\nexport function newBoardId(): string {\n const c = globalThis.crypto as Crypto | undefined;\n if (c?.randomUUID) return c.randomUUID();\n return `board-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/** An item the viewer can draw. `blob:` URLs died with the web tab that made them. */\nexport function isRenderableItem(item: unknown): item is CanvasItem {\n if (!item || typeof item !== 'object') return false;\n const c = item as Partial<CanvasItem>;\n return (\n typeof c.id === 'string' &&\n (c.type === 'image' || c.type === 'video' || c.type === 'audio') &&\n typeof c.src === 'string' &&\n c.src.length > 0 &&\n !c.src.startsWith('blob:') &&\n [c.x, c.y, c.width, c.height].every((n) => typeof n === 'number' && Number.isFinite(n)) &&\n (c.width as number) > 0 &&\n (c.height as number) > 0\n );\n}\n\n/**\n * Board frame for an upload — twin of canvas-surface `addMediaItem`.\n * Web does not use the file's pixel size (a 12MP photo would fill the plane\n * and force 25% zoom). Image 500×400, video 640×360, audio 400×80, at (100,100).\n */\nexport function frameForUpload(type: CanvasItem['type']): Pick<CanvasItem, 'x' | 'y' | 'width' | 'height'> {\n if (type === 'video') return { x: 100, y: 100, width: 640, height: 360 };\n if (type === 'audio') return { x: 100, y: 100, width: 400, height: 80 };\n return { x: 100, y: 100, width: 500, height: 400 };\n}\n\n/** Device-pixel dumps (a 12MP photo) must not sit at 100% zoom and fill the phone. */\nexport function coerceUploadSizedItem(item: CanvasItem): CanvasItem {\n const cap = frameForUpload(item.type);\n if (item.width <= cap.width * 1.5 && item.height <= cap.height * 1.5) return item;\n return { ...item, width: cap.width, height: cap.height, x: item.x || cap.x, y: item.y || cap.y };\n}\n\n/** Twin of canvas-surface `dimsForAspect` — generated video/image frame size. */\nexport function dimsForAspect(aspect: string, maxEdge: number): { width: number; height: number } {\n const [w, h] = aspect.split(':').map(Number);\n if (!w || !h) return { width: maxEdge, height: maxEdge };\n return w >= h\n ? { width: maxEdge, height: Math.round((maxEdge * h) / w) }\n : { width: Math.round((maxEdge * w) / h), height: maxEdge };\n}\n\n/** Floor for item edits - a rect can never be resized away entirely. */\nexport const MIN_ITEM_SIZE = 24;\n\nexport interface ItemFrame {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport type ResizeCorner = 'tl' | 'tr' | 'bl' | 'br';\n\n/**\n * Resize `frame` by dragging `corner` by (dx, dy) board units, keeping the\n * OPPOSITE corner anchored - the gesture the selection handles drive. Both\n * axes move independently (free-form resize); each side clamps at\n * MIN_ITEM_SIZE so a rect can shrink to a nub but never invert or vanish.\n *\n * Marked as a worklet: the resize handles call this per-frame on the UI\n * thread. The directive is inert everywhere else (plain string in node/jest).\n */\nexport function resizeFrame(frame: ItemFrame, corner: ResizeCorner, dx: number, dy: number): ItemFrame {\n 'worklet';\n\n const left = corner === 'tl' || corner === 'bl';\n const top = corner === 'tl' || corner === 'tr';\n\n const width = Math.max(MIN_ITEM_SIZE, left ? frame.width - dx : frame.width + dx);\n const height = Math.max(MIN_ITEM_SIZE, top ? frame.height - dy : frame.height + dy);\n\n return {\n // Anchor the opposite corner: a left/top edge moves by however much the\n // clamped size actually changed, not by the raw drag delta.\n x: left ? frame.x + (frame.width - width) : frame.x,\n y: top ? frame.y + (frame.height - height) : frame.y,\n width,\n height,\n };\n}\n\n/** New items array with `id`'s frame replaced - the commit step after a drag\n * or resize gesture ends. Unknown ids return the array unchanged. */\nexport function applyItemFrame(items: readonly CanvasItem[], id: string, frame: ItemFrame): CanvasItem[] {\n return items.map((item) => (item.id === id ? { ...item, ...frame } : item));\n}\n\nexport const MIN_ZOOM = 0.25;\nexport const MAX_ZOOM = 4;\n\nexport function clampZoom(zoom: number): number {\n if (!Number.isFinite(zoom)) return 1;\n return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));\n}\n\nexport interface BoardBounds {\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n}\n\n/** Union of item rects; null for an empty board (an empty union has no bounds). */\nexport function boardBounds(items: readonly CanvasItem[]): BoardBounds | null {\n if (items.length === 0) return null;\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n for (const it of items) {\n minX = Math.min(minX, it.x);\n minY = Math.min(minY, it.y);\n maxX = Math.max(maxX, it.x + it.width);\n maxY = Math.max(maxY, it.y + it.height);\n }\n return { minX, minY, maxX, maxY };\n}\n\nexport interface FitTransform {\n /** Uniform scale applied to board coordinates. */\n scale: number;\n /** Screen-space translation, applied after scaling. */\n translateX: number;\n translateY: number;\n}\n\n/**\n * Fit the whole board inside a viewport with a margin — the viewer's initial\n * camera. Pure math so pinch/pan gestures can start from a reproducible state.\n */\nexport function fitToViewport(\n bounds: BoardBounds,\n viewportWidth: number,\n viewportHeight: number,\n margin = 24,\n): FitTransform {\n const boardW = Math.max(1, bounds.maxX - bounds.minX);\n const boardH = Math.max(1, bounds.maxY - bounds.minY);\n const availW = Math.max(1, viewportWidth - margin * 2);\n const availH = Math.max(1, viewportHeight - margin * 2);\n const scale = clampZoom(Math.min(availW / boardW, availH / boardH));\n // Center the scaled board in the viewport.\n const translateX = (viewportWidth - boardW * scale) / 2 - bounds.minX * scale;\n const translateY = (viewportHeight - boardH * scale) / 2 - bounds.minY * scale;\n return { scale, translateX, translateY };\n}\n\n/** Painter's order: stable sort by zIndex so overlapping media stack as authored. */\nexport function paintOrder(items: readonly CanvasItem[]): CanvasItem[] {\n return [...items].sort((a, b) => a.zIndex - b.zIndex || a.id.localeCompare(b.id));\n}\n\n/**\n * Fan items with IDENTICAL rects into a row - a DISPLAY-layer fix.\n *\n * \"Save to canvas\" appends every file at the same default frame, so a board\n * the user never arranged holds its media exactly stacked: the viewer renders\n * all of them faithfully and shows only the top one. Items whose rect is an\n * exact duplicate of an earlier item's slide right by (width + gutter) each,\n * so every file is visible side by side; a board with a real layout has no\n * exact duplicates and passes through untouched.\n *\n * Deliberately not persisted: the spread is how the board is SHOWN, and only\n * becomes real geometry if the user drags an item, which commits that item's\n * new frame like any other edit.\n */\nexport function spreadStackedItems(items: readonly CanvasItem[], gutter = 24): CanvasItem[] {\n const seen = new Map<string, number>();\n return items.map((item) => {\n const key = `${item.x}|${item.y}|${item.width}|${item.height}`;\n const n = seen.get(key) ?? 0;\n seen.set(key, n + 1);\n return n === 0 ? item : { ...item, x: item.x + (item.width + gutter) * n };\n });\n}\n\n/** New list with `id` painted above every other item; absent id = no-op. */\nexport function bringToFront(items: readonly CanvasItem[], id: string): CanvasItem[] {\n const top = Math.max(...items.map((i) => i.zIndex), 0);\n return items.map((i) => (i.id === id && i.zIndex <= top ? { ...i, zIndex: top + 1 } : i));\n}\n\n/** New list with `id` painted below every other item; absent id = no-op. */\nexport function sendToBack(items: readonly CanvasItem[], id: string): CanvasItem[] {\n const bottom = Math.min(...items.map((i) => i.zIndex), 0);\n return items.map((i) => (i.id === id && i.zIndex >= bottom ? { ...i, zIndex: bottom - 1 } : i));\n}\n\n/** New list without `id`; absent id = same content. */\nexport function removeItem(items: readonly CanvasItem[], id: string): CanvasItem[] {\n return items.filter((i) => i.id !== id);\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAmCO,SAAS,gBAAgB,GAA2C,EAAA;AACzE,EAAA,IAAI,CAAC,GAAA,IAAO,OAAO,GAAA,KAAQ,UAAiB,OAAA,IAAA;AAC5C,EAAA,MAAM,SAAY,GAAA,GAAA;AAClB,EAAI,IAAA,SAAA,CAAU,YAAY,CAAK,IAAA,CAAC,MAAM,OAAQ,CAAA,SAAA,CAAU,KAAK,CAAA,EAAU,OAAA,IAAA;AACvE,EAAA,MAAM,KAAQ,GAAA,SAAA,CAAU,KAAM,CAAA,MAAA,CAAO,gBAAgB,CAAA;AACrD,EAAO,OAAA,cAAA,CAAA;AAAA,IACL,OAAS,EAAA,CAAA;AAAA,IACT,SAAS,OAAO,SAAA,CAAU,OAAY,KAAA,QAAA,GAAW,UAAU,OAAU,GAAA,CAAA;AAAA,IACrE,KAAA;AAAA,IACA,IAAA,EAAM,UAAU,OAAO,SAAA,CAAU,SAAS,QAAW,GAAA,SAAA,CAAU,OAAO,CAAC;AAAA,GAAA,EACnE,OAAO,SAAA,CAAU,KAAU,KAAA,QAAA,IAAY,UAAU,KAAQ,GAAA;AAAA,IAC3D,OAAO,SAAU,CAAA;AAAA,MACf,EAAC,CAAA;AAET;AAGO,SAAS,gBAAgB,KAAsC,EAAA;AACpE,EAAO,OAAA,cAAA,CAAA;AAAA,IACL,OAAS,EAAA,CAAA;AAAA,IACT,OAAS,EAAA,CAAA;AAAA,IACT,OAAO,EAAC;AAAA,IACR,IAAM,EAAA;AAAA,GAAA,EACF,KAAQ,GAAA;AAAA,IACV;AAAA,MACE,EAAC,CAAA;AAET;AA4DO,SAAS,iBAAiB,IAAmC,EAAA;AAClE,EAAA,IAAI,CAAC,IAAA,IAAQ,OAAO,IAAA,KAAS,UAAiB,OAAA,KAAA;AAC9C,EAAA,MAAM,CAAI,GAAA,IAAA;AACV,EAAO,OAAA,OAAO,EAAE,EAAO,KAAA,QAAA,KAAa,EAAE,IAAS,KAAA,OAAA,IAAW,CAAE,CAAA,IAAA,KAAS,OAAW,IAAA,CAAA,CAAE,SAAS,OAAY,CAAA,IAAA,OAAO,CAAE,CAAA,GAAA,KAAQ,QAAY,IAAA,CAAA,CAAE,IAAI,MAAS,GAAA,CAAA,IAAK,CAAC,CAAA,CAAE,GAAI,CAAA,UAAA,CAAW,OAAO,CAAK,IAAA,CAAC,CAAE,CAAA,CAAA,EAAG,CAAE,CAAA,CAAA,EAAG,EAAE,KAAO,EAAA,CAAA,CAAE,MAAM,CAAA,CAAE,KAAM,CAAA,CAAA,CAAA,KAAK,OAAO,CAAM,KAAA,QAAA,IAAY,MAAO,CAAA,QAAA,CAAS,CAAC,CAAC,KAAK,CAAE,CAAA,KAAA,GAAkB,CAAK,IAAA,CAAA,CAAE,MAAmB,GAAA,CAAA;AAC/T;AAwGO,MAAM,QAAW,GAAA;AACjB,MAAM,QAAW,GAAA;AACjB,SAAS,UAAU,IAAsB,EAAA;AAC9C,EAAA,IAAI,CAAC,MAAA,CAAO,QAAS,CAAA,IAAI,GAAU,OAAA,CAAA;AACnC,EAAA,OAAO,KAAK,GAAI,CAAA,QAAA,EAAU,KAAK,GAAI,CAAA,QAAA,EAAU,IAAI,CAAC,CAAA;AACpD"}
@@ -0,0 +1,21 @@
1
+ function b64urlJson(segment) {
2
+ try {
3
+ const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
4
+ const pad = b64 + "=".repeat((4 - b64.length % 4) % 4);
5
+ const text = typeof atob === "function" ? atob(pad) : (
6
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
7
+ require("buffer").Buffer.from(pad, "base64").toString("utf8")
8
+ );
9
+ return JSON.parse(text);
10
+ } catch (e) {
11
+ return null;
12
+ }
13
+ }
14
+ function tokenExpiryMs(token) {
15
+ if (!token) return null;
16
+ const parts = token.split(".");
17
+ if (parts.length < 2) return null;
18
+ const claims = b64urlJson(parts[1]);
19
+ const exp = claims == null ? void 0 : claims.exp;
20
+ return typeof exp === "number" && Number.isFinite(exp) ? exp * 1e3 : null;
21
+ }export{tokenExpiryMs};//# sourceMappingURL=sessionIdentity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionIdentity.js","sources":["../../../src/features/canvas/sessionIdentity.ts"],"sourcesContent":["/**\n * Best-effort identity readout from the surface session token - purely for\n * DISPLAY. The canvas index scopes rows by the token's tenant, so \"No\n * canvases yet\" is ambiguous without saying WHO the index was asked as: the\n * same account sees different boards per org, and a phone whose org\n * bootstrap picked a different default org reports empty while the web shows\n * boards. No verification here (display only) - the payload is decoded, not\n * trusted.\n */\nexport interface SessionIdentity {\n org?: string;\n user?: string;\n}\n\nfunction b64urlJson(segment: string): Record<string, unknown> | null {\n try {\n const b64 = segment.replace(/-/g, '+').replace(/_/g, '/');\n const pad = b64 + '='.repeat((4 - (b64.length % 4)) % 4);\n // atob exists in RN (Hermes) and the web; Buffer covers node/jest.\n const text =\n typeof atob === 'function'\n ? atob(pad)\n : // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require\n (\n require('buffer') as { Buffer: { from(s: string, e: string): { toString(e: string): string } } }\n ).Buffer.from(pad, 'base64').toString('utf8');\n return JSON.parse(text) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/**\n * The token's `exp` claim in epoch milliseconds, or null if it cannot be read.\n * Used to schedule a re-mint before the surface session expires. Display-only\n * decode (no verification), same as the identity readout.\n */\nexport function tokenExpiryMs(token: string | null | undefined): number | null {\n if (!token) return null;\n const parts = token.split('.');\n if (parts.length < 2) return null;\n const claims = b64urlJson(parts[1]);\n const exp = claims?.exp;\n return typeof exp === 'number' && Number.isFinite(exp) ? exp * 1000 : null;\n}\n\nexport function sessionIdentityFromToken(token: string | null | undefined): SessionIdentity {\n if (!token) return {};\n const parts = token.split('.');\n if (parts.length < 2) return {};\n const claims = b64urlJson(parts[1]);\n if (!claims) return {};\n const pick = (...keys: string[]): string | undefined => {\n for (const key of keys) {\n const value = claims[key];\n if (typeof value === 'string' && value.trim()) return value.trim();\n }\n return undefined;\n };\n return {\n org: pick('orgName', 'tenantId', 'org', 'organization'),\n user: pick('email', 'username', 'name', 'sub', 'userId'),\n };\n}\n\n/** \"org acme · user jo@x.com · index surface-index-backend.foo\" fine print. */\nexport function describeSession(token: string | null | undefined, indexUrl: string): string {\n const id = sessionIdentityFromToken(token);\n const host = indexUrl.replace(/^https?:\\/\\//, '').split('/')[0];\n const parts: string[] = [];\n if (id.org) parts.push(`org ${id.org}`);\n if (id.user) parts.push(id.user);\n if (host) parts.push(host);\n return parts.join(' · ');\n}\n"],"names":[],"mappings":"AAaA,SAAS,WAAW,OAAiD,EAAA;AACnE,EAAI,IAAA;AACF,IAAM,MAAA,GAAA,GAAM,QAAQ,OAAQ,CAAA,IAAA,EAAM,GAAG,CAAE,CAAA,OAAA,CAAQ,MAAM,GAAG,CAAA;AACxD,IAAM,MAAA,GAAA,GAAM,MAAM,GAAI,CAAA,MAAA,CAAA,CAAQ,IAAI,GAAI,CAAA,MAAA,GAAS,KAAK,CAAC,CAAA;AAErD,IAAA,MAAM,IAAO,GAAA,OAAO,IAAS,KAAA,UAAA,GAAa,KAAK,GAAG,CAAA;AAAA;AAAA,MAEjD,OAAA,CAAQ,QAAQ,CAMd,CAAA,MAAA,CAAO,KAAK,GAAK,EAAA,QAAQ,CAAE,CAAA,QAAA,CAAS,MAAM;AAAA,KAAA;AAC7C,IAAO,OAAA,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,GAChB,CAAA,OAAA,CAAA,EAAA;AACN,IAAO,OAAA,IAAA;AAAA;AAEX;AAOO,SAAS,cAAc,KAAiD,EAAA;AAC7E,EAAI,IAAA,CAAC,OAAc,OAAA,IAAA;AACnB,EAAM,MAAA,KAAA,GAAQ,KAAM,CAAA,KAAA,CAAM,GAAG,CAAA;AAC7B,EAAI,IAAA,KAAA,CAAM,MAAS,GAAA,CAAA,EAAU,OAAA,IAAA;AAC7B,EAAA,MAAM,MAAS,GAAA,UAAA,CAAW,KAAM,CAAA,CAAC,CAAC,CAAA;AAClC,EAAA,MAAM,MAAM,MAAQ,IAAA,IAAA,GAAA,MAAA,GAAA,MAAA,CAAA,GAAA;AACpB,EAAO,OAAA,OAAO,QAAQ,QAAY,IAAA,MAAA,CAAO,SAAS,GAAG,CAAA,GAAI,MAAM,GAAO,GAAA,IAAA;AACxE"}
@@ -1,3 +1,21 @@
1
+ import {parseBoardState,emptyBoardState}from'./canvasCore.js';var __defProp = Object.defineProperty;
2
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
3
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
4
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
5
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
6
+ var __spreadValues = (a, b) => {
7
+ for (var prop in b || (b = {}))
8
+ if (__hasOwnProp.call(b, prop))
9
+ __defNormalProp(a, prop, b[prop]);
10
+ if (__getOwnPropSymbols)
11
+ for (var prop of __getOwnPropSymbols(b)) {
12
+ if (__propIsEnum.call(b, prop))
13
+ __defNormalProp(a, prop, b[prop]);
14
+ }
15
+ return a;
16
+ };
17
+ const CHANNEL_FIELDS = "id surface channelId title metadata updatedAt";
18
+ const SCRATCH_BOARD_ID = "global";
1
19
  function surfaceIndexUrl(graphqlUrl, explicit) {
2
20
  const override = (explicit || "").trim();
3
21
  if (override) return override;
@@ -30,4 +48,107 @@ function parseOrigin(url) {
30
48
  protocol: match[1].toLowerCase(),
31
49
  host: match[2].toLowerCase()
32
50
  };
33
- }export{surfaceIndexUrl,surfaceIndexUrlFromTemplate};//# sourceMappingURL=surfaceIndex.js.map
51
+ }
52
+ let boardsCache = null;
53
+ function getCachedCanvasBoards(url) {
54
+ if (!url || !boardsCache || boardsCache.url !== url) return null;
55
+ return boardsCache.boards;
56
+ }
57
+ function setCachedCanvasBoards(url, boards) {
58
+ if (!url) return;
59
+ boardsCache = {
60
+ url,
61
+ boards
62
+ };
63
+ }
64
+ async function fetchMyCanvasBoards(opts) {
65
+ var _a, _b, _c, _d, _e;
66
+ if (!opts.url) {
67
+ return {
68
+ boards: [],
69
+ error: "No canvas index configured for this build."
70
+ };
71
+ }
72
+ let payload;
73
+ try {
74
+ const response = await fetch(opts.url, {
75
+ method: "POST",
76
+ headers: __spreadValues({
77
+ "content-type": "application/json"
78
+ }, opts.token ? {
79
+ authorization: `Bearer ${opts.token}`
80
+ } : {}),
81
+ body: JSON.stringify({
82
+ query: `query MySurfaceChannels($surface: SurfaceType) {
83
+ mySurfaceChannels(surface: $surface) { ${CHANNEL_FIELDS} }
84
+ }`,
85
+ variables: {
86
+ surface: "canvas"
87
+ }
88
+ }),
89
+ signal: opts.signal
90
+ });
91
+ if (response.status === 401 || response.status === 403) {
92
+ return {
93
+ boards: [],
94
+ error: "Your session expired. Sign in again to see your canvases."
95
+ };
96
+ }
97
+ if (!response.ok) {
98
+ return {
99
+ boards: [],
100
+ error: `The canvas index is unreachable (HTTP ${response.status}).`
101
+ };
102
+ }
103
+ payload = await response.json();
104
+ } catch (err) {
105
+ if (err instanceof Error && err.name === "AbortError") {
106
+ return {
107
+ boards: [],
108
+ error: null
109
+ };
110
+ }
111
+ return {
112
+ boards: [],
113
+ error: "Could not reach the canvas index. Check your connection."
114
+ };
115
+ }
116
+ if ((_a = payload == null ? void 0 : payload.errors) == null ? void 0 : _a.length) {
117
+ const raw = ((_b = payload.errors[0]) == null ? void 0 : _b.message) || "";
118
+ const isAuth = /tenantid|unauthenticated|unauthorized|not authorised|forbidden/i.test(raw);
119
+ return {
120
+ boards: [],
121
+ error: isAuth ? "Your session expired. Sign in again to see your canvases." : raw || "The canvas index rejected the request."
122
+ };
123
+ }
124
+ const rows = (_d = (_c = payload == null ? void 0 : payload.data) == null ? void 0 : _c.mySurfaceChannels) != null ? _d : [];
125
+ const boards = [];
126
+ for (const row of rows) {
127
+ const channelId = typeof (row == null ? void 0 : row.channelId) === "string" ? row.channelId : "";
128
+ if (!channelId || channelId === SCRATCH_BOARD_ID) continue;
129
+ const title = typeof (row == null ? void 0 : row.title) === "string" ? row.title : void 0;
130
+ const board = (_e = parseBoardState(coerceMetadata(row == null ? void 0 : row.metadata))) != null ? _e : emptyBoardState(title);
131
+ if (board.items.length === 0) continue;
132
+ boards.push({
133
+ channelId,
134
+ title: board.title || title,
135
+ savedAt: board.savedAt,
136
+ itemCount: board.items.length,
137
+ board
138
+ });
139
+ }
140
+ boards.sort((a, b) => b.savedAt - a.savedAt);
141
+ setCachedCanvasBoards(opts.url, boards);
142
+ return {
143
+ boards,
144
+ error: null
145
+ };
146
+ }
147
+ function coerceMetadata(value) {
148
+ if (typeof value !== "string") return value;
149
+ try {
150
+ return JSON.parse(value);
151
+ } catch (e) {
152
+ return null;
153
+ }
154
+ }export{fetchMyCanvasBoards,getCachedCanvasBoards,setCachedCanvasBoards,surfaceIndexUrl,surfaceIndexUrlFromTemplate};//# sourceMappingURL=surfaceIndex.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"surfaceIndex.js","sources":["../../../src/features/canvas/surfaceIndex.ts"],"sourcesContent":["/**\n * Native reader for the canvas boards in the SurfaceChannel index.\n *\n * The index (`servers/surface-index-subgraph-server`) is the ONLY store for a\n * canvas: the whole `PersistedCanvasState` rides in the row's `metadata`, and\n * `mySurfaceChannels` enumerates the caller's boards. Rows are per-USER, so the\n * phone — signed in as the same user — sees exactly what the web surface sees.\n *\n * This is the native twin of `@yantra/canvas-surface`'s `state/boards.ts`\n * (`pullBoards`). It is copied rather than imported for the same reason\n * `canvasCore.ts` is: that package builds for the DOM (vite, `import.meta.env`,\n * `window.location`), and importing it would drag the web toolchain into Metro.\n * The contract that matters is the wire shape, which both sides pin.\n *\n * Reads are best-effort: an unreachable index yields an error string for the UI\n * rather than throwing, matching how the web surface degrades.\n */\nimport { emptyBoardState, parseBoardState, type PersistedCanvasState } from './canvasCore';\n\n/** Fields the index returns for a channel row (mirror of CHANNEL_FIELDS). */\nconst CHANNEL_FIELDS = 'id surface channelId title metadata updatedAt';\n\n/**\n * The web surface's `/c/new` scratch board. It is not a real board (no host\n * session backs it), and `pullBoards` skips it — so must this, or an empty\n * scratch row would shadow the user's actual canvases.\n */\nconst SCRATCH_BOARD_ID = 'global';\n\nexport interface CanvasBoardSummary {\n channelId: string;\n title?: string;\n savedAt: number;\n itemCount: number;\n board: PersistedCanvasState;\n}\n\n/**\n * Where the index lives for this environment.\n *\n * Mirrors the surface-kit fallback: the index is a sibling host of the backend,\n * so drop the backend's first label and prefix `surface-index-backend.`\n * (`ideback.yantra-app-v1.cdebase.dev` -> `surface-index-backend.yantra-app-v1.cdebase.dev`).\n * An explicit URL always wins, so a non-standard environment sets one env var\n * instead of needing a code change.\n */\nexport function surfaceIndexUrl(graphqlUrl: string, explicit?: string): string {\n const override = (explicit || '').trim();\n if (override) return override;\n\n const base = (graphqlUrl || '').trim();\n if (!base) return '';\n try {\n const { protocol, host } = parseOrigin(base);\n const labels = host.split('.');\n if (labels.length < 3) return '';\n return `${protocol}//surface-index-backend.${labels.slice(1).join('.')}/graphql`;\n } catch {\n return '';\n }\n}\n\n/**\n * Index GraphQL URL from the same SURFACE_URL_TEMPLATE the native remotes use\n * (`https://{slug}-surface.<cluster>/` → `https://surface-index-backend.<cluster>/graphql`).\n * Local GRAPHQL_URL cannot derive a host; this is the develop-lane fallback.\n */\nexport function surfaceIndexUrlFromTemplate(template: string): string {\n const trimmed = (template || '').trim();\n const match = trimmed.match(/^(https?):\\/\\/\\{slug\\}-surface\\.([^/]+)/i);\n if (!match) return '';\n const host = match[2].replace(/\\/+$/, '');\n if (!host) return '';\n return `${match[1].toLowerCase()}://surface-index-backend.${host}/graphql`;\n}\n\nfunction parseOrigin(url: string): { protocol: string; host: string } {\n // RN has URL via react-native-url-polyfill (imported in the app entry), but\n // this module is also loaded by jest and the surface build, so parse by hand.\n const match = url.match(/^(https?:)\\/\\/([^/]+)/i);\n if (!match) throw new Error(`unparseable url: ${url}`);\n return { protocol: match[1].toLowerCase(), host: match[2].toLowerCase() };\n}\n\nexport interface FetchBoardsOptions {\n url: string;\n token?: string | null;\n signal?: AbortSignal;\n}\n\nexport interface FetchBoardsResult {\n boards: CanvasBoardSummary[];\n /** Plain sentence for the UI; null when the read succeeded. */\n error: string | null;\n}\n\n/**\n * The caller's canvases, newest first.\n *\n * Empty boards are dropped, matching web `pullBoards` — the grid only shows\n * canvases with media. Only the scratch `global` row is skipped for the same\n * reason as web (it would merge every `/c/new` scratch into one shared row).\n */\nexport async function fetchMyCanvasBoards(opts: FetchBoardsOptions): Promise<FetchBoardsResult> {\n if (!opts.url) {\n return { boards: [], error: 'No canvas index configured for this build.' };\n }\n\n let payload: SurfaceIndexResponse;\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `query MySurfaceChannels($surface: SurfaceType) {\n mySurfaceChannels(surface: $surface) { ${CHANNEL_FIELDS} }\n }`,\n variables: { surface: 'canvas' },\n }),\n signal: opts.signal,\n });\n\n if (response.status === 401 || response.status === 403) {\n return { boards: [], error: 'Your session expired. Sign in again to see your canvases.' };\n }\n if (!response.ok) {\n return { boards: [], error: `The canvas index is unreachable (HTTP ${response.status}).` };\n }\n payload = (await response.json()) as SurfaceIndexResponse;\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n return { boards: [], error: null };\n }\n return { boards: [], error: 'Could not reach the canvas index. Check your connection.' };\n }\n\n if (payload?.errors?.length) {\n // GraphQL errors arrive with HTTP 200. The index reports an unauthenticated\n // caller as \"Missing tenantId in userContext\", which is a developer\n // sentence - say what the user can actually do about it.\n const raw = payload.errors[0]?.message || '';\n const isAuth = /tenantid|unauthenticated|unauthorized|not authorised|forbidden/i.test(raw);\n return {\n boards: [],\n error: isAuth\n ? 'Your session expired. Sign in again to see your canvases.'\n : raw || 'The canvas index rejected the request.',\n };\n }\n\n const rows = payload?.data?.mySurfaceChannels ?? [];\n const boards: CanvasBoardSummary[] = [];\n for (const row of rows) {\n const channelId = typeof row?.channelId === 'string' ? row.channelId : '';\n if (!channelId || channelId === SCRATCH_BOARD_ID) continue;\n const title = typeof row?.title === 'string' ? row.title : undefined;\n const board = parseBoardState(coerceMetadata(row?.metadata)) ?? emptyBoardState(title);\n if (board.items.length === 0) continue;\n boards.push({\n channelId,\n title: board.title || title,\n savedAt: board.savedAt,\n itemCount: board.items.length,\n board,\n });\n }\n\n boards.sort((a, b) => b.savedAt - a.savedAt);\n return { boards, error: null };\n}\n\nexport interface PushBoardOptions {\n url: string;\n token?: string | null;\n channelId: string;\n state: PersistedCanvasState;\n}\n\n/**\n * Write one board to the index. Twin of canvas-surface `pushBoard`:\n * `registerSurfaceChannel` is the idempotent upsert (update throws when the\n * row does not exist). Metadata is replaced wholesale — send the complete state.\n *\n * Returns false when the id is unsyncable (`global` / empty) or the write fails.\n * Never throws.\n */\nexport async function pushCanvasBoard(opts: PushBoardOptions): Promise<boolean> {\n const channelId = opts.channelId?.trim();\n if (!channelId || channelId === SCRATCH_BOARD_ID) return false;\n if (!opts.url) return false;\n\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `mutation RegisterSurfaceChannel($input: RegisterSurfaceChannelInput!) {\n registerSurfaceChannel(input: $input) { ${CHANNEL_FIELDS} }\n }`,\n variables: {\n input: {\n surface: 'canvas',\n channelId,\n sessionId: channelId,\n ...(opts.state.title ? { title: opts.state.title } : {}),\n metadata: opts.state,\n },\n },\n }),\n });\n if (!response.ok) return false;\n const payload = (await response.json()) as {\n data?: { registerSurfaceChannel?: { id?: string } | null };\n errors?: Array<{ message?: string }>;\n };\n if (payload?.errors?.length) {\n if (__DEV__) {\n // eslint-disable-next-line no-console\n console.warn('[pushCanvasBoard]', payload.errors[0]?.message);\n }\n return false;\n }\n return Boolean(payload?.data?.registerSurfaceChannel?.id);\n } catch (err) {\n if (__DEV__) {\n // eslint-disable-next-line no-console\n console.warn('[pushCanvasBoard] failed:', err);\n }\n return false;\n }\n}\n\n/**\n * `metadata` is a JSON scalar, which arrives decoded — but a backend that types\n * it as a String hands back the encoded text instead, and a board that silently\n * fails to parse looks exactly like an empty canvas. Accept both.\n */\nfunction coerceMetadata(value: unknown): unknown {\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n}\n\ninterface SurfaceIndexResponse {\n data?: { mySurfaceChannels?: Array<Record<string, unknown>> | null } | null;\n errors?: Array<{ message?: string }>;\n}\n\n/** Mint a board id the same way the web does: the board IS its channel. */\nexport function newBoardChannelId(): string {\n const rand = () => Math.random().toString(36).slice(2, 10);\n try {\n const bytes = new Uint8Array(8);\n (globalThis.crypto as Crypto | undefined)?.getRandomValues?.(bytes);\n if (bytes.some((b) => b !== 0)) {\n return `canvas-${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}`;\n }\n } catch {\n // fall through to Math.random\n }\n return `canvas-${rand()}${rand()}`;\n}\n\n/**\n * Write one board to the index - the native twin of the web's `pushBoard`.\n * `registerSurfaceChannel` because it is the idempotent upsert (update throws\n * when the row does not exist). `metadata` is REPLACED wholesale by the\n * backend, so the complete state goes every time; never send a patch.\n */\nexport async function saveCanvasBoard(opts: {\n url: string;\n token?: string | null;\n channelId: string;\n board: PersistedCanvasState;\n}): Promise<{ ok: boolean; error: string | null }> {\n if (!opts.url) return { ok: false, error: 'No canvas index configured for this build.' };\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `mutation RegisterSurfaceChannel($input: RegisterSurfaceChannelInput!) {\n registerSurfaceChannel(input: $input) { id channelId }\n }`,\n variables: {\n input: {\n surface: 'canvas',\n channelId: opts.channelId,\n title: opts.board.title || 'Untitled canvas',\n metadata: opts.board,\n },\n },\n }),\n });\n if (!response.ok) return { ok: false, error: `Save failed (HTTP ${response.status}).` };\n const payload = (await response.json()) as { errors?: Array<{ message?: string }> };\n if (payload?.errors?.length) return { ok: false, error: payload.errors[0]?.message || 'Save rejected.' };\n return { ok: true, error: null };\n } catch {\n return { ok: false, error: 'Could not reach the canvas index to save.' };\n }\n}\n"],"names":[],"mappings":"AA6CgB,SAAA,eAAA,CAAgB,YAAoB,QAA2B,EAAA;AAC7E,EAAM,MAAA,QAAA,GAAA,CAAY,QAAY,IAAA,EAAA,EAAI,IAAK,EAAA;AACvC,EAAA,IAAI,UAAiB,OAAA,QAAA;AACrB,EAAM,MAAA,IAAA,GAAA,CAAQ,UAAc,IAAA,EAAA,EAAI,IAAK,EAAA;AACrC,EAAI,IAAA,CAAC,MAAa,OAAA,EAAA;AAClB,EAAI,IAAA;AACF,IAAM,MAAA;AAAA,MACJ,QAAA;AAAA,MACA;AAAA,KACF,GAAI,YAAY,IAAI,CAAA;AACpB,IAAM,MAAA,MAAA,GAAS,IAAK,CAAA,KAAA,CAAM,GAAG,CAAA;AAC7B,IAAI,IAAA,MAAA,CAAO,MAAS,GAAA,CAAA,EAAU,OAAA,EAAA;AAC9B,IAAO,OAAA,CAAA,EAAG,QAAQ,CAA2B,wBAAA,EAAA,MAAA,CAAO,MAAM,CAAC,CAAA,CAAE,IAAK,CAAA,GAAG,CAAC,CAAA,QAAA,CAAA;AAAA,GAChE,CAAA,OAAA,CAAA,EAAA;AACN,IAAO,OAAA,EAAA;AAAA;AAEX;AAOO,SAAS,4BAA4B,QAA0B,EAAA;AACpE,EAAM,MAAA,OAAA,GAAA,CAAW,QAAY,IAAA,EAAA,EAAI,IAAK,EAAA;AACtC,EAAM,MAAA,KAAA,GAAQ,OAAQ,CAAA,KAAA,CAAM,0CAA0C,CAAA;AACtE,EAAI,IAAA,CAAC,OAAc,OAAA,EAAA;AACnB,EAAA,MAAM,OAAO,KAAM,CAAA,CAAC,CAAE,CAAA,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACxC,EAAI,IAAA,CAAC,MAAa,OAAA,EAAA;AAClB,EAAA,OAAO,GAAG,KAAM,CAAA,CAAC,EAAE,WAAY,EAAC,4BAA4B,IAAI,CAAA,QAAA,CAAA;AAClE;AACA,SAAS,YAAY,GAGnB,EAAA;AAGA,EAAM,MAAA,KAAA,GAAQ,GAAI,CAAA,KAAA,CAAM,wBAAwB,CAAA;AAChD,EAAA,IAAI,CAAC,KAAO,EAAA,MAAM,IAAI,KAAM,CAAA,CAAA,iBAAA,EAAoB,GAAG,CAAE,CAAA,CAAA;AACrD,EAAO,OAAA;AAAA,IACL,QAAU,EAAA,KAAA,CAAM,CAAC,CAAA,CAAE,WAAY,EAAA;AAAA,IAC/B,IAAM,EAAA,KAAA,CAAM,CAAC,CAAA,CAAE,WAAY;AAAA,GAC7B;AACF"}
1
+ {"version":3,"file":"surfaceIndex.js","sources":["../../../src/features/canvas/surfaceIndex.ts"],"sourcesContent":["/**\n * Native reader for the canvas boards in the SurfaceChannel index.\n *\n * The index (`servers/surface-index-subgraph-server`) is the ONLY store for a\n * canvas: the whole `PersistedCanvasState` rides in the row's `metadata`, and\n * `mySurfaceChannels` enumerates the caller's boards. Rows are per-USER, so the\n * phone — signed in as the same user — sees exactly what the web surface sees.\n *\n * This is the native twin of `@yantra/canvas-surface`'s `state/boards.ts`\n * (`pullBoards`). It is copied rather than imported for the same reason\n * `canvasCore.ts` is: that package builds for the DOM (vite, `import.meta.env`,\n * `window.location`), and importing it would drag the web toolchain into Metro.\n * The contract that matters is the wire shape, which both sides pin.\n *\n * Reads are best-effort: an unreachable index yields an error string for the UI\n * rather than throwing, matching how the web surface degrades.\n */\nimport { emptyBoardState, parseBoardState, type PersistedCanvasState } from './canvasCore';\n\n/** Fields the index returns for a channel row (mirror of CHANNEL_FIELDS). */\nconst CHANNEL_FIELDS = 'id surface channelId title metadata updatedAt';\n\n/**\n * The web surface's `/c/new` scratch board. It is not a real board (no host\n * session backs it), and `pullBoards` skips it — so must this, or an empty\n * scratch row would shadow the user's actual canvases.\n */\nconst SCRATCH_BOARD_ID = 'global';\n\nexport interface CanvasBoardSummary {\n channelId: string;\n title?: string;\n savedAt: number;\n itemCount: number;\n board: PersistedCanvasState;\n}\n\n/**\n * Where the index lives for this environment.\n *\n * Mirrors the surface-kit fallback: the index is a sibling host of the backend,\n * so drop the backend's first label and prefix `surface-index-backend.`\n * (`ideback.yantra-app-v1.cdebase.dev` -> `surface-index-backend.yantra-app-v1.cdebase.dev`).\n * An explicit URL always wins, so a non-standard environment sets one env var\n * instead of needing a code change.\n */\nexport function surfaceIndexUrl(graphqlUrl: string, explicit?: string): string {\n const override = (explicit || '').trim();\n if (override) return override;\n\n const base = (graphqlUrl || '').trim();\n if (!base) return '';\n try {\n const { protocol, host } = parseOrigin(base);\n const labels = host.split('.');\n if (labels.length < 3) return '';\n return `${protocol}//surface-index-backend.${labels.slice(1).join('.')}/graphql`;\n } catch {\n return '';\n }\n}\n\n/**\n * Index GraphQL URL from the same SURFACE_URL_TEMPLATE the native remotes use\n * (`https://{slug}-surface.<cluster>/` → `https://surface-index-backend.<cluster>/graphql`).\n * Local GRAPHQL_URL cannot derive a host; this is the develop-lane fallback.\n */\nexport function surfaceIndexUrlFromTemplate(template: string): string {\n const trimmed = (template || '').trim();\n const match = trimmed.match(/^(https?):\\/\\/\\{slug\\}-surface\\.([^/]+)/i);\n if (!match) return '';\n const host = match[2].replace(/\\/+$/, '');\n if (!host) return '';\n return `${match[1].toLowerCase()}://surface-index-backend.${host}/graphql`;\n}\n\nfunction parseOrigin(url: string): { protocol: string; host: string } {\n // RN has URL via react-native-url-polyfill (imported in the app entry), but\n // this module is also loaded by jest and the surface build, so parse by hand.\n const match = url.match(/^(https?:)\\/\\/([^/]+)/i);\n if (!match) throw new Error(`unparseable url: ${url}`);\n return { protocol: match[1].toLowerCase(), host: match[2].toLowerCase() };\n}\n\nexport interface FetchBoardsOptions {\n url: string;\n token?: string | null;\n signal?: AbortSignal;\n}\n\nexport interface FetchBoardsResult {\n boards: CanvasBoardSummary[];\n /** Plain sentence for the UI; null when the read succeeded. */\n error: string | null;\n}\n\n/** In-memory board list so revisiting Canvas Native does not re-flash the spinner. */\nlet boardsCache: { url: string; boards: CanvasBoardSummary[] } | null = null;\n\nexport function getCachedCanvasBoards(url: string): CanvasBoardSummary[] | null {\n if (!url || !boardsCache || boardsCache.url !== url) return null;\n return boardsCache.boards;\n}\n\nexport function setCachedCanvasBoards(url: string, boards: CanvasBoardSummary[]): void {\n if (!url) return;\n boardsCache = { url, boards };\n}\n\n/**\n * The caller's canvases, newest first.\n *\n * Empty boards are dropped, matching web `pullBoards` — the grid only shows\n * canvases with media. Only the scratch `global` row is skipped for the same\n * reason as web (it would merge every `/c/new` scratch into one shared row).\n */\nexport async function fetchMyCanvasBoards(opts: FetchBoardsOptions): Promise<FetchBoardsResult> {\n if (!opts.url) {\n return { boards: [], error: 'No canvas index configured for this build.' };\n }\n\n let payload: SurfaceIndexResponse;\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `query MySurfaceChannels($surface: SurfaceType) {\n mySurfaceChannels(surface: $surface) { ${CHANNEL_FIELDS} }\n }`,\n variables: { surface: 'canvas' },\n }),\n signal: opts.signal,\n });\n\n if (response.status === 401 || response.status === 403) {\n return { boards: [], error: 'Your session expired. Sign in again to see your canvases.' };\n }\n if (!response.ok) {\n return { boards: [], error: `The canvas index is unreachable (HTTP ${response.status}).` };\n }\n payload = (await response.json()) as SurfaceIndexResponse;\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') {\n return { boards: [], error: null };\n }\n return { boards: [], error: 'Could not reach the canvas index. Check your connection.' };\n }\n\n if (payload?.errors?.length) {\n // GraphQL errors arrive with HTTP 200. The index reports an unauthenticated\n // caller as \"Missing tenantId in userContext\", which is a developer\n // sentence - say what the user can actually do about it.\n const raw = payload.errors[0]?.message || '';\n const isAuth = /tenantid|unauthenticated|unauthorized|not authorised|forbidden/i.test(raw);\n return {\n boards: [],\n error: isAuth\n ? 'Your session expired. Sign in again to see your canvases.'\n : raw || 'The canvas index rejected the request.',\n };\n }\n\n const rows = payload?.data?.mySurfaceChannels ?? [];\n const boards: CanvasBoardSummary[] = [];\n for (const row of rows) {\n const channelId = typeof row?.channelId === 'string' ? row.channelId : '';\n if (!channelId || channelId === SCRATCH_BOARD_ID) continue;\n const title = typeof row?.title === 'string' ? row.title : undefined;\n const board = parseBoardState(coerceMetadata(row?.metadata)) ?? emptyBoardState(title);\n if (board.items.length === 0) continue;\n boards.push({\n channelId,\n title: board.title || title,\n savedAt: board.savedAt,\n itemCount: board.items.length,\n board,\n });\n }\n\n boards.sort((a, b) => b.savedAt - a.savedAt);\n setCachedCanvasBoards(opts.url, boards);\n return { boards, error: null };\n}\n\nexport interface PushBoardOptions {\n url: string;\n token?: string | null;\n channelId: string;\n state: PersistedCanvasState;\n}\n\n/**\n * Write one board to the index. Twin of canvas-surface `pushBoard`:\n * `registerSurfaceChannel` is the idempotent upsert (update throws when the\n * row does not exist). Metadata is replaced wholesale — send the complete state.\n *\n * Returns false when the id is unsyncable (`global` / empty) or the write fails.\n * Never throws.\n */\nexport async function pushCanvasBoard(opts: PushBoardOptions): Promise<boolean> {\n const channelId = opts.channelId?.trim();\n if (!channelId || channelId === SCRATCH_BOARD_ID) return false;\n if (!opts.url) return false;\n\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `mutation RegisterSurfaceChannel($input: RegisterSurfaceChannelInput!) {\n registerSurfaceChannel(input: $input) { ${CHANNEL_FIELDS} }\n }`,\n variables: {\n input: {\n surface: 'canvas',\n channelId,\n sessionId: channelId,\n ...(opts.state.title ? { title: opts.state.title } : {}),\n metadata: opts.state,\n },\n },\n }),\n });\n if (!response.ok) return false;\n const payload = (await response.json()) as {\n data?: { registerSurfaceChannel?: { id?: string } | null };\n errors?: Array<{ message?: string }>;\n };\n if (payload?.errors?.length) {\n if (__DEV__) {\n // eslint-disable-next-line no-console\n console.warn('[pushCanvasBoard]', payload.errors[0]?.message);\n }\n return false;\n }\n return Boolean(payload?.data?.registerSurfaceChannel?.id);\n } catch (err) {\n if (__DEV__) {\n // eslint-disable-next-line no-console\n console.warn('[pushCanvasBoard] failed:', err);\n }\n return false;\n }\n}\n\n/**\n * `metadata` is a JSON scalar, which arrives decoded — but a backend that types\n * it as a String hands back the encoded text instead, and a board that silently\n * fails to parse looks exactly like an empty canvas. Accept both.\n */\nfunction coerceMetadata(value: unknown): unknown {\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value);\n } catch {\n return null;\n }\n}\n\ninterface SurfaceIndexResponse {\n data?: { mySurfaceChannels?: Array<Record<string, unknown>> | null } | null;\n errors?: Array<{ message?: string }>;\n}\n\n/** Mint a board id the same way the web does: the board IS its channel. */\nexport function newBoardChannelId(): string {\n const rand = () => Math.random().toString(36).slice(2, 10);\n try {\n const bytes = new Uint8Array(8);\n (globalThis.crypto as Crypto | undefined)?.getRandomValues?.(bytes);\n if (bytes.some((b) => b !== 0)) {\n return `canvas-${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}`;\n }\n } catch {\n // fall through to Math.random\n }\n return `canvas-${rand()}${rand()}`;\n}\n\n/**\n * Write one board to the index - the native twin of the web's `pushBoard`.\n * `registerSurfaceChannel` because it is the idempotent upsert (update throws\n * when the row does not exist). `metadata` is REPLACED wholesale by the\n * backend, so the complete state goes every time; never send a patch.\n */\nexport async function saveCanvasBoard(opts: {\n url: string;\n token?: string | null;\n channelId: string;\n board: PersistedCanvasState;\n}): Promise<{ ok: boolean; error: string | null }> {\n if (!opts.url) return { ok: false, error: 'No canvas index configured for this build.' };\n try {\n const response = await fetch(opts.url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}),\n },\n body: JSON.stringify({\n query: `mutation RegisterSurfaceChannel($input: RegisterSurfaceChannelInput!) {\n registerSurfaceChannel(input: $input) { id channelId }\n }`,\n variables: {\n input: {\n surface: 'canvas',\n channelId: opts.channelId,\n title: opts.board.title || 'Untitled canvas',\n metadata: opts.board,\n },\n },\n }),\n });\n if (!response.ok) return { ok: false, error: `Save failed (HTTP ${response.status}).` };\n const payload = (await response.json()) as { errors?: Array<{ message?: string }> };\n if (payload?.errors?.length) return { ok: false, error: payload.errors[0]?.message || 'Save rejected.' };\n return { ok: true, error: null };\n } catch {\n return { ok: false, error: 'Could not reach the canvas index to save.' };\n }\n}\n"],"names":[],"mappings":"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"}