@agent-native/core 0.137.1 → 0.137.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +258 -0
- package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +130 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +25 -337
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +73 -53
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +804 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +101 -45
- package/corpus/templates/slides/app/components/editor/SlideOverflowWarning.tsx +7 -4
- package/corpus/templates/slides/app/components/editor/bullet-editing.ts +11 -3
- package/corpus/templates/slides/app/components/editor/commit-active-edit.ts +21 -0
- package/corpus/templates/slides/app/components/editor/list-editing.ts +219 -0
- package/corpus/templates/slides/app/components/editor/selection-overlay-measurement.ts +0 -3
- package/corpus/templates/slides/app/components/editor/slide-style.ts +199 -0
- package/corpus/templates/slides/app/global.css +14 -3
- package/corpus/templates/slides/app/i18n/en-US.ts +8 -1
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +48 -22
- package/corpus/templates/slides/changelog/2026-08-01-add-slide-moved-to-the-toolbar-and-the-slide-rail-is-now-mor.md +9 -0
- package/corpus/templates/slides/changelog/2026-08-01-add-slide-undo-redo-and-the-text-tool-now-lead-the-slide-too.md +9 -0
- package/corpus/templates/slides/changelog/2026-08-01-slide-styling-now-appears-in-a-contextual-toolbar-above-the-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-01-the-slide-style-side-panel-is-retired-all-styling-now-lives-.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-03-the-slide-toolbar-is-denser-swatch-only-colors-dropdowns-for.md +10 -0
- package/corpus/templates/slides/changelog/2026-08-04-bullet-and-numbered-list-buttons-in-the-slide-toolbar-conver.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-04-italic-and-underline-are-now-one-click-away-in-the-slide-too.md +6 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
- package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +0 -763
package/corpus/README.md
CHANGED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { appBasePath } from "@agent-native/core/client/api-path";
|
|
2
|
+
import { PromptComposer } from "@agent-native/core/client/composer";
|
|
3
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
4
|
+
import { IconCopy, IconSquarePlus } from "@tabler/icons-react";
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { createPortal } from "react-dom";
|
|
7
|
+
import { toast } from "sonner";
|
|
8
|
+
|
|
9
|
+
import { GoogleDocImportHint } from "@/components/editor/GoogleDocImportHint";
|
|
10
|
+
import {
|
|
11
|
+
isInsidePortaledLayer,
|
|
12
|
+
type UploadedFile,
|
|
13
|
+
} from "@/components/editor/PromptDialog";
|
|
14
|
+
import { addSlideAgentMessage } from "@/lib/agent-visible-message";
|
|
15
|
+
|
|
16
|
+
const MAX_SOURCE_CONTEXT_CHARS = 60_000;
|
|
17
|
+
|
|
18
|
+
function truncateSourceForContext(prompt: string): {
|
|
19
|
+
text: string;
|
|
20
|
+
truncated: boolean;
|
|
21
|
+
} {
|
|
22
|
+
if (prompt.length <= MAX_SOURCE_CONTEXT_CHARS) {
|
|
23
|
+
return { text: prompt, truncated: false };
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
text: prompt.slice(0, MAX_SOURCE_CONTEXT_CHARS),
|
|
27
|
+
truncated: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function describeUploadedFilesForAgent(
|
|
32
|
+
files: UploadedFile[],
|
|
33
|
+
deckId: string,
|
|
34
|
+
): string {
|
|
35
|
+
if (files.length === 0) return "";
|
|
36
|
+
const fileList = files
|
|
37
|
+
.map(
|
|
38
|
+
(f) =>
|
|
39
|
+
`- ${f.originalName} (${f.type}, ${(f.size / 1024).toFixed(1)}KB) at path: ${f.path}${f.url ? `; embeddable URL: ${f.url}` : ""}`,
|
|
40
|
+
)
|
|
41
|
+
.join("\n");
|
|
42
|
+
return [
|
|
43
|
+
"",
|
|
44
|
+
`The user uploaded ${files.length} file(s). These paths are real uploaded files; process them with import actions before using their contents:`,
|
|
45
|
+
fileList,
|
|
46
|
+
"",
|
|
47
|
+
"File handling rules:",
|
|
48
|
+
`- PPTX files: call \`import-pptx --filePath "<path>" --deckId ${deckId}\` when the user wants the deck/slides imported, or to extract slide source from a presentation.`,
|
|
49
|
+
`- PDF and DOCX files: call \`import-file --filePath "<path>" --format auto --deckId ${deckId}\` and use the returned extracted text as source material. The returned text is capped for reliability; re-run with maxChars only if more context is needed.`,
|
|
50
|
+
"- Text-like files: use the uploaded-text-file blocks already included in the prompt; do not call import-file for them.",
|
|
51
|
+
'- Image files with an embeddable URL can be inserted directly into slide HTML as `<img src="...">` or used as visual references.',
|
|
52
|
+
"- Image files without a URL are visual/reference assets only; do not claim to have processed a PPTX/PDF/DOCX unless the relevant import action succeeds.",
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
export function AddSlidePopover({
|
|
56
|
+
open,
|
|
57
|
+
onOpenChange,
|
|
58
|
+
anchorRef,
|
|
59
|
+
deckId,
|
|
60
|
+
deckTitle,
|
|
61
|
+
activeSlideId,
|
|
62
|
+
slideCount,
|
|
63
|
+
activeSlideIndex,
|
|
64
|
+
agentSubmit,
|
|
65
|
+
onDuplicateCurrent,
|
|
66
|
+
onAddEmpty,
|
|
67
|
+
}: {
|
|
68
|
+
open: boolean;
|
|
69
|
+
onOpenChange: (open: boolean) => void;
|
|
70
|
+
anchorRef: React.RefObject<HTMLElement | null>;
|
|
71
|
+
deckId: string;
|
|
72
|
+
deckTitle: string;
|
|
73
|
+
activeSlideId: string;
|
|
74
|
+
slideCount: number;
|
|
75
|
+
activeSlideIndex: number;
|
|
76
|
+
agentSubmit: (message: string, context: string) => void;
|
|
77
|
+
onDuplicateCurrent?: () => void;
|
|
78
|
+
onAddEmpty?: () => void;
|
|
79
|
+
}) {
|
|
80
|
+
const t = useT();
|
|
81
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
82
|
+
const [promptText, setPromptText] = useState("");
|
|
83
|
+
const [googleDocContext, setGoogleDocContext] = useState("");
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!open) return;
|
|
87
|
+
const handleClick = (e: MouseEvent) => {
|
|
88
|
+
if (isInsidePortaledLayer(e.target)) return;
|
|
89
|
+
if (
|
|
90
|
+
panelRef.current &&
|
|
91
|
+
!panelRef.current.contains(e.target as Node) &&
|
|
92
|
+
anchorRef.current &&
|
|
93
|
+
!anchorRef.current.contains(e.target as Node)
|
|
94
|
+
) {
|
|
95
|
+
onOpenChange(false);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
99
|
+
if (e.key === "Escape") onOpenChange(false);
|
|
100
|
+
};
|
|
101
|
+
document.addEventListener("mousedown", handleClick);
|
|
102
|
+
document.addEventListener("keydown", handleKey);
|
|
103
|
+
return () => {
|
|
104
|
+
document.removeEventListener("mousedown", handleClick);
|
|
105
|
+
document.removeEventListener("keydown", handleKey);
|
|
106
|
+
};
|
|
107
|
+
}, [open, onOpenChange, anchorRef]);
|
|
108
|
+
|
|
109
|
+
const handleSubmit = useCallback(
|
|
110
|
+
async (text: string, files: File[]) => {
|
|
111
|
+
let uploaded: UploadedFile[] = [];
|
|
112
|
+
if (files.length > 0) {
|
|
113
|
+
try {
|
|
114
|
+
const formData = new FormData();
|
|
115
|
+
files.forEach((f) => formData.append("files", f));
|
|
116
|
+
const res = await fetch(`${appBasePath()}/api/uploads`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
body: formData,
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok) {
|
|
121
|
+
// coercion-ok: the request already failed; an unparseable error
|
|
122
|
+
// body just falls back to the generic upload-failed message.
|
|
123
|
+
const data = await res.json().catch(() => null);
|
|
124
|
+
throw new Error(data?.error || t("editorSidebar.uploadFailed"));
|
|
125
|
+
}
|
|
126
|
+
uploaded = (await res.json()) as UploadedFile[];
|
|
127
|
+
} catch (error) {
|
|
128
|
+
toast.error(t("editorSidebar.uploadFailed"), {
|
|
129
|
+
description:
|
|
130
|
+
error instanceof Error
|
|
131
|
+
? error.message
|
|
132
|
+
: t("editorSidebar.uploadAttachedFileFailed"),
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const trimmedText = text.trim();
|
|
139
|
+
const googleDocSourceForContext =
|
|
140
|
+
truncateSourceForContext(googleDocContext);
|
|
141
|
+
const fileContext = describeUploadedFilesForAgent(uploaded, deckId);
|
|
142
|
+
const context = [
|
|
143
|
+
`Add a new slide to deck "${deckTitle}" (id: ${deckId}).`,
|
|
144
|
+
`Insert after slide ${activeSlideIndex + 1} of ${slideCount} (active slide id: ${activeSlideId}).`,
|
|
145
|
+
"The visible user message above contains the user's request and/or pasted source material for the new slide(s). Treat pasted memo content as source material even if the user did not explicitly say they are pasting it.",
|
|
146
|
+
googleDocSourceForContext.text,
|
|
147
|
+
googleDocSourceForContext.truncated
|
|
148
|
+
? `The pasted source was longer than ${MAX_SOURCE_CONTEXT_CHARS} characters, so only the first ${MAX_SOURCE_CONTEXT_CHARS} characters were included to keep the agent request reliable.`
|
|
149
|
+
: "",
|
|
150
|
+
fileContext,
|
|
151
|
+
"",
|
|
152
|
+
"Create the slide content and insert it at the correct position using `add-slide` with --deckId=" +
|
|
153
|
+
deckId +
|
|
154
|
+
".",
|
|
155
|
+
"Every slide is rendered into a fixed native canvas (default 16:9 is 960x540 CSS pixels). Keep each slide within the density limits in AGENTS.md; split dense source material across more slides instead of packing it tightly.",
|
|
156
|
+
"If the user asked for multiple slides, call `add-slide` once per slide. Use positions starting at " +
|
|
157
|
+
(activeSlideIndex + 1) +
|
|
158
|
+
" so the new slides land after the active slide in order.",
|
|
159
|
+
"For larger requests, keep adding slides sequentially: wait for each add-slide result, then call add-slide for the next slide. Start slide 1 immediately; do not wait to design the entire sequence before adding it.",
|
|
160
|
+
].join("\n");
|
|
161
|
+
|
|
162
|
+
agentSubmit(addSlideAgentMessage(trimmedText), context);
|
|
163
|
+
onOpenChange(false);
|
|
164
|
+
},
|
|
165
|
+
[
|
|
166
|
+
activeSlideId,
|
|
167
|
+
activeSlideIndex,
|
|
168
|
+
agentSubmit,
|
|
169
|
+
deckId,
|
|
170
|
+
deckTitle,
|
|
171
|
+
googleDocContext,
|
|
172
|
+
onOpenChange,
|
|
173
|
+
slideCount,
|
|
174
|
+
],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!open) {
|
|
179
|
+
setPromptText("");
|
|
180
|
+
setGoogleDocContext("");
|
|
181
|
+
}
|
|
182
|
+
}, [open]);
|
|
183
|
+
|
|
184
|
+
if (!open || !anchorRef.current) return null;
|
|
185
|
+
|
|
186
|
+
const rect = anchorRef.current.getBoundingClientRect();
|
|
187
|
+
const panelWidth = Math.min(420, window.innerWidth - 24);
|
|
188
|
+
const left = Math.max(
|
|
189
|
+
12,
|
|
190
|
+
Math.min(rect.left, window.innerWidth - panelWidth - 12),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return createPortal(
|
|
194
|
+
<div
|
|
195
|
+
ref={panelRef}
|
|
196
|
+
className="fixed w-[min(420px,calc(100vw-24px))] rounded-xl border border-border bg-popover shadow-2xl shadow-black/60 z-[200] p-3"
|
|
197
|
+
style={{
|
|
198
|
+
top: rect.bottom + 8,
|
|
199
|
+
left,
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<p className="px-1 pb-2 text-sm font-medium text-foreground/90">
|
|
203
|
+
{t("editorSidebar.addSlides")}
|
|
204
|
+
</p>
|
|
205
|
+
{(onAddEmpty || (onDuplicateCurrent && slideCount > 0)) && (
|
|
206
|
+
<>
|
|
207
|
+
{onAddEmpty && (
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
onClick={() => {
|
|
211
|
+
onAddEmpty();
|
|
212
|
+
onOpenChange(false);
|
|
213
|
+
}}
|
|
214
|
+
className="w-full mb-1 px-2.5 py-2 text-left text-sm rounded-md hover:bg-accent transition-colors flex items-center gap-2 text-foreground/90 cursor-pointer"
|
|
215
|
+
>
|
|
216
|
+
<IconSquarePlus className="w-4 h-4 text-muted-foreground" />
|
|
217
|
+
<span>{t("editorSidebar.addEmptySlide")}</span>
|
|
218
|
+
<span className="ml-auto text-[11px] text-muted-foreground">
|
|
219
|
+
{t("editorSidebar.noAi")}
|
|
220
|
+
</span>
|
|
221
|
+
</button>
|
|
222
|
+
)}
|
|
223
|
+
{onDuplicateCurrent && slideCount > 0 && (
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onClick={() => {
|
|
227
|
+
onDuplicateCurrent();
|
|
228
|
+
onOpenChange(false);
|
|
229
|
+
}}
|
|
230
|
+
className="w-full mb-2 px-2.5 py-2 text-left text-sm rounded-md hover:bg-accent transition-colors flex items-center gap-2 text-foreground/90 cursor-pointer"
|
|
231
|
+
>
|
|
232
|
+
<IconCopy className="w-4 h-4 text-muted-foreground" />
|
|
233
|
+
<span>{t("editorSidebar.duplicateCurrentSlide")}</span>
|
|
234
|
+
<span className="ml-auto text-[11px] text-muted-foreground">
|
|
235
|
+
{t("editorSidebar.noAi")}
|
|
236
|
+
</span>
|
|
237
|
+
</button>
|
|
238
|
+
)}
|
|
239
|
+
<div className="-mx-3 mb-2 h-px bg-border" />
|
|
240
|
+
</>
|
|
241
|
+
)}
|
|
242
|
+
<PromptComposer
|
|
243
|
+
autoFocus
|
|
244
|
+
placeholder={t("editorSidebar.promptPlaceholder")}
|
|
245
|
+
draftScope={`slides:add-slide:${deckId}`}
|
|
246
|
+
onSubmit={handleSubmit}
|
|
247
|
+
onTextChange={setPromptText}
|
|
248
|
+
/>
|
|
249
|
+
<div className="-mx-1 mt-2">
|
|
250
|
+
<GoogleDocImportHint
|
|
251
|
+
promptText={promptText}
|
|
252
|
+
onSourceContextChange={setGoogleDocContext}
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
</div>,
|
|
256
|
+
document.body,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
+
import { IconLoader2, IconPlus, IconTextSize } from "@tabler/icons-react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Tooltip,
|
|
7
|
+
TooltipContent,
|
|
8
|
+
TooltipTrigger,
|
|
9
|
+
} from "@/components/ui/tooltip";
|
|
10
|
+
import { useAgentGenerating } from "@/hooks/use-agent-generating";
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
|
|
13
|
+
import { AddSlidePopover } from "./AddSlidePopover";
|
|
14
|
+
|
|
15
|
+
const BUTTON_CLASS =
|
|
16
|
+
"inline-flex size-7 flex-shrink-0 items-center justify-center rounded-md transition-colors";
|
|
17
|
+
const IDLE_CLASS =
|
|
18
|
+
"text-muted-foreground hover:bg-accent hover:text-foreground/70";
|
|
19
|
+
const ACTIVE_CLASS = "bg-accent text-foreground";
|
|
20
|
+
const DIVIDER_CLASS = "mx-1 h-4 w-px shrink-0 bg-border";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Add slide, undo, redo, and add-text-box — the actions that stay put
|
|
24
|
+
* regardless of what is selected. Rendered at the head of the contextual
|
|
25
|
+
* toolbar, and as a fallback in the deck toolbar where that row is hidden.
|
|
26
|
+
*/
|
|
27
|
+
export function EditorActionCluster({
|
|
28
|
+
deckId,
|
|
29
|
+
deckTitle,
|
|
30
|
+
currentSlideId,
|
|
31
|
+
slideCount,
|
|
32
|
+
currentSlideIndex,
|
|
33
|
+
addSlideGenerating = false,
|
|
34
|
+
onAddSlideGeneratingChange,
|
|
35
|
+
onAddEmptySlide,
|
|
36
|
+
onDuplicateCurrentSlide,
|
|
37
|
+
textBoxMode,
|
|
38
|
+
onToggleTextBoxMode,
|
|
39
|
+
className,
|
|
40
|
+
}: {
|
|
41
|
+
deckId: string;
|
|
42
|
+
deckTitle: string;
|
|
43
|
+
currentSlideId?: string;
|
|
44
|
+
slideCount: number;
|
|
45
|
+
currentSlideIndex: number;
|
|
46
|
+
addSlideGenerating?: boolean;
|
|
47
|
+
onAddSlideGeneratingChange?: (generating: boolean) => void;
|
|
48
|
+
onAddEmptySlide?: () => void;
|
|
49
|
+
onDuplicateCurrentSlide?: () => void;
|
|
50
|
+
textBoxMode?: boolean;
|
|
51
|
+
onToggleTextBoxMode?: () => void;
|
|
52
|
+
className?: string;
|
|
53
|
+
}) {
|
|
54
|
+
const t = useT();
|
|
55
|
+
const { generating, submit: agentSubmit } = useAgentGenerating();
|
|
56
|
+
const [addSlideOpen, setAddSlideOpen] = useState(false);
|
|
57
|
+
const addSlideRef = useRef<HTMLButtonElement>(null);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!generating) onAddSlideGeneratingChange?.(false);
|
|
61
|
+
}, [generating, onAddSlideGeneratingChange]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className={cn("flex items-center gap-1", className)}>
|
|
65
|
+
<Tooltip>
|
|
66
|
+
<TooltipTrigger asChild>
|
|
67
|
+
<button
|
|
68
|
+
ref={addSlideRef}
|
|
69
|
+
type="button"
|
|
70
|
+
onClick={() => setAddSlideOpen((open) => !open)}
|
|
71
|
+
disabled={addSlideGenerating}
|
|
72
|
+
className={cn(
|
|
73
|
+
BUTTON_CLASS,
|
|
74
|
+
addSlideOpen ? ACTIVE_CLASS : IDLE_CLASS,
|
|
75
|
+
)}
|
|
76
|
+
aria-label={t("editorSidebar.addSlides")}
|
|
77
|
+
>
|
|
78
|
+
{addSlideGenerating ? (
|
|
79
|
+
<IconLoader2 className="size-4 animate-spin" />
|
|
80
|
+
) : (
|
|
81
|
+
<IconPlus className="size-4" />
|
|
82
|
+
)}
|
|
83
|
+
</button>
|
|
84
|
+
</TooltipTrigger>
|
|
85
|
+
<TooltipContent>{t("editorSidebar.addSlides")}</TooltipContent>
|
|
86
|
+
</Tooltip>
|
|
87
|
+
<AddSlidePopover
|
|
88
|
+
open={addSlideOpen}
|
|
89
|
+
onOpenChange={setAddSlideOpen}
|
|
90
|
+
anchorRef={addSlideRef}
|
|
91
|
+
deckId={deckId}
|
|
92
|
+
deckTitle={deckTitle}
|
|
93
|
+
activeSlideId={currentSlideId ?? ""}
|
|
94
|
+
slideCount={slideCount}
|
|
95
|
+
activeSlideIndex={currentSlideIndex}
|
|
96
|
+
agentSubmit={(message, context) => {
|
|
97
|
+
onAddSlideGeneratingChange?.(true);
|
|
98
|
+
agentSubmit(message, context);
|
|
99
|
+
}}
|
|
100
|
+
onDuplicateCurrent={onDuplicateCurrentSlide}
|
|
101
|
+
onAddEmpty={onAddEmptySlide}
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
{onToggleTextBoxMode && (
|
|
105
|
+
<>
|
|
106
|
+
<div className={DIVIDER_CLASS} />
|
|
107
|
+
<Tooltip>
|
|
108
|
+
<TooltipTrigger asChild>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={onToggleTextBoxMode}
|
|
112
|
+
data-toolbar-textbox-button
|
|
113
|
+
aria-label={t("editorToolbar.addTextBox")}
|
|
114
|
+
aria-pressed={textBoxMode}
|
|
115
|
+
aria-keyshortcuts="T"
|
|
116
|
+
className={cn(
|
|
117
|
+
BUTTON_CLASS,
|
|
118
|
+
textBoxMode ? ACTIVE_CLASS : IDLE_CLASS,
|
|
119
|
+
)}
|
|
120
|
+
>
|
|
121
|
+
<IconTextSize className="size-4" />
|
|
122
|
+
</button>
|
|
123
|
+
</TooltipTrigger>
|
|
124
|
+
<TooltipContent>{t("editorToolbar.addTextBox")} (T)</TooltipContent>
|
|
125
|
+
</Tooltip>
|
|
126
|
+
</>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|