@agent-native/core 0.157.9 → 0.157.10
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/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +5 -2
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +3 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +18 -6
- package/corpus/templates/analytics/server/plugins/db.ts +139 -0
- package/corpus/templates/slides/AGENTS.md +14 -10
- package/corpus/templates/slides/actions/export-pptx.ts +1094 -94
- package/corpus/templates/slides/actions/import-file.ts +16 -1
- package/corpus/templates/slides/actions/import-pptx.ts +61 -43
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +222 -29
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +71 -0
- package/corpus/templates/slides/app/global.css +19 -1
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +44 -5
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +13 -6
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +556 -25
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +18 -1
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +1176 -26
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +13 -0
- package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +24 -0
- package/corpus/templates/slides/shared/aspect-ratios.ts +13 -1
- package/dist/agent/harness/migrations.js +24 -3
- package/dist/agent/production-agent.js +11 -0
- package/dist/agent/run-loop-with-resume.js +21 -4
- package/dist/automations/service.d.ts +6 -0
- package/dist/automations/service.js +35 -0
- package/dist/cli/code-agent-connector.d.ts +4 -1
- package/dist/cli/code-agent-connector.js +154 -23
- package/dist/cli/code-agent-executor.js +255 -13
- package/dist/cli/code-agent-runs.d.ts +1 -0
- package/dist/cli/code-agent-runs.js +2 -1
- package/dist/cli/portal-workspace.d.ts +49 -0
- package/dist/cli/portal-workspace.js +320 -0
- package/dist/client/AssistantChat.d.ts +1 -1
- package/dist/client/AssistantChat.js +39 -61
- package/dist/client/ConnectBuilderCard.js +33 -5
- package/dist/client/agent-chat-adapter.js +4 -0
- package/dist/client/analytics-platform.d.ts +2 -0
- package/dist/client/analytics-platform.js +12 -0
- package/dist/client/analytics.d.ts +3 -0
- package/dist/client/analytics.js +10 -1
- package/dist/client/chat/index.d.ts +1 -0
- package/dist/client/chat/index.js +1 -0
- package/dist/client/chat/run-recovery.d.ts +2 -1
- package/dist/client/chat/run-recovery.js +2 -2
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +11 -11
- package/dist/client/chat-first/surface-tabs.d.ts +1 -1
- package/dist/client/chat-first/surface-tabs.js +38 -36
- package/dist/client/chat-first/types.d.ts +5 -0
- package/dist/client/chat-first.d.ts +1 -1
- package/dist/client/chat-first.js +4 -4
- package/dist/client/desktop-local-code-change.d.ts +10 -0
- package/dist/client/desktop-local-code-change.js +16 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/observability/ThumbsFeedback.js +21 -7
- package/dist/client/terminal/AgentTerminal.d.ts +9 -1
- package/dist/client/terminal/AgentTerminal.js +38 -3
- package/dist/client/terminal/index.d.ts +1 -1
- package/dist/client/terminal/index.js +1 -1
- package/dist/client/track.js +9 -1
- package/dist/client/use-action.js +3 -0
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +1 -1
- package/dist/ingestion/pptx.d.ts +78 -0
- package/dist/ingestion/pptx.js +991 -107
- package/dist/integrations/index.d.ts +3 -3
- package/dist/integrations/index.js +2 -2
- package/dist/integrations/plugin.js +95 -6
- package/dist/integrations/remote-commands-store.d.ts +5 -0
- package/dist/integrations/remote-commands-store.js +63 -22
- package/dist/integrations/remote-devices-store.d.ts +2 -1
- package/dist/integrations/remote-devices-store.js +46 -0
- package/dist/integrations/remote-types.d.ts +20 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts +3 -0
- package/dist/jobs/actions/list-recurring-jobs.js +5 -1
- package/dist/jobs/frontmatter.d.ts +16 -0
- package/dist/jobs/frontmatter.js +53 -0
- package/dist/jobs/remote-execution.d.ts +42 -0
- package/dist/jobs/remote-execution.js +287 -0
- package/dist/jobs/scheduler.js +102 -3
- package/dist/jobs/tools.js +50 -3
- package/dist/notifications/routes.d.ts +4 -4
- package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
- package/dist/provider-api/actions/provider-api.d.ts +14 -14
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/action-routes.js +4 -2
- package/dist/server/agent-chat-plugin.js +6 -0
- package/dist/server/agent-run-context.d.ts +1 -0
- package/dist/server/agent-run-context.js +9 -0
- package/dist/server/core-routes-plugin.js +8 -1
- package/dist/server/request-context.d.ts +2 -0
- package/dist/shared/analytics-platform.d.ts +6 -0
- package/dist/shared/analytics-platform.js +14 -0
- package/dist/shared/mcp-embed-headers.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/styles/agent-native.css +83 -103
- package/dist/terminal/cli-registry.js +5 -0
- package/dist/tracking/registry.js +9 -1
- package/dist/triggers/actions/list-automations.d.ts +3 -0
- package/dist/triggers/actions/list-automations.js +3 -0
- package/dist/triggers/actions/manage-automation.d.ts +9 -0
- package/dist/triggers/actions/manage-automation.js +14 -2
- package/dist/triggers/actions.js +82 -7
- package/docs/content/template-content-developers.mdx +48 -0
- package/docs/content/toolkit-ui.mdx +57 -0
- package/package.json +2 -2
|
@@ -14,7 +14,10 @@ import { z } from "zod";
|
|
|
14
14
|
|
|
15
15
|
import { getDb, schema } from "../server/db/index.js";
|
|
16
16
|
import { notifyClients } from "../server/handlers/decks.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
assertPptxImagesRenderable,
|
|
19
|
+
uploadPptxSlideImages,
|
|
20
|
+
} from "../server/handlers/import/pptx-assets.js";
|
|
18
21
|
import { upsertBuilderProxyDesignSystem } from "../server/lib/builder-design-system-proxy.js";
|
|
19
22
|
import { setupPdfParse } from "../server/lib/pdf-parse-setup.js";
|
|
20
23
|
import {
|
|
@@ -148,6 +151,7 @@ export default defineAction({
|
|
|
148
151
|
await assertAccess("deck", deckId, "editor");
|
|
149
152
|
const pptxOwnerEmail = getRequestUserEmail();
|
|
150
153
|
if (!pptxOwnerEmail) throw new Error("no authenticated user");
|
|
154
|
+
assertPptxImagesRenderable(presentation.slides);
|
|
151
155
|
const pptxThemeFont = presentation.theme?.fonts?.[0];
|
|
152
156
|
const uploadLimit = pLimit(4);
|
|
153
157
|
const pptxResults = await Promise.all(
|
|
@@ -198,6 +202,7 @@ export default defineAction({
|
|
|
198
202
|
aspectRatio,
|
|
199
203
|
sourceImport,
|
|
200
204
|
pptxResults[0]?.sourceText,
|
|
205
|
+
presentation.theme,
|
|
201
206
|
);
|
|
202
207
|
return {
|
|
203
208
|
format: "pptx",
|
|
@@ -700,6 +705,7 @@ async function appendDeckSlides(
|
|
|
700
705
|
aspectRatio?: AspectRatio,
|
|
701
706
|
sourceImport?: ReturnType<typeof buildSourceImportMetadata>,
|
|
702
707
|
titleSource?: unknown,
|
|
708
|
+
theme?: import("../server/handlers/import/pptx-parser.js").ParsedPresentation["theme"],
|
|
703
709
|
): Promise<string> {
|
|
704
710
|
await assertAccess("deck", deckId, "editor");
|
|
705
711
|
|
|
@@ -748,6 +754,15 @@ async function appendDeckSlides(
|
|
|
748
754
|
title: nextTitle,
|
|
749
755
|
slides: [...previousSlides, ...slides],
|
|
750
756
|
...(!hadExistingSlides && aspectRatio ? { aspectRatio } : {}),
|
|
757
|
+
// Same rule as aspectRatio above: an appended file's palette only
|
|
758
|
+
// becomes the deck's theme when the deck had no slides yet, so
|
|
759
|
+
// appending onto an existing deck can't silently restyle every slide
|
|
760
|
+
// already on it. Deliberately keyed on slides rather than on whether a
|
|
761
|
+
// theme already exists: export writes one deck-level theme (OOXML allows
|
|
762
|
+
// one per master), so with zero slides there is nothing to protect, and
|
|
763
|
+
// refusing a new theme there would strand a deck whose slides were
|
|
764
|
+
// deleted with the old file's palette, unfixable by re-importing.
|
|
765
|
+
...(!hadExistingSlides && theme ? { theme } : {}),
|
|
751
766
|
...(nextSourceImport ? { sourceImport: nextSourceImport } : {}),
|
|
752
767
|
updatedAt: writeNow,
|
|
753
768
|
};
|
|
@@ -12,7 +12,10 @@ import { z } from "zod";
|
|
|
12
12
|
import { getDb, schema } from "../server/db/index.js";
|
|
13
13
|
import { notifyClients } from "../server/handlers/decks.js";
|
|
14
14
|
import { convertToSlideHtml } from "../server/handlers/import/html-converter.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
assertPptxImagesRenderable,
|
|
17
|
+
uploadPptxSlideImages,
|
|
18
|
+
} from "../server/handlers/import/pptx-assets.js";
|
|
16
19
|
import {
|
|
17
20
|
parsePptx,
|
|
18
21
|
type ParsedElement,
|
|
@@ -27,6 +30,7 @@ import {
|
|
|
27
30
|
} from "../shared/aspect-ratios.js";
|
|
28
31
|
import { getDeckUrl } from "./_app-url.js";
|
|
29
32
|
import { readUserUploadedFile } from "./_uploaded-files.js";
|
|
33
|
+
import { withDeckLock } from "./patch-deck.js";
|
|
30
34
|
|
|
31
35
|
export interface ImportedImageFallback {
|
|
32
36
|
slideIndex: number;
|
|
@@ -121,9 +125,18 @@ export async function importPptxBufferToDeck(args: {
|
|
|
121
125
|
// Check edit access before uploading any embedded images — uploads are
|
|
122
126
|
// a side effect with real storage cost, so an unauthorized caller must
|
|
123
127
|
// be rejected before that side effect happens, not after.
|
|
128
|
+
const db = getDb();
|
|
124
129
|
if (deckId) {
|
|
125
130
|
await assertAccess("deck", deckId, "editor");
|
|
131
|
+
const [deck] = await db
|
|
132
|
+
.select()
|
|
133
|
+
.from(schema.decks)
|
|
134
|
+
.where(eq(schema.decks.id, deckId));
|
|
135
|
+
if (!deck) {
|
|
136
|
+
throw new Error(`Deck ${deckId} not found`);
|
|
137
|
+
}
|
|
126
138
|
}
|
|
139
|
+
assertPptxImagesRenderable(presentation.slides);
|
|
127
140
|
|
|
128
141
|
// Convert each parsed slide to its positioned scene graph, uploading every
|
|
129
142
|
// browser-renderable image so the imported deck keeps the source layering
|
|
@@ -193,56 +206,60 @@ export async function importPptxBufferToDeck(args: {
|
|
|
193
206
|
presentation.slides[0]?.heightEmu,
|
|
194
207
|
);
|
|
195
208
|
|
|
196
|
-
const db = getDb();
|
|
197
209
|
const now = new Date().toISOString();
|
|
198
210
|
|
|
199
211
|
if (deckId) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
.
|
|
203
|
-
.
|
|
212
|
+
return withDeckLock(deckId, async () => {
|
|
213
|
+
// Image uploads happen before this lock because they are independent of
|
|
214
|
+
// the deck row. Re-read inside the lock so the replacement is based on
|
|
215
|
+
// the latest deck data rather than the preflight snapshot.
|
|
216
|
+
const [latestDeck] = await db
|
|
217
|
+
.select()
|
|
218
|
+
.from(schema.decks)
|
|
219
|
+
.where(eq(schema.decks.id, deckId));
|
|
220
|
+
if (!latestDeck) {
|
|
221
|
+
throw new Error(`Deck ${deckId} not found`);
|
|
222
|
+
}
|
|
204
223
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const previousData = safeParseDeckData(existing[0].data);
|
|
210
|
-
const data = {
|
|
211
|
-
...previousData,
|
|
212
|
-
title: deckTitle,
|
|
213
|
-
slides,
|
|
214
|
-
...(aspectRatio ? { aspectRatio } : {}),
|
|
215
|
-
sourceImport,
|
|
216
|
-
updatedAt: now,
|
|
217
|
-
};
|
|
218
|
-
await db
|
|
219
|
-
.update(schema.decks)
|
|
220
|
-
.set({
|
|
224
|
+
const previousData = safeParseDeckData(latestDeck.data);
|
|
225
|
+
const data = {
|
|
226
|
+
...previousData,
|
|
221
227
|
title: deckTitle,
|
|
222
|
-
|
|
223
|
-
...(
|
|
224
|
-
|
|
225
|
-
|
|
228
|
+
slides,
|
|
229
|
+
...(aspectRatio ? { aspectRatio } : {}),
|
|
230
|
+
...(presentation.theme ? { theme: presentation.theme } : {}),
|
|
231
|
+
sourceImport,
|
|
226
232
|
updatedAt: now,
|
|
227
|
-
}
|
|
228
|
-
|
|
233
|
+
};
|
|
234
|
+
await db
|
|
235
|
+
.update(schema.decks)
|
|
236
|
+
.set({
|
|
237
|
+
title: deckTitle,
|
|
238
|
+
data: JSON.stringify(data),
|
|
239
|
+
...(designSystemId !== undefined
|
|
240
|
+
? { designSystemId }
|
|
241
|
+
: { designSystemId: latestDeck.designSystemId }),
|
|
242
|
+
updatedAt: now,
|
|
243
|
+
})
|
|
244
|
+
.where(eq(schema.decks.id, deckId));
|
|
229
245
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
notifyClients(deckId);
|
|
247
|
+
await writeAppState("refresh-signal", {
|
|
248
|
+
ts: now,
|
|
249
|
+
source,
|
|
250
|
+
});
|
|
235
251
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
252
|
+
return {
|
|
253
|
+
id: deckId,
|
|
254
|
+
title: deckTitle,
|
|
255
|
+
slideCount: slides.length,
|
|
256
|
+
theme: presentation.theme,
|
|
257
|
+
imported: true,
|
|
258
|
+
url: getDeckUrl(deckId),
|
|
259
|
+
...(imagesSkipped > 0 ? { imagesSkipped } : {}),
|
|
260
|
+
...(tablesDegraded > 0 ? { tablesDegraded } : {}),
|
|
261
|
+
};
|
|
262
|
+
});
|
|
246
263
|
}
|
|
247
264
|
|
|
248
265
|
// Create new deck
|
|
@@ -251,6 +268,7 @@ export async function importPptxBufferToDeck(args: {
|
|
|
251
268
|
title: deckTitle,
|
|
252
269
|
slides,
|
|
253
270
|
...(aspectRatio ? { aspectRatio } : {}),
|
|
271
|
+
...(presentation.theme ? { theme: presentation.theme } : {}),
|
|
254
272
|
sourceImport,
|
|
255
273
|
createdAt: now,
|
|
256
274
|
updatedAt: now,
|
|
@@ -528,6 +528,11 @@ function useSlideAutofit(
|
|
|
528
528
|
|
|
529
529
|
root.addEventListener("load", scheduleMeasure, true);
|
|
530
530
|
document.fonts?.ready.then(scheduleMeasure).catch(() => {});
|
|
531
|
+
// An imported deck's webfonts are requested only once its HTML is on
|
|
532
|
+
// screen, so they can land after `fonts.ready` has already settled. That
|
|
533
|
+
// reflows text without touching the DOM the observers watch, leaving the
|
|
534
|
+
// fit transform measured against the fallback font.
|
|
535
|
+
document.fonts?.addEventListener("loadingdone", scheduleMeasure);
|
|
531
536
|
|
|
532
537
|
// `rootMargin` measures a thumbnail just before it scrolls in, so the fit
|
|
533
538
|
// transform is already applied by the time it is on screen.
|
|
@@ -554,6 +559,7 @@ function useSlideAutofit(
|
|
|
554
559
|
mutationObserver.disconnect();
|
|
555
560
|
visibilityObserver?.disconnect();
|
|
556
561
|
root.removeEventListener("load", scheduleMeasure, true);
|
|
562
|
+
document.fonts?.removeEventListener("loadingdone", scheduleMeasure);
|
|
557
563
|
};
|
|
558
564
|
}, [canvasWidth, canvasHeight, fitKey, ref]);
|
|
559
565
|
}
|
|
@@ -596,6 +602,186 @@ function AutoFitContent({
|
|
|
596
602
|
);
|
|
597
603
|
}
|
|
598
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Google Fonts families an imported deck may name. Split by what the `css2`
|
|
607
|
+
* endpoint accepts for each: the first list has a real 100..900 variable
|
|
608
|
+
* weight axis, the rest only serve discrete weights. Asking for an axis a
|
|
609
|
+
* family does not have is a 400 for the whole request, so both lists were
|
|
610
|
+
* checked against the live endpoint rather than guessed from the name.
|
|
611
|
+
*/
|
|
612
|
+
const VARIABLE_AXIS_GOOGLE_FONTS = [
|
|
613
|
+
"Archivo",
|
|
614
|
+
"Asap",
|
|
615
|
+
"Catamaran",
|
|
616
|
+
"Chivo",
|
|
617
|
+
"DM Sans",
|
|
618
|
+
"Epilogue",
|
|
619
|
+
"Exo 2",
|
|
620
|
+
"Geist",
|
|
621
|
+
"Heebo",
|
|
622
|
+
"Inter",
|
|
623
|
+
"Jost",
|
|
624
|
+
"League Spartan",
|
|
625
|
+
"Lexend",
|
|
626
|
+
"Libre Franklin",
|
|
627
|
+
"Montserrat",
|
|
628
|
+
"Noto Sans",
|
|
629
|
+
"Noto Serif",
|
|
630
|
+
"Onest",
|
|
631
|
+
"Outfit",
|
|
632
|
+
"Overpass",
|
|
633
|
+
"Public Sans",
|
|
634
|
+
"Raleway",
|
|
635
|
+
"Roboto",
|
|
636
|
+
"Roboto Condensed",
|
|
637
|
+
"Roboto Slab",
|
|
638
|
+
"Urbanist",
|
|
639
|
+
"Work Sans",
|
|
640
|
+
];
|
|
641
|
+
|
|
642
|
+
const STATIC_WEIGHT_GOOGLE_FONTS = [
|
|
643
|
+
"Abril Fatface",
|
|
644
|
+
"Anton",
|
|
645
|
+
"Arimo",
|
|
646
|
+
"Assistant",
|
|
647
|
+
"Barlow",
|
|
648
|
+
"Barlow Condensed",
|
|
649
|
+
"Bebas Neue",
|
|
650
|
+
"Bodoni Moda",
|
|
651
|
+
"Bricolage Grotesque",
|
|
652
|
+
"Cabin",
|
|
653
|
+
"Caveat",
|
|
654
|
+
"Cormorant Garamond",
|
|
655
|
+
"Cousine",
|
|
656
|
+
"Crimson Text",
|
|
657
|
+
"Dancing Script",
|
|
658
|
+
"David Libre",
|
|
659
|
+
"EB Garamond",
|
|
660
|
+
"Figtree",
|
|
661
|
+
"Fira Sans",
|
|
662
|
+
"Hind",
|
|
663
|
+
"Homemade Apple",
|
|
664
|
+
"IBM Plex Sans",
|
|
665
|
+
"Inconsolata",
|
|
666
|
+
"Instrument Sans",
|
|
667
|
+
"Josefin Sans",
|
|
668
|
+
"Kanit",
|
|
669
|
+
"Karla",
|
|
670
|
+
"Lato",
|
|
671
|
+
"Libre Baskerville",
|
|
672
|
+
"Lora",
|
|
673
|
+
"Manrope",
|
|
674
|
+
"Merriweather",
|
|
675
|
+
"Mulish",
|
|
676
|
+
"Nova Square",
|
|
677
|
+
"Nunito",
|
|
678
|
+
"Nunito Sans",
|
|
679
|
+
"Open Sans",
|
|
680
|
+
"Oswald",
|
|
681
|
+
"Oxygen",
|
|
682
|
+
"PT Sans",
|
|
683
|
+
"PT Serif",
|
|
684
|
+
"Pacifico",
|
|
685
|
+
"Playfair Display",
|
|
686
|
+
"Plus Jakarta Sans",
|
|
687
|
+
"Poppins",
|
|
688
|
+
"Prompt",
|
|
689
|
+
"Quicksand",
|
|
690
|
+
"Red Hat Display",
|
|
691
|
+
"Roboto Mono",
|
|
692
|
+
"Rubik",
|
|
693
|
+
"Schibsted Grotesk",
|
|
694
|
+
"Sora",
|
|
695
|
+
"Source Sans 3",
|
|
696
|
+
"Space Grotesk",
|
|
697
|
+
"Syne",
|
|
698
|
+
"Teko",
|
|
699
|
+
"Tinos",
|
|
700
|
+
"Titillium Web",
|
|
701
|
+
"Ubuntu",
|
|
702
|
+
"Yanone Kaffeesatz",
|
|
703
|
+
];
|
|
704
|
+
|
|
705
|
+
/** Families decks still name under a name Google Fonts does not serve. */
|
|
706
|
+
const GOOGLE_FONT_ALIASES: Record<string, string> = {
|
|
707
|
+
"source sans pro": "source sans 3",
|
|
708
|
+
bodoni: "bodoni moda",
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* PPTX stores a weight as part of the typeface name — a Work Sans deck is full
|
|
713
|
+
* of runs set in `Work Sans Medium` — but a webfont family covers its whole
|
|
714
|
+
* weight range, so the suffixed name matches nothing and falls back to the
|
|
715
|
+
* generic sans that made every imported deck look unstyled.
|
|
716
|
+
*/
|
|
717
|
+
const FONT_WEIGHT_SUFFIX =
|
|
718
|
+
/\s+(?:thin|extra ?light|ultra ?light|light|book|regular|normal|medium|semi ?bold|demi ?bold|bold|extra ?bold|ultra ?bold|black|heavy|italic|oblique)$/i;
|
|
719
|
+
|
|
720
|
+
const GOOGLE_FONTS = new Map<string, { family: string; href: string }>();
|
|
721
|
+
for (const [families, axis] of [
|
|
722
|
+
[VARIABLE_AXIS_GOOGLE_FONTS, "ital,wght@0,100..900;1,100..900"],
|
|
723
|
+
[STATIC_WEIGHT_GOOGLE_FONTS, "ital,wght@0,400;0,700;1,400;1,700"],
|
|
724
|
+
] as const) {
|
|
725
|
+
for (const family of families) {
|
|
726
|
+
GOOGLE_FONTS.set(family.toLowerCase(), {
|
|
727
|
+
family,
|
|
728
|
+
href: `https://fonts.googleapis.com/css2?family=${family.replace(/ /g, "+")}:${axis}&display=swap`,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** The webfont that should render `name`, or undefined when none can. */
|
|
734
|
+
export function resolveImportedFont(
|
|
735
|
+
name: string,
|
|
736
|
+
): { family: string; href: string } | undefined {
|
|
737
|
+
const key = name
|
|
738
|
+
.replace(/["']/g, "")
|
|
739
|
+
.trim()
|
|
740
|
+
.replace(/\s+/g, " ")
|
|
741
|
+
.toLowerCase();
|
|
742
|
+
if (!key) return undefined;
|
|
743
|
+
const lookup = (candidate: string) =>
|
|
744
|
+
GOOGLE_FONTS.get(GOOGLE_FONT_ALIASES[candidate] ?? candidate);
|
|
745
|
+
return lookup(key) ?? lookup(key.replace(FONT_WEIGHT_SUFFIX, "").trim());
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Point every quoted `font-family` in imported slide HTML at a family we can
|
|
750
|
+
* actually serve, and collect the stylesheets that serve them. Families we
|
|
751
|
+
* cannot serve are left exactly as authored so the browser can still match a
|
|
752
|
+
* locally installed copy.
|
|
753
|
+
*/
|
|
754
|
+
export function prepareImportedFonts(html: string): {
|
|
755
|
+
html: string;
|
|
756
|
+
hrefs: string[];
|
|
757
|
+
} {
|
|
758
|
+
const hrefs = new Set<string>();
|
|
759
|
+
const rewritten = html.replace(
|
|
760
|
+
/(font-family:\s*)'([^']*)'/gi,
|
|
761
|
+
(match, prefix: string, name: string) => {
|
|
762
|
+
const font = resolveImportedFont(name);
|
|
763
|
+
if (!font) return match;
|
|
764
|
+
hrefs.add(font.href);
|
|
765
|
+
return `${prefix}'${font.family}'`;
|
|
766
|
+
},
|
|
767
|
+
);
|
|
768
|
+
return { html: rewritten, hrefs: [...hrefs] };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const injectedFontHrefs = new Set<string>();
|
|
772
|
+
|
|
773
|
+
function loadImportedFonts(hrefs: string[]) {
|
|
774
|
+
if (typeof document === "undefined") return;
|
|
775
|
+
for (const href of hrefs) {
|
|
776
|
+
if (injectedFontHrefs.has(href)) continue;
|
|
777
|
+
injectedFontHrefs.add(href);
|
|
778
|
+
const link = document.createElement("link");
|
|
779
|
+
link.rel = "stylesheet";
|
|
780
|
+
link.href = href;
|
|
781
|
+
document.head.appendChild(link);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
599
785
|
/** Renders blank slide HTML content and applies white filter to logo images */
|
|
600
786
|
function BlankSlideContent({ content }: { content: string }) {
|
|
601
787
|
const scopeId = `slide-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
@@ -607,36 +793,43 @@ function BlankSlideContent({ content }: { content: string }) {
|
|
|
607
793
|
// literal each render therefore wipes any DOM mutations made on children. That
|
|
608
794
|
// includes the per-block `contentEditable="true"` set by SlideEditor's
|
|
609
795
|
// double-click-to-edit flow, which made inline text editing appear to do nothing.
|
|
610
|
-
const { mermaidBlocks, htmlWithPlaceholders, dangerousHtml } =
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
796
|
+
const { mermaidBlocks, htmlWithPlaceholders, dangerousHtml, fontHrefs } =
|
|
797
|
+
useMemo(() => {
|
|
798
|
+
// Extract mermaid blocks BEFORE sanitization — see mermaid-blocks.ts for
|
|
799
|
+
// why (sanitizer HTML-escaping breaks the mermaid parser).
|
|
800
|
+
const { blocks, contentWithPlaceholders } = extractMermaidBlocks(content);
|
|
801
|
+
|
|
802
|
+
// Apply white filter to all logo images (brandfetch, logo.dev, etc.) for dark backgrounds
|
|
803
|
+
const sanitized = sanitizeSlideHtml(
|
|
804
|
+
contentWithPlaceholders.replace(
|
|
805
|
+
/(<img\s+(?=[^>]*src="[^"]*(?:brandfetch|logo\.dev)[^"]*")[^>]*)(\/?>)/gi,
|
|
806
|
+
(_match, before, close) => {
|
|
807
|
+
if (before.includes('style="')) {
|
|
808
|
+
return (
|
|
809
|
+
before.replace(
|
|
810
|
+
'style="',
|
|
811
|
+
'style="filter:brightness(0) invert(1);',
|
|
812
|
+
) + close
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return before + ' style="filter:brightness(0) invert(1);"' + close;
|
|
816
|
+
},
|
|
817
|
+
),
|
|
818
|
+
{ scopeSelector },
|
|
819
|
+
);
|
|
820
|
+
const { html: processed, hrefs } = prepareImportedFonts(sanitized);
|
|
633
821
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
822
|
+
return {
|
|
823
|
+
mermaidBlocks: blocks,
|
|
824
|
+
htmlWithPlaceholders: processed,
|
|
825
|
+
dangerousHtml: { __html: processed },
|
|
826
|
+
fontHrefs: hrefs,
|
|
827
|
+
};
|
|
828
|
+
}, [content]);
|
|
829
|
+
|
|
830
|
+
useEffect(() => {
|
|
831
|
+
loadImportedFonts(fontHrefs);
|
|
832
|
+
}, [fontHrefs]);
|
|
640
833
|
|
|
641
834
|
if (mermaidBlocks.length > 0) {
|
|
642
835
|
return (
|
|
@@ -35,12 +35,52 @@ import {
|
|
|
35
35
|
DropdownMenuSubContent,
|
|
36
36
|
DropdownMenuSubTrigger,
|
|
37
37
|
} from "@/components/ui/dropdown-menu";
|
|
38
|
+
import { useDecks } from "@/context/DeckContext";
|
|
38
39
|
import type { GoogleSlidesExportResult } from "@/lib/export-google-slides-client";
|
|
39
40
|
|
|
40
41
|
/** Google Slides' File → Import dialog, primed to ask for a file. */
|
|
41
42
|
const GOOGLE_SLIDES_IMPORT_URL =
|
|
42
43
|
"https://docs.google.com/presentation/u/0/?usp=import";
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The importer stamps this on every slide it writes, and `parseSlideHtml` in
|
|
47
|
+
* actions/export-pptx.ts branches on the same marker: those slides carry the
|
|
48
|
+
* source file's own geometry, which the server emits as real `custGeom` vector
|
|
49
|
+
* shapes. dom-to-pptx has no custGeom at all and rasterizes them to PNGs.
|
|
50
|
+
*/
|
|
51
|
+
const IMPORTED_SLIDE_MARKER = 'data-imported-pptx="true"';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Objects whose geometry only exists once a browser has laid the slide out:
|
|
55
|
+
* `freezeSlideElementForFreeform` and the text-box tool mint
|
|
56
|
+
* `data-slide-object-id` client-side, while every object the importer emits
|
|
57
|
+
* also carries `data-pptx-element-kind`. The server has no layout engine to
|
|
58
|
+
* measure the former, so it refuses those slides rather than reflowing them.
|
|
59
|
+
*/
|
|
60
|
+
const BROWSER_AUTHORED_OBJECT =
|
|
61
|
+
"[data-slide-object-id]:not([data-pptx-element-kind]), .fmd-freeform-object";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether the vector-capable server exporter can render this deck losslessly.
|
|
65
|
+
* `get-deck` returns the import receipt alongside the deck body, so the client
|
|
66
|
+
* deck carries `sourceImport` at runtime even though the type predates it.
|
|
67
|
+
*/
|
|
68
|
+
export function canExportPptxFromServer(
|
|
69
|
+
deck:
|
|
70
|
+
| { sourceImport?: unknown; slides: { content?: string }[] }
|
|
71
|
+
| null
|
|
72
|
+
| undefined,
|
|
73
|
+
): boolean {
|
|
74
|
+
if (!deck?.sourceImport || deck.slides.length === 0) return false;
|
|
75
|
+
return deck.slides.every((slide) => {
|
|
76
|
+
const html = slide.content ?? "";
|
|
77
|
+
if (!html.includes(IMPORTED_SLIDE_MARKER)) return false;
|
|
78
|
+
return !new DOMParser()
|
|
79
|
+
.parseFromString(html, "text/html")
|
|
80
|
+
.querySelector(BROWSER_AUTHORED_OBJECT);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
44
84
|
interface ExportMenuProps {
|
|
45
85
|
deckId: string;
|
|
46
86
|
deckTitle: string;
|
|
@@ -76,6 +116,7 @@ export const ExportMenu = forwardRef<ExportMenuHandle, ExportMenuProps>(
|
|
|
76
116
|
ref,
|
|
77
117
|
) {
|
|
78
118
|
const t = useT();
|
|
119
|
+
const { getDeck, flushDeckSave } = useDecks();
|
|
79
120
|
const [googleSlidesImportOpen, setGoogleSlidesImportOpen] = useState(false);
|
|
80
121
|
const googleSlidesImportTarget = useRef<Window | null>(null);
|
|
81
122
|
const triggerBlobDownload = (blob: Blob, filename: string) => {
|
|
@@ -108,8 +149,38 @@ export const ExportMenu = forwardRef<ExportMenuHandle, ExportMenuProps>(
|
|
|
108
149
|
}
|
|
109
150
|
};
|
|
110
151
|
|
|
152
|
+
const exportPptxFromServer = async () => {
|
|
153
|
+
// The server exports the persisted deck, so an unflushed edit would be
|
|
154
|
+
// missing from the file the user just asked for.
|
|
155
|
+
await flushDeckSave(deckId);
|
|
156
|
+
const res = await fetch(`${appBasePath()}/api/exports/pptx`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "Content-Type": "application/json" },
|
|
159
|
+
body: JSON.stringify({ deckId }),
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
await readErrorMessage(res, t("editorExport.exportPptxError")),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
triggerBlobDownload(
|
|
167
|
+
await res.blob(),
|
|
168
|
+
filenameFromDisposition(
|
|
169
|
+
res.headers.get("content-disposition"),
|
|
170
|
+
".pptx",
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
};
|
|
174
|
+
|
|
111
175
|
const handleExportPptx = async () => {
|
|
112
176
|
try {
|
|
177
|
+
// An imported deck's shapes survive only on the server path. Falling
|
|
178
|
+
// back to the browser exporter on failure would hand back rasterized
|
|
179
|
+
// silhouettes of the same deck without saying so.
|
|
180
|
+
if (canExportPptxFromServer(getDeck(deckId))) {
|
|
181
|
+
await exportPptxFromServer();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
113
184
|
await onExportPptx();
|
|
114
185
|
} catch (err) {
|
|
115
186
|
console.error("Export failed:", err);
|
|
@@ -537,9 +537,21 @@ button[title="Open agent sidebar"] {
|
|
|
537
537
|
}
|
|
538
538
|
|
|
539
539
|
.fmd-autofit-scale {
|
|
540
|
+
transform-origin: top left;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/* Only apply the fit transform once autofit is actually shrinking content
|
|
544
|
+
(see SlideRenderer's `data-fmd-autofit-active`). Any non-"none" `transform`
|
|
545
|
+
value — even the identity transform this rule used to apply unconditionally
|
|
546
|
+
— creates a new containing block for `position: absolute` descendants
|
|
547
|
+
(CSS Transforms spec). Freeform slide objects are authored against the
|
|
548
|
+
full `.fmd-slide` box; an always-on transform here silently re-anchored
|
|
549
|
+
them to this layer's smaller, padding-inset box instead, shifting them
|
|
550
|
+
off-canvas. Scoping the transform to the active-fit state keeps the
|
|
551
|
+
common (no scaling needed) case free of a containing block at all. */
|
|
552
|
+
.fmd-autofit-scale[data-fmd-autofit-active] {
|
|
540
553
|
transform: translate(var(--fmd-fit-x, 0px), var(--fmd-fit-y, 0px))
|
|
541
554
|
scale(var(--fmd-fit-scale, 1));
|
|
542
|
-
transform-origin: top left;
|
|
543
555
|
}
|
|
544
556
|
|
|
545
557
|
.fmd-slide > [data-fmd-autofit-content] {
|
|
@@ -1084,8 +1096,14 @@ button[title="Open agent sidebar"] {
|
|
|
1084
1096
|
font-style: normal;
|
|
1085
1097
|
}
|
|
1086
1098
|
|
|
1099
|
+
/* An imported PPTX `a:srcRect` crop is rendered by oversizing the `<img>` past
|
|
1100
|
+
its `overflow: hidden` box (e.g. `width: 255%; left: -77%`), so both caps
|
|
1101
|
+
have to be lifted, not just the vertical one — `max-width: 100%` alone
|
|
1102
|
+
silently shrank the image back to its box while the negative `left` kept
|
|
1103
|
+
scrolling, leaving the visible crop window off the picture entirely. */
|
|
1087
1104
|
.slide-content .fmd-slide img {
|
|
1088
1105
|
border-radius: 0;
|
|
1106
|
+
max-width: none;
|
|
1089
1107
|
max-height: none;
|
|
1090
1108
|
margin: 0;
|
|
1091
1109
|
}
|
|
@@ -15,6 +15,40 @@ export type GoogleSlidesExportResult =
|
|
|
15
15
|
/** The export action should send the user through Google OAuth first. */
|
|
16
16
|
| { url: null; requiresConnection: true; reason: string };
|
|
17
17
|
|
|
18
|
+
export interface DeckPptxFile {
|
|
19
|
+
blob: Blob;
|
|
20
|
+
filename: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Renders the deck through the vector-capable server exporter — the only path
|
|
25
|
+
* that emits the source file's shapes as real `custGeom` geometry. Its
|
|
26
|
+
* positioned-object guard is rethrown verbatim so the caller can show it
|
|
27
|
+
* instead of quietly handing Google the rasterized browser export.
|
|
28
|
+
*/
|
|
29
|
+
export async function fetchDeckPptxFromServer(
|
|
30
|
+
deckId: string,
|
|
31
|
+
fallbackError: string,
|
|
32
|
+
): Promise<DeckPptxFile> {
|
|
33
|
+
const res = await fetch(`${appBasePath()}/api/exports/pptx`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
body: JSON.stringify({ deckId }),
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const payload = (await res.json().catch(() => null)) as {
|
|
40
|
+
error?: string;
|
|
41
|
+
message?: string;
|
|
42
|
+
} | null;
|
|
43
|
+
throw new Error(payload?.error || payload?.message || fallbackError);
|
|
44
|
+
}
|
|
45
|
+
const disposition = res.headers.get("content-disposition");
|
|
46
|
+
return {
|
|
47
|
+
blob: await res.blob(),
|
|
48
|
+
filename: disposition?.match(/filename="?([^"]+)"?/i)?.[1] ?? "deck.pptx",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
18
52
|
function triggerBlobDownload(blob: Blob, filename: string) {
|
|
19
53
|
const url = URL.createObjectURL(blob);
|
|
20
54
|
const a = document.createElement("a");
|
|
@@ -37,12 +71,17 @@ export async function exportDeckToGoogleSlides(
|
|
|
37
71
|
deckTitle: string,
|
|
38
72
|
slides: GoogleSlidesExportSlide[],
|
|
39
73
|
aspectRatio?: AspectRatio,
|
|
74
|
+
/**
|
|
75
|
+
* Overrides the browser exporter. Source-imported decks pass
|
|
76
|
+
* `fetchDeckPptxFromServer`: dom-to-pptx ships no custGeom support, so the
|
|
77
|
+
* browser build would upload PNG rasterizations of the very shapes Google
|
|
78
|
+
* Slides can otherwise keep editable.
|
|
79
|
+
*/
|
|
80
|
+
buildPptx?: () => Promise<DeckPptxFile>,
|
|
40
81
|
): Promise<GoogleSlidesExportResult> {
|
|
41
|
-
const { blob, filename } = await
|
|
42
|
-
|
|
43
|
-
slides,
|
|
44
|
-
aspectRatio,
|
|
45
|
-
);
|
|
82
|
+
const { blob, filename } = await (buildPptx
|
|
83
|
+
? buildPptx()
|
|
84
|
+
: buildDeckPptxBlob(deckTitle, slides, aspectRatio));
|
|
46
85
|
|
|
47
86
|
const form = new FormData();
|
|
48
87
|
form.append("file", blob, filename);
|