Commit Graph
9054 Commits
Author SHA1 Message Date
dotCooCoo acb6b880d5 Links: links inside symbols, patterns and masks are saved with relative paths
A save wrote the path relative to the document only for linked images and
placed documents in the layers. Links inside symbols, graph designs,
patterns and opacity masks kept only their absolute path, so after the
document and its files moved to another folder together, those files were
not found.

- The save sets the relative path of every link the document holds.
- On open, a link inside that art whose file was found away from its path
  takes the new path, as a link in the layers does, so the next save
  writes the right relative path.
- Document::update_links_where changes only the links it picks and copies
  only the nodes on the way to them.

Docs: docs/mcp.md (Linked images), docs/target-app-parity.md.
2026-11-08 12:47:57 +07:00
dotCooCoo 8d3a64453a Image Trace: buttons above the Preset menu choose the Auto Color, High Color, Low Color, Grayscale, Black and White and Outline presets
Six buttons above the Image Trace panel's Preset dropdown choose a preset as the dropdown does:
Auto Color chooses Auto Color, High Color chooses High Fidelity Photo, Low Color chooses Low
Fidelity Photo, Grayscale chooses Shades of Gray, Black and White chooses Black and White Logo,
and Outline chooses Outline. With Preview on, a click traces the selected Image Trace object again
as one undo step; with nothing traced selected, it changes the panel's settings only. The button of
the panel's preset shows as selected, and its tooltip names the preset.

The help page lists these options before Preset:
https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/image-trace-panel-options.html
Which preset each button chooses is VectorCraft's own.

High Color and Low Color get rows in every catalog.
2026-11-08 12:47:56 +07:00
dotCooCoo 12da9ef677 Artboard Options: the fade covers the box around crossing artboards when they would leave too many rectangles 2026-11-08 12:47:55 +07:00
dotCooCoo cdf42d5ab5 Docs: Fade Region Outside Artboard lines name Presentation Mode and the new tests 2026-11-08 12:47:55 +07:00
dotCooCoo 92d3505529 Artboard Options: Presentation Mode draws no fade, and the fade's rectangles stay few for many artboards
The fade outside the artboards is no longer drawn in Presentation Mode, which shows only the art, even when the Artboard tool is active.

The rectangles that make up the fade are now merged down from band to band when a gap keeps the same left and right edges, and the bands are swept with the boards sorted by their top edge. A row of 200 top-aligned artboards of different heights now gives at most 600 rectangles instead of about 20,000.
2026-11-08 12:47:55 +07:00
dotCooCoo e620da7ed6 Artboard Options: the fade covers art outside the artboards and OK sets the Global checkboxes after the artboard
With Fade Region Outside Artboard on, the canvas now draws the fade over the
art as well as the pasteboard: one mesh of black at 40% opacity covers the
canvas outside the artboards, so the pasteboard and any art on it show each
channel at 60% of its value, and art inside an artboard is unchanged. It
replaces the darker pasteboard fill.

Artboard Options sets the Global checkboxes' preferences only after
artboard.setProps or artboard.new succeeds. When the artboard command
refuses a value, the preferences stay as they were and the dialog stays
open. A Global value that is not true or false is refused before anything
changes.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-08 12:47:54 +07:00
dotCooCoo 1063f5090a Artboard Options: Fade Region Outside Artboard darkens the pasteboard while the Artboard tool is active
Artboard Options gets a Global group with Fade Region Outside Artboard and
Update While Dragging. Both are application preferences
(fadeRegionOutsideArtboard, artboardUpdateWhileDragging), on by default and
also listed under Preferences > General > Artboard Options.

While the Artboard tool is active, the canvas draws the pasteboard with each
color channel at 60% of its value. With Update While Dragging off, the
pasteboard draws as usual while the tool moves or resizes an artboard and
darker again when the drag ends. Exports and print do not change.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-08 12:47:53 +07:00
dotCooCoo e5ddabf51f Type: find and replace and Type menu commands reject a param of the wrong type
Find and Replace, Find Next, Change Case, Smart Punctuation, Insert
Character and Type on a Path Options read their params with helpers that
fell back to the default for a value of the wrong type: Find and Replace
with `replace: 5` replaced every match with nothing, and `flip: "true"`
did not flip. They now return an error and leave the document, the
selection and the undo history unchanged:

- `find`, `replace`, `case`, `char`, `text`, `unicode`, `effect` and
  `alignToPath` must be a string or null.
- `matchCase`, `wholeWord`, `dashes`, `ellipsis`, `quotes` and `flip`
  must be true, false or null.
- Smart Punctuation's `scope` must be `selection` or `document`.
- Type on a Path Options' `spacing` must be a number or null.
- Insert Character's `code` must be a whole number or null.
- Find and Replace's `ids` must be a list of existing object ids.

Insert Character checks `char`, `text`, `code` and `unicode` before it
picks one, so `{"char":"bullet","unicode":176}` is refused too. The
order the sources are used in is unchanged. Null counts as left out, so
`type.insert {code: null, unicode}` inserts the Unicode character.
Omitted params keep their defaults.
2026-11-08 12:47:53 +07:00
dotCooCoo 81a7a68388 .ai import: legacy type in CJK fonts decodes by the font's CMap
Legacy type in a composite font such as HeiseiMin-W3-83pv-RKSJ-H was
decoded one byte to a character, so Japanese, Chinese and Korean type
opened as unrelated single-byte characters, and the font name kept the
CMap.

Tf now finds a predefined CMap at the end of the font name, unless a TZ
gives the font encoding pairs, and the font is the name without it.
Strings in that font are read by the CMap's code space, mapped to CIDs
with the predefined CMaps craft-pdf carries, then to Unicode by the
character collection's CID-to-Unicode CMap. Codes the CMap doesn't map
are dropped. A -V CMap is read with its -H form's codes. Codes in the
Unicode CMaps (UCS2, UTF8, UTF16, UTF32) are read as Unicode; those
helpers follow mockydevs' #6067. A 0x0D byte ends a paragraph and a 0x09
byte is a tab in these fonts too.

The predefined CMaps craft-pdf doesn't carry (RKSJ, 78-RKSJ, 78ms-RKSJ,
78-EUC, 78, Add, Ext, NWP, GB, CNS1, KSC, B5 and the Hong Kong B5 sets
other than HKscs-B5) are read with the nearest
carried CMap, which can give a different character for some codes.
GBT, GBT-EUC, GBTpc-EUC, CNS2 and KSC-Johab type still decodes byte by
byte, with the CMap removed from the font name.

Docs: docs/file-format-parity.md.
2026-11-08 12:47:52 +07:00
dotCooCoo c32f44ba3a Color: Recent Colors keeps the last 30 colors used, across documents
Recent Colors now keeps up to 30 solid colors instead of 10, newest
first and each once; applying a 31st drops the oldest. The Session
holds the list, so it stays the same when another document is opened
or made active, and it lasts for the session.

The Color and Swatches panels wrap the chips onto further rows when
the panel is too narrow for one row, so every color can be clicked.
A list that fits one row draws as before.

Docs: paint.recent params text, docs/mcp.md, docs/target-app-parity.md.
2026-11-08 12:47:51 +07:00
dotCooCoo 19fd8d1d5e Control bar: the panel menu and the gripper dock it to the top or the bottom of the window
Squashed from #6090 onto main.
2026-11-08 12:47:51 +07:00
BFlat 704efb045c Merge pull request #6103 from dotCooCoo/fix/svg-missing-links-key
SVG import: each linked file keeps image keys of its own
2026-11-08 13:40:29 +08:00
BFlat fc8e3c538d Merge pull request #6102 from dotCooCoo/fix/huge-selection-brackets-test
Tests: huge-selection vertex counts leave out the art's raster
2026-11-08 13:40:15 +08:00
BFlat 9c3491a5be Merge pull request #6097 from dotCooCoo/fix/outline-low-path
Path ops: stroke outlining and unions sweep long, low paths along their length
2026-11-08 13:40:00 +08:00
BFlat a17bea9a8f Merge pull request #6095 from dotCooCoo/fix/reset-bounding-box-group-members
Object: Reset Bounding Box also squares the objects inside the selection
2026-11-08 13:39:45 +08:00
BFlat 449425e200 Merge pull request #6092 from dotCooCoo/fix/fill-changed-part
Canvas: moving anchors of a long path redraws only around them
2026-11-08 13:39:35 +08:00
BFlat c6025d74b9 Merge pull request #6091 from dotCooCoo/fix/ai-pdf-later-page-artboard
.ai import: type on a later page of a PDF of some artboards goes to its artboard
2026-11-08 13:39:20 +08:00
BFlat 6711bb1b70 Merge pull request #6088 from dotCooCoo/feat/prefs-search
Preferences: a search field finds a setting and goes to its category
2026-11-08 13:39:05 +08:00
BFlat 584a1665da Merge pull request #6087 from dotCooCoo/fix/shortcuts-path-param
Shortcuts: export, import, set, list and preset reject a param of the wrong type
2026-11-08 13:38:48 +08:00
BFlat 05aab8b437 Merge pull request #6086 from dotCooCoo/feat/snap-to-last-location
Smart Guides: Snap to Last Location puts a moved selection back where it began
2026-11-08 13:38:33 +08:00
BFlat 2f49bb6aa2 Merge pull request #6082 from dotCooCoo/fix/style-face-once
Type: layout looks up each style run's face once
2026-11-08 13:38:17 +08:00
BFlat b1f6203509 Merge pull request #6081 from dotCooCoo/fix/tr-unquoted-keys
i18n: error messages keep the keys and values they name without backticks
2026-11-08 13:38:02 +08:00
BFlat 91437c2091 Merge pull request #6080 from dotCooCoo/feat/ctrl-enter-ends-text-edit
Type: Cmd+Enter (Ctrl+Enter) ends text editing and keeps the text
2026-11-08 13:37:47 +08:00
BFlat 2c95bc9b87 Merge pull request #6079 from dotCooCoo/fix/issue-5816
Fix #5816: CJK and other fallback faces in the interface sit on the Latin baseline
2026-11-08 13:37:32 +08:00
BFlat d17dd59247 Merge pull request #6076 from dotCooCoo/feat/symbol-reset-transformation
Symbols: Reset Transformation returns an instance to its symbol's scale, rotation and shear
2026-11-08 13:37:16 +08:00
BFlat 263ddbea00 Merge pull request #6075 from dotCooCoo/fix/more-tests-without-time-limits
Tests: the CSS, glow and .ai Zstandard guards count work instead of timing it
2026-11-08 13:37:00 +08:00
BFlat e9271a7a00 Merge pull request #6074 from dotCooCoo/feat/history-new-document
History: the New Document button makes a document from the selected state
2026-11-08 13:36:45 +08:00
BFlat 70427de13d Merge pull request #6073 from dotCooCoo/fix/group-drag-outline
Selection: a group dragged with Real-time Drawing off shows each member's outline
2026-11-08 13:36:29 +08:00
dotCooCoo e29e01b650 SVG import: each linked file keeps image keys of its own
Images were keyed by their bytes alone, so two <image> elements that
link different files with the same bytes shared one image key. Every
missing file shows the same placeholder, and files can also be copies
of each other. Export reads a linked file's pixels into the image key
when the document holds only a preview, so once one of the files was
found or changed, the images of the other file exported and printed
that file's pixels.

A linked image's key now also holds a hash of its link's path, as the
.ai reader's linked images do. Images of one file still share a key.
The missing-file placeholder comes from
vectorcraft_doc::links::missing_placeholder instead of a copy in the
SVG importer.
2026-11-08 12:23:53 +07:00
dotCooCoo 7b8fccf785 Tests: huge-selection vertex counts leave out the art's raster
The huge type-on-a-path selection test compares the canvas mesh drawn with
the Rectangle tool against the one drawn with the Selection tool. When the
render worker's frame lands between the two (a slow first frame under heavy
load), the art's textured quad shows in only one of them, and the test
counted its 4 vertices as brackets.

canvas_vertices and frame_vertices now skip the mesh drawn with the canvas
art texture, so the counts hold only what the overlays draw. A huge
selection of type, images, symbols or meshes used the same comparison.
2026-11-08 12:23:52 +07:00
dotCooCoo 8a7011a9d3 Path ops: stroke outlining and unions sweep long, low paths along their length
The overlap sweep moves a horizontal line down the page, and its time
per event grows with the number of segments on that line. A long, low
path, such as rows of a zigzag, puts a whole row on the line at once.

stroke_region, unite_all_with and try_unite_all_with now estimate how
many segments the sweep holds at once in each direction, from up to
4,096 segment boxes (one from each run of segments, at a position that
changes from run to run). When the estimate for the input turned a
quarter turn is under two thirds of the upright one (2,048 segments or
more), the operation runs on the turned input and the result is turned
back. A quarter turn swaps and negates coordinates, so no point moves
by rounding. Offset Path is not turned. Callers of unite_all,
try_unite_all and outline_stroke take the turned sweep through these
functions.

Tidy's lookups of the input curves around a piece and of the kept
anchors can search along y: once lookups with more than 64 candidates
along x have scanned 16 per item, a lookup sorts the list along y once
and uses that window when it holds fewer candidates (under a quarter as
many for curves). Where the y window rarely helps, only one lookup in
32 measures it. is_kept counts the few points within its tolerance
directly instead of searching for the window's end.

In the sweep_axis tests, a turned result has as many subpaths as the
result without the turn, and the area where the two differ is no
larger than the refit precision times the outline's length.

On one open path of 200,000 anchors in rows of 1,000, Outline Stroke
takes 17.3 s of processor time instead of 31.3 s and Unite 2.9 s
instead of 6.5 s in a release build. Unite of 2,000 circles takes the
same time as before.
2026-11-08 12:22:04 +07:00
dotCooCoo d495b4a378 Object: Reset Bounding Box also squares the objects inside the selection
Reset Bounding Box set the angle to 0 on the selected objects only. A
rotation changes the box angle of the selected objects and of every
object inside them, so the members of a rotated group kept their angle:
after rotating two rectangles 20 degrees, grouping them, rotating the
group 10 degrees and resetting, Ungroup brought back a box at 30
degrees, and a second reset reported 2 objects to change.

The command now also squares every object inside the selected ones: the
children of groups, compound paths, clip groups, blends, envelopes,
repeats and intertwines, and the art of linked opacity masks. The art of
an unlinked mask does not turn with the object, so it keeps its angle.
Ungroup, Release Clipping Mask, Release Compound Path, Blend Expand,
Release Envelope, Release Repeat, Release Intertwine and Release Opacity
Mask then bring back boxes square to the page. The whole reset stays one
undo step.

Two results change. `changed` counts every object reset, the objects
inside included, so the steps above return 3 where they returned 1. A
group whose own box is square but whose members are turned used to
return 0 with no undo step; it now squares the members in one step.

Docs: params text, docs/mcp.md, docs/target-app-parity.md.
2026-11-08 12:22:01 +07:00
dotCooCoo 4fefdaf95a Canvas: moving anchors of a long path redraws only around them
When only anchors of a path or compound path move, the area an edit
changed is now the box of the segments that start or end at a moved
anchor (and the line that closes an open subpath's fill at a moved
end), before and after, grown by the farthest the strokes reach. The
canvas then draws that part of the frame again instead of the whole
object. Objects with a non-solid paint, dashes, a width profile,
arrowheads, a brush, effects or an opacity mask still count whole.

At 1600 x 1200, single-threaded, the frame after one anchor of a filled
closed path moves takes 16.6 million CPU cycles instead of 56.4 million
with 50,000 anchors, and 65.6 million instead of 180.0 million with
200,000 anchors.
2026-11-08 12:21:58 +07:00
dotCooCoo 0f39b54a05 .ai import: type on a later page of a PDF of some artboards goes to its artboard
A PDF saved with some of its artboards has a page for each of them, while
the editing data keeps them all. The importer took the pages after the
first to show the artboards right after the first page's artboard. In a
PDF of artboards 2 and 4 of 4, the second page's type went to artboard 3.
In a PDF of artboards 1 and 3 of 4, the second page's type went to the
left edge of artboard 2.

Each page after the first now goes to an artboard of its size that comes
after the previous page's artboard and leaves an artboard for each later
page. The artboard chosen for the first page also leaves one for each
later page. When several artboards fit a later page, the importer
compares their art with the page and takes the closest one, or the first
of them on a tie. It compares at most 16 artboards for all the later
pages together, the same limit it uses for the first page. A later page
with no comparisons left takes the first artboard that fits it.

When the PDF has fewer pages than artboards and its first page shows the
first artboard, the first page's type and page art still go to the art's
box or to the editing data's artboard.
2026-11-08 12:21:54 +07:00
dotCooCoo aa2aa40166 Preferences: a search field finds a setting and goes to its category
The Preferences dialog has a Search Preferences field above the category
list. It has the keyboard focus when the dialog opens, and Cmd/Ctrl+F
focuses it again. While it has text, the list shows every setting whose
label, section or category contains it (ignoring case, in the interface
language or in English), or "No settings found". Choosing a setting shows
its category, scrolls to its row and highlights it. Escape in the field
with text clears it; Escape in the empty field closes the dialog.

The dialog's `__search` field holds the text, so `ui.dialog.set` drives
the search too.
2026-11-08 12:21:47 +07:00
dotCooCoo 7a035cd88f Shortcuts: export, import, set, list and preset reject a param of the wrong type
shortcuts.export and shortcuts.import read `path` only when it was a
string, so a `path` such as 5 or true was dropped and the file dialog
opened. They now return "`path` must be a string" and open no dialog;
an omitted or null `path` still asks for the file.

shortcuts.set (`id`, `shortcut`), shortcuts.list (`query`) and
shortcuts.preset (`name`) give the same error for a value that is not a
string, and shortcuts.set gives "`force` must be true or false" for a
`force` that is not true, false or null. Nothing changes when a param
is rejected.
2026-11-08 12:21:44 +07:00
dotCooCoo 313768efd5 Docs: Snap to Last Location line sits after the Shift move line and names every test 2026-11-08 12:21:37 +07:00
dotCooCoo 6270fda90b Smart Guides: Snap to Pixel takes precedence over Snap to Last Location
A short move with View > Snap to Pixel on returned to where it began and
showed a "last location" label, skipping the pixel step. Snap to Last
Location is now off while Snap to Pixel is on, for the Selection tool and
for paths Direct Selection moves by an anchor, so the move lands on whole
pixels as it does with the option off.
2026-11-08 12:21:37 +07:00
dotCooCoo 61a99ed4ed Smart Guides: Snap to Last Location puts paths Direct Selection moves by an anchor back
A path selected whole that Direct Selection moves held by one of its anchors snapped only through the anchor-drag targets, so Snap to Last Location never put it back. The move now goes back to where it began within Snapping Tolerance, labeled "last location", as a Selection tool move does; an Alt copy is not pulled back.
2026-11-08 12:21:36 +07:00
dotCooCoo b093e53e77 Smart Guides: Snap to Last Location puts a moved selection back where it began
Preferences > Smart Guides gets Snap to Last Location (`snapToLastLocation`,
off by default). With it and Smart Guides on, a selection moved by dragging
with the Selection, Group Selection or Direct Selection tool, Shift too, goes
back to where the move began while the move is within Snapping Tolerance,
labeled "last location". An Alt copy is not pulled back onto its original.

Source: https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/smart-guides-options.html
The distance, the label and the copy rule are VectorCraft's own.
2026-11-08 12:21:36 +07:00
dotCooCoo f42fdf900f Type: layout looks up each style run's face once
Layout kept no record of a run's face, so each paragraph looked up the face
of its first character three times for its line metrics and once more to
shape it. The layout now keeps each run's face the first time it is looked
up and uses it for shaping, for paragraph metrics, for on-path type's empty
line and for the column offset of vertical point type. Glyphs, positions,
bounds and baselines do not change.

A test-only helper lays text out looking every face up at each use, and a
new test requires point, vertical point, area and path type laid out both
ways to match exactly.

The layouts of 5,000 short point type objects take a median of 215 million
CPU cycles of the laying-out thread instead of 236 million in a release
build (11 alternating runs of each build on Windows 11, measured while
other builds were running).
2026-11-08 12:21:32 +07:00
dotCooCoo a9fc7216a9 i18n: error messages keep the keys and values they name without backticks
Turkish error rows translated parameter keys and values that the English
writes without backticks, so a message named a key or value the command
does not accept: `group must be true or false` read as "grup doğru veya
yanlış", `index must be ≥ 0` as "dizin", and `span` with "object" and
"selection" as "yayılma", "nesne" and "seçim". The rows for the docking,
option and setting errors (onto, below, above, anchor, before, panel,
zone, column, group, action, move, tool, kerning, balance, brightness,
decimals, lang, percent, plane, scale, control, span, index) now keep the
key and its values as the English has them. `included` and `others` lose
the double quotes the English does not have.

Bulgarian and Catalan rows that translated bare true and false now keep
them. Hindi messages whose double-quoted values were translated or
misspelled ("auto", "contents", "none", "start", "corner", "selection" and
others) now keep the values as the English spells them.

A new test checks that every translated status or error message keeps
each value its English puts in double quotes.
2026-11-08 12:21:29 +07:00
dotCooCoo 0382ba5ee8 Type: Cmd+Enter (Ctrl+Enter) ends text editing and keeps the text
While the Type tool edits text, Cmd+Enter on macOS and Ctrl+Enter on
Windows and Linux typed a line break. It now ends editing as Escape
does: the typed text stays, the type stays selected, and the tool used
before editing is selected again. A held chord ends editing once; its
repeats type nothing and do not open the Move dialog.

Enter, Shift+Enter and Ctrl+Enter on macOS still type line breaks, and
so does the chord with Alt or Shift. Cmd+Ctrl+Enter on macOS ends
editing too. A command bound to the chord still runs instead, and
editing continues. While an IME composes, the chord stays the IME's.
2026-11-08 12:21:23 +07:00
dotCooCoo 5db4cfe9bc Fix #5816: bundled fallback faces and monospace text share the Latin baseline
The bundled craft-fonts Japanese, Georgian and Devanagari faces now get
the same baseline shift as installed fallback faces, so every glyph in
an interface label sits on the baseline of its family's first face.

Each fallback face's shift is measured against the first face of the
family it is added to (Inter for the proportional families, JetBrains
Mono for `mono`, egui's face for `Monospace`). Families whose first
faces need the same shift share one copy of the face; the others get a
copy named after their first face.

Tests: craft_fonts_sit_on_the_baseline_of_each_family checks each
bundled face in every family against its own baseline, and
fallback_faces_sit_on_the_ui_baseline now checks every family,
monospace ones included.
2026-11-08 12:21:20 +07:00
dotCooCoo 0ad4d4f06c Fix #5816: CJK fallback faces in the interface sit on the UI font's baseline
An installed face added for characters the interface fonts lack (Microsoft
YaHei for Simplified Chinese on Windows, for example) now gets a FontTweak
y_offset_factor computed from its ascent, descent and line gap against the
first face of the Proportional family, so its glyphs share that face's
baseline instead of centering the difference in row height. The bundled
faces and Latin text do not move.

This follows designcraft#394 (baseline_tweak from line metrics) and
effectcraft#1395. vectorcraft-text gains line_metrics, and a test face with
a CJK system face's vertical proportions checks the baseline at 13, 24 and
40 pt in the interface families.
2026-11-08 12:21:20 +07:00
dotCooCoo fff2ecd76b Symbols: Reset Transformation tests cover a mixed selection and negative stored sizes
An instance in perspective selected with a turned instance stays as it
was while the other is reset and returned in one undo step. Stored
natural sizes that are negative on either side give the art its own
size, unmirrored, as a zero size does.
2026-11-08 12:21:14 +07:00
dotCooCoo 283d46c1cc Symbols: Reset Transformation's enabled check does not measure the symbol's art 2026-11-08 12:21:14 +07:00
dotCooCoo 1cd607f596 Symbols: Reset Transformation keeps the art's size when the stored size is not positive 2026-11-08 12:21:14 +07:00
dotCooCoo 98265b7435 Docs: list every Reset Transformation test in the progress line 2026-11-08 12:21:13 +07:00
dotCooCoo d687456978 Symbols: Reset Transformation returns an instance to its symbol's scale, rotation and shear
The Symbols panel menu's Reset Transformation and the Control bar's Reset
(shown while symbol instances are selected) give the selected instances
their symbol's own scale again, without rotation, shear or reflection, as
one undo step (symbol.instance.resetTransform). Each instance keeps the
center of its art's bounding box in place. Instances in perspective and
instances already at their symbol's scale are left as they are, no undo
step is added when nothing changes, and both controls are enabled only
while a selected instance would change.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-08 12:21:12 +07:00