@agent-native/core 0.119.0 → 0.119.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +23 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +4 -0
- package/corpus/core/src/agent/production-agent.ts +1 -0
- package/corpus/core/src/client/AssistantChat.tsx +11 -50
- package/corpus/core/src/client/ErrorBoundary.tsx +25 -19
- package/corpus/core/src/client/agent-page/AgentTabFrame.tsx +1 -1
- package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +23 -0
- package/corpus/core/src/client/components/ui/message-scroller.tsx +1 -9
- package/corpus/core/src/client/resources/ResourceTree.tsx +3 -3
- package/corpus/core/src/client/resources/ResourcesPanel.tsx +64 -60
- package/corpus/core/src/client/settings/AccountSettingsCard.tsx +228 -0
- package/corpus/core/src/client/settings/SecretsSection.tsx +7 -5
- package/corpus/core/src/client/settings/SettingsPanel.tsx +67 -244
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +88 -21
- package/corpus/core/src/client/settings/agent-settings-search.ts +0 -2
- package/corpus/core/src/client/settings/index.ts +7 -0
- package/corpus/core/src/client/vite-dev-recovery-script.ts +18 -1
- package/corpus/core/src/db/client.ts +65 -15
- package/corpus/core/src/db/index.ts +2 -0
- package/corpus/core/src/ingestion/figma-node-to-html.ts +20 -99
- package/corpus/core/src/ingestion/figma-paint-math.ts +371 -0
- package/corpus/core/src/ingestion/index.ts +20 -0
- package/corpus/core/src/localization/default-messages.ts +1 -0
- package/corpus/core/src/server/email.ts +33 -1
- package/corpus/core/src/styles/agent-native.css +23 -0
- package/corpus/core/src/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/core/src/templates/default/app/routes/settings.tsx +2 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/core/src/vite/client.ts +76 -8
- package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +3 -1
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +9 -13
- package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +2 -2
- package/corpus/templates/analytics/app/global.css +0 -5
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -13
- package/corpus/templates/analytics/app/lib/sql-query.ts +15 -2
- package/corpus/templates/analytics/app/pages/Settings.tsx +2 -24
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +6 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -0
- package/corpus/templates/analytics/app/pages/settings/settings-search.ts +1 -1
- package/corpus/templates/analytics/changelog/2026-07-23-ask-analytics-is-better-centered-with-a-quieter-chat-rail-an.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-23-daily-dashboard-emails-load-read-heavy-panels-reliably.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +2 -2
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +39 -5
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +186 -62
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +3 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +18 -2
- package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +3 -0
- package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -4
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/assets/app/global.css +0 -5
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +0 -22
- package/corpus/templates/assets/app/routes/library.tsx +17 -9
- package/corpus/templates/assets/app/routes/settings.tsx +2 -0
- package/corpus/templates/assets/changelog/2026-07-23-create-and-chat-surfaces-are-better-centered-with-quieter-ch.md +6 -0
- package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/brain/app/global.css +0 -9
- package/corpus/templates/brain/app/hooks/use-navigation-state.ts +0 -12
- package/corpus/templates/brain/app/i18n-data.ts +1 -0
- package/corpus/templates/brain/app/lib/settings-navigation.ts +1 -1
- package/corpus/templates/brain/app/routes/settings.tsx +21 -22
- package/corpus/templates/brain/changelog/2026-07-23-ask-brain-is-better-centered-with-quieter-chat-history-and-a.md +6 -0
- package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/calendar/app/pages/Settings.tsx +2 -0
- package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/chat/app/hooks/use-navigation-state.ts +0 -15
- package/corpus/templates/chat/app/routes/settings.tsx +2 -0
- package/corpus/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
- package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +2 -0
- package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/content/app/routes/_app.settings.tsx +2 -0
- package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/design/AGENTS.md +18 -5
- package/corpus/templates/design/FIGMA_INTEROPERABILITY.md +25 -12
- package/corpus/templates/design/actions/hydrate-figma-paste-images.ts +115 -0
- package/corpus/templates/design/actions/import-figma-clipboard.ts +83 -18
- package/corpus/templates/design/actions/import-figma-frame.ts +34 -21
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +28 -1
- package/corpus/templates/design/app/components/design/DesignImportPanel.tsx +106 -11
- package/corpus/templates/design/app/components/design/FigmaHydrationDialog.tsx +273 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +7 -7
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +57 -9
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +10 -0
- package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +8 -1
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +3 -0
- package/corpus/templates/design/app/i18n-data.ts +150 -2
- package/corpus/templates/design/app/lib/design-file-upload.ts +85 -0
- package/corpus/templates/design/app/lib/design-import.ts +19 -1
- package/corpus/templates/design/app/lib/figma-clipboard.ts +110 -10
- package/corpus/templates/design/app/routes/settings.tsx +2 -0
- package/corpus/templates/design/changelog/2026-07-20-paste-figma-frames-directly-onto-the-canvas-no-token-needed.md +6 -0
- package/corpus/templates/design/changelog/2026-07-21-fig-upload-frame-url-and-rate-limit-errors.md +5 -0
- package/corpus/templates/design/changelog/2026-07-22-connecting-a-figma-token-now-actually-fills-in-a-pasted-desi.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-figma-line-arrow-vectors-and-stroked-icons-now-render-instea.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-fill-a-no-token-figma-paste-s-images-by-dropping-the-origina.md +6 -0
- package/corpus/templates/design/changelog/2026-07-22-imported-and-pasted-figma-frames-now-render-faithfully-fixed.md +6 -0
- package/corpus/templates/design/server/handlers/import-design-file.ts +50 -0
- package/corpus/templates/design/server/lib/fig-file-decoder.ts +90 -49
- package/corpus/templates/design/server/lib/fig-file-import.ts +19 -5
- package/corpus/templates/design/server/lib/fig-file-to-html.ts +583 -78
- package/corpus/templates/design/server/lib/figma-clipboard-local-decode.ts +238 -0
- package/corpus/templates/design/server/lib/figma-image-hydration.ts +389 -0
- package/corpus/templates/design/server/lib/figma-node-import.ts +136 -36
- package/corpus/templates/design/server/register-secrets.ts +14 -2
- package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/dispatch/app/routes/settings.tsx +2 -0
- package/corpus/templates/dispatch/changelog/2026-07-23-quieter-progressive-disclosure.md +6 -0
- package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/forms/app/global.css +0 -5
- package/corpus/templates/forms/app/hooks/use-navigation-state.ts +0 -7
- package/corpus/templates/forms/app/root.tsx +1 -7
- package/corpus/templates/forms/app/routes/_app.settings.tsx +2 -0
- package/corpus/templates/forms/changelog/2026-07-23-ask-forms-is-better-centered-with-quieter-chat-history-and-a.md +6 -0
- package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/macros/app/routes/settings.tsx +2 -0
- package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +3 -1
- package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/plan/app/components/layout/Layout.tsx +2 -2
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +1 -13
- package/corpus/templates/plan/app/hooks/use-navigation-state.ts +0 -4
- package/corpus/templates/plan/app/routes/settings.tsx +2 -0
- package/corpus/templates/plan/changelog/2026-07-23-ask-plan-is-now-a-clean-full-page-chat-surface-with-better-c.md +6 -0
- package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/slides/app/routes/settings.tsx +2 -0
- package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/chat-history.css +2 -2
- package/corpus/toolkit/src/composer/PromptComposer.tsx +0 -31
- package/corpus/toolkit/src/composer/RealtimeVoiceMode.tsx +7 -2
- package/corpus/toolkit/src/styles.css +183 -36
- package/dist/a2a/client.d.ts +2 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +2 -0
- package/dist/a2a/client.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +1 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +11 -42
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/ErrorBoundary.d.ts.map +1 -1
- package/dist/client/ErrorBoundary.js +3 -2
- package/dist/client/ErrorBoundary.js.map +1 -1
- package/dist/client/agent-page/AgentTabFrame.js +1 -1
- package/dist/client/agent-page/AgentTabFrame.js.map +1 -1
- package/dist/client/agent-page/AgentTabsPage.d.ts.map +1 -1
- package/dist/client/agent-page/AgentTabsPage.js +13 -1
- package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
- package/dist/client/components/ui/message-scroller.d.ts.map +1 -1
- package/dist/client/components/ui/message-scroller.js +2 -2
- package/dist/client/components/ui/message-scroller.js.map +1 -1
- package/dist/client/resources/ResourceTree.js +2 -2
- package/dist/client/resources/ResourceTree.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +11 -11
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/client/settings/AccountSettingsCard.d.ts +9 -0
- package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -0
- package/dist/client/settings/AccountSettingsCard.js +78 -0
- package/dist/client/settings/AccountSettingsCard.js.map +1 -0
- package/dist/client/settings/SecretsSection.d.ts.map +1 -1
- package/dist/client/settings/SecretsSection.js +2 -2
- package/dist/client/settings/SecretsSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +25 -77
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts +5 -1
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
- package/dist/client/settings/SettingsTabsPage.js +35 -9
- package/dist/client/settings/SettingsTabsPage.js.map +1 -1
- package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
- package/dist/client/settings/agent-settings-search.js +0 -2
- package/dist/client/settings/agent-settings-search.js.map +1 -1
- package/dist/client/settings/index.d.ts +2 -1
- package/dist/client/settings/index.d.ts.map +1 -1
- package/dist/client/settings/index.js +2 -1
- package/dist/client/settings/index.js.map +1 -1
- package/dist/client/vite-dev-recovery-script.d.ts.map +1 -1
- package/dist/client/vite-dev-recovery-script.js +18 -1
- package/dist/client/vite-dev-recovery-script.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts +18 -8
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +34 -11
- package/dist/db/client.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/ingestion/figma-node-to-html.d.ts.map +1 -1
- package/dist/ingestion/figma-node-to-html.js +14 -88
- package/dist/ingestion/figma-node-to-html.js.map +1 -1
- package/dist/ingestion/figma-paint-math.d.ts +157 -0
- package/dist/ingestion/figma-paint-math.d.ts.map +1 -0
- package/dist/ingestion/figma-paint-math.js +274 -0
- package/dist/ingestion/figma-paint-math.js.map +1 -0
- package/dist/ingestion/index.d.ts +1 -0
- package/dist/ingestion/index.d.ts.map +1 -1
- package/dist/ingestion/index.js +1 -0
- package/dist/ingestion/index.js.map +1 -1
- package/dist/localization/default-messages.d.ts +1 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +1 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
- package/dist/provider-api/actions/provider-api.d.ts +11 -11
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/email.d.ts +1 -0
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +29 -1
- package/dist/server/email.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +23 -0
- package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
- package/dist/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +1 -1
- package/dist/templates/chat/app/hooks/use-navigation-state.ts +0 -15
- package/dist/templates/chat/app/routes/settings.tsx +2 -0
- package/dist/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
- package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
- package/dist/templates/default/app/routes/settings.tsx +2 -0
- package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +68 -6
- package/dist/vite/client.js.map +1 -1
- package/package.json +2 -2
- package/src/a2a/client.ts +4 -0
- package/src/agent/production-agent.ts +1 -0
- package/src/client/AssistantChat.tsx +11 -50
- package/src/client/ErrorBoundary.tsx +25 -19
- package/src/client/agent-page/AgentTabFrame.tsx +1 -1
- package/src/client/agent-page/AgentTabsPage.tsx +23 -0
- package/src/client/components/ui/message-scroller.tsx +1 -9
- package/src/client/resources/ResourceTree.tsx +3 -3
- package/src/client/resources/ResourcesPanel.tsx +64 -60
- package/src/client/settings/AccountSettingsCard.tsx +228 -0
- package/src/client/settings/SecretsSection.tsx +7 -5
- package/src/client/settings/SettingsPanel.tsx +67 -244
- package/src/client/settings/SettingsTabsPage.tsx +88 -21
- package/src/client/settings/agent-settings-search.ts +0 -2
- package/src/client/settings/index.ts +7 -0
- package/src/client/vite-dev-recovery-script.ts +18 -1
- package/src/db/client.ts +65 -15
- package/src/db/index.ts +2 -0
- package/src/ingestion/figma-node-to-html.ts +20 -99
- package/src/ingestion/figma-paint-math.ts +371 -0
- package/src/ingestion/index.ts +20 -0
- package/src/localization/default-messages.ts +1 -0
- package/src/server/email.ts +33 -1
- package/src/styles/agent-native.css +23 -0
- package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
- package/src/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/src/templates/chat/app/components/layout/Sidebar.tsx +1 -1
- package/src/templates/chat/app/hooks/use-navigation-state.ts +0 -15
- package/src/templates/chat/app/routes/settings.tsx +2 -0
- package/src/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
- package/src/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
- package/src/templates/default/app/routes/settings.tsx +2 -0
- package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
- package/src/vite/client.ts +76 -8
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "@/lib/design-file-upload";
|
|
28
28
|
import {
|
|
29
29
|
importResultNotification,
|
|
30
|
+
isFigmaRateLimitImportError,
|
|
30
31
|
looksLikeStandaloneHtml,
|
|
31
32
|
VISUAL_EDIT_CONNECT_COMMAND,
|
|
32
33
|
VISUAL_EDIT_INSTALL_COMMAND,
|
|
@@ -42,6 +43,7 @@ import type { DesignExtensionSlotContext } from "./DesignExtensionsPanel";
|
|
|
42
43
|
|
|
43
44
|
interface DesignImportPanelProps {
|
|
44
45
|
context: Pick<DesignExtensionSlotContext, "designId" | "viewMode">;
|
|
46
|
+
onImport?: (result: ImportResult) => void;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
type ImportMode =
|
|
@@ -51,7 +53,11 @@ type ImportMode =
|
|
|
51
53
|
| "html"
|
|
52
54
|
| "local-app";
|
|
53
55
|
|
|
54
|
-
export function DesignImportPanel(
|
|
56
|
+
export function DesignImportPanel(p: DesignImportPanelProps) {
|
|
57
|
+
const context = p.context;
|
|
58
|
+
const onImport = p.onImport;
|
|
59
|
+
const onImportRef = useRef(onImport);
|
|
60
|
+
onImportRef.current = onImport;
|
|
55
61
|
const t = useT();
|
|
56
62
|
const { formatNumber } = useFormatters();
|
|
57
63
|
const navigate = useNavigate();
|
|
@@ -77,6 +83,8 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
|
|
|
77
83
|
const [htmlText, setHtmlText] = useState("");
|
|
78
84
|
const [activeMode, setActiveMode] = useState<ImportMode | null>(null);
|
|
79
85
|
const [lastResult, setLastResult] = useState<ImportResult | null>(null);
|
|
86
|
+
const [figmaRateLimitError, setFigmaRateLimitError] =
|
|
87
|
+
useState<ImportResult | null>(null);
|
|
80
88
|
const [figUploadName, setFigUploadName] = useState<string | null>(null);
|
|
81
89
|
const [figUploadProgress, setFigUploadProgress] = useState<number | null>(
|
|
82
90
|
null,
|
|
@@ -91,6 +99,7 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
|
|
|
91
99
|
queryClient.invalidateQueries({ queryKey: ["action", "get-design"] }),
|
|
92
100
|
queryClient.invalidateQueries({ queryKey: ["action"] }),
|
|
93
101
|
]);
|
|
102
|
+
if (result) onImportRef.current?.(result);
|
|
94
103
|
const fidelityWarnings: string[] = [];
|
|
95
104
|
const imageFallbackCount = result?.fidelityReport?.imageFallbacks.length;
|
|
96
105
|
if (imageFallbackCount) {
|
|
@@ -190,6 +199,7 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
|
|
|
190
199
|
figmaConnectionChecked && !figmaConnected && !figmaConnectionError;
|
|
191
200
|
|
|
192
201
|
const handleFigmaUrlImport = useCallback(async () => {
|
|
202
|
+
setFigmaRateLimitError(null);
|
|
193
203
|
const normalizedUrl = figmaUrl.trim();
|
|
194
204
|
if (!normalizedUrl) {
|
|
195
205
|
toast.error(t("designEditor.import.errors.figmaUrlRequired"));
|
|
@@ -218,9 +228,41 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
|
|
|
218
228
|
asNewScreen: true,
|
|
219
229
|
})) as ImportResult;
|
|
220
230
|
await finishImport(result, t("designEditor.import.figmaUrlSuccess"));
|
|
231
|
+
setFigmaRateLimitError(null);
|
|
221
232
|
} catch (error) {
|
|
222
233
|
// A rejected credential should not linger in component state or the DOM.
|
|
223
234
|
setFigmaAccessToken("");
|
|
235
|
+
const rateLimitDetails = error as Error & {
|
|
236
|
+
rateLimitRetryAfter?: number;
|
|
237
|
+
rateLimitPlanTier?: string;
|
|
238
|
+
figmaPlanTier?: string;
|
|
239
|
+
rateLimitType?: string;
|
|
240
|
+
figmaRateLimitType?: string;
|
|
241
|
+
rateLimitUpgradeUrl?: string;
|
|
242
|
+
figmaUpgradeUrl?: string;
|
|
243
|
+
};
|
|
244
|
+
const rateLimitResult: ImportResult = {
|
|
245
|
+
error:
|
|
246
|
+
typeof rateLimitDetails.message === "string"
|
|
247
|
+
? rateLimitDetails.message
|
|
248
|
+
: t("common.genericError"),
|
|
249
|
+
rateLimitRetryAfter: rateLimitDetails.rateLimitRetryAfter,
|
|
250
|
+
rateLimitPlanTier:
|
|
251
|
+
rateLimitDetails.rateLimitPlanTier ?? rateLimitDetails.figmaPlanTier,
|
|
252
|
+
rateLimitType:
|
|
253
|
+
rateLimitDetails.rateLimitType ?? rateLimitDetails.figmaRateLimitType,
|
|
254
|
+
rateLimitUpgradeUrl:
|
|
255
|
+
rateLimitDetails.rateLimitUpgradeUrl ??
|
|
256
|
+
rateLimitDetails.figmaUpgradeUrl,
|
|
257
|
+
};
|
|
258
|
+
const isRateLimitError =
|
|
259
|
+
(error instanceof Error &&
|
|
260
|
+
/rate limit|429|quota/i.test(error.message)) ||
|
|
261
|
+
isFigmaRateLimitImportError(rateLimitResult);
|
|
262
|
+
if (isRateLimitError) {
|
|
263
|
+
setFigmaRateLimitError(rateLimitResult);
|
|
264
|
+
setActiveMode("fig-upload");
|
|
265
|
+
}
|
|
224
266
|
toast.error(t("designEditor.import.errors.figmaImportFailed"), {
|
|
225
267
|
description:
|
|
226
268
|
error instanceof Error ? error.message : t("common.genericError"),
|
|
@@ -282,6 +324,7 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
|
|
|
282
324
|
onProgress: ({ percent }) => setFigUploadProgress(percent),
|
|
283
325
|
});
|
|
284
326
|
await finishImport(result, t("designEditor.import.uploadSuccess"));
|
|
327
|
+
setFigmaRateLimitError(null);
|
|
285
328
|
} catch (error) {
|
|
286
329
|
toast.error(t("designEditor.import.errors.uploadFailed"), {
|
|
287
330
|
description:
|
|
@@ -325,6 +368,64 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
|
|
|
325
368
|
|
|
326
369
|
<div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain px-3 pb-4 pt-3">
|
|
327
370
|
<div className="space-y-0.5">
|
|
371
|
+
{figmaRateLimitError ? (
|
|
372
|
+
<div className="space-y-2 rounded-md border border-destructive/30 bg-destructive/5 p-2.5 text-[11px] leading-snug">
|
|
373
|
+
<p className="font-medium text-destructive">
|
|
374
|
+
{t("designEditor.import.rateLimitTitle")}
|
|
375
|
+
</p>
|
|
376
|
+
<p className="text-muted-foreground">
|
|
377
|
+
{figmaRateLimitError.rateLimitType === "low"
|
|
378
|
+
? t("designEditor.import.rateLimitLowSeat")
|
|
379
|
+
: t("designEditor.import.rateLimitGeneric")}
|
|
380
|
+
</p>
|
|
381
|
+
{figmaRateLimitError.rateLimitRetryAfter ? (
|
|
382
|
+
<p className="text-muted-foreground">
|
|
383
|
+
{t("designEditor.import.rateLimitRetryIn", {
|
|
384
|
+
time:
|
|
385
|
+
figmaRateLimitError.rateLimitRetryAfter >= 60
|
|
386
|
+
? `${Math.ceil(figmaRateLimitError.rateLimitRetryAfter / 60)} min`
|
|
387
|
+
: `${figmaRateLimitError.rateLimitRetryAfter}s`,
|
|
388
|
+
})}
|
|
389
|
+
</p>
|
|
390
|
+
) : null}
|
|
391
|
+
<div className="flex flex-col gap-1.5">
|
|
392
|
+
<p className="text-[10px] font-medium text-muted-foreground">
|
|
393
|
+
{t("designEditor.import.rateLimitAlternatives")}
|
|
394
|
+
</p>
|
|
395
|
+
<Button
|
|
396
|
+
size="sm"
|
|
397
|
+
variant="outline"
|
|
398
|
+
className="h-7 w-full px-2 text-[11px]"
|
|
399
|
+
onClick={() => {
|
|
400
|
+
setFigmaRateLimitError(null);
|
|
401
|
+
setActiveMode("figma-paste");
|
|
402
|
+
}}
|
|
403
|
+
>
|
|
404
|
+
{t("designEditor.import.rateLimitUsePaste")}
|
|
405
|
+
</Button>
|
|
406
|
+
<Button
|
|
407
|
+
size="sm"
|
|
408
|
+
variant="outline"
|
|
409
|
+
className="h-7 w-full px-2 text-[11px]"
|
|
410
|
+
onClick={() => {
|
|
411
|
+
setActiveMode("fig-upload");
|
|
412
|
+
}}
|
|
413
|
+
>
|
|
414
|
+
{t("designEditor.import.rateLimitUseFig")}
|
|
415
|
+
</Button>
|
|
416
|
+
</div>
|
|
417
|
+
{figmaRateLimitError.rateLimitUpgradeUrl ? (
|
|
418
|
+
<a
|
|
419
|
+
href={figmaRateLimitError.rateLimitUpgradeUrl}
|
|
420
|
+
target="_blank"
|
|
421
|
+
rel="noreferrer"
|
|
422
|
+
className="block text-[10px] text-foreground underline-offset-2 hover:underline"
|
|
423
|
+
>
|
|
424
|
+
{t("designEditor.import.rateLimitUpgrade")}
|
|
425
|
+
</a>
|
|
426
|
+
) : null}
|
|
427
|
+
</div>
|
|
428
|
+
) : null}
|
|
328
429
|
<ImportSourceRow
|
|
329
430
|
id="figma-url-import"
|
|
330
431
|
icon={<IconBrandFigma className="size-3.5" />}
|
|
@@ -437,9 +538,7 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
|
|
|
437
538
|
id="figma-paste-import"
|
|
438
539
|
icon={<IconBrandFigma className="size-3.5" />}
|
|
439
540
|
title={t("designEditor.import.figmaPasteTitle")}
|
|
440
|
-
description={
|
|
441
|
-
"Copy a frame in Figma, then paste into the canvas." /* i18n-ignore */
|
|
442
|
-
}
|
|
541
|
+
description={t("designEditor.import.figmaPasteDescription")}
|
|
443
542
|
isOpen={activeMode === "figma-paste"}
|
|
444
543
|
onToggle={() =>
|
|
445
544
|
setActiveMode((mode) =>
|
|
@@ -448,12 +547,8 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
|
|
|
448
547
|
}
|
|
449
548
|
>
|
|
450
549
|
<div className="space-y-1.5 p-2 text-[11px] leading-snug text-muted-foreground">
|
|
451
|
-
<p>{t("designEditor.import.
|
|
452
|
-
<p>
|
|
453
|
-
{
|
|
454
|
-
"Click the canvas first, then paste with the same shortcut you use for copied Design content." /* i18n-ignore */
|
|
455
|
-
}
|
|
456
|
-
</p>
|
|
550
|
+
<p>{t("designEditor.import.figmaPasteBodyUnlimited")}</p>
|
|
551
|
+
<p>{t("designEditor.import.figmaPasteBodyImages")}</p>
|
|
457
552
|
</div>
|
|
458
553
|
</ImportSourceRow>
|
|
459
554
|
|
|
@@ -461,7 +556,7 @@ export function DesignImportPanel({ context }: DesignImportPanelProps) {
|
|
|
461
556
|
id="fig-file-import"
|
|
462
557
|
icon={<IconUpload className="size-3.5" />}
|
|
463
558
|
title={t("designEditor.import.figUploadTitle")}
|
|
464
|
-
description={t("designEditor.import.
|
|
559
|
+
description={t("designEditor.import.figUploadDescriptionShort")}
|
|
465
560
|
isOpen={activeMode === "fig-upload"}
|
|
466
561
|
onToggle={() =>
|
|
467
562
|
setActiveMode((mode) =>
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FigmaHydrationDialog — shown after a no-token local-kiwi clipboard import
|
|
3
|
+
* when IMAGE fills couldn't be resolved. Collects a Figma access token, saves
|
|
4
|
+
* it, then calls `hydrate-figma-paste-images` for each imported file to
|
|
5
|
+
* replace the `url("about:blank")` placeholders with real durable images.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { callAction } from "@agent-native/core/client/hooks";
|
|
9
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
10
|
+
import { useEffect, useRef, useState } from "react";
|
|
11
|
+
import { toast } from "sonner";
|
|
12
|
+
|
|
13
|
+
import { Button } from "@/components/ui/button";
|
|
14
|
+
import {
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogContent,
|
|
17
|
+
DialogDescription,
|
|
18
|
+
DialogFooter,
|
|
19
|
+
DialogHeader,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
} from "@/components/ui/dialog";
|
|
22
|
+
import { Input } from "@/components/ui/input";
|
|
23
|
+
import { Label } from "@/components/ui/label";
|
|
24
|
+
import {
|
|
25
|
+
hydrateImagesFromFig,
|
|
26
|
+
validateFigUploadFile,
|
|
27
|
+
} from "@/lib/design-file-upload";
|
|
28
|
+
import {
|
|
29
|
+
getFigmaConnectionStatus,
|
|
30
|
+
saveFigmaAccessToken,
|
|
31
|
+
} from "@/lib/figma-connection";
|
|
32
|
+
|
|
33
|
+
interface FigmaHydrationDialogProps {
|
|
34
|
+
open: boolean;
|
|
35
|
+
onOpenChange: (open: boolean) => void;
|
|
36
|
+
designId: string;
|
|
37
|
+
fileIds: string[];
|
|
38
|
+
imageCount: number;
|
|
39
|
+
onHydrated: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function FigmaHydrationDialog({
|
|
43
|
+
open,
|
|
44
|
+
onOpenChange,
|
|
45
|
+
designId,
|
|
46
|
+
fileIds,
|
|
47
|
+
imageCount,
|
|
48
|
+
onHydrated,
|
|
49
|
+
}: FigmaHydrationDialogProps) {
|
|
50
|
+
const t = useT();
|
|
51
|
+
const [token, setToken] = useState("");
|
|
52
|
+
const [busy, setBusy] = useState(false);
|
|
53
|
+
const [error, setError] = useState<string | null>(null);
|
|
54
|
+
const [docsUrl, setDocsUrl] = useState<string | null>(null);
|
|
55
|
+
const figInputRef = useRef<HTMLInputElement>(null);
|
|
56
|
+
|
|
57
|
+
const screensPlural = fileIds.length === 1 ? "" : "s";
|
|
58
|
+
const imagePlural = imageCount === 1 ? "" : "s";
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!open) return;
|
|
62
|
+
getFigmaConnectionStatus()
|
|
63
|
+
.then((status) => {
|
|
64
|
+
if (status.docsUrl) setDocsUrl(status.docsUrl);
|
|
65
|
+
})
|
|
66
|
+
.catch(() => {});
|
|
67
|
+
}, [open]);
|
|
68
|
+
|
|
69
|
+
async function handleFigSelected(e: React.ChangeEvent<HTMLInputElement>) {
|
|
70
|
+
const file = e.target.files?.[0];
|
|
71
|
+
e.target.value = "";
|
|
72
|
+
if (!file) return;
|
|
73
|
+
if (validateFigUploadFile(file)) {
|
|
74
|
+
setError(t("designEditor.import.figmaHydrationInvalidFig"));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setBusy(true);
|
|
78
|
+
setError(null);
|
|
79
|
+
try {
|
|
80
|
+
const result = await hydrateImagesFromFig({
|
|
81
|
+
designId,
|
|
82
|
+
file,
|
|
83
|
+
fileIds,
|
|
84
|
+
fallbackErrorMessage: t("designEditor.import.figmaHydrationFigError"),
|
|
85
|
+
});
|
|
86
|
+
if (result.error) {
|
|
87
|
+
setError(result.error);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const totalResolved = result.totalResolved ?? 0;
|
|
91
|
+
onOpenChange(false);
|
|
92
|
+
setToken("");
|
|
93
|
+
onHydrated();
|
|
94
|
+
toast.success(t("designEditor.import.figmaHydrationSuccess"), {
|
|
95
|
+
description: t(
|
|
96
|
+
"designEditor.import.figmaHydrationFigSuccessDescription",
|
|
97
|
+
{ count: totalResolved, plural: totalResolved === 1 ? "" : "s" },
|
|
98
|
+
),
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
setError(
|
|
102
|
+
err instanceof Error
|
|
103
|
+
? err.message
|
|
104
|
+
: t("designEditor.import.figmaHydrationFigError"),
|
|
105
|
+
);
|
|
106
|
+
} finally {
|
|
107
|
+
setBusy(false);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
if (!token.trim()) return;
|
|
114
|
+
setBusy(true);
|
|
115
|
+
setError(null);
|
|
116
|
+
try {
|
|
117
|
+
const status = await saveFigmaAccessToken(token.trim());
|
|
118
|
+
if (status.docsUrl) setDocsUrl(status.docsUrl);
|
|
119
|
+
|
|
120
|
+
let totalResolved = 0;
|
|
121
|
+
for (const fileId of fileIds) {
|
|
122
|
+
const result = await callAction<{
|
|
123
|
+
resolved?: number;
|
|
124
|
+
missing?: number;
|
|
125
|
+
}>("hydrate-figma-paste-images", { fileId });
|
|
126
|
+
totalResolved += result?.resolved ?? 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onOpenChange(false);
|
|
130
|
+
setToken("");
|
|
131
|
+
onHydrated();
|
|
132
|
+
toast.success(t("designEditor.import.figmaHydrationSuccess"), {
|
|
133
|
+
description: t("designEditor.import.figmaHydrationSuccessDescription", {
|
|
134
|
+
count: totalResolved,
|
|
135
|
+
plural: totalResolved === 1 ? "" : "s",
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const message =
|
|
140
|
+
err instanceof Error ? err.message : t("common.genericError");
|
|
141
|
+
const is403 =
|
|
142
|
+
message.includes("403") || message.toLowerCase().includes("forbidden");
|
|
143
|
+
const isServerError = /internal server error/i.test(message);
|
|
144
|
+
setError(
|
|
145
|
+
is403
|
|
146
|
+
? 'Token rejected (403). In Figma\'s token settings, enable the "File content" and "Current user" scopes, then generate a new token.'
|
|
147
|
+
: isServerError
|
|
148
|
+
? "Server error — Figma's API may be rate-limited. Wait ~1 minute then try again; repeated retries extend the cooldown."
|
|
149
|
+
: message,
|
|
150
|
+
);
|
|
151
|
+
} finally {
|
|
152
|
+
setBusy(false);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
158
|
+
<DialogContent className="sm:max-w-md">
|
|
159
|
+
<form
|
|
160
|
+
onSubmit={(e) => {
|
|
161
|
+
void handleSubmit(e);
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<DialogHeader>
|
|
165
|
+
<DialogTitle>
|
|
166
|
+
{t("designEditor.import.figmaHydrationDialogTitle")}
|
|
167
|
+
</DialogTitle>
|
|
168
|
+
<DialogDescription>
|
|
169
|
+
{t("designEditor.import.figmaHydrationDialogDescription", {
|
|
170
|
+
count: imageCount,
|
|
171
|
+
plural: imagePlural,
|
|
172
|
+
screensPlural,
|
|
173
|
+
})}
|
|
174
|
+
</DialogDescription>
|
|
175
|
+
</DialogHeader>
|
|
176
|
+
|
|
177
|
+
<div className="mt-4 rounded-md border border-border bg-muted/30 p-3">
|
|
178
|
+
<div className="flex items-center gap-2">
|
|
179
|
+
<p className="text-xs font-medium text-foreground">
|
|
180
|
+
{t("designEditor.import.figmaHydrationFigTitle")}
|
|
181
|
+
</p>
|
|
182
|
+
<span className="rounded-full bg-primary/10 px-1.5 py-px text-[9px] font-semibold uppercase tracking-wide text-primary">
|
|
183
|
+
{t("designEditor.import.figmaHydrationRecommended")}
|
|
184
|
+
</span>
|
|
185
|
+
</div>
|
|
186
|
+
<p className="mt-1 text-[10px] leading-snug text-muted-foreground">
|
|
187
|
+
{t("designEditor.import.figmaHydrationFigOption")}
|
|
188
|
+
</p>
|
|
189
|
+
<input
|
|
190
|
+
ref={figInputRef}
|
|
191
|
+
type="file"
|
|
192
|
+
accept=".fig"
|
|
193
|
+
className="hidden"
|
|
194
|
+
onChange={(e) => void handleFigSelected(e)}
|
|
195
|
+
/>
|
|
196
|
+
<Button
|
|
197
|
+
type="button"
|
|
198
|
+
size="sm"
|
|
199
|
+
variant="secondary"
|
|
200
|
+
className="mt-2 w-full"
|
|
201
|
+
disabled={busy}
|
|
202
|
+
onClick={() => figInputRef.current?.click()}
|
|
203
|
+
>
|
|
204
|
+
{t("designEditor.import.figmaHydrationChooseFig")}
|
|
205
|
+
</Button>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<div className="mt-3 text-center text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
209
|
+
{t("designEditor.import.figmaHydrationOrToken")}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="mt-2 space-y-2">
|
|
213
|
+
<div className="flex items-center justify-between gap-2">
|
|
214
|
+
<Label htmlFor="figma-hydration-token" className="text-xs">
|
|
215
|
+
{t("designEditor.import.figmaTokenLabel")}
|
|
216
|
+
</Label>
|
|
217
|
+
{docsUrl ? (
|
|
218
|
+
<a
|
|
219
|
+
href={docsUrl}
|
|
220
|
+
target="_blank"
|
|
221
|
+
rel="noreferrer"
|
|
222
|
+
className="shrink-0 text-[10px] font-medium text-foreground underline-offset-2 hover:underline"
|
|
223
|
+
>
|
|
224
|
+
{t("designEditor.import.figmaTokenDocs")}
|
|
225
|
+
</a>
|
|
226
|
+
) : null}
|
|
227
|
+
</div>
|
|
228
|
+
<Input
|
|
229
|
+
id="figma-hydration-token"
|
|
230
|
+
type="password"
|
|
231
|
+
value={token}
|
|
232
|
+
onChange={(e) => setToken(e.target.value)}
|
|
233
|
+
placeholder={t("designEditor.import.figmaTokenPlaceholder")}
|
|
234
|
+
autoComplete="new-password"
|
|
235
|
+
aria-invalid={error ? true : undefined}
|
|
236
|
+
className="h-8 text-xs"
|
|
237
|
+
disabled={busy}
|
|
238
|
+
/>
|
|
239
|
+
{error ? (
|
|
240
|
+
<p className="rounded-md border border-destructive/30 bg-destructive/5 px-2 py-1.5 text-[10px] leading-snug text-destructive">
|
|
241
|
+
{error}
|
|
242
|
+
</p>
|
|
243
|
+
) : (
|
|
244
|
+
<div className="space-y-1">
|
|
245
|
+
<p className="text-[10px] leading-snug text-muted-foreground">
|
|
246
|
+
{t("designEditor.import.figmaHydrationTokenDescription")}
|
|
247
|
+
</p>
|
|
248
|
+
<p className="text-[10px] leading-snug text-muted-foreground/70">
|
|
249
|
+
{t("designEditor.import.figmaHydrationRateLimit")}
|
|
250
|
+
</p>
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<DialogFooter className="mt-4">
|
|
256
|
+
<Button
|
|
257
|
+
type="button"
|
|
258
|
+
variant="ghost"
|
|
259
|
+
size="sm"
|
|
260
|
+
onClick={() => onOpenChange(false)}
|
|
261
|
+
disabled={busy}
|
|
262
|
+
>
|
|
263
|
+
{t("home.cancel")}
|
|
264
|
+
</Button>
|
|
265
|
+
<Button type="submit" size="sm" disabled={busy || !token.trim()}>
|
|
266
|
+
{t("designEditor.import.figmaHydrationConnectAndLoad")}
|
|
267
|
+
</Button>
|
|
268
|
+
</DialogFooter>
|
|
269
|
+
</form>
|
|
270
|
+
</DialogContent>
|
|
271
|
+
</Dialog>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
@@ -455,6 +455,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
455
455
|
onBoardElementDblClickText,
|
|
456
456
|
onBoardIframeHotkey,
|
|
457
457
|
onBoardFigmaClipboardPaste,
|
|
458
|
+
onBoardImagePaste,
|
|
458
459
|
onBoardIframeContextMenu,
|
|
459
460
|
onBoardTextEditingStateChange,
|
|
460
461
|
boardClearSelectionRequest,
|
|
@@ -7670,6 +7671,7 @@ export const MultiScreenCanvas = memo(function MultiScreenCanvas({
|
|
|
7670
7671
|
onClearSelection={onBoardElementClear}
|
|
7671
7672
|
onIframeHotkey={onBoardIframeHotkey}
|
|
7672
7673
|
onFigmaClipboardPaste={onBoardFigmaClipboardPaste}
|
|
7674
|
+
onImagePaste={onBoardImagePaste}
|
|
7673
7675
|
onIframeContextMenu={onBoardIframeContextMenu}
|
|
7674
7676
|
onVisualStructureChange={onBoardVisualStructureChange}
|
|
7675
7677
|
onVisualStyleChange={onBoardVisualStyleChange}
|
|
@@ -9170,14 +9172,12 @@ const Screen = memo(function Screen({
|
|
|
9170
9172
|
// Memoize the srcdoc with the hit-test responder injected so we don't
|
|
9171
9173
|
// rebuild the string every render (that would reload the iframe).
|
|
9172
9174
|
// Keyed only on screen.content; the hit-test script itself is constant.
|
|
9173
|
-
const srcdocWithHitTest = useMemo(
|
|
9174
|
-
(
|
|
9175
|
-
|
|
9176
|
-
|
|
9177
|
-
),
|
|
9175
|
+
const srcdocWithHitTest = useMemo(() => {
|
|
9176
|
+
return injectSessionReplayIframeBootstrap(
|
|
9177
|
+
appendHitTestResponder(screen.content),
|
|
9178
|
+
);
|
|
9178
9179
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
9179
|
-
|
|
9180
|
-
);
|
|
9180
|
+
}, [screen.content]);
|
|
9181
9181
|
|
|
9182
9182
|
const updateDirectHover = useCallback((next: boolean) => {
|
|
9183
9183
|
setDirectlyHovered((current) => (current === next ? current : next));
|
|
@@ -10447,15 +10447,63 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
10447
10447
|
}
|
|
10448
10448
|
var content = getFigmaClipboardContent(e.clipboardData);
|
|
10449
10449
|
clearPendingPlainPasteHotkey();
|
|
10450
|
-
if (
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
10450
|
+
if (content) {
|
|
10451
|
+
stopNativeInteraction(e);
|
|
10452
|
+
(window.parent as Window).postMessage(
|
|
10453
|
+
{ type: "figma-clipboard-paste", content: content },
|
|
10454
|
+
"*",
|
|
10455
|
+
);
|
|
10456
|
+
return;
|
|
10457
|
+
}
|
|
10458
|
+
// Relay image files pasted while the canvas has focus (e.g. "Copy as PNG"
|
|
10459
|
+
// from Figma, or a screenshot). The parent's handleEditorPaste cannot see
|
|
10460
|
+
// these because paste events inside an iframe don't bubble to the parent
|
|
10461
|
+
// document — the bridge reads each file as a data URL and relays it so
|
|
10462
|
+
// the parent's handlePastedImageFiles can insert an <img> layer.
|
|
10463
|
+
var imageFiles = Array.from(e.clipboardData?.items ?? [])
|
|
10464
|
+
.filter(function (item) {
|
|
10465
|
+
return item.kind === "file" && item.type.startsWith("image/");
|
|
10466
|
+
})
|
|
10467
|
+
.map(function (item) {
|
|
10468
|
+
return item.getAsFile();
|
|
10469
|
+
})
|
|
10470
|
+
.filter(function (f): f is File {
|
|
10471
|
+
return Boolean(f);
|
|
10472
|
+
});
|
|
10473
|
+
if (imageFiles.length > 0) {
|
|
10474
|
+
stopNativeInteraction(e);
|
|
10475
|
+
var readPromises = imageFiles.map(function (file) {
|
|
10476
|
+
return new Promise<{
|
|
10477
|
+
dataUrl: string;
|
|
10478
|
+
type: string;
|
|
10479
|
+
name: string;
|
|
10480
|
+
} | null>(function (resolve) {
|
|
10481
|
+
var reader = new FileReader();
|
|
10482
|
+
reader.onload = function () {
|
|
10483
|
+
resolve({
|
|
10484
|
+
dataUrl: typeof reader.result === "string" ? reader.result : "",
|
|
10485
|
+
type: file.type,
|
|
10486
|
+
name: file.name,
|
|
10487
|
+
});
|
|
10488
|
+
};
|
|
10489
|
+
reader.onerror = function () {
|
|
10490
|
+
resolve(null);
|
|
10491
|
+
};
|
|
10492
|
+
reader.readAsDataURL(file);
|
|
10493
|
+
});
|
|
10494
|
+
});
|
|
10495
|
+
void Promise.all(readPromises).then(function (results) {
|
|
10496
|
+
var valid = results.filter(function (r) {
|
|
10497
|
+
return r && r.dataUrl;
|
|
10498
|
+
});
|
|
10499
|
+
if (valid.length > 0) {
|
|
10500
|
+
(window.parent as Window).postMessage(
|
|
10501
|
+
{ type: "canvas-image-paste", files: valid },
|
|
10502
|
+
"*",
|
|
10503
|
+
);
|
|
10504
|
+
}
|
|
10505
|
+
});
|
|
10506
|
+
}
|
|
10459
10507
|
},
|
|
10460
10508
|
true,
|
|
10461
10509
|
);
|
|
@@ -14,6 +14,16 @@ export interface IframeFigmaClipboardPastePayload {
|
|
|
14
14
|
content: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export interface IframeImagePasteFile {
|
|
18
|
+
dataUrl: string;
|
|
19
|
+
type: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IframeImagePastePayload {
|
|
24
|
+
files: IframeImagePasteFile[];
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
export interface IframeContextMenuPayload {
|
|
18
28
|
screenId?: string;
|
|
19
29
|
clientX: number;
|
|
@@ -279,7 +279,14 @@ function stripExecutableStaticPreviewContent(html: string) {
|
|
|
279
279
|
.replace(/<(?:iframe|object|embed|audio|video|source)\b[^>]*\/?\s*>/gi, "")
|
|
280
280
|
.replace(/<(?:link|meta|base)\b[^>]*>/gi, "")
|
|
281
281
|
.replace(/@import\s+(?:url\([^)]*\)|["'][^"']*["'])\s*[^;]*;/gi, "")
|
|
282
|
-
.replace(/url\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)/gi,
|
|
282
|
+
.replace(/url\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)/gi, (match) => {
|
|
283
|
+
// Strip data:, blob:, and unknown-scheme url() references to prevent
|
|
284
|
+
// large embedded payloads or local-resource leaks. Allow https:// URLs
|
|
285
|
+
// (CDN images/fonts) — they can only fetch inert assets, not execute code.
|
|
286
|
+
const raw = match.slice(4, -1).trim();
|
|
287
|
+
const inner = raw.replace(/^['"]|['"]$/g, "").trim();
|
|
288
|
+
return /^https:\/\//i.test(inner) ? match : "none";
|
|
289
|
+
})
|
|
283
290
|
.replace(/<(?:img|image|use)\b[^>]*>/gi, (tag) =>
|
|
284
291
|
tag.replace(
|
|
285
292
|
/\s+(?:src|srcset|href|xlink:href)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi,
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
IframeContextMenuPayload,
|
|
11
11
|
IframeFigmaClipboardPastePayload,
|
|
12
12
|
IframeHotkeyPayload,
|
|
13
|
+
IframeImagePastePayload,
|
|
13
14
|
} from "../design-canvas/iframe-events";
|
|
14
15
|
import type {
|
|
15
16
|
DeviceFrameType,
|
|
@@ -363,6 +364,7 @@ export interface MultiScreenCanvasProps {
|
|
|
363
364
|
onBoardFigmaClipboardPaste?: (
|
|
364
365
|
event: IframeFigmaClipboardPastePayload,
|
|
365
366
|
) => void;
|
|
367
|
+
onBoardImagePaste?: (event: IframeImagePastePayload) => void;
|
|
366
368
|
onBoardIframeContextMenu?: (event: IframeContextMenuPayload) => void;
|
|
367
369
|
onBoardTextEditingStateChange?: (state: {
|
|
368
370
|
active: boolean;
|
|
@@ -940,6 +942,7 @@ export type {
|
|
|
940
942
|
IframeContextMenuPayload,
|
|
941
943
|
IframeFigmaClipboardPastePayload,
|
|
942
944
|
IframeHotkeyPayload,
|
|
945
|
+
IframeImagePastePayload,
|
|
943
946
|
};
|
|
944
947
|
|
|
945
948
|
export interface ResolvedScreenMetadata {
|