chore(editor): toggleable formatting toolbar
Add a localStorage-backed insert-menu toggle (default off) that surfaces the formatting toolbar outside focus mode, and redesign it as a lean inline row: ghost icon buttons grouped by thin dividers, a level-reflecting heading glyph (pilcrow / H1-H3), and the active command as the only filled control. Also keep the editor focused when using the toolbar: command buttons preventDefault on mousedown, and the dropdown menus return focus to the editor on close.
This commit is contained in:
parent
76aee4e177
commit
ba609c5cb8
7 changed files with 165 additions and 63 deletions
|
|
@ -8,7 +8,14 @@ import type { EditorToolbarProps } from "../types";
|
|||
import InsertMenu from "./InsertMenu";
|
||||
import VisibilitySelector from "./VisibilitySelector";
|
||||
|
||||
export const EditorToolbar: FC<EditorToolbarProps> = ({ onSave, onCancel, memoName, onAudioRecorderClick }) => {
|
||||
export const EditorToolbar: FC<EditorToolbarProps> = ({
|
||||
onSave,
|
||||
onCancel,
|
||||
memoName,
|
||||
onAudioRecorderClick,
|
||||
isFormattingToolbarVisible,
|
||||
onToggleFormattingToolbar,
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
const { actions, dispatch } = useEditorContext();
|
||||
// Subscribe to narrow/derived slices so typing (which only changes content)
|
||||
|
|
@ -42,6 +49,8 @@ export const EditorToolbar: FC<EditorToolbarProps> = ({ onSave, onCancel, memoNa
|
|||
onToggleFocusMode={handleToggleFocusMode}
|
||||
memoName={memoName}
|
||||
onAudioRecorderClick={onAudioRecorderClick}
|
||||
isFormattingToolbarVisible={isFormattingToolbarVisible}
|
||||
onToggleFormattingToolbar={onToggleFormattingToolbar}
|
||||
/>
|
||||
<VisibilitySelector value={visibility} onChange={handleVisibilityChange} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { HeadingIcon, type LucideIcon, Minimize2Icon, MoreHorizontalIcon } from "lucide-react";
|
||||
import { type RefObject, useRef } from "react";
|
||||
import { Heading1Icon, Heading2Icon, Heading3Icon, type LucideIcon, Minimize2Icon, MoreHorizontalIcon, PilcrowIcon } from "lucide-react";
|
||||
import { type ComponentPropsWithoutRef, forwardRef, type MouseEventHandler, type RefObject, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -10,21 +10,31 @@ import {
|
|||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { EDITOR_COMMANDS, EDITOR_COMMANDS_BY_ID, type EditorCommand, type EditorCommandId, isCommandActive } from "../formatting/commands";
|
||||
import {
|
||||
EDITOR_COMMANDS,
|
||||
EDITOR_COMMANDS_BY_ID,
|
||||
type EditorCommand,
|
||||
type EditorCommandId,
|
||||
isCommandActive,
|
||||
type ToolbarHeadingLevel,
|
||||
} from "../formatting/commands";
|
||||
import { isCompactWidth, useEditorActiveState, useElementWidth } from "../hooks";
|
||||
import type { EditorController } from "../types";
|
||||
|
||||
interface FormattingToolbarProps {
|
||||
controllerRef: RefObject<EditorController | null>;
|
||||
onExit: () => void;
|
||||
/** Extra classes for the host to frame the toolbar (e.g. the focus-mode header band). */
|
||||
/** Called by the exit button; when omitted (normal-mode toolbar) the button is hidden. */
|
||||
onExit?: () => void;
|
||||
/** Extra classes for the host to frame the toolbar row. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MARK_COMMANDS = EDITOR_COMMANDS.filter((command) => command.group === "mark");
|
||||
const LIST_COMMANDS = EDITOR_COMMANDS.filter((command) => command.group === "list");
|
||||
// Paragraph + headings render as a single label-only dropdown (a closed set).
|
||||
// Paragraph + headings render as a single icon dropdown (a closed set); the
|
||||
// trigger glyph reflects the current block level.
|
||||
const HEADING_COMMANDS = EDITOR_COMMANDS.filter((command) => command.group === "heading");
|
||||
const HEADING_LEVEL_ICONS: Record<ToolbarHeadingLevel, LucideIcon> = { 1: Heading1Icon, 2: Heading2Icon, 3: Heading3Icon };
|
||||
|
||||
interface ToolbarButton {
|
||||
Icon?: LucideIcon;
|
||||
|
|
@ -33,12 +43,29 @@ interface ToolbarButton {
|
|||
onClick: () => void;
|
||||
}
|
||||
|
||||
// Button styling: quiet ghost controls that sit directly on the editor surface
|
||||
// (no filled track or border — that container read as a heavy slab). The active
|
||||
// verb is the only filled element, so the toolbar recedes and the current state
|
||||
// carries the weight. Kept as raw buttons (not the Button kit) because the idle
|
||||
// hover + active treatment don't map to a single kit variant, and per policy a
|
||||
// custom look is raw HTML rather than className overrides on the kit.
|
||||
const SEGMENT_BASE =
|
||||
"inline-flex items-center justify-center h-7 min-w-7 px-1.5 rounded-md text-sm transition-colors outline-none touch-manipulation focus-visible:ring-2 focus-visible:ring-ring";
|
||||
const SEGMENT_IDLE = "text-muted-foreground hover:text-foreground hover:bg-foreground/5";
|
||||
const SEGMENT_ACTIVE = "bg-accent text-accent-foreground";
|
||||
|
||||
// Command buttons must not take focus on mousedown — that blurs the editor and
|
||||
// drops the selection the command targets. The click still fires and applies the
|
||||
// format to the live selection.
|
||||
const preventFocusSteal: MouseEventHandler<HTMLButtonElement> = (event) => event.preventDefault();
|
||||
|
||||
/**
|
||||
* Focus-mode header: a rich-text formatting toolbar driven entirely through the
|
||||
* editor controller's formatting capability. Every button is derived from the
|
||||
* shared command catalog (formatting/commands.ts), so adding a verb there
|
||||
* surfaces it here automatically. Responsive: below COMPACT_TOOLBAR_WIDTH the
|
||||
* list and link controls fold into a "more" menu while marks stay inline.
|
||||
* Formatting toolbar: a lean inline row of the heading picker plus mark/list/link
|
||||
* controls, every one derived from the shared command catalog
|
||||
* (formatting/commands.ts), so adding a verb there surfaces it here automatically.
|
||||
* Groups are separated by thin vertical dividers. Responsive: below
|
||||
* COMPACT_TOOLBAR_WIDTH the list and link controls fold into a "more" menu while
|
||||
* marks stay inline. In focus mode an exit button is pushed to the far edge.
|
||||
*/
|
||||
export function FormattingToolbar({ controllerRef, onExit, className }: FormattingToolbarProps) {
|
||||
const t = useTranslate();
|
||||
|
|
@ -49,6 +76,13 @@ export function FormattingToolbar({ controllerRef, onExit, className }: Formatti
|
|||
|
||||
const run = (id: EditorCommandId) => controllerRef.current?.formatting?.run(id);
|
||||
|
||||
// Menus grab focus while open and hand it back to their trigger on close; send
|
||||
// it to the editor instead so the user can keep typing after a pick.
|
||||
const returnFocusToEditor = (event: Event) => {
|
||||
event.preventDefault();
|
||||
controllerRef.current?.focus();
|
||||
};
|
||||
|
||||
const handleLink = () => {
|
||||
const formatting = controllerRef.current?.formatting;
|
||||
if (!formatting) {
|
||||
|
|
@ -56,18 +90,19 @@ export function FormattingToolbar({ controllerRef, onExit, className }: Formatti
|
|||
}
|
||||
if (formatting.getActiveFormats().link) {
|
||||
formatting.run("link");
|
||||
return;
|
||||
}
|
||||
const url = window.prompt(t("editor.format.link-prompt"));
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
const selected = formatting.getSelectedText();
|
||||
if (selected) {
|
||||
formatting.run("link", { url });
|
||||
} else {
|
||||
controllerRef.current?.insertMarkdown(`[${url}](${url})`);
|
||||
// window.prompt blurs the editor, so refocus below regardless of outcome.
|
||||
const url = window.prompt(t("editor.format.link-prompt"));
|
||||
if (url) {
|
||||
const selected = formatting.getSelectedText();
|
||||
if (selected) {
|
||||
formatting.run("link", { url });
|
||||
} else {
|
||||
controllerRef.current?.insertMarkdown(`[${url}](${url})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
controllerRef.current?.focus();
|
||||
};
|
||||
|
||||
// Map a catalog command to a toolbar button. `link` keeps its bespoke
|
||||
|
|
@ -79,7 +114,9 @@ export function FormattingToolbar({ controllerRef, onExit, className }: Formatti
|
|||
onClick: command.id === "link" ? handleLink : () => run(command.id),
|
||||
});
|
||||
|
||||
const headingLabel = active.headingLevel === null ? t("editor.format.paragraph") : t(`editor.format.heading-${active.headingLevel}`);
|
||||
// Pilcrow for paragraph, else the matching Hn glyph. Deeper levels (H4–H6)
|
||||
// aren't toolbar-addressable and report as null, i.e. the pilcrow.
|
||||
const HeadingGlyph = active.headingLevel === null ? PilcrowIcon : HEADING_LEVEL_ICONS[active.headingLevel];
|
||||
const markButtons = MARK_COMMANDS.map(toButton);
|
||||
const listButtons = LIST_COMMANDS.map(toButton);
|
||||
const linkButton = toButton(EDITOR_COMMANDS_BY_ID.link);
|
||||
|
|
@ -87,18 +124,15 @@ export function FormattingToolbar({ controllerRef, onExit, className }: Formatti
|
|||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={cn("w-full flex flex-row items-center gap-1 flex-nowrap", className)}
|
||||
className={cn("w-full flex flex-row items-center gap-0.5", className)}
|
||||
role="toolbar"
|
||||
aria-label={t("editor.format.heading")}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" aria-label={t("editor.format.heading")}>
|
||||
<HeadingIcon className="w-4 h-4" />
|
||||
{!compact && <span className="ml-1 text-xs">{headingLabel}</span>}
|
||||
</Button>
|
||||
<SegmentButton Icon={HeadingGlyph} label={t("editor.format.heading")} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuContent align="start" onCloseAutoFocus={returnFocusToEditor}>
|
||||
{HEADING_COMMANDS.map((command) => (
|
||||
<DropdownMenuItem key={command.id} onClick={() => run(command.id)}>
|
||||
{t(command.labelKey)}
|
||||
|
|
@ -110,17 +144,17 @@ export function FormattingToolbar({ controllerRef, onExit, className }: Formatti
|
|||
<Divider />
|
||||
|
||||
{markButtons.map((button) => (
|
||||
<FmtButton key={button.label} {...button} />
|
||||
<SegmentButton key={button.label} {...button} onMouseDown={preventFocusSteal} />
|
||||
))}
|
||||
|
||||
<Divider />
|
||||
|
||||
{compact ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" aria-label={t("editor.format.more")}>
|
||||
<MoreHorizontalIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<SegmentButton Icon={MoreHorizontalIcon} label={t("editor.format.more")} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuContent align="start" onCloseAutoFocus={returnFocusToEditor}>
|
||||
{listButtons.map((button) => (
|
||||
<DropdownMenuItem key={button.label} onClick={button.onClick}>
|
||||
{button.label}
|
||||
|
|
@ -132,34 +166,52 @@ export function FormattingToolbar({ controllerRef, onExit, className }: Formatti
|
|||
</DropdownMenu>
|
||||
) : (
|
||||
<>
|
||||
<Divider />
|
||||
{listButtons.map((button) => (
|
||||
<FmtButton key={button.label} {...button} />
|
||||
<SegmentButton key={button.label} {...button} onMouseDown={preventFocusSteal} />
|
||||
))}
|
||||
<Divider />
|
||||
<FmtButton {...linkButton} />
|
||||
<SegmentButton {...linkButton} onMouseDown={preventFocusSteal} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<Button variant="ghost" size="icon" aria-label={t("editor.exit-focus-mode")} title={t("editor.exit-focus-mode")} onClick={onExit}>
|
||||
<Minimize2Icon className="w-4 h-4" />
|
||||
</Button>
|
||||
{onExit && (
|
||||
<>
|
||||
<div className="flex-1" />
|
||||
<Button variant="ghost" size="icon" aria-label={t("editor.exit-focus-mode")} title={t("editor.exit-focus-mode")} onClick={onExit}>
|
||||
<Minimize2Icon className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Thin vertical rule between command groups (heading · marks · lists · link).
|
||||
function Divider() {
|
||||
return <span aria-hidden="true" className="w-px h-5 bg-border mx-1 shrink-0" />;
|
||||
return <span aria-hidden="true" className="w-px h-5 bg-border mx-1.5 shrink-0" />;
|
||||
}
|
||||
|
||||
function FmtButton({ Icon, active, label, onClick }: ToolbarButton) {
|
||||
// Active state shown by switching the kit variant (prop-only, per the shadcn
|
||||
// kit usage policy) rather than overriding className.
|
||||
return (
|
||||
<Button variant={active ? "secondary" : "ghost"} size="icon" aria-label={label} aria-pressed={active} title={label} onClick={onClick}>
|
||||
{Icon && <Icon className="w-4 h-4" />}
|
||||
</Button>
|
||||
);
|
||||
interface SegmentButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||
Icon?: LucideIcon;
|
||||
label: string;
|
||||
/** Toggle state; when set the segment gets aria-pressed and the active fill. */
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
// The one segment element, shared by command toggles and dropdown triggers.
|
||||
// Forwards ref + rest props so it also works as a Radix `asChild` trigger
|
||||
// (which injects its own onClick/aria attributes).
|
||||
const SegmentButton = forwardRef<HTMLButtonElement, SegmentButtonProps>(({ Icon, label, active, className, ...rest }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
aria-label={label}
|
||||
aria-pressed={active}
|
||||
title={label}
|
||||
className={cn(SEGMENT_BASE, active ? SEGMENT_ACTIVE : SEGMENT_IDLE, className)}
|
||||
{...rest}
|
||||
>
|
||||
{Icon && <Icon className="w-4 h-4" />}
|
||||
</button>
|
||||
));
|
||||
SegmentButton.displayName = "SegmentButton";
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
MicIcon,
|
||||
MoreHorizontalIcon,
|
||||
PlusIcon,
|
||||
TypeIcon,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { LinkMemoDialog, LocationDialog } from "@/components/MemoMetadata";
|
||||
|
|
@ -18,6 +19,7 @@ import { useReverseGeocoding } from "@/components/map/useReverseGeocoding";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
|
|
@ -39,7 +41,14 @@ const InsertMenu = (props: InsertMenuProps) => {
|
|||
const t = useTranslate();
|
||||
const { actions, dispatch } = useEditorContext();
|
||||
const relations = useEditorSelector((s) => s.metadata.relations);
|
||||
const { location: initialLocation, onLocationChange, onToggleFocusMode, isUploading: isUploadingProp } = props;
|
||||
const {
|
||||
location: initialLocation,
|
||||
onLocationChange,
|
||||
onToggleFocusMode,
|
||||
onToggleFormattingToolbar,
|
||||
isFormattingToolbarVisible,
|
||||
isUploading: isUploadingProp,
|
||||
} = props;
|
||||
|
||||
const [linkDialogOpen, setLinkDialogOpen] = useState(false);
|
||||
const [locationDialogOpen, setLocationDialogOpen] = useState(false);
|
||||
|
|
@ -133,6 +142,11 @@ const InsertMenu = (props: InsertMenuProps) => {
|
|||
setMoreSubmenuOpen(false);
|
||||
}, [onToggleFocusMode]);
|
||||
|
||||
const handleToggleFormattingToolbar = useCallback(() => {
|
||||
onToggleFormattingToolbar?.();
|
||||
setMoreSubmenuOpen(false);
|
||||
}, [onToggleFormattingToolbar]);
|
||||
|
||||
const handleMediaUploadClick = useCallback(() => {
|
||||
handleUploadClick("image/*,video/*");
|
||||
}, [handleUploadClick]);
|
||||
|
|
@ -201,7 +215,7 @@ const InsertMenu = (props: InsertMenuProps) => {
|
|||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
{/* View submenu with Focus Mode + editor display mode */}
|
||||
{/* View submenu: focus mode + normal-mode formatting toolbar toggle */}
|
||||
<DropdownMenuSub open={moreSubmenuOpen} onOpenChange={setMoreSubmenuOpen}>
|
||||
<DropdownMenuSubTrigger onPointerEnter={handleTriggerEnter} onPointerLeave={handleTriggerLeave}>
|
||||
<MoreHorizontalIcon className="w-4 h-4" />
|
||||
|
|
@ -212,6 +226,10 @@ const InsertMenu = (props: InsertMenuProps) => {
|
|||
<Maximize2Icon className="w-4 h-4" />
|
||||
{t("editor.focus-mode")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuCheckboxItem checked={Boolean(isFormattingToolbarVisible)} onCheckedChange={handleToggleFormattingToolbar}>
|
||||
<TypeIcon className="w-4 h-4" />
|
||||
{t("editor.formatting-toolbar")}
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@ export const FOCUS_MODE_STYLES = {
|
|||
},
|
||||
transition: "transition-all duration-300 ease-in-out",
|
||||
exitButton: "absolute top-2 right-2 z-10 opacity-60 hover:opacity-100",
|
||||
// Full-bleed formatting-toolbar header band. The negative margins + width
|
||||
// counteract the card's px-4 pt-3 padding (1rem each side) so the band sits
|
||||
// flush to the inner borders; w-[calc(100%+2rem)] overrides the toolbar's own
|
||||
// w-full (twMerge keeps this later width). Keep in sync with the card padding.
|
||||
formattingHeader: "-mx-4 -mt-3 mb-1 w-[calc(100%+2rem)] px-3 py-2 bg-muted/50 border-b border-border rounded-t-lg",
|
||||
} as const;
|
||||
|
||||
export const EDITOR_HEIGHT = {
|
||||
// Max height for normal mode - focus mode uses flex-1 to grow dynamically
|
||||
normal: "max-h-[50vh]",
|
||||
} as const;
|
||||
|
||||
// localStorage key for the user's preference to show the formatting toolbar in
|
||||
// normal (non-focus) mode. Defaults to off.
|
||||
export const FORMATTING_TOOLBAR_STORAGE_KEY = "memos-editor-formatting-toolbar";
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { toast } from "react-hot-toast";
|
|||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useInstance } from "@/contexts/InstanceContext";
|
||||
import { useNewMemo } from "@/contexts/NewMemoContext";
|
||||
import { useLocalStorage } from "@/hooks";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { memoKeys } from "@/hooks/useMemoQueries";
|
||||
import { userKeys } from "@/hooks/useUserQueries";
|
||||
|
|
@ -13,7 +14,7 @@ import { InstanceSetting_Key } from "@/types/proto/api/v1/instance_service_pb";
|
|||
import { useTranslate } from "@/utils/i18n";
|
||||
import { convertVisibilityFromString } from "@/utils/memo";
|
||||
import { AudioRecorderPanel, EditorContent, EditorMetadata, FocusModeOverlay, TimestampPopover } from "./components";
|
||||
import { FOCUS_MODE_STYLES } from "./constants";
|
||||
import { FOCUS_MODE_STYLES, FORMATTING_TOOLBAR_STORAGE_KEY } from "./constants";
|
||||
import { useAudioRecorder, useAutoSave, useFocusMode, useKeyboard, useMemoInit } from "./hooks";
|
||||
import { errorService, memoService, transcriptionService, validationService } from "./services";
|
||||
import { EditorProvider, useEditorContext, useEditorSelector } from "./state";
|
||||
|
|
@ -54,6 +55,9 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
|
|||
const { markNewMemo } = useNewMemo();
|
||||
const [isAudioRecorderOpen, setIsAudioRecorderOpen] = useState(false);
|
||||
const [isTranscribingAudio, setIsTranscribingAudio] = useState(false);
|
||||
// Persisted preference: also show the formatting toolbar in normal mode. Focus
|
||||
// mode always shows it regardless; this only governs the non-focus layout.
|
||||
const [isFormattingToolbarVisible, setFormattingToolbarVisible] = useLocalStorage(FORMATTING_TOOLBAR_STORAGE_KEY, false);
|
||||
|
||||
const memoName = memo?.name;
|
||||
const canTranscribe = useMemo(() => {
|
||||
|
|
@ -188,6 +192,10 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
|
|||
dispatch(actions.toggleFocusMode());
|
||||
};
|
||||
|
||||
const handleToggleFormattingToolbar = useCallback(() => {
|
||||
setFormattingToolbarVisible((visible) => !visible);
|
||||
}, [setFormattingToolbarVisible]);
|
||||
|
||||
const handleStartAudioRecording = async () => {
|
||||
setIsAudioRecorderOpen(true);
|
||||
await audioRecorder.startRecording();
|
||||
|
|
@ -314,9 +322,11 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
|
|||
className,
|
||||
)}
|
||||
>
|
||||
{/* Focus-mode header: the formatting toolbar (exit lives in it). */}
|
||||
{isFocusMode && (
|
||||
<FormattingToolbar controllerRef={editorRef} onExit={handleToggleFocusMode} className={FOCUS_MODE_STYLES.formattingHeader} />
|
||||
{/* Formatting toolbar. Always shown in focus mode (with an exit button);
|
||||
in normal mode it appears only when the user toggled it on via the
|
||||
insert menu. */}
|
||||
{(isFocusMode || isFormattingToolbarVisible) && (
|
||||
<FormattingToolbar controllerRef={editorRef} onExit={isFocusMode ? handleToggleFocusMode : undefined} />
|
||||
)}
|
||||
|
||||
{(memoName || (!memo && hasTimestamp)) && (
|
||||
|
|
@ -343,7 +353,14 @@ const MemoEditorImpl: React.FC<MemoEditorProps> = ({
|
|||
{/* Metadata and toolbar grouped together at bottom */}
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
<EditorMetadata memoName={memoName} />
|
||||
<EditorToolbar onSave={handleSave} onCancel={onCancel} memoName={memoName} onAudioRecorderClick={handleAudioRecorderClick} />
|
||||
<EditorToolbar
|
||||
onSave={handleSave}
|
||||
onCancel={onCancel}
|
||||
memoName={memoName}
|
||||
onAudioRecorderClick={handleAudioRecorderClick}
|
||||
isFormattingToolbarVisible={isFormattingToolbarVisible}
|
||||
onToggleFormattingToolbar={handleToggleFormattingToolbar}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ export interface EditorToolbarProps {
|
|||
onCancel?: () => void;
|
||||
memoName?: string;
|
||||
onAudioRecorderClick: () => void;
|
||||
/** Whether the formatting toolbar is shown in normal mode (persisted preference). */
|
||||
isFormattingToolbarVisible: boolean;
|
||||
onToggleFormattingToolbar: () => void;
|
||||
}
|
||||
|
||||
export interface EditorMetadataProps {
|
||||
|
|
@ -65,6 +68,9 @@ export interface InsertMenuProps {
|
|||
onToggleFocusMode?: () => void;
|
||||
memoName?: string;
|
||||
onAudioRecorderClick?: () => void;
|
||||
/** Persisted toggle for the normal-mode formatting toolbar. */
|
||||
isFormattingToolbarVisible?: boolean;
|
||||
onToggleFormattingToolbar?: () => void;
|
||||
}
|
||||
|
||||
export interface VisibilitySelectorProps {
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@
|
|||
"any-thoughts": "Any thoughts...",
|
||||
"exit-focus-mode": "Exit Focus Mode",
|
||||
"focus-mode": "Focus Mode",
|
||||
"formatting-toolbar": "Formatting toolbar",
|
||||
"format": {
|
||||
"heading": "Heading",
|
||||
"paragraph": "Paragraph",
|
||||
|
|
|
|||
Loading…
Reference in a new issue