feat(editor): float image with text wrap (#145, port from Forkmost)

Adds floatLeft / floatRight image alignment so text wraps beside the image,
beyond the existing block left/center/right. Ported from Forkmost PR #7 /
upstream Docmost PR #1132 (fuscodev), adapted to gitmost's imperative image
node-view (the upstream uses a React styled component; ours styles the node-view
container directly via applyAlignment).

- editor-ext image.ts: `setImageAlign` accepts `floatLeft`/`floatRight`;
  `applyAlignment` resets float/padding then, for a float mode, sets
  `float:left|right` + side padding on the (shrink-to-fit) container so text
  flows beside it (the inner <img> already has max-width:100%). The resolved
  align is mirrored onto the container as `data-image-align` for the responsive
  rule. `data-align` already round-trips the value through parse/renderHTML, so
  float survives serialization / collab / history with no schema change.
- image-menu.tsx: Float-left / Float-right bubble-menu buttons (IconFloatLeft/
  Right) with active state.
- image-resize.module.css: on narrow screens (<=600px) a floated image collapses
  to full width and drops the float (`!important`, keyed on data-image-align) —
  the upstream "100% width on small screen" follow-up.
- i18n: en-US + ru-RU strings.

editor-ext build + client tsc --noEmit clean. Visual wrap behavior is best
confirmed in-browser (logic/serialization verified by build + types).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-24 06:35:34 +03:00
parent acf6d85b07
commit 8ef66ba712
5 changed files with 87 additions and 4 deletions

View File

@@ -51,7 +51,9 @@ declare module "@tiptap/core" {
setImageAt: (
attributes: ImageAttributes & { pos: number | Range },
) => ReturnType;
setImageAlign: (align: "left" | "center" | "right") => ReturnType;
setImageAlign: (
align: "left" | "center" | "right" | "floatLeft" | "floatRight",
) => ReturnType;
setImageWidth: (width: number) => ReturnType;
setImageSize: (width: number, height: number) => ReturnType;
};
@@ -375,7 +377,26 @@ export const TiptapImage = Image.extend<ImageOptions>({
});
function applyAlignment(container: HTMLElement, align: string) {
if (align === "left") {
// Reset the float-mode styles first so toggling between any two modes is clean
// (a previous float must not leak into a later left/center/right).
container.style.float = "";
container.style.padding = "";
// Mirror the resolved alignment onto the CONTAINER as a data attribute so the
// responsive stylesheet can neutralize the float on small screens (an inline
// `float` can only be overridden by `!important`, which keys off this attr).
container.dataset.imageAlign = align;
if (align === "floatLeft") {
// Real text wrap: the (shrink-to-fit) container floats left, text flows on
// its right. The inner <img> already carries max-width:100%.
container.style.float = "left";
container.style.padding = "0 10px 0 0";
container.style.justifyContent = "flex-start";
} else if (align === "floatRight") {
container.style.float = "right";
container.style.padding = "0 0 0 10px";
container.style.justifyContent = "flex-end";
} else if (align === "left") {
container.style.justifyContent = "flex-start";
} else if (align === "right") {
container.style.justifyContent = "flex-end";