Commit Graph
506 Commits
Author SHA1 Message Date
bflatastic f4a7b39731 Merge #444: Fix Send to back leaving layers above siblings across frames (@Silvertree2010)
# Conflicts:
#	crates/engine/src/tests.rs
2026-10-15 15:08:45 +07:00
bflatastic f9b7dcf9d1 Merge #276: fix(ui): disable alignment when the selection can't be aligned (#256) (@Nael-Nathanael) 2026-10-15 15:08:11 +07:00
bflatastic 36c6cf6bd2 Merge #311: fix(pages): inherit the current page background when adding pages (M1) (@tylerjenningsw)
# Conflicts:
#	ROADMAP.md
2026-10-15 15:08:00 +07:00
bflatastic 4ac1d80fe8 Merge #386: Outline stroke on every selected layer, not just the first (@Silvertree2010) 2026-10-15 15:07:19 +07:00
bflatastic 2f0369a7ad Merge #372: fix(engine): new text is readable on the frame it is drawn in (#362) (@vxMacDev) 2026-10-15 15:07:17 +07:00
NetterSilverandClaude Haiku 5.5 7f2f8ee09a Refuse negative or fractional indexes in page.select and doc.switch
Found while testing crates/engine/src/cmd/page.rs:50

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:02:06 +02:00
LosminekandClaude Opus 5.5 49a2c643c6 i18n: add Czech (cs) UI catalog as a draft
Adds crates/ui-egui/locales/cs.toml with all 2165 entries (ui, commands,
menu), marked "draft — needs native review" and kept out of COMPLETE.
Plurals use one/other only; the other form is worded count-neutral so it
reads correctly for 2-4 as well. Lists cs in the pref.language help text,
adds a cs_CZ.UTF-8 detection test and updates the localization docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:01:27 +02:00
NetterSilverandClaude Haiku 5.5 1199f690ad Fix Send to back leaving layers above siblings across frames
Found while testing crates/engine/src/cmd/arrange.rs:327-330

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 06:12:21 +02:00
Edivan TeixeiraandClaude Opus 5.5 13691c1cc0 Let batch steps reference ids created by earlier steps
Inside command.batch, `{"$step": n}` anywhere in a step's params is
replaced by the `id` in step n's result before the step runs
(`{"$step": n, "field": "..."}` picks another field). A nested tree,
such as a frame with children, now takes one batch and one undo step,
not one round-trip per level with ids copied between calls by hand.

Malformed references (forward or self, negative or fractional index,
missing or non-string field, extra keys) fail the batch with BadParams
naming the step, and the existing group machinery rolls it back. The
params walk is depth-capped. Steps are journalled with resolved params.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:43:31 -03:00
NetterSilverandClaude Haiku 5.5 fc6f52e852 Keep corner resize cursors diagonal on wide and tall layers
Fixes #370

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:50:00 +02:00
NetterSilverandClaude Haiku 5.5 3c00ea6440 Outline stroke on every selected layer, not just the first
Fixes #378

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:41:17 +02:00
NetterSilverandClaude Haiku 5.5 3d90d8edfe Keep View toggles such as Rulers in the preferences
Fixes #379

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:33:28 +02:00
NetterSilverandClaude Haiku 5.5 87b9a3a9ce Reject negative or fractional index in effect.set, export.update and export.remove
Found while testing crates/engine/src/cmd/edit.rs:256 (effect.set), :305 (export.update), :317 (export.remove)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:08:36 +00:00
vxMacDev 12d4dbfbc9 fix(engine): new text is readable on the frame it is drawn in (#362)
The colour of new text in the app was chosen from the page alone, so
on a dark page text drawn in a white frame (a new frame's default) was
white and invisible. readable_text_fills now looks at what the text is
drawn on: the nearest layer from its parent out whose top visible fill
is a mostly opaque solid colour, else the page. The Text tool and
text.create / node.create pass the text's parent.

The two tests that asserted the page-only rule in a frame now expect
black text in a white frame; new cases cover dark, unfilled, nested,
faint and stacked fills, and the Text tool.
2026-10-14 18:35:21 -05:00
Ahmed5Emad 93a75a4720 M1: adding auto layout to an existing frame keeps its size instead of hugging 2026-10-15 02:02:00 +03:00
vxMacDev b87d2e476f fix(engine): drag with the Text tool to make a fixed-width text box (#360)
The Text tool made its text on the press and ignored the drag, so a
drag gave auto-width text like a click. The new text now stays in a
TextBox gesture while pressed: past the drag slop it becomes a box as
wide as the drag, its top-left at the drag's, with a fixed width and a
height that follows its text. A click, or a drag within the slop, still
makes auto-width text.
2026-10-14 17:29:09 -05:00
NetterSilverandClaude Haiku 5.5 2aa3ca3c45 Make edit.findReplace replace case-insensitive matches
Found while testing crates/engine/src/cmd/edit.rs:962-977

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 22:26:37 +00:00
NetterSilverandClaude Haiku 5.5 c02e864873 Replace every occurrence in edit.findReplace when replacement contains find
Found while testing crates/engine/src/cmd/edit.rs:975-981

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 21:22:29 +00:00
midasmatterhorn 65e52aa7b9 engine: long digit strings no longer overflow object.opacityKeys
opacity_from_digits multiplied and added on every character of its input
string before the one-or-two-digit check, so "9999999999" overflowed u32 on
the way (panic in debug builds, wrap in release). The scan now steps n with
checked_mul/checked_add and stops (None) at the first character the number
cannot take, so a long digit string is refused like any other bad input.

Test: opacity_digits (engine keymap) covers the ten-digit string and the
command's BadParams refusal.
2026-10-14 23:22:41 +03:00
midasmatterhorn 245837ff27 doc: name counters stop at u64::MAX instead of overflowing
Document::next_name summed max + 1 over numbers parsed from existing layer
names, and bump_name did the same for the duplicate-rename path: a document
(or peer, or node.set) holding a layer named "Frame 18446744073709551615"
overflowed both (panic in debug, wrap to a wrong name in release). next_name
now collects the numbers it sees and, when the counter is spent, picks the
lowest free number (mirrors new_style_id); bump_name only bumps when the
number can step, else falls to its existing " 2" branch.

Tests: next_name_never_overflows (doc) and
duplicating_a_layer_named_past_the_counter_does_not_overflow (engine panel
behaviour).
2026-10-14 22:52:15 +03:00
Barzabel 5e14632207 M1: cursor and double-click tests scan the system fonts themselves (#333) 2026-10-14 22:39:17 +03:00
HaSh3003 fa0365470e M9: remove someone from a live session
Right-click a person's avatar ▸ "Remove <name> from session", or
`collab.removePerson {client}`, when you host the session or own the file
on a server. The hub closes their connection as the end of the session for
them (close 1001: they keep their copy and don't reconnect) and bars their
address from that file while it runs, so the code or link they hold no
longer gets them in; everyone else stays connected.

Protocol addition within version 2: `Admin::RemovePeer {file, client}` on
an open hosting connection with the file's owner token (or the server
key). Older servers say they can't.

Tests: loopback (removed, can't rejoin, others stay), engine (host can,
guest can't, bad ids refused), codec round trip; the collab junk sweep
covers the new command.
2026-10-14 22:32:56 +03:00
vxMacDev b821b6f8f4 fix(recovery): keep the version history and branches in recovery copies (#185)
Recovery copies were written with plain `save_bytes`, so a file
recovered after a crash came back without its version history and
branches, even though `file.recover` reads both. Copies now go through
`save_bytes_with_history` like a normal save, with each open branch
tab's latest edits (`branches::current`), since `sync` only puts those
in before the next command.

A copy was also only redone when the main file's revision changed, and
a file with edits only in a branch tab didn't count as changed. Both
checks now include the file's branch tabs.
2026-10-14 13:41:17 -05:00
midasmatterhorn f1f7da1f40 engine: refuse page.move for node ids that are not pages 2026-10-14 18:01:41 +03:00
tylerjenningsw a3a273cfd3 fix(pages): inherit the current page background when adding pages (M1)
page.add gave every added page the interface theme's colour. Figma's
documentation says added pages inherit the current page's background;
the theme only decides a new file's page. Read the active page's colour
before creating the new page, in the command, so the app, CLI, control
channel and MCP agree.

Refs #295
2026-10-14 10:58:17 -04:00
AganzeFelicite 00ac44f6ca M1: save files automatically
Preferences > Save files automatically (pref.autoSave, off by default;
hidden in the browser, which has no files to save to). With it on, each
open file that has a path is saved 3 s after it changes, never during a
gesture, a live text preview or while a version is shown. A failed save
says why in a toast and tries again a minute later.

Version history checkpoints (version.autosave, every 30 minutes) only ran
for files with unsaved changes, so a file that saves itself would stop
getting them; with auto-save on, edits made since the file was opened
count.

Closes #296
2026-10-14 16:48:56 +02:00
MarekSpodBiedry 6eeac4fa04 Keep the redo steps when a failed command batch rolls back (#196) 2026-10-14 16:14:35 +02:00
Marco Arnulfo e92792e3e8 M1: a press on a corner handle resizes instead of picking the corner-radius handle (fixes #292)
The corner-radius handle sits 10 px in from each corner and answers within 6 px, so a press on the inner half of the 8 px corner square picked it: the drag rounded the corners or, dragged outward, did nothing. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the constrain-proportions link); further in the radius handle still answers.
2026-10-14 14:36:39 +02:00
notdezzi 661144021e fix(format): exclusive unique staging files for atomic save/export
Saving and exporting wrote a truncating std::fs::write to a deterministic
staging name (<path>.tmp-figcraft, <file>.figcraft.tmp) before renaming, so an
unrelated file or a planted symlink at that name was clobbered or written
through, and separate writers of one destination shared a staging file they
could corrupt. Both paths now go through figcraft_format::write_file_atomic: a
uniquely named sibling staging file is created exclusively (create_new) and
written through its own handle, renamed over the destination, and removed again
on any failure, so only the staging file a write created is ever touched. Tests
cover pre-existing staging files and symlinks, concurrent writers, and
injected rename and permission failures.
2026-10-14 13:11:28 +02:00
RealtaNuaandClaude Opus 5.5 73b2845ba4 fix(engine): Alt-drag copies snap to their original; Duplicate repeats the step
- A copy Alt-dragged off a layer snaps to that layer too: its edges,
  centres and equal spacing (Targets::add_box with the originals' box).
  Before, the originals were left out of the snap targets, so a copy
  couldn't line up with or space evenly from the layer it came from.
- Duplicate (Cmd+D) repeats the last duplicate's page offset while its
  copies are what it duplicates: Alt-drag a copy, or Duplicate and move
  the copy, then each Cmd+D steps out the next one as far again. The
  step is measured when Duplicate runs. It lasts as long as the
  selection (undo reuses ids), copies placed by an auto layout set no
  step, and explicit dx/dy win.
- measure_tests: an Alt-dragged copy between its original and another
  layer now snaps to equal spacing (45/45 instead of 50/40).

Tests: duplicate_spacing_tests (snap to the original, equal spacing,
repeat after moving a copy and after an Alt-drag, only for the last
copies, undo then a new layer, rotated frame, auto layout).

Refs #256

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 18:03:48 +07:00
notdezzi 10dc38495f fix(engine): roll back failed commands during interactions too
A command that fails part-way (node.set applies geometry keys before
values like visible, which can fail validation) mutated the document
even when an interaction was under way, because the rollback in
Session::execute was gated on interaction.is_none(); drop the gate so
the command-entry snapshot is restored during interactions as well,
which is the interaction's last valid preview and leaves the gesture
itself intact.
2026-10-14 12:54:02 +02:00
Justin John 61def74383 M0: finish canvas drags released outside 2026-10-14 16:04:34 +05:30
RealtaNuaandClaude Opus 5.5 b8e5b8e133 fix(pen): show the bezier handles while drawing
The Pen showed only its anchors while a path was drawn; handles appeared
only in vector edit mode afterwards. Now:

- The point being dragged shows its in and out handles, and the point
  before it its out handle (the handles shaping the segments at the pen),
  drawn like vector edit mode's (one shared canvas helper).
- Moving on to the next point, the rubber band is the segment a click
  would make: it leaves the last anchor along its out handle instead of
  going straight.
- A press no longer leaves the previous straight rubber band on screen
  over the segment being dragged.

Overlay gains pen_handles and rubber_handle (shown in ui.inspect).

Tests: pen_overlay_tests (handles during and after a drag, none and a
straight band for corner points, cleared on finish); canvas_fixes_tests
(handle lines and dots painted mid-drag, curved band after release).

Refs #256

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 16:58:51 +07:00
RealtaNuaandClaude Opus 5.5 9c0b441119 fix(ui): disable alignment when the selection can't be aligned
One layer directly on the page has nothing to align to (one layer aligns
to its parent frame), so the six Alignment buttons looked active and did
nothing. They are now disabled there, with a hover explaining why.

- cmd/arrange.rs: align_target (unchanged rule) and pub can_align.
- right.rs: the align rows in add_enabled_ui + on_disabled_hover_text.
- The hover text in every catalog.

Tests: a_layer_alone_on_the_page_has_nothing_to_align_to (engine),
the_align_buttons_are_off_for_a_layer_alone_on_the_page (AccessKit).

Refs #256

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 16:53:33 +07:00
VanTan 4964910cc9 M7: follow system theme from the first frame 2026-10-14 10:34:14 +03:00
VanTan 7a0bd980c6 M9: add Russian interface translation 2026-10-14 09:33:58 +03:00
Piettro CastroandClaude Opus 5.5 f3cd0509cb fix(engine): read the documented gradient shorthand on every route (#252)
`{"type":"linearGradient","stops":["#hex",…]}` was only expanded by the MCP
tools, so `figcraft-cli run` and the control channel's `engine.execute`
refused it ("`fills` must be paints or colours"). The expansion now lives in
the engine (`params::normalize_paint`, used by `paint_of`), so node.set,
node.create props, fill.add/set, strokes and paint styles all take it; MCP
calls the engine's copy instead of its own.

Tests: engine (every paint command, MCP-equal defaults, junk still refused),
control channel `engine.execute`, and `figcraft-cli run` end to end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:02:45 -05:00
bcedfeba10 M7: recent files as thumbnail cards on the home screen (#249)
The Recents section shows recent files as cards: the picture each file
carries (thumbnail.png in .figcraft and .fig, an image file itself) fitted
on its first page's colour, a divider, and a footer with the file's icon,
name and "Edited ... ago" (or "File not found"); the full path on hover.
Pictures are read off the frame's thread, only for cards in view, cached
by path and modification time, capped in bytes and pixels; missing,
damaged or oversized ones give a placeholder. They fade in, and cards
show a skeleton until their file has been looked at.

A sort menu (Alphabetical, Date created, Last modified; either order) and
a grid/list toggle sit above them. The list is a Name, Last modified and
Created table whose headers sort. View (per section) and sort are kept in
the preferences and set with ui.set {homeRecentsView, homeRecentsSort,
homeRecentsOrder}. A click selects, a double-click or Enter opens, arrow
keys move the selection; a right-click offers Open, Show in folder and
Remove from recents (file.openRecent {remove}).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-10-14 12:00:20 +07:00
rcoenenandClaude Opus 5.5 6267ae5638 Vector edit mode: hover, hatch, select and drag filled regions (#243)
With the Move tool, the filled area under the pointer is hatched with "/" lines in the selection colour (1.4 px), clipped by the area's own fill rule so holes stay clear. The hatch goes when the pointer is 3.5 px from a point or 2 px from a segment (screen pixels at any zoom); between those clearances and the 6 px hit radius the area wins. Bezier handles, the transform box's handles and other edited layers' parts come first. Line spacing follows octave folding: 8 px at every power-of-two zoom, growing with the zoom and stepping back at sqrt(2) * 2^n. The pattern is anchored to the vector's origin, so it stays on the shape when panning, and several selected areas share one pattern. The last area listed wins where areas overlap, and only vectors with a visible fill have areas.

A click selects an area with its points; they move with it (drags, nudges, the Design panel) but keep the unselected look and show no handles. Shift adds an area or takes a selected one out (its own points go, points selected on their own or shared with another selected area stay). A press on a selected area drags everything selected, as one undo step; Escape puts it back. Drags show no hover hatch. Any other press without Shift drops the areas and the points selected only through them, and a click on an empty spot such as a hole clears them and stays in edit mode. Inside the transform box around points selected on their own, the box keeps its drag unless the area is already selected. Area indexes are dropped when the network's topology changes; moving points keeps them.

The network gets region walking, an inside test per region by its own fill rule, the region under a point, a region's points and a topology fingerprint (vector_regions.rs). The engine does hit testing and lays out and clips the hatch in screen space (vectoredit/regions.rs); the canvas only draws the lines. vector.edit.select takes regions, and vector.edit.state reports regions, regionVertices and regionHover. Tests: vector_regions_tests (doc) and regions_tests (engine), covering fill rules, overlap, curves, reversed segments, clearances at 100% and 400%, selection, drags, undo and cancel, and the hatch spacing at seven zoom levels, its direction, anchoring, coverage and clipping.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:17:17 +07:00
5e90592e80 M1: report images a native file lost on loading (#207) (#242)
A `.figcraft` image member that is damaged, too large or missing was skipped
without a word, and saving the file again dropped it for good. The load now
lists each lost image (up to 20, then a count) with its hash and the reason,
file.open puts it in `repairs` and sets a session message, and the app shows
that as a toast.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-10-14 10:32:44 +07:00
24c5617230 feat(dev-mode): Leptos code with component properties, instance calls and Code Connect (#240)
* feat(dev-mode): Leptos code for a layer tree (#223)

inspect.code takes language: leptos, and the Inspect panel lists it: the
HTML markup inside a #[component] fn's view!, with the stylesheet after it
as for React. Text is Rust string literals (line breaks as <br/>) so the
macro parses it; px or rem like the other web languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(dev-mode): Leptos components take their properties, instances are calls (#223)

Leptos joins React in component code: a component or set is a #[component]
fn with #[prop] defaults (variant enums with as_str, bool, String, ViewFn
for instance swaps, Option<ViewFn> for slots), layers bound to booleans
show through .then, variants differ by data-* CSS rules, and instances
(also inside layer trees) are calls passing the values they change. Names
are snake_case, Rust keywords get a trailing _. Code Connect takes leptos
too (use <path>::<Name>; view! markup). Markup now takes the language
instead of jsx/leptos flags; React and Leptos share the stylesheet pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Justin John <babyjjbabyjj549@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-10-14 10:32:14 +07:00
fb8ace5bf4 M1: full screen (#227) (#238)
View > Full screen (`view.fullscreen {on?}`, F11 on Windows and Linux,
Ctrl+Cmd+F on macOS) makes the window fill the screen, taskbar included,
and leaves it again. FigCraft's own menu item, translated in every catalog.

Tests: the key and the command toggle the window, junk parameters are
ignored; the shortcut table pins the new binding (49 pinned).

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-10-14 10:31:59 +07:00
Giuseppe Cautieroandbflatastic ac65168417 Implement centralized transactional fix for command batch failures (#229)
* Agent Host changes for agents/fix-command-batch-history-restore

* Implement centralized transactional fix for command batch failures - #1

* The batch rollback tests compile, are formatted and expect the tab that was in front

The new module used a private helper of plugins_tests and an unused import; the plugin test
asserted tab 0 was in front, but it opens a second file first, which is the one in front
when the plugin starts and the one the rollback puts back.

---------

Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-10-14 09:30:16 +07:00
Sir Clangington III. 79a0e58a3c fix(plugin): meter every host call and copied byte (#221)
Every plugin import now counts as a host call (reply_len, reply_read and set_output were free), and bytes copied out of or into the plugin's memory spend fuel, reserved before the copy or allocation, including when call overhead is set to zero. Fixes #192.

The log byte budget test from #216 runs with the default fuel, since copying a megabyte of log bytes is itself charged now.
2026-10-14 07:42:06 +07:00
fede9234 db55720c93 fix(engine): keep proportions when a linked width or height is typed (#222)
With a layer's proportions locked, setting only its width or only its height through node.set (the W and H fields) scales the other side by the layer's own ratio; setting both in one edit leaves them as typed, an unlinked layer is unchanged, and a zero side has no ratio to keep. Fixes #220.
2026-10-14 07:31:19 +07:00
YTwsy 0efbd55564 Bound plugin logs before allocation and host forwarding (#216)
Plugin log lines are admitted before guest memory is copied: a line over 8 KiB, more than 1,000 lines or more than 1 MiB of log bytes in a run fails the run with a limit error, and the same budgets bound what is forwarded to the host logger. Fixes #191.
2026-10-14 05:49:32 +07:00
Euisuh Jeong 5b52eeeb36 fix(collab): reject reverting an active live document (#214)
file.revert is refused while the file is in a live session (hosting, joined, publishing or reconnecting): reverting replaced the document's identity and broke its session. The refusal changes nothing, and reverting works again after leaving. Fixes #198.
2026-10-14 05:47:39 +07:00
YTwsyandbflatastic 33dcb1aef6 Refuse incomplete SVG/PDF exports beyond the depth limit (#218)
SVG and PDF exports check the visible subtree's depth before bounds or drawing and refuse, with an actionable message, a document nested past 256 levels instead of silently leaving the deep layers out; nothing partial is written, and an inner layer can still be exported. The CLI and MCP report the same error. Fixes #208.

Also: on Windows, figcraft and figcraft-cli link with an 8 MB main-thread stack (Windows gives 1 MB where Linux and macOS give 8), so an export at the depth limit no longer overflows it.

Co-authored-by: bflatastic <bflatastic@users.noreply.github.com>
2026-10-14 05:23:28 +07:00
Euisuh Jeong d30255bf62 fix(engine): preserve the underlying repeat command (#212)
edit.repeat no longer records itself as the last command, so repeating keeps applying the command it repeats instead of recursing, and it refuses when there is nothing to repeat. Fixes #181.
2026-10-14 03:26:16 +07:00
bflatastic e56856f401 M3: component playground: variant, boolean and text properties 2026-10-13 13:36:54 +07:00