mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-22 19:08:51 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64efe12e6f | ||
|
|
4bb30782ff | ||
|
|
8a46478e81 | ||
|
|
11ecf3e887 | ||
|
|
05da6b2125 | ||
|
|
9ee0c7f572 |
No files matched your search
@@ -316,6 +316,8 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-20 | minor | Readable disabled and selected text (#890): the dim text colour (disabled buttons, placeholders, hints) is at least 3:1 against panels, fields and selected rows in both themes (it was 2.1:1 in the light theme), a disabled button is grey with that text instead of fading to 40%, and selected text (renaming a page or layer, any text box) takes the theme's readable green on the highlight (1.4:1 in the dark theme before, now 4.2:1 or more); checked with the colour contrast helper (`theme::tests`). Numbers unchanged |
|
||||
| 2026-10-20 | minor | Layer data reads back (#879): `node.get` (the command, the control method and MCP `get_node`) and `document.inspect` report a layer's or page's `pluginData` as `{key: value}` when it has any, so a tool that keeps state in a file with `node.set {props: {pluginData}}` can read it on its next run (`plugin_data_tests`). Numbers unchanged |
|
||||
| 2026-10-20 | minor | Measuring while nudging (#894): with Alt held over another layer, the arrows nudge the selection (Shift: the big step) and the distances follow every nudge (Alt+arrows used to do nothing); while measuring, the selection's handles and size label hide and both layers keep their outlines (`measure_nudge_tests`). Numbers unchanged |
|
||||
| 2026-10-20 | minor | Release fonts (#885): the web release embeds craft-fonts again (Inter), so the browser version draws text and lists fonts; it had none since releases moved to the shared workflow. The craft-fonts pin goes back to the commit with the Latin manifest (the Georgian change had moved it to one without Inter, which would have failed the next release). Numbers unchanged. |
|
||||
| 2026-10-17 | minor | Community batch #753 (10 PRs): Selection colors for mixed selections (#403); smart-selection spacing fields and canvas handles (#402); a Present tab with device and scale controls (#537); adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys to frames only and warns about font styles a family lacks (#603); canvas images on whole device pixels (#359); Adwaita window controls on Wayland without server decorations (#592, not run on GNOME or KDE by the maintainers); the keyboard shortcuts panel closes on the home screen (#548); macOS paste modifiers in tests (#750); credits column tests (#347). `cargo xtask parity` re-run: 212 / 212 menu items (100%; the catalog gained View ▸ Full screen with #227 on 2026-10-14), 561 commands. Language table re-measured from the catalogs (2,272 entries): the drafts read 95–99% (strings added since 2026-10-12 show English), Indonesian has a draft (99%; it read none), and Russian, Swedish (its command labels from #599), Turkish and Czech are listed with their counts. Per-area parity rows for the day's landings in `docs/gaps.md`, `target-app-parity.md` and the area docs. Numbers unchanged |
|
||||
|
||||
@@ -165,6 +165,10 @@ pub fn node_json(doc: &Document, id: NodeId, depth: usize) -> Value {
|
||||
o.insert("operandOperation".into(), json!(op));
|
||||
}
|
||||
}
|
||||
// Layer data written with `node.set {props: {pluginData}}` (pages too).
|
||||
if !n.plugin_data.is_empty() {
|
||||
o.insert("pluginData".into(), json!(n.plugin_data));
|
||||
}
|
||||
match &n.kind {
|
||||
NodeKind::Text(t) => {
|
||||
o.insert("characters".into(), json!(t.characters));
|
||||
|
||||
@@ -1775,6 +1775,8 @@ mod pixel_preview_tests;
|
||||
#[cfg(test)]
|
||||
mod playground_tests;
|
||||
#[cfg(test)]
|
||||
mod plugin_data_tests;
|
||||
#[cfg(test)]
|
||||
mod plugin_directory_tests;
|
||||
#[cfg(test)]
|
||||
mod plugin_panels_tests;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Layer data (`pluginData`) reads back through `node.get` and `document.inspect` (#879): what
|
||||
//! `node.set` writes on a layer or a page comes back as `{key: value}`, a removed key goes, and a
|
||||
//! layer without any reports none.
|
||||
|
||||
use crate::Session;
|
||||
use crate::components::tests::{ex, idv};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn node_get_reports_the_layer_data_node_set_wrote() {
|
||||
let mut s = Session::new();
|
||||
let f = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 100, "height": 100})));
|
||||
let r = idv(&ex(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10})));
|
||||
let page = s.ds().unwrap().page;
|
||||
assert!(ex(&mut s, "node.get", json!({"id": f})).get("pluginData").is_none(), "no data, no field");
|
||||
ex(&mut s, "node.set", json!({"ids": [f], "props": {"pluginData": {"com.example.sync": "{\"v\":1}", "n": 2}}}));
|
||||
ex(&mut s, "node.set", json!({"ids": [page], "props": {"pluginData": {"com.example.sync": "page"}}}));
|
||||
let got = ex(&mut s, "node.get", json!({"id": f}))["pluginData"].clone();
|
||||
assert_eq!(got, json!({"com.example.sync": "{\"v\":1}", "n": "2"}), "values read back as the strings stored");
|
||||
assert_eq!(ex(&mut s, "node.get", json!({"id": page}))["pluginData"], json!({"com.example.sync": "page"}), "pages keep data too");
|
||||
assert!(ex(&mut s, "node.get", json!({"id": r})).get("pluginData").is_none(), "other layers report none");
|
||||
// The document tree reports it as well (the page and its children).
|
||||
let tree = crate::inspect::document(&s, 2, false);
|
||||
assert_eq!(tree["pages"][0]["pluginData"], json!({"com.example.sync": "page"}));
|
||||
// A null removes a key; undo brings it back.
|
||||
ex(&mut s, "node.set", json!({"ids": [f], "props": {"pluginData": {"n": null}}}));
|
||||
assert_eq!(ex(&mut s, "node.get", json!({"id": f}))["pluginData"], json!({"com.example.sync": "{\"v\":1}"}));
|
||||
ex(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(ex(&mut s, "node.get", json!({"id": f}))["pluginData"]["n"], "2");
|
||||
}
|
||||
@@ -14,6 +14,8 @@ pub struct Tokens {
|
||||
pub border_strong: Color32,
|
||||
pub text: Color32,
|
||||
pub text_secondary: Color32,
|
||||
/// Dim text: disabled controls, placeholders, hints. At least 3:1 against the panels, fields
|
||||
/// and selected rows, yet dimmer than `text_secondary`.
|
||||
pub text_tertiary: Color32,
|
||||
pub icon: Color32,
|
||||
pub icon_secondary: Color32,
|
||||
@@ -58,11 +60,15 @@ const CANVAS_ACCENT: &str = "figcraft-canvas-accent";
|
||||
/// Remember the page colour the canvas shows. Its overlays (outlines, handles, arrows) take
|
||||
/// whichever green contrasts more with it: the bright one on a dark page, the darker on a light one.
|
||||
pub fn set_canvas_page(ctx: &Context, page: Color32) {
|
||||
let c = |x: Color32| figcraft_color::Color::new(f32::from(x.r()) / 255.0, f32::from(x.g()) / 255.0, f32::from(x.b()) / 255.0, 1.0);
|
||||
let pick = if c(BRAND).contrast(c(page)) > c(ACCENT).contrast(c(page)) { BRAND } else { ACCENT };
|
||||
ctx.data_mut(|d| d.insert_temp(egui::Id::new(CANVAS_ACCENT), pick));
|
||||
}
|
||||
|
||||
/// `x` (opaque) as a document colour, for its contrast with another.
|
||||
fn c(x: Color32) -> figcraft_color::Color {
|
||||
figcraft_color::Color::new(f32::from(x.r()) / 255.0, f32::from(x.g()) / 255.0, f32::from(x.b()) / 255.0, 1.0)
|
||||
}
|
||||
|
||||
/// The green for overlays drawn on the page ([`set_canvas_page`]); [`ACCENT`] before a page shows.
|
||||
pub fn canvas_accent(ctx: &Context) -> Color32 {
|
||||
ctx.data(|d| d.get_temp(egui::Id::new(CANVAS_ACCENT))).unwrap_or(ACCENT)
|
||||
@@ -91,7 +97,7 @@ impl Tokens {
|
||||
text: Color32::from_rgb(0x1E, 0x1E, 0x1E),
|
||||
// 4.7:1 on fields and hovered rows (#F5F5F5), 4.6:1 on selected rows: WCAG AA.
|
||||
text_secondary: Color32::from_rgb(0x6E, 0x6E, 0x6E),
|
||||
text_tertiary: Color32::from_rgb(0xB3, 0xB3, 0xB3),
|
||||
text_tertiary: Color32::from_rgb(0x8A, 0x8A, 0x8A),
|
||||
icon: Color32::from_rgb(0x1E, 0x1E, 0x1E),
|
||||
icon_secondary: Color32::from_rgb(0x75, 0x75, 0x75),
|
||||
hover: Color32::from_rgb(0xF5, 0xF5, 0xF5),
|
||||
@@ -132,7 +138,7 @@ impl Tokens {
|
||||
border_strong: Color32::from_rgb(0x55, 0x55, 0x55),
|
||||
text: Color32::from_rgb(0xFF, 0xFF, 0xFF),
|
||||
text_secondary: Color32::from_rgb(0xB3, 0xB3, 0xB3),
|
||||
text_tertiary: Color32::from_rgb(0x75, 0x75, 0x75),
|
||||
text_tertiary: Color32::from_rgb(0x88, 0x88, 0x88),
|
||||
icon: Color32::from_rgb(0xF5, 0xF5, 0xF5),
|
||||
icon_secondary: Color32::from_rgb(0xB3, 0xB3, 0xB3),
|
||||
hover: Color32::from_rgb(0x38, 0x38, 0x38),
|
||||
@@ -216,7 +222,9 @@ fn visuals(t: Tokens) -> Visuals {
|
||||
v.window_corner_radius = egui::CornerRadius::same(13);
|
||||
v.menu_corner_radius = egui::CornerRadius::same(13);
|
||||
v.selection.bg_fill = t.accent.gamma_multiply(0.3);
|
||||
v.selection.stroke = Stroke::new(1.0, ACCENT);
|
||||
// Also the colour of selected text on that highlight (and a focused text box's outline): the
|
||||
// theme's readable green, not the darker one that sinks into the dark theme's highlight.
|
||||
v.selection.stroke = Stroke::new(1.0, t.accent_text);
|
||||
v.hyperlink_color = t.accent_text;
|
||||
for w in [&mut v.widgets.inactive, &mut v.widgets.hovered, &mut v.widgets.active, &mut v.widgets.open, &mut v.widgets.noninteractive] {
|
||||
w.corner_radius = egui::CornerRadius::same(5);
|
||||
@@ -462,6 +470,34 @@ mod tests {
|
||||
assert_eq!(under_white(COMPONENT), COMPONENT);
|
||||
}
|
||||
|
||||
/// `fill` (premultiplied, maybe translucent) painted over the opaque `bg`.
|
||||
fn over(fill: Color32, bg: Color32) -> Color32 {
|
||||
let k = 1.0 - f32::from(fill.a()) / 255.0;
|
||||
let mix = |f: u8, b: u8| (f32::from(f) + f32::from(b) * k).round().min(255.0) as u8;
|
||||
Color32::from_rgb(mix(fill.r(), bg.r()), mix(fill.g(), bg.g()), mix(fill.b(), bg.b()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_and_selected_text_stay_readable_in_both_themes() {
|
||||
for t in [Tokens::light(), Tokens::dark()] {
|
||||
let theme = if t.dark { "dark" } else { "light" };
|
||||
// Disabled text (a disabled button's label, placeholders) on panels, fields and
|
||||
// selected rows, still dimmer than secondary text.
|
||||
for bg in [t.panel, t.field, t.hover, t.selected_row] {
|
||||
let ratio = c(t.text_tertiary).contrast(c(bg));
|
||||
assert!(ratio >= 3.0, "{theme}: dim text {:?} on {bg:?} is {ratio:.2}:1", t.text_tertiary);
|
||||
assert!(ratio < c(t.text_secondary).contrast(c(bg)), "{theme}: dim text reads dimmer than secondary text");
|
||||
}
|
||||
// Selected text (renaming a page or layer, any text box) on the selection highlight.
|
||||
let v = visuals(t);
|
||||
for bg in [t.panel, t.field] {
|
||||
let highlight = over(v.selection.bg_fill, bg);
|
||||
let ratio = c(v.selection.stroke.color).contrast(c(highlight));
|
||||
assert!(ratio >= 3.0, "{theme}: selected text on {highlight:?} is {ratio:.2}:1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_font_draws_every_mac_shortcut_glyph() {
|
||||
figcraft_text::scan_system_fonts();
|
||||
|
||||
@@ -1030,26 +1030,26 @@ pub fn radio(ui: &mut Ui, on: bool, label: &str) -> bool {
|
||||
}
|
||||
|
||||
/// A text button: `primary` is filled with the accent colour, otherwise outlined. A disabled
|
||||
/// button is dimmed and never reports clicks.
|
||||
/// button is grey with dim text (still readable) and never reports clicks.
|
||||
pub fn button(ui: &mut Ui, label: &str, primary: bool, enabled: bool) -> Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let galley = ui.painter().layout_no_wrap(label.to_string(), regular(11.0), t.text);
|
||||
let sense = if enabled { Sense::click() } else { Sense::hover() };
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(galley.size().x + 24.0, FIELD_H), sense);
|
||||
a11y::button(&resp, label);
|
||||
let hot = enabled && resp.hovered();
|
||||
let (fill, text) = match (primary, hot) {
|
||||
(true, false) => (t.accent, t.accent_on),
|
||||
(true, true) => (t.accent_hover, t.accent_on),
|
||||
(false, false) => (Color32::TRANSPARENT, t.text),
|
||||
(false, true) => (t.hover, t.text),
|
||||
let (fill, text) = match (enabled, primary, resp.hovered()) {
|
||||
(false, true, _) => (t.field, t.text_tertiary),
|
||||
(false, false, _) => (Color32::TRANSPARENT, t.text_tertiary),
|
||||
(true, true, false) => (t.accent, t.accent_on),
|
||||
(true, true, true) => (t.accent_hover, t.accent_on),
|
||||
(true, false, false) => (Color32::TRANSPARENT, t.text),
|
||||
(true, false, true) => (t.hover, t.text),
|
||||
};
|
||||
let dim = |c: Color32| if enabled { c } else { c.gamma_multiply(0.4) };
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), dim(fill));
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), fill);
|
||||
if !primary {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, t.border_strong), StrokeKind::Inside);
|
||||
}
|
||||
ui.painter().text(rect.center(), Align2::CENTER_CENTER, label, regular(11.0), dim(text));
|
||||
ui.painter().text(rect.center(), Align2::CENTER_CENTER, label, regular(11.0), text);
|
||||
resp
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,9 @@ component/instance links, overrides, `slot` (a slot layer's property name), boun
|
||||
`devStatus` (handoff status), `devChanged` (changed since it was marked ready for dev),
|
||||
`annotations` (Dev Mode notes: `[{label, properties, category?}]`), `codeLinks` (a component's
|
||||
links to code: `[{language, importPath, name, template}]`), `devResources` (links for developers,
|
||||
its own and inherited: `[{name, url, index?, from?}]`). The auto layout props
|
||||
its own and inherited: `[{name, url, index?, from?}]`), `pluginData` (layer data written with
|
||||
`node.set {props: {pluginData: {key: value}}}`, pages included: `{key: value}`, values as strings;
|
||||
only when there is some). The auto layout props
|
||||
that `node.set` accepts are listed in [mcp.md](mcp.md#conventions-agents-must-know).
|
||||
|
||||
### `ui.inspect {}`
|
||||
|
||||
+1
-1
@@ -299,7 +299,7 @@ Or in a project's `.mcp.json`:
|
||||
| `create_node {type, x, y, width, height, parent, index, name, text, path, props}` | Create a frame, rectangle, ellipse, polygon, star, line, arrow, text, vector, section, component or slice; returns `{id, node}`. |
|
||||
| `set_properties {ids, props}` | `node.set` on layers (geometry, fills, strokes, effects, corner radius, auto layout, sizing, constraints, masks (`isMask`, `maskType: alpha\|vector\|luminance`), handoff status (`devStatus: none\|readyForDev\|completed`; marking again clears "Changed"), a section's `sectionContentsHidden`, text…); returns the updated layers. |
|
||||
| `inspect_document {depth, allPages}` | The document as JSON (layer tree, styles, selection, history). |
|
||||
| `get_node {id, depth}` | One layer as JSON. |
|
||||
| `get_node {id, depth}` | One layer as JSON; `pluginData` (`{key: value}`, values as strings) when the layer or page has layer data written with `set_properties {props: {pluginData}}`. |
|
||||
| `select {ids}` | Replace the selection (`[]` deselects). |
|
||||
| `export {ids, format, scale \| size, path?, bleed?, cropMarks?, bleedMarks?, cmyk?, contentsOnly?, includeBoundingBox?, svgOutlineText?, svgIdAttribute?, svgSimplifyStroke?}` | png, jpg, svg, webp, pdf, or `fig`: the whole file as `.fig`, experimental (see Exporting `.fig` files below); `size` is `2x`, `512w` or `512h`. The image covers what the layers paint (shadows towards their offset, strokes, unclipped children, a mask group's mask; hidden layers take no room; a slice is its area with everything under it); `contentsOnly: false` also draws every other layer showing in that area, `includeBoundingBox` exports the layers' own boxes; SVG: `svgOutlineText: false` keeps `<text>`, `svgIdAttribute` writes unique ids from layer names, `svgSimplifyStroke: false` writes every stroke as an outline. The same options go in export settings (`export.add` / `export.update`). PDF: frames one page each (sized in points), anything else together on one page; print options: `bleed` (points or a length like `"3mm"`, `"0.125in"`), crop and bleed marks, CMYK colour (see Exports below), also taken by `file.export`, `file.exportLayers`, `file.exportFramesPdf` and stored per export setting by `export.add` / `export.update`; `run_command` `file.exportFramesPdf {page?, path?}` writes every top-level frame of a page. Without `path`, images come back as MCP image content, SVG as text. Layers' own export settings: `run_command` with `export.add` / `export.update {index, size?, suffix?, format?}` / `export.remove` and `file.exportLayers {ids?, dir?, assets?}` (every layer once per setting, named `<layer><suffix>.<format>`, a slash in a layer name making a folder; `assets: true` exports the Inspect tab's assets instead: icons as SVG, images as PNG). |
|
||||
| `screenshot {ids?, scale?, maxSize?, window?}` | PNG of the page (with background) or of layers, scaled to fit `maxSize` (default 1600). `window: true` captures the app window in front when attached, a window id (see Windows below) that window. |
|
||||
|
||||
+3
-1
@@ -460,4 +460,6 @@ import what you need from `figcraft`.
|
||||
vectors, components or variables).
|
||||
- Panels: no images, sliders or free layout inside a panel.
|
||||
- Data in the document: plugins keep data on layers (pages included) with `node.set {props:
|
||||
{pluginData: {key: value}}}`; there is no separate per-document store.
|
||||
{pluginData: {key: value}}}` (a `null` value removes a key) and read it back with `node.get`, which
|
||||
reports `pluginData: {key: value}` (values as strings) when a layer has any; there is no separate
|
||||
per-document store.
|
||||
@@ -111,7 +111,7 @@ Breadth: does it exist. Ready: depth, correctness and fidelity for real projects
|
||||
| 15 | Dev Mode / inspect | 2 | 96% | 72% | Import lines for connected instances inside generated layer trees; an editor extension is left to the MCP server (inspect behaviour suite with colour formats, rem and icon and image assets, code connect, plugins in Dev Mode and dev resources links landed) ([dev-mode-parity](dev-mode-parity.md)) | 0–1 |
|
||||
| 16 | Comments, versions, find, misc | 3 | 87% | 63% | Comments are local only; comparing two versions; branches in live sessions; text runs and other lists besides fills, strokes and effects merge whole (branches with review and merge, their own version history and per-item list merges, and a comments and version history behaviour suite from the documentation landed) | 1–3 |
|
||||
| 17 | Multiplayer & sharing | 8 | 17% | 5% | Core landed (`crates/collab`: ids, change sets, server ordering, convergence tested); live sessions (preview): WebSocket transport, hosting from the desktop app, joining from the app or the browser, invite links that open the app, cursors, selections and follow, cursor chat and spotlight, per-person undo, offline resume; a self-hosted `figcraft-server` with persistence and share links to edit or view, which can be open to the public (owner tokens, limits per address, expiry, moderation). Left: accounts and teams, audio ([collaboration-parity](collaboration-parity.md)) | 42–84 |
|
||||
| 18 | Plugins & widgets | 4 | 60% | 35% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK), with log byte/count limits checked before allocation and host forwarding ([logging limits](plugins.md#logging-limits)), plugin panels called back per button or changed value and per-plugin settings (ABI v2), then widgets (API 3: plugin-drawn layers on the canvas with state kept in the document, redrawn on clicks; a Counter example), panels side by side and a colour picker for plugin colour fields, then a plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version). Left: a public catalogue, publisher accounts and signatures, ratings, background update checks, the directory in the browser; widgets take clicks only (no text input, property menus, hover or timers) and draw frames, text, shapes and images only; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 8–26 |
|
||||
| 18 | Plugins & widgets | 4 | 60% | 35% | Own sandboxed WASM plugins landed (#101: manifest, permissions, menu, Run last, Manage, SDK), with log byte/count limits checked before allocation and host forwarding ([logging limits](plugins.md#logging-limits)), plugin panels called back per button or changed value and per-plugin settings (ABI v2), then widgets (API 3: plugin-drawn layers on the canvas with state kept in the document, redrawn on clicks; a Counter example), panels side by side and a colour picker for plugin colour fields, then a plugin directory on `figcraft-server` (publish with the server key, browse, install after a permissions confirmation, hash-checked downloads, updates by version); layer data written with `node.set {props: {pluginData}}` reads back through `node.get` (#879). Left: a public catalogue, publisher accounts and signatures, ratings, background update checks, the directory in the browser; widgets take clicks only (no text input, property menus, hover or timers) and draw frames, text, shapes and images only; Figma's plugins are JavaScript and don't run (a JS engine would be an owner decision) ([plugins.md](plugins.md)) | 8–26 |
|
||||
| 19 | Team libraries & organization | 2 | 70% | 40% | Libraries kept on `figcraft-server` under their file and enabled by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names. Left: team-wide library lists, accounts and permissions, analytics over time, pushed update notices | 4–8 |
|
||||
| 20 | Figma Draw extras | 2 | 88% | 60% | Have: variable width, lasso, shape builder, noise/texture, text on a path, pattern fills, stretch and scatter brushes, pen pressure (Windows pens, browser styluses), multi-edit (several vector layers in edit mode at once). Missing: pressure from macOS/Linux tablets | 1–3 |
|
||||
| 21 | AI features | 3 | 0% | 0% | First draft, rename layers, image edit/remove background, visual search, Make: needs local models (owner decision) | 30–60 |
|
||||
@@ -638,6 +638,7 @@ Carried over from the ROADMAP's "Shipped so far" (2026-10-10). The test count th
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-20 | minor | Layer data reads back (#879): `node.get` (the command, the control method and MCP `get_node`) and `document.inspect` report a layer's or page's `pluginData` as `{key: value}` when it has any, so a tool that keeps state in a file with `node.set {props: {pluginData}}` can read it on its next run (`plugin_data_tests`). Numbers unchanged |
|
||||
| 2026-10-17 | minor | Menu breadth re-run with `cargo xtask parity`: 212 / 212 (100%; the catalog gained View ▸ Full screen with #227 on 2026-10-14), 561 commands. Rows for the 2026-10-17 landings: import/export (area 14) and the file formats dimension name the `.fig` import's new fidelity (instance identity, overrides by key, text style precedence, grid tracks, slots, connector labels, per-point joins, text on a path, image adjustments, list start numbers, vertical trim), budgets from the file's size, the import report, a `.fig` copy from Save as and paste from another design app; the localization evidence takes the re-measured catalog counts (2,272 entries, drafts at 95–99%, Indonesian and Czech drafts, Swedish and Turkish catalogs); the Automation line adds control requests answered while the window is minimised (#720); batch #753's Selection colors, smart-selection spacing, Present tab and auto layout on empty frames are in ui-parity, prototyping-parity and auto-layout-parity. Numbers unchanged: every `.fig` and paste change is checked on synthetic files or clipboard HTML made from our own export, and real files are what gap 1's blocker and the file-exchange discounts wait for; the UI changes (menus, the Actions palette, editor fixes) are not compared side by side, so UI/UX stays at 58%; the localization grade and hours stay (Indonesian's draft is offset by the drafts' new gaps) |
|
||||
| 2026-10-17 | minor | Automation roots (M8, #715, #481): `figcraft-cli mcp --automation-read-root DIR --automation-write-root DIR` (each repeatable, existing folders) and the desktop app with the same flags confine every file a command reads to the read and write roots and every file it writes to the write roots; paths are resolved (links followed, `..`, Windows `\\?\` and UNC paths, case) and compared by whole folder names, anything else is an ordinary error. The engine reaches the file system through one module (enforced by `crates/engine/clippy.toml`), and a sweep runs every command with paths outside the roots. `--attach` refuses the flags (the app confines itself); `ui.screenshot` and `ui.render` return PNG data without a `path`, so the attached MCP server writes no temporary files. Numbers unchanged. |
|
||||
| 2026-10-17 | minor | Number variables on text content (M3, #629): the Typography section's variable picker also lists number variables for a text layer's content, as the engine already accepted (the text shows the number, e.g. 42.5, and follows the variable). Other text fields keep their own types. Numbers unchanged. |
|
||||
|
||||
+2
-1
@@ -165,13 +165,14 @@ in the table. `crates/ui-egui/src/shortcut_keys_tests.rs` drives the keys a look
|
||||
| Keyboard navigation: F6 regions, Tab within, focus ring | done | |
|
||||
| Layers panel from the keyboard: arrows, F2 / Cmd+R rename (the row gets the keyboard back), Shift+F10 context menu; 10k-layer lists | done | revealing a layer, expanding, collapsing and arrow keys timed in `cargo xtask bench` |
|
||||
| Reduced motion | done | system setting or preference |
|
||||
| WCAG AA contrast of secondary and accent text | done | |
|
||||
| WCAG AA contrast of secondary and accent text | done | disabled and dim text at least 3:1 against panels, fields and selected rows, selected text at least 3:1 on its highlight, in both themes (`theme::tests`, #890) |
|
||||
| Tested with VoiceOver, NVDA, Narrator, Orca | missing | human QA |
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-20 | minor | Readable disabled and selected text (#890): the dim text colour (disabled buttons, placeholders, hints) is at least 3:1 against panels, fields and selected rows in both themes (it was 2.1:1 in the light theme), a disabled button is grey with that text instead of fading to 40%, and selected text (renaming a page or layer, any text box) takes the theme's readable green on the highlight (1.4:1 in the dark theme before, now 4.2:1 or more); checked with the colour contrast helper (`theme::tests`). Numbers unchanged |
|
||||
| 2026-10-20 | minor | Measuring while nudging (#894): with Alt held over another layer, the arrows nudge the selection (Shift: the big step) and the distances follow every nudge (Alt+arrows used to do nothing); while measuring, the selection's handles and size label hide and both layers keep their outlines (`measure_nudge_tests`). Numbers unchanged |
|
||||
| 2026-10-18 | minor | A rename (layers double-click, F2, File ▸ Rename, a page) selects the whole name in the frame its field first draws, so the first key replaces the name instead of being appended. Checked in the app on Windows over the control channel |
|
||||
| 2026-10-17 | minor | Escape or Enter while the pen draws ends the path and keeps its vector in edit mode with the pen, so the next path is drawn into the same layer (unconnected); another Escape leaves edit mode (#760). Entering edit mode on a layer that already is a vector no longer records an empty undo step. Numbers unchanged |
|
||||
|
||||
Reference in new issue
Block a user