- Every font-family fell back to serif, sans-serif typefaces included.
The fallback now comes from the font's classification (PANOSE, OS/2
class, name: serif, sans-serif, monospace, cursive, fantasy), passed
in by the engine; without one, from the family name.
- file.exportEpub takes embedFonts: the faces the stylesheet names are
packaged (OEBPS/fonts, @font-face, manifest items with font/ttf, otf,
woff or woff2). Faces that aren't available, whose licence (OS/2
fsType) forbids embedding, or that sit in a font collection are left
out and listed in the new warnings array. Fonts are packaged whole;
subsetting is a follow-up.
Fixes#775
- An image without alt text (Object Export Options) got its file name as
alt, which a screen reader then reads aloud. It is now decorative
(alt=""), and file.exportEpub lists those images in a new warnings
array so they can be given alt text.
- Images and media were packaged as images/<asset id>.<ext>. They are
now <name>-<asset id>.<ext>, the name from the original file name
(lowercase ASCII); the id keeps two files of the same name apart.
Fixes#774
- dcterms:modified was always 2026-01-01T00:00:00Z. It is now the
export time (from the command; on the web, and in the crate without
one, the document's last save or creation time).
- The default dc:identifier was urn:designcraft:<title>, shared by
books with the same title. It is now a urn:uuid derived from the
title and the document's creation time: stable from export to export,
different between documents. file.exportEpub takes identifier to set
it (an ISBN, say).
- file.exportEpub takes coverImage, a PNG or JPEG file used as the
cover (cover-image); a JPEG is packaged as cover.jpg/image/jpeg.
Fixes#771Fixes#773
nav.xhtml had one entry per placed story, labelled with its first
paragraph (body text too), and always the title "Contents".
- Headings (h1-h6) get an id in the EPUB, and the navigation lists the
h1-h3 headings of the stories in reading order, nested by level and
linked to them. A document without headings keeps one entry per
story (its first paragraph).
- The navigation's title follows the language (Inhoud, Inhalt, Table
des matières, Índice, Indice, 目次, 目录; Contents otherwise), and
file.exportEpub takes tocTitle to set it.
Fixes#769Fixes#770
Table cell paragraphs go through the same run and inline path as body paragraphs, so deleted and moved-from text and field instructions stay out of cells, and field results, tabs and breaks come in.
Word's Normal imports as a paragraph style named Normal whose definition is w:docDefaults with Normal's own properties on top; paragraphs without a style use Word's default paragraph style. A styleMap entry can still map Normal to [Basic Paragraph].
to_bytes returns a Result and every caller propagates it, so a document
that can't be written is an error: the existing file, the unsaved state
and the recovery checkpoint stay. Asset entries near or over 4 GiB are
written with zip64 sizes. On the web, Save marks the document saved only
after the browser download has started.
InDesign draws a radial gradient in the object's bounding box: on a
non-square frame it's an ellipse reaching the frame's sides, and a
Gradient Swatch tool drag keeps those proportions. LYH Studio episode 6
(3:16) gives the left page's background a radial gradient: magenta in
the middle, cyan out to every edge. DesignCraft drew a circle reaching
only the longer side, so a portrait page stayed purple at its sides.
- doc: `Fill::radial_ellipse(bounds)`: centre and radii (default: the
box's centre and half its width and height; with a tool vector, its
start, and its length measured with the box made square).
- render: the radial paint is that circle, stretched by the paint
transform; PDF: the shading's transform does the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InDesign's Swatches panel New Swatch takes what is current: a gradient
built in the Gradient panel becomes "New Gradient Swatch" (applied, and
listed), an unnamed colour a listed swatch, a selected named swatch a
"… copy". DesignCraft's button always added the same orange swatch, and
gradient swatches could not be created at all (LYH Studio episode 6,
2:12: "this is our Swatch … create a new swatch for it").
- engine: `swatch.new {swatch?, target?}` does the above for the
selection's fill (or stroke) or a named swatch; `swatch.create` also
takes `gradient: {kind?, stops: [{location, swatch | color, opacity?,
midpoint?}]}`, its stops taking colour swatches by name, as swatches
dragged onto the Gradient panel's stops.
- Swatches panel: New Swatch runs `swatch.new`, and with nothing
selected still adds a colour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening or saving a document moves it to the front of File › Open Recent
(no duplicates; Save a Copy and exports don't count). The submenu lists the
files by name with the full path as tooltip, then Clear Recent Files; the
start screen shows the last eight with their folders. A file that is gone is
dimmed and, when chosen, says so in an alert and stays listed (a disk that
isn't connected comes back). Preferences › File Handling sets how many are
kept (default 20, 0–100).
Engine: Prefs.recent_files / recent_files_max (saved in prefs.json),
file.openRecent {index}, file.recentFiles, file.clearRecent. UI:
app.openRecent; the macOS menu bar rebuilds the submenu when the list
changes.
InDesign's Text Frame Options has a section list (General, Baseline
Options, Column Rules, ...). General holds Columns (Fixed Number / Fixed
Width with Number, Width, Gutter), Balance Columns, Inset Spacing as a
2x2 grid with a link button, Vertical Justification and Ignore Text
Wrap; Baseline Options holds First Baseline Offset and Min. DesignCraft
showed one flat list without the columns type, width, balance, link,
ignore-wrap or baseline settings, although the model keeps them.
- dialogs: a section list (General, Baseline Options, Column Rules);
General as above (Width shows what Fixed Number columns get, editable
for Fixed Width; a side typed while linked sets all four insets);
Baseline Options; Column Rules as before. Enum captions translated.
- OK sends the new fields only when edited, as before.
- engine: `object.textFrameOptions` takes `columnWidth` (Fixed Width)
and `firstBaselineMin`, which compose already honours.
LYH Studio episode 14 sets two columns (9:45) and a 0.1875 in inset
(10:57) here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a placed graphic selected (Direct Selection, or the content
grabber), the graphic's own brown bounding box has eight handles:
dragging one resizes the image inside its frame (Shift keeps its
proportions, Alt from its centre) and leaves the frame as it is.
- tools: a content-handle hit test on the selected graphic's bounds
(after the frame's anchors under Direct Selection), a ContentResize
drag previewing `transform.set {content, bounds}`, the handle cursors,
and the content ghost while dragging. The frame's handles don't resize
the frame while its graphic is selected.
- engine: `transform.set` takes `bounds` for content: the graphic's new
bounding box on the spread (normalized, never empty).
- canvas: the brown content box gets its handles; the frame's handles
are hidden while the graphic is selected.
(cherry picked from commit 8e0e6c2f2e)
The engine's atomic module keeps its functions and signatures and delegates to craft-libs/craft-atomic (pinned to e9546e19), so every save and export caller is unchanged. write_atomic_with also turns a panicking writer into an error, so its temporary file is removed. Recovery checkpoints and package output keep their own writers, which replace links instead of following them.
On Unix, new files follow the umask and replaced files keep their permissions.
A frame holding items pasted into it isn't a group. Ungroup took it
apart like one, leaving its content loose and losing the clipping shape,
and the Properties panel called it "Group" with an Ungroup quick action.
As in InDesign, Ungroup now only takes real groups apart; other objects
stay as they are and stay selected (Ungroup used to clear them from the
selection). Such frames are labelled by their shape (<ellipse>, ...), so
Properties shows "Ellipse" with Convert Shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in InDesign, outlining a text frame gives a group with one compound
path per line (split further by text colour within a line), so Ungroup
leaves each line on its own: LYH Studio episode 14 ungroups the outlined
title at 4:56 to tighten the space between its lines before making one
compound path again. A single line in one colour stays a single compound
path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backspace at the start of the second of two consecutive tables merged their
anchor paragraphs: Story::delete kept one table id, the other table was
dropped and the paragraph held two anchors. Pasting a table next to an anchor
replaced the existing table the same way, and pasting several paragraphs
right before an anchor left its table unreferenced. Saving succeeded;
reopening failed with "paragraph must hold exactly one anchor".
fix_tables now splits a table paragraph that holds further anchors, so every
anchor gets its own paragraph again. delete and insert_story tell it which
table each surviving anchor belongs to (fix_tables_with), and insert_story
fixes tables up once after the whole paste instead of after every run.
Regression tests in doc (Backspace between tables, deleting the middle of
three, paste before/after an anchor, paragraphs pasted before an anchor) and
engine (text.delete and edit.paste through commands, then a file round-trip);
all failed before the fix.
Claude-Session: https://claude.ai/code/session_01La6SDgsmdZfgfDDmx3wiGU
With a placed image selected via Direct Selection (InDesign's content
selection), Copy now copies the graphic itself: a frame of the whole
image's bounds (uncropped) at its spread position, without the frame's
fill, stroke or effects. Paste Into then puts the full picture in place
inside the target shape, as InDesign does (LYH Studio episode 14, 5:44).
Cut and Delete with the graphic selected remove only the graphic; the
frame stays, empty and selected.
Nested items copy in spread coordinates (parent transform applied).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As in InDesign, the Eyedropper applies a colour sampled from a placed image
to the fill or the stroke, whichever is in front in the toolbox: with the
Stroke box in front a selected line takes it on its stroke (it has no
fill to show it). The toolbox's state reaches tools through
ViewInfo/ToolContext::stroke_in_front.
Depends on #572.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Side handles map to the frame's local axis through spread and frame
rotation. Graphic frames honour the axis. Locked frames and frames on
locked layers are left alone. Text frames fit by searching the frame
height with its own columns and vertical justification, so two columns
stay two columns and Bottom/Center/Justify don't inflate it. A mouse
double-click on a handle fits (main's canvas counts clicks on the press);
a UI test covers it with one undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In InDesign double-clicking a selection handle fits the frame to its
content: a corner fits both sides, a top or bottom handle the height, a
side handle the width. LYH Studio's tutorial uses it at 9:30 to fit the
INDESIGN text frame before moving it to the corner. In DesignCraft the
double-click switched to the Type tool.
- Selection tool: a double-click on a handle runs object.fit
{mode: fitFrameToContent, axis: both|height|width}.
- object.fit fitFrameToContent fits text frames to their composed text:
the bottom to the last line's descent, the right side to the longest
line (one column), plus the insets, keeping the top-left. Overset text
grows the frame first. Before, it fitted graphic frames only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The DestIn mask now covers the whole render target (and the PDF's
exported area), with the gradient kept in item space through the paint
transform, since past the gradient's ends the end opacity carries on: a
child's long drop shadow or thick stroke is faded, not cut off at a box.
feather_bounds counts only shown children and active states, walking
nested groups without recursion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A gradient feather on a group (Gradient Feather tool over grouped text, as
in LYH Studio's poster tutorial) did nothing on screen: groups never went
through the effects path. In PDF the group's mask was the group's own path
bounds, and a group has no path, so the mask covered a box of zero size and
hid the group.
- Item::feather_bounds: the box the feather spans, the objects' bounds for
a group.
- Render: a feathered group draws its objects in one layer, then fades it
with the same mask as other objects (fx::gradient_feather_mask).
- PDF: the group's mask spans its objects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>