Commit Graph
1272 Commits
Author SHA1 Message Date
Nikita RakitinandCursor 36ec1dc0ca Format Shape: 3-D Format and 3-D Rotation controls
shape.effects gains `threeD`: top and bottom bevel (preset, width,
height), depth, contour, material, lighting, camera preset, X/Y/Z
rotation, distance from ground, and reset. The settings are read from and
written back to the shape's kept a:scene3d / a:sp3d XML (new
cmd::three_d), so they render through the existing camera and bevel code
and survive a PPTX save; extrusion and contour colours are kept as is.

The `bevel` key used to set only Effects::bevel, which neither the
renderer nor the PPTX writer reads, so a bevel set from the app vanished
on save; it now goes through the same path.

The Effects page of the Format Shape pane gets 3-D Format and 3-D
Rotation sections.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 22:13:36 +03:00
Nikita RakitinandCursor e9315bc141 UI: seeded whole-window soak with memory and timing bounds
ui_soak_tests.rs drives the app through the egui test harness with seeded
random clicks, drags, keys (plain, Ctrl, Shift), typing and scrolling over the
sample deck. Every ten actions it checks that every document validates, the
selection is in range, undo, journal, thumbnail and texture caches stay
bounded, and no command or render panic was caught; at the end, that input
really reached the editor and the mean frame stays under 250 ms. Two seeds x
150 actions run in CI; DECKCRAFT_UI_SOAK_STEPS/SEED run it long.

Panics swallowed by the guards were invisible to tests: Session now counts
them (panics_caught) and deckcraft_render::panics_caught() counts render
panics drawn as blank. Textures::thumb_count exposes the thumbnail cache size.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 22:09:48 +03:00
Nikita RakitinandCursor ad24f545e2 Format Shape: picture or texture fill options
shape.fill gains pictureProps for a picture fill: tile on/off, tile
offset X/Y (pt), scale X/Y, alignment and mirror type, stretch offsets
(left/top/right/bottom), and rotate with shape. `transparency` now also
sets a picture fill's opacity. Values are clamped and unknown keywords or
non-finite numbers ignored. The Fill page of the Format Shape pane gets a
"Picture or texture fill" option with a picture source button and these
controls. The model, renderer and PPTX reader/writer already carry the
fields; a new engine test checks they survive a PPTX save and reopen.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 22:09:33 +03:00
Nikita RakitinandCursor 460a2712e4 Transitions: choose a transition sound and play it in the show
Transition sounds were read from and written to .pptx but could not be
set and never played. Transitions › Timing gets a Sound box ([No Sound]
or Other Sound…), backed by a new transition.sound command that embeds
an audio file on the selected slides (or removes it with none). In the
slide show the sound plays as its slide comes in and, as in PowerPoint,
keeps playing into the next slide unless that slide has a sound of its
own. Built-in sounds, Stop Previous Sound and Loop Until Next Sound are
still missing; the parity docs say so.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 22:07:06 +03:00
Nikita RakitinandCursor 283b30f12d UI: WordArt Transform gallery applies a text warp
Shape Format ▸ Text Effects lists the 40 prstTxWarp presets and
No Transform. format.textWarp sets or clears that warp on the
selected text, with optional adjust values.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:58:35 +03:00
Nikita RakitinandCursor 607ffc5591 Text: set text transparency from Text Fill (#32)
There was no way to make text see-through: the only Transparency
slider was the shape fill's. The new format.textTransparency command
keeps the text's colour and sets its alpha (replacing any it had, and
dropping it at 0). A run without its own colour takes the colour it
shows now, so inherited placeholder or theme colours stay as they are.
Gradient text gets the alpha on every stop. The Format Shape pane's
Text Options page gains a Text Fill section (colour and Transparency),
and the Shape Format › Text Fill dropdown has the same slider.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:55:21 +03:00
Nikita RakitinandCursor 14bc6a7ef9 Format Shape: shadow, glow and reflection detail controls
shape.effects gains shadowProps / glowProps / reflectionProps that adjust
the current effect (or add a default one): shadow colour, transparency,
blur, angle and distance; glow colour, transparency and size; reflection
transparency, size, distance and blur. Values are clamped, non-finite ones
ignored. The Effects page of the Format Shape pane exposes them as sliders
and colour pickers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:52:56 +03:00
Nikita RakitinandCursor 85833cf5f5 Engine: seeded editor soak tests; tiny charts no longer blank the slide
soak_tests.rs drives every non-file command with plausible and hostile
parameters over a ~100-slide deck, checking after each step that the
document validates, the selection is in range, the undo stack is bounded,
undo then redo restores the exact document, and every 100 steps that the
deck saves and reopens as .deckcraft and .pptx. Four seeds x 400 steps run
in CI (under a second); DECKCRAFT_SOAK_STEPS/SEED run it long.

The long soak found that a chart a few points wide panicked in f64::clamp
(label gutter min above max), and the render guard then blanked the whole
slide. chart.rs now uses a non-panicking clamp_soft for the gutters and the
zero line; a render test draws tiny charts next to a shape without the
guard.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:50:23 +03:00
Nikita RakitinandCursor 16c5d6a6a3 Format Shape: line compound, cap, join, arrows and sizes; lock aspect ratio
The Format Shape pane's Line page had only colour, width and dash. It now
has PowerPoint's compound type, cap type, join type, begin/end arrow type
and begin/end arrow size drop-downs, and the Size page has Lock aspect
ratio for the width and height fields. shape.line gains compound,
headSize and tailSize, and changing an arrow's kind keeps its size.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:45:42 +03:00
Nikita RakitinandCursor e6920f8ebe Print: choose paper size and orientation for notes and handouts
The Print page always laid notes pages and handouts out on portrait US
Letter. It now offers Letter, A4, Legal and A3 in portrait or landscape,
as PowerPoint's Print settings do, and the preview shows the chosen
page. file.export, app.print and the MCP export tool take the same
`paper` and `orientation` params; unknown values are an error. A notes
page keeps its slide within the top half of the page, so landscape
paper still leaves room for the notes. Full-page slides still print on
slide-shaped pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:43:54 +03:00
Nikita RakitinandCursor 1b9b830f32 Animation Pane: advanced timeline with draggable bars
The pane's bars now come from deckcraft_anim::Timeline, the same schedule the
slide show plays, so With/After Previous, delays, repeats and auto-reverse line
up on a shared seconds ruler. Dragging a bar sets the delay and dragging its
right edge sets the duration (snapped to 0.05 s), each through
animation.timing, so one undoable command per drag. Bar geometry and drag maths
live in animtimeline.rs as pure, unit-tested functions; kittest drives the
drags. Parity and gaps docs updated.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:30:05 +03:00
Nikita RakitinandCursor 2a79cb7671 Shapes: smooth a rectangle's corners into a squircle (#286)
deckcraft_geom::squircle builds a rectangle outline whose corners start
curving earlier along the edge and keep a shorter circular arc, as design
tools' corner smoothing does. shape.smoothCorners turns rectangles and
rounded rectangles into that outline (custom geometry, so it round-trips
through PPTX), and Edit Shape gains a Smooth Corners slider.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:17:39 +03:00
Nikita RakitinandCursor eec67560df Fonts: find fonts where Linux and Flatpak keep them; add and rescan commands (#277)
The font menu lists every family the scan of the system font folders finds,
but on Linux the scan only looked in /usr/share/fonts, /usr/local/share/fonts,
~/.fonts and ~/.local/share/fonts. Fonts under XDG data dirs, in folders
fontconfig is configured with (NixOS, Guix, custom setups) and, inside
Flatpak, the host's fonts never showed.

- system_font_dirs adds $XDG_DATA_HOME/fonts and each $XDG_DATA_DIRS/fonts,
  Flatpak's /run/host/{fonts,local-fonts,user-fonts}, and the <dir> entries
  of /etc/fonts/fonts.conf, local.conf and conf.d; duplicates are dropped.
- font.add {data} adds a font from its bytes for the session; font.rescan
  catalogs the installed fonts again, and the font menu has a
  "Rescan Installed Fonts" item for fonts installed while DeckCraft runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:16:23 +03:00
Nikita RakitinandCursor 07dd513c54 Show: render the next slide in the background while the current one rests (#240)
Advancing rendered the new slide on the UI thread at full screen size
before anything changed on screen. While a slide rests (no animation or
transition playing) the show now renders the next slide's first frame on
a background thread; entering that slide uploads the ready image instead
of rendering, waiting for the render if it is still running. Slides with
animated GIFs and the web build keep rendering on entry.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:11:38 +03:00
Nikita RakitinandCursor ede2ca69b6 Designer: layout ideas that rearrange the current slide (#314)
Design Ideas showed the current slide once per built-in theme, and picking
one ran design.theme on the whole presentation; with a large picture every
idea looked the same. The pane also re-rendered eight slides every frame.

- New commands design.ideas (query) and design.applyIdea {id, slide?}
  (cmd/ideas.rs). A slide with a picture gets full-bleed with the title on a
  band, picture left or right with the text beside it, and a framed picture;
  a text slide gets a title column, a centred title and a title at the foot.
  An idea moves the slide's own shapes, crops the picture to cover its new
  box, sends it to the back, and changes only that slide (one undo step).
- The pane shows the layout ideas first, then the themes under a "whole
  presentation" heading, and caches the thumbnails until the deck revision,
  slide or pane width changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:10:59 +03:00
Nikita RakitinandCursor c7a4930c4c UI: open presentations in the background with a progress window (#350)
Opening from the file picker or the Backstage recent list read and parsed
the file on the UI thread, so a large PPTX froze the window. The read now
runs on a thread (engine: read_presentation_file / add_presentation_file),
and a modal shows the file name, elapsed time and Cancel; Escape cancels
too. A cancelled read is dropped when it finishes. Scripted opens (control,
MCP, CLI) and the web build stay synchronous.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:08:47 +03:00
Nikita RakitinandCursor 7d23e4670b Charts: pie and doughnut data labels use the number format (#324)
Pie and doughnut data labels still went through fmt_num, which rounded
anything non-integral to one decimal (999.99 drew as "1000.0"). They now
use Chart::data_label like the other label paths (series format, else
axis format, else General), and fmt_num is gone.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 21:01:43 +03:00
Nikita RakitinandCursor 6b071aa053 Video: check the whole picture store against a memory budget (#398)
Per-axis size caps let AV1 accept 65536x65536 frames (over 8 GiB of luma)
and did not count references or pictures in flight. A shared budget module
in deckcraft-bitstream sizes a picture with checked arithmetic, and the
AV1, VP9, HEVC and H.264 decoders refuse a picture size whose current
picture, reference slots and in-flight pictures would exceed 4 GiB (1 GiB
on 32-bit), before allocating. set_memory_budget lowers it for hosts and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:58:31 +03:00
Nikita RakitinandCursor 5d4bfc0846 Media: keep playback with the document and the clip
Shape ids are local to one presentation, and decoded audio was cached by the
address of a buffer the cache did not keep. Switching decks, closing the last
one, or replacing a clip could keep the previous sound. Playback now stops
when the document changes or closes, a new source replaces the voice's audio,
and the decode and duration caches hold the bytes they are keyed by.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:57:08 +03:00
Nikita RakitinandCursor 241ecd59d7 Recovery: unique uids, per-instance ownership, keep snapshots until replaced (#390)
Document uids started at 1 in every process, so two instances sharing
the recovery folder overwrote and deleted each other's entries, and
file.recovery.open deleted each entry as soon as it was loaded, leaving
nothing if the app died before the next checkpoint.

- Uids are a random per-process base plus a counter (below 2^53).
- Each instance holds a lock on owner-<id>.lock in the folder; entries
  record their owner, and recovery open skips a running owner's entries
  and removes stale lock files.
- Opening an entry no longer deletes it: the recovered presentation keeps
  the entry's uid and takes ownership, so the snapshot stays until the
  next recovery save replaces it or a save/close removes it.
- Entry files and metadata are both written via temp file + rename.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:56:21 +03:00
Nikita RakitinandCursor 1fe638bcf8 AV1: reject duplicate film-grain scaling points; a failed job still publishes (#394)
The film-grain header accepted scaling points whose values didn't strictly
increase, and building the scaling lookup then divided by zero. In threaded
decoding the panic left the frame's reference and picture slots unset, so
the output and dependent frames waited forever.

- The header rejects Y/Cb/Cr point values that don't strictly increase
  (spec 6.8.20).
- The scaling lookup moves into scaling_lut, which skips a non-increasing
  pair and uses checked indexing (defence in depth).
- run_job decodes inside guarded (catch_unwind), so a panic becomes an error
  published to the picture and reference slots.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:55:12 +03:00
Nikita RakitinandCursor d2066a1f07 Pptx: budget chart points, chart XML size, inflate work and VML lookups (#401)
- Charts: every category/value/size cache draws from one chart-wide
  budget of 1M decoded slots, so sparse ptCount/idx declarations across
  many series can't expand far beyond the XML.
- Chart parts go through the same 96 MiB XML size guard as other XML.
- Package::open charges every inflated byte, including parts it then
  discards, against a 4 GiB decompression budget and stops reading
  entries once it is spent.
- Legacy OLE previews index each VML drawing once (shape id -> picture
  part) instead of reparsing it and its rels for every object.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:53:49 +03:00
Nikita RakitinandCursor 8bab5c2053 Web: keep a picked file on the document and slide that asked for it
The browser file picker only queued a name and bytes. When the read
finished, DeckCraft guessed the command and ran it on whichever
document and slide were active, so a picture, sound or outline could
land in the wrong place, and a later pick could finish first.

The request now keeps the document, the slide or master, the operation
and a cancellation id. A completion applies there, or is dropped when
that target is gone or the picker was dismissed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:53:10 +03:00
Nikita RakitinandCursor 201ab63c48 Render: cap RGBA conversion and bound the failed-image cache (#402)
decode_at accepted up to 400 million pixels and then called to_rgba8, which
the decoder's allocation limit doesn't cover (a grey image converts to four
times its decoded size), and every picture that failed to decode was kept,
bytes and all, for the life of the process.

- Raster decodes check the RGBA size (512 MiB cap) from the header before
  decoding and again before the conversion.
- The failed-source cache keeps at most 256 entries and 64 MiB of encoded
  bytes, oldest forgotten first; a larger failed input isn't kept, and a hit
  must be the same Arc.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:52:20 +03:00
Nikita RakitinandCursor cb26e7a04f Text: bidi levels for language runs in linear time (#403)
Currency and percent signs in right-to-left runs looked ahead through the
whole ET run for every sign, and each neutral span searched both ways for
the nearest strong direction. Both are now precomputed or carried along, so
a long run of signs or many short neutral spans cost linear work. The old
code stays in the tests as a reference and 4000 random paragraphs match it.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:52:15 +03:00
Nikita RakitinandCursor ce878ef74e Render: budget shape path-gradient work on dense outlines (#404)
A shape path gradient tested every raster pixel against every flattened
outline edge, so an outline with tens of thousands of segments cost
billions of tests and stalled rendering. The outline is now simplified
before rasterising so pixels x edges stays under 64M: points closer than
a raster pixel are dropped first, then every k-th point is kept, with
each polygon left closed and at least 8 edges.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:49:02 +03:00
Nikita RakitinandCursor 272ab5f96f Edit: copies remap media, connector ends, animation targets and triggers (#406)
Paste added clashing media under a fresh id but left the pasted shapes on the
old id, reid did not touch connector ends despite its comment, pasted slides
kept animations on the original shape ids, and slide.duplicate remapped
animation targets but not triggers.

- reid returns the old-to-new shape map; connector ends on a copied shape
  follow the copy, ends on a shape outside the copied set are detached.
- remap_animations points targets and triggers at the copies (an animation
  whose target wasn't copied is dropped, an uncopied trigger cleared); used
  by slide paste and slide.duplicate.
- bring_media returns the media ids it had to change and paste rewrites the
  shapes (and a pasted slide's picture background) to use them; slide copy
  now also carries a picture background's media.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:47:27 +03:00
Nikita RakitinandCursor fb2a83edb6 Table: inserting and deleting rows and columns keeps merges consistent (#408)
Row and column insert/delete ignored merged cells, leaving covered cells
with no anchor or new cells overlapping an existing span. These edits now
read the merged areas first, shift or resize each one (a row or column
inserted inside a merge widens it, one deleted from it narrows it), move
the anchor and its text to the next covered cell when the anchor row or
column is deleted, and rebuild spans and merge flags from the result.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:44:37 +03:00
Nikita RakitinandCursor de098cfa52 Anim: group animations by sequence in one pass (#410)
Timeline::with found trigger sequences with Vec::contains and then rescanned
every animation once per sequence, and checked MAX_EFFECTS only after a
match, so a slide with many distinct triggers cost quadratic work before the
effect cap could stop it.

Animations are now grouped once (an index by sequence, first-appearance order
kept, main first), the build stops at MAX_EFFECTS before the next sequence,
and paragraph groups are deduplicated with a set instead of Vec::contains.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:39:57 +03:00
Nikita RakitinandCursor 952ebad3cf Media: a failed or stalled audio output hands the clock back to wall time (#417)
A cpal stream error was only kept and logged on stop, and the player kept
`streaming` true, so a device that died mid-play froze the voice and video.

AudioOut gains `failure()` (CpalOut reports DeviceNotAvailable), and
Player::tick also treats a stream with no callbacks for a second while a
voice plays as stalled. Either way the stream is stopped, voices continue on
the wall clock, and the next play retries the device; after three failures
the player gives up on the device and plays silently.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 20:27:21 +03:00
Nikita 6873a25635 Codecs: a hostile number is a format error, not an overflow
H.264 and HEVC quantiser offsets, Matroska block times, Opus granule timing and QuickTime PCM frame sizes are range-checked before the arithmetic.
2026-10-13 20:16:51 +03:00
Nikita 1be57fee4b H.264: bound frame_num gap work and reject gaps the SPS forbids (#427) 2026-10-13 19:59:48 +03:00
ZetenypappandClaude Opus 5.5 1baf1f5569 Copy and Cut buttons and menus put the text on the system clipboard (#431)
Ctrl+C and Ctrl+X ran edit.copy / edit.cut and handed the returned text
to egui's clipboard; the ribbon's Copy and Cut, the slide's right-click
menu, the menus, the palette and the control channel ran the same
commands through SlideApp::run, which dropped the text, so it never left
DeckCraft. SlideApp::run now keeps the text of a successful Copy or Cut
and hands it to the clipboard on the next frame, like pending URLs.

Regression test: copy_and_cut_commands_reach_the_system_clipboard runs
both commands and checks the frame's CopyText output (it fails without
the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:45:38 +02:00
Sazid Al Bayazid 8ba97e61be Slide show: Honeycomb's hexagons cover the whole slide again (#420)
The row count came from 1/(1.5*rh) - the row spacing alone - and
ignored how far a settled hexagon actually reaches. On a 16:9 slide
the eleven rows stopped at ~75% of the height: from t~0.9, when every
cell has settled, the bottom quarter was plain black until the t=1
endpoint snapped to the full new slide.

The count now derives from the real geometry: rows sit 0.866*rh apart
and the flat bottom zone of a settled hexagon reaches 0.289*rh past
its centre, so counting to the flat-zone reach covers the slide for
every aspect ratio - counting to the vertex reach still left small
V-notches of black above the bottom edge on about a third of them
(US Letter, square, ...). 16:9 gains four rows (15, was 11), 4:3
seven (20, was 13).

Tests: at t=0.99, when every cell is settled and the seams are nearly
closed, a 0.02-step grid over the slide must sit inside settled
incoming cells (padded 1% past the hairline seams) on five aspect
ratios including US Letter and square. Fails on the old count at
(0.2, 0.74); before/after frames rendered with render-transition.
2026-10-13 21:58:25 +06:00
MarekSpodBiedry 007741718e Slide: leading tabs in a new slide's body set the outline level (#370) 2026-10-13 17:52:00 +02:00
MarekSpodBiedry 97855c70a0 File: save through a unique, exclusively created temporary file (#391) 2026-10-13 17:48:28 +02:00
MarekSpodBiedry 09b72e4b70 Pptx: keep the math namespace equations inherit from the slide root or a wrapper (#412) 2026-10-13 17:41:28 +02:00
MarekSpodBiedry 902edb3c85 UI: notes, alt text and rename drafts no longer carry over to another document (#413) 2026-10-13 17:40:31 +02:00
MarekSpodBiedry d4cf50dd3d MCP: do not resend a command whose reply was lost (#409) 2026-10-13 17:38:28 +02:00
MarekSpodBiedry 33ade63687 Table: split and cell fill stay inside the table (#400) 2026-10-13 17:38:20 +02:00
MarekSpodBiedry 45b8dce220 Info: keep a property draft when another field or the side menu is clicked (#422) 2026-10-13 17:38:13 +02:00
MarekSpodBiedry 27d73db0d9 Engine: a command that fails after editing puts the presentation back (#405) 2026-10-13 17:38:10 +02:00
MarekSpodBiedry 863f7cbd87 Engine: moving the selection to another text body settles the composition (#421) 2026-10-13 17:37:44 +02:00
MarekSpodBiedry a4a7ebe658 Bitstream: reject bad NAL length widths, check NAL endpoints and signed Exp-Golomb limits (#397) 2026-10-13 17:37:25 +02:00
MarekSpodBiedry 9aebc8ba51 AV1: reject tile groups that repeat, skip or reorder tiles (#395) 2026-10-13 17:37:24 +02:00
MarekSpodBiedry efb2127e4e Format scientific number formats (0.00E+00) in chart labels (#419) 2026-10-13 17:36:03 +02:00
MarekSpodBiedry c623b4f555 Matroska: stop following chapter and tag nesting past 32 levels (#396) 2026-10-13 17:35:58 +02:00
NikitaandCursor 0b35b74c4d Show: a laser pointer that leaves no ink
Cmd/Ctrl+L and the on-screen button draw the show's laser colour at the pointer. The pen and the laser are not on together, and a click does not advance the slide while the laser is up.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 18:35:32 +03:00
MarekSpodBiedry 3ed12de918 Opus: validate the header in Decoder::from_head (#425) 2026-10-13 17:33:29 +02:00
NikitaandCursor 525b7ed3f9 Palette: skip commands that need a value
Searching for Export ran file.export with an empty object and reported a missing path. The palette now offers only commands that accept that call, so Export opens the dialog.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 18:31:02 +03:00