fix: make the formatting toolbar one tab stop that says which styles are on

This commit is contained in:
Linus Rath
2026-10-29 17:53:41 +01:00
parent caee90facc
commit 447a3fa474
31 changed files with 1042 additions and 185 deletions
@@ -1,4 +1,4 @@
import { render, waitFor } from '@testing-library/react';
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import React from 'react';
import type { Editor } from '@tiptap/core';
@@ -49,3 +49,80 @@ describe('RichTextEditor editing area (SR-07)', () => {
await waitFor(() => expect(editor.view.dom).not.toHaveAttribute('aria-invalid'));
});
});
describe('RichTextEditor formatting toolbar (SR-08)', () => {
const toolbar = () => screen.getByRole('toolbar', { name: 'formatting' });
const item = (name: string) => toolbar().querySelector<HTMLButtonElement>(`[data-toolbar-item="${name}"]`)!;
const tabStops = () => Array.from(toolbar().querySelectorAll<HTMLElement>('[data-toolbar-item]')).filter((el) => el.tabIndex === 0);
it('is one Tab stop and moves with the arrows, Home and End', async () => {
await setup();
expect(tabStops()).toEqual([item('font-size')]);
item('font-size').focus();
fireEvent.keyDown(item('font-size'), { key: 'ArrowRight' });
expect(document.activeElement).toBe(item('bold'));
expect(tabStops()).toEqual([item('bold')]);
fireEvent.keyDown(item('bold'), { key: 'ArrowLeft' });
expect(document.activeElement).toBe(item('font-size'));
// End skips undo and redo while they are disabled.
fireEvent.keyDown(item('font-size'), { key: 'End' });
expect(document.activeElement).toBe(item('clear-formatting'));
fireEvent.keyDown(item('clear-formatting'), { key: 'Home' });
expect(document.activeElement).toBe(item('font-size'));
});
it('says which toggles are on', async () => {
// As in the composer, a change re-renders the editor through its parent.
let editor: Editor | null = null;
function Host() {
const [html, setHtml] = React.useState('<p>Hello</p>');
return <RichTextEditor content={html} onChange={setHtml} onEditorReady={(ed) => { editor = ed as unknown as Editor; }} />;
}
render(<Host />);
await waitFor(() => expect(editor).not.toBeNull());
expect(item('bold')).toHaveAttribute('aria-pressed', 'false');
act(() => { editor!.chain().selectAll().toggleBold().run(); });
await waitFor(() => expect(item('bold')).toHaveAttribute('aria-pressed', 'true'));
expect(item('italic')).toHaveAttribute('aria-pressed', 'false');
});
it('opens the font size menu by keyboard, moves inside it and gives the focus back on Escape', async () => {
await setup();
const button = item('font-size');
expect(button).toHaveAttribute('aria-haspopup', 'menu');
expect(button).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(button);
expect(button).toHaveAttribute('aria-expanded', 'true');
const menu = screen.getByRole('menu', { name: 'font_size' });
const entries = within(menu).getAllByRole('menuitemradio');
await waitFor(() => expect(document.activeElement).toBe(entries[0]));
fireEvent.keyDown(entries[0], { key: 'ArrowDown' });
expect(document.activeElement).toBe(entries[1]);
fireEvent.keyDown(entries[1], { key: 'Escape' });
expect(screen.queryByRole('menu', { name: 'font_size' })).toBeNull();
expect(document.activeElement).toBe(button);
});
it('names the colour swatches and moves through them as a grid', async () => {
await setup();
fireEvent.click(item('text-color'));
const menu = screen.getByRole('menu', { name: 'text_color' });
const swatches = within(menu).getAllByRole('menuitemradio');
expect(swatches[0]).toHaveAccessibleName('colors.black');
await waitFor(() => expect(document.activeElement).toBe(swatches[0]));
fireEvent.keyDown(swatches[0], { key: 'ArrowDown' });
expect(document.activeElement).toBe(swatches[8]);
fireEvent.keyDown(swatches[8], { key: 'ArrowRight' });
expect(document.activeElement).toBe(swatches[9]);
});
it('jumps from the text to the toolbar with Alt+F10', async () => {
const { editor } = await setup();
editor.commands.focus();
fireEvent.keyDown(editor.view.dom, { key: 'F10', altKey: true });
expect(document.activeElement).toBe(item('font-size'));
});
});
+384 -155
View File
@@ -1,6 +1,6 @@
"use client";
import React, { useEffect, useCallback, useState, useRef } from "react";
import React, { useEffect, useCallback, useLayoutEffect, useState, useRef } from "react";
import { createPortal } from "react-dom";
import { useEditor, EditorContent, type Editor } from "@tiptap/react";
import type { SuggestionProps } from "@tiptap/suggestion";
@@ -146,35 +146,142 @@ export function editorA11yAttributes({ ariaLabel, ariaInvalid, ariaDescribedBy }
};
}
function ToolbarButton({
active,
onClick,
children,
title,
disabled,
}: {
/**
* One button of the formatting toolbar. `item` names it for the toolbar's
* roving focus (one Tab stop, arrows inside). A toggle passes `pressed`, a
* button that opens a popover passes `popup` and `expanded`.
*/
const ToolbarButton = React.forwardRef<HTMLButtonElement, {
item: string;
active?: boolean;
pressed?: boolean;
popup?: "menu" | "dialog";
expanded?: boolean;
onClick: () => void;
children: React.ReactNode;
title: string;
disabled?: boolean;
}) {
}>(function ToolbarButton({ item, active, pressed, popup, expanded, onClick, children, title, disabled }, ref) {
return (
<button
ref={ref}
type="button"
data-toolbar-item={item}
onClick={onClick}
disabled={disabled}
title={title}
aria-label={title}
aria-pressed={pressed === undefined ? undefined : pressed}
aria-haspopup={popup}
aria-expanded={popup ? !!expanded : undefined}
className={cn(
// Bigger on a phone, where 28px was hard to hit with a finger.
"p-1.5 max-md:p-2 rounded hover:bg-accent transition-colors",
active && "bg-accent text-accent-foreground",
(active || pressed) && "bg-accent text-accent-foreground",
disabled && "opacity-40 cursor-not-allowed"
)}
>
{children}
</button>
);
});
/**
* A toolbar popover (font size, colours, table) that works from the
* keyboard: it takes the focus when it opens (the checked entry, else the
* first), the arrows move inside it (in a grid with `columns`), and Escape
* or Tab close it and put the focus back on its button.
*/
function ToolbarPopover({
label,
role,
columns,
onClose,
triggerRef,
className,
children,
}: {
label: string;
role: "menu" | "dialog";
/** Arrange the leading `[data-grid-item]` entries in rows of this many. */
columns?: number;
onClose: () => void;
triggerRef: React.RefObject<HTMLButtonElement | null>;
className?: string;
children: React.ReactNode;
}) {
const ref = useRef<HTMLDivElement>(null);
const items = () =>
Array.from(ref.current?.querySelectorAll<HTMLElement>("button:not([disabled])") ?? []);
useEffect(() => {
const list = items();
const checked = list.find((el) => el.getAttribute("aria-checked") === "true");
(checked ?? list[0])?.focus();
}, []);
const close = (returnFocus: boolean) => {
onClose();
if (returnFocus) triggerRef.current?.focus();
};
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const list = items();
const i = list.indexOf(document.activeElement as HTMLElement);
const rtl = ref.current ? getComputedStyle(ref.current).direction === "rtl" : false;
const gridCount = columns ? list.filter((el) => el.hasAttribute("data-grid-item")).length : 0;
const inGrid = columns && i >= 0 && i < gridCount;
let next = -1;
switch (e.key) {
case "Escape":
case "Tab":
e.preventDefault();
e.stopPropagation();
close(true);
return;
case "ArrowDown":
next = inGrid ? (i + columns! < gridCount ? i + columns! : gridCount < list.length ? gridCount : i) : Math.min(i + 1, list.length - 1);
break;
case "ArrowUp":
next = inGrid ? (i - columns! >= 0 ? i - columns! : i) : i === gridCount && gridCount > 0 ? gridCount - 1 : Math.max(i - 1, 0);
break;
case "ArrowRight":
case "ArrowLeft": {
if (!columns) return;
const forward = (e.key === "ArrowRight") !== rtl;
next = forward ? Math.min(i + 1, list.length - 1) : Math.max(i - 1, 0);
break;
}
case "Home":
next = 0;
break;
case "End":
next = list.length - 1;
break;
default:
return;
}
e.preventDefault();
e.stopPropagation();
list[next]?.focus();
};
return (
<div
ref={ref}
role={role}
aria-label={label}
onKeyDown={onKeyDown}
// Focus moving out (a click elsewhere, a screen reader jump) closes it.
onBlur={(e) => {
const to = e.relatedTarget as Node | null;
if (to && !ref.current?.contains(to) && !triggerRef.current?.contains(to)) close(false);
}}
className={cn("absolute z-50 top-full start-0 mt-1 bg-popover border border-border rounded-md shadow-md", className)}
>
{children}
</div>
);
}
function ToolbarSeparator() {
@@ -191,6 +298,14 @@ const TEXT_COLORS = [
"#000000", "#5f6368", "#9aa0a6", "#c5221f", "#e8710a", "#f9ab00", "#188038", "#1967d2",
"#7627bb", "#c2185b", "#795548", "#fa5252", "#fd7e14", "#40c057", "#4dabf7", "#e64980",
];
// What each swatch is called, for screen readers and the tooltip
// (email_composer.toolbar.colors.*).
const TEXT_COLOR_NAMES: Record<string, string> = {
"#000000": "black", "#5f6368": "dark_gray", "#9aa0a6": "gray", "#c5221f": "dark_red",
"#e8710a": "orange", "#f9ab00": "yellow", "#188038": "green", "#1967d2": "blue",
"#7627bb": "purple", "#c2185b": "dark_pink", "#795548": "brown", "#fa5252": "red",
"#fd7e14": "light_orange", "#40c057": "light_green", "#4dabf7": "light_blue", "#e64980": "pink",
};
function TableSizePicker({ onPick }: { onPick: (rows: number, cols: number) => void }) {
const t = useTranslations("email_composer.toolbar");
@@ -210,7 +325,10 @@ function TableSizePicker({ onPick }: { onPick: (rows: number, cols: number) => v
<button
key={i}
type="button"
data-grid-item=""
aria-label={t("table_size", { rows: r + 1, cols: c + 1 })}
onMouseEnter={() => setHover({ r, c })}
onFocus={() => setHover({ r, c })}
onClick={() => onPick(r + 1, c + 1)}
className={cn(
"w-4 h-4 border border-border/60 rounded-[2px] transition-colors",
@@ -220,7 +338,7 @@ function TableSizePicker({ onPick }: { onPick: (rows: number, cols: number) => v
);
})}
</div>
<div className="text-xs text-muted-foreground mt-1.5 text-center">
<div className="text-xs text-muted-foreground mt-1.5 text-center" aria-hidden>
{hover ? `${hover.r + 1} × ${hover.c + 1}` : t("pick_size")}
</div>
</div>
@@ -259,6 +377,8 @@ export function RichTextEditor({
mentionContactsRef.current = { findMentionContacts, mentionAddsTo, onMentionAdd };
const [mention, setMention] = useState<SuggestionProps<MentionCandidate, MentionCandidate> | null>(null);
const mentionListRef = useRef<RecipientMentionListHandle>(null);
// Alt+F10 in the text moves to the toolbar, as in other editors.
const focusToolbarRef = useRef<(() => boolean) | null>(null);
const editor = useEditor({
extensions: [
@@ -351,6 +471,13 @@ export function RichTextEditor({
content,
editorProps: {
attributes: editorA11yAttributes({ ariaLabel, ariaInvalid, ariaDescribedBy }),
handleKeyDown: (_view, event) => {
if (event.altKey && event.key === "F10" && focusToolbarRef.current?.()) {
event.preventDefault();
return true;
}
return false;
},
handleDrop: (view, event) => {
if (!event.dataTransfer?.files?.length) return false;
const upload = onImageUploadRef.current;
@@ -482,6 +609,37 @@ export function RichTextEditor({
const bgColorWrapperRef = useRef<HTMLDivElement>(null);
const [fontSizeMenuOpen, setFontSizeMenuOpen] = useState(false);
const fontSizeWrapperRef = useRef<HTMLDivElement>(null);
const fontSizeButtonRef = useRef<HTMLButtonElement>(null);
const colorButtonRef = useRef<HTMLButtonElement>(null);
const bgColorButtonRef = useRef<HTMLButtonElement>(null);
const tableButtonRef = useRef<HTMLButtonElement>(null);
// The toolbar is one Tab stop (roving tabindex): the button that had the
// focus last keeps it, the arrows, Home and End move inside.
const toolbarRef = useRef<HTMLDivElement>(null);
const rovingItemRef = useRef<string | null>(null);
const toolbarItems = () =>
Array.from(toolbarRef.current?.querySelectorAll<HTMLButtonElement>("[data-toolbar-item]") ?? [])
.filter((el) => !el.disabled);
const syncToolbarTabStops = (focus?: HTMLButtonElement) => {
const all = Array.from(toolbarRef.current?.querySelectorAll<HTMLButtonElement>("[data-toolbar-item]") ?? []);
const enabled = all.filter((el) => !el.disabled);
const current = focus ?? enabled.find((el) => el.dataset.toolbarItem === rovingItemRef.current) ?? enabled[0];
if (current) rovingItemRef.current = current.dataset.toolbarItem ?? null;
for (const el of all) el.tabIndex = el === current ? 0 : -1;
};
// After every render: buttons come and go (RTL, undo turning disabled).
useLayoutEffect(() => {
syncToolbarTabStops();
});
focusToolbarRef.current = () => {
const toolbar = toolbarRef.current;
if (!toolbar || getComputedStyle(toolbar).display === "none") return false;
syncToolbarTabStops();
const stop = toolbar.querySelector<HTMLButtonElement>('[data-toolbar-item][tabindex="0"]');
stop?.focus();
return !!stop && document.activeElement === stop;
};
useEffect(() => {
if (!fontSizeMenuOpen) return;
@@ -534,8 +692,82 @@ export function RichTextEditor({
}
const currentFontSize: string | null = editor.getAttributes("textStyle").fontSize ?? null;
const currentColor: string | null = editor.getAttributes("textStyle").color ?? null;
const currentBgColor: string | null = editor.getAttributes("textStyle").backgroundColor ?? null;
const onToolbarKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement;
// Keys inside a popover are the popover's.
if (!target.hasAttribute("data-toolbar-item")) return;
const items = toolbarItems();
const i = items.indexOf(target as HTMLButtonElement);
if (i < 0) return;
const rtl = toolbarRef.current ? getComputedStyle(toolbarRef.current).direction === "rtl" : false;
let next: number;
if (e.key === (rtl ? "ArrowLeft" : "ArrowRight")) next = (i + 1) % items.length;
else if (e.key === (rtl ? "ArrowRight" : "ArrowLeft")) next = (i - 1 + items.length) % items.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = items.length - 1;
else return;
e.preventDefault();
syncToolbarTabStops(items[next]);
items[next].focus();
};
const onToolbarFocus = (e: React.FocusEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement;
if (target instanceof HTMLButtonElement && target.hasAttribute("data-toolbar-item")) syncToolbarTabStops(target);
};
const renderPalette = (current: string | null, apply: (color: string) => void, removeLabel: string, remove: () => void) => (
<>
<div className="grid gap-0.5" style={{ gridTemplateColumns: "repeat(8, 1fr)" }}>
{TEXT_COLORS.map((color) => {
const name = tToolbar(`colors.${TEXT_COLOR_NAMES[color]}`);
return (
<button
key={color}
type="button"
role="menuitemradio"
aria-checked={current === color}
aria-label={name}
title={name}
data-grid-item=""
onClick={() => apply(color)}
className={cn(
"w-4 h-4 border border-border/60 rounded-[2px] transition-transform hover:scale-110 focus-visible:scale-110",
current === color && "ring-1 ring-ring ring-offset-1"
)}
style={{ backgroundColor: color }}
/>
);
})}
</div>
<div className="h-px bg-border my-1.5" role="separator" />
<button
type="button"
role="menuitem"
className="flex items-center gap-2 px-2 py-1 text-sm rounded hover:bg-accent focus-visible:bg-accent text-start w-full"
onClick={remove}
>
<RemoveFormatting className="w-4 h-4" aria-hidden /> {removeLabel}
</button>
</>
);
const tableMenuItem = (icon: React.ReactNode, label: string, run: () => void, destructive = false) => (
<button
type="button"
role="menuitem"
className={cn(
"flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent focus-visible:bg-accent text-start",
destructive && "text-destructive"
)}
onClick={() => { run(); setTableMenuOpen(false); }}
>
{icon} {label}
</button>
);
return (
<div className={cn("flex flex-col", hasError && "ring-2 ring-red-500 dark:ring-red-400 rounded", className)}>
{/* Toolbar - sticky within the composer's scroll container so it stays
@@ -545,7 +777,12 @@ export function RichTextEditor({
through, which a backdrop blur alone did not guarantee (browsers
that skip backdrop-filter drew the text sharp under the buttons). */}
<div
ref={toolbarRef}
data-testid="rich-text-toolbar"
role="toolbar"
aria-label={tComposer("formatting")}
onKeyDown={onToolbarKeyDown}
onFocus={onToolbarFocus}
className={cn(
"sticky z-20 flex flex-wrap items-center gap-0.5 px-3 py-1.5 bg-[color-mix(in_oklab,var(--color-muted)_30%,var(--color-background))]",
toolbarPlacement === 'bottom'
@@ -559,9 +796,13 @@ export function RichTextEditor({
once a size is chosen the button shows that number instead. */}
<div ref={fontSizeWrapperRef} className="relative">
<ToolbarButton
ref={fontSizeButtonRef}
item="font-size"
active={fontSizeMenuOpen || !!currentFontSize}
popup="menu"
expanded={fontSizeMenuOpen}
onClick={() => setFontSizeMenuOpen((v) => !v)}
title={tToolbar("font_size")}
title={currentFontSize ? `${tToolbar("font_size")}: ${currentFontSize}` : tToolbar("font_size")}
>
{currentFontSize ? (
<span className="text-xs font-semibold min-w-6 text-center leading-4 tabular-nums">
@@ -575,17 +816,25 @@ export function RichTextEditor({
)}
</ToolbarButton>
{fontSizeMenuOpen && (
<div className="absolute z-50 top-full start-0 mt-1 bg-popover border border-border rounded-md shadow-md p-1 min-w-[64px]">
<ToolbarPopover
label={tToolbar("font_size")}
role="menu"
triggerRef={fontSizeButtonRef}
onClose={() => setFontSizeMenuOpen(false)}
className="p-1 min-w-[64px]"
>
{FONT_SIZES.map((size) => (
<button
key={size}
type="button"
role="menuitemradio"
aria-checked={currentFontSize === size}
onClick={() => {
editor.chain().focus().setFontSize(size).run();
setFontSizeMenuOpen(false);
}}
className={cn(
"block w-full text-start px-2 py-1 rounded hover:bg-accent transition-colors",
"block w-full text-start px-2 py-1 rounded hover:bg-accent focus-visible:bg-accent transition-colors",
currentFontSize === size && "bg-accent text-accent-foreground"
)}
style={{ fontSize: size, lineHeight: 1.4 }}
@@ -593,46 +842,51 @@ export function RichTextEditor({
{size}
</button>
))}
<div className="h-px bg-border my-1" />
<div className="h-px bg-border my-1" role="separator" />
<button
type="button"
className="flex items-center gap-2 px-2 py-1 text-sm rounded hover:bg-accent text-start w-full"
role="menuitem"
className="flex items-center gap-2 px-2 py-1 text-sm rounded hover:bg-accent focus-visible:bg-accent text-start w-full"
onClick={() => {
editor.chain().focus().unsetFontSize().run();
setFontSizeMenuOpen(false);
}}
>
<RemoveFormatting className="w-4 h-4" /> {tToolbar("font_size_default")}
<RemoveFormatting className="w-4 h-4" aria-hidden /> {tToolbar("font_size_default")}
</button>
</div>
</ToolbarPopover>
)}
</div>
<ToolbarSeparator />
<ToolbarButton
active={editor.isActive("bold")}
item="bold"
pressed={editor.isActive("bold")}
onClick={() => editor.chain().focus().toggleBold().run()}
title={tToolbar("bold")}
>
<Bold className="w-4 h-4" />
</ToolbarButton>
<ToolbarButton
active={editor.isActive("italic")}
item="italic"
pressed={editor.isActive("italic")}
onClick={() => editor.chain().focus().toggleItalic().run()}
title={tToolbar("italic")}
>
<Italic className="w-4 h-4" />
</ToolbarButton>
<ToolbarButton
active={editor.isActive("underline")}
item="underline"
pressed={editor.isActive("underline")}
onClick={() => editor.chain().focus().toggleUnderline().run()}
title={tToolbar("underline")}
>
<UnderlineIcon className="w-4 h-4" />
</ToolbarButton>
<ToolbarButton
active={editor.isActive("strike")}
item="strike"
pressed={editor.isActive("strike")}
onClick={() => editor.chain().focus().toggleStrike().run()}
title={tToolbar("strikethrough")}
>
@@ -640,45 +894,39 @@ export function RichTextEditor({
</ToolbarButton>
<div ref={colorWrapperRef} className="relative">
<ToolbarButton
active={!!editor.getAttributes("textStyle").color}
ref={colorButtonRef}
item="text-color"
active={!!currentColor}
popup="menu"
expanded={colorMenuOpen}
onClick={() => setColorMenuOpen((v) => !v)}
title={tToolbar("text_color")}
>
{/* The icon itself previews the active colour - no layout shift. */}
<Baseline className="w-4 h-4" style={{ color: editor.getAttributes("textStyle").color || undefined }} />
<Baseline className="w-4 h-4" style={{ color: currentColor || undefined }} />
</ToolbarButton>
{colorMenuOpen && (
<div className="absolute z-50 top-full start-0 mt-1 bg-popover border border-border rounded-md shadow-md p-2">
<div className="grid gap-0.5" style={{ gridTemplateColumns: "repeat(8, 1fr)" }}>
{TEXT_COLORS.map((color) => (
<button
key={color}
type="button"
title={color}
onClick={() => {
editor.chain().focus().setColor(color).run();
setColorMenuOpen(false);
}}
className={cn(
"w-4 h-4 border border-border/60 rounded-[2px] transition-transform hover:scale-110",
editor.getAttributes("textStyle").color === color && "ring-1 ring-ring ring-offset-1"
)}
style={{ backgroundColor: color }}
/>
))}
</div>
<div className="h-px bg-border my-1.5" />
<button
type="button"
className="flex items-center gap-2 px-2 py-1 text-sm rounded hover:bg-accent text-start w-full"
onClick={() => {
<ToolbarPopover
label={tToolbar("text_color")}
role="menu"
columns={8}
triggerRef={colorButtonRef}
onClose={() => setColorMenuOpen(false)}
className="p-2"
>
{renderPalette(
currentColor,
(color) => {
editor.chain().focus().setColor(color).run();
setColorMenuOpen(false);
},
tToolbar("remove_color"),
() => {
editor.chain().focus().unsetColor().run();
setColorMenuOpen(false);
}}
>
<RemoveFormatting className="w-4 h-4" /> {tToolbar("remove_color")}
</button>
</div>
}
)}
</ToolbarPopover>
)}
</div>
{/* Background color - same palette, same grid and same swatch
@@ -688,7 +936,11 @@ export function RichTextEditor({
survives email round-trips like `color` does. */}
<div ref={bgColorWrapperRef} className="relative">
<ToolbarButton
ref={bgColorButtonRef}
item="background-color"
active={!!currentBgColor}
popup="menu"
expanded={bgColorMenuOpen}
onClick={() => setBgColorMenuOpen((v) => !v)}
title={tToolbar("background_color")}
>
@@ -702,51 +954,43 @@ export function RichTextEditor({
</span>
</ToolbarButton>
{bgColorMenuOpen && (
<div className="absolute z-50 top-full start-0 mt-1 bg-popover border border-border rounded-md shadow-md p-2">
<div className="grid gap-0.5" style={{ gridTemplateColumns: "repeat(8, 1fr)" }}>
{TEXT_COLORS.map((color) => (
<button
key={color}
type="button"
title={color}
onClick={() => {
editor.chain().focus().setBackgroundColor(color).run();
setBgColorMenuOpen(false);
}}
className={cn(
"w-4 h-4 border border-border/60 rounded-[2px] transition-transform hover:scale-110",
currentBgColor === color && "ring-1 ring-ring ring-offset-1"
)}
style={{ backgroundColor: color }}
/>
))}
</div>
<div className="h-px bg-border my-1.5" />
<button
type="button"
className="flex items-center gap-2 px-2 py-1 text-sm rounded hover:bg-accent text-start w-full"
onClick={() => {
<ToolbarPopover
label={tToolbar("background_color")}
role="menu"
columns={8}
triggerRef={bgColorButtonRef}
onClose={() => setBgColorMenuOpen(false)}
className="p-2"
>
{renderPalette(
currentBgColor,
(color) => {
editor.chain().focus().setBackgroundColor(color).run();
setBgColorMenuOpen(false);
},
tToolbar("remove_background_color"),
() => {
editor.chain().focus().unsetBackgroundColor().run();
setBgColorMenuOpen(false);
}}
>
<RemoveFormatting className="w-4 h-4" /> {tToolbar("remove_background_color")}
</button>
</div>
}
)}
</ToolbarPopover>
)}
</div>
<ToolbarSeparator />
<ToolbarButton
active={editor.isActive("heading", { level: 1 })}
item="heading-1"
pressed={editor.isActive("heading", { level: 1 })}
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
title={tToolbar("heading_1")}
>
<Heading1 className="w-4 h-4" />
</ToolbarButton>
<ToolbarButton
active={editor.isActive("heading", { level: 2 })}
item="heading-2"
pressed={editor.isActive("heading", { level: 2 })}
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
title={tToolbar("heading_2")}
>
@@ -756,28 +1000,32 @@ export function RichTextEditor({
<ToolbarSeparator />
<ToolbarButton
active={editor.isActive("bulletList")}
item="bullet-list"
pressed={editor.isActive("bulletList")}
onClick={() => editor.chain().focus().toggleBulletList().run()}
title={tToolbar("bullet_list")}
>
<List className="w-4 h-4" />
</ToolbarButton>
<ToolbarButton
active={editor.isActive("orderedList")}
item="ordered-list"
pressed={editor.isActive("orderedList")}
onClick={() => editor.chain().focus().toggleOrderedList().run()}
title={tToolbar("ordered_list")}
>
<ListOrdered className="w-4 h-4" />
</ToolbarButton>
<ToolbarButton
active={editor.isActive("blockquote")}
item="quote"
pressed={editor.isActive("blockquote")}
onClick={() => editor.chain().focus().toggleBlockquote().run()}
title={tToolbar("quote")}
>
<Quote className="w-4 h-4" />
</ToolbarButton>
<ToolbarButton
active={editor.isActive("codeBlock")}
item="code-block"
pressed={editor.isActive("codeBlock")}
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
title={tToolbar("code_block")}
>
@@ -787,21 +1035,24 @@ export function RichTextEditor({
<ToolbarSeparator />
<ToolbarButton
active={editor.isActive({ textAlign: "left" })}
item="align-left"
pressed={editor.isActive({ textAlign: "left" })}
onClick={() => editor.chain().focus().setTextAlign("left").run()}
title={tToolbar("align_left")}
>
<AlignLeft className="w-4 h-4" />
</ToolbarButton>
<ToolbarButton
active={editor.isActive({ textAlign: "center" })}
item="align-center"
pressed={editor.isActive({ textAlign: "center" })}
onClick={() => editor.chain().focus().setTextAlign("center").run()}
title={tToolbar("align_center")}
>
<AlignCenter className="w-4 h-4" />
</ToolbarButton>
<ToolbarButton
active={editor.isActive({ textAlign: "right" })}
item="align-right"
pressed={editor.isActive({ textAlign: "right" })}
onClick={() => editor.chain().focus().setTextAlign("right").run()}
title={tToolbar("align_right")}
>
@@ -810,7 +1061,8 @@ export function RichTextEditor({
{rtlEditingSupport && (
<ToolbarButton
active={
item="text-direction"
pressed={
(editor.getAttributes("paragraph").dir || editor.getAttributes("heading").dir) === "rtl"
}
onClick={() => {
@@ -827,7 +1079,10 @@ export function RichTextEditor({
<ToolbarSeparator />
<ToolbarButton
item="link"
active={editor.isActive("link")}
popup="dialog"
expanded={!!linkDialog}
onClick={addLink}
title={tToolbar("link")}
>
@@ -836,90 +1091,62 @@ export function RichTextEditor({
<div ref={tableWrapperRef} className="relative">
<ToolbarButton
ref={tableButtonRef}
item="table"
active={editor.isActive("table")}
popup={editor.isActive("table") ? "menu" : "dialog"}
expanded={tableMenuOpen}
onClick={() => setTableMenuOpen((v) => !v)}
title={tToolbar("table")}
>
<TableIcon className="w-4 h-4" />
</ToolbarButton>
{tableMenuOpen && (
<div className="absolute z-50 top-full start-0 mt-1 bg-popover border border-border rounded-md shadow-md p-2 min-w-[200px]">
{editor.isActive("table") ? (
editor.isActive("table") ? (
<ToolbarPopover
label={tToolbar("table")}
role="menu"
triggerRef={tableButtonRef}
onClose={() => setTableMenuOpen(false)}
className="p-2 min-w-[200px]"
>
<div className="flex flex-col gap-0.5">
<button
type="button"
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
onClick={() => { editor.chain().focus().addRowBefore().run(); setTableMenuOpen(false); }}
>
<Rows3 className="w-4 h-4" /> {tToolbar("add_row_above")}
</button>
<button
type="button"
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
onClick={() => { editor.chain().focus().addRowAfter().run(); setTableMenuOpen(false); }}
>
<Rows3 className="w-4 h-4" /> {tToolbar("add_row_below")}
</button>
<button
type="button"
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
onClick={() => { editor.chain().focus().addColumnBefore().run(); setTableMenuOpen(false); }}
>
<Columns3 className="w-4 h-4" /> {tToolbar("add_column_before")}
</button>
<button
type="button"
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
onClick={() => { editor.chain().focus().addColumnAfter().run(); setTableMenuOpen(false); }}
>
<Columns3 className="w-4 h-4" /> {tToolbar("add_column_after")}
</button>
<div className="h-px bg-border my-1" />
<button
type="button"
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
onClick={() => { editor.chain().focus().deleteRow().run(); setTableMenuOpen(false); }}
>
<Trash2 className="w-4 h-4" /> {tToolbar("delete_row")}
</button>
<button
type="button"
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
onClick={() => { editor.chain().focus().deleteColumn().run(); setTableMenuOpen(false); }}
>
<Trash2 className="w-4 h-4" /> {tToolbar("delete_column")}
</button>
<button
type="button"
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start"
onClick={() => { editor.chain().focus().toggleHeaderRow().run(); setTableMenuOpen(false); }}
>
<Rows3 className="w-4 h-4" /> {tToolbar("toggle_header_row")}
</button>
<div className="h-px bg-border my-1" />
<button
type="button"
className="flex items-center gap-2 px-2 py-1.5 text-sm rounded hover:bg-accent text-start text-destructive"
onClick={() => { editor.chain().focus().deleteTable().run(); setTableMenuOpen(false); }}
>
<Trash2 className="w-4 h-4" /> {tToolbar("delete_table")}
</button>
{tableMenuItem(<Rows3 className="w-4 h-4" aria-hidden />, tToolbar("add_row_above"), () => editor.chain().focus().addRowBefore().run())}
{tableMenuItem(<Rows3 className="w-4 h-4" aria-hidden />, tToolbar("add_row_below"), () => editor.chain().focus().addRowAfter().run())}
{tableMenuItem(<Columns3 className="w-4 h-4" aria-hidden />, tToolbar("add_column_before"), () => editor.chain().focus().addColumnBefore().run())}
{tableMenuItem(<Columns3 className="w-4 h-4" aria-hidden />, tToolbar("add_column_after"), () => editor.chain().focus().addColumnAfter().run())}
<div className="h-px bg-border my-1" role="separator" />
{tableMenuItem(<Trash2 className="w-4 h-4" aria-hidden />, tToolbar("delete_row"), () => editor.chain().focus().deleteRow().run())}
{tableMenuItem(<Trash2 className="w-4 h-4" aria-hidden />, tToolbar("delete_column"), () => editor.chain().focus().deleteColumn().run())}
{tableMenuItem(<Rows3 className="w-4 h-4" aria-hidden />, tToolbar("toggle_header_row"), () => editor.chain().focus().toggleHeaderRow().run())}
<div className="h-px bg-border my-1" role="separator" />
{tableMenuItem(<Trash2 className="w-4 h-4" aria-hidden />, tToolbar("delete_table"), () => editor.chain().focus().deleteTable().run(), true)}
</div>
) : (
</ToolbarPopover>
) : (
<ToolbarPopover
label={tToolbar("table")}
role="dialog"
columns={TABLE_PICKER_COLS}
triggerRef={tableButtonRef}
onClose={() => setTableMenuOpen(false)}
className="p-2 min-w-[200px]"
>
<TableSizePicker
onPick={(rows, cols) => {
editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run();
setTableMenuOpen(false);
}}
/>
)}
</div>
</ToolbarPopover>
)
)}
</div>
<ToolbarSeparator />
<ToolbarButton
item="clear-formatting"
onClick={() => editor.chain().focus().clearNodes().unsetAllMarks().run()}
title={tToolbar("clear_formatting")}
>
@@ -929,6 +1156,7 @@ export function RichTextEditor({
<ToolbarSeparator />
<ToolbarButton
item="undo"
onClick={() => editor.chain().focus().undo().run()}
disabled={!editor.can().undo()}
title={tToolbar("undo")}
@@ -936,6 +1164,7 @@ export function RichTextEditor({
<Undo className="w-4 h-4" />
</ToolbarButton>
<ToolbarButton
item="redo"
onClick={() => editor.chain().focus().redo().run()}
disabled={!editor.can().redo()}
title={tToolbar("redo")}
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "حجم الخط",
"font_size_default": "افتراضي",
"background_color": "لون الخلفية",
"remove_background_color": "إزالة لون الخلفية"
"remove_background_color": "إزالة لون الخلفية",
"table_size": "جدول {rows} × {cols}",
"colors": {
"black": "أسود",
"dark_gray": "رمادي داكن",
"gray": "رمادي",
"dark_red": "أحمر داكن",
"orange": "برتقالي",
"yellow": "أصفر",
"green": "أخضر",
"blue": "أزرق",
"purple": "بنفسجي",
"dark_pink": "وردي داكن",
"brown": "بني",
"red": "أحمر",
"light_orange": "برتقالي فاتح",
"light_green": "أخضر فاتح",
"light_blue": "أزرق فاتح",
"pink": "وردي"
}
},
"send_filing_warning": "تم الإرسال - لكن التنظيف بعد الإرسال فشل، وقد تبقى مسودة قديمة.",
"pop_out": "فتح الرد في نافذة منفصلة",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Mida de la lletra",
"font_size_default": "Per defecte",
"background_color": "Color de fons",
"remove_background_color": "Elimina el color de fons"
"remove_background_color": "Elimina el color de fons",
"table_size": "Taula de {rows} × {cols}",
"colors": {
"black": "Negre",
"dark_gray": "Gris fosc",
"gray": "Gris",
"dark_red": "Vermell fosc",
"orange": "Taronja",
"yellow": "Groc",
"green": "Verd",
"blue": "Blau",
"purple": "Morat",
"dark_pink": "Rosa fosc",
"brown": "Marró",
"red": "Vermell",
"light_orange": "Taronja clar",
"light_green": "Verd clar",
"light_blue": "Blau clar",
"pink": "Rosa"
}
},
"send_filing_warning": "Enviat, però la neteja posterior a l'enviament ha fallat; és possible que quedi un esborrany obsolet.",
"pop_out": "Obre la resposta en una finestra",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Velikost písma",
"font_size_default": "Výchozí",
"background_color": "Barva pozadí",
"remove_background_color": "Odebrat barvu pozadí"
"remove_background_color": "Odebrat barvu pozadí",
"table_size": "Tabulka {rows} × {cols}",
"colors": {
"black": "Černá",
"dark_gray": "Tmavě šedá",
"gray": "Šedá",
"dark_red": "Tmavě červená",
"orange": "Oranžová",
"yellow": "Žlutá",
"green": "Zelená",
"blue": "Modrá",
"purple": "Fialová",
"dark_pink": "Tmavě růžová",
"brown": "Hnědá",
"red": "Červená",
"light_orange": "Světle oranžová",
"light_green": "Světle zelená",
"light_blue": "Světle modrá",
"pink": "Růžová"
}
},
"send_filing_warning": "Odesláno - ale následný úklid selhal, může zůstat zastaralý koncept.",
"pop_out": "Otevřít odpověď v okně",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Skriftstørrelse",
"font_size_default": "Standard",
"background_color": "Baggrundsfarve",
"remove_background_color": "Fjern baggrundsfarve"
"remove_background_color": "Fjern baggrundsfarve",
"table_size": "Tabel med {rows} × {cols}",
"colors": {
"black": "Sort",
"dark_gray": "Mørkegrå",
"gray": "Grå",
"dark_red": "Mørkerød",
"orange": "Orange",
"yellow": "Gul",
"green": "Grøn",
"blue": "Blå",
"purple": "Lilla",
"dark_pink": "Mørk pink",
"brown": "Brun",
"red": "Rød",
"light_orange": "Lys orange",
"light_green": "Lysegrøn",
"light_blue": "Lyseblå",
"pink": "Pink"
}
},
"send_filing_warning": "Sendt - men oprydningen bagefter mislykkedes, en forældet kladde kan blive stående.",
"pop_out": "Åbn svaret i et vindue",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Schriftgröße",
"font_size_default": "Standard",
"background_color": "Hintergrundfarbe",
"remove_background_color": "Hintergrundfarbe entfernen"
"remove_background_color": "Hintergrundfarbe entfernen",
"table_size": "Tabelle mit {rows} × {cols} Zellen",
"colors": {
"black": "Schwarz",
"dark_gray": "Dunkelgrau",
"gray": "Grau",
"dark_red": "Dunkelrot",
"orange": "Orange",
"yellow": "Gelb",
"green": "Grün",
"blue": "Blau",
"purple": "Lila",
"dark_pink": "Dunkelrosa",
"brown": "Braun",
"red": "Rot",
"light_orange": "Hellorange",
"light_green": "Hellgrün",
"light_blue": "Hellblau",
"pink": "Rosa"
}
},
"send_filing_warning": "Gesendet - aber das Aufräumen danach schlug fehl, evtl. bleibt ein alter Entwurf sichtbar.",
"pop_out": "Antwort in eigenem Fenster öffnen",
+20 -1
View File
@@ -996,7 +996,26 @@
"font_size": "Font size",
"font_size_default": "Default",
"background_color": "Background color",
"remove_background_color": "Remove background color"
"remove_background_color": "Remove background color",
"table_size": "{rows} by {cols} table",
"colors": {
"black": "Black",
"dark_gray": "Dark gray",
"gray": "Gray",
"dark_red": "Dark red",
"orange": "Orange",
"yellow": "Yellow",
"green": "Green",
"blue": "Blue",
"purple": "Purple",
"dark_pink": "Dark pink",
"brown": "Brown",
"red": "Red",
"light_orange": "Light orange",
"light_green": "Light green",
"light_blue": "Light blue",
"pink": "Pink"
}
},
"send_filing_warning": "Sent - but the post-send cleanup failed, a stale draft may remain.",
"pop_out": "Pop out reply",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Tamaño de fuente",
"font_size_default": "Predeterminado",
"background_color": "Color de fondo",
"remove_background_color": "Quitar color de fondo"
"remove_background_color": "Quitar color de fondo",
"table_size": "Tabla de {rows} × {cols}",
"colors": {
"black": "Negro",
"dark_gray": "Gris oscuro",
"gray": "Gris",
"dark_red": "Rojo oscuro",
"orange": "Naranja",
"yellow": "Amarillo",
"green": "Verde",
"blue": "Azul",
"purple": "Morado",
"dark_pink": "Rosa oscuro",
"brown": "Marrón",
"red": "Rojo",
"light_orange": "Naranja claro",
"light_green": "Verde claro",
"light_blue": "Azul claro",
"pink": "Rosa"
}
},
"send_filing_warning": "Enviado - pero la limpieza posterior falló, puede quedar un borrador obsoleto.",
"pop_out": "Abrir la respuesta en una ventana",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Fondi suurus",
"font_size_default": "Vaikimisi",
"background_color": "Taustavärv",
"remove_background_color": "Eemalda taustavärv"
"remove_background_color": "Eemalda taustavärv",
"table_size": "Tabel {rows} × {cols}",
"colors": {
"black": "Must",
"dark_gray": "Tumehall",
"gray": "Hall",
"dark_red": "Tumepunane",
"orange": "Oranž",
"yellow": "Kollane",
"green": "Roheline",
"blue": "Sinine",
"purple": "Lilla",
"dark_pink": "Tumeroosa",
"brown": "Pruun",
"red": "Punane",
"light_orange": "Heleoranž",
"light_green": "Heleroheline",
"light_blue": "Helesinine",
"pink": "Roosa"
}
},
"send_filing_warning": "Saadetud - kuid saatmisjärgne koristus ebaõnnestus, mustand võib alles jääda.",
"pop_out": "Ava vastus eraldi aknas",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "اندازه فونت",
"font_size_default": "پیش‌فرض",
"background_color": "رنگ پس‌زمینه",
"remove_background_color": "حذف رنگ پس‌زمینه"
"remove_background_color": "حذف رنگ پس‌زمینه",
"table_size": "جدول {rows} × {cols}",
"colors": {
"black": "سیاه",
"dark_gray": "خاکستری تیره",
"gray": "خاکستری",
"dark_red": "قرمز تیره",
"orange": "نارنجی",
"yellow": "زرد",
"green": "سبز",
"blue": "آبی",
"purple": "بنفش",
"dark_pink": "صورتی تیره",
"brown": "قهوه‌ای",
"red": "قرمز",
"light_orange": "نارنجی روشن",
"light_green": "سبز روشن",
"light_blue": "آبی روشن",
"pink": "صورتی"
}
},
"send_filing_warning": "ارسال شد - اما پاک‌سازی پس از ارسال ناموفق بود، ممکن است پیش‌نویس قدیمی باقی بماند.",
"pop_out": "باز کردن پاسخ در پنجره جدا",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Taille de police",
"font_size_default": "Par défaut",
"background_color": "Couleur d'arrière-plan",
"remove_background_color": "Supprimer la couleur d'arrière-plan"
"remove_background_color": "Supprimer la couleur d'arrière-plan",
"table_size": "Tableau de {rows} × {cols}",
"colors": {
"black": "Noir",
"dark_gray": "Gris foncé",
"gray": "Gris",
"dark_red": "Rouge foncé",
"orange": "Orange",
"yellow": "Jaune",
"green": "Vert",
"blue": "Bleu",
"purple": "Violet",
"dark_pink": "Rose foncé",
"brown": "Marron",
"red": "Rouge",
"light_orange": "Orange clair",
"light_green": "Vert clair",
"light_blue": "Bleu clair",
"pink": "Rose"
}
},
"send_filing_warning": "Envoyé - mais le nettoyage après envoi a échoué, un ancien brouillon peut subsister.",
"pop_out": "Ouvrir la réponse dans une fenêtre",
+20 -1
View File
@@ -959,7 +959,26 @@
"font_size": "גודל גופן",
"font_size_default": "ברירת מחדל",
"background_color": "צבע רקע",
"remove_background_color": "הסר צבע רקע"
"remove_background_color": "הסר צבע רקע",
"table_size": "טבלה {rows} × {cols}",
"colors": {
"black": "שחור",
"dark_gray": "אפור כהה",
"gray": "אפור",
"dark_red": "אדום כהה",
"orange": "כתום",
"yellow": "צהוב",
"green": "ירוק",
"blue": "כחול",
"purple": "סגול",
"dark_pink": "ורוד כהה",
"brown": "חום",
"red": "אדום",
"light_orange": "כתום בהיר",
"light_green": "ירוק בהיר",
"light_blue": "כחול בהיר",
"pink": "ורוד"
}
},
"send_filing_warning": "נשלח - אך הניקוי שלאחר השליחה נכשל, ייתכן שתישאר טיוטה ישנה.",
"pop_out": "פתיחת התשובה בחלון נפרד",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Betűméret",
"font_size_default": "Alapértelmezett",
"background_color": "Háttérszín",
"remove_background_color": "Háttérszín eltávolítása"
"remove_background_color": "Háttérszín eltávolítása",
"table_size": "{rows} × {cols} méretű táblázat",
"colors": {
"black": "Fekete",
"dark_gray": "Sötétszürke",
"gray": "Szürke",
"dark_red": "Sötétvörös",
"orange": "Narancs",
"yellow": "Sárga",
"green": "Zöld",
"blue": "Kék",
"purple": "Lila",
"dark_pink": "Sötét rózsaszín",
"brown": "Barna",
"red": "Piros",
"light_orange": "Világos narancs",
"light_green": "Világoszöld",
"light_blue": "Világoskék",
"pink": "Rózsaszín"
}
},
"send_filing_warning": "Elküldve - de az utólagos rendrakás nem sikerült, egy elavult piszkozat megmaradhat.",
"pop_out": "Válasz megnyitása külön ablakban",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Dimensione carattere",
"font_size_default": "Predefinita",
"background_color": "Colore di sfondo",
"remove_background_color": "Rimuovi colore di sfondo"
"remove_background_color": "Rimuovi colore di sfondo",
"table_size": "Tabella {rows} × {cols}",
"colors": {
"black": "Nero",
"dark_gray": "Grigio scuro",
"gray": "Grigio",
"dark_red": "Rosso scuro",
"orange": "Arancione",
"yellow": "Giallo",
"green": "Verde",
"blue": "Blu",
"purple": "Viola",
"dark_pink": "Rosa scuro",
"brown": "Marrone",
"red": "Rosso",
"light_orange": "Arancione chiaro",
"light_green": "Verde chiaro",
"light_blue": "Azzurro",
"pink": "Rosa"
}
},
"send_filing_warning": "Inviato - ma la pulizia successiva non è riuscita, potrebbe restare una bozza obsoleta.",
"pop_out": "Apri la risposta in una finestra",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "文字サイズ",
"font_size_default": "デフォルト",
"background_color": "背景色",
"remove_background_color": "背景色を削除"
"remove_background_color": "背景色を削除",
"table_size": "{rows} × {cols} の表",
"colors": {
"black": "黒",
"dark_gray": "濃い灰色",
"gray": "灰色",
"dark_red": "濃い赤",
"orange": "オレンジ",
"yellow": "黄",
"green": "緑",
"blue": "青",
"purple": "紫",
"dark_pink": "濃いピンク",
"brown": "茶",
"red": "赤",
"light_orange": "明るいオレンジ",
"light_green": "明るい緑",
"light_blue": "水色",
"pink": "ピンク"
}
},
"send_filing_warning": "送信されましたが、送信後の整理に失敗しました。古い下書きが残る場合があります。",
"pop_out": "返信を別ウィンドウで開く",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "글자 크기",
"font_size_default": "기본값",
"background_color": "배경색",
"remove_background_color": "배경색 제거"
"remove_background_color": "배경색 제거",
"table_size": "{rows} × {cols} 표",
"colors": {
"black": "검정",
"dark_gray": "진한 회색",
"gray": "회색",
"dark_red": "진한 빨강",
"orange": "주황",
"yellow": "노랑",
"green": "초록",
"blue": "파랑",
"purple": "보라",
"dark_pink": "진한 분홍",
"brown": "갈색",
"red": "빨강",
"light_orange": "연한 주황",
"light_green": "연한 초록",
"light_blue": "하늘색",
"pink": "분홍"
}
},
"send_filing_warning": "보냈지만 전송 후 정리에 실패했습니다. 오래된 임시 보관 메일이 남아 있을 수 있습니다.",
"pop_out": "답장을 별도 창에서 열기",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Fonta izmērs",
"font_size_default": "Noklusējums",
"background_color": "Fona krāsa",
"remove_background_color": "Noņemt fona krāsu"
"remove_background_color": "Noņemt fona krāsu",
"table_size": "Tabula {rows} × {cols}",
"colors": {
"black": "Melna",
"dark_gray": "Tumši pelēka",
"gray": "Pelēka",
"dark_red": "Tumši sarkana",
"orange": "Oranža",
"yellow": "Dzeltena",
"green": "Zaļa",
"blue": "Zila",
"purple": "Violeta",
"dark_pink": "Tumši rozā",
"brown": "Brūna",
"red": "Sarkana",
"light_orange": "Gaiši oranža",
"light_green": "Gaiši zaļa",
"light_blue": "Gaiši zila",
"pink": "Rozā"
}
},
"send_filing_warning": "Nosūtīts - bet pēcapstrāde neizdevās, var palikt novecojis melnraksts.",
"pop_out": "Atvērt atbildi atsevišķā logā",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Фонтын хэмжээ",
"font_size_default": "Үндсэн",
"background_color": "Дэвсгэр өнгө",
"remove_background_color": "Дэвсгэр өнгийг арилгах"
"remove_background_color": "Дэвсгэр өнгийг арилгах",
"table_size": "{rows} × {cols} хүснэгт",
"colors": {
"black": "Хар",
"dark_gray": "Бараан саарал",
"gray": "Саарал",
"dark_red": "Бараан улаан",
"orange": "Улбар шар",
"yellow": "Шар",
"green": "Ногоон",
"blue": "Цэнхэр",
"purple": "Нил ягаан",
"dark_pink": "Бараан ягаан",
"brown": "Хүрэн",
"red": "Улаан",
"light_orange": "Цайвар улбар шар",
"light_green": "Цайвар ногоон",
"light_blue": "Цайвар цэнхэр",
"pink": "Ягаан"
}
},
"send_filing_warning": "Илгээсэн - гэхдээ илгээсний дараах цэвэрлэгээ амжилтгүй болсон тул хуучирсан ноорог үлдэж болзошгүй.",
"pop_out": "Хариуг тусдаа цонхонд нээх",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Skriftstørrelse",
"font_size_default": "Standard",
"background_color": "Bakgrunnsfarge",
"remove_background_color": "Fjern bakgrunnsfarge"
"remove_background_color": "Fjern bakgrunnsfarge",
"table_size": "Tabell med {rows} × {cols}",
"colors": {
"black": "Svart",
"dark_gray": "Mørkegrå",
"gray": "Grå",
"dark_red": "Mørkerød",
"orange": "Oransje",
"yellow": "Gul",
"green": "Grønn",
"blue": "Blå",
"purple": "Lilla",
"dark_pink": "Mørk rosa",
"brown": "Brun",
"red": "Rød",
"light_orange": "Lys oransje",
"light_green": "Lysegrønn",
"light_blue": "Lyseblå",
"pink": "Rosa"
}
},
"send_filing_warning": "Meldingen ble sendt, men oppryddingen etterpå mislyktes. Et gammelt utkast kan bli liggende.",
"pop_out": "Åpne svaret i et eget vindu",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Lettergrootte",
"font_size_default": "Standaard",
"background_color": "Achtergrondkleur",
"remove_background_color": "Achtergrondkleur verwijderen"
"remove_background_color": "Achtergrondkleur verwijderen",
"table_size": "Tabel van {rows} × {cols}",
"colors": {
"black": "Zwart",
"dark_gray": "Donkergrijs",
"gray": "Grijs",
"dark_red": "Donkerrood",
"orange": "Oranje",
"yellow": "Geel",
"green": "Groen",
"blue": "Blauw",
"purple": "Paars",
"dark_pink": "Donkerroze",
"brown": "Bruin",
"red": "Rood",
"light_orange": "Lichtoranje",
"light_green": "Lichtgroen",
"light_blue": "Lichtblauw",
"pink": "Roze"
}
},
"send_filing_warning": "Verzonden - maar het opruimen daarna is mislukt, mogelijk blijft een oud concept staan.",
"pop_out": "Antwoord in een apart venster openen",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Rozmiar czcionki",
"font_size_default": "Domyślny",
"background_color": "Kolor tła",
"remove_background_color": "Usuń kolor tła"
"remove_background_color": "Usuń kolor tła",
"table_size": "Tabela {rows} × {cols}",
"colors": {
"black": "Czarny",
"dark_gray": "Ciemnoszary",
"gray": "Szary",
"dark_red": "Ciemnoczerwony",
"orange": "Pomarańczowy",
"yellow": "Żółty",
"green": "Zielony",
"blue": "Niebieski",
"purple": "Fioletowy",
"dark_pink": "Ciemnoróżowy",
"brown": "Brązowy",
"red": "Czerwony",
"light_orange": "Jasnopomarańczowy",
"light_green": "Jasnozielony",
"light_blue": "Jasnoniebieski",
"pink": "Różowy"
}
},
"send_filing_warning": "Wysłano - ale późniejsze porządkowanie nie powiodło się, może pozostać nieaktualna wersja robocza.",
"pop_out": "Otwórz odpowiedź w oknie",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Tamanho da fonte",
"font_size_default": "Padrão",
"background_color": "Cor de fundo",
"remove_background_color": "Remover cor de fundo"
"remove_background_color": "Remover cor de fundo",
"table_size": "Tabela de {rows} × {cols}",
"colors": {
"black": "Preto",
"dark_gray": "Cinza-escuro",
"gray": "Cinza",
"dark_red": "Vermelho-escuro",
"orange": "Laranja",
"yellow": "Amarelo",
"green": "Verde",
"blue": "Azul",
"purple": "Roxo",
"dark_pink": "Rosa-escuro",
"brown": "Marrom",
"red": "Vermelho",
"light_orange": "Laranja-claro",
"light_green": "Verde-claro",
"light_blue": "Azul-claro",
"pink": "Rosa"
}
},
"send_filing_warning": "Enviado - mas a limpeza posterior falhou, um rascunho antigo pode permanecer.",
"pop_out": "Abrir a resposta numa janela",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Dimensiune font",
"font_size_default": "Implicit",
"background_color": "Culoarea fundalului",
"remove_background_color": "Elimină culoarea fundalului"
"remove_background_color": "Elimină culoarea fundalului",
"table_size": "Tabel {rows} × {cols}",
"colors": {
"black": "Negru",
"dark_gray": "Gri închis",
"gray": "Gri",
"dark_red": "Roșu închis",
"orange": "Portocaliu",
"yellow": "Galben",
"green": "Verde",
"blue": "Albastru",
"purple": "Mov",
"dark_pink": "Roz închis",
"brown": "Maro",
"red": "Roșu",
"light_orange": "Portocaliu deschis",
"light_green": "Verde deschis",
"light_blue": "Albastru deschis",
"pink": "Roz"
}
},
"send_filing_warning": "Trimis - dar curățarea ulterioară a eșuat, poate rămâne o ciornă veche.",
"pop_out": "Deschide răspunsul într-o fereastră",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Размер шрифта",
"font_size_default": "По умолчанию",
"background_color": "Цвет фона",
"remove_background_color": "Убрать цвет фона"
"remove_background_color": "Убрать цвет фона",
"table_size": "Таблица {rows} × {cols}",
"colors": {
"black": "Чёрный",
"dark_gray": "Тёмно-серый",
"gray": "Серый",
"dark_red": "Тёмно-красный",
"orange": "Оранжевый",
"yellow": "Жёлтый",
"green": "Зелёный",
"blue": "Синий",
"purple": "Фиолетовый",
"dark_pink": "Тёмно-розовый",
"brown": "Коричневый",
"red": "Красный",
"light_orange": "Светло-оранжевый",
"light_green": "Светло-зелёный",
"light_blue": "Голубой",
"pink": "Розовый"
}
},
"send_filing_warning": "Отправлено - но последующая очистка не удалась, может остаться устаревший черновик.",
"pop_out": "Открыть ответ в отдельном окне",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Veľkosť písma",
"font_size_default": "Predvolené",
"background_color": "Farba pozadia",
"remove_background_color": "Odstrániť farbu pozadia"
"remove_background_color": "Odstrániť farbu pozadia",
"table_size": "Tabuľka {rows} × {cols}",
"colors": {
"black": "Čierna",
"dark_gray": "Tmavosivá",
"gray": "Sivá",
"dark_red": "Tmavočervená",
"orange": "Oranžová",
"yellow": "Žltá",
"green": "Zelená",
"blue": "Modrá",
"purple": "Fialová",
"dark_pink": "Tmavoružová",
"brown": "Hnedá",
"red": "Červená",
"light_orange": "Svetlooranžová",
"light_green": "Svetlozelená",
"light_blue": "Svetlomodrá",
"pink": "Ružová"
}
},
"send_filing_warning": "Odoslané - ale následné upratovanie zlyhalo, môže zostať zastaraný koncept.",
"pop_out": "Otvoriť odpoveď v okne",
+20 -1
View File
@@ -1233,7 +1233,26 @@
"font_size": "ขนาดตัวอักษร",
"font_size_default": "ค่าเริ่มต้น",
"background_color": "สีพื้นหลังข้อความ",
"remove_background_color": "ล้างสีพื้นหลัง"
"remove_background_color": "ล้างสีพื้นหลัง",
"table_size": "ตาราง {rows} × {cols}",
"colors": {
"black": "ดำ",
"dark_gray": "เทาเข้ม",
"gray": "เทา",
"dark_red": "แดงเข้ม",
"orange": "ส้ม",
"yellow": "เหลือง",
"green": "เขียว",
"blue": "น้ำเงิน",
"purple": "ม่วง",
"dark_pink": "ชมพูเข้ม",
"brown": "น้ำตาล",
"red": "แดง",
"light_orange": "ส้มอ่อน",
"light_green": "เขียวอ่อน",
"light_blue": "ฟ้า",
"pink": "ชมพู"
}
},
"send_filing_warning": "ส่งแล้ว - แต่การล้างข้อมูลหลังการส่งล้มเหลว อาจมีข้อความร่างค้างอยู่",
"pop_out": "เปิดการตอบกลับในหน้าต่างใหม่",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Yazı boyutu",
"font_size_default": "Varsayılan",
"background_color": "Arka plan rengi",
"remove_background_color": "Arka plan rengini kaldır"
"remove_background_color": "Arka plan rengini kaldır",
"table_size": "{rows} × {cols} tablo",
"colors": {
"black": "Siyah",
"dark_gray": "Koyu gri",
"gray": "Gri",
"dark_red": "Koyu kırmızı",
"orange": "Turuncu",
"yellow": "Sarı",
"green": "Yeşil",
"blue": "Mavi",
"purple": "Mor",
"dark_pink": "Koyu pembe",
"brown": "Kahverengi",
"red": "Kırmızı",
"light_orange": "Açık turuncu",
"light_green": "Açık yeşil",
"light_blue": "Açık mavi",
"pink": "Pembe"
}
},
"send_filing_warning": "Gönderildi - ancak sonrasındaki temizleme başarısız oldu, eski bir taslak kalabilir.",
"pop_out": "Yanıtı ayrı pencerede aç",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "Розмір шрифту",
"font_size_default": "За замовчуванням",
"background_color": "Колір фону",
"remove_background_color": "Прибрати колір фону"
"remove_background_color": "Прибрати колір фону",
"table_size": "Таблиця {rows} × {cols}",
"colors": {
"black": "Чорний",
"dark_gray": "Темно-сірий",
"gray": "Сірий",
"dark_red": "Темно-червоний",
"orange": "Помаранчевий",
"yellow": "Жовтий",
"green": "Зелений",
"blue": "Синій",
"purple": "Фіолетовий",
"dark_pink": "Темно-рожевий",
"brown": "Коричневий",
"red": "Червоний",
"light_orange": "Світло-помаранчевий",
"light_green": "Світло-зелений",
"light_blue": "Блакитний",
"pink": "Рожевий"
}
},
"send_filing_warning": "Надіслано - але подальше очищення не вдалося, може залишитися застаріла чернетка.",
"pop_out": "Відкрити відповідь в окремому вікні",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "字型大小",
"font_size_default": "預設",
"background_color": "背景顏色",
"remove_background_color": "移除背景顏色"
"remove_background_color": "移除背景顏色",
"table_size": "{rows} × {cols} 表格",
"colors": {
"black": "黑色",
"dark_gray": "深灰色",
"gray": "灰色",
"dark_red": "深紅色",
"orange": "橘色",
"yellow": "黃色",
"green": "綠色",
"blue": "藍色",
"purple": "紫色",
"dark_pink": "深粉紅色",
"brown": "棕色",
"red": "紅色",
"light_orange": "淺橘色",
"light_green": "淺綠色",
"light_blue": "淺藍色",
"pink": "粉紅色"
}
},
"send_filing_warning": "郵件已傳送,但寄送後清理失敗,可能仍保留舊草稿。",
"pop_out": "在獨立視窗中開啟回覆",
+20 -1
View File
@@ -994,7 +994,26 @@
"font_size": "字号",
"font_size_default": "默认",
"background_color": "背景颜色",
"remove_background_color": "移除背景颜色"
"remove_background_color": "移除背景颜色",
"table_size": "{rows} × {cols} 表格",
"colors": {
"black": "黑色",
"dark_gray": "深灰色",
"gray": "灰色",
"dark_red": "深红色",
"orange": "橙色",
"yellow": "黄色",
"green": "绿色",
"blue": "蓝色",
"purple": "紫色",
"dark_pink": "深粉色",
"brown": "棕色",
"red": "红色",
"light_orange": "浅橙色",
"light_green": "浅绿色",
"light_blue": "浅蓝色",
"pink": "粉色"
}
},
"send_filing_warning": "已发送,但发送后的清理失败,可能会残留旧草稿。",
"pop_out": "在单独窗口中打开回复",