78 Commits
Author SHA1 Message Date
d3a9c2a195 fix(packages/excalidraw): complete cross-document runtime ownership (#11997)
* fix: complete cross-document runtime ownership

* fix: schedule cross-document animations in owner window

* fix: Prewarm the right font even if it's not Excalifont

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Hyperlink and tooltip host staleness issues

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Various hyperlink fixes

* feat: Host abstraction

* fix: exportToSvg support for document override

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Remove caches

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Refactor tests (cut non-critical)

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Break out host from App

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: hold after release fixes

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: repaint when link image decode is complete per app

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Font loading per-doc test

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Lint

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: draw SVG frame labels without text measurement

Frame labels were baked into exports as measured, ellipsis-truncated text
elements, which made `exportToSvg` depend on a canvas (through the render
environment) for the only text it ever measures.

- add `getFrameLabel` (element package): a measurement-free label spec --
  position, font, color, arithmetic height, the frame's width as max width
- SVG: draw labels from the spec in the frame branch, full title clipped to
  the frame's width; extract `appendSvgTextLines` so text elements and
  labels produce identical markup
- canvas: unchanged output (measured, truncated text elements), now built
  from the same spec
- fit label boxes into the SVG export bounds explicitly (`extraBounds`)
- test: SVG export of a named frame succeeds with a render environment
  whose `createCanvas` throws and creates no canvas through `document`

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: measurement-free embed placeholders; no render environment in SVG export

The placeholder drawn for an embed that isn't rendered live was the last
text `exportToSvg` measured, which kept a canvas (via the render
environment) in the SVG path and made headless SVG export throw on any
scene with an embed unless the process had been polyfilled.

- show the placeholder as a single line capped at 30 characters
  (`getPlaceholderLinkText`: scheme and `www.` dropped, ellipsis) instead
  of wrapping the whole link: at or below that length the existing
  `width / length` font size fits the box by construction, so nothing is
  measured, wrapped or clipped, and output is identical on every host. The
  full link stays on the element's `<a href>` in SVG
- `newTextElement` accepts pre-computed `metrics` to skip measuring
- drop `renderEnvironment` from `SVGRenderConfig`, `exportToSvg` and the
  `@excalidraw/utils` signature: SVG export's only host dependency is its
  owner document
- throw a guided error from the default canvas/image factories when there
  is no `document`/`Image`, instead of a bare ReferenceError
- tests: placeholder text capping and layout; SVG export of an iframe and
  a long-link embed with a render environment whose `createCanvas` throws

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: scope top picks drag to source window

* feat: StickyNotes support

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* Revert "fix: measurement-free embed placeholders; no render environment in SVG export"

This reverts commit 886bea142. Rich text makes SVG export measure text
anyway (explicit x per line, run boundaries, baselines), so placeholder
labels go back to being measured and wrapped like on canvas, through the
render environment.

Kept: the guided errors thrown by the default canvas/image factories when
there is no `document`/`Image`.

Claude Opus 5.5 (xhigh)

* Revert "fix: draw SVG frame labels without text measurement"

This reverts commit 295e7d88b. SVG export measures text again (rich text
needs it regardless), so both formats go back to master's single path:
frame labels baked in as measured, truncated text elements, measured
through the export's render environment. Drops `getFrameLabel` and the
SVG label renderer.

Claude Opus 5.5 (xhigh)

* export: one `config.environment` for every format, `document` for SVG

SVG exports take the same environment as canvas ones: they measure text
(frame labels, embed placeholders) through its `createCanvas`, and are
built in its `document` (was the SVG-only `ownerDocument`).

Claude Opus 5.5 (xhigh)

* RenderEnvironment: carry the `document`

The environment is the one host parameter: its `document` is what SVG
exports are built in (the editor's owner document by default), so export
call sites pass a single environment instead of an environment plus an
`ownerDocument`. `exportCanvas` drops its `ownerDocument` option, and
`config.environment.document` defaults to the environment's.

Rich text's DOM layout will read the same member, rather than threading
`ownerDocument` through the measuring functions next to the environment.

Claude Opus 5.5 (xhigh)

* drop the process-wide `setRenderEnvironment` / `resetRenderEnvironment`

Every render and export now takes its environment explicitly: exports
through `config.environment`, the editor through its `renderEnvironment`
prop. Text measured outside both (e.g. creating elements on a server) has
`setCustomTextMetricsProvider`. A second, global way to configure the same
thing would be state shared by every job in the process, which is what
per-call export environments avoid. The default is now a constant
(the browser's).

Claude Opus 5.5 (xhigh)

* render: log per-element failures on export too, rather than throwing

As in the editor (and on master): one element that fails to render is
logged and skipped, so the rest of the exported scene survives it rather
than the whole export failing.

Claude Opus 5.5 (xhigh)

* RenderEnvironment: carry the text metrics provider

`textMetrics` replaces the per-environment registry of
`setCustomTextMetricsProvider(provider, env)`, which looked providers up
by the environment's identity, so any rebuilt environment (as exports
build one from `config.environment`) silently lost them. It now travels
with the environment, and `config.environment.textMetrics` measures an
export's text. The global `setCustomTextMetricsProvider(provider)` stays
for text measured outside both (e.g. elements created on a server).

Exports use a complete `config.environment` as-is (the editor's keeps its
caches warm), and document that it needn't be the same object across
exports. Frame label truncation measures through the environment too,
rather than on a canvas of its own.

Claude Opus 5.5 (xhigh)

* render: restore the canvas state when an element fails to draw

The scene renderer logs a failing element and carries on, but the
`save()`s it had made were never restored: its transforms and frame clip
stayed on the context for every element after it (a failing freedraw at
(80, 30) moved the next rectangle from (10, 10) to (90, 40)). Every
`save()` around drawing now restores in a `finally` (`withSavedContext`).
Pre-existing on master, in the editor and on export.

Claude Opus 5.5 (xhigh)

* export: truncate frame names down to "...", or none, to fit the frame

When not even one character plus the ellipsis fit, the frame name kept
its full title (overflowing narrow frames into their neighbors) while
reporting the frame's width. It now shrinks to just "...", and is dropped
where even that doesn't fit. Pre-existing on master.

Claude Opus 5.5 (xhigh)

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-20 05:53:27 +02:00
David Luzar e1b7aacb35 feat(packages/excalidraw): replace the export functions with exportScene (#12347)
* [export] rewrite - init

* restore elements + filter deleted in export API (restores #11470 behavior)

* fix snaps

* port export callers added on master to the new API + snaps

- renderOverrides (#12100), sticky notes (#12064) and arrow-label hole
  (#10947) tests called the old positional exportToSvg/exportToCanvas
- embedded-scene snapshots gain master's new element fields (created,
  baseFontSize, labelPosition); decoded payloads match master's

Claude Opus 5.5 (xhigh)

* restore elements in embedded scene + json clipboard export (restores #12060 behavior)

- master's #12060 made every public export entry point restore its input
  (legacy elements reach lower-level code with `created: null`), incl. the
  PNG metadata embed and the json clipboard copy in the removed
  @excalidraw/utils wrappers
- SVG embed now serializes the restored, non-deleted elements (same as
  master's utils path), PNG embed serializes restoreElements(data.elements)
  and json clipboard copies the non-deleted restored elements
- port #12060's utils tests to the new API, observing renderers/serializer
  instead of the (now identical) scene export functions

Claude Opus 5.5 (xhigh)

* make config.canvasBackgroundColor the single export background knob

- background resolved once in configExportDimension; exportToSvg no longer
  reads appState.exportBackground (now omitted from ExportSceneData's
  appState type), and honors `canvasBackgroundColor: false`
- outline arrowheads (circle/triangle/diamond_outline, cardinality_zero_*)
  are filled with the resolved background color, also when the background
  isn't painted (as on master) — the canvas path filled them transparent,
  showing the arrow's shaft through them; SVG ignored the config color
- JPEG: `canvasBackgroundColor: false` falls back to the default background
  (+ warning) instead of rendering black; no more warning for the default
- callers (exportCanvas, ImageExportDialog, library/chart/publish previews,
  AI) pass the background via config only

Claude Opus 5.5 (xhigh)

* resolve export theme before creating frame labels

- frame labels were colored by appState.exportWithDarkMode, ignoring
  config.theme (export dialog's dark toggle), and in dark exports the
  dark-theme label color got dark-filtered again by the renderer
  (#7a7a7a -> #848484)
- theme is now resolved once in configExportDimension; dark labels are
  pre-reverted so they render with the editor's frame name color

Claude Opus 5.5 (xhigh)

* drop appState.exportWithDarkMode theme fallback in export

- `config.theme` is the only theme input (defaults to light); the type
  already omitted `exportWithDarkMode` from `data.appState`
- callers that relied on the fallback (PublishLibrary item preview, AI
  frame export) pass `theme` explicitly, keeping their behavior

Claude Opus 5.5 (xhigh)

* exportToClipboard: don't restore appState before exportToSvg (already restored there)

Claude Opus 5.5 (xhigh)

* remove unused getExportSize, type getCanvasSize's [x, y, width, height] (not Bounds)

Claude Opus 5.5 (xhigh)

* fix typos in export comments/JSDoc

Claude Opus 5.5 (xhigh)

* port with-script-in-browser example to the new export API

- exportToSvg/Blob/Canvas/Clipboard take `{ data, config }`; theme via
  `config.theme`, padding 10 kept explicit (new default is 0)
- dropped the no-op `width`/`height` appState overrides from the SVG export

Claude Opus 5.5 (xhigh)

* call getDimensions once, ignore it when widthOrHeight is set

- with an explicit `fit: "contain"`, getDimensions ran twice (the second
  time on its own output); it now supplies width/height (+ scale) once,
  and `contain` scales the content into that frame
- getDimensions + widthOrHeight both applied; widthOrHeight now wins (with
  a warning), like width/height/maxWidthOrHeight already did
- `fit: "none"` (the E+ export server path) renders identically

Claude Opus 5.5 (xhigh)

* type the export size options as mutually exclusive

- `width`/`height`, `maxWidthOrHeight`, `widthOrHeight` and `getDimensions`
  are now a union where the unused options are `?: never`, so combining
  them is a type error (runtime still ignores/warns for JS callers)
- the `never` keys are remapped over `keyof`, so every option keeps its
  JSDoc in completions & hovers (verified via the TS language service)

Claude Opus 5.5 (xhigh)

* replace getDimensions with a `scale` function, round the canvas size

- `config.scale` also accepts `(width, height) => number`, called with the
  export dimensions at scale 1 (padding included) — covers content-dependent
  scaling (e.g. E+'s pixel budgets) without the getDimensions round trip
- remove `getDimensions`: its returned `scale` replaced `config.scale` and
  multiplied the returned size, so callers thinking in output pixels had
  to divide it back out
- exportToCanvas rounds the canvas size instead of letting the canvas
  truncate it (float noise like 28.999999999999996 lost a pixel); half
  pixels now round up (frame label area 20.5px, mermaid preview 89.5px)

Claude Opus 5.5 (xhigh)

* align fit/padding/position export docs with the code

- `fit` defaults to "contain" when `width`/`height` is set, otherwise
  "none" (the doc said the opposite); dropped the unimplemented
  "x/y force none" claim
- padding is always subtracted for `maxWidthOrHeight`/`widthOrHeight`
  (they ignore `fit`), not added
- `position` values are "center" | "topLeft" (doc listed "none")

Claude Opus 5.5 (xhigh)

* derive a missing export width/height from the aspect ratio

- with `fit: "contain"` (the default when a size is given), passing only
  `width` or `height` now derives the other from the content's aspect
  ratio, as documented — it defaulted to the content's unscaled size,
  e.g. `{ height: 50 }` of 200x100 content gave a 200x50 canvas with
  60x30 content in wide empty bands (now 80x50 with padding 10)
- with `fit: "none"` the missing side stays the content extent

Claude Opus 5.5 (xhigh)

* default export `fit` to "none" when x/y is set

- `{ x, y, width, height }` was scaled to fit with `x`/`y` silently
  ignored (`fit` defaulted to "contain" whenever a size was given); an
  explicit region now crops by default, `fit: "contain"` still opts in
  (and ignores `x`/`y`, as now documented)

Claude Opus 5.5 (xhigh)

* ignore export x/y when the content is scaled to fit

- with `fit: "contain"`, `maxWidthOrHeight` or `widthOrHeight`, `x`/`y`
  shifted the fitted content by a mix of scene units and pixels; they're
  now dropped with a warning, which the previous commit already
  documented (its "ignored" claim was based on a coincidental probe)

Claude Opus 5.5 (xhigh)

* pad an export x/y region on all sides

- with `position: "center"` (the default), the centering adjustment also
  ran on axes pinned by `x`/`y`, shifting the region by the padding: the
  canvas started exactly at `x`/`y` with all padding on the right/bottom
  (`position: "topLeft"` padded all sides); `position` now only applies to
  unpinned axes, as documented

Claude Opus 5.5 (xhigh)

* consolidate exportToCanvas/Blob/Svg into exportScene({ type, data, config })

- `type: "canvas" | "png" | "jpeg" | "svg"`, return type per `type`
  (overloads, plus a union overload for dynamic callers); per-type options
  are type-checked: `quality` only for jpeg, `embedScene` for png/svg,
  `renderEmbeddables`/`skipInliningFonts`/`reuseImages` for svg
- the format replaces `mimeType` (and its "image/jpg" typo handling)
- `appState.exportEmbedScene` -> `config.embedScene` (no longer read
  from `data.appState`); `data.files` is optional
- `config.createCanvas` -> `config.environment.createCanvas` (where
  #11997's render environment and richtext's text metrics will go)
- exportToCanvas/Blob/Svg are no longer exported (no aliases);
  exportToClipboard stays, routed through exportScene
- callers, the with-script-in-browser example and tests ported

Claude Opus 5.5 (xhigh)

* export: take only scene settings from data.appState, frameRendering -> config.frames

- `data.appState` is typed as (and stripped at runtime to) the scene
  settings a .excalidraw file stores; the renderer state is built from
  those + defaults, so editor state passed in (a live AppState: open
  dialog, cropping, frame highlight…) can't affect the output. It only
  supplies the default background and what `embedScene` embeds
- `appState.frameRendering` -> `config.frames: { name, outline, clip }`
- editor callers translate their export settings via one internal
  `getExportConfigFromAppState`; `exportCanvas` drops its redundant
  exportBackground/viewBackgroundColor options
- fixes the editor's SVG export ignoring `exportScale` (the branch's
  exportCanvas SVG path had dropped it; master passed it)

Claude Opus 5.5 (xhigh)

* type export positioning options as width/height-only

- `fit`, `position`, `x`, `y`, `origin` move into the size-option union's
  `width`/`height` member, so combining them with `maxWidthOrHeight` /
  `widthOrHeight` (which ignore them) is a type error; runtime unchanged
  (still ignores/warns for JS callers)
- docs stay visible in completions (same `keyof` remap as the size options)

Claude Opus 5.5 (xhigh)

* move export `loadFonts` into `config.environment`

- font loading is a host capability, next to `createCanvas` (svg gets
  `environment: { loadFonts }` only, as it creates no canvas)

Claude Opus 5.5 (xhigh)

* changelog: export API (exportScene, config, migration table)

Claude Opus 5.5 (xhigh)

* export: default padding back to 10, canvas at least 1px

- export bounds are geometric (no stroke width), so padding 0 cut half
  of the strokes on the content's edge and made flat content (a line)
  zero-height — the library/chart/publish previews, which inherited
  master's default of 10, rendered blank thumbnails for lines
- canvas dimensions never go below 1px (a 0-sized canvas can't be
  encoded); SVG snapshot sizes are back to master's

Claude Opus 5.5 (xhigh)

* export: clamp padding against the fitted size, before deriving from it

- the padding clamp used the content's unpadded size, so flat content (a
  horizontal line) lost its padding with maxWidthOrHeight/widthOrHeight
  (`{ padding: 10, maxWidthOrHeight: 400 }` → 200x0); it now only
  considers the target being fitted into
- a missing width/height was derived before clamping
  (`{ height: 10, padding: 10 }` → 0x10, SVG viewBox NaN/Infinity)
- a zero-extent content axis no longer yields a NaN scale under contain

Claude Opus 5.5 (xhigh)

* export: clip frame exports to the frame

- frame clipping was off for exportingFrame (to avoid the rounded
  per-element clip), relying on the canvas/SVG edge — which only crops
  when the export is exactly the frame; a larger export (size options)
  painted children and overlapping elements into the margins, and SVG
  frame exports had stopped clipping (master clipped them)
- everything drawn is now clipped to the frame with square corners: a
  canvas clip after the background (`renderConfig.clipToFrame`), and an
  SVG group with the frame's clip path; the background isn't clipped

Claude Opus 5.5 (xhigh)

* changelog: fill in PR numbers for the placeholder links

Claude Opus 5.5 (xhigh)
2026-10-18 20:24:17 +02:00
c10499eebb feat(editor): text tool edits what you click, shows what a click will do, drags out free text in shapes (#12082)
* fix(editor): do not inherit groupIds/angle from container if not binding the text to it

* fix(editor): text tool edits a label only when clicking the label itself

Clicking elsewhere inside a labeled container creates an independent text at
the clicked position, with no modifier needed (so also on touch/pen), instead
of warping to the label center and editing the existing label.

* feat(editor): highlight what the text tool would act on when hovering

Mirror the click logic on hover: the text element a click would edit gets
a box highlight, and an empty text-bindable container the click would bind
into gets the same binding highlight arrows use (box for arrow containers,
which the binding outline renderer can't draw). Cleared on pointerdown and
managed only while the text tool owns the interaction, so the frame flows
sharing elementsToHighlight are untouched.

Also documents suggestedBinding, isMidpointSnappingEnabled and
elementsToHighlight in AppState.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): hit-test arrow labels at their derived position

An arrow label's stored coords aren't updated when the arrow moves
(dragging deliberately skips them — the position is derived at render
time), which left hitElementItself internally inconsistent: its bounds
pre-check derives the position, but the precise shape test read the stale
stored coords, so a moved arrow's label was only hittable where its old
and new rects overlap. Substitute the derived coords for the precise test,
and skip the version-keyed point cache for container-bound labels (their
position can change without a version bump, mirroring ElementBounds).

Pass the elements map through renderElementsBoxHighlight so the highlight
box is also drawn at the label's derived position.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): dashed text box for the text-tool hover, cleared when the tool is left

The text a click would edit gets the same subtle dashed box as a wrapped text
being edited (at the label's derived position for arrows), instead of the
frame-style bounding box. The hover highlights are cleared whichever way the
text tool is left — Esc/finalize paths bypass setActiveTool.

* refactor(editor): dedupe container-bound label position derivation

An arrow label's accurate position is derived from the arrow rather than
stored on the element (dragging deliberately skips updating it), and three
call sites hand-rolled the same clone-with-derived-coords dance. Extract it
into getTextElementWithAccuratePosition next to the existing position
helpers.

Also routes the text-tool dashed hover highlight through the helper, so it
renders at the label's actual position instead of its stale stored coords.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): defer text-tool hover highlights to arrow text anchors

The arrow-endpoint text binding (#11777) added its own text-tool hover
affordance (hoveredArrowTextAnchor) covering arrow endpoints and midpoint
label anchors, and gave endpoints precedence on click. Mirror that in the
text-tool highlight updater: skip positions where a bindable endpoint would
take the click, and leave arrow anchors (endpoint & midpoint label) entirely
to the anchor affordance — the highlights now only cover editing an existing
text and binding into an empty non-arrow container.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(editor): make endpoint-vs-text preference z-aware to prevent affordance flicker

The endpoint offer was suppressed by any text element under the cursor,
regardless of stacking order. With an arrow drawn over an existing text,
the text's bbox edge cut into the endpoint's hit circle, and slow pointer
movement flickered between the endpoint anchor and text editing.

Drop the z-blind text check and let the existing z-order occlusion rule
decide: whatever is stacked above owns its hit area. A text above the
arrow keeps its edit behavior; when the arrow is on top, its endpoint has
full preference over the whole hit circle, matching the drawn highlight.
Container-bound labels need no special casing — a labeled container is
interior-hittable, so the occlusion rule already yields to labels whose
container sits above the arrow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(editor): defer a text-tool center click so a drag creates free text

A click on an empty container's center still binds a label, but the decision
now waits for pointerup: dragging horizontally past the autowrap threshold
instead creates an independent fixed-width text at the pointer origin, and
small shapes are no longer provisionally enlarged while dragging. Tool
changes and the missing-pointerup cleanup cancel the pending click.

Shape hover and pointerdown share the empty-container / center-snap / Alt
eligibility via getTextCreationContainerAtPosition, and the hover indicators
are cleared when deferred creation starts (stale outline under a locked tool
at 200% zoom).

Also: a direct collision-cache regression for labels of moved arrows, exact
width/position assertions for drag creation across mouse/pen/touch,
labelPosition coverage at 25/50/75%, and stale AppState comments fixed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(editor): redraw the dragged text box under a locked text tool

Drag-sizing a new text mutates it in place without informing the scene, so
sceneNonce never moves and the interactive canvas — which draws the dashed
text box from the live element but memoizes on canvasNonce — kept the first
size. Unlocked drags only repainted by accident: the tool had already
reverted to selection, whose box-selection branch churns state every move.

Fold the new text's versionNonce into canvasNonce, as is already done for
framed new elements. Four rendering regressions cover successive drag widths
on empty canvas and from a container's center, locked and unlocked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(editor): extract the text tool's pointer interaction into AppTextTool

Pure move of handleTextOnPointerDown, resetTextTool, maybeStartPendingText,
maybeUpdateTextToolHighlightOnPointerMove and getTextCreationContainerAtPosition
into App.textTool.ts — only `this.` became `this.app.` and the names lost
their "Text" prefix. maybeDragNewGenericElement is now public so the pending
center-drag can re-dispatch to it. No behavior change.

Text editing itself (startTextEditing / handleTextWysiwyg and the hit
helpers) is shared with Enter, double-click and the other entry points and
stays in App.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(editor): resolve text-tool targets once; single textToolHover state

What a text-tool click would do at a position was computed in three places
— the text/container highlight, the arrow-anchor hover and the pointerdown —
with different modifier awareness, and shown through three AppState fields
(elementsToHighlight, suggestedBinding, hoveredArrowTextAnchor) cleared from
seven sites. The affordance could promise what the click wouldn't deliver:

- pressing or releasing Alt without moving left the container outline stale
  (and Ctrl/Cmd left the text box stale)
- Alt-hovering an empty arrow's midpoint advertised a label while the click
  created free text
- Ctrl/Cmd hid the container outline (the renderer gates suggestedBinding on
  isBindingEnabled) while the click still bound a label
- over a shape's or arrow's label the cursor was a crosshair while the box
  promised an edit

AppTextTool.getTargetAt now answers the question once — endpoint → text →
empty centered container (unless Alt) → free — and feeds the hover, the
cursor and the pointerdown. The hover lives in one field, textToolHover
(replacing hoveredArrowTextAnchor), drawn by one renderTextToolHover branch;
it is cleared on pointerdown and when the tool is left, nowhere else. Alt and
Ctrl/Cmd keydown/keyup refresh it at the last pointer position. The scene is
hit-tested once per pointermove instead of twice.

Also drops AppArrowText's anchor-state members (the resolver covers them) and
getLabelCenter, dead since the label-center warp was removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(editor): let AppTextTool own the pending center click; explicit textElement

The deferred center click was threaded through PointerDownState.text and a
textCreation parameter that made startTextEditing — the entry point shared
with Enter, double-click, sticky-note drops and autoshape — re-derive what the
text tool's pointerdown had already established, then bail. The helper now
arms `pending` itself (like AppBucketFill), decides it on pointermove/up, and
PointerDownState loses its text slot. The affordance armed at pointerdown now
stays up until the click resolves instead of being cleared and re-shown.

startTextEditing takes an explicit `textElement` instead: the tool passes
the text it resolved, or null to create — so a text selected through the
host API can no longer hijack a text-tool click elsewhere (the Enter and
double-click paths, which leave it undefined, resolve as before). The same
selection shortcut leaves getTextBindableContainerAtPosition, which is now
purely positional; the double-click path keeps it inline.

Also: a pending container deleted mid-press (collab) now still ends the
tool's turn, and the drag threshold's choices are documented in place.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(editor): ctrl/cmd, not alt, opts out of labeling a container with the text tool

Ctrl/cmd already turns off arrow-endpoint binding for the text tool; it now
also keeps a center click from binding a label to an empty container, so one
key means "no binding" throughout the tool, as elsewhere in the editor. It is
read off the event rather than the arrow-binding preference — a label is not
an arrow binding. Alt no longer plays a part in the text tool, so its hover
refresh hooks go (alt keeps its generic drag meaning: resize from center).
The double-click path is unchanged.

Ported from #11210.

Co-authored-by: Loric Brevet <loric.brevet@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(editor): cancel a pending text-tool click on tool exit and pointer cancel; refresh hover and cursor when the viewport moves

Three lifecycle gaps in the text tool's hover and pending center click:

- A pending center click only checked the active tool at the next pointer
  event, so leaving the tool (Esc) and picking it again before releasing
  revived the old press: moving then created free text at the original
  position. And with no pointerup after a pointercancel (scroll, palm
  rejection), the pending click stayed armed for unrelated movement. The
  helper now has an explicit cancel, called when the tool is left and on
  pointercancel next to the bucket fill's.
  The cancel also clears the outline the armed click had set, which no
  hovering pointer would clear otherwise — including when a second finger
  turns the press into a pinch/pan (that discard goes through the same
  cancel).
- The hover was refreshed on pointer moves and modifier keys, but not when
  the viewport scrolled or zoomed under a still pointer, so an outline stayed
  on a shape that had moved away. The refresh now re-derives the scene point
  from the pointer's last viewport position, remembers the last modifiers,
  and runs from the viewport-change lifecycle.
- A modifier press changed the target without updating the cursor (an
  endpoint's pointer cursor survived Ctrl). The refresh sets the cursor from
  the same target, mirroring the pointermove path and leaving a panning or
  scrollbar gesture its own cursor.

Also fixes a stale renderer comment about the container outline's binding
gate.

Tests: tool exit and re-pick mid-press, pointercancel, wheel scroll and zoom
under a still pointer for container and text targets, cursor on modifier
press and release. Verified in Chromium as well.

Claude Fable 5.1, high effort
Claude Opus 5.5 (1M), xhigh effort (moved the snapshot update to the caret-overflow branch)
Claude Opus 5.5 (1M), xhigh effort (the cancel clears the armed outline, from the 2026-09-26 review)

* fix(editor): tear a gesture down on pointercancel; a replayed cleanup no longer counts as a click

A canceled press (the browser taking the pointer for a scroll, a pinch or
palm rejection) left the gesture's window listeners and its missing-pointerup
cleanup subscription in place. The next pointerdown then replayed the old up
handler, whose end-of-gesture revert switched the tool to selection while the
new press was being armed — so after a canceled center press with the text
tool, the next center click switched tools and created nothing.

pointercancel now runs the missing-pointerup cleanup immediately, so nothing
of the canceled gesture lies in wait. And the up handler's tool revert is
gated on a genuine pointerup: a replay by the cleanup — a second finger
landing, a pointercancel, the next press after a lost pointerup — only tears
the gesture down and leaves the tool to the press that follows, instead of
reverting it under that press.

The pointercancel regression no longer sends a synthetic mouse-up that hid
this: after the cancel the tool must still be the text tool and the next
center click must bind a label. It fails without the fix.

Claude Fable 5.1, high effort

* refactor(editor): cursor from the text-tool target alone; a hover refresh needs a hovering pointer

Follow-ups from the whole-branch simplification review (simplification.md),
narrowed with the reviewer:

- The cursor is derived from the resolved target only. The old fallback to
  the plain hit under the pointer kept a text cursor over other texts while
  editing or drag-sizing — where a click has nothing to point at — and forced
  a second hit test and a coordinate recomputation on every modifier press
  and viewport change. Both go. With a locked text tool, editing or dragging
  now shows the crosshair everywhere; hover, endpoint and label cursors are
  unchanged.
- The hover refresh takes the pointer's last viewport position only. The
  fallback to the last *scene* coordinates was reachable when a tap preceded
  any pointer movement, and after a viewport change it resolved at a position
  nothing hovers — a phantom affordance. A tap leaves nothing to refresh,
  pinned by a test that fails with the fallback in place. Nor does a finger
  ever hover: a touch's last move is where it lifted, so touch input doesn't
  count as a hovering pointer (mouse and pen still refresh). The test drives
  real touches — a tap with jitter on a text, a short drag from a center —
  instead of clearing the pointer bookkeeping by hand. The pointercancel
  test now checks the armed outline is gone right after the cancel, and a
  second-finger test covers the missing-pointerup discard.
- The explicit pending-click cancel on pointercancel stays, and its reason is
  written down: the gesture teardown that follows flushes a queued
  pointermove, which would otherwise find the click still pending and resolve
  it as a drag. Verified in Chromium by the reviewer; the test harness runs
  the throttle synchronously, so it cannot be reproduced there.
- Two duplicated test scenarios removed: the arrow-anchor hover test the
  endpoint suite already covers, and a repeated Ctrl assertion.

Claude Fable 5.1, high effort
Claude Opus 5.5 (1M), xhigh effort (touch input never hovers; tests, from the 2026-09-26 review)

* refactor(editor): the text tool refreshes its own hover and cursor; viewport navigation check lives in AppPan

App's `refreshTextToolHover` was text-tool logic kept in App only because
its cursor gate read App-private state. It moves into `AppTextTool` as
`refresh(modifiers?)`, which re-resolves the hover and sets the cursor that
goes with it, taking the place of `refreshHover`.

The gate — space held, a pan in progress, a scrollbar drag, the hand tool —
is the same check the canvas pointermove makes before touching hover or
cursor. It becomes `AppPan.isNavigating()`, used by both. The scrollbar drag
stays App's own and reaches AppPan as a dependency, like the pointer count.

No behavior change.

Claude Opus 5.5 (1M), xhigh effort

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Loric Brevet <loric.brevet@gmail.com>
2026-09-26 12:38:23 +02:00
David LuzarandGitUserVivek 5db42c3ddb feat(editor): zen mode polish, hide toolbar hints preference & delayed canvas tooltips (#12165)
* feat(editor): preference to hide toolbar hints

Add a "Show hints" checkbox to the Preferences menu, backed by a new
browser-persisted `appState.showHints` (default true). When off,
HintViewer renders nothing. Also exposed as `Preferences.ToggleShowHints`
for hosts composing their own preferences list.

Opus 5.5 (1M context), low

* fix(editor): keep main menu open when toggling view mode

Entering view mode ran terminateActiveInteraction, which reset
`openMenu` and closed the main menu the user had just toggled view mode
from (Preferences submenu). The main menu stays available in view mode,
so only close it when interaction gets disabled.

Opus 5.5 (1M context), low

* feat(editor): chrome-less canvas controls & in-place fades in zen mode

- UI hidden in zen mode fades out in place (opacity + visibility) instead
  of sliding off the viewport, and is non-interactive from the start of
  the fade
- visible canvas controls (toolbar, main menu trigger, zoom, scroll back
  to content, exit zen mode) drop their background/border/shadow;
  hover/active states still apply, popups & dialogs are unaffected
- only the zoom level stays visible out of the zoom controls
- chrome fades are applied only while toggling (data-zen-mode-transition
  on the container, set before the DOM update) so hover transitions stay
  instant
- single ZEN_MODE_TRANSITION_DURATION, exposed to CSS as
  --zen-mode-transition-duration
- zoom & undo/redo button groups paint a single background (on the
  group) instead of one per button

Opus 5.5 (1M context), low

* feat(editor): shift zoom level into the zoom-out slot in zen mode

With the zoom +/- buttons hidden in zen mode, slide the zoom level over
to the left edge of the group. The shift is applied to the Tooltip
wrapper (new `className` prop) rather than the button, so the tooltip
(positioned off the wrapper) moves along.

Also give the reset-zoom button its own hover background: unlike the
+/- buttons it has no .ToolIcon__icon, and lost its (incidental) hover
once the per-button backgrounds were removed.

Opus 5.5 (1M context), low

* feat(editor): delayed tooltips on canvas buttons

- Tooltip gets a `delay` prop: shows after 500ms, or right away if a
  tooltip was visible within the last 300ms (moving across adjacent
  buttons); passing over a button quickly doesn't flash one
- enable it for the zoom, undo/redo & help buttons
- zoom +/- use our Tooltip instead of the native `title`, and the reset
  zoom button drops its native `title` (it was showing both)
- zen mode now hides the zoom +/- tooltip wrappers rather than the
  buttons, so the invisible wrappers don't show tooltips

Opus 5.5 (1M context), low

Co-authored-by: GitUserVivek <jagtapvs9832@gmail.com>
2026-09-25 00:42:59 +02:00
David LuzarandMark Tolmacs 1118751f3e feat(editor): customizable font picker top picks (#12160)
* feat(editor): customizable font picker top picks

Font families can be dragged from the font picker list onto the top-picks
strip to replace a pick, the strip can be reordered by dragging, and
right-clicking it resets to the default picks — same as the color picker
top picks. The drag ghost is only the font's icon (on a strip-button tile).

The color picker's drag & drop is generalized into
components/TopPicksDnD (value-agnostic hook, reorder preview, drop outline,
reset context menu, shared styles), with thin per-picker adapters
(colorTopPicksDnD / fontTopPicksDnD) supplying value equality and the
ghost visual.

Custom picks are kept in `appState.fontTopPicks` (browser-persisted only,
kept on canvas clear); restore drops unknown/internal/fallback families,
dedupes, and caps at the strip's 3 slots.

Claude Opus 5.5 (1M context), reasoning effort: 40

* feat(editor): reset link in top picks tips

The "drag to pin" tip in the font picker and color picker popups now ends
with a plain-text "Reset" link while that picker's top picks are
customized, restoring the defaults without leaving the popup.

Shared as TopPicksTip (tip text styles merged into it). The link never
takes focus on click and hands it back to the picker when activated by
keyboard, since it unmounts on reset; Enter/Space are kept from the
pickers' key handlers.

Claude Opus 5.5 (1M context), reasoning effort: 15

* fix(editor): don't clip or overlap font list item focus/pressed rings

The font list scrolls (clipping outer box-shadows at its edges) and each
item paints over the previous one's bottom edge, so the focus-visible and
:active rings are drawn inset there. Pressing and holding items became
common with dragging fonts onto the top picks.

Claude Opus 5.5 (1M context), reasoning effort: 15

* fix(editor): cancelled top picks drag flying to viewport top-left

The ghost (and its home rect) was built on drag activation, by which time
the source could have been re-rendered away — the font list re-renders
on hover — and a detached element measures as a zero rect. Build and
measure it on pointerdown instead, while the source is mounted.

Claude Opus 5.5 (1M context), reasoning effort: 15

* fix: Always show 3 slots to reorder

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Cleanup

Inconsistent state updates. Font picks go through useExcalidrawSetAppState directly, while color picks go through the action's updateData.

Duplicate icons in the strip. Virgil and Excalifont both use FreedrawIcon, so pinning both shows two identical tiles that can only be told apart by their tooltips.

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: remove useMemo

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
2026-09-24 15:06:17 +02:00
David Luzar 4e758db17e feat(editor): wheel-button zoom and "zoom with scroll wheel" preference (#12099)
* refactor(editor): move wheel handling out of App.tsx into App.wheel.ts

The wheel handler (pan, ctrl/cmd zoom, shift horizontal pan) and the
non-interactive browser-zoom guard move as-is into a new `AppWheel`
helper (`app.wheel`), in the same shape as `App.cursor` / `App.viewport`.
The zoom math becomes a private `zoomBy(deltaY)` so the upcoming wheel
features (wheel-button zoom, zoom-with-scroll-wheel preference) only add
branches to the dispatch instead of another copy of it.

`isPanning` is module state in App.tsx, so it is handed in as a
dependency the way `AppViewport` gets `isGestureActive`; the debounced
`shouldCacheIgnoreZoom` reset is shared with the touch pinch handler and
becomes public. No behavior change.

Claude Fable 5.1, high effort

* feat(editor): zoom by scrolling with the wheel button held down

Scrolling while the wheel (middle) mouse button is held zooms the canvas
around the pointer, exactly like ctrl/cmd+wheel. The button is what
starts a drag-pan and the wheel sits under the same finger, so a pan can
be zoomed one-handed without reaching for a modifier. It wins over the
modifiers (shift included) and over the drag-pan's own wheel gate, which
until now swallowed all wheel input mid-pan. It is read off
`event.buttons`, so no extra state is tracked.

Zooming mid-pan exposed three ways the point grabbed by the pan could
drift from under the cursor, compounding with every tick while the
pointer moves between zooms:

- The drag-pan wrote its scroll as a plain object computed from
  `this.state`. Under React 19 a wheel zoom is flushed asynchronously,
  and the pan's animation frame runs before that flush, so the pan
  overwrote the zoom's anchoring scroll with a pre-zoom value — the zoom
  then happened around the viewport origin instead of the cursor. The pan
  is now a state updater.
- A pointer move still waiting for its frame landed after the zoom, which
  had anchored on a viewport that didn't include it. The wheel handler
  flushes the pending move first.
- The zoom updater read `viewport.lastPosition` when React evaluated it,
  by which time the pointer had moved on; it also started from
  `app.state.zoom`, so ticks queued before a flush collapsed into one.
  Both are now taken at wheel time / from the updater's own state.

Tests: plain wheel pans, shift pans horizontally and ctrl/cmd zooms
(pinning the baseline); with the button held, scrolling zooms in and out
regardless of shift; during a wheel-button drag-pan, plain wheel input is
still swallowed while wheel-button scrolling zooms, and panning is back
after release. Two regression tests pin the grabbed point under the
cursor: one batches the zoom with a pan landing in the same React flush,
one holds the pan's frame back until after the wheel fires (through a
file-level `withBatchedUpdatesThrottled` mock, since the test setup makes
the throttle synchronous). Both fail on the previous code by 11.8 and
2.7 scene units respectively.

Claude Fable 5.1, high effort

* feat(editor): "zoom with scroll wheel" preference

Adds `appState.zoomWithScrollWheel` (default `false`, persisted to
browser storage like the other preferences), for mouse users who would
rather scroll to zoom: a plain wheel zooms around the pointer, and
ctrl/cmd+wheel pans vertically instead — the two roles swap, the way
tldraw's wheel-behavior preference does it. Shift+wheel keeps panning
horizontally, and the wheel-button zoom keeps winning over everything.
One consequence to be aware of: a trackpad pinch arrives as ctrl+wheel,
so with the preference on it pans.

The toggle lives in the main menu's preferences submenu ("Zoom with
scroll wheel", right under "Select on"), exposed as
`MainMenu.DefaultItems.Preferences.ToggleZoomWithScrollWheel` and backed
by the `zoomWithScrollWheel` action, allowed in view mode since it only
concerns navigation.

Tests: the preference is off by default and its action toggles it; with
it on, plain wheel zooms while ctrl and cmd wheel pan vertically, shift
still pans horizontally and the wheel button still zooms. Snapshots pick
up the new default.

Claude Fable 5.1, high effort

* perf(editor): keep the zoom bitmap cache across pan frames during a wheel zoom

`viewport.translate` cleared `shouldCacheIgnoreZoom` on every call. Wheel
zoom sets the flag so elements draw from zoom-scaled cached bitmaps, but
a drag-pan frame landing between two ticks cleared it again, and the
static renderer then re-rasterized every element at a zoom the next tick
was about to change. Wheel-button zoom-while-panning interleaves the two
by design (the old `isPanning` gate made the combination impossible with
a mouse); trackpad pan-plus-pinch has always alternated plain and ctrl
wheel events the same way.

Measured with 1,200 rough shapes on screen, a 12-tick zoom with a small
pointer move on every frame: 10,434 / 8,774 bitmaps regenerated in two
runs, longest frame 242 / 283 ms, gesture 2.9 / 2.3 s. With the pointer
still: 0 bitmaps, 9 ms, 0.39 s.

The reset now happens only when a gesture interrupts an animated
`setViewport`, whose frames set the flag and whose completion handler
would have cleared it. Everywhere else the flag is left to the zoom paths
and their 300 ms debounce. After the change the moving-pointer run
regenerates 0 bitmaps, longest frame 25–33 ms, gesture 0.7–0.8 s. A
plain wheel pan within 300 ms of a zoom now draws from scaled bitmaps
until the debounce fires instead of re-rasterizing on its first frame.

Tests: pan frames between wheel-button zoom ticks keep the flag until the
gesture is idle (fails on the previous code); the interrupted-animation
test also pins the reset, with a 60 s animation and a polling assertion
so a saturated machine cannot finish the animation before the check.

Claude Fable 5.1, high effort

* fix(editor): handle wheel input over a frame label once

The frame label had a React `onWheel` handler while also sitting inside
the editor container, whose native wheel listener already accepts
`.frame-name` targets. Both received the same event, so every wheel tick
over a label panned or zoomed twice (measured: −140 px of scroll for a
tick that moves the canvas −70 px elsewhere). React registers `wheel`
as passive, so the React path could not even preventDefault. Predates
this branch; with the zoom-with-scroll-wheel preference it became a
two-step zoom per tick over labels.

Drop the prop. With no React path left, the wheel handler's event type
narrows to the native `WheelEvent`.

Test: a wheel event on a frame label pans one step; it fails on the
previous code by exactly one extra step.

Claude Fable 5.1, high effort

* refactor(editor): model the wheel preference as an input device

    `appState.zoomWithScrollWheel` becomes `appState.inputDevice:
    "auto" | "mouse" | "trackpad"`.

    Claude Fable 5.1, high effort

* fix(editor): decide whether a translate zoomed against the state it applies to

`viewport.translate` captured `prevZoom` from `app.state` before queuing
its update, then a second updater compared it with the zoom it found in
`prevState` to tell a zoom apart from a pan — a zoom drops the rubberband
overscroll to 0, a pan keeps it. With a zoom queued in the same React
flush, `app.state` still holds the pre-zoom value, so a pan applied on
top of that zoom read as a zoom of its own and hard-clamped the
overscroll away in one frame instead of easing it back. Two wheel events
in one flush could already do this; the wheel-button zoom makes it
routine, as the handler applies the pending pan move on every tick.

The two `setState` calls become one updater: the update is applied to
`prevState`, the zoom comparison is between that state and the result,
and the constrained viewport is merged into it. No captured state, no
ordering to get wrong.

Test: rubberbanded past the box edge, a ctrl+wheel zoom tick and a wheel
pan batched into one flush keep the overscroll; on the previous code it
lands exactly on the edge.

Claude Fable 5.1, high effort

* fix(editor): pan sideways on a horizontal-only wheel; no-op zoom ticks at the limit

In mouse mode a wheel event with only a horizontal delta — a tilt wheel,
a sideways two-finger scroll — took the zoom branch with `deltaY` 0: no
viewport change, but every such event flipped the bitmap-cache flag,
cancelled collab follow and re-armed the cache debounce, and the sideways
scroll itself went nowhere. A wheel with no vertical delta never zooms
now; it falls through to the pan, which moves the canvas by `deltaX`. A
wheel with no delta at all is ignored outright.

The zoom updater also returns early when the tick lands on the zoom it
started from — at MIN or MAX zoom — instead of setting the cache flag and
redrawing the scene on every tick. This predates the branch. To make
that a real no-op, `viewport.translate` treats a `null` update as nothing
to do rather than re-constraining and re-setting the whole state.

Tests: in mouse mode a horizontal-only wheel pans sideways and leaves the
cache flag alone; a zoom-out tick at MIN_ZOOM changes nothing. Both fail
on the previous code.

Claude Fable 5.1, high effort
2026-09-14 16:17:41 +02:00
David LuzarandClaude Fable 5.1 afa3a653fc feat(editor): sticky notes (#12064)
* feat(editor): sticky notes

* clamp sticky note font size

* [dragInput] reject non-finite values

* chore: prettier formatting after rebase

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(editor): sticky notes cleanup (plan §7.1)

- hide `transparent` in the sticky stroke picker through master's
  `excludedColors` (#11872) instead of the branch's own
  `hiddenPaletteColorNames`; the swatch becomes a grid-preserving
  placeholder, its hotkey stays dead, and arrow-key navigation steps over
  it in the arrow's direction (unit + integration tests)
- move the text pixel-snap change out to `dwelle/text-pixel-snap`
  (705e497e2): it affects every text element at every zoom and is not
  zoom-aware, so it lands separately
- rename `STICKY_NOTE_DEFAULT_FONT_SIZE` → `STICKY_NOTE_FALLBACK_FONT_SIZE`:
  it is only the non-finite fallback; new sticky text seeds its ceiling from
  `currentItemFontSize` (decision D3)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(editor): one sticky note layout core + resize intents (plan §7.2, 1/2)

- `getStickyNoteLayout` is the single fit/grow/position calculation (pure;
  ceiling-anchored size grid, warm-started binary search instead of the
  linear descent); `updateStickyNoteLayout` applies it to a scene and runs
  the bound-arrow pass by default (callers forward `simultaneouslyUpdated`
  or opt out when they own a pass); `relayoutStickyNotes` applies it inside
  immutable element arrays, identity- and version-preserving
- resize intents are absolute values recomputed from the gesture-start
  snapshot on every pointer-move (`getStickyNoteResizeIntent`): width-only
  gestures keep the base height, height gestures set it, proportional
  gestures (Shift on any handle, aspect-locked multi-select, Stats group)
  also scale the font ceiling, flips preserve everything; the content
  correction anchors at the held edge (n* → bottom, Alt → center,
  Shift+E/W → center). The corner pre-fit clamp and the multi-resize
  sentinel/veto are gone: one fit per pointer-move
- arrows bound to a note follow text-driven growth, and multi-resize /
  Stats group update them after the content correction, not before
- `fontSizeMax` is `number | null`; every read/write goes through the
  container-aware `getUserFontSize` / `getUserFontSizeUpdate` (a detached
  label with a stale ceiling neither shows nor keeps writing it); unbind
  clears it and returns an empty note to its base height; restore seeds or
  clears it after binding repair and normalizes a sticky label's stroke
- paste styles lays out a restyled note and its label together in a
  post-pass (no phantom-container mismatch) and reads the source ceiling
  from the copied snapshot rather than the fitted size
- `redrawTextBoundingBox` / `handleBindTextResize` delegate sticky notes up
  front (no wasted generic wrap); the `refreshTextDimensions` sticky arm and
  the wysiwyg duplicate arm are gone; `clampStickyNoteProps` becomes the
  identity-preserving `normalizeStickyNote`

Tests: content-pinned corner drag keeps the requested base, Shift+E scales
note/base/ceiling, width-only drag preserves the base of a grown note,
Shift released mid-gesture restores gesture-start values, flipping a rotated
grown note keeps base/ceiling/label angle, multi-resize syncs an empty
note's base.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(editor): sticky note invariants in changeProperty + regression tests (plan §7.2, 2/2)

- `changeProperty` normalizes sticky notes after any property write
  (identity-preserving `normalizeStickyNote`), replacing the three per-action
  wraps in the background, sloppiness and roundness actions
- tests (new `tests/stickyNotes.test.tsx` + restore): arrows bound to a
  note follow text-driven growth while typing; binding free text seeds the
  ceiling and unbinding clears it (`null`) and returns the note to its base
  height; pasting styles copies the user's ceiling and lays out the note
  together with its label; unselected notes keep object identity through
  property actions; a transparent background write is normalized; restore
  clears a stale ceiling on unbound text and seeds ceiling + visible stroke
  on a sticky label

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(editor): sticky note creation feel + sticky color domain (plan §7.3)

Creation
- a note starts as a 0×0 draft inserted like every other tool and is
  previewed at its true dragged size (no jump to 40px on the first move);
  a gesture under the drag threshold is a click → the default 250×250
  square centered on the pointer; a longer drag snaps to the 40×40 minimum
  only on release, growing away from the origin corner. The preview
  suppression flag and the pointer-move sticky arm are gone
- `normalizeStickyNote` splits into style (constructor + every pass) and
  geometry (finalize, restore, action passes) so a draft is legal

Colors — sticky notes are their own color domain (decision D2)
- new `currentItemStickynoteBackgroundColor` default (yellow) next to the
  stroke one; both persisted/restored non-transparent; new notes and their
  labels seed from the sticky defaults (a label takes the note's stroke)
- own top-pick slots (`colorTopPicks.stickyNoteStroke/Background`) and
  classic note picks; transparent excluded like bucket fill
- one `resolveColorTarget` (regular / sticky / mixed, incl. the text being
  edited) resolved at execution time in the stroke and background actions'
  `perform`; the panel only sends `{ color }`, so the memoized picker's
  stale closure can no longer write the wrong tool's default (A1)
- one `getColorUpdate` policy used by the actions, both eyedroppers (which
  now include bound labels, so a stroke pick colors a note's visible text —
  1.12), paste styles and bind-existing-text (a transparent label bound to
  a note takes the note's text color — A3); the per-surface forks and
  `isStickyNoteStrokeColorElement` are gone

Tests: click under the threshold, 20px drag previews at 20 and snaps to 40
with the origin corner fixed (both directions); closed-popup top pick after
a tool switch writes the sticky default; mixed selection writes both
domains; notes are created from the sticky defaults; bind normalizes a
transparent label; the keyboard eyedropper colors a note's label. App-state
snapshots gain the three new keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(editor): font-aware minimum sticky note size

The 40×40 floor let a freshly placed note grow on the very first keystroke:
one line at the default 20px ceiling plus the 16px padding needs 57px.

- `STICKY_NOTE_MIN_SIZE = 75` replaces the width/height pair as the data
  floor (normalization, layout, restore)
- `getStickyNoteMinSize({ fontSize, fontFamily })` =
  max(75, round(ceiling × lineHeight) + 2 × padding) is the UI floor: the
  creation snap on release uses the current font size/family, the single
  resize floor uses the label's ceiling (constant floor for an empty note).
  Excalifont: 75 up to the L ceiling, 77 at 36px, 92 at 48px

Tests: the 20px drag now snaps to 75; a 48px ceiling yields a 92px note
that keeps its height on the first character.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(editor): sticky note regression matrix (plan §7.4) + two Stats fixes

Tests (27 new): Alt/center resize keeps the center of a content-pinned
note; proportional multi-select scales the label's ceiling, a free
multi-select of an empty note uses the requested height; a proportional
gesture returning to scale 1 restores everything; rotated growth keeps the
top edge; the fit takes ≤ 2 measurements on a warm keystroke and ≤ 12 on a
cold search from a 512 ceiling; a lowered ceiling wins over a still-fitting
old size; odd/fractional ceilings stay reachable on the ceiling-anchored
grid; the font-aware minimum size. Bound arrows follow the note on unbind,
after a content-constrained resize and after a font-size action, and are
not moved twice when resized as part of the same selection. A top pick
while editing a note's label targets the label and the sticky default; the
preview eyedropper colors a selected note's label; paste styles resolves a
since-deleted source's ceiling from the copied snapshot; a detached label
with a stale ceiling neither shows nor writes it; duplication keeps the
ceiling and base height; an empty note escaped from editing stays and is
removed by undo; Stats W keeps the base height and a group W edit scales
the ceiling; restore normalizes both sticky defaults, keeps the new
top-pick slots and refits note + label under `refreshDimensions`; unit
tests for `resolveColorTarget` (regular / sticky / mixed / editing) and the
default normalization.

Source fixes the tests surfaced:
- Stats `DragInput`: the typed-value path handed callbacks the live
  elements map as "original", so gesture-start values (a note's width, its
  label's ceiling) read as the already-applied values; it now snapshots the
  scene like the pointer-drag path does
- Stats group resize (`MultiDimension`): a group unit lists the note's
  label too and scaled it as a standalone element after the note, wiping
  the fitted size; the note's layout owns its label

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(editor): sticky note skeleton API, tool hint, help row, docs (plan §7.5)

- `convertToExcalidrawElements` accepts `{ type: "stickynote", x, y, label?,
  ... }` (new `ValidStickyNote` skeleton member): the note is constructed
  through `newStickyNoteElement` + `normalizeStickyNoteGeometry` (250×250
  default, `baseHeight` finalized, sticky color invariants), and a label is
  bound through the sticky fit so its `fontSize` becomes the ceiling and the
  note grows only if the text overflows at the minimum size (decision D7)
- tool hint for the sticky note tool ("Click to place a note, or drag to
  size it") and the `N` shortcut row in the help dialog
- dev-docs: "Sticky Notes" section in the element-skeleton page; CHANGELOG
  entry (PR number placeholder)

Tests: skeleton defaults, invariant enforcement on given props, label
binding with the ceiling and the note's text color, growth for an
overflowing label.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor: move the seeded PRNG (mulberry32) to @excalidraw/common

Pure utility next to `randomInteger`/`reseed`; the sticky note corner
jitter imports it from there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* refactor(editor): drop the dead roughjs Drawable for sticky notes

Nothing ever generated or drew it: both renderers paint a note directly from
`getStickyNoteRenderPoints`, and every `ShapeCache` consumer is type-gated
to the rectangle family or linear elements. It also disagreed with the
rendered outline (generic corner radius, no jitter). Sticky notes now sit
with text/image/frame in the `null`-shape group, which the cache treats
as a valid value.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(editor): sticky note creation-date footer

Every note reserves a 32px band under its label body and paints the
note's `created` date there, on canvas and in SVG export: "7 Sep" while
the year is the current one, "7 Sep 2025" otherwise, nothing for an
unknown timestamp (the band stays reserved so geometry never depends on
data availability). The label form is chosen by width bucket, never
measured, so painting stays measurement-free — server-side SVG export has
no text measurer — and an absolute date means exports never go stale.

- `STICKY_NOTE_FOOTER` and `STICKY_NOTE_BODY_INSET_Y` in @excalidraw/common;
  `getStickyNoteDateLabel` / `getStickyNoteFooter` in stickyNote.ts
- the layout, `getBoundTextMaxHeight`, the text editor and both renderers
  share the one inset; a rotated label rotates about the note center, not
  the now-asymmetric body center
- `getStickyNoteMinSize` returns `{ width, height }`; creation and resize
  clamp each axis to its own floor (a Shift drag stays square)
- tests: date label, width buckets, body height, resize floor, SVG + canvas
  export, creation minimums

Plan: sticky_notes_review_final.md §9.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(editor): image tool into "More tools", letter shortcut badges

With the sticky tool on N between text (8) and eraser (0), the numbered
badge row no longer reads as a sequence. On desktop and tablet, move the
image tool into the "More tools" menu (first entry, still `9`,
`UIOptions.tools.image` keeps hiding it) and badge the toolbar tools
with their letter shortcuts (V R D O A L P T N E) instead of numbers.
The number keys keep working; tooltips and the help dialog are unchanged.
The mobile toolbar keeps its image slot.

- Toolbar.tsx: image item in `ExtraToolsDropdown` (trigger highlight and
  icon while active), `UIOptions` passed through for the gate
- Tools.tsx: `keyBindingLabel` prefers the letter
- tests: `UI.clickExtraTool`, image test uses it, toolbar describe in
  tool.test.tsx (menu placement, UIOptions gate, letter badges)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(editor): a sticky note and its label share one ink

The creation-date footer paints from the note's `strokeColor`, the label
from its own, and three writes let them drift: coloring the label while
editing it (the wysiwyg clears the selection, so `changeProperty` targets
only the edited text), binding an already-colored text (only transparent
was rewritten), and paste styles from a source whose container and label
carry two colors. Red text under a black date.

The note's `strokeColor` is its ink — the seed for a new label and what
the footer paints with — and the label always mirrors it:

- `syncStickyNoteInk(elements, prevElementsMap)`: after a write, copies
  the side that changed onto the other; when both changed or the data
  drifted, the label wins (it is the text the user styled); a transparent
  label takes the note's color; identity-preserving when nothing changes
- entrances: the stroke action (after `changeProperty`, which covers edit
  mode), paste styles (before the relayout), bind text (the text's color
  wins, transparent falls back), the skeleton path (a label that sets its
  own color gives it to the note), restore (label wins over a drifted note)
- the stroke picker reads "Text color" for notes (`labels.textColor`)
- tests for the helper and for every entrance

Plan: sticky_notes_review_final.md §12.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* make footer opacity=1

* chore: fix sticky note test import order

* feat(editor): drag a tool out of the toolbar to drop a default-sized element

Press a tool button and drag onto the canvas: a default-sized element
follows the pointer as a translucent preview and is placed where the
pointer is released. Sticky notes opt in; the mechanism is generic.

`App.toolDrag.ts` owns the gesture. `DRAGGABLE_TOOLS` is the registry —
a tool provides `createElement(app, center)` (the element it drops,
centered on the pointer, from the same `currentItem*` defaults a click
uses) and an optional `onDrop` (a sticky note starts editing its label).
Adding a tool is one entry.

The preview exists only on the unit until the drop: not in the scene, the
store or the history, so collaborators never see it and Escape,
pointercancel, window blur or a release outside the canvas leave nothing
behind. It is painted by the existing `NewElementCanvas`, drawn exactly as
it will land, with the canvas itself made translucent (a CSS opacity on
the canvas — per-draw-call alpha would darken where a shadow and a fill
overlap).

The button arms on pointerdown (a new `onPointerDown` hook on the toggle
`IconButton`) and the drag starts once the pointer has moved
`DRAGGING_THRESHOLD`, so a plain click still selects the tool. Listeners
live on the owner window like the other pointer gestures. The drop is
`insertNewElement` + one store capture (one undo entry), selects the
element and switches to the preferred selection tool — whatever tool was
active (an arrow, say) must not stay armed under the dropped element or
its editor. The hit test prefers `elementFromPoint` (sees through a
touch's implicit capture on the button) and falls back to the event
target where it is missing (jsdom).

Made public on App for the unit: `store`, `startTextEditing`,
`getCurrentItemRoundness`, `getCurrentItemStrokeWidth`.

Tests: drag → preview with an untouched scene and history → drop centered
at the pointer with one history entry, the editor open and the selection
tool active, undo removes it; Escape mid-drag leaves nothing and a late
pointerup is inert; a release outside the canvas drops nothing; a plain
click still selects.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(editor): default a text element's fontSizeMax to null

`newTextElement` left `fontSizeMax` undefined while restore normalizes it
to `null`. `Delta.distinctKeysIterator` compares with `!==`, so a
restored copy of a locally created text element reported a phantom
`fontSizeMax` change — today only in collab reconciliation deltas, which
are not undoable, but a latent source of spurious version bumps.

Normalize to `null` in the constructor, like `labelPosition`. Snapshots
change accordingly (`undefined` → `null`), including the embedded scene
payload in the SVG export snapshot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore(editor): drop a duplicated scheduleCapture in the sticky note finalize

A paste error: the two calls in one tick set the same flag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(editor): decide history visibility from the applied result

Creating a sticky note and abandoning its editor by clicking out left two
history entries: the note's creation, and one that only deleted the empty
label and dropped the note's reference to it — nothing a user can see, so
the first undo appeared to do nothing. (Escape left the same deletion
pending instead, folded into the next entry.) The text tool's own
click-create-and-abandon had the same invisible entry.

History already skips entries with no visible change, but visibility was
decided per property patch, before binding repair and layout ran: an
`isDeleted` flip or a `boundElements` edit always counted, and anything
layout moved afterwards never did.

`ElementsDelta.applyTo` now decides it from the applied result. After the
deltas, reordering, binding repair and text/arrow layout, the "before"
and "after" of every affected element are compared:

- `idsToCheck` = the changed elements plus what layout can move for them:
  a text's container and everything in `boundElements` (a container's
  label and arrows); the set is walked as it grows, so an arrow brings in
  its own label.
- Unchanged elements are shared between the incoming map and the result
  and layout mutates them in place, so their "before" is a shallow copy;
  changed elements already have a fresh instance. The whole scene is
  never copied.
- An element counts as visible only if it is not deleted and, for text,
  not empty — except an empty label carrying arrow bindings or sitting on
  an arrow, where it cuts the gap. `version`, `versionNonce`, `updated`,
  `created` are metadata and `index` is covered by the z-order check;
  `boundElements` is compared as `type:id` strings with references to
  empty labels dropped, so binding repair that rebuilds equal objects
  reads as unchanged. Everything else counts.

A dev/test-only assertion records every element's version before layout
and throws if layout bumped one outside `idsToCheck`, so a new layout
dependency fails a test instead of letting undo silently skip a real
change. For that to work the `return` had to leave the `finally` block: a
return in `finally` overrides a thrown exception, so the existing
dev/test rethrow for failures in reordering and layout never actually
propagated. Production behavior is unchanged — the catch still returns
with the flag forced to visible.

Cost, production build, median per `applyTo`: sparse changes at parity
through 50k elements; 10k visible bulk updates +13%; 10k metadata-only
updates 44 → 95 ms, because proving "nothing visible" cannot stop at the
first element — the case the old code answered wrongly. `applyTo` runs
on undo/redo and the version slider, both user-paced.

Creation and typing still undo separately, and a drop from the toolbar
still records at the drop. Tests: delta-level (empty-label deletion and
restoration on a rectangle and a sticky note ignored; a non-empty label,
an arrow's label gap, a container resized by a restored label, arrow
bindings on empty text, a label and an arrow repositioned by a container
redraw all visible; excluded or already-applied properties and a
metadata-only update invisible; snapshot restoration visible) and the
sticky note flows (Escape, click-out, font change on an empty label,
typing).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(editor): snap sticky note placement to the grid

* perf(editor): ignore arrow bindings in sticky layout input check

* feat(editor): center a sticky note's label in the whole note, tighten the footer

A middle-aligned label was centered in the body above the creation-date
footer, so it sat visibly high — half a footer above the note's center.
It is now centered in the whole padded note (`height − 2·padding`), the
footer ignored, and only pushed up against the body's bottom once
centering would run it into the footer. The fit and the growth rule are
unchanged: the note still grows at the same label height, no vertical
budget is lost, and top and bottom alignment are as before. Rotation
already turned the label about the note's center.

The footer takes less room: its band shrinks from 32px to 20px and the
date's baseline is defined from the note's bottom edge (14px) rather than
from the padding, so the glyphs sit ~11px from the edge (was 17) with
~13px to the label body above (was 19). The vertical inset follows
(64 → 52), and with it the font-aware minimum note sizes.

Tests: a middle-aligned label is centered on the note's center at three
angles; centering is clamped only once it would overlap the footer;
existing sizes derive from the inset constant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(editor): resize sticky notes proportionally by default

* fix(editor): preserve resize flips and text container anchors

Apply minimum sizes and proportional font scaling to dimension magnitudes, then restore their signs for shared flip geometry.

Share rotation-aware positioning for text-driven height changes and pass vertical flip state through single and multiple-element resizing.

Add regression coverage for mirrored resizing, proportional minimum sizes, and text growth after crossing the resize anchor.

* feat(editor): lift a seeded sticky note corner at high roughness

Curve one corner at roughness 2, chosen from the note seed so it stays consistent through redraws and resizes. Pull its shadow inward by half the bend while retaining the existing down-right offset.

Share the generated paper and shadow paths between Canvas and SVG rendering.

* fix(editor): share the default sticky note color with its quick pick

* refactor(editor): rename a label's `fontSizeMax` to `baseFontSize`

The field holds the font size the user picked, from which the sticky note
layout derives the fitted `fontSize`. "Max" described today's fitter (it
only shrinks); "base" names the intent and pairs with the note's
`baseHeight`, the other user-set value the layout derives from. Renamed
before the field ships, since it is persisted in scene files.

The type is now required (`number | null`): the constructor and restore
already always write it, and history deltas compare with strict
inequality, so an omitted key against `null` would record a spurious
change. Making it required lets tsc catch producers that omit it (one test
fixture did). Helpers follow: `getBaseFontSize`, `getBaseFontSizeUpdate`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(editor): slightly strengthen sticky note edges

* feat(editor): color a sticky note's background while editing its label

While editing a note's label the styles panel targets the label, which has
no fill, so the background picker was gone and the note's color could only
be changed after leaving the editor. The picker now stays available while
editing, shows the note's fill, and a pick colors the note — text color and
background in one editing pass.

One helper, `getColorTargetElement`, decides where a color pick on an
element lands (a background pick on a note's label goes to the note); the
target resolver, the panel predicate, the panel value and the action all
use it, so the picker also gets the sticky domain (sticky picks, no
transparent, the sticky default written).

Also fixes a latent bug in the background action: with nothing selected,
`selectedElements.every(isLine…)` was vacuously true, so the action took
the polygon branch and did nothing. Unreachable before, since no background
picker was available without a selection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix changelog

* fix snaps

* more snap

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 16:45:37 +02:00
David LuzarandMárk Tolmács 854d00c31b feat(editor): track element creation timestamps (#12060)
feat(editor): element creation timestamp tracking

Co-authored-by: Márk Tolmács <mark@lazycat.hu>
2026-09-08 15:26:14 +02:00
David Luzar abeeaeba21 feat(editor): customizing color top picks (#11872) 2026-08-11 15:59:35 +02:00
David LuzarandTamas Lakatos 85270fcc8d feat(packages/excalidraw): ViewportStatusFrame & factor out user-follow state (#11819)
Co-authored-by: Tamas Lakatos <tamas@excalidraw.com>
2026-08-05 00:01:20 +02:00
David Luzar 1acf66edab feat(editor): bind text to hovered arrow endpoint (#11777) 2026-07-28 18:12:33 +02:00
f179f7ffd2 feat(editor): draw to shape (auto-detection) (#9313)
* Initial commit

* Segments detection

* Remove aspect ratio

* Refactor

* Fix tests

* Toggle shapeSnap

* Fix linting

* debug: live preview

* Rebase fixes

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* Bind to elements (no hover background)

* Shape closed

* Fix rad calc + rename segments

* Comments

* Fix binding highlighting + lints

* Rename to convert to shape

* fix: Binding

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Refactor spline simplification

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Remove unused doBoundsIntersect

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Restore type signature

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Propose new button shapes

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Remove unused types

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Icon shapes

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Element cloning

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Element cloning

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Remove unused

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Shape settings in recognition mode

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Remove debug

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Actions

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Lint

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Arrowheads for freedraw

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Draw shape tool instead of freedraw

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Test

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Lint

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Keyboard shortcut visibility

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Unique cursor for draw shape

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Remove selection

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Shape selection

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Improve shape recognition

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Further refine shape recognition

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Preview

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Add hint

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Refined shape recognition

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Less ellipse more rectanguloid

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Refined arrow recognition

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Refinerecognition

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Lower linear threshold

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Rectangle head arrow

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: More arrowhead sizes

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Arrow remains an arrow

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Draw line between elements converts to arrow

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* remove pendingDrawShapeElement

* fix: Arrow start point binding

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Different arrow endpoint heuristic

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Point type

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Various small fixes

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Avoid recompute

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Type issues

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* test(editor): cover the drawShape (snap-to-shape) tool

The drawShape tool had no dedicated tests, which let master's toolbar /
active-tool rewrite silently break it during the merge: actionFinalize
reverted to the selection tool after every sketch, because the drawShape
flow finalizes without a `newElement` and so failed master's new
`&& element` guard.

Covers the behavior the merge had to preserve:
- sketch recognition (rectangle, ellipse) and the too-small no-op
- the tool stays active after finalizing, so shapes can be sketched
  consecutively (regression guard for the above)
- activation via the S shortcut and the extra-tools dropdown
- the tool's own cursor

Points are fed into the trail directly rather than through pointermove,
which is rAF-throttled and would drop all but one point per frame — the
same approach lasso.test.tsx uses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: Add test

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Breakout from App.tsx

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Shortcut is now Shift+X

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: No selecting arrows for shape recog

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Double history

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: PCA + feature extraction recognition

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* reduce and normalize min-size gates

* never select newElement when shape recog tool active

* simplify lifecycle & remove double finalize

* make arrows bindings orbit

* funnel though actionFinalize & fix two unrelated cleanup cases

* use crosshair cursor

* show strokeWidth

* switch icons for drawShape & extraTools

* dedupe getBoundsFromPoints

* remove unused helpers

* jsdoc cleanup

* dynamic trail size

* improve rectangle recognition

* disallow linears to deviate too much from a straight line

* put under freedraw tool on tablet/mobile

* tweak label

* rename `drawShape` -> `autoshape`

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 18:42:42 +02:00
Augusto XavierandMark Tolmacs dd8296af18 fix(editor): narrow NonDeleted type to isDeleted: false (#11470)
---------
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
2026-07-07 16:22:31 +02:00
51ca8abde4 feat(packages/excalidraw): viewport locking (#11554)
Co-authored-by: barnabasmolnar <barnabas@excalidraw.com>
Co-authored-by: Arnost Pleskot <arnostpleskot@gmail.com>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2026-07-03 22:57:47 +02:00
Márk Tolmácsanddwelle 070df27e4d fix(editor): Modern TS require imports from rootDir (#11552)
---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2026-06-24 10:52:56 +00:00
Márk Tolmácsanddwelle cd514d72d6 feat(editor): LaserPointer based freedraw (#11507)
Introduces constant width freedraw mode, keeping the original variable mode as default.

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2026-06-24 09:16:22 +02:00
Márk Tolmács b6d80e4256 fix(packages/excalidraw): consolidate bounds checks (#11275)
Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2026-06-04 19:27:25 +02:00
Márk Tolmács 2dfcc6f0ce chore: Remove startBoundElement from state (#11264) 2026-05-02 16:36:42 +02:00
David LuzarandMark Tolmacs d9e8a33aa4 feat(editor): implement overlap box selection (#11053)
Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
2026-04-01 18:41:11 +02:00
David Luzar 21dd1cfacc feat(packages/excalidraw): state tracking, api hook, and others (#10870) 2026-03-08 23:15:18 +01:00
Márk Tolmácsanddwelle 437595fa65 feat: Arrow binding is a preference (#10839)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2026-03-03 21:55:40 +00:00
David Luzar 60b275880d feat(editor): support radar chart and multiple series for other chart types (#10824) 2026-02-26 16:13:15 +01:00
David Luzar b9a255407f refactor: update SCSS syntax & remove open-color dep (#10633) 2026-01-10 18:15:14 +01:00
David Luzar 6c908553a9 fix: reconciliation of server updates & refactor restore (#10597) 2026-01-04 15:13:38 +01:00
Márk Tolmács d87620b239 fix: Circular reference (#10544)
* fix: Circular reference

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Lint

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* Trigger CI

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
2025-12-21 22:14:21 +01:00
David Luzar d080833f4d chore: bump typescript@5.9.3 (#10431) 2025-12-01 22:37:42 +01:00
Márk Tolmácsanddwelle c141960ada feat: Non-elbow arrow snapping and behavior changes (#9670)
* Fixed point binding for simple arrows

Tests added

Fix binding

Remove unneeded params

Unfinished simple arrow avoidance

Fix newly created jumping arrow when gets outside

Do not apply the jumping logic to elbow arrows for new elements

Existing arrows now jump out

Type updates to support fixed binding for simple arrows

Fix crash for elbow arrws in mutateElement()

Refactored simple arrow creation

Updating tests

No confirm threshold when inside biding range

Fix multi-point arrow grid off

Make elbow arrows respect grids

Unbind arrow if bound and moved at shaft of arrow key

Fix binding test

Fix drag unbind when the bound element is in the selection

Do not move mid point for simple arrows bound on both ends

Add test for mobing mid points for simple arrows when bound on the same element on both ends

Fix linear editor bug when both midpoint and endpoint is moved

Fix all point multipoint arrow highlight and binding

Arrow dragging gets a little drag to avoid accidental unbinding

Fixed point binding for simple arrows when the arrow doesn't point to the element

Fix binding disabled use-case triggering arrow editor

Timed binding mode change for simple arrows

Apply fixes

Remove code to unbind on drag

Update simple arrow fixed point when arrow is dragged or moved by arrow keys

Binding highlight fixes

Change bind mode timeout logic

Fix tests

Add Alt bindMode switch

 No dragging of arrows when bound, similar to elbow

Fix timeout not taking effect immediately

Bumop z-index for arrows when dragged

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Only transparent bindables allow binding fallthrough

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fix lint

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fix point click array creation interaction with fixed point binding

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Restrict new behavior to arrows only

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Allow binding inside images

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fix already existing fixed binding retention

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Refactor and implement fixed point binding for unfilled elements

Restore drag

Removed point binding

Binding code refactor

Added centered focus point

Binding & focus point debug

Add invariants to check binding integrity in elements

Binding fixes

Small refactors

Completely rewritten binding

Include point updates after binding update

Fix point updates when endpoint dragged and opposite endpoint orbits

centered focus point only for new arrows

Make z-index arrow reorder on bind

Turn off inside binding mode after leaving a shape

Remove invariants from debug

feat: expose `applyTo` options, don't commit empty text element (#9744)

* Expose applyTo options, skip re-draw for empty text

* Don't commit empty text elements

test: added test file for distribute (#9754)

z-index update

Bind mode on precise binding

Fix binding to inside element

Fix initial arrow not following cursor (white dot)

Fix elbow arrow

Fix z-index so it works on hover

Fix fixed angle orbiting

Move point click arrow creation over to common strategy

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Add binding strategy for drag arrow creation

Fix elbow arrow

Fix point handles

Snap to center

Fix transparent shape binding

Internal arrow creation fix

Fix point binding

Fix selection bug

Fix new arrow focus point

Images now always bind inside

Flashing arrow creation on binding band

Add watchState debug method to window.h

Fix debug canvas crash

Remove non-needed bind mode

Fix restore

No keyboard movement when bound

Add actionFinalize when arrow in edit mode

Add drag to the Stats panel when bound arrow is moved

Further simplify curve tracking

Add typing to action register()

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fix point at finalize

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fix type errors

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

New arrow binding rules

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fix cyclical dep

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fix jiggly arrows

Fix jiggly arrow x2

Long inside-other binding

Click-click binding

Fix arrows

Performance

[PERF] Replace in-place Jacobian derivation with analytical version

Different approach to inside binding

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fixes

Fix inconsistent arrow start jump out

Change how images are bound to on new arrow creation

Lower timeout

Small insurance fix

Fix curve test

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

No center focus point

90% inside center binding

Fixing tests

fix: Elbow arrow fixes

fix: More arrow fixes

Do not trigger arrow binding for linear elements

fix: Linear elements

fix: Refactor actionFinalize for linear

Binding tests updated

fix: Jump when cursor not moved

fix: history tests

Fix history snapshot

Fix undo issue

fix(eraser): Remove binding from the other element

fix(tests): Update tests

chore: Attempt filtering new set state

Fix excessive history recording

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fix all tests

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

fix(transform): Fix group resize and rotate

fix(binding): Harmonize binding param usage

fix: Center focus point

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

chore: Trigger build

Remove binding gap

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Binding highlight refactor

fix: Refactored timeout bind mode handling

fix: Center when orbiting

feat: Color change on highlight

Fix orbit binding highlight

fix: hiding arrow

Fix arrow binding

Fix arrow drag selection logic

Binding highlight is now hot pink

Change inside binding logic for start point

Render focus point in debug mode

Fix snap to center

Fix actionFinalize for new arrow creation

fix: snapToCenter()

80% by length

fix: attempt at fixing the dancing arrows

feat: No center snap when start is not bound

Fix centering for existing arrows

tweak binding highlight color

change `appState.suggestedBindings` -> `suggestedBinding` & remove unused code

Refactor delayed bind mode change

Binding highlight rotation support + image support

fix(highlight): Overdraw fixes

feat: Do not allow drag bound arrow closer to the shape than dragging distance

feat: Stroke width adaptive fixed binding distance

chore: More point dragging centralization

New element behavior

Refactor dragging

Fix incorrect highlight sizing

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fix delayed bind mode for multiElement arrows

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fix multi-point

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fix elbow arrows

Simplify state

Small positional fixes

Fix jiggly arrows

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Fixes for arrow dragging

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

Elbow arrow fixes

Highlight fixes

Fix elbow arrow binding

Frame highlight

Fix elbow mid-point binding

Fix binding suggestion for disabled binding state

Implement Alt

Remove debug

* CHange new arrow creation

* fix: allow inside binding via timeout if arrow has no startBinding

* fix: Delete invariant violation with arrows

* fix: Deleted arrow causes problems

* fix: Dragging issues

* fix: Dragging fix 2

* fix: Disable drag drag when arrow is bound

* fix: Multipoint arrow opposite point movement

* fix: Ctrl+Alt precedence

* feat: Alt inside start binding mode change

* Fix multipoint arrow orbit

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Arrow start inside binding switch

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: New arrow never binds inside

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Small refactor

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Multi-point arrows and linears

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Lint

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Nested shapes handling

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Overlap behavior

* Alt unbind fix

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Existing arrow nested bindable

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Binding suggestions

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Circular dep

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: snapshots

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Alt immediate update

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Laxing on invariants

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: New highlight overdraws arrow

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Image binding rule changed

* Trigger Rebuild

* fix:Highlight flicker

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Fully nested shapes

* fix: Tune nested shape binding

* fix: Size-based orbit jump-in

* fix: Binding highlight stroke on sharp bindables

* fix: Nested shape binding

* fix: history

* fix:More precise element nesting check

* feat:Add tolerance to shape nesting detection

* fix: Reverse

* fix:Change center binding to circular

* ignore invisible elements when binding

* feat: Center point with more precise highlight outlines

* fix:Arrow tool hover stuck highlight

* fix:More stroke width for highlight

* POC: highlight center on hover

* tweak binding highlight width

* render highlight on the outside

* fix: Locked elbow arrow creation

* update hints

* reduce timeout

* handle overlap when both elements the same size

* tweak highlight stroke width some more

* fix:Add intersection padding

* fix: New arrow start orbit when nested binds on the end

* fix: Update history snapshot

* feat: Allow inside binding for new arrows in nested cases

* chore: Logic for measurement

* fix: Locked tool + arrow

* feat: Remove center binding

* fix: Jump arrow inside it gets visially too short

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore:Basic interactive canvas animation re-render trigger for highlights

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat:Highlight animations

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix:Refactored and fixed highlight animation

* fix:Poisoned arrow

* fix Arrow edit mode selection

* fix:Tool lock binding behavior restored

* fix:Overlap inside binding

* fix:Animated binding highlight

* alt anims + increase timeout to 700

* tweak animation some more + remove countdown

* fix: False bind timeout indicator

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: better file normalization (#10024)

* feat: better file normalization

* fix lint

* fix png detection

* optimize

* fix type

* fix: increase rejection delay for opening files with legacy api (#8961)

* Increased input change interval to 1000 ms to fix IOS 18 file opening issue

* increase more

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>

* feat: library search (#9903)

* feat(utils): add support for search input type in isWritableElement

* feat(i18n): add search text

* feat(cmdp+lib): add search functionality for command pallete and lib menu items

* chore: fix formats, and whitespaces

* fix: opt to optimal code changes

* chore: fix for linting

* focus input on mount

* tweak placeholder

* design and UX changes

* tweak item hover/active/seletected states

* unrelated: move publish button above delete/clear to keep it more stable

* esc to clear search input / close sidebar

* refactor command pallete library stuff

* make library commands bigger

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>

* fix: Allow already inside bound arrows to continue inside binding

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: No angle lock over bindable elements

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Center binding on SHIFT key

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* Fix ghost start binding

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* FEAT: No binding to frame cutout

* feat: Bind to frame when frame-bound object hidden part is approached

* fix: revert preferred selection to box once you switch to `full` UI (#10160)

* fix: mobile UI and other fixes (#10177)

* remove legacy openMenu=shape state and unused actions

* close menus/popups in applicable cases when opening a different one

* split ui z-indexes to account prefer different overlap

* make top canvas area clickable on mobile

* make mobile main menu closable by clicking outside and reduce width

* offset picker popups from viewport border on mobile

* reduce items gap in mobile main menu

* show top picks for canvas bg colors in all ui modes

* fix menu separator visibility on mobile

* fix command palette items not being filtered

* fix: Increase transform handle offset (#10180)

* fix: Increase transform handle offset

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Temporarily disable transform handles for linear elements on mobile and tablets

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Linear hidden resize

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* disable mobielOrTablet linear element bbox completely

* fix: Test

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Lint

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>

* fix: context menu getting covered (#10199)

* do not show z-index actions on mobile or tablet

* fix: context menu getting covered

* fix lint

* fix style popup getting covered

* put contextmenu z-index above sidebar

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>

* feat: More prominent keyboard shortcuts in hints (#10057)

* Initial

* Memoize

* Styling

* Use double angle brackets for keyboard shortcuts

* Use rem in gap

* Use an existing function for substituting tags in a string

* Revert styling

* Avoid unique key warnings

* Styling

* getTransChildren -> nodesFromTextWithTags

* Use height and padding instead of padding only

* Initial new idea

* WIP shortcut substitutions

* Use simple regex for parsing shortcuts

* Use single shortcut for combos

* Use kbd instead of span

* shortcutFromKeyString -> getTaggedShortcutKey

* Bug fix

* FlowChart -> Flowchart

* memo is useless here

* Trigger CI

* Translate in getShortcutKey

* More normalized shortcuts

* improve shortcut normalization and replacement & support multi-key tagged shortcuts

* fix regex

* tweak css

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>

* fix: small tweaks to shortcut hints (#10214)

* fix: Test

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Bind mode

* feat: Support special key shortcut highlight

* fix: Lint

* fix: Remove non-needed function

* fix: Skip frame cutout for hover, but keep shape for binding

* fix: Lint

* fix: Restore removal of deleted elements on restore

* fix: Inside-inside during drag

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Arrow vanishes when orbit binding to the same element

* feat: Feature flag support

* Simplified binding

* fix: Diamond corner binding

* feat: Binding highlight band re-added

* feat: Settings menu

* fix: Same shape binding

* fix: set radix PropertiesPopover collision boundary (#10221)

* Set collision boundary

* Calculate collisionPadding dynamically based on container

* Add appState offsetTop and offsetLeft to padding calculation.

Refactor collisionPadding calculation to use app state offsets.

* Update PropertiesPopover.tsx

* popover positioning relative to container

* fix: prevent wrap text in a container to only text that are not bound to a container (#10250)

* fix: only enable wrap text in a container when at least one text element selected is unbound

* Trigger Rebuild

---------

Co-authored-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Uncap the nodejs version requirement (#10238)

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>

* refactor: single source of truths with editor interface (#10178)

* refactor device to editor interface and derive styles panel

* allow host app to control form factor and ui mode

* add editor interface event listener

* put new props inside UIOptions

* refactor: move related apis into one file

* expose getFormFactor

* privatize the setting of desktop mode and fix snapshots

* refactor and fix test

* remove unimplemented code

* export getFormFactor()

* replace `getFormFactor` with `getEditorInterface`

* remove dead & useless

* comment

* fix ts

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>

* chore: Update translations from Crowdin (#7429)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Persian)

* New translations en.json (Khmer)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Marathi)

* New translations en.json (Thai)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Azerbaijani)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Sinhala)

* New translations en.json (Uzbek)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (German, Switzerland)

* New translations en.json (Bengali, India)

* New translations en.json (Kabyle)

* New translations en.json (Karakalpak)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* New translations en.json (German)

* New translations en.json (Chinese Simplified)

* New translations en.json (Polish)

* New translations en.json (Romanian)

* New translations en.json (Korean)

* New translations en.json (Chinese Traditional)

* New translations en.json (Hebrew)

* New translations en.json (Hebrew)

* New translations en.json (Slovak)

* New translations en.json (Slovak)

* New translations en.json (Hungarian)

* New translations en.json (Hungarian)

* New translations en.json (Slovak)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Korean)

* New translations en.json (Chinese Traditional)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Khmer)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Marathi)

* New translations en.json (Thai)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Azerbaijani)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Sinhala)

* New translations en.json (Uzbek)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (German, Switzerland)

* New translations en.json (Bengali, India)

* New translations en.json (Kabyle)

* New translations en.json (Karakalpak)

* New translations en.json (Romanian)

* New translations en.json (German)

* New translations en.json (Slovenian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Spanish)

* New translations en.json (Russian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Turkish)

* New translations en.json (Slovak)

* New translations en.json (Slovak)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Korean)

* New translations en.json (Chinese Traditional)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Khmer)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Marathi)

* New translations en.json (Thai)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Azerbaijani)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Sinhala)

* New translations en.json (Uzbek)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (German, Switzerland)

* New translations en.json (Bengali, India)

* New translations en.json (Kabyle)

* New translations en.json (Karakalpak)

* New translations en.json (German)

* New translations en.json (Russian)

* New translations en.json (Romanian)

* New translations en.json (Spanish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* New translations en.json (Slovak)

* New translations en.json (German)

* New translations en.json (Portuguese)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Khmer)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Marathi)

* New translations en.json (Thai)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Azerbaijani)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Sinhala)

* New translations en.json (Uzbek)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (German, Switzerland)

* New translations en.json (Bengali, India)

* New translations en.json (Kabyle)

* New translations en.json (Karakalpak)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (Polish)

* Auto commit: Calculate translation coverage

* New translations en.json (Polish)

* Auto commit: Calculate translation coverage

* New translations en.json (Turkish)

* Auto commit: Calculate translation coverage

* New translations en.json (Turkish)

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>

* fix: mobile view ui issues (#10284)

* hide zen mode when formFactor = phone

* tool bar fixes: icon and width

* view mode

* fix lint

* add exit-view-mode button

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>

* chore: Update snaps

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Blue highlight

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Diagonal binding point

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Remove settings

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Jump other binding

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Hovered arrow mode highlight

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* feat: Alt does not snap

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Check debug

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Alt precise positioning

* fix: Jump out to orbit for new arrows when dragged outside

* fix: New arrow preserved projection

* chore: Remove debug

* chore: Introduce different debug color for orbit and other binding modes

* fix: Restore arrow start point when self binding

* fix: Turn of start jump-out

* fix: Tests

* fix: Select the first possible altBindPoint

* fix: Random projection

* fix: Use last point for existing arrows

* fix: Preserve alternate orbit focus point during drag

* fix: Lint

* fix: Update tests

* fix: Elbow arrow direction at binding

* binding gap and distance and binding highlight tweaks

* chore: Naming refactors

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Tests

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Alt-duplication copied elements placement (#10152)

* feat: Animation support (#10042)

* fix: banner url (#10315)

* feat: Animation support (#10042)

* fix: Merge discrepancy

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* chore: Remove non-needed code

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* Trigger build

* chore: Remove hint for V1

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* shorten focus point diagonal helpers to fix corner binding cases

* fix: Tests

* fix: Multi-point arrow closeness fallback

* fix: Finalize multipoint arrow on binding area click

* fix: Finalize arrow now truly finalzes

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Point click arrow creation jumping to orbit

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Alt+drag movement block

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* fix: Tests

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* Trigger build

* feat: hide point highlight when dragging

* feat: hide bbox when dragging points

* revert binding gap increase for elbow arrows

* reset selectionLinearElement on tool change

* chore: Remove debug

* feat: Better restore for bindings

* use elementsMap instead of array when passing to restoreElement

* fix: Arrow angle reset

* fix: Arrow angle

* Arrow angle support

* fix trashing cached canvases in `LinearElementEditor.getElementAbsoluteCoords`

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-11-25 15:46:02 +01:00
Márk Tolmács 8fb16669ab feat: Add binding visual debug (#10222) 2025-11-10 12:08:57 +01:00
Ryan Dianddwelle 47cbb5b6fb refactor: single source of truths with editor interface (#10178)
* refactor device to editor interface and derive styles panel

* allow host app to control form factor and ui mode

* add editor interface event listener

* put new props inside UIOptions

* refactor: move related apis into one file

* expose getFormFactor

* privatize the setting of desktop mode and fix snapshots

* refactor and fix test

* remove unimplemented code

* export getFormFactor()

* replace `getFormFactor` with `getEditorInterface`

* remove dead & useless

* comment

* fix ts

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-11-03 23:34:17 +01:00
416e8b3e42 feat: new mobile layout (#9996)
* compact bottom toolbar

* put menu trigger to top left

* add popup to switch between grouped tool types

* add a dedicated mobile toolbar

* update position for mobile

* fix active tool type

* add mobile mode as well

* mobile actions

* remove refactored popups

* excali logo mobile

* include mobile

* update mobile menu layout

* move selection and deletion back to right

* do not fill eraser

* fix styling

* fix active styling

* bigger buttons, smaller gaps

* fix other tools not opened

* fix: Style panel persistence and restore

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>

* move hidden action btns to extra popover

* fix dropdown overlapping with welcome screen

* replace custom popup with popover

* improve button styles

* swapping redo and delete

* always show undo & redo and improve styling

* change background

* toolbar styles

* no any

* persist perferred selection tool and align tablet as well

* add a renderTopLeftUI to props

* tweak border and bg

* show combined properties only when using suitable tools

* fix preferred tool

* new stroke icon

* hide color picker hot keys

* init preferred tool based on device

* fix main menu sizing

* fix welcome screen offset

* put text before image

* disable call highlight on buttons

* fix renderTopLeftUI

---------

Signed-off-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-10-09 23:48:31 +02:00
Ryan Dianddwelle 204e06b77b feat: compact layout for tablets (#9910)
* feat: allow the hiding of top picks

* feat: allow the hiding of default fonts

* refactor: rename to compactMode

* feat: introduce layout (incomplete)

* tweak icons

* do not show border

* lint

* add isTouchMobile to device

* add isTouchMobile to device

* refactor to use showCompactSidebar instead

* hide library label in compact

* fix icon color in dark theme

* fix library and share btns getting hidden in smaller tablet widths

* update tests

* use a smaller gap between shapes

* proper fix of range

* quicker switching between different popovers

* to not show properties panel at all when editing text

* fix switching between different popovers for texts

* fix popover not closable and font search auto focus

* change properties for a new or editing text

* change icon for more style settings

* use bolt icon for extra actions

* fix breakpoints

* use rem for icon sizes

* fix tests

* improve switching between triggers (incomplete)

* improve trigger switching (complete)

* clean up code

* put compact into app state

* fix button size

* remove redundant PanelComponentProps["compactMode"]

* move fontSize UI on top

* mobile detection (breakpoints incomplete)

* tweak compact mode detection

* rename appState prop & values

* update snapshots

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-09-12 10:18:31 +10:00
David Luzar dda3affcb0 fix: do not strip invisible elements from array (#9844) 2025-08-12 11:56:11 +02:00
Marcel Mraz 54c148f390 fix: text restore & deletion issues (#9853) 2025-08-12 09:27:04 +02:00
Márk Tolmácsanddwelle d6a934ed19 chore: Remove editingLinearElement (#9771)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-07-24 17:02:21 +02:00
Márk Tolmács c141500400 chore: Relocate visualdebug so ESLint doesn't complain (#9668) 2025-06-18 14:45:51 +02:00
Marcel Mrazanddwelle a7b64f02b3 fix: remove image preview on image insertion (#9626)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-06-10 21:31:11 +02:00
Márk Tolmács ca1a4f25e7 feat: Precise hit testing (#9488) 2025-06-07 12:56:32 +02:00
Ryan Dianddwelle 712f267519 feat: better unlock (#9546)
* change lock label

* feat: add unlock logic for single units on pointer up

* feat: add unlock popup

* fix: linting errors

* style: padding tweaks

* style: remove redundant line

* feat: lock multiple units together

* style: tweak color & position

* feat: add highlight for locked elements

* feat: select groups correctly after unlocking

* test: update snapshots

* fix: lasso from selecting locked elements

* fix: should prevent selecting unlocked elements and setting locked id at the same time

* fix: reset hit locked id immediately when appropriate

* feat: capture locked units in delta

* test: update locking test

* feat: show lock highlight when locking (including undo/redo)

* feat: make locked highlighting consistent

* feat: show correct cursor type when moving over locked elements

* fix history

* remove `lockedUnits.singleUnits`

* tweak button

* do not render UnlockPopup if not locked element selected

* tweak actions

* refactor: simplify type

* refactor: rename type

* refactor: simplify hit element setting & checking

* fix: prefer locked over link

* rename to `activeLockedId`

* refactor: getElementAtPosition takes an optional hitelments array

* fix: avoid setting active locked id after resizing

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-05-21 21:57:12 +10:00
Marcel Mraz d92a849038 fix: issues when importing package outside of browser (#9525) 2025-05-14 16:01:43 +02:00
Marcel Mraz 7e41026812 refactor: export everything from @excalidraw/element, don't import from subpaths (#9466)
* Don't import from subpaths

* Fix tests, move related tests to element
2025-05-09 23:01:33 +02:00
Ryan Dianddwelle a30e1b25c6 feat: include frame names in canvas searches (#9484)
* fix frame name clipping on zooming

* include assistant font

* default frame name

* extend search to frame names

* add a simple test

* collpase search match items

* id check out of loop

* fix frame name check

* include focusedId for small perf improvement

* optionally show and hide collapse icon

* update section title

* fix tests

* rename `serverSide` -> `private`

* revert: do not reset zoom on zoom change

* feat: do not close menu on repeated ctrl+f

* remove collapsible

* tweak results CSS

* remove redundant check

* set `appState.searchMatches` to null if empty

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-05-09 18:32:16 +02:00
Narek Malkhasyan e058a08b33 fix: use rimraf instead of rm -rf (#9460) 2025-05-07 14:13:27 +02:00
ce267aa0d3 feat: lasso selection (#9169)
* lasso without 'real' shape detection

* select a single linear el

* improve ux

* feed segments to worker

* simplify path threshold adaptive to zoom

* add a tiny threshold for checks

* refactor code

* lasso tests

* fix: ts

* do not capture lasso tool

* try worker-loader in next config

* update config

* refactor

* lint

* feat: show active tool when using "more tools"

* keep lasso if selected from toolbar

* fix incorrect checks for resetting to selection

* shift for additive selection

* bound text related fixes

* lint

* keep alt toggled lasso selection if shift pressed

* fix regression

* fix 'dead' lassos

* lint

* use workerpool and polyfill

* fix worker bundled with window related code

* refactor

* add file extension for worker constructor error

* another attempt at constructor error

* attempt at build issue

* attempt with dynamic import

* test not importing from math

* narrow down imports

* Reusing existing workers infrastructure (fallback to the main thread, type-safety)

* Points on curve inside the shared chunk

* Give up on experimental code splitting

* Remove potentially unnecessary optimisation

* Removing workers as the complexit is much worse, while perf. does not seem to be much better

* fix selecting text containers and containing frames together

* render fill directly from animated trail

* do not re-render static when setting selected element ids in lasso

* remove unnecessary property

* tweak trail animation

* slice points to remove notch

* always start alt-lasso from initial point

* revert build & worker changes (unused)

* remove `lasso` from `hasStrokeColor`

* label change

* remove unused props

* remove unsafe optimization

* snaps

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
Co-authored-by: Marcel Mraz <marcel@excalidraw.com>
2025-04-07 16:44:25 +10:00
Marcel Mraz 0cd5a259ae fix: incorrect type imports (#9308) 2025-03-27 12:00:12 +01:00
Marcel Mraz 432a46ef9e refactor: separate elements logic into a standalone package (#9285) 2025-03-26 15:24:59 +01:00
Marcel Mraz 21ffaf4d76 refactor: auto ordered imports (#9163) 2025-03-12 15:23:31 +01:00
Márk Tolmács a9e2d2348b chore: Logging and fixing extremely large scenes (#9225) 2025-03-05 23:06:01 +01:00
Marcel Mraz ecef5d12f4 chore: release @excalidraw/excalidraw@18.0.0 🎉 (#9127) 2025-02-28 16:49:09 +01:00
Márk Tolmácsanddwelle 0ffeaeaecf feat: Remove GA code from binding (#9042)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-02-25 22:52:06 +01:00
David Luzar 36274f1f3e feat: cleanup svg export and move payload to <metadata> (#8975) 2025-01-05 16:53:05 +01:00