- {borderLines.map((line) => (
-
- ))}
+ ) : (
+ borderLines.map((line) => (
+
+ ))
+ )}
{handlerVisible && (
<>
{resizeHandlers.map((point) => (
diff --git a/components/slide-renderer/Editor/Canvas/hooks/useInsertFromCreateSelection.ts b/components/slide-renderer/Editor/Canvas/hooks/useInsertFromCreateSelection.ts
index fb309ca67e..c10c2df352 100644
--- a/components/slide-renderer/Editor/Canvas/hooks/useInsertFromCreateSelection.ts
+++ b/components/slide-renderer/Editor/Canvas/hooks/useInsertFromCreateSelection.ts
@@ -1,11 +1,23 @@
import { useCallback, type RefObject } from 'react';
import { useCanvasStore } from '@/lib/store';
+import { createElementId } from '@/lib/edit/element-id';
+import { useCanvasOperations } from '@/lib/hooks/use-canvas-operations';
import type { CreateElementSelectionData } from '@/lib/types/edit';
+import type { PPTTextElement } from '@/lib/types/slides';
+
+// Click-fallback default size when the user clicks instead of drags (or wobbles
+// under this in either dimension): a sensibly-sized text box at the start point.
+const TEXT_CLICK_MIN = 24;
+const TEXT_DEFAULT_W = 300;
+const TEXT_DEFAULT_H = 60;
+// Empty centered paragraph — caret-ready, no placeholder text to delete.
+const EMPTY_TEXT_CONTENT = '
';
export function useInsertFromCreateSelection(viewportRef: RefObject
) {
const canvasScale = useCanvasStore.use.canvasScale();
const creatingElement = useCanvasStore.use.creatingElement();
const setCreatingElement = useCanvasStore.use.setCreatingElement();
+ const { addElement } = useCanvasOperations();
// Calculate selection position and size from the start and end points of mouse drag selection
const formatCreateSelection = useCallback(
@@ -74,7 +86,35 @@ export function useInsertFromCreateSelection(viewportRef: RefObject,
React.ComponentPropsWithoutRef
@@ -28,4 +30,4 @@ const PopoverContent = React.forwardRef<
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
-export { Popover, PopoverTrigger, PopoverContent };
+export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent };
diff --git a/configs/font.ts b/configs/font.ts
index 7dfc1416ab..387fab4fe2 100644
--- a/configs/font.ts
+++ b/configs/font.ts
@@ -1,31 +1,39 @@
-export const FONTS = [
- { label: '默认字体', value: '' },
- { label: '思源黑体', value: 'SourceHanSans' },
- { label: '思源宋体', value: 'SourceHanSerif' },
- { label: '文鼎PL楷体', value: 'WenDingPLKaiTi' },
- { label: '文鼎PL宋体', value: 'WenDingPLSongTi' },
- { label: '朱雀仿宋', value: 'ZhuqueFangSong' },
- { label: '霞鹜文楷', value: 'LXGWWenKai' },
- { label: '阿里巴巴普惠体', value: 'AlibabaPuHuiTi' },
- { label: 'MiSans', value: 'MiSans' },
- { label: '得意黑', value: 'DeYiHei' },
- { label: '仓耳小丸子', value: 'CangerXiaowanzi' },
- { label: '优设标题黑', value: 'YousheTitleBlack' },
- { label: '峰广明锐体', value: 'FengguangMingrui' },
- { label: '摄图摩登小方体', value: 'ShetuModernSquare' },
- { label: '站酷快乐体', value: 'ZcoolHappy' },
- { label: '字制区喜脉体', value: 'ZizhiQuXiMai' },
- { label: '素材集市康康体', value: 'SucaiJishiKangkang' },
- { label: '素材集市酷方体', value: 'SucaiJishiCoolSquare' },
- { label: '途牛类圆体', value: 'TuniuRounded' },
- { label: '锐字真言体', value: 'RuiziZhenyan' },
- { label: 'Source Serif 4', value: 'SourceSerif4' },
- { label: 'JetBrains Mono', value: 'JetBrainsMono' },
- { label: 'Literata', value: 'Literata' },
+/**
+ * Fonts offered in the slide editor's text-format picker.
+ *
+ * Every entry is a real web font: Inter via `next/font` (`app/layout.tsx`),
+ * the rest via `@fontsource` packages loaded in `app/editor-fonts.ts`.
+ * `@fontsource` `unicode-range`-subsets the CJK faces, so they download lazily
+ * per glyph range — a picked font actually renders.
+ *
+ * Adding a font: install its `@fontsource` package, import the weight CSS in
+ * `app/editor-fonts.ts`, then add an entry here whose `value` matches the
+ * package's `@font-face` family name.
+ */
+export interface FontEntry {
+ /** Display name; rendered as the fallback when `labelKey` is absent. */
+ readonly label: string;
+ /** CSS font-family value; "" means the element's own default (no override). */
+ readonly value: string;
+ /** Optional i18n key — preferred over `label` when present. */
+ readonly labelKey?: string;
+}
+
+export const FONTS: readonly FontEntry[] = [
+ { labelKey: 'edit.text.fontDefault', label: 'Default', value: '' },
+ // Chinese
+ { label: '思源黑体', value: 'Noto Sans SC' },
+ { label: '思源宋体', value: 'Noto Serif SC' },
+ { label: '霞鹜文楷', value: 'LXGW WenKai' },
+ { label: '站酷快乐体', value: 'ZCOOL KuaiLe' },
+ // Latin
{ label: 'Inter', value: 'Inter' },
{ label: 'Roboto', value: 'Roboto' },
- { label: 'Open Sans', value: 'OpenSans' },
+ { label: 'Open Sans', value: 'Open Sans' },
{ label: 'Montserrat', value: 'Montserrat' },
- { label: 'Source Sans Pro', value: 'SourceSansPro' },
+ { label: 'Source Sans 3', value: 'Source Sans 3' },
{ label: 'Merriweather', value: 'Merriweather' },
+ { label: 'Literata', value: 'Literata' },
+ { label: 'Source Serif 4', value: 'Source Serif 4' },
+ { label: 'JetBrains Mono', value: 'JetBrains Mono' },
];
diff --git a/lib/edit/scene-editor-surface.ts b/lib/edit/scene-editor-surface.ts
index 0b95ee41d1..c4838e64da 100644
--- a/lib/edit/scene-editor-surface.ts
+++ b/lib/edit/scene-editor-surface.ts
@@ -36,6 +36,12 @@ export interface InsertPaletteItem extends UiAffordance {
* "choose a shape" or "choose an image source".
*/
popoverContent?: () => ReactNode;
+ /**
+ * Whether the item is in an "armed" state — e.g. the surface is waiting for
+ * a canvas gesture to complete an insert. CommandBar renders this with the
+ * active/toggle style. Defaults to false.
+ */
+ active?: boolean;
}
/**
diff --git a/lib/i18n/locales/ar-SA.json b/lib/i18n/locales/ar-SA.json
index bd733173fb..d686f8bb73 100644
--- a/lib/i18n/locales/ar-SA.json
+++ b/lib/i18n/locales/ar-SA.json
@@ -174,6 +174,7 @@
"fontDefault": "افتراضي",
"sizeUp": "تكبير الحجم",
"sizeDown": "تصغير الحجم",
+ "fontSize": "حجم الخط",
"bold": "غامق",
"italic": "مائل",
"underline": "تسطير",
diff --git a/lib/i18n/locales/en-US.json b/lib/i18n/locales/en-US.json
index 7ba647389e..cb414a7171 100644
--- a/lib/i18n/locales/en-US.json
+++ b/lib/i18n/locales/en-US.json
@@ -174,6 +174,7 @@
"fontDefault": "Default",
"sizeUp": "Increase size",
"sizeDown": "Decrease size",
+ "fontSize": "Font size",
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
diff --git a/lib/i18n/locales/ja-JP.json b/lib/i18n/locales/ja-JP.json
index a4a767330b..920750d884 100644
--- a/lib/i18n/locales/ja-JP.json
+++ b/lib/i18n/locales/ja-JP.json
@@ -174,6 +174,7 @@
"fontDefault": "デフォルト",
"sizeUp": "文字を大きく",
"sizeDown": "文字を小さく",
+ "fontSize": "フォントサイズ",
"bold": "太字",
"italic": "斜体",
"underline": "下線",
diff --git a/lib/i18n/locales/ru-RU.json b/lib/i18n/locales/ru-RU.json
index 758ec71a87..0a9648d06e 100644
--- a/lib/i18n/locales/ru-RU.json
+++ b/lib/i18n/locales/ru-RU.json
@@ -174,6 +174,7 @@
"fontDefault": "По умолчанию",
"sizeUp": "Увеличить размер",
"sizeDown": "Уменьшить размер",
+ "fontSize": "Размер шрифта",
"bold": "Полужирный",
"italic": "Курсив",
"underline": "Подчёркнутый",
diff --git a/lib/i18n/locales/zh-CN.json b/lib/i18n/locales/zh-CN.json
index 74cecdac3d..4b4cfb4f99 100644
--- a/lib/i18n/locales/zh-CN.json
+++ b/lib/i18n/locales/zh-CN.json
@@ -174,6 +174,7 @@
"fontDefault": "默认",
"sizeUp": "增大字号",
"sizeDown": "减小字号",
+ "fontSize": "字号",
"bold": "加粗",
"italic": "斜体",
"underline": "下划线",
diff --git a/lib/i18n/locales/zh-TW.json b/lib/i18n/locales/zh-TW.json
index 1971409276..678f56f8cb 100644
--- a/lib/i18n/locales/zh-TW.json
+++ b/lib/i18n/locales/zh-TW.json
@@ -174,6 +174,7 @@
"fontDefault": "預設",
"sizeUp": "放大字級",
"sizeDown": "縮小字級",
+ "fontSize": "字級",
"bold": "粗體",
"italic": "斜體",
"underline": "底線",
diff --git a/lib/prosemirror/schema/marks.ts b/lib/prosemirror/schema/marks.ts
index 7e15f1123d..e80ba99ddf 100644
--- a/lib/prosemirror/schema/marks.ts
+++ b/lib/prosemirror/schema/marks.ts
@@ -134,7 +134,15 @@ const fontname: MarkSpec = {
toDOM: (mark) => {
const { fontname } = mark.attrs;
let style = '';
- if (fontname) style += `font-family: ${fontname};`;
+ // Quote the family name — unquoted, a name with spaces or a trailing digit
+ // (e.g. "Source Sans 3") is an invalid font-family value and gets dropped.
+ // parseDOM's getAttrs strips the quotes again, so the attr round-trips clean.
+ // Reject `"` or `\` (illegal in a CSS family name): rendering them unescaped
+ // here would let a hand-crafted mark close the quoted string and inject
+ // arbitrary CSS at `toDOM`.
+ if (fontname && !/["\\]/.test(fontname)) {
+ style += `font-family: "${fontname}";`;
+ }
return ['span', { style }, 0];
},
};
diff --git a/package.json b/package.json
index 2a9b58c1cf..72ff6a9741 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,18 @@
"@copilotkit/backend": "^0.37.0",
"@copilotkit/runtime": "^1.51.2",
"@fontsource-variable/inter": "^5.2.8",
+ "@fontsource/jetbrains-mono": "^5.2.8",
+ "@fontsource/literata": "^5.2.8",
+ "@fontsource/lxgw-wenkai": "^5.2.5",
+ "@fontsource/merriweather": "^5.2.11",
+ "@fontsource/montserrat": "^5.2.8",
+ "@fontsource/noto-sans-sc": "^5.2.9",
+ "@fontsource/noto-serif-sc": "^5.2.8",
+ "@fontsource/open-sans": "^5.2.7",
+ "@fontsource/roboto": "^5.2.10",
+ "@fontsource/source-sans-3": "^5.2.9",
+ "@fontsource/source-serif-4": "^5.2.9",
+ "@fontsource/zcool-kuaile": "^5.2.8",
"@langchain/core": "^1.1.16",
"@langchain/langgraph": "^1.1.1",
"@modelcontextprotocol/sdk": "^1.27.1",
@@ -84,6 +96,7 @@
"prosemirror-view": "^1.41.5",
"radix-ui": "^1.4.3",
"react": "19.2.3",
+ "react-colorful": "^5.7.0",
"react-dom": "19.2.3",
"react-i18next": "^17.0.1",
"shadcn": "^3.6.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2a3c254919..cfedc3d18d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -32,6 +32,42 @@ importers:
'@fontsource-variable/inter':
specifier: ^5.2.8
version: 5.2.8
+ '@fontsource/jetbrains-mono':
+ specifier: ^5.2.8
+ version: 5.2.8
+ '@fontsource/literata':
+ specifier: ^5.2.8
+ version: 5.2.8
+ '@fontsource/lxgw-wenkai':
+ specifier: ^5.2.5
+ version: 5.2.5
+ '@fontsource/merriweather':
+ specifier: ^5.2.11
+ version: 5.2.11
+ '@fontsource/montserrat':
+ specifier: ^5.2.8
+ version: 5.2.8
+ '@fontsource/noto-sans-sc':
+ specifier: ^5.2.9
+ version: 5.2.9
+ '@fontsource/noto-serif-sc':
+ specifier: ^5.2.8
+ version: 5.2.8
+ '@fontsource/open-sans':
+ specifier: ^5.2.7
+ version: 5.2.7
+ '@fontsource/roboto':
+ specifier: ^5.2.10
+ version: 5.2.10
+ '@fontsource/source-sans-3':
+ specifier: ^5.2.9
+ version: 5.2.9
+ '@fontsource/source-serif-4':
+ specifier: ^5.2.9
+ version: 5.2.9
+ '@fontsource/zcool-kuaile':
+ specifier: ^5.2.8
+ version: 5.2.8
'@langchain/core':
specifier: ^1.1.16
version: 1.1.31(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@4.3.6))
@@ -194,6 +230,9 @@ importers:
react:
specifier: 19.2.3
version: 19.2.3
+ react-colorful:
+ specifier: ^5.7.0
+ version: 5.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
react-dom:
specifier: 19.2.3
version: 19.2.3(react@19.2.3)
@@ -779,24 +818,28 @@ packages:
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@biomejs/cli-linux-arm64@1.9.4':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@biomejs/cli-linux-x64-musl@1.9.4':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@biomejs/cli-linux-x64@1.9.4':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@biomejs/cli-win32-arm64@1.9.4':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
@@ -1120,6 +1163,42 @@ packages:
'@fontsource-variable/inter@5.2.8':
resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==}
+ '@fontsource/jetbrains-mono@5.2.8':
+ resolution: {integrity: sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==}
+
+ '@fontsource/literata@5.2.8':
+ resolution: {integrity: sha512-Rj3eEQeu7+yBw9ZjXxcRiKDDrhbhpCyb0b79VJVCjDF1fdgJ8gDr9C1SRFLwfi+VaHEkhJDnRGm25fq1Yk+wNg==}
+
+ '@fontsource/lxgw-wenkai@5.2.5':
+ resolution: {integrity: sha512-lIBouZW7qNw3VdVnjc0NC80iDfxjU7OnaKGaz6QHiHjC8z0fv9MyrHpeBMzkPnyEVabcpUu4Wf8DnaihfGVYtw==}
+
+ '@fontsource/merriweather@5.2.11':
+ resolution: {integrity: sha512-ZiIMeUh5iT8d73o6xlSF8GKgjV5pgiFrufYc5jZTVAfExtWKqM2vQHnsqXSFMv4ELhAcjt6Vf+5T3oVGXhAizQ==}
+
+ '@fontsource/montserrat@5.2.8':
+ resolution: {integrity: sha512-xTjLxSbSfCycDB0pwmNsfNvdfWPaDaRQ2LC6yt/ZI7SdvXG52zHnzNYC/09mzuAuWNJyShkteutfCoDgym56hQ==}
+
+ '@fontsource/noto-sans-sc@5.2.9':
+ resolution: {integrity: sha512-bTUIWGBgJDpwi5qAr+x0/lcgv80IHTB9vl6s2f6EymZEa7qYV99yNRBZuKFT+SYDKVunZrjCEhWtpxqmbXWl5Q==}
+
+ '@fontsource/noto-serif-sc@5.2.8':
+ resolution: {integrity: sha512-C7fAr+d1GjOAw1qIbntsnqbA3l5dkdzcmNNgeCXLC8QZ7VNubL7MTrX8UcYKHceX4mI//z8gGtXbOeeQrB6P7g==}
+
+ '@fontsource/open-sans@5.2.7':
+ resolution: {integrity: sha512-8yfgDYjE5O0vmTPdrcjV35y4yMnctsokmi9gN49Gcsr0sjzkMkR97AnKDe6OqZh2SFkYlR28fxOvi21bYEgMSw==}
+
+ '@fontsource/roboto@5.2.10':
+ resolution: {integrity: sha512-8HlA5FtSfz//oFSr2eL7GFXAiE7eIkcGOtx7tjsLKq+as702x9+GU7K95iDeWFapHC4M2hv9RrpXKRTGGBI8Zg==}
+
+ '@fontsource/source-sans-3@5.2.9':
+ resolution: {integrity: sha512-u3ymIq4rfmCCyB9MEw/sFR5lPVJ1yTNXmIMbUz+9kVCFIHvNtfzXOEBuvkg3Tk0zhmioPeJ28ZK5smZ7TurezQ==}
+
+ '@fontsource/source-serif-4@5.2.9':
+ resolution: {integrity: sha512-er/Pym9emsEVJNf947umJ4kXarXfsiN6CN7kTYinefKRaHLwiquiiHOZvKvxWgkV8JMCf3pV3g0NcsPFpVCH9w==}
+
+ '@fontsource/zcool-kuaile@5.2.8':
+ resolution: {integrity: sha512-pIeFNRM0rwrJkO+TP7WG+KXvqy0YFRrPecNNzHTNOQLYhxrqN67/wzyx3fH+Ky63Y/Am5heB5XIzsEx/Io+P4w==}
+
'@google/generative-ai@0.11.5':
resolution: {integrity: sha512-DviMgrnljEKh6qkDT2pVFW+NEuVhggqBUoEnyy2PNL7l4ewxXRJubk3PctC9yPl1AdRIlhqP7E076QQt+IWuTg==}
engines: {node: '>=18.0.0'}
@@ -1246,89 +1325,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -1988,30 +2083,35 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@napi-rs/canvas-linux-arm64-musl@0.1.96':
resolution: {integrity: sha512-UvOi7fii3IE2KDfEfhh8m+LpzSRvhGK7o1eho99M2M0HTik11k3GX+2qgVx9EtujN3/bhFFS1kSO3+vPMaJ0Mg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.96':
resolution: {integrity: sha512-MBSukhGCQ5nRtf9NbFYWOU080yqkZU1PbuH4o1ROvB4CbPl12fchDR35tU83Wz8gWIM9JTn99lBn9DenPIv7Ig==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@napi-rs/canvas-linux-x64-gnu@0.1.96':
resolution: {integrity: sha512-I/ccu2SstyKiV3HIeVzyBIWfrJo8cN7+MSQZPnabewWV6hfJ2nY7Df2WqOHmobBRUw84uGR6zfQHsUEio/m5Vg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@napi-rs/canvas-linux-x64-musl@0.1.96':
resolution: {integrity: sha512-H3uov7qnTl73GDT4h52lAqpJPsl1tIUyNPWJyhQ6gHakohNqqRq3uf80+NEpzcytKGEOENP1wX3yGwZxhjiWEQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@napi-rs/canvas-win32-arm64-msvc@0.1.96':
resolution: {integrity: sha512-ATp6Y+djOjYtkfV/VRH7CZ8I1MEtkUQBmKUbuWw5zWEHHqfL0cEcInE4Cxgx7zkNAhEdBbnH8HMVrqNp+/gwxA==}
@@ -2061,24 +2161,28 @@ packages:
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-arm64-musl@16.1.2':
resolution: {integrity: sha512-Sn6LxPIZcADe5AnqqMCfwBv6vRtDikhtrjwhu+19WM6jHZe31JDRcGuPZAlJrDk6aEbNBPUUAKmySJELkBOesg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@next/swc-linux-x64-gnu@16.1.2':
resolution: {integrity: sha512-nwzesEQBfQIOOnQ7JArzB08w9qwvBQ7nC1i8gb0tiEFH94apzQM3IRpY19MlE8RBHxc9ArG26t1DEg2aaLaqVQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@next/swc-linux-x64-musl@16.1.2':
resolution: {integrity: sha512-s60bLf16BDoICQHeKEm0lDgUNMsL1UpQCkRNZk08ZNnRpK0QUV+6TvVHuBcIA7oItzU0m7kVmXe8QjXngYxJVA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@next/swc-win32-arm64-msvc@16.1.2':
resolution: {integrity: sha512-Sq8k4SZd8Y8EokKdz304TvMO9HoiwGzo0CTacaiN1bBtbJSQ1BIwKzNFeFdxOe93SHn1YGnKXG6Mq3N+tVooyQ==}
@@ -2909,36 +3013,42 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.9':
resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9':
resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9':
resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.9':
resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.9':
resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.9':
resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==}
@@ -3031,66 +3141,79 @@ packages:
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.59.0':
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.59.0':
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.59.0':
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.59.0':
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
cpu: [loong64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.59.0':
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
cpu: [ppc64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.59.0':
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.59.0':
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.59.0':
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.59.0':
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-openbsd-x64@4.59.0':
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
@@ -3249,24 +3372,28 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.1':
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.1':
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.1':
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.1':
resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
@@ -3572,41 +3699,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -6710,48 +6845,56 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-gnu@1.32.0:
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-arm64-musl@1.31.1:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-arm64-musl@1.32.0:
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-gnu@1.31.1:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-gnu@1.32.0:
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
lightningcss-linux-x64-musl@1.31.1:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-linux-x64-musl@1.32.0:
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
lightningcss-win32-arm64-msvc@1.31.1:
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
@@ -7913,6 +8056,12 @@ packages:
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
engines: {node: '>= 0.10'}
+ react-colorful@5.7.0:
+ resolution: {integrity: sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
react-dom@19.2.3:
resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
peerDependencies:
@@ -10387,6 +10536,30 @@ snapshots:
'@fontsource-variable/inter@5.2.8': {}
+ '@fontsource/jetbrains-mono@5.2.8': {}
+
+ '@fontsource/literata@5.2.8': {}
+
+ '@fontsource/lxgw-wenkai@5.2.5': {}
+
+ '@fontsource/merriweather@5.2.11': {}
+
+ '@fontsource/montserrat@5.2.8': {}
+
+ '@fontsource/noto-sans-sc@5.2.9': {}
+
+ '@fontsource/noto-serif-sc@5.2.8': {}
+
+ '@fontsource/open-sans@5.2.7': {}
+
+ '@fontsource/roboto@5.2.10': {}
+
+ '@fontsource/source-sans-3@5.2.9': {}
+
+ '@fontsource/source-serif-4@5.2.9': {}
+
+ '@fontsource/zcool-kuaile@5.2.8': {}
+
'@google/generative-ai@0.11.5': {}
'@graphql-tools/executor@1.5.1(graphql@16.13.1)':
@@ -17845,6 +18018,11 @@ snapshots:
iconv-lite: 0.7.2
unpipe: 1.0.0
+ react-colorful@5.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
+ dependencies:
+ react: 19.2.3
+ react-dom: 19.2.3(react@19.2.3)
+
react-dom@19.2.3(react@19.2.3):
dependencies:
react: 19.2.3
diff --git a/tests/edit/surfaces/slide/editing-state.test.ts b/tests/edit/surfaces/slide/editing-state.test.ts
new file mode 100644
index 0000000000..2fbc7783de
--- /dev/null
+++ b/tests/edit/surfaces/slide/editing-state.test.ts
@@ -0,0 +1,52 @@
+import { describe, expect, test } from 'vitest';
+import {
+ resolveEditingElementId,
+ resolveSelectedElement,
+} from '@/components/edit/surfaces/slide/editing-state';
+import {
+ createDefaultImageElement,
+ createDefaultTextElement,
+} from '@/lib/edit/slide-edit-elements';
+
+const text = createDefaultTextElement('t1');
+const image = createDefaultImageElement('i1', 'gen_img_x');
+
+describe('resolveSelectedElement', () => {
+ test('returns undefined when nothing is selected', () => {
+ expect(resolveSelectedElement([], [text])).toBeUndefined();
+ });
+
+ test('returns undefined for a multi-selection', () => {
+ expect(resolveSelectedElement(['t1', 'i1'], [text, image])).toBeUndefined();
+ });
+
+ test('returns undefined when the selected id is not found', () => {
+ expect(resolveSelectedElement(['ghost'], [text])).toBeUndefined();
+ });
+
+ test('returns the element for a single selection', () => {
+ expect(resolveSelectedElement(['i1'], [text, image])).toBe(image);
+ });
+});
+
+describe('resolveEditingElementId', () => {
+ test('returns "" when nothing is selected', () => {
+ expect(resolveEditingElementId([], [text])).toBe('');
+ });
+
+ test('returns "" for a multi-selection', () => {
+ expect(resolveEditingElementId(['t1', 'i1'], [text, image])).toBe('');
+ });
+
+ test('returns "" when the single selection is not a text element', () => {
+ expect(resolveEditingElementId(['i1'], [text, image])).toBe('');
+ });
+
+ test('returns "" when the selected id is not found', () => {
+ expect(resolveEditingElementId(['ghost'], [text])).toBe('');
+ });
+
+ test('returns the id when a single text element is selected', () => {
+ expect(resolveEditingElementId(['t1'], [text, image])).toBe('t1');
+ });
+});
diff --git a/tests/edit/surfaces/slide/insert-items.test.ts b/tests/edit/surfaces/slide/insert-items.test.ts
index af5cf6199b..c99c0732a5 100644
--- a/tests/edit/surfaces/slide/insert-items.test.ts
+++ b/tests/edit/surfaces/slide/insert-items.test.ts
@@ -1,93 +1,59 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
buildInsertItems,
- buildFloatingActions,
+ deleteSlideElement,
} from '@/components/edit/surfaces/slide/use-slide-surface';
import { useSlideEditSession } from '@/components/edit/surfaces/slide/slide-edit-session';
-import {
- createDefaultImageElement,
- createDefaultTextElement,
-} from '@/lib/edit/slide-edit-elements';
+import { useCanvasStore } from '@/lib/store/canvas';
+
+function seedEmptySlideSession() {
+ /* eslint-disable @typescript-eslint/no-explicit-any */
+ useSlideEditSession.setState({
+ history: {
+ past: [],
+ present: { type: 'slide', canvas: { id: 's', elements: [] } } as any,
+ future: [],
+ },
+ } as any);
+ /* eslint-enable @typescript-eslint/no-explicit-any */
+}
describe('slide insert palette', () => {
- beforeEach(() => {
- /* eslint-disable @typescript-eslint/no-explicit-any */
- useSlideEditSession.setState({
- history: {
- past: [],
- present: { type: 'slide', canvas: { id: 's', elements: [] } } as any,
- future: [],
- },
- } as any);
- /* eslint-enable @typescript-eslint/no-explicit-any */
- });
-
- afterEach(() => {
- vi.restoreAllMocks();
- });
+ beforeEach(seedEmptySlideSession);
+ afterEach(() => vi.restoreAllMocks());
it('exposes a text-box and an image insert item', () => {
- const items = buildInsertItems((k) => k);
+ const items = buildInsertItems((k) => k, undefined);
expect(items.map((i) => i.id)).toEqual(['insert-text', 'insert-image']);
expect(items[1].popoverContent).toBeTypeOf('function');
expect(items[0].onInvoke).toBeTypeOf('function');
});
- it('text-box invoke dispatches element.add with a text element', () => {
- const spy = vi.spyOn(useSlideEditSession.getState(), 'applyOp');
- buildInsertItems((k) => k)[0].onInvoke();
- expect(spy).toHaveBeenCalledWith(
- expect.objectContaining({
- type: 'element.add',
- element: expect.objectContaining({ type: 'text' }),
- }),
- );
- });
-
- it('no longer contributes a geometry floating action', () => {
- const actions = buildFloatingActions((k) => k, undefined);
- expect(actions.find((a) => a.id === 'geometry')).toBeUndefined();
- });
-});
-
-describe('slide floating actions', () => {
- beforeEach(() => {
- /* eslint-disable @typescript-eslint/no-explicit-any */
- useSlideEditSession.setState({
- history: {
- past: [],
- present: { type: 'slide', canvas: { id: 's', elements: [] } } as any,
- future: [],
- },
- } as any);
- /* eslint-enable @typescript-eslint/no-explicit-any */
+ it('text-box invoke arms text-insertion (sets creatingElement)', () => {
+ const spy = vi.spyOn(useCanvasStore.getState(), 'setCreatingElement');
+ buildInsertItems((k) => k, undefined)[0].onInvoke();
+ expect(spy).toHaveBeenCalledWith({ type: 'text' });
});
- afterEach(() => {
- vi.restoreAllMocks();
+ it('text-box invoke when already armed disarms (sets creatingElement to null)', () => {
+ const spy = vi.spyOn(useCanvasStore.getState(), 'setCreatingElement');
+ buildInsertItems((k) => k, 'text')[0].onInvoke();
+ expect(spy).toHaveBeenCalledWith(null);
});
- it('returns no actions when nothing is selected', () => {
- expect(buildFloatingActions((k) => k, undefined)).toEqual([]);
- });
-
- it('a selected text element gets the text-format bar plus a delete action', () => {
- const actions = buildFloatingActions((k) => k, createDefaultTextElement('text-9'));
- expect(actions.map((a) => a.id)).toEqual(['text-format', 'delete']);
+ it('text-box reports active when creating-text is armed', () => {
+ expect(buildInsertItems((k) => k, 'text')[0].active).toBe(true);
+ expect(buildInsertItems((k) => k, undefined)[0].active).toBe(false);
});
+});
- it('a selected image element gets only a delete action (no text-format)', () => {
- const actions = buildFloatingActions((k) => k, createDefaultImageElement('img-9', 'gen_img_x'));
- expect(actions.map((a) => a.id)).toEqual(['delete']);
- });
+describe('slide element deletion', () => {
+ beforeEach(seedEmptySlideSession);
+ afterEach(() => vi.restoreAllMocks());
- it('the delete action dispatches element.delete for the selected element', () => {
+ it('deleteSlideElement dispatches an element.delete op', () => {
const spy = vi.spyOn(useSlideEditSession.getState(), 'applyOp');
- const del = buildFloatingActions(
- (k) => k,
- createDefaultImageElement('img-9', 'gen_img_x'),
- ).find((a) => a.id === 'delete');
- del?.onInvoke?.();
+ deleteSlideElement('img-9');
expect(spy).toHaveBeenCalledWith({ type: 'element.delete', elementId: 'img-9' });
});
});
diff --git a/tests/edit/surfaces/slide/text-format-bar.test.ts b/tests/edit/surfaces/slide/text-format-bar.test.ts
index 4164e98e8e..6bf4831a2d 100644
--- a/tests/edit/surfaces/slide/text-format-bar.test.ts
+++ b/tests/edit/surfaces/slide/text-format-bar.test.ts
@@ -1,8 +1,6 @@
import { describe, it, expect, vi } from 'vitest';
import * as registry from '@/lib/prosemirror/active-editor-registry';
-import { stepFontSize } from '@/components/edit/surfaces/slide/text-format-bar';
-import { buildFloatingActions } from '@/components/edit/surfaces/slide/use-slide-surface';
-import type { PPTTextElement } from '@/lib/types/slides';
+import { currentFontLabel, stepFontSize } from '@/components/edit/surfaces/slide/text-format-bar';
describe('TextFormatBar — pure logic', () => {
it('stepFontSize increments and decrements by delta', () => {
@@ -23,6 +21,24 @@ describe('TextFormatBar — pure logic', () => {
});
});
+describe('TextFormatBar — currentFontLabel', () => {
+ const t = (k: string) => `T:${k}`;
+
+ it('returns the i18n label for the default (empty) font', () => {
+ expect(currentFontLabel('', t)).toBe('T:edit.text.fontDefault');
+ });
+
+ it("returns the registry entry's label for a matched font", () => {
+ expect(currentFontLabel('Roboto', t)).toBe('Roboto');
+ expect(currentFontLabel('Noto Sans SC', t)).toBe('思源黑体');
+ });
+
+ it('returns the raw family name for an unmatched legacy font', () => {
+ expect(currentFontLabel('Microsoft YaHei', t)).toBe('Microsoft YaHei');
+ expect(currentFontLabel('PingFang SC', t)).toBe('PingFang SC');
+ });
+});
+
describe('TextFormatBar — C1 integration (runActiveTextCommand)', () => {
it('runActiveTextCommand is callable for bold', () => {
const spy = vi.spyOn(registry, 'runActiveTextCommand').mockImplementation(() => {});
@@ -53,30 +69,3 @@ describe('TextFormatBar — C1 integration (runActiveTextCommand)', () => {
spy.mockRestore();
});
});
-
-describe('buildFloatingActions — text-format wiring', () => {
- const t = (k: string) => k;
-
- it('returns [] when no text target', () => {
- expect(buildFloatingActions(t, undefined)).toEqual([]);
- });
-
- it('leads with the text-format action when a text element is selected', () => {
- const textEl = { id: 'el-42', type: 'text' } as PPTTextElement;
- const actions = buildFloatingActions(t, textEl);
- expect(actions[0].id).toBe('text-format');
- });
-
- it('text-format action has a popoverContent factory', () => {
- const textEl = { id: 'el-42', type: 'text' } as PPTTextElement;
- const actions = buildFloatingActions(t, textEl);
- expect(typeof actions[0].popoverContent).toBe('function');
- });
-
- it('text-format action label and tooltip are i18n keys', () => {
- const textEl = { id: 'el-42', type: 'text' } as PPTTextElement;
- const actions = buildFloatingActions(t, textEl);
- expect(actions[0].label).toBe('edit.text.label');
- expect(actions[0].tooltip).toBe('edit.text.label');
- });
-});