M12: board templates and tables (#1363)

* M12: board templates and tables

Five built-in board templates (retrospective, brainstorm, weekly planner,
flow chart, kanban board) placed as ordinary board objects in one undo step
(board.template, board.templates) from a toolbar picker with pictures, and
board tables made of auto layout frames (board.table, board.tableEdit; up to
50 x 20), typed into with a double-click and edited by row and column from
the bar over the selection. Docs: boards-parity, gap 24, ROADMAP, mcp.

* M12: board templates picker on egui 0.37

Id::new and Ui::id are deprecated in egui 0.37: the templates picker's area uses Id::unique, its buttons' interaction ids come from ui.scope_id(), and its test reads the board bar's area by Id::unique.
This commit is contained in:
BFlat
2026-10-26 12:52:39 +07:00
committed by GitHub
parent bfd6e5f83a
commit d3782fdc43
40 changed files with 1413 additions and 31 deletions
+4 -2
View File
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7)
FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow,
native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through
@@ -93,7 +93,7 @@ native speakers' reviews of the nine language drafts need people.
| Team libraries | 70% | 40% | 4–8 | [components-parity](docs/components-parity.md) |
| Figma Draw extras | 88% | 60% | 1–3 | [gaps](docs/gaps.md) |
| AI features | 0% | 0% | 30–60 | [gaps](docs/gaps.md) |
| Boards (whiteboarding; not in the weighted numbers) | ~42% | ~25% | 24–50 | [boards-parity](docs/boards-parity.md) |
| Boards (whiteboarding; not in the weighted numbers) | ~47% | ~29% | 20–44 | [boards-parity](docs/boards-parity.md) |
## Languages
@@ -138,6 +138,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
- **2026-10-24** · Installed weights of an embedded font family (M7, #1246): release builds embed some families with only a few styles (Poppins Regular, Italic and Bold), and the first font scan dropped every installed face of such a family, so an installed Poppins SemiBold or Medium was never offered (a rescan did offer them). The first scan now keeps the embedded faces, which still win for the styles they cover, and offers the installed styles they lack beside them. Regression test in the font database tests. Numbers unchanged.
- **2026-10-24** · Dev Mode tail (M6, gap 16): Measurements kept on the canvas (gap 16): `devMode.addMeasurement {from?, to?, side?}` places one between two layers, along one side of a layer or (neither) to the layer around it, stored on the layer it starts from (saved in `.figcraft`, synced, undoable; old files open without them); Dev Mode draws them as labelled redlines that follow the layers, the Inspect tab lists them (those of the selected layer, or every one on the page), places and removes them; `devMode.removeMeasurement` and `devMode.measurements` remove and list them; a copy keeps those to layers copied with it. The layers panel is read-only in Dev Mode: rows select and expand, but don't rename, reorder, hide or lock. Dev Mode 98% / 78%.
- **2026-10-24** · Board facilitation (M12): a timer everyone on a board sees (presets or any minutes, pause, resume, a minute more, stop; synced as its end time, so nothing ticks over the network) and voting sessions (votes per person, on everything, on sticky notes or on the selection; a click votes, your badge takes it back; your votes show while it runs, everyone's with a ranked list once it ends; clear results), in a pill at the top right of the board. `board.timer`, `board.timerState`, `board.vote`, `board.voteResults`; ballots sync per voter. Checked in the app, with two app instances in one live session through a local `figcraft-server`, and through MCP; the web build compiles ([boards-parity](docs/boards-parity.md)).
- **2026-10-24** · Board templates and tables (M12): a templates picker in the board toolbar places a retrospective, a brainstorm, a weekly planner, a flow chart or a kanban board (`board.template`, all FigCraft's own) as ordinary board objects in one undo step; `board.table` makes a table of auto layout frames (up to 50 × 20) whose rows grow with their text, edited by row and column from the bar over the selection or `board.tableEdit`, typed into with a double-click. Boards ~35% → ~40% breadth, 30–60 h → 26–54 h; the weighted numbers are unchanged.
- **2026-10-24** · Boards (M12): File ▸ New board makes a board, a file kind of its own saved in `.figcraft` and carried to people joining a live session. Sticky notes (8 colours, text that shrinks as the note grows, the author's name, Shift+Cmd/Ctrl+Enter for the next one), shapes with text (7 outlines), connectors that re-route when what they join moves (computed in the engine's normalize pass, so local edits, undo and others' edits agree), marker, highlighter and eraser, stamps with the placer's name on hover; a board toolbar, a contextual bar over the selection and the Design panel hidden until asked for; prototyping and Dev Mode refuse on boards. Design files and boards have their own marks (a pen nib, a note) on tabs, the home screen and the Open files list. Checked in the app, with two app instances in one live session, and through MCP; the web build compiles ([boards-parity](docs/boards-parity.md)).
- **2026-10-22** · CJK line breaking completes JIS X 4051's kinsoku (M4): the line-start set now also has the halfwidth small kana ァ–ッ and small tsu, the halfwidth (semi-)voiced sound marks ゙゚, the spacing (semi-)voiced sound marks ゛゜, the kana iteration marks 〱–〵 and the halfwidth closing parenthesis ⦆; the line-end set has the halfwidth opening parenthesis ⦅. Text layers, auto layout and the canvas otherwise wrap Japanese as before: ideographs break anywhere and the behaviour suite pins every category plus that a manual newline may still start a line with a prohibited character. Numbers unchanged.
- **2026-10-15** · Auto-save (M1, #305): Preferences ▸ Save files automatically (`pref.autoSave`, off by default, desktop only) saves each open file that has a path 3 s after its last change, never during a gesture, a live text preview or while a version is shown; the file is encoded off the UI thread, a failed save says why and is tried again a minute later. Numbers unchanged.
@@ -346,6 +347,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
| 2026-10-24 | minor | Installed weights of an embedded font family (M7, #1246): release builds embed some families with only a few styles (Poppins Regular, Italic and Bold), and the first font scan dropped every installed face of such a family, so an installed Poppins SemiBold or Medium was never offered (a rescan did offer them). The first scan now keeps the embedded faces, which still win for the styles they cover, and offers the installed styles they lack beside them. Regression test in the font database tests. Numbers unchanged |
| 2026-10-24 | minor | Dev Mode tail (M6, gap 16): Measurements kept on the canvas (gap 16): `devMode.addMeasurement {from?, to?, side?}` places one between two layers, along one side of a layer or (neither) to the layer around it, stored on the layer it starts from (saved in `.figcraft`, synced, undoable; old files open without them); Dev Mode draws them as labelled redlines that follow the layers, the Inspect tab lists them (those of the selected layer, or every one on the page), places and removes them; `devMode.removeMeasurement` and `devMode.measurements` remove and list them; a copy keeps those to layers copied with it. The layers panel is read-only in Dev Mode: rows select and expand, but don't rename, reorder, hide or lock. Dev Mode 97% → 98% breadth, 76% → 78% ready, 2–4 h → 1–2 h; headline numbers unchanged at this rounding |
| 2026-10-24 | minor | Board facilitation (M12): a shared timer and voting sessions (`board.timer`, `board.vote` and their queries; timer and vote sync as document state, ballots per voter; neither is an undo step). Boards row ~35%/~20% → ~40%/~24%, 30–60 h → 26–54 h; headline numbers unchanged (boards are outside the weighted sum) |
| 2026-10-24 | minor | Board templates and tables (M12): five built-in templates (`board.template`, `board.templates`, a toolbar picker with pictures) and tables (`board.table`, `board.tableEdit`; rows and columns from the bar over the selection). Boards row ~35%/~20% → ~40%/~24%, 30–60 h → 26–54 h; headline numbers unchanged (boards are outside the weighted sum) |
| 2026-10-24 | minor | Boards (M12): a file kind of its own (`file.newBoard`, saved in `.figcraft`, carried to live-session joiners) with sticky notes, shapes with text, connectors that re-route when what they join moves (an engine recompute, the same for local edits, undo and others' edits), marker, highlighter and eraser, stamps with the placer's name, a board toolbar and a contextual bar; prototyping and Dev Mode refuse on boards; design files and boards have their own marks on tabs and file rows. Boards row added outside the weighted numbers; headline numbers unchanged at this rounding |
| 2026-10-21 | minor | Best-effort `.sketch` import (Sketch 48 and newer): open, import, drop, CLI and MCP; symbols become live components and instances, shared styles and Color Variables come across, Sketch 2025 frames, stacks (as auto layout) and tints too, text placed as Sketch places it; checked on three real files against Sketch's own renders ([file-format-parity](docs/file-format-parity.md)). Headline numbers unchanged |
| 2026-10-22 | minor | CJK line breaking: JIS X 4051 line-start/line-end sets completed (halfwidth small kana, halfwidth and spacing voiced sound marks, iteration marks 〱–〵, halfwidth parentheses) with behaviour cases; numbers unchanged |
+5 -1
View File
@@ -57,13 +57,16 @@ pub enum BoardItem {
},
/// A marker or highlighter stroke (what the eraser takes).
Ink,
/// A table: a frame of row frames holding cell frames with text (its column lines are thin
/// rectangles laid over it).
Table,
}
impl BoardItem {
/// Objects selected and moved as a whole: a click on their text or outline picks them, and
/// they never take other layers in.
pub fn is_atom(&self) -> bool {
matches!(self, BoardItem::Sticky { .. } | BoardItem::Shape { .. } | BoardItem::Label { .. } | BoardItem::Stamp { .. })
matches!(self, BoardItem::Sticky { .. } | BoardItem::Shape { .. } | BoardItem::Label { .. } | BoardItem::Stamp { .. } | BoardItem::Table)
}
/// Objects whose first text layer is their text (a double-click or Enter edits it).
@@ -109,6 +112,7 @@ impl BoardItem {
BoardItem::Label { .. } => "label",
BoardItem::Stamp { .. } => "stamp",
BoardItem::Ink => "ink",
BoardItem::Table => "table",
}
}
}
+5 -4
View File
@@ -34,7 +34,7 @@ const STICKY_GAP: f64 = 24.0;
/// A new shape's size.
const SHAPE: f64 = 160.0;
/// Text on board objects, and the author line under a sticky's note.
const INK: Color = Color::from_rgb8_const(0x1E, 0x1E, 0x1E);
pub(crate) const INK: Color = Color::from_rgb8_const(0x1E, 0x1E, 0x1E);
const AUTHOR_INK: Color = Color::from_rgb8_const(0x5C, 0x5A, 0x55);
/// Connectors' default colour and weight.
const LINE: Color = Color::from_rgb8_const(0x3A, 0x39, 0x36);
@@ -285,7 +285,7 @@ fn soft_shadow() -> Effect {
}
/// A text layer filling its frame's width (the frame's auto layout places it).
fn add_text(doc: &mut Document, parent: NodeId, text: &str, size: f64, color: Color, align: TextAlignH, width: f64) -> Result<NodeId> {
pub(crate) fn add_text(doc: &mut Document, parent: NodeId, text: &str, size: f64, color: Color, align: TextAlignH, width: f64) -> Result<NodeId> {
let first: String = text.lines().next().unwrap_or("").chars().take(64).collect();
let name = if first.trim().is_empty() { "Text" } else { first.as_str() };
let mut td = TextData {
@@ -774,7 +774,7 @@ fn set_color(s: &mut Session, p: &Value) -> Result<Value> {
n.strokes = vec![Paint::solid(line_shade(color))];
}
BoardItem::Connector(_) | BoardItem::Ink => doc.get_mut(id)?.strokes = vec![Paint::solid(line_shade(color))],
BoardItem::Label { .. } | BoardItem::Stamp { .. } => continue,
BoardItem::Label { .. } | BoardItem::Stamp { .. } | BoardItem::Table => continue,
}
changed.push(id);
}
@@ -1472,7 +1472,7 @@ impl Session {
/// label to type in). True when it was one.
pub(crate) fn board_double_click(&mut self, doc: &Document, leaf: NodeId, sel: &[NodeId]) -> Result<bool> {
let whole = figcraft_doc::geometry::select_target(doc, leaf, sel, false);
let editing = self.ds()?.text_edit.as_ref().is_some_and(|te| owner_of_text(doc, te.id) == Some(whole));
let editing = self.ds()?.text_edit.as_ref().is_some_and(|te| doc.is_ancestor(whole, te.id));
let Some(item) = doc.node(whole).and_then(|n| n.board.clone()).filter(|_| !editing) else { return Ok(false) };
self.tool.gesture = Gesture::None;
self.cancel_interaction()?;
@@ -1487,6 +1487,7 @@ impl Session {
};
edit_text(self, label)
}
BoardItem::Table => crate::board_table::edit_cell(self, doc, leaf),
b if b.has_text() => edit_text(self, whole),
_ => Ok(false),
}
+300
View File
@@ -0,0 +1,300 @@
//! Board tables: a frame of row frames holding cell frames with text, all ordinary auto layout
//! layers. The table hugs its rows' height, the rows share its width and the cells share their
//! row's width, so a cell that wraps more text makes its row taller. Each row but the last draws
//! a line under itself and the column lines are thin rectangles over the table that stretch with
//! it (they are rebuilt after every edit, like the first row's header tint).
use figcraft_color::{Color, Paint};
use figcraft_doc::{
AutoLayout, BoardItem, Constraints, Document, HConstraint, LayoutMode, Node, NodeId, NodeKind, Sizing, StrokeAlign, TextAlignH, VConstraint,
};
use figcraft_geom::Affine;
use serde_json::{Value, json};
use crate::board::{INK, add_text, require};
use crate::cmd::CommandSpec;
use crate::params::{get, id, num, num_or, opt_index, req_string};
use crate::{EngineError, Result, Session, bad, spec, textedit};
/// The most rows and columns a table has.
pub const MAX_ROWS: usize = 50;
pub const MAX_COLUMNS: usize = 20;
/// A new column's width, and a cell's height before its text is measured.
const CELL_W: f64 = 160.0;
const CELL_H: f64 = 40.0;
/// Room around a cell's text: [top, right, bottom, left].
const CELL_PAD: [f64; 4] = [10.0, 12.0, 10.0, 12.0];
const TEXT_SIZE: f64 = 14.0;
const LINE_W: f64 = 1.0;
const GRID: Color = Color::from_rgb8_const(0xCF, 0xCB, 0xC4);
const HEADER: Color = Color::from_rgb8_const(0xF1, 0xEF, 0xEB);
pub fn specs() -> Vec<CommandSpec> {
vec![
spec!(
"board.table",
"Table",
"{rows?, columns? (default 3 × 3, or the size of `cells`; at most 50 × 20), cells? ([[text…]…] row by row), x?, y? (its top-left on the page; default centred in the view)} → {id, rows, columns, cells: [[text id…]…]}",
true,
table_cmd
),
spec!(
"board.tableEdit",
"Edit table",
"{id? (a table or a layer in one; default the selection), action: addRow|addColumn|removeRow|removeColumn, index? (where the new one goes, or which one goes; default the last)} → {id, rows, columns, cells}",
true,
edit_cmd
),
]
}
/// A count from 1 to `max` (`default` when missing).
fn count(cmd: &str, p: &Value, key: &str, default: usize, max: usize) -> Result<usize> {
match num(p, key) {
None if get(p, key).is_none() => Ok(default),
Some(v) if v.fract() == 0.0 && (1.0..=max as f64).contains(&v) => Ok(v as usize),
_ => Err(bad(cmd, format!("`{key}` is a whole number from 1 to {max}"))),
}
}
/// A frame for the table at `index` in `parent`, made by `f`.
fn frame(doc: &mut Document, parent: NodeId, index: Option<usize>, name: &str, size: (f64, f64), f: impl FnOnce(&mut Node)) -> Result<NodeId> {
let id = doc.alloc_id();
let mut n = Node::new(id, NodeKind::Frame, name, size.0, size.1);
n.fills.clear();
n.clip = false;
f(&mut n);
doc.insert(n, parent, index)?;
Ok(id)
}
fn make_cell(doc: &mut Document, row: NodeId, index: Option<usize>, width: f64, text: &str) -> Result<NodeId> {
let cell = frame(doc, row, index, "Cell", (width, CELL_H), |n| {
n.layout = Some(AutoLayout { mode: LayoutMode::Vertical, gap: 0.0, padding: CELL_PAD, ..AutoLayout::default() });
n.child_layout.sizing_h = Sizing::Fill;
n.child_layout.sizing_v = Sizing::Hug;
n.child_layout.min_h = Some(CELL_H);
})?;
add_text(doc, cell, text, TEXT_SIZE, INK, TextAlignH::Left, (width - CELL_PAD[1] - CELL_PAD[3]).max(1.0))?;
Ok(cell)
}
fn make_row(doc: &mut Document, table: NodeId, index: Option<usize>, width: f64, texts: &[&str]) -> Result<NodeId> {
let row = frame(doc, table, index, "Row", (width, CELL_H), |n| {
n.layout = Some(AutoLayout { mode: LayoutMode::Horizontal, gap: 0.0, padding: [0.0; 4], ..AutoLayout::default() });
n.child_layout.sizing_h = Sizing::Fill;
n.child_layout.sizing_v = Sizing::Hug;
n.stroke.weight = LINE_W;
n.stroke.align = StrokeAlign::Inside;
n.stroke.sides = Some([0.0, 0.0, LINE_W, 0.0]);
})?;
let cw = width / texts.len().max(1) as f64;
for t in texts {
make_cell(doc, row, None, cw, t)?;
}
Ok(row)
}
/// A table of `texts` (rows of cell texts, all as long) at `at` on `parent`.
pub(crate) fn make_table(doc: &mut Document, parent: NodeId, at: (f64, f64), texts: &[Vec<&str>]) -> Result<NodeId> {
let columns = texts.first().map_or(1, Vec::len).max(1);
let w = CELL_W * columns as f64;
let table = frame(doc, parent, None, "Table", (w, CELL_H * texts.len() as f64), |n| {
n.transform = Affine::translate(at);
n.fills = vec![Paint::solid(Color::WHITE)];
n.strokes = vec![Paint::solid(GRID)];
n.stroke.weight = LINE_W;
n.stroke.align = StrokeAlign::Inside;
n.corners.radii = [4.0; 4];
n.clip = true;
// The outline draws over the rows' edges rather than taking room from them.
n.layout = Some(AutoLayout { mode: LayoutMode::Vertical, gap: 0.0, padding: [0.0; 4], strokes_included: false, ..AutoLayout::default() });
n.child_layout.sizing_v = Sizing::Hug;
n.board = Some(BoardItem::Table);
})?;
for row in texts {
make_row(doc, table, None, w, row)?;
}
restyle(doc, table)?;
Ok(table)
}
/// Table `table`'s rows (its laid-out children) and column lines (the others).
fn parts(doc: &Document, table: NodeId) -> (Vec<NodeId>, Vec<NodeId>) {
let kids = doc.node(table).map(|n| n.children.clone()).unwrap_or_default();
kids.into_iter().partition(|c| doc.node(*c).is_some_and(|n| !n.child_layout.absolute))
}
fn cells(doc: &Document, row: NodeId) -> Vec<NodeId> {
doc.node(row).map(|n| n.children.clone()).unwrap_or_default()
}
/// The first row tinted, a line under every row but the last, and the column lines over it all.
fn restyle(doc: &mut Document, table: NodeId) -> Result<()> {
let (rows, lines) = parts(doc, table);
for l in lines {
doc.remove(l)?;
}
let last = rows.len().saturating_sub(1);
for (i, r) in rows.iter().enumerate() {
let n = doc.get_mut(*r)?;
n.fills = if i == 0 { vec![Paint::solid(HEADER)] } else { Vec::new() };
n.strokes = if i < last { vec![Paint::solid(GRID)] } else { Vec::new() };
}
let columns = rows.first().map_or(0, |r| cells(doc, *r).len());
let (w, h) = doc.get(table).map(|t| (t.width, t.height))?;
for i in 1..columns {
let x = w * i as f64 / columns as f64 - LINE_W / 2.0;
let id = doc.alloc_id();
let mut n = Node::new(id, NodeKind::Rectangle, "Column line", LINE_W, h);
n.fills = vec![Paint::solid(GRID)];
n.transform = Affine::translate((x, 0.0));
n.child_layout.absolute = true;
n.constraints = Constraints { horizontal: HConstraint::Scale, vertical: VConstraint::TopBottom };
n.locked = true;
doc.insert(n, table, None)?;
}
Ok(())
}
/// Each cell's text layer, row by row (null for a cell without one).
fn cell_texts(doc: &Document, table: NodeId) -> Value {
let text = |cell: NodeId| {
doc.node(cell).and_then(|n| n.children.iter().copied().find(|c| doc.node(*c).is_some_and(|k| matches!(k.kind, NodeKind::Text(_)))))
};
let (rows, _) = parts(doc, table);
Value::Array(rows.iter().map(|r| Value::Array(cells(doc, *r).into_iter().map(|c| json!(text(c))).collect())).collect())
}
fn answer(doc: &Document, table: NodeId) -> Value {
let (rows, _) = parts(doc, table);
let columns = rows.first().map_or(0, |r| cells(doc, *r).len());
json!({"id": table, "rows": rows.len(), "columns": columns, "cells": cell_texts(doc, table)})
}
fn table_cmd(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "board.table";
require(s, cmd)?;
let given: Vec<Vec<String>> = match get(p, "cells") {
None | Some(Value::Null) => Vec::new(),
Some(Value::Array(rows)) => rows
.iter()
.map(|r| match r {
Value::Array(c) => c.iter().map(|t| t.as_str().map(str::to_string).ok_or_else(|| bad(cmd, "`cells` holds text"))).collect(),
_ => Err(bad(cmd, "`cells` is a list of rows, each a list of texts")),
})
.collect::<Result<_>>()?,
Some(_) => return Err(bad(cmd, "`cells` is a list of rows, each a list of texts")),
};
let widest = given.iter().map(Vec::len).max().unwrap_or(0);
let rows = count(cmd, p, "rows", if given.is_empty() { 3 } else { given.len() }, MAX_ROWS)?;
let columns = count(cmd, p, "columns", if widest == 0 { 3 } else { widest }, MAX_COLUMNS)?;
if given.len() > rows || widest > columns {
return Err(bad(cmd, format!("`cells` doesn't fit in {rows} × {columns}")));
}
let texts: Vec<Vec<&str>> =
(0..rows).map(|r| (0..columns).map(|c| given.get(r).and_then(|row| row.get(c)).map_or("", String::as_str)).collect()).collect();
let (w, h) = (CELL_W * columns as f64, CELL_H * rows as f64);
let view = &s.ds()?.view;
let at = (num_or(p, "x", view.cx - w / 2.0), num_or(p, "y", view.cy - h / 2.0));
if !(at.0.is_finite() && at.1.is_finite()) {
return Err(bad(cmd, "`x` and `y` are finite numbers"));
}
let page = s.page()?;
let doc = s.doc_mut()?;
let table = make_table(doc, page, at, &texts)?;
let out = answer(doc, table);
s.set_selection(vec![table]);
Ok(out)
}
/// The table `id` is or is in.
fn table_of(doc: &Document, id: NodeId) -> Option<NodeId> {
std::iter::once(id).chain(doc.ancestors(id)).find(|a| doc.node(*a).is_some_and(|n| n.board == Some(BoardItem::Table)))
}
fn edit_cmd(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "board.tableEdit";
require(s, cmd)?;
let target = id(p, "id").or_else(|| s.selection().first().copied()).ok_or_else(|| EngineError::Disabled(cmd.into(), "select a table".into()))?;
let doc = s.doc()?;
doc.get(target)?;
let table = table_of(doc, target).ok_or_else(|| bad(cmd, "that isn't a table"))?;
let action = req_string(cmd, p, "action")?;
let index = opt_index(cmd, p, "index")?;
let (rows, _) = parts(doc, table);
let columns = rows.first().map_or(0, |r| cells(doc, *r).len()).max(1);
let width = doc.get(table)?.width;
let doc = s.doc_mut()?;
// `n` things: `index` (default `last`) must be at most `end`.
let at = |end: usize, last: usize| match index {
Some(i) if i > end => Err(bad(cmd, format!("`index` is from 0 to {end}"))),
Some(i) => Ok(i),
None => Ok(last),
};
match action.as_str() {
"addRow" => {
if rows.len() >= MAX_ROWS {
return Err(bad(cmd, format!("a table has at most {MAX_ROWS} rows")));
}
let i = at(rows.len(), rows.len())?;
// Rows come before the column lines among the table's layers.
let child = rows.get(i).or(rows.last()).and_then(|r| doc.node(table).and_then(|t| t.children.iter().position(|c| c == r)));
let child = child.map(|c| if i >= rows.len() { c + 1 } else { c });
make_row(doc, table, child, width, &vec![""; columns])?;
}
"removeRow" => {
if rows.len() <= 1 {
return Err(bad(cmd, "a table keeps at least one row"));
}
let i = at(rows.len() - 1, rows.len() - 1)?;
if let Some(r) = rows.get(i) {
doc.remove(*r)?;
}
}
"addColumn" => {
if columns >= MAX_COLUMNS {
return Err(bad(cmd, format!("a table has at most {MAX_COLUMNS} columns")));
}
let i = at(columns, columns)?;
let cw = width / columns as f64;
doc.get_mut(table)?.width = width + cw;
for r in &rows {
make_cell(doc, *r, Some(i), cw, "")?;
}
}
"removeColumn" => {
if columns <= 1 {
return Err(bad(cmd, "a table keeps at least one column"));
}
let i = at(columns - 1, columns - 1)?;
doc.get_mut(table)?.width = width - width / columns as f64;
for r in &rows {
if let Some(c) = cells(doc, *r).get(i) {
doc.remove(*c)?;
}
}
}
other => return Err(bad(cmd, format!("`action` is addRow, addColumn, removeRow or removeColumn, not `{other}`"))),
}
restyle(doc, table)?;
let out = answer(doc, table);
s.set_selection(vec![table]);
Ok(out)
}
/// A double-click on a table: type in the cell under the pointer (`leaf`). False when it hit no
/// cell (a column line, the table's edge).
pub(crate) fn edit_cell(s: &mut Session, doc: &Document, leaf: NodeId) -> Result<bool> {
let in_table = |c: &NodeId| {
let row = doc.node(*c).and_then(|n| n.parent);
row.and_then(|r| doc.node(r)).and_then(|r| r.parent).and_then(|t| doc.node(t)).is_some_and(|t| t.board == Some(BoardItem::Table))
};
let Some(cell) = std::iter::once(leaf).chain(doc.ancestors(leaf)).find(in_table) else { return Ok(false) };
let Some(text) = cells(doc, cell).into_iter().find(|c| doc.node(*c).is_some_and(|n| matches!(n.kind, NodeKind::Text(_)))) else {
return Ok(false);
};
textedit::begin(s, text, None, false)?;
textedit::key(s, "SelectAll", false, false, false)?;
Ok(true)
}
+257
View File
@@ -0,0 +1,257 @@
//! Boards: templates (each placed as board objects in one undo step) and tables (made from auto
//! layout frames, edited by rows and columns, typed into with the text tools, synced live).
use figcraft_doc::{BoardItem, NodeId, NodeKind};
use serde_json::{Value, json};
use crate::board_tests::{board, id, node};
use crate::collab_live_tests::Room;
use crate::tools::{PointerEvent, PointerKind};
use crate::{EngineError, Session};
fn count(s: &Session, f: impl Fn(&BoardItem) -> bool) -> usize {
s.doc().unwrap().nodes().filter(|n| n.board.as_ref().is_some_and(&f)).count()
}
fn ids(v: &Value, key: &str) -> Vec<NodeId> {
v[key].as_array().unwrap().iter().map(|i| NodeId(i.as_u64().unwrap())).collect()
}
/// Rows of cell texts' characters.
fn texts(s: &Session, table: &Value) -> Vec<Vec<String>> {
let chars = |v: &Value| match &node(s, NodeId(v.as_u64().unwrap())).kind {
NodeKind::Text(td) => td.characters.clone(),
_ => panic!("not text"),
};
table["cells"].as_array().unwrap().iter().map(|r| r.as_array().unwrap().iter().map(chars).collect()).collect()
}
/// A table's column lines (its laid-out-over children): their x and height.
fn lines(s: &Session, table: NodeId) -> Vec<(f64, f64)> {
let doc = s.doc().unwrap();
let t = doc.node(table).unwrap();
t.children.iter().map(|c| doc.node(*c).unwrap()).filter(|n| n.child_layout.absolute).map(|n| (n.transform.translation().x, n.height)).collect()
}
#[test]
fn every_template_places_board_objects_in_one_undo_step() {
let mut s = board();
let list = s.execute("board.templates", &json!({})).unwrap();
let names: Vec<&str> = list.as_array().unwrap().iter().map(|t| t["id"].as_str().unwrap()).collect();
assert_eq!(names, ["retrospective", "brainstorm", "weeklyPlanner", "flowChart", "kanban"]);
for (i, name) in names.iter().enumerate() {
let before = s.doc().unwrap().nodes().count();
let r = s.execute("board.template", &json!({"name": name, "x": i as f64 * 2000.0, "y": 0})).unwrap();
let made = ids(&r, "ids");
assert!(!made.is_empty(), "{name}");
assert_eq!(s.selection(), made, "{name}: what it placed is selected");
let doc = s.doc().unwrap();
// It sits where it was asked to, about its listed size.
let b = doc.bounds_of(&made).unwrap();
assert!((b.x0 - i as f64 * 2000.0).abs() < 1.0 && b.y0.abs() < 1.0, "{name} at {b:?}");
let size = &list[i];
assert!((b.width() - size["width"].as_f64().unwrap()).abs() < 2.0, "{name}: width {}", b.width());
assert!(b.height() <= size["height"].as_f64().unwrap() + 2.0, "{name}: height {}", b.height());
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().nodes().count(), before, "{name}: one undo step");
s.execute("edit.redo", &json!({})).unwrap();
}
// By name too, centred in the view without a place.
let view = s.ds().unwrap().view.clone();
let r = s.execute("board.template", &json!({"name": "Kanban board"})).unwrap();
let b = s.doc().unwrap().bounds_of(&ids(&r, "ids")).unwrap();
assert!((b.center().x - view.cx).abs() < 2.0, "{b:?} centred on {}", view.cx);
assert!(s.execute("board.template", &json!({"name": "party"})).is_err());
assert!(s.execute("board.template", &json!({})).is_err());
let mut d = Session::new();
d.execute("file.new", &json!({})).unwrap();
assert!(matches!(d.execute("board.template", &json!({"name": "kanban"})), Err(EngineError::Disabled(..))), "boards only");
assert!(matches!(d.execute("board.table", &json!({})), Err(EngineError::Disabled(..))), "boards only");
}
#[test]
fn templates_are_made_of_ordinary_board_objects() {
let mut s = board();
s.execute("board.template", &json!({"name": "retrospective", "x": 0, "y": 0})).unwrap();
let doc = s.doc().unwrap();
let sections: Vec<_> = doc.nodes().filter(|n| matches!(n.kind, NodeKind::Section { .. })).collect();
assert_eq!(sections.iter().map(|n| n.name.as_str()).collect::<Vec<_>>(), ["Went well", "To improve", "Next steps"]);
for sec in &sections {
let stickies = sec.children.iter().filter(|c| matches!(doc.node(**c).unwrap().board, Some(BoardItem::Sticky { .. }))).count();
assert_eq!(stickies, 2, "{}", sec.name);
}
// No author on a template's stickies.
assert!(doc.nodes().filter(|n| matches!(n.board, Some(BoardItem::Sticky { .. }))).all(|n| n.board.as_ref().unwrap().author().is_empty()));
// The flow chart's connectors join its shapes, labelled at the decision.
s.execute("board.template", &json!({"name": "flowChart", "x": 0, "y": 1000})).unwrap();
let doc = s.doc().unwrap();
let joined: Vec<_> = doc
.nodes()
.filter_map(|n| match &n.board {
Some(BoardItem::Connector(c)) => Some((c.from.node().unwrap(), c.to.node().unwrap())),
_ => None,
})
.collect();
assert_eq!(joined.len(), 6);
assert!(joined.iter().all(|(a, b)| matches!(doc.node(*a).unwrap().board, Some(BoardItem::Shape { .. }))
&& matches!(doc.node(*b).unwrap().board, Some(BoardItem::Shape { .. }))));
assert_eq!(count(&s, |b| matches!(b, BoardItem::Label { .. })), 2);
// The brainstorm's topic is joined to each of its four clusters.
s.execute("board.template", &json!({"name": "brainstorm", "x": 3000, "y": 0})).unwrap();
assert_eq!(count(&s, |b| matches!(b, BoardItem::Connector(_))), 10);
}
#[test]
fn a_table_is_auto_layout_frames_with_a_header_and_column_lines() {
let mut s = board();
let r = s.execute("board.table", &json!({"x": 0, "y": 0, "cells": [["Name", "Role"], ["Ada", "Lead"]], "rows": 3})).unwrap();
let t = id(&r, "id");
assert_eq!((r["rows"].as_u64(), r["columns"].as_u64()), (Some(3), Some(2)));
assert_eq!(texts(&s, &r), [["Name", "Role"], ["Ada", "Lead"], ["", ""]]);
assert_eq!(node(&s, t).board, Some(BoardItem::Table));
assert_eq!(s.selection(), [t]);
let table = node(&s, t);
assert_eq!(table.width, 320.0);
let rows: Vec<_> = table.children.iter().map(|c| node(&s, *c)).filter(|n| !n.child_layout.absolute).collect();
assert_eq!(rows.len(), 3);
assert!(rows[0].fills.len() == 1 && rows[1].fills.is_empty(), "the header row is tinted");
assert!(rows[1].strokes.len() == 1 && rows[2].strokes.is_empty(), "a line under every row but the last");
let cell = node(&s, rows[1].children[1]);
assert_eq!((cell.width, cell.transform.translation().x), (160.0, 160.0), "cells share their row");
assert_eq!(table.height, rows.iter().map(|r| r.height).sum::<f64>(), "the table hugs its rows");
assert_eq!(lines(&s, t), [(159.5, table.height)], "the column line runs the table's height");
// Caps and refusals.
for bad in
[json!({"rows": 51}), json!({"columns": 21}), json!({"rows": 0}), json!({"rows": 2.5}), json!({"cells": [["a", "b", "c"]], "columns": 2})]
{
assert!(s.execute("board.table", &bad).is_err(), "{bad}");
}
assert!(s.execute("board.table", &json!({"rows": 50, "columns": 20})).is_ok(), "the largest table");
// It saves and reopens.
let doc = s.doc().unwrap();
let (back, _) = figcraft_format::from_json(&figcraft_format::to_json(doc).unwrap().to_string()).unwrap();
assert_eq!(back.node(t).unwrap().board, Some(BoardItem::Table));
}
#[test]
fn table_edits_add_and_remove_rows_and_columns_keeping_text() {
let mut s = board();
let r = s.execute("board.table", &json!({"x": 0, "y": 0, "cells": [["a", "b"], ["c", "d"]]})).unwrap();
let t = id(&r, "id");
let edit = |s: &mut Session, p: Value| s.execute("board.tableEdit", &p);
let r = edit(&mut s, json!({"id": t, "action": "addRow", "index": 1})).unwrap();
assert_eq!(texts(&s, &r), [["a", "b"], ["", ""], ["c", "d"]]);
let r = edit(&mut s, json!({"action": "addColumn", "index": 0})).unwrap();
assert_eq!(texts(&s, &r), [["", "a", "b"], ["", "", ""], ["", "c", "d"]]);
assert_eq!(node(&s, t).width, 480.0, "a new column as wide as the others");
assert_eq!(lines(&s, t).len(), 2);
let r = edit(&mut s, json!({"action": "removeRow", "index": 0})).unwrap();
assert_eq!(texts(&s, &r), [["", "", ""], ["", "c", "d"]]);
let first = node(&s, node(&s, t).children[0]);
assert_eq!(first.fills.len(), 1, "the new first row is the header");
let r = edit(&mut s, json!({"action": "removeColumn"})).unwrap();
assert_eq!(texts(&s, &r), [["", ""], ["", "c"]]);
assert_eq!(node(&s, t).width, 320.0);
// From a cell inside it too.
let cell_text = NodeId(r["cells"][1][1].as_u64().unwrap());
let r = edit(&mut s, json!({"id": cell_text, "action": "addRow"})).unwrap();
assert_eq!(r["rows"], 3);
// Undo, one step each.
s.execute("edit.undo", &json!({})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(node(&s, t).width, 480.0);
// Refusals: the wrong action or index, the last row or column, beyond the caps, not a table.
for p in [
json!({"id": t, "action": "explode"}),
json!({"id": t, "action": "addRow", "index": 9}),
json!({"id": t, "action": "removeColumn", "index": 3}),
json!({"id": t, "action": "addRow", "index": -1}),
json!({"id": t}),
] {
assert!(edit(&mut s, p.clone()).is_err(), "{p}");
}
let one = id(&s.execute("board.table", &json!({"rows": 1, "columns": 1})).unwrap(), "id");
assert!(edit(&mut s, json!({"id": one, "action": "removeRow"})).is_err());
assert!(edit(&mut s, json!({"id": one, "action": "removeColumn"})).is_err());
let full = id(&s.execute("board.table", &json!({"rows": 50, "columns": 20})).unwrap(), "id");
assert!(edit(&mut s, json!({"id": full, "action": "addRow"})).is_err());
assert!(edit(&mut s, json!({"id": full, "action": "addColumn"})).is_err());
let sticky = id(&s.execute("board.sticky", &json!({"x": 0, "y": 900})).unwrap(), "id");
assert!(edit(&mut s, json!({"id": sticky, "action": "addRow"})).is_err());
}
#[test]
fn a_double_click_types_in_the_cell_and_its_row_grows_with_the_text() {
let mut s = board();
let r = s.execute("board.table", &json!({"x": 0, "y": 0})).unwrap();
let t = id(&r, "id");
let h0 = node(&s, t).height;
s.execute("select.none", &json!({})).unwrap();
// The middle cell (columns 160 wide, rows about 40 tall).
let row_h = h0 / 3.0;
s.pointer(&PointerEvent::at(PointerKind::DoubleClick, 240.0, row_h * 1.5, 1.0)).unwrap();
let te = s.ds().unwrap().text_edit.clone().expect("typing in the cell");
assert_eq!(te.id.0, r["cells"][1][1].as_u64().unwrap());
crate::textedit::insert(
&mut s,
"A longer note
over
several lines",
)
.unwrap();
crate::textedit::end(&mut s).unwrap();
let h1 = node(&s, t).height;
assert!(h1 > h0 + 10.0, "the row grew: {h0} → {h1}");
assert!(lines(&s, t).iter().all(|(_, h)| (*h - h1).abs() < 1e-6), "column lines follow: {:?} vs {h1}", lines(&s, t));
// A plain click picks the table whole.
s.execute("select.none", &json!({})).unwrap();
s.pointer(&PointerEvent::at(PointerKind::Down, 240.0, 10.0, 1.0)).unwrap();
s.pointer(&PointerEvent::at(PointerKind::Up, 240.0, 10.0, 1.0)).unwrap();
assert_eq!(s.selection(), [t]);
}
#[test]
fn tables_and_templates_sync_in_live_sessions() {
let s = board();
let mut room = Room::new(s.doc().unwrap().clone(), 2, 11);
let t = id(&room.s(0).execute("board.table", &json!({"x": 0, "y": 0})).unwrap(), "id");
room.s(1).execute("board.template", &json!({"name": "flowChart", "x": 0, "y": 600})).unwrap();
room.settle();
room.check();
room.s(1).execute("board.tableEdit", &json!({"id": t, "action": "addColumn"})).unwrap();
room.settle();
room.check();
let doc = room.server.document();
assert_eq!(doc.node(t).unwrap().width, 640.0);
assert_eq!(doc.nodes().filter(|n| matches!(n.board, Some(BoardItem::Connector(_)))).count(), 6);
}
#[test]
fn template_and_table_commands_survive_junk() {
let junk = [
json!({}),
json!(null),
json!([1]),
json!({"name": 7, "x": "NaN", "y": 1e308, "rows": "x", "columns": -1, "cells": 5}),
json!({"name": "kanban", "x": -1e308, "y": 1e308, "rows": 1e308, "columns": 0.5, "cells": [[1, 2]]}),
json!({"name": "\u{0}", "cells": [["a"], "b"], "rows": 50, "columns": 20}),
json!({"id": "x", "action": 5, "index": 1e308}),
json!({"id": -1, "action": "addRow", "index": -1}),
json!({"id": 99_999, "action": "removeColumn", "index": "NaN"}),
json!({"id": 2, "action": "addColumn", "index": 0}),
];
for cmd in ["board.template", "board.templates", "board.table", "board.tableEdit"] {
for j in &junk {
let mut s = board();
let t = s.execute("board.table", &json!({"x": 0, "y": 0})).unwrap()["id"].clone();
let _ = s.execute("board.template", &json!({"name": "brainstorm"}));
let mut j = j.clone();
if j.get("action").is_some_and(|a| a == "addColumn") {
j["id"] = t;
}
if let Err(EngineError::Internal { cmd, msg }) = s.execute(cmd, &j) {
panic!("{cmd} panicked on {j}: {msg}");
}
}
}
}
+261
View File
@@ -0,0 +1,261 @@
//! Board templates: ready-made layouts (a retrospective, a brainstorm, a weekly planner, a flow
//! chart, a kanban board) placed on a board as ordinary board objects in one undo step. They are
//! built in code by running the board commands, so everything in them edits like anything else on
//! the board; their wording is FigCraft's own.
use std::sync::Arc;
use figcraft_doc::{Document, NodeId};
use serde_json::{Value, json};
use crate::board::require;
use crate::cmd::CommandSpec;
use crate::params::{id_of, num, string};
use crate::{EngineError, Result, Session, bad, spec};
/// A board template.
pub struct BoardTemplate {
pub id: &'static str,
pub name: &'static str,
/// One line about what is in it.
pub description: &'static str,
/// Its width and height on the board.
pub size: (f64, f64),
make: fn(&mut Builder) -> Result<()>,
}
pub static TEMPLATES: &[BoardTemplate] = &[
BoardTemplate {
id: "retrospective",
name: "Retrospective",
description: "Three sections to look back on a stretch of work: what went well, what to improve, what to do next",
size: (1280.0, 592.0),
make: retrospective,
},
BoardTemplate {
id: "brainstorm",
name: "Brainstorm",
description: "A question in the middle and four clusters of ideas around it",
size: (1344.0, 760.0),
make: brainstorm,
},
BoardTemplate {
id: "weeklyPlanner",
name: "Weekly planner",
description: "A column for each working day with a note to start from",
size: (1380.0, 632.0),
make: weekly_planner,
},
BoardTemplate {
id: "flowChart",
name: "Flow chart",
description: "Steps, a decision and its two outcomes joined by connectors",
size: (1220.0, 360.0),
make: flow_chart,
},
BoardTemplate {
id: "kanban",
name: "Kanban board",
description: "To do, Doing and Done columns with cards to move along",
size: (900.0, 812.0),
make: kanban,
},
];
pub fn specs() -> Vec<CommandSpec> {
vec![
spec!(
"board.template",
"Board template",
"{name (a template id from board.templates), x?, y? (its top-left on the page; default centred in the view)} → {name, ids} its sections, stickies, shapes, connectors and text, selected",
true,
template_cmd
),
spec!("board.templates", "List board templates", "{} → [{id, name, description, width, height}]", false, |_, _| {
Ok(Value::Array(
TEMPLATES
.iter()
.map(|t| json!({"id": t.id, "name": t.name, "description": t.description, "width": t.size.0, "height": t.size.1}))
.collect(),
))
}),
]
}
/// The template called `name` (its id, or its name in any case).
pub fn find(name: &str) -> Option<&'static BoardTemplate> {
let name = name.trim();
TEMPLATES.iter().find(|t| t.id.eq_ignore_ascii_case(name) || t.name.eq_ignore_ascii_case(name))
}
fn template_cmd(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "board.template";
require(s, cmd)?;
let names = || TEMPLATES.iter().map(|t| t.id).collect::<Vec<_>>().join(", ");
let name = string(p, "name").ok_or_else(|| bad(cmd, format!("`name` is one of {}", names())))?;
let t = find(&name).ok_or_else(|| bad(cmd, format!("no template `{name}`: one of {}", names())))?;
let view = &s.ds()?.view;
let at = (num(p, "x").unwrap_or(view.cx - t.size.0 / 2.0), num(p, "y").unwrap_or(view.cy - t.size.1 / 2.0));
if !(at.0.is_finite() && at.1.is_finite()) {
return Err(bad(cmd, "`x` and `y` are finite numbers"));
}
let page = s.page()?;
let mut b = Builder { s, page, at, made: Vec::new() };
(t.make)(&mut b)?;
let made = b.made;
s.set_selection(made.clone());
Ok(json!({"name": t.id, "ids": made}))
}
/// A board with template `id` at the page's origin and nothing else (the picker's previews).
pub fn preview(id: &str) -> Result<Document> {
let mut s = Session::new();
s.execute("file.newBoard", &json!({"name": id}))?;
s.execute("board.template", &json!({"name": id, "x": 0, "y": 0}))?;
let ds = s.docs.pop().ok_or(EngineError::NoDocument)?;
Ok(Arc::try_unwrap(ds.doc).unwrap_or_else(|d| (*d).clone()))
}
/// Places a template's objects: positions are from its top-left (`at` on the page), or inside the
/// section they go in.
struct Builder<'a> {
s: &'a mut Session,
page: NodeId,
at: (f64, f64),
/// What it placed on the page.
made: Vec<NodeId>,
}
impl Builder<'_> {
/// Run board command `cmd` (in this command's undo step); the layer it made.
fn run(&mut self, cmd: &str, p: Value) -> Result<NodeId> {
let spec = crate::cmd::find_command(cmd).ok_or_else(|| EngineError::UnknownCommand(cmd.into()))?;
let v = (spec.run)(self.s, &p)?;
v.get("id").and_then(id_of).ok_or_else(|| EngineError::Other(format!("`{cmd}` made no layer")))
}
/// A layer on the page at (`x`, `y`) from the template's corner.
fn top(&mut self, cmd: &str, x: f64, y: f64, p: Value) -> Result<NodeId> {
let id = self.run(cmd, placed(p, self.page, (self.at.0 + x, self.at.1 + y)))?;
self.made.push(id);
Ok(id)
}
fn title(&mut self, text: &str) -> Result<NodeId> {
self.top("node.create", 0.0, 0.0, json!({"type": "text", "text": text, "props": {"fontSize": 32, "fills": ["#1E1E1E"]}}))
}
fn section(&mut self, name: &str, (x, y, w, h): (f64, f64, f64, f64), fill: &str) -> Result<NodeId> {
self.top("node.create", x, y, json!({"type": "section", "name": name, "width": w, "height": h, "props": {"fills": [fill]}}))
}
/// A sticky in `section` (or on the page) at (`x`, `y`) in it.
fn sticky(&mut self, section: Option<NodeId>, (x, y): (f64, f64), text: &str, color: &str) -> Result<NodeId> {
let p = json!({"text": text, "color": color, "author": false});
match section {
Some(sec) => self.run("board.sticky", placed(p, sec, (x, y))),
None => self.top("board.sticky", x, y, p),
}
}
fn shape(&mut self, shape: &str, (x, y, w, h): (f64, f64, f64, f64), text: &str, color: &str) -> Result<NodeId> {
self.top("board.shape", x, y, json!({"shape": shape, "width": w, "height": h, "text": text, "color": color}))
}
fn connector(&mut self, from: NodeId, to: NodeId, extra: Value) -> Result<NodeId> {
let id = self.run("board.connector", with(extra, [("from", json!(from)), ("to", json!(to))]))?;
self.made.push(id);
Ok(id)
}
}
/// Params `p` with `entries` added (`p` an object).
fn with<const N: usize>(mut p: Value, entries: [(&str, Value); N]) -> Value {
if let Some(o) = p.as_object_mut() {
o.extend(entries.map(|(k, v)| (k.to_string(), v)));
}
p
}
/// Params `p` placing a layer in `parent` at `at`.
fn placed(p: Value, parent: NodeId, at: (f64, f64)) -> Value {
with(p, [("parent", json!(parent)), ("x", json!(at.0)), ("y", json!(at.1))])
}
fn retrospective(b: &mut Builder) -> Result<()> {
b.title("Retrospective")?;
let parts = [
("Went well", "#EAF6E1", "green", ["Something that helped us", "A win worth repeating"]),
("To improve", "#FCE8EF", "pink", ["Something that slowed us down", "A question we still have"]),
("Next steps", "#E6F0FC", "blue", ["One change to try next", "Who looks after it?"]),
];
for (i, (name, fill, color, notes)) in parts.into_iter().enumerate() {
let sec = b.section(name, (i as f64 * 440.0, 72.0, 400.0, 520.0), fill)?;
for (j, note) in notes.into_iter().enumerate() {
b.sticky(Some(sec), (100.0, 60.0 + j as f64 * 230.0), note, color)?;
}
}
Ok(())
}
fn brainstorm(b: &mut Builder) -> Result<()> {
let topic = b.shape("ellipse", (532.0, 300.0, 280.0, 160.0), "What are we trying to solve?", "violet")?;
let clusters = [((0.0, 0.0), "yellow", 1), ((920.0, 0.0), "orange", 0), ((0.0, 560.0), "green", 1), ((920.0, 560.0), "blue", 0)];
for ((x, y), color, near) in clusters {
let notes = [b.sticky(None, (x, y), "An idea", color)?, b.sticky(None, (x + 224.0, y), "Build on it", color)?];
if let Some(n) = notes.get(near) {
b.connector(topic, *n, json!({"style": "straight", "endCap": "none"}))?;
}
}
Ok(())
}
fn weekly_planner(b: &mut Builder) -> Result<()> {
b.title("This week")?;
let days = [
("Monday", "Plan the week"),
("Tuesday", "Focus for the day"),
("Wednesday", "Focus for the day"),
("Thursday", "Focus for the day"),
("Friday", "Wrap up and share"),
];
for (i, (day, note)) in days.into_iter().enumerate() {
let sec = b.section(day, (i as f64 * 280.0, 72.0, 260.0, 560.0), "#FFFFFF")?;
b.sticky(Some(sec), (30.0, 60.0), note, if i % 2 == 0 { "yellow" } else { "orange" })?;
}
Ok(())
}
fn flow_chart(b: &mut Builder) -> Result<()> {
let start = b.shape("ellipse", (0.0, 30.0, 160.0, 100.0), "Start", "green")?;
let step = b.shape("rectangle", (240.0, 20.0, 180.0, 120.0), "Do the first step", "blue")?;
let check = b.shape("diamond", (500.0, 0.0, 200.0, 160.0), "Did it work?", "yellow")?;
let carry = b.shape("roundedRectangle", (800.0, 20.0, 180.0, 120.0), "Carry on", "blue")?;
let done = b.shape("ellipse", (1060.0, 30.0, 160.0, 100.0), "Done", "green")?;
let other = b.shape("roundedRectangle", (510.0, 240.0, 180.0, 120.0), "Try another way", "orange")?;
let elbow = json!({"style": "elbow"});
b.connector(start, step, elbow.clone())?;
b.connector(step, check, elbow.clone())?;
b.connector(check, carry, json!({"style": "elbow", "label": "Yes"}))?;
b.connector(carry, done, elbow)?;
b.connector(check, other, json!({"style": "elbow", "fromSide": "bottom", "toSide": "top", "label": "No"}))?;
// Back round to the step: out of the side, into the step from below.
b.connector(other, step, json!({"style": "elbow", "fromSide": "left", "toSide": "bottom"}))?;
Ok(())
}
fn kanban(b: &mut Builder) -> Result<()> {
b.title("Team board")?;
let columns: [(&str, &str, &[&str]); 3] = [
("To do", "yellow", &["Write the brief", "Sketch the first screens", "Plan a test session"]),
("Doing", "blue", &["Review the copy"]),
("Done", "green", &["Set up the board"]),
];
for (i, (name, color, cards)) in columns.into_iter().enumerate() {
let sec = b.section(name, (i as f64 * 310.0, 72.0, 280.0, 740.0), "#FFFFFF")?;
for (j, card) in cards.iter().enumerate() {
b.sticky(Some(sec), (40.0, 60.0 + j as f64 * 220.0), card, color)?;
}
}
Ok(())
}
+2
View File
@@ -115,6 +115,8 @@ fn registry() -> &'static Registry {
list.extend(crate::textfit::specs());
list.extend(crate::board::specs());
list.extend(facilitate::specs());
list.extend(crate::board_table::specs());
list.extend(crate::board_templates::specs());
let mut index = HashMap::new();
for (i, s) in list.iter().enumerate() {
if index.insert(s.id, i).is_some() {
+4
View File
@@ -15,6 +15,8 @@ pub mod arc_handles;
pub mod background;
pub mod batch;
pub mod board;
pub mod board_table;
pub mod board_templates;
pub mod booleans;
pub mod branches;
pub mod catalog;
@@ -1640,6 +1642,8 @@ mod binding_lists_tests;
#[cfg(test)]
mod board_connector_tests;
#[cfg(test)]
mod board_tables_tests;
#[cfg(test)]
mod board_tests;
#[cfg(test)]
mod board_tools_tests;
+13
View File
@@ -45,6 +45,7 @@ status = "draft — needs native review"
"Add another condition" = "আরেকটি শর্ত যোগ করুন"
"Add auto layout" = "অটো লেআউট যোগ করুন"
"Add background behind overlay" = "ওভারলে-এর পেছনে ব্যাকগ্রাউন্ড যোগ করুন"
"Add column" = "কলাম যোগ করুন"
"Add content to this slot" = "এই স্লটে কন্টেন্ট যোগ করুন"
"Add dev resource" = "ডেভ রিসোর্স যোগ করুন"
"Add effect" = "ইফেক্ট যোগ করুন"
@@ -64,6 +65,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "একটি ভেরিয়েবল JSON থেকে সংগ্রহ যোগ বা আপডেট করুন"
"Add properties to make instances configurable" = "ইনস্ট্যান্সগুলোকে কনফিগারযোগ্য করতে প্রপার্টি যোগ করুন"
"Add reaction" = "রিঅ্যাকশন যোগ করুন"
"Add row" = "সারি যোগ করুন"
"Add starting point" = "শুরু করার বিন্দু যোগ করুন"
"Add stroke" = "স্ট্রোক যোগ করুন"
"Add variant" = "ভ্যারিয়েন্ট যোগ করুন"
@@ -183,6 +185,7 @@ status = "draft — needs native review"
"Bottom" = "নিচে"
"Bottom left radius" = "নিচের বাম ব্যাসার্ধ"
"Bottom right radius" = "নিচের ডান ব্যাসার্ধ"
"Brainstorm" = "ব্রেইনস্টর্ম"
"Branch" = "ব্রাঞ্চ"
"Branch archived" = "ব্রাঞ্চ আর্কাইভ করা হয়েছে"
"Branch merged" = "ব্রাঞ্চ মার্জ করা হয়েছে"
@@ -542,6 +545,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "টেক্সট অন্য পাশে উল্টান"
"Flip vertical" = "উল্লম্বভাবে উল্টান"
"Flip width profile" = "প্রস্থ প্রোফাইল উল্টান"
"Flow chart" = "ফ্লো চার্ট"
"Flow starting point" = "ফ্লো শুরুর বিন্দু"
"Flow starting points" = "ফ্লো শুরুর বিন্দুসমূহ"
"Flows" = "ফ্লোসমূহ"
@@ -696,6 +700,7 @@ status = "draft — needs native review"
"Jun" = "জুন"
"Just now" = "এইমাত্র"
"Justify" = "উভয়মুখী সারিবদ্ধকরণ (Justify)"
"Kanban board" = "কানবান বোর্ড"
"Keep a measurement on the canvas for developers" = "ডেভেলপারদের জন্য ক্যানভাসে একটি মাপ রাখুন"
"Keep appearance" = "চেহারা অপরিবর্তিত রাখুন"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "চেহারা অপরিবর্তিত রাখলে প্রতিটি রঙের মান {profile}-এ রূপান্তরিত হয় যাতে রঙগুলো একই দেখায়। মান অপরিবর্তিত রাখলে সংখ্যাগুলো যেমন আছে তেমনই থাকে, ফলে রঙ পরিবর্তিত হয়।"
@@ -1097,6 +1102,7 @@ status = "draft — needs native review"
"Remove all" = "সব সরান"
"Remove annotation" = "অ্যানোটেশন সরান"
"Remove auto layout" = "অটো লেআউট সরান"
"Remove column" = "কলাম সরান"
"Remove dev resource" = "ডেভ রিসোর্স সরান"
"Remove export setting" = "এক্সপোর্ট সেটিংস সরান"
"Remove from recents" = "সাম্প্রতিক থেকে সরান"
@@ -1110,6 +1116,7 @@ status = "draft — needs native review"
"Remove min and max height" = "সর্বনিম্ন এবং সর্বোচ্চ উচ্চতা সরান"
"Remove min and max width" = "সর্বনিম্ন এবং সর্বোচ্চ প্রস্থ সরান"
"Remove regions" = "অঞ্চলগুলো সরান"
"Remove row" = "সারি সরান"
"Remove slot" = "স্লট সরান"
"Remove starting point" = "শুরু করার বিন্দু সরান"
"Remove stop" = "স্টপ সরান"
@@ -1162,6 +1169,7 @@ status = "draft — needs native review"
"Restore main component" = "প্রধান কম্পোনেন্ট পুনরুদ্ধার করুন"
"Restore this version" = "এই সংস্করণ পুনরুদ্ধার করুন"
"Restored “{name}”" = "“{name}” পুনরুদ্ধার করা হয়েছে"
"Retrospective" = "রেট্রোস্পেক্টিভ"
"Results" = "ফলাফল"
"Resume" = "আবার চালু করুন"
"Reverse the order" = "ক্রম উল্টে দিন"
@@ -1548,6 +1556,7 @@ status = "draft — needs native review"
"WebSocket URL" = "WebSocket URL"
"WebSocket URL copied" = "WebSocket URL কপি করা হয়েছে"
"Wedge" = "ওয়েজ"
"Weekly planner" = "সাপ্তাহিক পরিকল্পনা"
"Weight" = "ওজন"
"What you try here changes this preview and the code below, not the file" = "আপনি এখানে যা চেষ্টা করেন তা ফাইলটিকে নয়, কেবল এই প্রিভিউ এবং নিচের কোডটিকে পরিবর্তন করে"
"What changed since it was marked ready for dev" = "এটি ডেভের জন্য প্রস্তুত হিসেবে চিহ্নিত করার পর থেকে কী পরিবর্তিত হয়েছে"
@@ -1811,6 +1820,10 @@ status = "draft — needs native review"
"board.shape" = "লেখাসহ আকৃতি"
"board.stamp" = "স্ট্যাম্প"
"board.sticky" = "স্টিকি নোট"
"board.table" = "টেবিল"
"board.tableEdit" = "টেবিল সম্পাদনা"
"board.template" = "বোর্ড টেমপ্লেট"
"board.templates" = "বোর্ড টেমপ্লেটের তালিকা"
"board.timer" = "টাইমার"
"board.timerState" = "টাইমারের অবস্থা"
"board.vote" = "ভোটিং সেশন"
+13
View File
@@ -41,6 +41,7 @@ status = "draft — needs native review"
"Add another condition" = "Přidat další podmínku"
"Add auto layout" = "Přidat automatické rozvržení"
"Add background behind overlay" = "Přidat pozadí za překryv"
"Add column" = "Přidat sloupec"
"Add content to this slot" = "Přidat obsah do tohoto slotu"
"Add dev resource" = "Přidat zdroj pro vývojáře"
"Add effect" = "Přidat efekt"
@@ -60,6 +61,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "Přidat nebo aktualizovat kolekce ze souboru JSON s proměnnými"
"Add properties to make instances configurable" = "Přidejte vlastnosti pro nastavení instancí"
"Add reaction" = "Přidat reakci"
"Add row" = "Přidat řádek"
"Add starting point" = "Přidat počáteční bod"
"Add stroke" = "Přidat tah"
"Add variant" = "Přidat variantu"
@@ -177,6 +179,7 @@ status = "draft — needs native review"
"Bottom" = "Dole"
"Bottom left radius" = "Poloměr levého dolního rohu"
"Bottom right radius" = "Poloměr pravého dolního rohu"
"Brainstorm" = "Brainstorming"
"Branch" = "Větev"
"Branch archived" = "Větev archivována"
"Branch merged" = "Větev sloučena"
@@ -528,6 +531,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "Převrátit text na druhou stranu"
"Flip vertical" = "Převrátit svisle"
"Flip width profile" = "Převrátit profil šířky"
"Flow chart" = "Vývojový diagram"
"Flow starting point" = "Počáteční bod toku"
"Flow starting points" = "Počáteční body toků"
"Flows" = "Toky"
@@ -681,6 +685,7 @@ status = "draft — needs native review"
"Jun" = "června"
"Just now" = "Právě teď"
"Justify" = "Do bloku"
"Kanban board" = "Kanbanová tabule"
"Keep a measurement on the canvas for developers" = "Ponechat měření na plátně pro vývojáře"
"Keep appearance" = "Zachovat vzhled"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Zachovat vzhled převede všechny hodnoty barev do profilu {profile}, aby barvy vypadaly stejně. Zachovat hodnoty ponechá čísla beze změny, takže se barvy posunou."
@@ -1076,6 +1081,7 @@ status = "draft — needs native review"
"Remove all" = "Odebrat vše"
"Remove annotation" = "Odebrat anotaci"
"Remove auto layout" = "Odebrat automatické rozvržení"
"Remove column" = "Odebrat sloupec"
"Remove dev resource" = "Odebrat zdroj pro vývojáře"
"Remove export setting" = "Odebrat nastavení exportu"
"Remove from recents" = "Odebrat z nedávných"
@@ -1089,6 +1095,7 @@ status = "draft — needs native review"
"Remove min and max height" = "Odebrat minimální a maximální výšku"
"Remove min and max width" = "Odebrat minimální a maximální šířku"
"Remove regions" = "Odebrat oblasti"
"Remove row" = "Odebrat řádek"
"Remove slot" = "Odebrat slot"
"Remove starting point" = "Odebrat počáteční bod"
"Remove stop" = "Odebrat zarážku"
@@ -1140,6 +1147,7 @@ status = "draft — needs native review"
"Restore main component" = "Obnovit hlavní komponentu"
"Restore this version" = "Obnovit tuto verzi"
"Restored “{name}”" = "Obnoveno: „{name}“"
"Retrospective" = "Retrospektiva"
"Results" = "Výsledky"
"Resume" = "Pokračovat"
"Reverse the order" = "Obrátit pořadí"
@@ -1515,6 +1523,7 @@ status = "draft — needs native review"
"WebSocket URL" = "URL WebSocketu"
"WebSocket URL copied" = "URL WebSocketu zkopírována"
"Wedge" = "Klín"
"Weekly planner" = "Týdenní plán"
"Weight" = "Tloušťka"
"What you try here changes this preview and the code below, not the file" = "Zdejší zkoušení mění tento náhled a kód níže, soubor zůstává beze změny"
"What changed since it was marked ready for dev" = "Co se změnilo od označení jako připravené pro vývoj"
@@ -1799,6 +1808,10 @@ status = "draft — needs native review"
"board.shape" = "Tvar s textem"
"board.stamp" = "Razítko"
"board.sticky" = "Lepicí poznámka"
"board.table" = "Tabulka"
"board.tableEdit" = "Upravit tabulku"
"board.template" = "Šablona tabule"
"board.templates" = "Seznam šablon tabule"
"board.timer" = "Časovač"
"board.timerState" = "Stav časovače"
"board.vote" = "Hlasování"
+14
View File
@@ -42,6 +42,7 @@ status = "draft — needs native review"
"Add another condition" = "Weitere Bedingung hinzufügen"
"Add auto layout" = "Auto-Layout hinzufügen"
"Add background behind overlay" = "Hintergrund hinter dem Overlay hinzufügen"
"Add column" = "Spalte hinzufügen"
"Add content to this slot" = "Inhalt zu diesem Slot hinzufügen"
"Add dev resource" = "Entwicklerressource hinzufügen"
"Add effect" = "Effekt hinzufügen"
@@ -61,6 +62,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "Sammlungen aus einer Variablen-JSON hinzufügen oder aktualisieren"
"Add properties to make instances configurable" = "Fügen Sie Eigenschaften hinzu, um Instanzen konfigurierbar zu machen"
"Add reaction" = "Reaktion hinzufügen"
"Add row" = "Zeile hinzufügen"
"Add starting point" = "Startpunkt hinzufügen"
"Add stroke" = "Kontur hinzufügen"
"Add variant" = "Variante hinzufügen"
@@ -174,6 +176,7 @@ status = "draft — needs native review"
"Bottom" = "Unten"
"Bottom left radius" = "Radius unten links"
"Bottom right radius" = "Radius unten rechts"
"Brainstorm" = "Brainstorming"
"Branch" = "Branch"
"Branch archived" = "Branch archiviert"
"Branch merged" = "Branch zusammengeführt"
@@ -521,6 +524,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "Text auf die andere Seite setzen"
"Flip vertical" = "Vertikal spiegeln"
"Flip width profile" = "Breitenprofil umkehren"
"Flow chart" = "Flussdiagramm"
"Flow starting point" = "Startpunkt des Ablaufs"
"Flow starting points" = "Startpunkte von Abläufen"
"Flows" = "Abläufe"
@@ -665,6 +669,7 @@ status = "draft — needs native review"
"Join|stroke" = "Ecke"
"Just now" = "Gerade eben"
"Justify" = "Blocksatz"
"Kanban board" = "Kanban-Board"
"Keep a measurement on the canvas for developers" = "Eine Messung für Entwickler auf der Arbeitsfläche behalten"
"Keep appearance" = "Aussehen beibehalten"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "„Aussehen beibehalten“ wandelt jeden Farbwert in {profile} um, damit die Farben gleich aussehen. „Werte beibehalten“ lässt die Zahlen unverändert, sodass sich die Farben verschieben."
@@ -1045,6 +1050,7 @@ status = "draft — needs native review"
"Remove all" = "Alle entfernen"
"Remove annotation" = "Anmerkung entfernen"
"Remove auto layout" = "Auto-Layout entfernen"
"Remove column" = "Spalte entfernen"
"Remove dev resource" = "Entwicklerressource entfernen"
"Remove export setting" = "Exporteinstellung entfernen"
"Remove from server" = "Vom Server entfernen"
@@ -1057,6 +1063,7 @@ status = "draft — needs native review"
"Remove min and max height" = "Min. und max. Höhe entfernen"
"Remove min and max width" = "Min. und max. Breite entfernen"
"Remove regions" = "Bereiche entfernen"
"Remove row" = "Zeile entfernen"
"Remove slot" = "Slot entfernen"
"Remove starting point" = "Startpunkt entfernen"
"Remove stop" = "Farbunterbrechung entfernen"
@@ -1110,6 +1117,7 @@ status = "draft — needs native review"
"Restore main component" = "Hauptkomponente wiederherstellen"
"Restore this version" = "Diese Version wiederherstellen"
"Restored “{name}”" = "„{name}“ wiederhergestellt"
"Retrospective" = "Retrospektive"
"Results" = "Ergebnisse"
"Resume" = "Fortsetzen"
"Reverse the order" = "Reihenfolge umkehren"
@@ -1329,6 +1337,7 @@ status = "draft — needs native review"
"Tell the server's operator what is wrong with it" = "Teilen Sie dem Betreiber des Servers mit, was damit nicht stimmt"
"Temperature" = "Temperatur"
"Template" = "Vorlage"
"Templates" = "Vorlagen"
"Terms" = "Bedingungen"
"Text" = "Text"
"Text align" = "Textausrichtung"
@@ -1475,6 +1484,7 @@ status = "draft — needs native review"
"WebSocket URL" = "WebSocket-URL"
"WebSocket URL copied" = "WebSocket-URL kopiert"
"Wedge" = "Keil"
"Weekly planner" = "Wochenplaner"
"Weight" = "Stärke"
"What changed since a saved version" = "Was sich seit einer gespeicherten Version geändert hat"
"What changed since it was marked ready for dev" = "Was sich seit der Markierung als bereit für die Entwicklung geändert hat"
@@ -1755,6 +1765,10 @@ status = "draft — needs native review"
"board.shape" = "Form mit Text"
"board.stamp" = "Stempel"
"board.sticky" = "Haftnotiz"
"board.table" = "Tabelle"
"board.tableEdit" = "Tabelle bearbeiten"
"board.template" = "Whiteboard-Vorlage"
"board.templates" = "Whiteboard-Vorlagen auflisten"
"board.timer" = "Timer"
"board.timerState" = "Timer-Status"
"board.vote" = "Abstimmung"
+9
View File
@@ -44,6 +44,7 @@ name = "English"
"Add another condition" = "Add another condition"
"Add auto layout" = "Add auto layout"
"Add background behind overlay" = "Add background behind overlay"
"Add column" = "Add column"
"Add content to this slot" = "Add content to this slot"
"Add dev resource" = "Add dev resource"
"Add effect" = "Add effect"
@@ -63,6 +64,7 @@ name = "English"
"Add or update collections from a variables JSON" = "Add or update collections from a variables JSON"
"Add properties to make instances configurable" = "Add properties to make instances configurable"
"Add reaction" = "Add reaction"
"Add row" = "Add row"
"Add starting point" = "Add starting point"
"Add stroke" = "Add stroke"
"Add variant" = "Add variant"
@@ -183,6 +185,7 @@ name = "English"
"Bottom left radius" = "Bottom left radius"
"Bottom padding" = "Bottom padding"
"Bottom right radius" = "Bottom right radius"
"Brainstorm" = "Brainstorm"
"Branch" = "Branch"
"Branch archived" = "Branch archived"
"Branch merged" = "Branch merged"
@@ -555,6 +558,7 @@ name = "English"
"Flip text to the other side" = "Flip text to the other side"
"Flip vertical" = "Flip vertical"
"Flip width profile" = "Flip width profile"
"Flow chart" = "Flow chart"
"Flow starting point" = "Flow starting point"
"Flow starting points" = "Flow starting points"
"Flows" = "Flows"
@@ -712,6 +716,7 @@ name = "English"
"Jun" = "Jun"
"Just now" = "Just now"
"Justify" = "Justify"
"Kanban board" = "Kanban board"
"Keep a measurement on the canvas for developers" = "Keep a measurement on the canvas for developers"
"Keep appearance" = "Keep appearance"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift."
@@ -1122,6 +1127,7 @@ name = "English"
"Remove all" = "Remove all"
"Remove annotation" = "Remove annotation"
"Remove auto layout" = "Remove auto layout"
"Remove column" = "Remove column"
"Remove dev resource" = "Remove dev resource"
"Remove export setting" = "Remove export setting"
"Remove from recents" = "Remove from recents"
@@ -1135,6 +1141,7 @@ name = "English"
"Remove min and max height" = "Remove min and max height"
"Remove min and max width" = "Remove min and max width"
"Remove regions" = "Remove regions"
"Remove row" = "Remove row"
"Remove slot" = "Remove slot"
"Remove starting point" = "Remove starting point"
"Remove stop" = "Remove stop"
@@ -1189,6 +1196,7 @@ name = "English"
"Restore main component" = "Restore main component"
"Restore this version" = "Restore this version"
"Restored “{name}”" = "Restored “{name}”"
"Retrospective" = "Retrospective"
"Results" = "Results"
"Resume" = "Resume"
"Reverse the order" = "Reverse the order"
@@ -1589,6 +1597,7 @@ name = "English"
"WebSocket URL" = "WebSocket URL"
"WebSocket URL copied" = "WebSocket URL copied"
"Wedge" = "Wedge"
"Weekly planner" = "Weekly planner"
"Weight" = "Weight"
"What changed since a saved version" = "What changed since a saved version"
"What you try here changes this preview and the code below, not the file" = "What you try here changes this preview and the code below, not the file"
+2
View File
@@ -25,6 +25,7 @@ status = "draft — needs native review"
"Add another condition" = "Agregar otra condición"
"Add auto layout" = "Agregar diseño automático"
"Add background behind overlay" = "Agregar fondo tras la superposición"
"Add column" = "Agregar columna"
"Add content to this slot" = "Agregar contenido a esta ranura"
"Add dev resource" = "Agregar recurso de desarrollo"
"Add effect" = "Agregar efecto"
@@ -44,6 +45,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "Agregar o actualizar colecciones desde un JSON de variables"
"Add properties to make instances configurable" = "Agrega propiedades para que las instancias sean configurables"
"Add reaction" = "Agregar reacción"
"Add row" = "Agregar fila"
"Add starting point" = "Agregar punto de inicio"
"Add stroke" = "Agregar trazo"
"Add variant" = "Agregar variante"
+14 -1
View File
@@ -8,8 +8,10 @@ name = "Español"
status = "draft — needs native review"
[ui]
"Add a minute" = "Añadir un minuto"
"Add column" = "Añadir columna"
"Add measurement" = "Añadir medición"
"Add row" = "Añadir fila"
"Add a minute" = "Añadir un minuto"
"All objects" = "Todos los objetos"
"Along the bottom" = "A lo largo del borde inferior"
"Along the left" = "A lo largo del borde izquierdo"
@@ -17,6 +19,7 @@ status = "draft — needs native review"
"Along the top" = "A lo largo del borde superior"
"Arrow at the end" = "Flecha al final"
"Between the selected layers" = "Entre las capas seleccionadas"
"Brainstorm" = "Lluvia de ideas"
"Check" = "Visto"
"Chevron" = "Cheurón"
"Clear results" = "Borrar resultados"
@@ -550,6 +553,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "Pasar el texto al otro lado"
"Flip vertical" = "Voltear verticalmente"
"Flip width profile" = "Invertir perfil de ancho"
"Flow chart" = "Diagrama de flujo"
"Flow starting point" = "Punto de inicio del flujo"
"Flow starting points" = "Puntos de inicio de flujo"
"Flows" = "Flujos"
@@ -707,6 +711,7 @@ status = "draft — needs native review"
"Jun" = "jun"
"Just now" = "Justo ahora"
"Justify" = "Justificar"
"Kanban board" = "Tablero kanban"
"Keep a measurement on the canvas for developers" = "Mantén una medición en el lienzo para desarrolladores"
"Keep appearance" = "Mantener apariencia"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Mantener apariencia convierte cada valor de color a {profile} para que los colores se vean igual. Mantener valores deja los números como están, así que los colores cambian."
@@ -1116,6 +1121,7 @@ status = "draft — needs native review"
"Remove all" = "Quitar todo"
"Remove annotation" = "Quitar anotación"
"Remove auto layout" = "Quitar diseño automático"
"Remove column" = "Quitar columna"
"Remove dev resource" = "Quitar recurso de desarrollo"
"Remove export setting" = "Quitar ajuste de exportación"
"Remove from recents" = "Quitar de recientes"
@@ -1129,6 +1135,7 @@ status = "draft — needs native review"
"Remove min and max height" = "Quitar alto mín. y máx."
"Remove min and max width" = "Quitar ancho mín. y máx."
"Remove regions" = "Quitar regiones"
"Remove row" = "Quitar fila"
"Remove slot" = "Quitar ranura"
"Remove starting point" = "Quitar punto de inicio"
"Remove stop" = "Quitar parada"
@@ -1183,6 +1190,7 @@ status = "draft — needs native review"
"Restore main component" = "Restaurar componente principal"
"Restore this version" = "Restaurar esta versión"
"Restored “{name}”" = "“{name}” restaurada"
"Retrospective" = "Retrospectiva"
"Results" = "Resultados"
"Resume" = "Reanudar"
"Reverse the order" = "Invertir el orden"
@@ -1579,6 +1587,7 @@ status = "draft — needs native review"
"WebSocket URL" = "URL de WebSocket"
"WebSocket URL copied" = "URL de WebSocket copiada"
"Wedge" = "Cuña"
"Weekly planner" = "Planificador semanal"
"Weight" = "Grosor"
"What changed since a saved version" = "Qué cambió desde una versión guardada"
"What you try here changes this preview and the code below, not the file" = "Lo que pruebes aquí cambia esta vista previa y el código de abajo, no el archivo"
@@ -1873,6 +1882,10 @@ status = "draft — needs native review"
"board.shape" = "Forma con texto"
"board.stamp" = "Sello"
"board.sticky" = "Nota adhesiva"
"board.table" = "Tabla"
"board.tableEdit" = "Editar tabla"
"board.template" = "Plantilla de pizarra"
"board.templates" = "Listar plantillas de pizarra"
"board.timer" = "Temporizador"
"board.timerState" = "Estado del temporizador"
"board.vote" = "Votación"
+13
View File
@@ -46,6 +46,7 @@ status = "draft — needs native review"
"Add another condition" = "Lisää toinen ehto"
"Add auto layout" = "Lisää automaattinen asettelu"
"Add background behind overlay" = "Lisää tausta peittokuvan taakse"
"Add column" = "Lisää sarake"
"Add content to this slot" = "Lisää sisältöä tähän sisältöpaikkaan"
"Add dev resource" = "Lisää kehittäjäresurssi"
"Add effect" = "Lisää tehoste"
@@ -65,6 +66,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "Lisää tai päivitä kokoelmia muuttujien JSON-tiedostosta"
"Add properties to make instances configurable" = "Lisää ominaisuuksia, jotta ilmentymiä voi mukauttaa"
"Add reaction" = "Lisää reaktio"
"Add row" = "Lisää rivi"
"Add starting point" = "Lisää aloituspiste"
"Add stroke" = "Lisää reunaviiva"
"Add variant" = "Lisää variantti"
@@ -185,6 +187,7 @@ status = "draft — needs native review"
"Bottom left radius" = "Vasemman alakulman säde"
"Bottom right radius" = "Oikean alakulman säde"
"Bound to {name}. Click to detach the variable." = "Sidottu muuttujaan {name}. Irrota muuttuja napsauttamalla."
"Brainstorm" = "Ideointi"
"Branch" = "Haara"
"Branch archived" = "Haara arkistoitu"
"Branch merged" = "Haara yhdistetty"
@@ -544,6 +547,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "Siirrä teksti toiselle puolelle"
"Flip vertical" = "Peilaa pystysuunnassa"
"Flip width profile" = "Peilaa leveysprofiili"
"Flow chart" = "Vuokaavio"
"Flow starting point" = "Kulun aloituspiste"
"Flow starting points" = "Kulkujen aloituspisteet"
"Flows" = "Kulut"
@@ -698,6 +702,7 @@ status = "draft — needs native review"
"Jun" = "kesäk."
"Just now" = "Juuri nyt"
"Justify" = "Tasaa molemmat reunat"
"Kanban board" = "Kanban-taulu"
"Keep a measurement on the canvas for developers" = "Säilytä mittaus piirtoalueella kehittäjille"
"Keep appearance" = "Säilytä ulkoasu"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Säilytä ulkoasu muuntaa kaikki väriarvot profiiliin {profile}, jotta värit näyttävät samoilta. Säilytä arvot jättää lukuarvot ennalleen, jolloin värit muuttuvat."
@@ -1101,6 +1106,7 @@ status = "draft — needs native review"
"Remove all" = "Poista kaikki"
"Remove annotation" = "Poista huomautus"
"Remove auto layout" = "Poista automaattinen asettelu"
"Remove column" = "Poista sarake"
"Remove dev resource" = "Poista kehittäjäresurssi"
"Remove export setting" = "Poista vientiasetus"
"Remove from recents" = "Poista viimeisimmistä"
@@ -1114,6 +1120,7 @@ status = "draft — needs native review"
"Remove min and max height" = "Poista korkeuden vähimmäis- ja enimmäisrajat"
"Remove min and max width" = "Poista leveyden vähimmäis- ja enimmäisrajat"
"Remove regions" = "Poista alueet"
"Remove row" = "Poista rivi"
"Remove slot" = "Poista sisältöpaikka"
"Remove starting point" = "Poista aloituspiste"
"Remove stop" = "Poista liukuvärin piste"
@@ -1166,6 +1173,7 @@ status = "draft — needs native review"
"Restore main component" = "Palauta pääkomponentti"
"Restore this version" = "Palauta tämä versio"
"Restored “{name}”" = "”{name}” palautettu"
"Retrospective" = "Retrospektiivi"
"Results" = "Tulokset"
"Resume" = "Jatka"
"Reverse the order" = "Käännä järjestys"
@@ -1553,6 +1561,7 @@ status = "draft — needs native review"
"WebSocket URL copied" = "WebSocket-URL kopioitu"
"Websites" = "Verkkosivustot"
"Wedge" = "Kiila"
"Weekly planner" = "Viikkosuunnitelma"
"Weight" = "Paksuus"
"What changed since it was marked ready for dev" = "Muutokset kehitysvalmiiksi merkitsemisen jälkeen"
"What is wrong with this file? The server's operator gets your report." = "Mikä tässä tiedostossa on vikana? Palvelimen ylläpitäjä saa ilmoituksesi."
@@ -1841,6 +1850,10 @@ status = "draft — needs native review"
"board.shape" = "Muoto ja teksti"
"board.stamp" = "Leima"
"board.sticky" = "Muistilappu"
"board.table" = "Taulukko"
"board.tableEdit" = "Muokkaa taulukkoa"
"board.template" = "Taulun malli"
"board.templates" = "Luettele taulun mallit"
"board.timer" = "Ajastin"
"board.timerState" = "Ajastimen tila"
"board.vote" = "Äänestys"
+13
View File
@@ -45,6 +45,7 @@ status = "draft — needs native review"
"Add another condition" = "Ajouter une autre condition"
"Add auto layout" = "Ajouter la mise en page auto"
"Add background behind overlay" = "Ajouter un fond derrière la superposition"
"Add column" = "Ajouter une colonne"
"Add content to this slot" = "Ajouter du contenu à cet emplacement"
"Add dev resource" = "Ajouter une ressource de développement"
"Add effect" = "Ajouter un effet"
@@ -64,6 +65,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "Ajouter ou mettre à jour des collections depuis un JSON de variables"
"Add properties to make instances configurable" = "Ajoutez des propriétés pour rendre les instances configurables"
"Add reaction" = "Ajouter une réaction"
"Add row" = "Ajouter une ligne"
"Add starting point" = "Ajouter un point de départ"
"Add stroke" = "Ajouter un contour"
"Add variant" = "Ajouter une variante"
@@ -183,6 +185,7 @@ status = "draft — needs native review"
"Bottom left radius" = "Rayon inférieur gauche"
"Bottom right radius" = "Rayon inférieur droit"
"Bound to {name}. Click to detach the variable." = "Lié à {name}. Cliquez pour détacher la variable."
"Brainstorm" = "Remue-méninges"
"Branch" = "Branche"
"Branch archived" = "Branche archivée"
"Branch merged" = "Branche fusionnée"
@@ -543,6 +546,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "Passer le texte de l’autre côté"
"Flip vertical" = "Retourner verticalement"
"Flip width profile" = "Inverser le profil de largeur"
"Flow chart" = "Logigramme"
"Flow starting point" = "Point de départ du flux"
"Flow starting points" = "Points de départ de flux"
"Flows" = "Flux"
@@ -698,6 +702,7 @@ status = "draft — needs native review"
"Jun" = "juin"
"Just now" = "À l’instant"
"Justify" = "Justifier"
"Kanban board" = "Tableau kanban"
"Keep a measurement on the canvas for developers" = "Garder une mesure sur le canevas pour les développeurs"
"Keep appearance" = "Conserver l’apparence"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Conserver l’apparence convertit chaque valeur de couleur en {profile} pour que les couleurs restent identiques. Conserver les valeurs laisse les nombres tels quels, si bien que les couleurs changent."
@@ -1101,6 +1106,7 @@ status = "draft — needs native review"
"Remove all" = "Tout retirer"
"Remove annotation" = "Retirer l’annotation"
"Remove auto layout" = "Retirer la mise en page auto"
"Remove column" = "Retirer la colonne"
"Remove dev resource" = "Retirer la ressource de développement"
"Remove export setting" = "Retirer le réglage d’exportation"
"Remove from recents" = "Retirer des récents"
@@ -1114,6 +1120,7 @@ status = "draft — needs native review"
"Remove min and max height" = "Retirer la hauteur min. et max."
"Remove min and max width" = "Retirer la largeur min. et max."
"Remove regions" = "Retirer les régions"
"Remove row" = "Retirer la ligne"
"Remove slot" = "Retirer l’emplacement"
"Remove starting point" = "Retirer le point de départ"
"Remove stop" = "Retirer l’arrêt"
@@ -1167,6 +1174,7 @@ status = "draft — needs native review"
"Restore main component" = "Restaurer le composant principal"
"Restore this version" = "Restaurer cette version"
"Restored “{name}”" = "« {name} » restaurée"
"Retrospective" = "Rétrospective"
"Results" = "Résultats"
"Resume" = "Reprendre"
"Reverse the order" = "Inverser l’ordre"
@@ -1554,6 +1562,7 @@ status = "draft — needs native review"
"WebSocket URL copied" = "URL WebSocket copiée"
"Websites" = "Sites web"
"Wedge" = "Coin"
"Weekly planner" = "Planning de la semaine"
"Weight" = "Graisse"
"What changed since a saved version" = "Ce qui a changé depuis une version enregistrée"
"What changed since it was marked ready for dev" = "Ce qui a changé depuis le marquage prêt pour le dev"
@@ -1842,6 +1851,10 @@ status = "draft — needs native review"
"board.shape" = "Forme avec texte"
"board.stamp" = "Tampon"
"board.sticky" = "Note autocollante"
"board.table" = "Tableau"
"board.tableEdit" = "Modifier le tableau"
"board.template" = "Modèle de tableau blanc"
"board.templates" = "Lister les modèles de tableau blanc"
"board.timer" = "Minuteur"
"board.timerState" = "État du minuteur"
"board.vote" = "Session de vote"
+13
View File
@@ -45,6 +45,7 @@ status = "draft — needs native review"
"Add another condition" = "Tambah kondisi lain"
"Add auto layout" = "Tambah auto layout"
"Add background behind overlay" = "Tambah latar belakang di balik overlay"
"Add column" = "Tambah kolom"
"Add content to this slot" = "Tambah konten ke slot ini"
"Add dev resource" = "Tambah sumber daya dev"
"Add effect" = "Tambah efek"
@@ -64,6 +65,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "Tambah atau perbarui koleksi dari JSON variabel"
"Add properties to make instances configurable" = "Tambahkan properti agar instance dapat dikonfigurasi"
"Add reaction" = "Tambah reaksi"
"Add row" = "Tambah baris"
"Add starting point" = "Tambah titik awal"
"Add stroke" = "Tambah stroke"
"Add variant" = "Tambah variant"
@@ -181,6 +183,7 @@ status = "draft — needs native review"
"Bottom" = "Bawah"
"Bottom left radius" = "Radius kiri bawah"
"Bottom right radius" = "Radius kanan bawah"
"Brainstorm" = "Curah gagasan"
"Branch" = "Branch"
"Branch archived" = "Branch diarsipkan"
"Branch merged" = "Branch di-merge"
@@ -532,6 +535,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "Balik teks ke sisi lain"
"Flip vertical" = "Balik vertikal"
"Flip width profile" = "Balik profil lebar"
"Flow chart" = "Diagram alir"
"Flow starting point" = "Titik awal alur"
"Flow starting points" = "Titik awal alur"
"Flows" = "Alur"
@@ -685,6 +689,7 @@ status = "draft — needs native review"
"Jun" = "Jun"
"Just now" = "Baru saja"
"Justify" = "Rata kiri-kanan"
"Kanban board" = "Papan kanban"
"Keep a measurement on the canvas for developers" = "Simpan pengukuran di kanvas untuk developer"
"Keep appearance" = "Pertahankan tampilan"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Pertahankan tampilan mengonversi setiap nilai warna ke {profile} agar warna terlihat sama. Pertahankan nilai membiarkan angkanya apa adanya, sehingga warna bergeser."
@@ -1080,6 +1085,7 @@ status = "draft — needs native review"
"Remove all" = "Hapus semua"
"Remove annotation" = "Hapus anotasi"
"Remove auto layout" = "Hapus auto layout"
"Remove column" = "Hapus kolom"
"Remove dev resource" = "Hapus sumber daya dev"
"Remove export setting" = "Hapus pengaturan ekspor"
"Remove from recents" = "Hapus dari terbaru"
@@ -1093,6 +1099,7 @@ status = "draft — needs native review"
"Remove min and max height" = "Hapus tinggi min dan maks"
"Remove min and max width" = "Hapus lebar min dan maks"
"Remove regions" = "Hapus area"
"Remove row" = "Hapus baris"
"Remove slot" = "Hapus slot"
"Remove starting point" = "Hapus titik awal"
"Remove stop" = "Hapus stop"
@@ -1144,6 +1151,7 @@ status = "draft — needs native review"
"Restore main component" = "Pulihkan main component"
"Restore this version" = "Pulihkan versi ini"
"Restored “{name}”" = "“{name}” dipulihkan"
"Retrospective" = "Retrospektif"
"Results" = "Hasil"
"Resume" = "Lanjutkan"
"Reverse the order" = "Balik urutan"
@@ -1519,6 +1527,7 @@ status = "draft — needs native review"
"WebSocket URL" = "URL WebSocket"
"WebSocket URL copied" = "URL WebSocket disalin"
"Wedge" = "Baji"
"Weekly planner" = "Perencana mingguan"
"Weight" = "Ketebalan"
"What you try here changes this preview and the code below, not the file" = "Yang Anda coba di sini mengubah pratinjau ini dan kode di bawah, bukan file-nya"
"What changed since it was marked ready for dev" = "Yang berubah sejak ditandai siap untuk dev"
@@ -1803,6 +1812,10 @@ status = "draft — needs native review"
"board.shape" = "Bentuk dengan teks"
"board.stamp" = "Stempel"
"board.sticky" = "Catatan tempel"
"board.table" = "Tabel"
"board.tableEdit" = "Edit tabel"
"board.template" = "Templat papan"
"board.templates" = "Daftar templat papan"
"board.timer" = "Pengatur waktu"
"board.timerState" = "Status pengatur waktu"
"board.vote" = "Sesi pemungutan suara"
+13
View File
@@ -47,6 +47,7 @@ status = "draft — needs native review"
"Add another condition" = "条件を追加"
"Add auto layout" = "オートレイアウトを追加"
"Add background behind overlay" = "オーバーレイの背後に背景を追加"
"Add column" = "列を追加"
"Add content to this slot" = "このスロットにコンテンツを追加"
"Add dev resource" = "開発リソースを追加"
"Add effect" = "エフェクトを追加"
@@ -66,6 +67,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "変数 JSON からコレクションを追加・更新"
"Add properties to make instances configurable" = "プロパティを追加してインスタンスを設定可能にする"
"Add reaction" = "リアクションを追加"
"Add row" = "行を追加"
"Add starting point" = "開始点を追加"
"Add stroke" = "線を追加"
"Add variant" = "バリアントを追加"
@@ -184,6 +186,7 @@ status = "draft — needs native review"
"Bottom left radius" = "左下の半径"
"Bottom right radius" = "右下の半径"
"Bound to {name}. Click to detach the variable." = "「{name}」を適用中。クリックで変数の割り当てを解除。"
"Brainstorm" = "ブレインストーミング"
"Branch" = "ブランチ"
"Branch archived" = "ブランチをアーカイブ済み"
"Branch merged" = "ブランチをマージ済み"
@@ -538,6 +541,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "テキストを反対側に反転"
"Flip vertical" = "上下反転"
"Flip width profile" = "幅プロファイルを反転"
"Flow chart" = "フローチャート"
"Flow starting point" = "フローの開始点"
"Flow starting points" = "フローの開始点"
"Flows" = "フロー"
@@ -692,6 +696,7 @@ status = "draft — needs native review"
"Jun" = "6月"
"Just now" = "たった今"
"Justify" = "両端揃え"
"Kanban board" = "カンバンボード"
"Keep a measurement on the canvas for developers" = "開発者向けに測定をキャンバスに残す"
"Keep appearance" = "見た目を保持"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "「見た目を保持」はすべてのカラー値を {profile} に変換し、色の見た目を保つ。「値を保持」は数値をそのまま残すため、色が変わる。"
@@ -1088,6 +1093,7 @@ status = "draft — needs native review"
"Remove all" = "すべて削除"
"Remove annotation" = "注釈を削除"
"Remove auto layout" = "オートレイアウトを解除"
"Remove column" = "列を削除"
"Remove dev resource" = "開発リソースを削除"
"Remove export setting" = "書き出し設定を削除"
"Remove from recents" = "最近使ったファイルから削除"
@@ -1101,6 +1107,7 @@ status = "draft — needs native review"
"Remove min and max height" = "最小と最大の高さを削除"
"Remove min and max width" = "最小と最大の幅を削除"
"Remove regions" = "領域を削除"
"Remove row" = "行を削除"
"Remove slot" = "スロットを削除"
"Remove starting point" = "開始点を削除"
"Remove stop" = "カラー分岐点を削除"
@@ -1154,6 +1161,7 @@ status = "draft — needs native review"
"Restore main component" = "メインコンポーネントを復元"
"Restore this version" = "このバージョンを復元"
"Restored “{name}”" = "「{name}」を復元済み"
"Retrospective" = "振り返り"
"Results" = "結果"
"Resume" = "再開"
"Reverse the order" = "順序を反転"
@@ -1536,6 +1544,7 @@ status = "draft — needs native review"
"WebSocket URL copied" = "WebSocket URL をコピー済み"
"Websites" = "Webサイト"
"Wedge" = "くさび"
"Weekly planner" = "週間プランナー"
"Weight" = "太さ"
"What changed since a saved version" = "保存したバージョンからの変更"
"What changed since it was marked ready for dev" = "開発準備完了にしてからの変更内容"
@@ -1822,6 +1831,10 @@ status = "draft — needs native review"
"board.shape" = "テキスト付きシェイプ"
"board.stamp" = "スタンプ"
"board.sticky" = "付箋"
"board.table" = "表"
"board.tableEdit" = "表を編集"
"board.template" = "ボードテンプレート"
"board.templates" = "ボードテンプレートの一覧"
"board.timer" = "タイマー"
"board.timerState" = "タイマーの状態"
"board.vote" = "投票セッション"
+13
View File
@@ -47,6 +47,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Add another condition" = "კიდევ ერთი პირობის დამატება"
"Add auto layout" = "ავტოგანლაგების დამატება"
"Add background behind overlay" = "ფონის დამატება გადაფარვის უკან"
"Add column" = "სვეტის დამატება"
"Add content to this slot" = "შიგთავსის დამატება ამ სლოტში"
"Add dev resource" = "დეველოპერის რესურსის დამატება"
"Add effect" = "ეფექტის დამატება"
@@ -66,6 +67,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Add or update collections from a variables JSON" = "კოლექციების დამატება ან განახლება ცვლადების JSON-იდან"
"Add properties to make instances configurable" = "დაამატეთ თვისებები, რომ ინსტანსები კონფიგურირებადი გახდეს"
"Add reaction" = "რეაქციის დამატება"
"Add row" = "მწკრივის დამატება"
"Add starting point" = "საწყისი წერტილის დამატება"
"Add stroke" = "კონტურის დამატება"
"Add variant" = "ვარიანტის დამატება"
@@ -185,6 +187,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Bottom" = "ქვემოთ"
"Bottom left radius" = "ქვედა მარცხენა რადიუსი"
"Bottom right radius" = "ქვედა მარჯვენა რადიუსი"
"Brainstorm" = "გონებრივი იერიში"
"Branch" = "განშტოება"
"Branch archived" = "განშტოება დაარქივებულია"
"Branch merged" = "განშტოება შერწყმულია"
@@ -544,6 +547,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Flip text to the other side" = "ტექსტის მეორე მხარეს გადატანა"
"Flip vertical" = "ვერტიკალურად არეკვლა"
"Flip width profile" = "სიგანის პროფილის არეკვლა"
"Flow chart" = "ბლოკ-სქემა"
"Flow starting point" = "ნაკადის საწყისი წერტილი"
"Flow starting points" = "ნაკადის საწყისი წერტილები"
"Flows" = "ნაკადები"
@@ -698,6 +702,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Jun" = "ივნ"
"Just now" = "ახლახან"
"Justify" = "ორმხრივი სწორება"
"Kanban board" = "კანბან დაფა"
"Keep a measurement on the canvas for developers" = "გაზომვის შენახვა ტილოზე დეველოპერებისთვის"
"Keep appearance" = "იერსახის შენარჩუნება"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "„იერსახის შენარჩუნება“ ყველა ფერის მნიშვნელობას {profile}-ში გარდაქმნის, რომ ფერები იგივენაირად გამოჩნდეს. „მნიშვნელობების შენარჩუნება“ რიცხვებს უცვლელად ტოვებს, ამიტომ ფერები იცვლება."
@@ -1099,6 +1104,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Remove all" = "ყველას წაშლა"
"Remove annotation" = "ანოტაციის წაშლა"
"Remove auto layout" = "ავტოგანლაგების მოხსნა"
"Remove column" = "სვეტის წაშლა"
"Remove dev resource" = "დეველოპერის რესურსის წაშლა"
"Remove export setting" = "ექსპორტის პარამეტრის წაშლა"
"Remove from recents" = "ბოლოდროინდელებიდან ამოშლა"
@@ -1112,6 +1118,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Remove min and max height" = "მინ. და მაქს. სიმაღლის მოხსნა"
"Remove min and max width" = "მინ. და მაქს. სიგანის მოხსნა"
"Remove regions" = "არეების წაშლა"
"Remove row" = "მწკრივის წაშლა"
"Remove slot" = "სლოტის მოხსნა"
"Remove starting point" = "საწყისი წერტილის წაშლა"
"Remove stop" = "გადასვლის წერტილის წაშლა"
@@ -1164,6 +1171,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Restore main component" = "მთავარი კომპონენტის აღდგენა"
"Restore this version" = "ამ ვერსიის აღდგენა"
"Restored “{name}”" = "„{name}“ აღდგა"
"Retrospective" = "რეტროსპექტივა"
"Results" = "შედეგები"
"Resume" = "გაგრძელება"
"Reverse the order" = "რიგის შებრუნება"
@@ -1550,6 +1558,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"WebSocket URL" = "WebSocket URL"
"WebSocket URL copied" = "WebSocket URL დაკოპირდა"
"Wedge" = "სოლი"
"Weekly planner" = "კვირის გეგმა"
"Weight" = "სისქე"
"What you try here changes this preview and the code below, not the file" = "რასაც აქ ცდით, ცვლის ამ გადახედვას და ქვემოთ კოდს და არა ფაილს"
"What changed since it was marked ready for dev" = "რა შეიცვალა მზად დეველოპერისთვის მონიშვნის შემდეგ"
@@ -1837,6 +1846,10 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"board.shape" = "ფიგურა ტექსტით"
"board.stamp" = "შტამპი"
"board.sticky" = "სტიკერი"
"board.table" = "ცხრილი"
"board.tableEdit" = "ცხრილის რედაქტირება"
"board.template" = "დაფის შაბლონი"
"board.templates" = "დაფის შაბლონების სია"
"board.timer" = "ტაიმერი"
"board.timerState" = "ტაიმერის მდგომარეობა"
"board.vote" = "ხმის მიცემის სესია"
+14
View File
@@ -43,6 +43,7 @@ status = "draft — needs native review"
"Add another condition" = "조건 추가"
"Add auto layout" = "자동 레이아웃 추가"
"Add background behind overlay" = "오버레이 뒤에 배경 추가"
"Add column" = "열 추가"
"Add content to this slot" = "이 슬롯에 콘텐츠 추가"
"Add dev resource" = "개발 리소스 추가"
"Add effect" = "효과 추가"
@@ -62,6 +63,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "변수 JSON에서 컬렉션 추가 또는 업데이트"
"Add properties to make instances configurable" = "속성을 추가해 인스턴스를 설정 가능하게 만들기"
"Add reaction" = "반응 추가"
"Add row" = "행 추가"
"Add starting point" = "시작점 추가"
"Add stroke" = "선 추가"
"Add variant" = "배리언트 추가"
@@ -174,6 +176,7 @@ status = "draft — needs native review"
"Bottom" = "아래쪽"
"Bottom left radius" = "왼쪽 아래 반경"
"Bottom right radius" = "오른쪽 아래 반경"
"Brainstorm" = "브레인스토밍"
"Branch" = "브랜치"
"Branch archived" = "브랜치 보관됨"
"Branch merged" = "브랜치 병합됨"
@@ -517,6 +520,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "텍스트를 반대쪽으로 뒤집기"
"Flip vertical" = "세로로 뒤집기"
"Flip width profile" = "너비 프로필 뒤집기"
"Flow chart" = "순서도"
"Flow starting point" = "플로우 시작점"
"Flow starting points" = "플로우 시작점"
"Flows" = "플로우"
@@ -661,6 +665,7 @@ status = "draft — needs native review"
"Join|stroke" = "선 연결"
"Just now" = "방금"
"Justify" = "양쪽 정렬"
"Kanban board" = "칸반 보드"
"Keep a measurement on the canvas for developers" = "개발자를 위해 캔버스에 측정 유지"
"Keep appearance" = "모양 유지"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "\"모양 유지\"는 모든 색상 값을 {profile}(으)로 변환해 색이 똑같아 보이게 함. \"값 유지\"는 숫자를 그대로 두므로 색이 달라짐."
@@ -1039,6 +1044,7 @@ status = "draft — needs native review"
"Remove all" = "모두 제거"
"Remove annotation" = "주석 제거"
"Remove auto layout" = "자동 레이아웃 제거"
"Remove column" = "열 제거"
"Remove dev resource" = "개발 리소스 제거"
"Remove export setting" = "내보내기 설정 제거"
"Remove from server" = "서버에서 제거"
@@ -1051,6 +1057,7 @@ status = "draft — needs native review"
"Remove min and max height" = "최소 및 최대 높이 제거"
"Remove min and max width" = "최소 및 최대 너비 제거"
"Remove regions" = "영역 제거"
"Remove row" = "행 제거"
"Remove slot" = "슬롯 제거"
"Remove starting point" = "시작점 제거"
"Remove stop" = "정지점 제거"
@@ -1104,6 +1111,7 @@ status = "draft — needs native review"
"Restore main component" = "메인 컴포넌트 복원"
"Restore this version" = "이 버전 복원"
"Restored “{name}”" = "\"{name}\" 복원됨"
"Retrospective" = "회고"
"Results" = "결과"
"Resume" = "재개"
"Reverse the order" = "순서 뒤집기"
@@ -1323,6 +1331,7 @@ status = "draft — needs native review"
"Tell the server's operator what is wrong with it" = "무엇이 문제인지 서버 운영자에게 알리기"
"Temperature" = "색온도"
"Template" = "템플릿"
"Templates" = "템플릿"
"Terms" = "약관"
"Text" = "텍스트"
"Text align" = "텍스트 정렬"
@@ -1466,6 +1475,7 @@ status = "draft — needs native review"
"WebSocket URL" = "WebSocket URL"
"WebSocket URL copied" = "WebSocket URL 복사됨"
"Wedge" = "쐐기"
"Weekly planner" = "주간 계획표"
"Weight" = "굵기"
"What changed since a saved version" = "저장된 버전 이후 변경된 내용"
"What changed since it was marked ready for dev" = "개발 준비 완료로 표시된 뒤 변경된 내용"
@@ -1746,6 +1756,10 @@ status = "draft — needs native review"
"board.shape" = "텍스트가 있는 도형"
"board.stamp" = "스탬프"
"board.sticky" = "스티커 메모"
"board.table" = "표"
"board.tableEdit" = "표 편집"
"board.template" = "보드 템플릿"
"board.templates" = "보드 템플릿 목록"
"board.timer" = "타이머"
"board.timerState" = "타이머 상태"
"board.vote" = "투표 세션"
+13
View File
@@ -46,6 +46,7 @@ status = "draft — needs native review"
"Add another condition" = "Nog een voorwaarde toevoegen"
"Add auto layout" = "Auto layout toevoegen"
"Add background behind overlay" = "Achtergrond achter overlay toevoegen"
"Add column" = "Kolom toevoegen"
"Add content to this slot" = "Voeg inhoud toe aan dit slot"
"Add dev resource" = "Dev-resource toevoegen"
"Add effect" = "Effect toevoegen"
@@ -65,6 +66,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "Voeg verzamelingen toe of werk ze bij vanuit een variables-JSON"
"Add properties to make instances configurable" = "Voeg eigenschappen toe om instances configureerbaar te maken"
"Add reaction" = "Reactie toevoegen"
"Add row" = "Rij toevoegen"
"Add starting point" = "Startpunt toevoegen"
"Add stroke" = "Lijn toevoegen"
"Add variant" = "Variant toevoegen"
@@ -185,6 +187,7 @@ status = "draft — needs native review"
"Bottom left radius" = "Radius linksonder"
"Bottom right radius" = "Radius rechtsonder"
"Bound to {name}. Click to detach the variable." = "Gekoppeld aan {name}. Klik om de variabele los te koppelen."
"Brainstorm" = "Brainstorm"
"Branch" = "Branch"
"Branch archived" = "Branch gearchiveerd"
"Branch merged" = "Branch samengevoegd"
@@ -544,6 +547,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "Tekst naar de andere kant spiegelen"
"Flip vertical" = "Verticaal spiegelen"
"Flip width profile" = "Breedteprofiel spiegelen"
"Flow chart" = "Stroomdiagram"
"Flow starting point" = "Flow-startpunt"
"Flow starting points" = "Flow-startpunten"
"Flows" = "Flows"
@@ -698,6 +702,7 @@ status = "draft — needs native review"
"Jun" = "jun."
"Just now" = "Zojuist"
"Justify" = "Uitvullen"
"Kanban board" = "Kanbanbord"
"Keep a measurement on the canvas for developers" = "Een meting voor ontwikkelaars op het canvas houden"
"Keep appearance" = "Uiterlijk behouden"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Kleur behouden converteert elke kleurwaarde naar {profile}, zodat kleuren er hetzelfde uitzien. Waarden behouden laat de getallen staan, waardoor kleuren verschuiven."
@@ -1101,6 +1106,7 @@ status = "draft — needs native review"
"Remove all" = "Alles verwijderen"
"Remove annotation" = "Annotatie verwijderen"
"Remove auto layout" = "Auto layout verwijderen"
"Remove column" = "Kolom verwijderen"
"Remove dev resource" = "Dev-resource verwijderen"
"Remove export setting" = "Exportinstelling verwijderen"
"Remove from recents" = "Verwijderen uit onlangs geopend"
@@ -1114,6 +1120,7 @@ status = "draft — needs native review"
"Remove min and max height" = "Minimale en maximale hoogte verwijderen"
"Remove min and max width" = "Minimale en maximale breedte verwijderen"
"Remove regions" = "Gebieden verwijderen"
"Remove row" = "Rij verwijderen"
"Remove slot" = "Vak verwijderen"
"Remove starting point" = "Startpunt verwijderen"
"Remove stop" = "Stop verwijderen"
@@ -1166,6 +1173,7 @@ status = "draft — needs native review"
"Restore main component" = "Hoofdcomponent terugzetten"
"Restore this version" = "Deze versie terugzetten"
"Restored “{name}”" = "“{name}” teruggezet"
"Retrospective" = "Retrospectief"
"Results" = "Resultaten"
"Resume" = "Hervatten"
"Reverse the order" = "Volgorde omdraaien"
@@ -1553,6 +1561,7 @@ status = "draft — needs native review"
"WebSocket URL copied" = "WebSocket-URL gekopieerd"
"Websites" = "Websites"
"Wedge" = "Wedge"
"Weekly planner" = "Weekplanner"
"Weight" = "Dikte"
"What changed since it was marked ready for dev" = "Wat er veranderd is sinds het is gemarkeerd als klaar voor Dev Mode"
"What is wrong with this file? The server's operator gets your report." = "Wat klopt er niet aan dit bestand? Je melding gaat naar de beheerder van de server."
@@ -1840,6 +1849,10 @@ status = "draft — needs native review"
"board.shape" = "Vorm met tekst"
"board.stamp" = "Stempel"
"board.sticky" = "Plakbriefje"
"board.table" = "Tabel"
"board.tableEdit" = "Tabel bewerken"
"board.template" = "Whiteboardsjabloon"
"board.templates" = "Whiteboardsjablonen weergeven"
"board.timer" = "Timer"
"board.timerState" = "Timerstatus"
"board.vote" = "Stemronde"
+13
View File
@@ -44,6 +44,7 @@ name = "Polski"
"Add another condition" = "Dodaj kolejny warunek"
"Add auto layout" = "Dodaj Auto layout"
"Add background behind overlay" = "Dodaj tło za nakładką"
"Add column" = "Dodaj kolumnę"
"Add content to this slot" = "Dodaj zawartość do tego gniazda"
"Add dev resource" = "Dodaj zasób programistyczny"
"Add effect" = "Dodaj efekt"
@@ -63,6 +64,7 @@ name = "Polski"
"Add or update collections from a variables JSON" = "Dodaj lub zaktualizuj kolekcje z pliku JSON ze zmiennymi"
"Add properties to make instances configurable" = "Dodaj właściwości, aby umożliwić konfigurację instancji"
"Add reaction" = "Dodaj reakcję"
"Add row" = "Dodaj wiersz"
"Add starting point" = "Dodaj punkt startowy"
"Add stroke" = "Dodaj obrys"
"Add variant" = "Dodaj wariant"
@@ -183,6 +185,7 @@ name = "Polski"
"Bottom left radius" = "Promień lewego dolnego narożnika"
"Bottom padding" = "Margines dolny"
"Bottom right radius" = "Promień prawego dolnego narożnika"
"Brainstorm" = "Burza mózgów"
"Branch" = "Gałąź"
"Branch archived" = "Gałąź zarchiwizowana"
"Branch merged" = "Gałąź scalona"
@@ -543,6 +546,7 @@ name = "Polski"
"Flip text to the other side" = "Przerzuć tekst na drugą stronę"
"Flip vertical" = "Odbij w pionie"
"Flip width profile" = "Odwróć profil szerokości"
"Flow chart" = "Schemat blokowy"
"Flow starting point" = "Punkt startowy przepływu"
"Flow starting points" = "Punkty startowe przepływu"
"Flows" = "Przepływy"
@@ -699,6 +703,7 @@ name = "Polski"
"Jun" = "Cze"
"Just now" = "Przed chwilą"
"Justify" = "Wyjustuj"
"Kanban board" = "Tablica kanban"
"Keep a measurement on the canvas for developers" = "Zachowaj pomiar na płótnie dla programistów"
"Keep appearance" = "Zachowaj wygląd"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Zachowanie wyglądu konwertuje każdą wartość koloru na {profile}, aby kolory wyglądały tak samo. Zachowanie wartości pozostawia liczby bez zmian, co może zmienić odcienie barw."
@@ -1104,6 +1109,7 @@ name = "Polski"
"Remove all" = "Usuń wszystko"
"Remove annotation" = "Usuń adnotację"
"Remove auto layout" = "Usuń Auto layout"
"Remove column" = "Usuń kolumnę"
"Remove dev resource" = "Usuń zasób programistyczny"
"Remove export setting" = "Usuń ustawienie eksportu"
"Remove from recents" = "Usuń z ostatnich"
@@ -1117,6 +1123,7 @@ name = "Polski"
"Remove min and max height" = "Usuń minimalną i maksymalną wysokość"
"Remove min and max width" = "Usuń minimalną i maksymalną szerokość"
"Remove regions" = "Usuń obszary"
"Remove row" = "Usuń wiersz"
"Remove slot" = "Usuń gniazdo"
"Remove starting point" = "Usuń punkt startowy"
"Remove stop" = "Usuń punkt"
@@ -1170,6 +1177,7 @@ name = "Polski"
"Restore main component" = "Przywróć główny komponent"
"Restore this version" = "Przywróć tę wersję"
"Restored “{name}”" = "Przywrócono „{name}”"
"Retrospective" = "Retrospektywa"
"Results" = "Wyniki"
"Resume" = "Wznów"
"Reverse the order" = "Odwróć kolejność"
@@ -1563,6 +1571,7 @@ name = "Polski"
"WebSocket URL" = "Adres WebSocket URL"
"WebSocket URL copied" = "Skopiowano adres WebSocket URL"
"Wedge" = "Klin"
"Weekly planner" = "Plan tygodnia"
"Weight" = "Grubość"
"What you try here changes this preview and the code below, not the file" = "To, co wypróbujesz tutaj, zmienia ten podgląd i kod poniżej, a nie plik"
"What changed since it was marked ready for dev" = "Co się zmieniło od czasu oznaczenia jako gotowe do wdrożenia"
@@ -1853,6 +1862,10 @@ name = "Polski"
"board.shape" = "Kształt z tekstem"
"board.stamp" = "Pieczątka"
"board.sticky" = "Karteczka"
"board.table" = "Tabela"
"board.tableEdit" = "Edytuj tabelę"
"board.template" = "Szablon tablicy"
"board.templates" = "Lista szablonów tablicy"
"board.timer" = "Minutnik"
"board.timerState" = "Stan minutnika"
"board.vote" = "Głosowanie"
+13
View File
@@ -41,6 +41,7 @@ status = "draft — needs native review"
"Add another condition" = "Adicionar outra condição"
"Add auto layout" = "Adicionar layout automático"
"Add background behind overlay" = "Adicionar fundo atrás da sobreposição"
"Add column" = "Adicionar coluna"
"Add content to this slot" = "Adicionar conteúdo a este slot"
"Add dev resource" = "Adicionar recurso de desenvolvimento"
"Add effect" = "Adicionar efeito"
@@ -60,6 +61,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "Adicionar ou atualizar coleções a partir de um JSON de variáveis"
"Add properties to make instances configurable" = "Adicione propriedades para tornar as instâncias configuráveis"
"Add reaction" = "Adicionar reação"
"Add row" = "Adicionar linha"
"Add starting point" = "Adicionar ponto de partida"
"Add stroke" = "Adicionar traço"
"Add variant" = "Adicionar variante"
@@ -172,6 +174,7 @@ status = "draft — needs native review"
"Bottom" = "Embaixo"
"Bottom left radius" = "Raio inferior esquerdo"
"Bottom right radius" = "Raio inferior direito"
"Brainstorm" = "Brainstorming"
"Branch" = "Branch"
"Branch archived" = "Branch arquivada"
"Branch merged" = "Branch mesclada"
@@ -516,6 +519,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "Passar o texto para o outro lado"
"Flip vertical" = "Inverter verticalmente"
"Flip width profile" = "Inverter perfil de largura"
"Flow chart" = "Fluxograma"
"Flow starting point" = "Ponto de partida do fluxo"
"Flow starting points" = "Pontos de partida de fluxo"
"Flows" = "Fluxos"
@@ -660,6 +664,7 @@ status = "draft — needs native review"
"Join|stroke" = "Junção"
"Just now" = "Agora mesmo"
"Justify" = "Justificar"
"Kanban board" = "Quadro kanban"
"Keep a measurement on the canvas for developers" = "Manter uma medição no canvas para desenvolvedores"
"Keep appearance" = "Manter aparência"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Manter aparência converte cada valor de cor para {profile} para que as cores pareçam iguais. Manter valores deixa os números como estão, então as cores mudam."
@@ -1038,6 +1043,7 @@ status = "draft — needs native review"
"Remove all" = "Remover tudo"
"Remove annotation" = "Remover anotação"
"Remove auto layout" = "Remover layout automático"
"Remove column" = "Remover coluna"
"Remove dev resource" = "Remover recurso de desenvolvimento"
"Remove export setting" = "Remover configuração de exportação"
"Remove from server" = "Remover do servidor"
@@ -1050,6 +1056,7 @@ status = "draft — needs native review"
"Remove min and max height" = "Remover altura mín. e máx."
"Remove min and max width" = "Remover largura mín. e máx."
"Remove regions" = "Remover regiões"
"Remove row" = "Remover linha"
"Remove slot" = "Remover slot"
"Remove starting point" = "Remover ponto de partida"
"Remove stop" = "Remover parada"
@@ -1103,6 +1110,7 @@ status = "draft — needs native review"
"Restore main component" = "Restaurar componente principal"
"Restore this version" = "Restaurar esta versão"
"Restored “{name}”" = "“{name}” restaurada"
"Retrospective" = "Retrospectiva"
"Results" = "Resultados"
"Resume" = "Retomar"
"Reverse the order" = "Inverter a ordem"
@@ -1465,6 +1473,7 @@ status = "draft — needs native review"
"WebSocket URL" = "URL do WebSocket"
"WebSocket URL copied" = "URL do WebSocket copiada"
"Wedge" = "Cunha"
"Weekly planner" = "Planejador semanal"
"Weight" = "Espessura"
"What changed since a saved version" = "O que mudou desde uma versão salva"
"What changed since it was marked ready for dev" = "O que mudou desde que foi marcado como pronto para dev"
@@ -1859,6 +1868,10 @@ status = "draft — needs native review"
"board.shape" = "Forma com texto"
"board.stamp" = "Carimbo"
"board.sticky" = "Nota adesiva"
"board.table" = "Tabela"
"board.tableEdit" = "Editar tabela"
"board.template" = "Modelo de quadro"
"board.templates" = "Listar modelos de quadro"
"board.timer" = "Timer"
"board.timerState" = "Estado do timer"
"board.vote" = "Sessão de votação"
+13
View File
@@ -44,6 +44,7 @@ name = "Русский"
"Add another condition" = "Добавить ещё одно условие"
"Add auto layout" = "Добавить автолейаут"
"Add background behind overlay" = "Добавить фон за оверлеем"
"Add column" = "Добавить столбец"
"Add content to this slot" = "Добавить содержимое в этот слот"
"Add dev resource" = "Добавить ресурс для разработки"
"Add effect" = "Добавить эффект"
@@ -63,6 +64,7 @@ name = "Русский"
"Add or update collections from a variables JSON" = "Добавить или обновить коллекции из JSON переменных"
"Add properties to make instances configurable" = "Добавьте свойства, чтобы настраивать экземпляры"
"Add reaction" = "Добавить реакцию"
"Add row" = "Добавить строку"
"Add starting point" = "Добавить начальную точку"
"Add stroke" = "Добавить обводку"
"Add variant" = "Добавить вариант"
@@ -183,6 +185,7 @@ name = "Русский"
"Bottom left radius" = "Радиус нижнего левого угла"
"Bottom padding" = "Нижний внутренний отступ"
"Bottom right radius" = "Радиус нижнего правого угла"
"Brainstorm" = "Мозговой штурм"
"Branch" = "Ветка"
"Branch archived" = "Ветка архивирована"
"Branch merged" = "Ветка объединена"
@@ -543,6 +546,7 @@ name = "Русский"
"Flip text to the other side" = "Перенести текст на другую сторону"
"Flip vertical" = "Отразить по вертикали"
"Flip width profile" = "Отразить профиль ширины"
"Flow chart" = "Блок-схема"
"Flow starting point" = "Начальная точка сценария"
"Flow starting points" = "Начальные точки сценариев"
"Flows" = "Сценарии"
@@ -699,6 +703,7 @@ name = "Русский"
"Jun" = "июн."
"Just now" = "Только что"
"Justify" = "По ширине"
"Kanban board" = "Канбан-доска"
"Keep a measurement on the canvas for developers" = "Оставить измерение на холсте для разработчиков"
"Keep appearance" = "Сохранить внешний вид"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "«Сохранить внешний вид» преобразует все значения цветов в {profile}, сохраняя их вид. «Сохранить значения» оставляет числа без изменений, поэтому цвета меняются."
@@ -1104,6 +1109,7 @@ name = "Русский"
"Remove all" = "Удалить всё"
"Remove annotation" = "Удалить аннотацию"
"Remove auto layout" = "Убрать автолейаут"
"Remove column" = "Удалить столбец"
"Remove dev resource" = "Удалить ресурс для разработки"
"Remove export setting" = "Удалить настройку экспорта"
"Remove from recents" = "Убрать из недавних"
@@ -1117,6 +1123,7 @@ name = "Русский"
"Remove min and max height" = "Убрать минимальную и максимальную высоту"
"Remove min and max width" = "Убрать минимальную и максимальную ширину"
"Remove regions" = "Удалить области"
"Remove row" = "Удалить строку"
"Remove slot" = "Убрать слот"
"Remove starting point" = "Убрать начальную точку"
"Remove stop" = "Удалить точку градиента"
@@ -1170,6 +1177,7 @@ name = "Русский"
"Restore main component" = "Восстановить основной компонент"
"Restore this version" = "Восстановить эту версию"
"Restored “{name}”" = "«{name}» восстановлено"
"Retrospective" = "Ретроспектива"
"Results" = "Результаты"
"Resume" = "Продолжить"
"Reverse the order" = "Обратить порядок"
@@ -1563,6 +1571,7 @@ name = "Русский"
"WebSocket URL" = "URL WebSocket"
"WebSocket URL copied" = "URL WebSocket скопирован"
"Wedge" = "Клин"
"Weekly planner" = "План на неделю"
"Weight" = "Толщина"
"What you try here changes this preview and the code below, not the file" = "Здесь меняются только предпросмотр и код ниже; файл остаётся без изменений"
"What changed since it was marked ready for dev" = "Изменения после отметки «Готово к разработке»"
@@ -1853,6 +1862,10 @@ name = "Русский"
"board.shape" = "Фигура с текстом"
"board.stamp" = "Штамп"
"board.sticky" = "Стикер"
"board.table" = "Таблица"
"board.tableEdit" = "Изменить таблицу"
"board.template" = "Шаблон доски"
"board.templates" = "Список шаблонов доски"
"board.timer" = "Таймер"
"board.timerState" = "Состояние таймера"
"board.vote" = "Голосование"
+13
View File
@@ -44,6 +44,7 @@ name = "Svenska"
"Add another condition" = "Lägg till ett villkor till"
"Add auto layout" = "Lägg till auto layout"
"Add background behind overlay" = "Lägg till bakgrund bakom överlägg"
"Add column" = "Lägg till kolumn"
"Add content to this slot" = "Lägg till innehåll i facket"
"Add dev resource" = "Lägg till utvecklarresurs"
"Add effect" = "Lägg till effekt"
@@ -63,6 +64,7 @@ name = "Svenska"
"Add or update collections from a variables JSON" = "Lägg till eller uppdatera samlingar från en variabel-JSON"
"Add properties to make instances configurable" = "Lägg till egenskaper för att göra instanser konfigurerbara"
"Add reaction" = "Lägg till reaktion"
"Add row" = "Lägg till rad"
"Add starting point" = "Lägg till startpunkt"
"Add stroke" = "Lägg till kontur"
"Add variant" = "Lägg till variant"
@@ -183,6 +185,7 @@ name = "Svenska"
"Bottom left radius" = "Radie nere till vänster"
"Bottom padding" = "Nedre padding"
"Bottom right radius" = "Radie nere till höger"
"Brainstorm" = "Idéstorm"
"Branch" = "Gren"
"Branch archived" = "Grenen har arkiverats"
"Branch merged" = "Grenen har sammanfogats"
@@ -543,6 +546,7 @@ name = "Svenska"
"Flip text to the other side" = "Vänd texten till andra sidan"
"Flip vertical" = "Vänd lodrätt"
"Flip width profile" = "Vänd breddprofil"
"Flow chart" = "Flödesschema"
"Flow starting point" = "Startpunkt för flöde"
"Flow starting points" = "Startpunkter för flöden"
"Flows" = "Flöden"
@@ -699,6 +703,7 @@ name = "Svenska"
"Jun" = "jun"
"Just now" = "Nyss"
"Justify" = "Marginaljustera"
"Kanban board" = "Kanbantavla"
"Keep a measurement on the canvas for developers" = "Behåll ett mått på arbetsytan för utvecklare"
"Keep appearance" = "Behåll utseende"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Behåll utseende konverterar varje färgvärde till {profile} så att färgerna ser likadana ut. Behåll värden lämnar siffrorna som de är, så färgerna förskjuts."
@@ -1104,6 +1109,7 @@ name = "Svenska"
"Remove all" = "Ta bort alla"
"Remove annotation" = "Ta bort anteckning"
"Remove auto layout" = "Ta bort auto layout"
"Remove column" = "Ta bort kolumn"
"Remove dev resource" = "Ta bort utvecklarresurs"
"Remove export setting" = "Ta bort exportinställning"
"Remove from recents" = "Ta bort från Senaste"
@@ -1117,6 +1123,7 @@ name = "Svenska"
"Remove min and max height" = "Ta bort min- och maxhöjd"
"Remove min and max width" = "Ta bort min- och maxbredd"
"Remove regions" = "Ta bort områden"
"Remove row" = "Ta bort rad"
"Remove slot" = "Ta bort plats"
"Remove starting point" = "Ta bort startpunkt"
"Remove stop" = "Ta bort stopp"
@@ -1170,6 +1177,7 @@ name = "Svenska"
"Restore main component" = "Återställ huvudkomponent"
"Restore this version" = "Återställ den här versionen"
"Restored “{name}”" = "Återställde ”{name}”"
"Retrospective" = "Retrospektiv"
"Results" = "Resultat"
"Resume" = "Återuppta"
"Reverse the order" = "Vänd på ordningen"
@@ -1563,6 +1571,7 @@ name = "Svenska"
"WebSocket URL" = "WebSocket-URL"
"WebSocket URL copied" = "WebSocket-URL kopierad"
"Wedge" = "Kil"
"Weekly planner" = "Veckoplanering"
"Weight" = "Tjocklek"
"What you try here changes this preview and the code below, not the file" = "Det du provar här ändrar den här förhandsgranskningen och koden nedan, inte filen"
"What changed since it was marked ready for dev" = "Vad som har ändrats sedan den markerades som klar för utveckling"
@@ -1854,6 +1863,10 @@ name = "Svenska"
"board.shape" = "Form med text"
"board.stamp" = "Stämpel"
"board.sticky" = "Lapp"
"board.table" = "Tabell"
"board.tableEdit" = "Redigera tabell"
"board.template" = "Whiteboardmall"
"board.templates" = "Lista whiteboardmallar"
"board.timer" = "Timer"
"board.timerState" = "Timerstatus"
"board.vote" = "Omröstning"
+13
View File
@@ -45,6 +45,7 @@ name = "Türkçe"
"Add another condition" = "Başka bir koşul ekle"
"Add auto layout" = "Otomatik yerleşim ekle"
"Add background behind overlay" = "Bindirmenin arkasına arka plan ekle"
"Add column" = "Sütun ekle"
"Add content to this slot" = "Bu yuvaya içerik ekle"
"Add dev resource" = "Geliştirici kaynağı ekle"
"Add effect" = "Efekt ekle"
@@ -64,6 +65,7 @@ name = "Türkçe"
"Add or update collections from a variables JSON" = "Bir değişken JSON'undan koleksiyon ekleyin veya güncelleyin"
"Add properties to make instances configurable" = "Örnekleri yapılandırılabilir kılmak için özellik ekleyin"
"Add reaction" = "Tepki ekle"
"Add row" = "Satır ekle"
"Add starting point" = "Başlangıç noktası ekle"
"Add stroke" = "Kontur ekle"
"Add variant" = "Varyant ekle"
@@ -181,6 +183,7 @@ name = "Türkçe"
"Bottom" = "Alt"
"Bottom left radius" = "Sol alt yarıçap"
"Bottom right radius" = "Sağ alt yarıçap"
"Brainstorm" = "Beyin fırtınası"
"Branch" = "Dal"
"Branch archived" = "Dal arşivlendi"
"Branch merged" = "Dal birleştirildi"
@@ -532,6 +535,7 @@ name = "Türkçe"
"Flip text to the other side" = "Metni diğer tarafa çevir"
"Flip vertical" = "Dikey çevir"
"Flip width profile" = "Genişlik profilini çevir"
"Flow chart" = "Akış şeması"
"Flow starting point" = "Akış başlangıç noktası"
"Flow starting points" = "Akış başlangıç noktaları"
"Flows" = "Akışlar"
@@ -685,6 +689,7 @@ name = "Türkçe"
"Jun" = "Haz"
"Just now" = "Az önce"
"Justify" = "İki yana yasla"
"Kanban board" = "Kanban panosu"
"Keep a measurement on the canvas for developers" = "Geliştiriciler için tuvalde bir ölçüm tut"
"Keep appearance" = "Görünümü koru"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "Görünümü koru, renkler aynı görünsün diye her renk değerini {profile} profiline dönüştürür. Değerleri koru, sayıları olduğu gibi bırakır; bu yüzden renkler kayar."
@@ -1080,6 +1085,7 @@ name = "Türkçe"
"Remove all" = "Tümünü kaldır"
"Remove annotation" = "Ek açıklamayı kaldır"
"Remove auto layout" = "Otomatik yerleşimi kaldır"
"Remove column" = "Sütunu kaldır"
"Remove dev resource" = "Geliştirici kaynağını kaldır"
"Remove export setting" = "Dışa aktarma ayarını kaldır"
"Remove from recents" = "Son kullanılanlardan kaldır"
@@ -1093,6 +1099,7 @@ name = "Türkçe"
"Remove min and max height" = "En küçük ve en büyük yüksekliği kaldır"
"Remove min and max width" = "En küçük ve en büyük genişliği kaldır"
"Remove regions" = "Bölgeleri kaldır"
"Remove row" = "Satırı kaldır"
"Remove slot" = "Yuvayı kaldır"
"Remove starting point" = "Başlangıç noktasını kaldır"
"Remove stop" = "Durağı kaldır"
@@ -1144,6 +1151,7 @@ name = "Türkçe"
"Restore main component" = "Ana bileşeni geri yükle"
"Restore this version" = "Bu sürümü geri yükle"
"Restored “{name}”" = "“{name}” geri yüklendi"
"Retrospective" = "Retrospektif"
"Results" = "Sonuçlar"
"Resume" = "Sürdür"
"Reverse the order" = "Sırayı ters çevir"
@@ -1519,6 +1527,7 @@ name = "Türkçe"
"WebSocket URL" = "WebSocket URL'si"
"WebSocket URL copied" = "WebSocket URL'si kopyalandı"
"Wedge" = "Kama"
"Weekly planner" = "Haftalık planlayıcı"
"Weight" = "Kalınlık"
"What you try here changes this preview and the code below, not the file" = "Burada denedikleriniz dosyayı değil, bu önizlemeyi ve aşağıdaki kodu değiştirir"
"What changed since it was marked ready for dev" = "Geliştirmeye hazır olarak işaretlendiğinden beri neler değişti"
@@ -1803,6 +1812,10 @@ name = "Türkçe"
"board.shape" = "Metinli şekil"
"board.stamp" = "Damga"
"board.sticky" = "Yapışkan not"
"board.table" = "Tablo"
"board.tableEdit" = "Tabloyu düzenle"
"board.template" = "Pano şablonu"
"board.templates" = "Pano şablonlarını listele"
"board.timer" = "Zamanlayıcı"
"board.timerState" = "Zamanlayıcı durumu"
"board.vote" = "Oylama oturumu"
+14
View File
@@ -44,6 +44,7 @@ status = "draft — needs native review"
"Add another condition" = "添加其他条件"
"Add auto layout" = "添加自动布局"
"Add background behind overlay" = "在浮层后添加背景"
"Add column" = "添加列"
"Add content to this slot" = "向此插槽添加内容"
"Add dev resource" = "添加开发资源"
"Add effect" = "添加效果"
@@ -63,6 +64,7 @@ status = "draft — needs native review"
"Add or update collections from a variables JSON" = "从变量 JSON 添加或更新集合"
"Add properties to make instances configurable" = "添加属性,使实例可配置"
"Add reaction" = "添加表情回应"
"Add row" = "添加行"
"Add starting point" = "添加起点"
"Add stroke" = "添加描边"
"Add variant" = "添加变体"
@@ -175,6 +177,7 @@ status = "draft — needs native review"
"Bottom" = "底部"
"Bottom left radius" = "左下圆角"
"Bottom right radius" = "右下圆角"
"Brainstorm" = "头脑风暴"
"Branch" = "分支"
"Branch archived" = "分支已归档"
"Branch merged" = "分支已合并"
@@ -521,6 +524,7 @@ status = "draft — needs native review"
"Flip text to the other side" = "将文本翻转到另一侧"
"Flip vertical" = "垂直翻转"
"Flip width profile" = "翻转宽度配置"
"Flow chart" = "流程图"
"Flow starting point" = "流程起点"
"Flow starting points" = "流程起点"
"Flows" = "流程"
@@ -666,6 +670,7 @@ status = "draft — needs native review"
"Join|stroke" = "转角"
"Just now" = "刚刚"
"Justify" = "两端对齐"
"Kanban board" = "看板"
"Keep a measurement on the canvas for developers" = "为开发者在画布上保留测量"
"Keep appearance" = "保持外观"
"Keep appearance converts every color value to {profile} so colors look the same. Keep values leaves the numbers as they are, so colors shift." = "“保持外观”会将每个颜色值转换为 {profile},使颜色看起来不变。“保持数值”则保留数值不变,颜色会有偏移。"
@@ -1047,6 +1052,7 @@ status = "draft — needs native review"
"Remove all" = "全部移除"
"Remove annotation" = "移除标注"
"Remove auto layout" = "移除自动布局"
"Remove column" = "删除列"
"Remove dev resource" = "移除开发资源"
"Remove export setting" = "移除导出设置"
"Remove from server" = "从服务器移除"
@@ -1059,6 +1065,7 @@ status = "draft — needs native review"
"Remove min and max height" = "移除最小和最大高度"
"Remove min and max width" = "移除最小和最大宽度"
"Remove regions" = "移除区域"
"Remove row" = "删除行"
"Remove slot" = "移除插槽"
"Remove starting point" = "移除起点"
"Remove stop" = "移除色标"
@@ -1112,6 +1119,7 @@ status = "draft — needs native review"
"Restore main component" = "恢复主组件"
"Restore this version" = "恢复此版本"
"Restored “{name}”" = "已恢复“{name}”"
"Retrospective" = "回顾"
"Results" = "结果"
"Resume" = "继续"
"Reverse the order" = "反转顺序"
@@ -1331,6 +1339,7 @@ status = "draft — needs native review"
"Tell the server's operator what is wrong with it" = "告诉服务器的运营者它有什么问题"
"Temperature" = "色温"
"Template" = "模板"
"Templates" = "模板"
"Terms" = "条款"
"Text" = "文本"
"Text align" = "文本对齐"
@@ -1474,6 +1483,7 @@ status = "draft — needs native review"
"WebSocket URL" = "WebSocket URL"
"WebSocket URL copied" = "已复制 WebSocket URL"
"Wedge" = "楔形"
"Weekly planner" = "每周计划"
"Weight" = "粗细"
"What changed since a saved version" = "自已保存版本以来的更改"
"What changed since it was marked ready for dev" = "自标记为可供开发以来的更改"
@@ -1754,6 +1764,10 @@ status = "draft — needs native review"
"board.shape" = "带文字的形状"
"board.stamp" = "印章"
"board.sticky" = "便利贴"
"board.table" = "表格"
"board.tableEdit" = "编辑表格"
"board.template" = "白板模板"
"board.templates" = "列出白板模板"
"board.timer" = "计时器"
"board.timerState" = "计时器状态"
"board.vote" = "投票"
+10 -2
View File
@@ -96,6 +96,7 @@ struct Has {
shape: Option<BoardShape>,
text: bool,
connector: Option<(Routing, bool)>,
table: bool,
}
/// What the selected board objects take from the bar (layers that aren't board objects, a section
@@ -116,10 +117,11 @@ fn has(app: &FigApp) -> Option<Has> {
h.connector = Some((c.routing, n.stroke.cap_end != StrokeCap::None));
}
Some(BoardItem::Label { .. }) => h.text = true,
Some(BoardItem::Table) => h.table = true,
Some(BoardItem::Stamp { .. }) | None => {}
}
}
(h.color || h.text || h.connector.is_some()).then_some(h)
(h.color || h.text || h.connector.is_some() || h.table).then_some(h)
}
const CELL: f32 = 28.0;
@@ -144,6 +146,9 @@ impl Has {
if self.connector.is_some() {
w += 4.0 * (CELL + 2.0) + PART_END;
}
if self.table {
w += crate::board_picker::TABLE_PART_W;
}
w
}
}
@@ -188,6 +193,9 @@ pub fn show(app: &mut FigApp, ctx: &egui::Context) {
if h.text {
x = size_part(app, ui, x, cy, &t);
}
if h.table {
x = crate::board_picker::table_part(app, ui, x, cy, &t);
}
if let Some((routing, arrow)) = h.connector {
line_part(app, ui, (routing, arrow), pos2(x, cy), &t);
}
@@ -274,7 +282,7 @@ fn line_part(app: &mut FigApp, ui: &mut Ui, line: (Routing, bool), at: Pos2, t:
}
/// A thin separator after `x`; where the next part starts.
fn divider(ui: &Ui, t: &Tokens, x: f32, cy: f32) -> f32 {
pub(crate) fn divider(ui: &Ui, t: &Tokens, x: f32, cy: f32) -> f32 {
ui.painter().line_segment([pos2(x + 2.0, cy - 10.0), pos2(x + 2.0, cy + 10.0)], Stroke::new(1.0, t.border));
x + 8.0
}
+162
View File
@@ -0,0 +1,162 @@
//! Boards in the UI, continued: the toolbar's templates picker (a popover of the board templates,
//! each with a picture of itself, and a table) and the table's part of the bar over the selection
//! (add or remove a row or a column).
use std::collections::HashMap;
use egui::{Align2, CornerRadius, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
use figcraft_engine::board_templates::TEMPLATES;
use serde_json::json;
use crate::FigApp;
use crate::a11y;
use crate::i18n::tr;
use crate::icons::{self, Icon};
use crate::theme::{Tokens, regular};
use crate::toolbar::tool_cell;
/// The picker's key while it is open (`app.ui.tool_flyout`).
const KEY: &str = "board_templates";
const CARD: egui::Vec2 = vec2(136.0, 104.0);
const PICTURE_H: f32 = 76.0;
const COLUMNS: usize = 3;
const PAD: f32 = 10.0;
const GAP: f32 = 8.0;
const TITLE_H: f32 = 26.0;
/// The templates button at `at` (its left edge, the toolbar's middle) and, while open, its picker
/// above the toolbar (whose top is `top`). Where the next button starts.
pub(crate) fn button(app: &mut FigApp, ui: &mut Ui, at: Pos2, top: f32, t: &Tokens) -> f32 {
let r = Rect::from_min_size(pos2(at.x, at.y - 16.0), vec2(32.0, 32.0));
let open = app.ui.tool_flyout == Some(KEY);
let resp = tool_cell(ui, t, r, "board_templates", Icon::Templates, open, tr("Templates"));
if open {
a11y::popup(&mut app.ui.nav, resp.id, area_id());
}
if resp.clicked() {
app.ui.tool_flyout = if open { None } else { Some(KEY) };
}
if app.ui.tool_flyout == Some(KEY) {
picker(app, ui, r, top - 8.0, t);
}
r.max.x + 4.0
}
fn area_id() -> egui::Id {
egui::Id::unique("board_templates_area")
}
/// A template's picture: the template built on a board of its own and drawn small.
fn picture(ctx: &egui::Context, id: &str) -> Option<egui::TextureHandle> {
let doc = figcraft_engine::board_templates::preview(id).ok()?;
let layers = doc.node(doc.first_page()?)?.children.clone();
let b = layers.iter().filter_map(|l| figcraft_render::export_bounds(&doc, *l)).reduce(|a, b| a.union(b))?;
if !(b.width() > 0.0 && b.height() > 0.0) {
return None;
}
let fit = (f64::from(CARD.x - 16.0) / b.width()).min(f64::from(PICTURE_H - 12.0) / b.height());
let pix = figcraft_render::render_area(&doc, &layers, b, (fit * f64::from(ctx.pixels_per_point())).min(1.0))?;
let image = crate::render_worker::screen_image(&pix, doc.color_profile);
Some(ctx.load_texture(format!("board-template-{id}"), image, egui::TextureOptions::LINEAR))
}
/// The templates' pictures, drawn the first time the picker opens.
#[derive(Clone, Default)]
struct Pictures(HashMap<&'static str, Option<egui::TextureHandle>>);
/// The picker: a card for each template and one for a table, its bottom at `bottom`, from the
/// button `anchor`'s right edge leftwards (inside the canvas).
fn picker(app: &mut FigApp, ui: &mut Ui, anchor: Rect, bottom: f32, t: &Tokens) {
let ctx = ui.ctx().clone();
let mut pictures: Pictures = ctx.data(|d| d.get_temp(area_id())).unwrap_or_default();
for tpl in TEMPLATES {
pictures.0.entry(tpl.id).or_insert_with(|| picture(&ctx, tpl.id));
}
let count = TEMPLATES.len() + 1;
let rows = count.div_ceil(COLUMNS);
let size = vec2(COLUMNS as f32 * (CARD.x + GAP) - GAP + 2.0 * PAD, TITLE_H + rows as f32 * (CARD.y + GAP) - GAP + 2.0 * PAD);
let canvas = app.canvas.rect;
let x = (anchor.max.x - size.x).min(canvas.max.x - size.x - 8.0).max(canvas.min.x + 8.0);
let mut chosen = None;
egui::Area::new(area_id()).fixed_pos(pos2(x, bottom - size.y)).order(egui::Order::Foreground).show(&ctx, |ui| {
let (rect, _) = ui.allocate_exact_size(size, Sense::hover());
a11y::container_bounds(ui, rect);
let p = ui.painter();
p.rect_filled(rect, CornerRadius::same(13), t.menu);
p.text(pos2(rect.min.x + PAD + 2.0, rect.min.y + PAD + 8.0), Align2::LEFT_CENTER, tr("Templates"), crate::theme::semibold(12.0), t.menu_text);
for i in 0..count {
let (col, row) = (i % COLUMNS, i / COLUMNS);
let at = pos2(rect.min.x + PAD + col as f32 * (CARD.x + GAP), rect.min.y + PAD + TITLE_H + row as f32 * (CARD.y + GAP));
let r = Rect::from_min_size(at, CARD);
let tpl = TEMPLATES.get(i);
let label = tpl.map_or(tr("Table"), |tpl| tr(tpl.name));
let resp = ui.interact(r, ui.scope_id().with(("board_template", i)), Sense::click());
a11y::menu_item(&resp, label, "", None);
a11y::own_focus(&resp);
let p = ui.painter();
if resp.hovered() || resp.has_focus() {
p.rect_filled(r, CornerRadius::same(8), t.menu_hover);
}
let pic = Rect::from_min_size(r.min + vec2(4.0, 4.0), vec2(CARD.x - 8.0, PICTURE_H));
p.rect_filled(pic, CornerRadius::same(6), crate::theme::c32(figcraft_doc::BOARD_PAGE));
p.rect_stroke(pic, CornerRadius::same(6), Stroke::new(1.0, t.border), StrokeKind::Inside);
match tpl.and_then(|tpl| pictures.0.get(tpl.id)).and_then(Option::as_ref) {
Some(tex) => {
let s = tex.size_vec2() / ctx.pixels_per_point();
let k = ((pic.width() - 12.0) / s.x).min((pic.height() - 12.0) / s.y).min(1.0);
p.image(
tex.id(),
Rect::from_center_size(pic.center(), s * k),
Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
egui::Color32::WHITE,
);
}
// The table (and a template whose picture couldn't be drawn): its mark.
None => icons::paint(
p,
Rect::from_center_size(pic.center(), vec2(32.0, 32.0)),
if tpl.is_some() { Icon::Templates } else { Icon::Table },
egui::Color32::from_gray(90),
),
}
p.text(pos2(r.min.x + 6.0, pic.max.y + 13.0), Align2::LEFT_CENTER, label, regular(12.0), t.menu_text);
if resp.clicked() {
chosen = Some(i);
}
}
});
ctx.data_mut(|d| d.insert_temp(area_id(), pictures));
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
app.ui.tool_flyout = None;
}
if let Some(i) = chosen {
let (cmd, params) = match TEMPLATES.get(i) {
Some(tpl) => ("board.template", json!({"name": tpl.id})),
None => ("board.table", json!({})),
};
app.run(cmd, params);
app.ui.tool_flyout = None;
}
}
/// The table's part of the bar over the selection from `x`: add a row or a column at the end,
/// remove the last row or column. Where the next part starts.
pub(crate) fn table_part(app: &mut FigApp, ui: &mut Ui, mut x: f32, cy: f32, t: &Tokens) -> f32 {
let cells = [
(Icon::AddRow, tr("Add row"), "addRow"),
(Icon::RemoveRow, tr("Remove row"), "removeRow"),
(Icon::AddColumn, tr("Add column"), "addColumn"),
(Icon::RemoveColumn, tr("Remove column"), "removeColumn"),
];
for (icon, tip, action) in cells {
let r = Rect::from_center_size(pos2(x + 14.0, cy), vec2(28.0, 28.0));
if tool_cell(ui, t, r, ("bb_table", action), icon, false, tip).clicked() {
app.run("board.tableEdit", json!({"action": action}));
}
x += 30.0;
}
crate::board_bar::divider(ui, t, x, cy)
}
/// The table part's width (with the gap after it).
pub(crate) const TABLE_PART_W: f32 = 4.0 * 30.0 + 8.0;
+83
View File
@@ -0,0 +1,83 @@
//! Boards in the UI: the toolbar's templates picker (pictures, a template or a table placed with
//! a click) and the table's part of the bar over the selection.
use egui::{Event, Modifiers, PointerButton, Pos2, pos2};
use figcraft_doc::{BoardItem, NodeId};
use figcraft_engine::Session;
use serde_json::json;
use crate::layers_drag_tests::Ui;
use crate::live_tests::{flat, has_text, text_center};
fn click(u: &mut Ui, p: Pos2) {
for pressed in [true, false] {
u.frame(vec![Event::PointerMoved(p), Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE }]);
}
u.frame(vec![]);
}
fn board() -> Ui {
let mut s = Session::new();
s.execute("file.newBoard", &json!({})).unwrap();
let mut u = Ui::new(s);
u.frame(vec![]);
u
}
/// The templates button: left of the Design panel's button at the board toolbar's right end.
fn templates_button(u: &Ui) -> Pos2 {
let p = crate::toolbar::pill_rect(u.app.canvas.rect, crate::toolbar::bar_width(&u.app));
pos2(p.max.x - 8.0 - 32.0 - 4.0 - 16.0, p.center().y)
}
fn count(u: &Ui, f: impl Fn(&BoardItem) -> bool) -> usize {
u.app.session.doc().unwrap().nodes().filter(|n| n.board.as_ref().is_some_and(&f)).count()
}
#[test]
fn the_templates_picker_places_a_template_or_a_table() {
let mut u = board();
let at = templates_button(&u);
click(&mut u, at);
assert_eq!(u.app.ui.tool_flyout, Some("board_templates"));
let shapes = flat(&u.frame_output(vec![]).shapes);
for name in ["Retrospective", "Brainstorm", "Weekly planner", "Flow chart", "Kanban board", "Table"] {
assert!(has_text(&shapes, name), "{name} in the picker");
}
let pictures = shapes.iter().filter(|s| matches!(s, egui::Shape::Mesh(m) if m.texture_id != egui::TextureId::default())).count();
assert!(pictures >= 5, "each template has its picture ({pictures})");
click(&mut u, text_center(&shapes, "Flow chart"));
assert_eq!(u.app.ui.tool_flyout, None, "a pick closes the picker");
assert_eq!(count(&u, |b| matches!(b, BoardItem::Connector(_))), 6);
assert!(!u.app.session.selection().is_empty(), "what it placed is selected");
// Again, for a table.
click(&mut u, at);
let shapes = flat(&u.frame_output(vec![]).shapes);
click(&mut u, text_center(&shapes, "Table"));
assert_eq!(count(&u, |b| *b == BoardItem::Table), 1);
// Escape closes it.
click(&mut u, at);
u.frame(vec![Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE }]);
assert_eq!(u.app.ui.tool_flyout, None);
}
#[test]
fn the_selection_bar_adds_and_removes_a_tables_rows_and_columns() {
let mut u = board();
let t = NodeId(u.app.session.execute("board.table", &json!({"x": 0, "y": 0, "rows": 2, "columns": 2})).unwrap()["id"].as_u64().unwrap());
u.frame(vec![]);
u.frame(vec![]);
let size = |u: &Ui| {
let doc = u.app.session.doc().unwrap();
let n = doc.node(t).unwrap();
let rows: Vec<_> = n.children.iter().filter(|c| !doc.node(**c).unwrap().child_layout.absolute).collect();
(rows.len(), doc.node(*rows[0]).unwrap().children.len())
};
// Add row, remove row, add column, remove column: 30 apart, 8 in.
for (i, want) in [(0, (3, 2)), (1, (2, 2)), (2, (2, 3)), (3, (2, 2))] {
// The bar follows the table's middle as it widens.
let r = u.ctx.memory(|m| m.area_rect(egui::Id::unique("board_bar"))).expect("the bar shows for a table");
click(&mut u, pos2(r.min.x + 8.0 + i as f32 * 30.0 + 14.0, r.center().y));
assert_eq!(size(&u), want, "cell {i}");
}
}
+1
View File
@@ -133,6 +133,7 @@ fn dynamic_labels() -> Vec<&'static str> {
// Board shapes and stamps (the board tools' flyouts and the contextual bar).
v.extend(figcraft_doc::BoardShape::ALL.iter().map(|b| b.label()));
v.extend(figcraft_doc::Stamp::ALL.iter().map(|b| b.label()));
v.extend(figcraft_engine::board_templates::TEMPLATES.iter().map(|t| t.name));
v.extend(Group::ALL.iter().map(|g| g.label()));
v.extend(GESTURES.iter().flat_map(|(_, label, mac, other)| [*label, *mac, *other]));
v.extend(DATA_LABELS);
+38
View File
@@ -203,6 +203,14 @@ icons! {
PenNib,
/// A board file (and the sticky note tool): a note with its bottom corner folded up.
Board,
/// Board templates: a board laid out in blocks, a note and lines of text. A table: a grid.
/// The table bar's rows and columns: a strip with a plus or a minus beyond its end.
Templates,
Table,
AddRow,
RemoveRow,
AddColumn,
RemoveColumn,
/// Board tools: a chisel-tipped marker; a broad highlighter over its band; a connector
/// between two boxes; shapes (a square behind a circle); a stamp (a badge with a check).
Marker,
@@ -1172,6 +1180,36 @@ fn draw(p: &P, icon: Icon) {
p.line(5.0, 6.5, 11.0, 6.5);
p.line(5.0, 9.5, 8.0, 9.5);
}
Templates => {
p.rect(1.5, 2.5, 14.5, 13.5, 1.5);
p.fill_rect(3.5, 4.5, 7.5, 8.5, 0.5);
p.line(9.5, 5.5, 12.5, 5.5);
p.line(9.5, 7.5, 12.5, 7.5);
p.line(3.5, 11.0, 12.5, 11.0);
}
Table => {
p.rect(1.5, 2.5, 14.5, 13.5, 1.0);
p.line(1.5, 6.5, 14.5, 6.5);
p.line(1.5, 10.0, 14.5, 10.0);
p.line(6.0, 2.5, 6.0, 13.5);
p.line(10.0, 2.5, 10.0, 13.5);
}
AddRow | RemoveRow => {
p.rect(1.5, 2.5, 14.5, 8.5, 1.0);
p.line(8.0, 2.5, 8.0, 8.5);
p.line(5.5, 12.5, 10.5, 12.5);
if icon == AddRow {
p.line(8.0, 10.0, 8.0, 15.0);
}
}
AddColumn | RemoveColumn => {
p.rect(2.5, 1.5, 8.5, 14.5, 1.0);
p.line(2.5, 8.0, 8.5, 8.0);
p.line(10.0, 8.0, 15.0, 8.0);
if icon == AddColumn {
p.line(12.5, 5.5, 12.5, 10.5);
}
}
Marker => {
p.poly(&[(3.0, 13.0), (3.0, 10.5), (10.0, 3.5), (12.5, 6.0), (5.5, 13.0)], true);
p.line(8.5, 5.0, 11.0, 7.5);
+3
View File
@@ -10,6 +10,7 @@ pub mod assets_panel;
pub mod bidi;
pub mod board_bar;
pub mod board_facilitate;
mod board_picker;
pub mod branch_ui;
pub mod canvas;
pub mod cjk;
@@ -72,6 +73,8 @@ pub mod versions;
pub mod widgets;
pub mod windows;
#[cfg(test)]
mod board_picker_tests;
#[cfg(test)]
mod board_ui_tests;
#[cfg(test)]
+2 -1
View File
@@ -134,7 +134,7 @@ pub(crate) fn bar_width(app: &FigApp) -> f32 {
fn main_width(groups: &[Group], board: bool) -> f32 {
let (cell, gap) = (TOOL_CELL, 4.0);
let carets = groups.iter().filter(|g| g.has_flyout()).count() as f32 * (FLYOUT_GAP + FLYOUT_W);
let end = if board { cell + 16.0 } else { 4.0 * 34.0 + 24.0 };
let end = if board { 2.0 * cell + gap + 16.0 } else { 4.0 * 34.0 + 24.0 };
groups.len() as f32 * (cell + gap) + carets + 2.0 * (cell + gap) + 16.0 + end
}
@@ -157,6 +157,7 @@ fn main_toolbar(app: &mut FigApp, ui: &mut Ui, canvas: Rect, t: &Tokens, board:
x += 8.0;
if board {
// Boards have no prototypes or Dev Mode; the Design panel is hidden until asked for.
x = crate::board_picker::button(app, ui, pos2(x, cy), rect.min.y, t);
let r = Rect::from_center_size(pos2(x + TOOL_CELL / 2.0, cy), vec2(TOOL_CELL, TOOL_CELL));
let tip = crate::shortcuts::tip(tr("Design panel"), "view.design");
if tool_cell(ui, t, r, "board_panel", Icon::Panels, app.ui.board_panel, tip).clicked() {
+3 -1
View File
@@ -370,7 +370,9 @@ fn the_unsaved_changes_question_offers_save_dont_save_and_cancel() {
let (ctx, mut app, _) = asking(None, vec![]);
ctx.enable_accesskit();
frame(&ctx, &mut app, false);
let out = ctx.run_ui(RawInput::default(), |ui| app.ui(ui));
let mut out = ctx.run_ui(RawInput::default(), |ui| app.ui(ui));
// A canvas picture that lands in this frame uploads a texture nobody applies here.
out.textures_delta.clear();
let nodes = out.platform_output.accesskit_update.expect("an AccessKit update").nodes;
let buttons: Vec<String> =
nodes.iter().filter(|(_, n)| n.role() == egui::accesskit::Role::Button).filter_map(|(_, n)| n.label().map(str::to_string)).collect();
+9 -7
View File
@@ -1,13 +1,14 @@
# Boards parity
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-24 · **Change:** minor (facilitation: a shared timer and voting sessions; the dot grid, dragging a connector's end to re-attach it, curved connectors, elbows that go around what they join) · **Target:** the incumbent's whiteboard product (behaviour from its public help centre)
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-24 · **Change:** minor (templates and tables; facilitation: a shared timer and voting sessions; the dot grid, dragging a connector's end to re-attach it, curved connectors, elbows that go around what they join) · **Target:** the incumbent's whiteboard product (behaviour from its public help centre)
**Breadth ~42%, ready ~25% (estimated, erring low).** A board is a FigCraft file of its own kind
**Breadth ~47%, ready ~29% (estimated, erring low).** A board is a FigCraft file of its own kind
(`file.newBoard`) with a dot grid on the page, sticky notes, shapes with text, connectors that
follow what they join (straight, elbow or curved, their ends dragged onto other objects), marker,
highlighter and eraser, stamps, text and sections, edited together in the existing live sessions,
with a shared countdown and voting sessions to run a workshop. Music, board widgets, templates,
tables, mind maps and the rest of the tail are missing. Remaining: ~24–50 h. Tracked as area 22 of
highlighter and eraser, stamps, text and sections, five built-in templates and tables, edited
together in the existing live sessions, with a shared countdown and voting sessions to run a
workshop. Music, board widgets, mind maps and the rest of the tail are missing. Remaining:
~20–44 h. Tracked as area 22 of
[`target-app-parity.md`](target-app-parity.md) (not in its weighted sum: the target there is the
design product). The work item is gap 24 in [`gaps.md`](gaps.md).
@@ -49,8 +50,8 @@ all end with the same geometry.
| Voting sessions | done | one at a time: votes per person (1–99), on every object, on sticky notes or on the selection; a click on an object places a vote (several on one object allowed), a click on your badge or an Alt-click takes it back; your own votes show as badges while it runs, everyone's once it ends, with a ranked list (ties share a rank) that selects an object from its row; Clear results. `board.vote {action: start\|cast\|retract\|end\|clear}`, `board.voteResults`. Ballots sync per voter (keyed by the live-session client id, or your name offline), so people voting at once never overwrite each other. Not undo steps |
| Voting: only the facilitator ends or clears it, who voted for what | missing | anyone who can edit the board can end or clear a vote; results are counts only |
| Board widgets | missing | design-file widgets exist (`crates/engine/src/widgets.rs`), not wired to boards |
| Templates (retrospective, brainstorm, flow chart…) | missing | |
| Tables | missing | |
| Templates (retrospective, brainstorm, flow chart…) | partial | five built-in templates drawn up for FigCraft (retrospective, brainstorm, weekly planner, flow chart, kanban board), placed from the toolbar's templates picker (each with a picture of itself) or `board.template {name, x?, y?}` as ordinary sections, stickies, shapes, connectors and text in one undo step; `board.templates` lists them. No community templates, no saving your own |
| Tables | partial | `board.table {rows, columns, cells?, x?, y?}` (at most 50 × 20): an auto layout frame of rows of cells with text, a tinted header row and lines between rows and columns; a cell's row grows with its text; a double-click types in the cell under the pointer; add or remove a row or column from the bar over the selection or `board.tableEdit {id, action, index?}`; undoable and synced in live sessions. No column resizing by dragging, no cell colours, no Tab between cells, no CSV paste |
| Mind maps | missing | |
| Washi tape, code blocks, image cropping on boards | missing | images can be placed as in design files |
| Audio and huddles | missing | |
@@ -64,4 +65,5 @@ all end with the same geometry.
|---|---|---|
| 2026-10-24 | minor | M12: the dot grid on board pages (a render pass under the layers), dragging a selected connector's end onto another object or empty canvas (`board.reattach`), curved connectors, and elbows that go around the objects they join. Breadth ~35% → ~37%, ready ~20% → ~21%, ~30–60 h → ~28–56 h |
| 2026-10-24 | minor | Facilitation (M12): a shared timer and voting sessions. Breadth ~35% → ~40%, ready ~20% → ~24%, ~30–60 h → ~26–54 h |
| 2026-10-24 | minor | Templates (retrospective, brainstorm, weekly planner, flow chart, kanban board) from a toolbar picker or `board.template`, and tables (`board.table`, `board.tableEdit`): breadth ~35% → ~40%, ready ~20% → ~24%, remaining 30–60 h → 26–54 h |
| 2026-10-24 | major | Created with board files (M12): sticky notes, shapes with text, connectors, ink and eraser, stamps, the board toolbar and contextual bar, live-session sync |
+13 -9
View File
File diff suppressed because one or more lines are too long
+11
View File
@@ -414,6 +414,17 @@ one undo step and returns the ids it made:
- `board.setColor {ids?, color}`: stickies and shapes take it as their fill, connectors and ink as
their line. `board.setStyle {ids?, shape?, fontSize?, style?: straight|elbow|curved, startCap?,
endCap?}`: a shape's outline and text size, a connector's routing and arrowheads.
- `board.templates` → `[{id, name, description, width, height}]` lists the built-in templates:
`retrospective brainstorm weeklyPlanner flowChart kanban`. `board.template {name, x?, y?}` →
`{name, ids}` places one (its top-left at `x`, `y`; centred in the view without them) as ordinary
sections, stickies, shapes, connectors and text, selected, in one undo step.
- `board.table {rows?, columns?, cells?, x?, y?}` → `{id, rows, columns, cells}`: a table (default
3 × 3, at most 50 × 20; `cells` is `[["Name", "Role"], …]` row by row and sets the size when
`rows`/`columns` are left out). It is an auto layout frame of row frames holding cell frames, each
with one text layer; `cells` in the answer holds those text layers' ids row by row, so
`node.set {id, props: {characters}}` changes a cell. A cell's row grows with its text. `board.tableEdit {id?, action: addRow|addColumn|removeRow|removeColumn,
index?}` → the same answer: `index` is where the new row or column goes, or which one goes
(default the last); `id` may be the table or any layer in it.
- Tools for `pointer`: `tool.sticky {color?}`, `tool.shape {shape?}`, `tool.connector {style?: straight|elbow|curved}`
(drag from one object to another), `tool.marker {color?}`, `tool.highlighter {color?}`,
`tool.eraser` (drag over strokes), `tool.stamp {stamp?}`; Text and Section are the usual tools.
+1 -1
View File
@@ -60,7 +60,7 @@ blockers.
| M9 Real-time collaboration | core done (#103: ids, change sets, server ordering, convergence); live sessions (WebSocket transport with limits and reconnect, hosting from the app, invite links, document sync between apps, cursors, selections, avatars, follow, `collab.*` commands); per-user undo, rejection handling, offline queue and resuming after a reconnect; styles, variables, collections, modes and comments created in a session get ids from their creator's space; others' text carets and selections on the canvas; the browser client (joins desktop-hosted sessions); `figcraft://` links open the app and join (Windows registers per user; macOS and Linux packages declare the scheme); the standalone `figcraft-server` (persistence with compaction, restarts that lose nothing confirmed, publishing from the app, share links to edit or view, revocable); team libraries kept on the server and enabled by a share link, with usage counts, and swapping a library; `wss://` from the desktop app, server releases and a deployment guide | accounts, teams | 45–90 |
| M10 Localization | ~39%: the layer, English, Russian and Turkish reviewed by native speakers; Spanish (with a Latin American overlay), French, German, Brazilian Portuguese, Japanese, Korean, Simplified Chinese, Indonesian and Czech drafts | native reviews, Hindi, Vietnamese, Arabic (with mirrored panels), engine messages and number formatting ([localization-parity](localization-parity.md)) | 29–51 |
| M11 Plugins | runtime, API, menu, manager and SDK done (#101); plugin panels and settings (ABI v2); widgets (API 3), panels side by side, colour picker on plugin fields; a plugin directory on `figcraft-server` (publish, browse, install, update) | a public catalogue, publisher accounts and signatures, the directory in the browser, richer widget events; Figma plugin compatibility is an owner decision | 8–26 |
| M12 Boards (new) | first slice landed (2026-10-24): board files, sticky notes, shapes with text, connectors that follow, marker, highlighter, eraser, stamps, board toolbar and contextual bar, live sessions; then a shared timer and voting sessions | widgets, templates, tables, mind maps ([boards-parity](boards-parity.md)) | 24–50 |
| M12 Boards (new) | first slice landed (2026-10-24): board files, sticky notes, shapes with text, connectors that follow, marker, highlighter, eraser, stamps, board toolbar and contextual bar, live sessions; then a shared timer and voting sessions | widgets, templates, tables, mind maps ([boards-parity](boards-parity.md)) | 20–44 |
| M13 AI features (new, owner decision) | not started | local models | 30–60 |
## Next, after the current focus
File diff suppressed because one or more lines are too long