@agent-native/core 0.178.0-nightly-20260909183543 → 0.178.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 (42) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +16 -0
  3. package/corpus/templates/calendar/CHANGELOG.md +19 -0
  4. package/corpus/templates/clips/CHANGELOG.md +6 -0
  5. package/corpus/templates/content/CHANGELOG.md +5 -0
  6. package/corpus/templates/content/app/i18n-data.ts +3 -3
  7. package/corpus/templates/content/app/lib/local-folder-picker-safety.ts +9 -0
  8. package/corpus/templates/content/app/routes/_app.local-files.tsx +5 -0
  9. package/corpus/templates/design/CHANGELOG.md +6 -0
  10. package/corpus/templates/factory/CHANGELOG.md +18 -0
  11. package/corpus/templates/mail/CHANGELOG.md +20 -0
  12. package/corpus/templates/plan/CHANGELOG.md +6 -0
  13. package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +36 -152
  14. package/corpus/templates/slides/CHANGELOG.md +11 -0
  15. package/corpus/templates/slides/actions/add-slide.ts +1 -0
  16. package/corpus/templates/slides/actions/create-deck.ts +1 -1
  17. package/corpus/templates/slides/actions/export-html.ts +342 -12
  18. package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +69 -20
  19. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +3 -2
  20. package/corpus/templates/slides/app/components/editor/SlideInlineEditor.tsx +49 -15
  21. package/corpus/templates/slides/app/global.css +27 -22
  22. package/corpus/templates/slides/app/hooks/use-deck-design-system.ts +19 -15
  23. package/corpus/templates/slides/app/lib/create-deck-generation.ts +2 -2
  24. package/corpus/templates/slides/app/lib/normalize-slide-padding.ts +2 -2
  25. package/corpus/templates/slides/app/pages/Index.tsx +2 -2
  26. package/corpus/templates/slides/changelog/2026-09-09-slides-default-generation-style.md +6 -0
  27. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -1
  28. package/corpus/templates/slides/shared/slide-background.ts +18 -2
  29. package/dist/cli/index.js +17 -0
  30. package/dist/client/org/OrgSwitcher.js +16 -21
  31. package/dist/collab/awareness.d.ts +2 -2
  32. package/dist/collab/routes.d.ts +1 -1
  33. package/dist/file-upload/actions/upload-image.d.ts +3 -3
  34. package/dist/observability/routes.d.ts +2 -2
  35. package/dist/org/federation.d.ts +1 -1
  36. package/dist/org/federation.js +6 -4
  37. package/dist/org/index.d.ts +2 -1
  38. package/dist/org/index.js +2 -1
  39. package/dist/org/membership.js +1 -1
  40. package/dist/org/workspace-app-access.d.ts +1 -0
  41. package/dist/org/workspace-app-access.js +21 -7
  42. package/package.json +3 -3
@@ -2,20 +2,31 @@ import fs from "fs";
2
2
  import path from "path";
3
3
 
4
4
  import { defineAction, fail } from "@agent-native/core/action";
5
+ import {
6
+ hydrateBuilderDesignSystemReference,
7
+ parseBuilderDesignSystemProxyReference,
8
+ } from "@agent-native/core/server";
5
9
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
6
10
  import { resolveAccess } from "@agent-native/core/sharing";
7
11
  import { z } from "zod";
8
12
 
9
13
  import "../server/db/index.js"; // ensure registerShareableResource runs
14
+ import { sanitizeCssValue } from "../app/lib/sanitize-slide-html.js";
10
15
  import {
11
16
  safeGeneratedFilename,
12
17
  tenantExportDir,
13
18
  } from "../server/lib/tenant-files.js";
19
+ import type { DesignSystemData } from "../shared/api.js";
14
20
  import {
15
21
  type AspectRatio,
16
22
  getAspectRatioDims,
17
23
  ASPECT_RATIO_VALUES,
18
24
  } from "../shared/aspect-ratios.js";
25
+ import {
26
+ backgroundCssValue,
27
+ DEFAULT_SLIDE_BACKGROUND,
28
+ resolveSlideBackground,
29
+ } from "../shared/slide-background.js";
19
30
 
20
31
  /**
21
32
  * Minimal server-side HTML sanitizer for exported slide content.
@@ -37,17 +48,254 @@ function sanitizeSlideContent(html: string): string {
37
48
  .replace(/\s+srcdoc\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, "");
38
49
  }
39
50
 
51
+ function safeCssToken(
52
+ value: unknown,
53
+ fallback: string,
54
+ builderTokenValues?: Record<string, string>,
55
+ ): string {
56
+ if (typeof value !== "string") return fallback;
57
+ const sanitized = sanitizeCssValue(value);
58
+ if (!sanitized) return fallback;
59
+ const resolved = sanitized
60
+ .replace(/[{}<>;]/g, "")
61
+ .replace(/var\(\s*(--[a-zA-Z][\w-]*)\s*\)/g, (_, name: string) => {
62
+ const replacement = builderTokenValues?.[name];
63
+ const safeReplacement = replacement
64
+ ? sanitizeCssValue(replacement)
65
+ : null;
66
+ return safeReplacement && !/[{}<>;]/.test(safeReplacement)
67
+ ? safeReplacement
68
+ : `var(${name})`;
69
+ })
70
+ .trim();
71
+ const finalValue = sanitizeCssValue(resolved)?.replace(/[{}<>;]/g, "");
72
+ return !finalValue || /var\(\s*--/i.test(finalValue)
73
+ ? fallback
74
+ : finalValue.slice(0, 240) || fallback;
75
+ }
76
+
77
+ const STANDALONE_TAILWIND_BACKGROUNDS: Record<string, string> = {
78
+ // guard:allow-raw-color - standalone Tailwind background compatibility
79
+ "bg-black": "#000000",
80
+ // guard:allow-raw-color - standalone Tailwind background compatibility
81
+ "bg-white": "#FFFFFF",
82
+ // guard:allow-raw-color - standalone Tailwind background compatibility
83
+ "bg-slate-900": "#0F172A",
84
+ // guard:allow-raw-color - standalone Tailwind background compatibility
85
+ "bg-slate-950": "#020617",
86
+ // guard:allow-raw-color - standalone Tailwind background compatibility
87
+ "bg-gray-900": "#111827",
88
+ // guard:allow-raw-color - standalone Tailwind background compatibility
89
+ "bg-zinc-900": "#18181B",
90
+ // guard:allow-raw-color - standalone Tailwind background compatibility
91
+ "bg-neutral-900": "#171717",
92
+ // guard:allow-raw-color - standalone Tailwind background compatibility
93
+ "bg-stone-900": "#1C1917",
94
+ // guard:allow-raw-color - standalone Tailwind background compatibility
95
+ "bg-red-500": "#EF4444",
96
+ // guard:allow-raw-color - standalone Tailwind background compatibility
97
+ "bg-orange-500": "#F97316",
98
+ // guard:allow-raw-color - standalone Tailwind background compatibility
99
+ "bg-amber-500": "#F59E0B",
100
+ // guard:allow-raw-color - standalone Tailwind background compatibility
101
+ "bg-yellow-400": "#FACC15",
102
+ // guard:allow-raw-color - standalone Tailwind background compatibility
103
+ "bg-lime-500": "#84CC16",
104
+ // guard:allow-raw-color - standalone Tailwind background compatibility
105
+ "bg-green-500": "#22C55E",
106
+ // guard:allow-raw-color - standalone Tailwind background compatibility
107
+ "bg-emerald-500": "#10B981",
108
+ // guard:allow-raw-color - standalone Tailwind background compatibility
109
+ "bg-teal-500": "#14B8A6",
110
+ // guard:allow-raw-color - standalone Tailwind background compatibility
111
+ "bg-cyan-500": "#06B6D4",
112
+ // guard:allow-raw-color - standalone Tailwind background compatibility
113
+ "bg-sky-500": "#0EA5E9",
114
+ // guard:allow-raw-color - standalone Tailwind background compatibility
115
+ "bg-blue-500": "#3B82F6",
116
+ // guard:allow-raw-color - standalone Tailwind background compatibility
117
+ "bg-indigo-500": "#6366F1",
118
+ // guard:allow-raw-color - standalone Tailwind background compatibility
119
+ "bg-indigo-950": "#1E1B4B",
120
+ // guard:allow-raw-color - standalone Tailwind background compatibility
121
+ "bg-violet-500": "#8B5CF6",
122
+ // guard:allow-raw-color - standalone Tailwind background compatibility
123
+ "bg-purple-600": "#9333EA",
124
+ // guard:allow-raw-color - standalone Tailwind background compatibility
125
+ "bg-fuchsia-500": "#D946EF",
126
+ // guard:allow-raw-color - standalone Tailwind background compatibility
127
+ "bg-pink-500": "#EC4899",
128
+ // guard:allow-raw-color - standalone Tailwind background compatibility
129
+ "bg-rose-500": "#F43F5E",
130
+ // guard:allow-raw-color - standalone Tailwind background compatibility
131
+ "bg-slate-700": "#334155",
132
+ // guard:allow-raw-color - standalone Tailwind background compatibility
133
+ "bg-slate-800": "#1E293B",
134
+ // guard:allow-raw-color - standalone Tailwind background compatibility
135
+ "bg-zinc-50": "#FAFAFA",
136
+ // guard:allow-raw-color - standalone Tailwind background compatibility
137
+ "bg-gray-100": "#F3F4F6",
138
+ };
139
+
140
+ const TAILWIND_GRADIENT_DIRECTIONS: Record<string, string> = {
141
+ "bg-gradient-to-t": "to top",
142
+ "bg-gradient-to-tr": "to top right",
143
+ "bg-gradient-to-r": "to right",
144
+ "bg-gradient-to-br": "to bottom right",
145
+ "bg-gradient-to-b": "to bottom",
146
+ "bg-gradient-to-bl": "to bottom left",
147
+ "bg-gradient-to-l": "to left",
148
+ "bg-gradient-to-tl": "to top left",
149
+ };
150
+
151
+ function standaloneBackgroundCssValue(value: string): string {
152
+ const cssValue = backgroundCssValue(value);
153
+ if (cssValue) return cssValue;
154
+ const classes = value.split(/\s+/);
155
+ const solidBackground = classes.find(
156
+ (className) => STANDALONE_TAILWIND_BACKGROUNDS[className],
157
+ );
158
+ if (solidBackground) return STANDALONE_TAILWIND_BACKGROUNDS[solidBackground];
159
+ const direction = classes.find(
160
+ (className) => TAILWIND_GRADIENT_DIRECTIONS[className],
161
+ );
162
+ if (direction) {
163
+ const stops = classes
164
+ .filter((className) => /^(?:from|via|to)-/.test(className))
165
+ .map((className) => {
166
+ const [, color, shade] =
167
+ className.match(/^(?:from|via|to)-([\w]+)-([\d]+)$/) ?? [];
168
+ return color && shade
169
+ ? STANDALONE_TAILWIND_BACKGROUNDS[`bg-${color}-${shade}`]
170
+ : undefined;
171
+ })
172
+ .filter((stop): stop is string => Boolean(stop));
173
+ if (stops.length >= 2) {
174
+ return `linear-gradient(${TAILWIND_GRADIENT_DIRECTIONS[direction]}, ${stops.join(", ")})`;
175
+ }
176
+ }
177
+ return STANDALONE_TAILWIND_BACKGROUNDS[value] ?? DEFAULT_SLIDE_BACKGROUND;
178
+ }
179
+
180
+ function isDarkStandaloneBackground(value: string): boolean {
181
+ const colors = [
182
+ ...value.matchAll(/#([\da-f]{3,8})\b/gi),
183
+ ...value.matchAll(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/gi),
184
+ ];
185
+ if (colors.length === 0) return false;
186
+ return colors.every((match) => {
187
+ const channels = match[0].startsWith("#")
188
+ ? match[1].length === 3 || match[1].length === 4
189
+ ? match[1]
190
+ .slice(0, 3)
191
+ .split("")
192
+ .map((channel) => parseInt(channel + channel, 16))
193
+ : [
194
+ match[1].slice(0, 2),
195
+ match[1].slice(2, 4),
196
+ match[1].slice(4, 6),
197
+ ].map((channel) => parseInt(channel, 16))
198
+ : match.slice(1, 4).map(Number);
199
+ return (
200
+ channels[0] * 0.299 + channels[1] * 0.587 + channels[2] * 0.114 < 128
201
+ );
202
+ });
203
+ }
204
+
205
+ function googleFontHref(font: unknown): string | undefined {
206
+ if (typeof font !== "string") return undefined;
207
+ const family = font.split(",", 1)[0]?.replace(/["']/g, "").trim();
208
+ const supported = [
209
+ "DM Sans",
210
+ "Inter",
211
+ "Manrope",
212
+ "Montserrat",
213
+ "Open Sans",
214
+ "Poppins",
215
+ "Plus Jakarta Sans",
216
+ "Roboto",
217
+ "Space Grotesk",
218
+ "Work Sans",
219
+ ];
220
+ if (
221
+ !family ||
222
+ !supported.some(
223
+ (candidate) => candidate.toLowerCase() === family.toLowerCase(),
224
+ )
225
+ ) {
226
+ return undefined;
227
+ }
228
+ return `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family).replace(/%20/g, "+")}:wght@400;700&display=swap`;
229
+ }
230
+
231
+ function standaloneDesignSystemVars(
232
+ designSystem: DesignSystemData | undefined,
233
+ slideBackground: string,
234
+ builderTokenValues?: Record<string, string>,
235
+ ): string {
236
+ const colors = designSystem?.colors;
237
+ const typography = designSystem?.typography;
238
+ const borders = designSystem?.borders;
239
+ const standaloneBackground = standaloneBackgroundCssValue(slideBackground);
240
+ const safeBackground = safeCssToken(
241
+ standaloneBackground,
242
+ DEFAULT_SLIDE_BACKGROUND,
243
+ builderTokenValues,
244
+ );
245
+ const darkBackground = isDarkStandaloneBackground(safeBackground);
246
+ return [
247
+ `--ds-bg: ${safeBackground}`,
248
+ // guard:allow-raw-color - standalone export fallback palette
249
+ // guard:allow-raw-color - standalone export dark-background fallback
250
+ `--ds-text: ${safeCssToken(colors?.text, darkBackground ? "#FFFFFF" : "#1F2933", builderTokenValues)}`,
251
+ // guard:allow-raw-color - standalone export fallback palette
252
+ // guard:allow-raw-color - standalone export dark-background fallback
253
+ `--ds-text-muted: ${safeCssToken(colors?.textMuted, darkBackground ? "rgba(255, 255, 255, 0.72)" : "#667085", builderTokenValues)}`,
254
+ // guard:allow-raw-color - standalone export fallback palette
255
+ `--ds-accent: ${safeCssToken(colors?.accent, "#2457D6", builderTokenValues)}`,
256
+ // guard:allow-raw-color - standalone export fallback palette
257
+ `--ds-primary: ${safeCssToken(colors?.primary, "#2457D6", builderTokenValues)}`,
258
+ // guard:allow-raw-color - standalone export fallback palette
259
+ `--ds-secondary: ${safeCssToken(colors?.secondary, "#C85C3A", builderTokenValues)}`,
260
+ // guard:allow-raw-color - standalone export fallback palette
261
+ `--ds-surface: ${safeCssToken(colors?.surface, "#FFFFFF", builderTokenValues)}`,
262
+ `--ds-heading-font: ${safeCssToken(typography?.headingFont, "Inter, sans-serif", builderTokenValues)}`,
263
+ `--ds-body-font: ${safeCssToken(typography?.bodyFont, "Inter, sans-serif", builderTokenValues)}`,
264
+ `--ds-radius: ${safeCssToken(borders?.radius, "14px", builderTokenValues)}`,
265
+ ...Object.entries(builderTokenValues ?? {})
266
+ .filter(
267
+ ([name, value]) =>
268
+ /^--[a-zA-Z][\w-]*$/.test(name) && typeof value === "string",
269
+ )
270
+ .map(
271
+ ([name, value]) =>
272
+ `${name}: ${safeCssToken(value, "initial", builderTokenValues)}`,
273
+ ),
274
+ ].join("; ");
275
+ }
276
+
40
277
  function buildStandaloneHtml(
41
278
  title: string,
42
- slides: Array<{ id: string; content: string; notes?: string }>,
279
+ slides: Array<{
280
+ id: string;
281
+ content: string;
282
+ notes?: string;
283
+ background?: string;
284
+ }>,
43
285
  aspectRatio?: AspectRatio,
286
+ designSystem?: DesignSystemData,
287
+ builderTokenValues?: Record<string, string>,
44
288
  ): string {
45
289
  const dims = getAspectRatioDims(aspectRatio);
46
290
  const slideHtmlSections = slides
47
- .map(
48
- (slide, i) =>
49
- `<section class="slide" data-index="${i}" style="display: ${i === 0 ? "flex" : "none"};">${sanitizeSlideContent(slide.content)}</section>`,
50
- )
291
+ .map((slide, i) => {
292
+ const slideBackground = resolveSlideBackground(
293
+ slide.background,
294
+ designSystem,
295
+ );
296
+ const style = `display: ${i === 0 ? "flex" : "none"}; background: ${safeCssToken(standaloneBackgroundCssValue(slideBackground), DEFAULT_SLIDE_BACKGROUND, builderTokenValues)}; ${standaloneDesignSystemVars(designSystem, slideBackground, builderTokenValues)}`;
297
+ return `<section class="slide" data-index="${i}" style="${escapeHtml(style)}">${sanitizeSlideContent(slide.content)}</section>`;
298
+ })
51
299
  .join("\n");
52
300
 
53
301
  return `<!DOCTYPE html>
@@ -56,9 +304,16 @@ function buildStandaloneHtml(
56
304
  <meta charset="UTF-8">
57
305
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
58
306
  <title>${escapeHtml(title)}</title>
59
- <!-- Self-hosted Poppins via Bunny Fonts CDN (privacy-respecting, no tracking) -->
60
- <link rel="preconnect" href="https://fonts.bunny.net">
61
- <link href="https://fonts.bunny.net/css?family=poppins:400,600,700,800,900&display=swap" rel="stylesheet">
307
+ ${[
308
+ googleFontHref(designSystem?.typography?.headingFont),
309
+ googleFontHref(designSystem?.typography?.bodyFont),
310
+ ]
311
+ .filter(
312
+ (href, index, all): href is string =>
313
+ Boolean(href) && all.indexOf(href) === index,
314
+ )
315
+ .map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}">`)
316
+ .join("\n ")}
62
317
  <style>
63
318
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
64
319
 
@@ -66,7 +321,7 @@ function buildStandaloneHtml(
66
321
  width: 100%; height: 100%;
67
322
  background: #111;
68
323
  overflow: hidden;
69
- font-family: 'Poppins', sans-serif;
324
+ font-family: 'Inter', sans-serif;
70
325
  }
71
326
 
72
327
  .viewport {
@@ -88,7 +343,9 @@ function buildStandaloneHtml(
88
343
  .slide {
89
344
  width: ${dims.width}px;
90
345
  height: ${dims.height}px;
91
- background: #000;
346
+ background: var(--ds-bg);
347
+ color: var(--ds-text);
348
+ font-family: var(--ds-body-font);
92
349
  overflow: hidden;
93
350
  position: absolute;
94
351
  top: 0;
@@ -106,6 +363,31 @@ function buildStandaloneHtml(
106
363
  width: 100%;
107
364
  height: 100%;
108
365
  box-sizing: border-box;
366
+ padding: 64px 80px;
367
+ display: flex;
368
+ flex-direction: column;
369
+ color: var(--ds-text);
370
+ background: var(--ds-bg);
371
+ font-family: var(--ds-body-font);
372
+ }
373
+
374
+ .fmd-slide[data-imported-pptx="true"], .fmd-slide[data-imported-pdf="true"] { padding: 0; }
375
+
376
+ .fmd-slide h1, .fmd-slide h2, .fmd-slide h3 {
377
+ color: var(--ds-text);
378
+ font-family: var(--ds-heading-font);
379
+ }
380
+
381
+ .fmd-slide h1 { font-size: 56px; line-height: 1.05; }
382
+ .fmd-slide h2 { font-size: 34px; line-height: 1.12; }
383
+ .fmd-slide h3 { font-size: 24px; line-height: 1.2; }
384
+ .fmd-slide p, .fmd-slide li { color: var(--ds-text-muted); }
385
+ .fmd-slide strong { color: var(--ds-text); }
386
+ .fmd-slide hr { border-color: var(--ds-accent); }
387
+
388
+ .fmd-slide .fmd-img-placeholder {
389
+ border-radius: var(--ds-radius);
390
+ background: var(--ds-surface);
109
391
  }
110
392
 
111
393
  .bottom-bar {
@@ -120,7 +402,7 @@ function buildStandaloneHtml(
120
402
  align-items: center;
121
403
  justify-content: space-between;
122
404
  padding: 0 20px;
123
- font-family: 'Poppins', sans-serif;
405
+ font-family: 'Inter', sans-serif;
124
406
  font-size: 13px;
125
407
  color: rgba(255, 255, 255, 0.5);
126
408
  z-index: 100;
@@ -258,6 +540,20 @@ function escapeHtml(str: string): string {
258
540
  .replace(/"/g, "&quot;");
259
541
  }
260
542
 
543
+ function parseStoredDesignSystem(
544
+ rawData: string,
545
+ ): DesignSystemData | undefined {
546
+ try {
547
+ const parsed = JSON.parse(rawData);
548
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
549
+ ? (parsed as DesignSystemData)
550
+ : undefined;
551
+ } catch (error) {
552
+ if (!(error instanceof SyntaxError)) throw error;
553
+ return undefined;
554
+ }
555
+ }
556
+
261
557
  export default defineAction({
262
558
  description:
263
559
  "Export a deck as a standalone HTML file with built-in keyboard navigation. Returns a download URL for the generated file.",
@@ -296,7 +592,41 @@ export default defineAction({
296
592
  });
297
593
  }
298
594
 
299
- const html = buildStandaloneHtml(row.title, slides, aspectRatio);
595
+ const designSystemId = row.designSystemId ?? deckData.designSystemId;
596
+ let designSystem: DesignSystemData | undefined;
597
+ let builderTokenValues: Record<string, string> | undefined;
598
+ if (typeof designSystemId === "string" && designSystemId.trim()) {
599
+ const designSystemAccess = await resolveAccess(
600
+ "design-system",
601
+ designSystemId,
602
+ );
603
+ const rawData = designSystemAccess?.resource?.data;
604
+ if (typeof rawData === "string") {
605
+ designSystem = parseStoredDesignSystem(rawData);
606
+ const builderReference =
607
+ parseBuilderDesignSystemProxyReference(rawData);
608
+ if (builderReference) {
609
+ try {
610
+ builderTokenValues = (
611
+ await hydrateBuilderDesignSystemReference(builderReference)
612
+ ).tokenValues;
613
+ } catch (error) {
614
+ console.warn(
615
+ "Builder design-system export hydration failed",
616
+ error,
617
+ );
618
+ }
619
+ }
620
+ }
621
+ }
622
+
623
+ const html = buildStandaloneHtml(
624
+ row.title,
625
+ slides,
626
+ aspectRatio,
627
+ designSystem,
628
+ builderTokenValues,
629
+ );
300
630
  const filename = safeGeneratedFilename(row.title, ".html");
301
631
 
302
632
  // Disk write is only useful when the same process can later serve the
@@ -23,6 +23,10 @@ import {
23
23
  } from "@/lib/sanitize-slide-html";
24
24
 
25
25
  import type { DesignSystemData } from "../../../shared/api";
26
+ import {
27
+ backgroundCssValue,
28
+ resolveSlideBackground,
29
+ } from "../../../shared/slide-background";
26
30
  import { ExcalidrawThumbnail, parseExcalidrawData } from "./ExcalidrawSlide";
27
31
  import { MermaidRenderer } from "./MermaidRenderer";
28
32
 
@@ -54,6 +58,29 @@ export const layoutClasses: Record<string, string> = {
54
58
  blank: "flex flex-col",
55
59
  };
56
60
 
61
+ function isDarkSlideBackground(value: string): boolean {
62
+ if (
63
+ /^(?:bg-black|bg-(?:slate|gray|zinc|neutral|stone)-(?:900|950))$/i.test(
64
+ value,
65
+ )
66
+ ) {
67
+ return true;
68
+ }
69
+ const match = value.match(/^#([\da-f]{3,8})$/i);
70
+ if (!match) return /(?:^|-)black(?:$|\s)/i.test(value);
71
+ const hex = match[1];
72
+ const channels =
73
+ hex.length === 3 || hex.length === 4
74
+ ? hex
75
+ .slice(0, 3)
76
+ .split("")
77
+ .map((channel) => parseInt(channel + channel, 16))
78
+ : [hex.slice(0, 2), hex.slice(2, 4), hex.slice(4, 6)].map((channel) =>
79
+ parseInt(channel, 16),
80
+ );
81
+ return channels[0] * 0.299 + channels[1] * 0.587 + channels[2] * 0.114 < 128;
82
+ }
83
+
57
84
  /** Custom image component that shows skeleton while loading */
58
85
  function LazyImage({
59
86
  src,
@@ -878,7 +905,9 @@ function BlankSlideContent({ content }: { content: string }) {
878
905
  if (mermaidBlocks.length > 0) {
879
906
  return (
880
907
  <div
881
- className="slide-content text-white/90 w-full block h-full"
908
+ className="slide-content w-full block h-full"
909
+ // guard:allow-raw-color - design-system text fallback for raw HTML
910
+ style={{ color: "var(--ds-text, #1f2933)" }}
882
911
  data-slide-content-scope={scopeId}
883
912
  >
884
913
  <MermaidHtmlContent
@@ -891,7 +920,9 @@ function BlankSlideContent({ content }: { content: string }) {
891
920
 
892
921
  return (
893
922
  <div
894
- className="slide-content text-white/90 w-full block h-full"
923
+ className="slide-content w-full block h-full"
924
+ // guard:allow-raw-color - design-system text fallback for raw HTML
925
+ style={{ color: "var(--ds-text, #1f2933)" }}
895
926
  data-slide-content-scope={scopeId}
896
927
  dangerouslySetInnerHTML={dangerousHtml}
897
928
  />
@@ -962,25 +993,43 @@ export function SlideInner({
962
993
  height: dims.height,
963
994
  };
964
995
 
965
- const bg = slide.background || "bg-[#000000]";
996
+ const bg = resolveSlideBackground(slide.background, designSystem);
966
997
  const isGradientClass = bg.startsWith("bg-");
967
- const safeBackground = !isGradientClass ? sanitizeCssValue(bg) : null;
998
+ const cssBackground = isGradientClass ? backgroundCssValue(bg) : bg;
999
+ const safeBackground = cssBackground ? sanitizeCssValue(cssBackground) : null;
968
1000
  const bgStyle = safeBackground ? { background: safeBackground } : undefined;
969
1001
  const bgClass = isGradientClass ? bg : "";
970
1002
  const isCentered = slide.layout === "title";
971
-
972
- const dsStyle = designSystem
973
- ? ({
974
- "--ds-accent": designSystem.colors.accent,
975
- "--ds-bg": designSystem.colors.background,
976
- "--ds-text": designSystem.colors.text,
977
- "--ds-text-muted": designSystem.colors.textMuted,
978
- "--ds-heading-font": designSystem.typography.headingFont,
979
- "--ds-body-font": designSystem.typography.bodyFont,
980
- "--ds-primary": designSystem.colors.primary,
981
- "--ds-radius": designSystem.borders.radius,
982
- } as React.CSSProperties)
983
- : {};
1003
+ const darkSlide = isDarkSlideBackground(safeBackground ?? bg);
1004
+
1005
+ const dsStyle = {
1006
+ "--ds-bg": safeBackground ?? "transparent",
1007
+ ...(designSystem
1008
+ ? {
1009
+ "--ds-accent": designSystem.colors.accent,
1010
+ "--ds-text": designSystem.colors.text,
1011
+ "--ds-text-muted": designSystem.colors.textMuted,
1012
+ "--ds-heading-font": designSystem.typography.headingFont,
1013
+ "--ds-body-font": designSystem.typography.bodyFont,
1014
+ "--ds-primary": designSystem.colors.primary,
1015
+ "--ds-secondary": designSystem.colors.secondary,
1016
+ // guard:allow-raw-color - safe placeholder surface fallback
1017
+ "--ds-surface":
1018
+ // guard:allow-raw-color - safe placeholder surface fallback
1019
+ sanitizeCssValue(designSystem.colors.surface) ?? "#FFFFFF",
1020
+ "--ds-radius": designSystem.borders.radius,
1021
+ }
1022
+ : {}),
1023
+ } as React.CSSProperties & Record<string, string>;
1024
+ if (
1025
+ darkSlide &&
1026
+ (!designSystem || isDarkSlideBackground(designSystem.colors.text))
1027
+ ) {
1028
+ // guard:allow-raw-color - readable Markdown defaults on explicit dark slides
1029
+ dsStyle["--ds-text"] = "#FFFFFF";
1030
+ // guard:allow-raw-color - readable Markdown defaults on explicit dark slides
1031
+ dsStyle["--ds-text-muted"] = "rgba(255, 255, 255, 0.72)";
1032
+ }
984
1033
 
985
1034
  const overflowByTargetRef = useRef(new Map<string, SlideOverflowInfo>());
986
1035
  const reportTargetOverflow = useCallback(
@@ -1119,7 +1168,7 @@ export function SlideInner({
1119
1168
  canvasWidth={dims.width}
1120
1169
  canvasHeight={dims.height}
1121
1170
  fitKey={`${slide.layoutFitRevision ?? ""}:${left}`}
1122
- className="slide-content text-white/90"
1171
+ className="slide-content"
1123
1172
  {...(slideDeclaresTextColor(left)
1124
1173
  ? { contentScope: AUTHORED_COLOR_SCOPE }
1125
1174
  : {})}
@@ -1137,7 +1186,7 @@ export function SlideInner({
1137
1186
  canvasWidth={dims.width}
1138
1187
  canvasHeight={dims.height}
1139
1188
  fitKey={`${slide.layoutFitRevision ?? ""}:${right}`}
1140
- className="slide-content text-white/90"
1189
+ className="slide-content"
1141
1190
  {...(slideDeclaresTextColor(right)
1142
1191
  ? { contentScope: AUTHORED_COLOR_SCOPE }
1143
1192
  : {})}
@@ -1192,7 +1241,7 @@ export function SlideInner({
1192
1241
  canvasWidth={dims.width}
1193
1242
  canvasHeight={dims.height}
1194
1243
  fitKey={`${slide.layoutFitRevision ?? ""}:${content}`}
1195
- className="slide-content text-white/90 w-full"
1244
+ className="slide-content w-full"
1196
1245
  {...(slideDeclaresTextColor(content)
1197
1246
  ? { contentScope: AUTHORED_COLOR_SCOPE }
1198
1247
  : {})}
@@ -4492,7 +4492,8 @@ export default function SlideEditor({
4492
4492
  if (dragSized) box.style.height = `${geometry.height}px`;
4493
4493
  box.style.fontSize = "24px";
4494
4494
  box.style.color = getSlideTextBoxDefaultColor(target, positioningLayer);
4495
- box.style.fontFamily = "'Poppins', sans-serif";
4495
+ box.style.fontFamily =
4496
+ designSystem?.typography.bodyFont ?? "Inter, sans-serif";
4496
4497
  box.style.lineHeight = "1.3";
4497
4498
  if (text !== ZERO_WIDTH_SPACE) {
4498
4499
  box.style.whiteSpace = "pre-wrap";
@@ -4505,7 +4506,7 @@ export default function SlideEditor({
4505
4506
  enterInlineEdit(box);
4506
4507
  return box;
4507
4508
  },
4508
- [enterInlineEdit],
4509
+ [designSystem?.typography.bodyFont, enterInlineEdit],
4509
4510
  );
4510
4511
 
4511
4512
  const pastePlainTextAsTextBox = useCallback(