M12: board timer and voting

A countdown everyone on a board sees (board.timer, board.timerState), synced as its end time or the
time left while paused, and voting sessions (board.vote, board.voteResults): votes per person, on
everything, stickies or the selection; clicks vote, your badge takes a vote back; your own votes
show while it runs, everyone's with a ranked list once it ends. Timer and vote sync as document
properties and ballots as one entry per voter; neither is an undo step. A pill at the top right
of the board, new Timer, Pause and Vote icons, labels and strings in every catalog, docs.
This commit is contained in:
bflatastic
2026-10-24 11:49:27 +07:00
parent a1a842af9a
commit 5a635d46b2
42 changed files with 1611 additions and 27 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-24 · **Change:** minor (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-24 · **Change:** minor (boards: a shared timer and voting sessions; 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) | ~35% | ~20% | 30–60 | [boards-parity](docs/boards-parity.md) |
| Boards (whiteboarding; not in the weighted numbers) | ~40% | ~24% | 26–54 | [boards-parity](docs/boards-parity.md) |
## Languages
@@ -132,6 +132,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
## Progress log
- **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** · 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-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.
- **2026-10-15** · A text box by dragging (M1, #363): a drag with the Text tool makes a fixed-size text box the size of the drag; a click still makes auto-width text. Numbers unchanged.
@@ -327,6 +328,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
| Date | Change | Summary |
|---|---|---|
| 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 | 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-15 | minor | Auto-save (#305, #296): Preferences ▸ Save files automatically (`pref.autoSave`, off by default; desktop only, the browser has no files to save to) saves each open file that has a path 3 s after its last change, so steady editing doesn't save every few seconds, and never during a gesture, a live text preview or while a version is shown. The file is encoded on a thread of its own and written as of that moment (edits made meanwhile stay unsaved); a failed save says why in a toast and is tried again a minute later, and version history checkpoints keep coming with it on. Numbers unchanged |
| 2026-10-15 | minor | A text box by dragging (#363, #360): a drag with the Text tool makes a fixed-size text box the size of the drag (at least one line tall); a click, or a drag shorter than the drag threshold, still makes auto-width text. Numbers unchanged |
+9 -2
View File
@@ -222,6 +222,10 @@ pub enum DocProp {
VariableOrder,
/// A design file or a board.
Kind,
/// A board's countdown (`null`: none).
Timer,
/// A board's vote (`null`: none); the ballots are [`Entry::Ballot`]s.
Vote,
}
/// A document-level entry, by id.
@@ -241,6 +245,8 @@ pub enum Entry {
DevSnapshot(NodeId),
/// An image by content hash (value: `{width, height, bytes}` with the encoded bytes in base64).
Image(String),
/// A voter's ballot in a board's vote (value: the layer ids voted on).
Ballot(String),
}
/// One synced value a change writes: a node property, a document-level property or a
@@ -314,7 +320,7 @@ impl Entry {
Entry::Variable(id) => Some(id_space(id, VARIABLE_ID_PREFIX)),
Entry::Collection(id) => Some(id_space(id, COLLECTION_ID_PREFIX)),
Entry::Comment(id) => Some(space_of(*id)),
Entry::DevSnapshot(_) | Entry::Image(_) => None,
Entry::DevSnapshot(_) | Entry::Image(_) | Entry::Ballot(_) => None,
}
}
@@ -327,6 +333,7 @@ impl Entry {
Entry::Variable(id) => format!("variable {id}"),
Entry::DevSnapshot(id) => format!("ready-for-dev snapshot of {id}"),
Entry::Image(hash) => format!("image {hash}"),
Entry::Ballot(voter) => format!("ballot of {voter}"),
}
}
@@ -337,7 +344,7 @@ impl Entry {
Entry::Comment(_) => Some(Family::Comments),
Entry::Collection(_) => Some(Family::Collections),
Entry::Variable(_) => Some(Family::Variables),
Entry::DevSnapshot(_) | Entry::Image(_) => None,
Entry::DevSnapshot(_) | Entry::Image(_) | Entry::Ballot(_) => None,
}
}
}
+12 -1
View File
@@ -214,6 +214,8 @@ doc_scalars! {
AnnotationCategories => annotation_categories,
Libraries => libraries,
Kind => kind,
Timer => timer,
Vote => vote,
}
/// Keyed maps (no order): changed and added entries are put, missing ones removed.
@@ -253,7 +255,8 @@ pub(crate) fn diff(a: &Document, b: &Document, out: &mut Vec<Change>) -> Result<
|k| Entry::Image(k.clone()),
image_json,
out,
)
)?;
diff_map(&a.votes, &b.votes, |x, y| x == y, |k| Entry::Ballot(k.clone()), Json::of, out)
}
/// Apply a document-level change. Returns whether it can change which pages are listed (the
@@ -289,6 +292,9 @@ pub(crate) fn apply(doc: &mut Document, change: &Change) -> Result<bool, Error>
let img = image_of(value)?;
doc.images.insert(k.clone(), img);
}
Entry::Ballot(k) => {
doc.votes.insert(k.clone(), value.read("ballot")?);
}
},
Change::Remove { entry } => match entry {
Entry::Style(k) => STYLES.remove(doc, k),
@@ -301,6 +307,9 @@ pub(crate) fn apply(doc: &mut Document, change: &Change) -> Result<bool, Error>
Entry::Image(k) => {
doc.images.remove(k);
}
Entry::Ballot(k) => {
doc.votes.remove(k);
}
},
_ => return Err(Error::Invalid("not a document-level change".into())),
}
@@ -337,6 +346,7 @@ pub(crate) fn entry_value(doc: &Document, entry: &Entry) -> Result<Option<Json>,
Entry::Variable(k) => doc.variables.variables.iter().find(|v| v.id == *k).map(Json::of),
Entry::DevSnapshot(k) => doc.dev_snapshots.get(k).map(|s| Json::of(s.as_ref())),
Entry::Image(k) => doc.images.get(k).map(image_json),
Entry::Ballot(k) => doc.votes.get(k).map(Json::of),
};
value.transpose()
}
@@ -350,6 +360,7 @@ pub(crate) fn has(doc: &Document, entry: &Entry) -> bool {
Entry::Variable(k) => doc.variables.variables.iter().any(|v| v.id == *k),
Entry::DevSnapshot(k) => doc.dev_snapshots.contains_key(k),
Entry::Image(k) => doc.images.contains_key(k),
Entry::Ballot(k) => doc.votes.contains_key(k),
}
}
+1
View File
@@ -91,6 +91,7 @@ impl fmt::Display for Key {
Entry::Variable(k) => write!(f, "variable/{k}"),
Entry::DevSnapshot(k) => write!(f, "devSnapshot/{k}"),
Entry::Image(k) => write!(f, "image/{k}"),
Entry::Ballot(k) => write!(f, "ballot/{k}"),
},
}
}
+111
View File
@@ -0,0 +1,111 @@
//! A board's facilitation state, shared by everyone on it: the countdown timer and the vote.
//!
//! The timer keeps when it ends (or, paused, how long is left), never the ticks: each client works
//! out the time left from its own clock. A vote's ballots are kept per voter, so two people voting
//! at once change different entries.
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::{BoardItem, Document, NodeId};
/// The longest a timer runs (24 hours, in milliseconds).
pub const TIMER_MAX_MS: u64 = 24 * 60 * 60 * 1000;
/// The most votes one person gets in a vote.
pub const VOTES_MAX: u32 = 99;
/// A board's countdown.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "camelCase")]
pub enum BoardTimer {
/// Counting down to `ends` (milliseconds since the Unix epoch).
Running { ends: u64 },
/// Stopped with `left` milliseconds to go.
Paused { left: u64 },
}
impl BoardTimer {
/// Milliseconds left at `now` (ms since the Unix epoch); 0 once it has run out.
pub fn left(&self, now: u64) -> u64 {
match *self {
BoardTimer::Running { ends } => ends.saturating_sub(now).min(TIMER_MAX_MS),
BoardTimer::Paused { left } => left.min(TIMER_MAX_MS),
}
}
}
/// What a vote is on.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum VoteScope {
/// Any object on the board.
#[default]
All,
/// Sticky notes only.
Stickies,
/// The objects that were selected when it started.
Only { ids: Vec<NodeId> },
}
impl VoteScope {
/// The object a vote at layer `leaf` goes to: the nearest layer from it up that the vote is
/// on (`All`: `whole`, the object a click there picks).
pub fn target(&self, doc: &Document, leaf: NodeId, whole: NodeId) -> Option<NodeId> {
if matches!(self, VoteScope::All) {
return self.takes(doc, whole).then_some(whole);
}
std::iter::once(leaf).chain(doc.ancestors(leaf)).find(|id| self.takes(doc, *id))
}
/// Whether object `id` takes votes.
pub fn takes(&self, doc: &Document, id: NodeId) -> bool {
let Some(n) = doc.node(id) else { return false };
if doc.pages.contains(&id) || n.parent.is_none() {
return false;
}
match self {
VoteScope::All => true,
VoteScope::Stickies => matches!(n.board, Some(BoardItem::Sticky { .. })),
VoteScope::Only { ids } => ids.contains(&id),
}
}
}
/// A vote: how many votes each person has, what they go on, and whether it has ended (then
/// everyone's votes show). The ballots are [`Document::votes`].
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Vote {
pub per_person: u32,
#[serde(default)]
pub scope: VoteScope,
#[serde(default)]
pub ended: bool,
/// Who started it.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub by: String,
}
/// Every voter's ballot: the objects they put their votes on (once per vote).
pub type Ballots = BTreeMap<String, Vec<NodeId>>;
impl Vote {
/// The votes of `ballot` that count: on objects that are still there, up to the cap.
pub fn counted<'a>(&self, doc: &'a Document, ballot: &'a [NodeId]) -> impl Iterator<Item = NodeId> + 'a {
ballot.iter().copied().filter(|id| doc.node(*id).is_some()).take(self.per_person.min(VOTES_MAX) as usize)
}
/// The votes each object got, most first (ties in layer id order).
pub fn tally(&self, doc: &Document, ballots: &Ballots) -> Vec<(NodeId, u32)> {
let mut counts: BTreeMap<NodeId, u32> = BTreeMap::new();
for ballot in ballots.values() {
for id in self.counted(doc, ballot) {
*counts.entry(id).or_default() += 1;
}
}
let mut out: Vec<(NodeId, u32)> = counts.into_iter().collect();
out.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
out
}
}
+14
View File
@@ -11,6 +11,7 @@
pub mod board;
pub mod colors;
pub mod comments;
pub mod facilitation;
pub mod geometry;
pub mod handoff;
pub mod ids;
@@ -27,6 +28,7 @@ pub mod vector_regions;
pub use board::*;
pub use colors::*;
pub use comments::*;
pub use facilitation::*;
pub use figcraft_color as color;
pub use figcraft_geom as geom;
pub use handoff::*;
@@ -204,6 +206,15 @@ pub struct Document {
/// [`handoff`]).
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dev_snapshots: BTreeMap<NodeId, Arc<DevSnapshot>>,
/// A board's countdown (see [`facilitation`]).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timer: Option<BoardTimer>,
/// A board's vote, running or ended.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vote: Option<Vote>,
/// The vote's ballots, by voter.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub votes: Ballots,
}
impl Default for Document {
@@ -243,6 +254,9 @@ impl Document {
image_presets: Vec::new(),
annotation_categories: None,
dev_snapshots: BTreeMap::new(),
timer: None,
vote: None,
votes: BTreeMap::new(),
}
}
+1 -1
View File
@@ -232,7 +232,7 @@ fn line_shade(c: Color) -> Color {
/// Your name on what you make: the name from Preferences, else the one a live session gave you
/// (none for the default name outside a session).
fn author(s: &Session) -> String {
pub(crate) fn author(s: &Session) -> String {
let name = crate::live::display_name(&s.prefs);
if name.is_empty() { s.live().info.as_ref().map(|i| i.name.clone()).unwrap_or_default() } else { name }
}
+1 -1
View File
@@ -458,7 +458,7 @@ fn entry_label(plan: &Plan, entry: &Entry, value: Option<&Value>) -> Option<(&'s
Entry::Variable(_) => "variable",
Entry::Collection(_) => "variable collection",
Entry::Comment(_) => "comment",
Entry::DevSnapshot(_) | Entry::Image(_) => return None,
Entry::DevSnapshot(_) | Entry::Image(_) | Entry::Ballot(_) => return None,
};
let base = plan.base().value(&Target::Entry(entry.clone())).ok().flatten();
let named = value.or(base.as_ref().map(|j| &j.0)).and_then(|v| v.get("name").or_else(|| v.get("text"))).and_then(Value::as_str);
+366
View File
@@ -0,0 +1,366 @@
//! Running a session on a board: a countdown everyone sees and votes on objects.
//!
//! Both live in the document ([`figcraft_doc::facilitation`]) so they reach everyone in a live
//! session, and neither is an undo step: undoing an edit leaves the timer and the vote as they are.
use std::sync::Arc;
use figcraft_doc::{BoardTimer, Document, NodeId, TIMER_MAX_MS, VOTES_MAX, Vote, VoteScope};
use figcraft_geom::{Point, Rect};
use serde_json::{Value, json};
use crate::board::{require, text_of};
use crate::cmd::CommandSpec;
use crate::params::{id, num, req_string, req_targets, string};
use crate::tools::PointerEvent;
use crate::{EngineError, Result, Session, bad, spec};
/// Minutes a timer starts with, and adds, when none are given.
const START_MINUTES: f64 = 5.0;
const ADD_MINUTES: f64 = 1.0;
/// Votes each person gets when none are given.
const VOTES: u32 = 3;
/// A vote badge's radius on screen (logical pixels).
pub const BADGE_PX: f64 = 11.0;
/// The longest a voter name is kept.
const MAX_VOTER: usize = 64;
pub fn specs() -> Vec<CommandSpec> {
vec![
spec!(
"board.timer",
"Timer",
"{action: start|pause|resume|add|stop, minutes? (start: default 5; add: default 1; up to 1440)} → the timer's state (as board.timerState) one countdown per board, the same for everyone in a live session",
false,
timer_cmd
),
spec!(
"board.timerState",
"Timer state",
"{} → {state: off|running|paused|done, left (ms), text (m:ss), ends? (ms since 1970, while running)}",
false,
|s, _| {
require(s, "board.timerState")?;
Ok(timer_json(s.doc()?.timer, now_ms()))
}
),
spec!(
"board.vote",
"Voting session",
"{action: start|cast|retract|end|clear, votes? (start: per person, 1–99, default 3), on? (start: all|stickies|selection; default all), ids? (start with on: selection; default the selection), id (cast, retract: the object), voter? (default you)} → the vote's state (as board.voteResults) one vote at a time; others' votes show once it ends",
false,
vote_cmd
),
spec!(
"board.voteResults",
"Voting results",
"{voter?} → {state: off|running|ended, votesPerPerson, on, by, left (your votes left), mine [{id, votes}], voters, results? [{id, name, votes, rank}] (once ended, most votes first)}",
false,
|s, p| {
require(s, "board.voteResults")?;
let who = voter(s, p);
Ok(vote_json(s.doc()?, &who))
}
),
]
}
/// Milliseconds since the Unix epoch (0 if the clock is before it).
pub fn now_ms() -> u64 {
web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH).map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
}
/// `left` milliseconds as a clock: "4:05", "1:02:03" (whole seconds, rounded up).
pub fn clock(left: u64) -> String {
let s = left.div_ceil(1000);
let (h, m, s) = (s / 3600, s / 60 % 60, s % 60);
if h > 0 { format!("{h}:{m:02}:{s:02}") } else { format!("{m}:{s:02}") }
}
/// The timer's state at `now`: off, running, paused or done (run out, until someone stops it).
pub fn timer_state(t: Option<BoardTimer>, now: u64) -> &'static str {
match t {
None => "off",
Some(BoardTimer::Paused { .. }) => "paused",
Some(t) if t.left(now) == 0 => "done",
Some(_) => "running",
}
}
fn timer_json(t: Option<BoardTimer>, now: u64) -> Value {
let left = t.map_or(0, |t| t.left(now));
let ends = match t {
Some(BoardTimer::Running { ends }) => Some(ends),
_ => None,
};
json!({"state": timer_state(t, now), "left": left, "text": clock(left), "ends": ends})
}
/// `minutes` (or `default`) as milliseconds: more than 0, at most a day.
fn duration(cmd: &str, p: &Value, default: f64) -> Result<u64> {
let m = match p.get("minutes").filter(|v| !v.is_null()) {
Some(_) => num(p, "minutes").ok_or_else(|| bad(cmd, "`minutes` must be a number"))?,
None => default,
};
let ms = (m * 60_000.0).round();
if !(1000.0..=TIMER_MAX_MS as f64).contains(&ms) {
return Err(bad(cmd, "`minutes` must be from 1 second to 1440 minutes"));
}
Ok(ms as u64)
}
/// The timer after `action` at `now` (`Ok(None)`: stopped).
pub fn timer_after(cmd: &str, t: Option<BoardTimer>, action: &str, p: &Value, now: u64) -> Result<Option<BoardTimer>> {
let off = || EngineError::Disabled(cmd.into(), "there is no timer: start one".into());
Ok(match action {
"start" => Some(BoardTimer::Running { ends: now.saturating_add(duration(cmd, p, START_MINUTES)?) }),
"stop" => None,
"pause" => match t {
Some(r @ BoardTimer::Running { .. }) if r.left(now) > 0 => Some(BoardTimer::Paused { left: r.left(now) }),
_ => return Err(EngineError::Disabled(cmd.into(), "the timer isn't running".into())),
},
"resume" => match t {
Some(BoardTimer::Paused { left }) => Some(BoardTimer::Running { ends: now.saturating_add(left) }),
_ => return Err(EngineError::Disabled(cmd.into(), "the timer isn't paused".into())),
},
"add" => {
let more = duration(cmd, p, ADD_MINUTES)?;
let left = t.ok_or_else(off)?.left(now).saturating_add(more).min(TIMER_MAX_MS);
match t {
Some(BoardTimer::Paused { .. }) => Some(BoardTimer::Paused { left }),
_ => Some(BoardTimer::Running { ends: now.saturating_add(left) }),
}
}
other => return Err(bad(cmd, format!("unknown action `{other}` (start|pause|resume|add|stop)"))),
})
}
fn timer_cmd(s: &mut Session, p: &Value) -> Result<Value> {
require(s, "board.timer")?;
let action = req_string("board.timer", p, "action")?;
let now = now_ms();
let t = timer_after("board.timer", s.doc()?.timer, &action, p, now)?;
if s.doc()?.timer != t {
s.doc_mut()?.timer = t;
}
Ok(timer_json(t, now))
}
/// Who votes: `voter` from the params, else you as the live session knows you (your client id
/// there, so two people of the same name vote apart), else your name.
pub fn voter(s: &Session, p: &Value) -> String {
if let Some(v) = string(p, "voter").map(|v| v.trim().chars().filter(|c| !c.is_control()).take(MAX_VOTER).collect::<String>())
&& !v.is_empty()
{
return v;
}
let live = s.ds().ok().filter(|ds| s.live_doc(ds.uid)).and_then(|ds| s.live_of(ds.uid).info.as_ref());
match live {
Some(info) => format!("peer {}", info.me),
None => crate::cmd::comment::user_name(s),
}
}
fn running<'a>(cmd: &str, doc: &'a Document) -> Result<&'a Vote> {
match &doc.vote {
Some(v) if !v.ended => Ok(v),
_ => Err(EngineError::Disabled(cmd.into(), "no vote is running".into())),
}
}
fn vote_cmd(s: &mut Session, p: &Value) -> Result<Value> {
const CMD: &str = "board.vote";
require(s, CMD)?;
let action = req_string(CMD, p, "action")?;
let who = voter(s, p);
match action.as_str() {
"start" => {
if s.doc()?.vote.as_ref().is_some_and(|v| !v.ended) {
return Err(EngineError::Disabled(CMD.into(), "a vote is already running: end it first".into()));
}
let per = match p.get("votes").filter(|v| !v.is_null()) {
Some(_) => num(p, "votes")
.filter(|n| n.fract() == 0.0 && (1.0..=f64::from(VOTES_MAX)).contains(n))
.ok_or_else(|| bad(CMD, "`votes` must be a whole number from 1 to 99"))?,
None => f64::from(VOTES),
} as u32;
let scope = match string(p, "on").as_deref().unwrap_or("all") {
"all" => VoteScope::All,
"stickies" => VoteScope::Stickies,
"selection" => {
let doc = s.doc()?;
let ids: Vec<NodeId> = req_targets(CMD, s, p)?.into_iter().filter(|i| VoteScope::All.takes(doc, *i)).collect();
if ids.is_empty() {
return Err(bad(CMD, "pick the objects to vote on (not pages)"));
}
VoteScope::Only { ids }
}
other => return Err(bad(CMD, format!("unknown `on` `{other}` (all|stickies|selection)"))),
};
let by = crate::board::author(s);
let doc = s.doc_mut()?;
doc.votes.clear();
doc.vote = Some(Vote { per_person: per, scope, ended: false, by });
}
"cast" | "retract" => {
let target = id(p, "id").ok_or_else(|| bad(CMD, "`id` must be the object to vote on"))?;
let doc = s.doc()?;
let vote = running(CMD, doc)?;
let mut ballot: Vec<NodeId> = vote.counted(doc, doc.votes.get(&who).map_or(&[], Vec::as_slice)).collect();
if action == "cast" {
if !vote.scope.takes(doc, target) {
return Err(match doc.node(target) {
None => EngineError::NoNode(target),
Some(_) => EngineError::Disabled(CMD.into(), "this vote isn't on that object".into()),
});
}
if ballot.len() >= vote.per_person as usize {
return Err(EngineError::Disabled(CMD.into(), "you have no votes left: remove one first".into()));
}
ballot.push(target);
} else {
let i = ballot
.iter()
.rposition(|i| *i == target)
.ok_or_else(|| EngineError::Disabled(CMD.into(), "you have no vote on that object".into()))?;
ballot.remove(i);
}
let votes = &mut s.doc_mut()?.votes;
if ballot.is_empty() {
votes.remove(&who);
} else {
votes.insert(who.clone(), ballot);
}
}
"end" => {
running(CMD, s.doc()?)?;
if let Some(v) = s.doc_mut()?.vote.as_mut() {
v.ended = true;
}
}
"clear" => {
let doc = s.doc()?;
if doc.vote.is_some() || !doc.votes.is_empty() {
let doc = s.doc_mut()?;
doc.vote = None;
doc.votes.clear();
}
}
other => return Err(bad(CMD, format!("unknown action `{other}` (start|cast|retract|end|clear)"))),
}
Ok(vote_json(s.doc()?, &who))
}
/// `who`'s votes per object, in the order first voted on.
pub fn mine(doc: &Document, who: &str) -> Vec<(NodeId, u32)> {
let (Some(vote), Some(ballot)) = (&doc.vote, doc.votes.get(who)) else { return Vec::new() };
let mut out: Vec<(NodeId, u32)> = Vec::new();
for id in vote.counted(doc, ballot) {
match out.iter_mut().find(|(i, _)| *i == id) {
Some((_, n)) => *n += 1,
None => out.push((id, 1)),
}
}
out
}
/// What an object is called in the results: its text (a sticky's note), else its name.
pub fn object_name(doc: &Document, id: NodeId) -> String {
let text = text_of(doc, id).and_then(|t| doc.node(t)).and_then(|n| n.kind.text()).map(|t| t.characters.trim().to_string());
let name = text.filter(|t| !t.is_empty()).or_else(|| doc.node(id).map(|n| n.name.clone())).unwrap_or_default();
let line = name.lines().next().unwrap_or_default();
let short: String = line.chars().take(60).collect();
if short.len() < name.len() { format!("{short}…") } else { short }
}
fn vote_json(doc: &Document, who: &str) -> Value {
let Some(vote) = &doc.vote else { return json!({"state": "off"}) };
let on = match &vote.scope {
VoteScope::All => json!("all"),
VoteScope::Stickies => json!("stickies"),
VoteScope::Only { ids } => json!({"selection": ids}),
};
let mine = mine(doc, who);
let used: u32 = mine.iter().map(|m| m.1).sum();
let voters = doc.votes.values().filter(|b| vote.counted(doc, b).next().is_some()).count();
let mut out = json!({
"state": if vote.ended { "ended" } else { "running" },
"votesPerPerson": vote.per_person,
"on": on,
"by": vote.by,
"left": vote.per_person.saturating_sub(used),
"mine": mine.iter().map(|(id, n)| json!({"id": id, "votes": n})).collect::<Vec<_>>(),
"voters": voters,
});
if vote.ended {
let results: Vec<Value> =
ranked(doc, vote).into_iter().map(|(rank, id, n)| json!({"id": id, "name": object_name(doc, id), "votes": n, "rank": rank})).collect();
out["results"] = json!(results);
}
out
}
/// The objects with votes, most first, each with its rank (ties share one: "1, 2, 2, 4").
pub fn ranked(doc: &Document, vote: &Vote) -> Vec<(usize, NodeId, u32)> {
let mut last = None;
let mut rank = 0;
vote.tally(doc, &doc.votes)
.into_iter()
.enumerate()
.map(|(i, (id, n))| {
if last != Some(n) {
rank = i + 1;
last = Some(n);
}
(rank, id, n)
})
.collect()
}
/// Where object `b`'s vote badge sits (its top-right corner) and its radius, in page units at
/// `zoom` screen pixels per unit.
pub fn badge(b: Rect, zoom: f64) -> (Point, f64) {
(Point::new(b.x1, b.y0), BADGE_PX / zoom.max(1e-6))
}
impl Session {
/// A press with the Move tool while a vote runs: on one of your vote badges, or with Alt, it
/// takes that vote back; on an object the vote is on, it puts a vote there. False when the
/// press is for the tool (no vote running, or nothing to vote on there).
pub(crate) fn vote_down(&mut self, ev: &PointerEvent) -> Result<bool> {
let doc = self.doc()?;
let Some(vote) = doc.vote.as_ref().filter(|v| !v.ended) else { return Ok(false) };
let who = voter(self, &Value::Null);
let p = ev.p();
let on_badge = mine(doc, &who).into_iter().map(|(id, _)| id).find(|id| {
doc.world_bounds(*id).is_some_and(|b| {
let (c, r) = badge(b, ev.zoom);
(p - c).hypot() <= r
})
});
let hit = || {
let leaf = self.hit(doc, self.page().ok()?, p, 2.0 / ev.zoom.max(1e-6))?;
vote.scope.target(doc, leaf, figcraft_doc::geometry::select_target(doc, leaf, &[], false))
};
let (action, target) = match on_badge {
Some(id) => ("retract", id),
None => match hit() {
Some(id) => (if ev.alt { "retract" } else { "cast" }, id),
None => return Ok(false),
},
};
self.execute("board.vote", &json!({"action": action, "id": target}))?;
Ok(true)
}
}
/// Undo and redo bring back a document as it was, but the timer and the vote aren't undoable:
/// keep them as they are now (`from`).
pub(crate) fn keep(from: &Document, to: &mut Arc<Document>) {
if from.timer != to.timer || from.vote != to.vote || from.votes != to.votes {
let d = Arc::make_mut(to);
d.timer = from.timer;
d.vote = from.vote.clone();
d.votes = from.votes.clone();
}
}
+2
View File
@@ -5,6 +5,7 @@ pub mod comment;
pub mod component;
pub mod create;
pub mod edit;
pub mod facilitate;
pub mod file;
pub mod image;
pub mod layout;
@@ -110,6 +111,7 @@ fn registry() -> &'static Registry {
list.extend(crate::invite_links::specs());
list.extend(crate::templates::specs());
list.extend(crate::board::specs());
list.extend(facilitate::specs());
let mut index = HashMap::new();
for (i, s) in list.iter().enumerate() {
if index.insert(s.id, i).is_some() {
+324
View File
@@ -0,0 +1,324 @@
//! A board's timer and votes: the commands, what undo leaves alone, saving, live sessions, clicks
//! that vote, and junk parameters.
use figcraft_doc::{BoardTimer, NodeId, VoteScope};
use serde_json::{Value, json};
use crate::board_tests::{board, id};
use crate::cmd::facilitate::{clock, timer_after, timer_state};
use crate::collab_live_tests::Room;
use crate::tools::{PointerEvent, PointerKind};
use crate::{EngineError, Session};
const MIN: u64 = 60_000;
fn ex(s: &mut Session, cmd: &str, p: Value) -> Value {
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))
}
fn sticky(s: &mut Session, x: f64, text: &str) -> NodeId {
id(&ex(s, "board.sticky", json!({"x": x, "y": 0, "text": text})), "id")
}
#[test]
fn the_timer_counts_from_its_end_time() {
let now = 1_000_000;
let none = json!({});
let t = timer_after("t", None, "start", &json!({"minutes": 2}), now).unwrap();
assert_eq!(t, Some(BoardTimer::Running { ends: now + 2 * MIN }));
assert_eq!(timer_state(t, now + MIN), "running");
assert_eq!(timer_state(t, now + 3 * MIN), "done", "run out: done until stopped");
// Paused half way: the time left stays put however long the pause.
let p = timer_after("t", t, "pause", &none, now + MIN).unwrap();
assert_eq!(p, Some(BoardTimer::Paused { left: MIN }));
assert_eq!(p.unwrap().left(now + 100 * MIN), MIN);
// A minute more while paused, then on again.
let p = timer_after("t", p, "add", &none, now + 5 * MIN).unwrap();
assert_eq!(p, Some(BoardTimer::Paused { left: 2 * MIN }));
let r = timer_after("t", p, "resume", &none, now + 10 * MIN).unwrap();
assert_eq!(r, Some(BoardTimer::Running { ends: now + 12 * MIN }));
// Adding to a timer that ran out starts from now.
let r = timer_after("t", r, "add", &json!({"minutes": 0.5}), now + 20 * MIN).unwrap();
assert_eq!(r, Some(BoardTimer::Running { ends: now + 20 * MIN + MIN / 2 }));
assert_eq!(timer_after("t", r, "stop", &none, now).unwrap(), None);
// What can't be done says so.
assert!(timer_after("t", None, "pause", &none, now).is_err());
assert!(timer_after("t", None, "add", &none, now).is_err());
assert!(timer_after("t", r, "resume", &none, now).is_err());
assert!(timer_after("t", Some(BoardTimer::Running { ends: now }), "pause", &none, now).is_err(), "nothing left to pause");
for m in [json!(0), json!(-1), json!(1441), json!("NaN"), json!(1e308), json!("x"), json!([1])] {
assert!(timer_after("t", None, "start", &json!({"minutes": m}), now).is_err(), "{m}");
}
// At most a day, also by adding.
let long = timer_after("t", None, "start", &json!({"minutes": 1440}), now).unwrap();
assert_eq!(timer_after("t", long, "add", &none, now).unwrap().unwrap().left(now), 1440 * MIN);
assert!(timer_after("t", None, "start", &json!({"minutes": 1}), u64::MAX).is_ok(), "no overflow");
}
#[test]
fn clocks_read_minutes_and_seconds() {
assert_eq!(clock(0), "0:00");
assert_eq!(clock(1), "0:01", "a part of a second left still shows");
assert_eq!(clock(5 * MIN), "5:00");
assert_eq!(clock(65_000), "1:05");
assert_eq!(clock(3_723_000), "1:02:03");
}
#[test]
fn timer_commands_drive_the_boards_timer() {
let mut s = board();
assert_eq!(ex(&mut s, "board.timerState", json!({}))["state"], "off");
let r = ex(&mut s, "board.timer", json!({"action": "start", "minutes": 3}));
assert_eq!(r["state"], "running");
let left = r["left"].as_u64().unwrap();
assert!(left > 3 * MIN - 5000 && left <= 3 * MIN, "{r}");
assert!(r["ends"].as_u64().is_some());
assert_eq!(ex(&mut s, "board.timer", json!({"action": "pause"}))["state"], "paused");
let r = ex(&mut s, "board.timer", json!({"action": "add"}));
assert!(r["left"].as_u64().unwrap() > 4 * MIN - 5000, "{r}");
assert_eq!(ex(&mut s, "board.timer", json!({"action": "resume"}))["state"], "running");
assert_eq!(ex(&mut s, "board.timer", json!({"action": "stop"}))["state"], "off");
assert!(s.doc().unwrap().timer.is_none());
// Not an undo step.
assert_eq!(s.undo_depth().0, 0);
assert!(matches!(s.execute("board.timer", &json!({"action": "pause"})), Err(EngineError::Disabled(..))));
assert!(s.execute("board.timer", &json!({"action": "jump"})).is_err());
// Boards only.
let mut d = Session::new();
d.execute("file.new", &json!({})).unwrap();
for (cmd, p) in [
("board.timer", json!({"action": "start"})),
("board.timerState", json!({})),
("board.vote", json!({"action": "start"})),
("board.voteResults", json!({})),
] {
assert!(matches!(d.execute(cmd, &p), Err(EngineError::Disabled(..))), "{cmd}");
}
}
#[test]
fn votes_are_capped_hidden_until_the_end_and_ranked() {
let mut s = board();
let a = sticky(&mut s, 0.0, "Ship it");
let b = sticky(&mut s, 300.0, "Wait");
let c = sticky(&mut s, 600.0, "Rethink");
let shape = id(&ex(&mut s, "board.shape", json!({"x": 0, "y": 400})), "id");
let r = ex(&mut s, "board.vote", json!({"action": "start", "votes": 2, "on": "stickies"}));
assert_eq!((r["state"].as_str(), r["left"].as_u64(), r["on"].as_str()), (Some("running"), Some(2), Some("stickies")));
assert!(matches!(s.execute("board.vote", &json!({"action": "start"})), Err(EngineError::Disabled(..))), "one vote at a time");
let cast = |s: &mut Session, who: &str, on: NodeId| s.execute("board.vote", &json!({"action": "cast", "id": on, "voter": who}));
// Ada puts both her votes on one sticky; a third is refused, and so is a shape.
cast(&mut s, "Ada", a).unwrap();
let r = cast(&mut s, "Ada", a).unwrap();
assert_eq!((r["left"].as_u64(), r["mine"].clone()), (Some(0), json!([{"id": a.0, "votes": 2}])));
assert!(matches!(cast(&mut s, "Ada", b), Err(EngineError::Disabled(..))), "no votes left");
assert!(matches!(cast(&mut s, "Bo", shape), Err(EngineError::Disabled(..))), "stickies only");
assert!(matches!(cast(&mut s, "Bo", NodeId(99_999)), Err(EngineError::NoNode(_))));
cast(&mut s, "Bo", b).unwrap();
cast(&mut s, "Bo", c).unwrap();
cast(&mut s, "Cy", b).unwrap();
cast(&mut s, "Cy", c).unwrap();
// Cy takes one back and puts it elsewhere.
s.execute("board.vote", &json!({"action": "retract", "id": c, "voter": "Cy"})).unwrap();
assert!(s.execute("board.vote", &json!({"action": "retract", "id": c, "voter": "Cy"})).is_err(), "no vote there any more");
cast(&mut s, "Cy", a).unwrap();
// While it runs, you see your own votes only.
let r = ex(&mut s, "board.voteResults", json!({"voter": "Bo"}));
assert!(r.get("results").is_none(), "{r}");
assert_eq!(r["mine"], json!([{"id": b.0, "votes": 1}, {"id": c.0, "votes": 1}]));
assert_eq!(r["voters"], 3);
assert_eq!(s.undo_depth().0, 4, "votes are no undo steps (only the four objects)");
ex(&mut s, "board.vote", json!({"action": "end"}));
assert!(s.execute("board.vote", &json!({"action": "cast", "id": b, "voter": "Dee"})).is_err(), "ended");
let r = ex(&mut s, "board.voteResults", json!({}));
assert_eq!(r["state"], "ended");
assert_eq!(
r["results"],
json!([
{"id": a.0, "name": "Ship it", "votes": 3, "rank": 1},
{"id": b.0, "name": "Wait", "votes": 2, "rank": 2},
{"id": c.0, "name": "Rethink", "votes": 1, "rank": 3},
])
);
// A deleted object's votes no longer count.
ex(&mut s, "edit.delete", json!({"ids": [a]}));
assert_eq!(ex(&mut s, "board.voteResults", json!({}))["results"][0]["id"], b.0);
ex(&mut s, "board.vote", json!({"action": "clear"}));
assert_eq!(ex(&mut s, "board.voteResults", json!({}))["state"], "off");
assert!(s.doc().unwrap().votes.is_empty());
}
#[test]
fn a_vote_on_the_selection_takes_only_those() {
let mut s = board();
let a = sticky(&mut s, 0.0, "A");
let b = sticky(&mut s, 300.0, "B");
ex(&mut s, "select.set", json!({"ids": [a]}));
let r = ex(&mut s, "board.vote", json!({"action": "start", "on": "selection"}));
assert_eq!(r["on"], json!({"selection": [a.0]}));
assert_eq!(r["votesPerPerson"], 3);
assert!(s.execute("board.vote", &json!({"action": "cast", "id": b})).is_err());
ex(&mut s, "board.vote", json!({"action": "cast", "id": a}));
// A finished vote may be replaced by a new one, which starts with no votes.
ex(&mut s, "board.vote", json!({"action": "end"}));
ex(&mut s, "board.vote", json!({"action": "start", "on": "all", "votes": 1}));
assert!(s.doc().unwrap().votes.is_empty());
assert_eq!(s.doc().unwrap().vote.as_ref().unwrap().scope, VoteScope::All);
for bad in [json!({"votes": 0}), json!({"votes": 100}), json!({"votes": 1.5}), json!({"on": "pages"}), json!({"on": "selection", "ids": []})] {
let mut p = bad.clone();
p["action"] = json!("start");
ex(&mut s, "board.vote", json!({"action": "clear"}));
assert!(s.execute("board.vote", &p).is_err(), "{p}");
}
}
#[test]
fn undo_leaves_the_timer_and_the_vote_alone() {
let mut s = board();
let a = sticky(&mut s, 0.0, "A");
ex(&mut s, "board.timer", json!({"action": "start"}));
ex(&mut s, "board.vote", json!({"action": "start"}));
ex(&mut s, "board.vote", json!({"action": "cast", "id": a}));
let b = sticky(&mut s, 300.0, "B");
s.undo().unwrap();
s.undo().unwrap();
let doc = s.doc().unwrap();
assert!(doc.node(a).is_none() && doc.node(b).is_none(), "the stickies are undone");
assert!(doc.timer.is_some() && doc.vote.is_some() && doc.votes.len() == 1, "the timer and the vote stay");
ex(&mut s, "board.timer", json!({"action": "stop"}));
s.redo().unwrap();
assert!(s.doc().unwrap().timer.is_none(), "redo doesn't bring a stopped timer back");
assert!(s.doc().unwrap().node(a).is_some());
}
#[test]
fn the_timer_and_votes_save_and_old_files_open() {
let mut s = board();
let a = sticky(&mut s, 0.0, "A");
ex(&mut s, "board.timer", json!({"action": "start"}));
ex(&mut s, "board.timer", json!({"action": "pause"}));
ex(&mut s, "board.vote", json!({"action": "start", "on": "stickies"}));
ex(&mut s, "board.vote", json!({"action": "cast", "id": a}));
let doc = s.doc().unwrap().clone();
let bytes = figcraft_format::save_bytes(&doc, None).unwrap();
let (back, _) = figcraft_format::load_bytes(&bytes).unwrap();
assert_eq!((back.timer, &back.vote, &back.votes), (doc.timer, &doc.vote, &doc.votes));
// A file from before: none of the fields.
let mut v = figcraft_format::to_json(&doc).unwrap();
for k in ["timer", "vote", "votes"] {
assert!(v.as_object_mut().unwrap().remove(k).is_some(), "{k} is saved");
}
let (old, _) = figcraft_format::from_json(&v.to_string()).unwrap();
assert!(old.timer.is_none() && old.vote.is_none() && old.votes.is_empty());
// Design files don't write them.
let plain = figcraft_format::to_json(&figcraft_doc::Document::new("D")).unwrap();
assert!(plain.get("timer").is_none() && plain.get("vote").is_none() && plain.get("votes").is_none());
}
#[test]
fn everyone_in_a_live_session_shares_the_timer_and_the_vote() {
let mut s = board();
let a = sticky(&mut s, 0.0, "A");
let b = sticky(&mut s, 300.0, "B");
ex(&mut s, "board.timer", json!({"action": "start", "minutes": 10}));
let mut room = Room::new(s.doc().unwrap().clone(), 2, 11);
assert!(room.s(1).doc().unwrap().timer.is_some(), "joiners get the running timer");
let t0 = room.s(0).doc().unwrap().timer;
assert_eq!(room.s(1).doc().unwrap().timer, t0);
ex(room.s(1), "board.timer", json!({"action": "pause"}));
ex(room.s(0), "board.vote", json!({"action": "start", "votes": 2}));
room.settle();
room.check();
assert!(matches!(room.s(0).doc().unwrap().timer, Some(BoardTimer::Paused { .. })), "a pause reaches everyone");
// Both vote at once: each ballot is its own entry, so neither is lost.
ex(room.s(0), "board.vote", json!({"action": "cast", "id": a}));
ex(room.s(1), "board.vote", json!({"action": "cast", "id": a}));
ex(room.s(1), "board.vote", json!({"action": "cast", "id": b}));
room.settle();
room.check();
assert_eq!(room.server.document().votes.len(), 2, "two voters, apart though both are called the same");
assert_eq!(ex(room.s(0), "board.voteResults", json!({}))["mine"], json!([{"id": a.0, "votes": 1}]));
ex(room.s(1), "board.vote", json!({"action": "end"}));
room.settle();
let r = ex(room.s(0), "board.voteResults", json!({}));
assert_eq!(r["results"], json!([{"id": a.0, "name": "A", "votes": 2, "rank": 1}, {"id": b.0, "name": "B", "votes": 1, "rank": 2}]));
// Undo in a session takes back your own edits only, not the vote.
room.s(0).undo().unwrap();
room.settle();
room.check();
assert!(room.server.document().vote.is_some());
}
#[test]
fn clicks_vote_while_a_vote_runs() {
let mut s = board();
let a = sticky(&mut s, 0.0, "A");
ex(&mut s, "select.none", json!({}));
let click = |s: &mut Session, x: f64, y: f64, alt: bool| {
let mut down = PointerEvent::at(PointerKind::Down, x, y, 1.0);
down.alt = alt;
s.pointer(&down).unwrap();
s.pointer(&PointerEvent::at(PointerKind::Up, x, y, 1.0)).unwrap();
};
// No vote: a click selects.
click(&mut s, 100.0, 100.0, false);
assert_eq!(s.selection(), vec![a]);
ex(&mut s, "select.none", json!({}));
ex(&mut s, "board.vote", json!({"action": "start"}));
click(&mut s, 100.0, 100.0, false);
click(&mut s, 50.0, 150.0, false);
assert!(s.selection().is_empty(), "votes, not selection");
assert_eq!(crate::cmd::facilitate::mine(s.doc().unwrap(), "You"), vec![(a, 2)]);
// Alt takes one back; so does a click on your badge (the sticky's top-right corner).
click(&mut s, 100.0, 100.0, true);
click(&mut s, 199.0, 1.0, false);
assert!(s.doc().unwrap().votes.is_empty());
// Empty canvas is the tool's as ever.
click(&mut s, 900.0, 900.0, false);
assert!(s.doc().unwrap().votes.is_empty());
}
#[test]
fn facilitation_commands_survive_junk() {
let junk = [
json!({}),
json!(null),
json!([1]),
json!({"action": 5, "minutes": "NaN", "votes": -1, "on": [1], "id": "x", "voter": {"a": 1}}),
json!({"action": "start", "minutes": 1e308, "votes": 1e308, "on": "selection", "ids": ["x", -1]}),
json!({"action": "add", "minutes": -1e308}),
json!({"action": "cast", "id": 99_999, "voter": "\u{0}\u{7}"}),
json!({"action": "retract", "id": -1, "voter": "x".repeat(10_000)}),
json!({"action": "end"}),
json!({"action": "clear", "ids": 7}),
json!({"action": "resume", "minutes": [1]}),
];
for cmd in ["board.timer", "board.timerState", "board.vote", "board.voteResults"] {
for j in &junk {
let mut s = board();
let a = sticky(&mut s, 0.0, "A");
ex(&mut s, "board.timer", json!({"action": "start"}));
ex(&mut s, "board.vote", json!({"action": "start"}));
ex(&mut s, "board.vote", json!({"action": "cast", "id": a}));
if let Err(EngineError::Internal { cmd, msg }) = s.execute(cmd, j) {
panic!("{cmd} panicked on {j}: {msg}");
}
// A voter name is kept short.
assert!(s.doc().unwrap().votes.keys().all(|k| k.chars().count() <= 64));
}
}
}
#[test]
fn ties_share_a_rank() {
let mut s = board();
let ids: Vec<NodeId> = (0..4).map(|i| sticky(&mut s, f64::from(i) * 300.0, "x")).collect();
ex(&mut s, "board.vote", json!({"action": "start", "votes": 2}));
for (who, on) in [("a", 0), ("a", 1), ("b", 1), ("b", 0), ("c", 2), ("c", 3), ("d", 2)] {
ex(&mut s, "board.vote", json!({"action": "cast", "id": ids[on], "voter": who}));
}
ex(&mut s, "board.vote", json!({"action": "end"}));
let r = ex(&mut s, "board.voteResults", json!({}));
let ranks: Vec<u64> = r["results"].as_array().unwrap().iter().map(|x| x["rank"].as_u64().unwrap()).collect();
assert_eq!(ranks, vec![1, 1, 1, 4], "{r}");
}
+6 -2
View File
@@ -1194,8 +1194,9 @@ impl Session {
}
let Some(e) = ds.history.undo.pop() else { return Ok(false) };
let redo = HistoryEntry { label: e.label.clone(), doc: ds.doc.clone(), selection: ds.selection.clone(), page: ds.page };
ds.history.redo.push(redo);
ds.doc = e.doc;
cmd::facilitate::keep(&redo.doc, &mut ds.doc);
ds.history.redo.push(redo);
ds.selection = e.selection;
if ds.doc.pages.contains(&e.page) {
ds.page = e.page;
@@ -1230,8 +1231,9 @@ impl Session {
}
let Some(e) = ds.history.redo.pop() else { return Ok(false) };
let undo = HistoryEntry { label: e.label.clone(), doc: ds.doc.clone(), selection: ds.selection.clone(), page: ds.page };
ds.history.undo.push(undo);
ds.doc = e.doc;
cmd::facilitate::keep(&undo.doc, &mut ds.doc);
ds.history.undo.push(undo);
ds.selection = e.selection;
if ds.doc.pages.contains(&e.page) {
ds.page = e.page;
@@ -1684,6 +1686,8 @@ mod export_depth_tests;
#[cfg(test)]
mod extended_chain_tests;
#[cfg(test)]
mod facilitate_tests;
#[cfg(test)]
mod fig_export_assets_tests;
#[cfg(test)]
mod fig_export_tests;
+4
View File
@@ -1032,6 +1032,10 @@ impl Session {
PointerKind::Hover => self.hover(ev),
PointerKind::Down => match tool {
ToolId::Move | ToolId::Scale => {
// While a board's vote runs, a click on an object votes.
if tool == ToolId::Move && !self.tool.dev_mode && self.vote_down(ev)? {
return Ok(());
}
// A click on the canvas deselects a ruler guide (the UI selects guides itself).
self.ds_mut()?.guide = None;
self.move_down(ev)
+23
View File
@@ -37,6 +37,7 @@ status = "draft — needs native review"
"Add a description" = "একটি বিবরণ যোগ করুন"
"Add a frame (F) or a shape (R) to get started" = "শুরু করতে একটি ফ্রেম (F) বা শেপ (R) যোগ করুন"
"Add a layer or component to tile first" = "প্রথমে টাইল করার জন্য একটি লেয়ার বা কম্পোনেন্ট যোগ করুন"
"Add a minute" = "এক মিনিট যোগ করুন"
"Add a note for developers" = "ডেভেলপারদের জন্য একটি নোট যোগ করুন"
"Add action" = "অ্যাকশন যোগ করুন"
"Add actions for when no condition holds" = "কোনো শর্ত পূর্ণ না হলে তার জন্য অ্যাকশন যোগ করুন"
@@ -91,6 +92,7 @@ status = "draft — needs native review"
"All comments" = "সব মন্তব্য"
"All components" = "সব কম্পোনেন্ট"
"All fonts" = "সব ফন্ট"
"All objects" = "সব অবজেক্ট"
"All supported properties" = "সমর্থিত সব প্রপার্টি"
"All the fonts this file uses are installed" = "এই ফাইলে ব্যবহৃত সব ফন্ট ইনস্টল করা আছে"
"All variables" = "সব ভেরিয়েবল"
@@ -205,6 +207,8 @@ status = "draft — needs native review"
"Checking the server…" = "সার্ভার চেক করা হচ্ছে..."
"Chevron" = "শেভরন"
"Clear from this list" = "এই তালিকা থেকে মুছে ফেলুন"
"Clear results" = "ফলাফল মুছুন"
"Click objects to vote; click your badge to take a vote back" = "ভোট দিতে অবজেক্টে ক্লিক করুন; ভোট ফেরাতে আপনার ব্যাজে ক্লিক করুন"
"Code or invite link" = "কোড বা আমন্ত্রণ লিংক"
"Color profile: {name}" = "কালার প্রোফাইল: {name}"
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "রঙ ও টেক্সট স্টাইল, লাইট এবং ডার্ক ভেরিয়েবল, বোতাম, ইনপুট এবং কার্ড"
@@ -454,6 +458,7 @@ status = "draft — needs native review"
"Empty" = "খালি"
"End" = "শেষ"
"End point" = "শেষ বিন্দু"
"End vote" = "ভোট শেষ করুন"
"Ends the session; everyone keeps their copy" = "সেশন শেষ করে; সবাই তাদের কপি নিজের কাছে রাখে"
"Enter" = "এন্টার"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "একটি figcraft-সার্ভারের ঠিকানা দিন এবং এটি যে প্লাগইনগুলো অফার করে তা দেখতে ব্রাউজ টিপুন।"
@@ -780,6 +785,7 @@ status = "draft — needs native review"
"Min H" = "সর্বনিম্ন উচ্চতা"
"Min W" = "সর্বনিম্ন প্রস্থ"
"Minimize" = "ছোট করুন"
"Minutes" = "মিনিট"
"Mirror angle" = "মিরর কোণ"
"Mirror angle and length" = "মিরর কোণ ও দৈর্ঘ্য"
"Mirroring" = "মিররিং"
@@ -877,6 +883,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "এখনো কোনো টেক্সট স্টাইল নেই। নির্বাচিত টেক্সট থেকে একটি তৈরি করতে + এ ক্লিক করুন।"
"No variables fit here." = "এখানে কোনো ভেরিয়েবল মানানসই নয়।"
"No variables yet. Create them in Local variables." = "এখনো কোনো ভেরিয়েবল নেই। স্থানীয় ভেরিয়েবলে এগুলো তৈরি করুন।"
"No votes yet" = "এখনও কোনো ভোট নেই"
"Nobody else is here yet" = "এখানে আর কেউ নেই"
"Noise" = "নয়েজ"
"None" = "কিছুই না"
@@ -963,6 +970,7 @@ status = "draft — needs native review"
"Page" = "পেজ"
"Pages" = "পেজসমূহ"
"Pages added to the document" = "ডকুমেন্টে পেজ যোগ করা হয়েছে"
"Pause" = "বিরতি"
"Pink" = "গোলাপি"
"Placed on the current page" = "বর্তমান পেজে রাখা হয়েছে"
"Pages: {pages}" = "পেজসমূহ: {pages}"
@@ -1143,6 +1151,8 @@ status = "draft — needs native review"
"Restore main component" = "প্রধান কম্পোনেন্ট পুনরুদ্ধার করুন"
"Restore this version" = "এই সংস্করণ পুনরুদ্ধার করুন"
"Restored “{name}”" = "“{name}” পুনরুদ্ধার করা হয়েছে"
"Results" = "ফলাফল"
"Resume" = "আবার চালু করুন"
"Reverse the order" = "ক্রম উল্টে দিন"
"Reversed triangle" = "উল্টানো ত্রিভুজ"
"Review and merge" = "পর্যালোচনা ও মার্জ করুন"
@@ -1320,17 +1330,20 @@ status = "draft — needs native review"
"Start point" = "শুরুর বিন্দু"
"Start sharing" = "শেয়ার করা শুরু করুন"
"Start sharing with the Share button, then pass on the code it gives" = "শেয়ার বোতাম দিয়ে শেয়ার করা শুরু করুন, তারপর এটি যে কোড দেয় তা দিন"
"Start vote" = "ভোট শুরু করুন"
"Starting to share…" = "শেয়ার করা শুরু হচ্ছে..."
"Status" = "স্ট্যাটাস"
"Sticky" = "স্টিকি"
"Sticky note" = "স্টিকি নোট"
"Sticky note colors" = "স্টিকি নোটের রং"
"Sticky notes" = "স্টিকি নোট"
"Sticky notes, shapes and connectors" = "স্টিকি নোট, আকৃতি ও সংযোগকারী"
"Stop" = "থামুন"
"Stop following {name}" = "{name}-কে অনুসরণ করা বন্ধ করুন"
"Stop session" = "সেশন বন্ধ করুন"
"Stop sharing" = "শেয়ার করা বন্ধ করুন"
"Stop spotlighting" = "স্পটলাইটিং বন্ধ করুন"
"Stop timer" = "টাইমার থামান"
"Stop {n} {hex}" = "স্টপ {n} {hex}"
"Straight" = "সোজা"
"Stretch" = "টেনে বড় করা (Stretch)"
@@ -1416,6 +1429,7 @@ status = "draft — needs native review"
"Tidy up" = "পরিপাটি করুন"
"Tile" = "টাইল"
"Tile type" = "টাইল টাইপ"
"Timer" = "টাইমার"
"Tint" = "টিন্ট"
"Title" = "শিরোনাম"
"Title case" = "টাইটেল কেস"
@@ -1510,6 +1524,8 @@ status = "draft — needs native review"
"Visibility" = "দৃশ্যমানতা"
"Visible" = "দৃশ্যমান"
"Vivid" = "ভিভিড"
"Votes per person" = "জনপ্রতি ভোট"
"Voting session" = "ভোটিং সেশন"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND সেট করা আছে এবং ব্যবহারের জন্য গ্রাফিক্স API-এর নাম দেয়; এটি আনসেট করলে FigCraft কে বেছে নিতে দেয়।"
"Warm" = "উষ্ণ"
"Watch" = "ওয়াচ"
@@ -1726,6 +1742,7 @@ status = "draft — needs native review"
"{n} layers selected" = { one = "{n}টি লেয়ার নির্বাচিত", other = "{n}টি লেয়ার নির্বাচিত" }
"{n} layers · {parts}" = { one = "{n}টি লেয়ার · {parts}", other = "{n}টি লেয়ার · {parts}" }
"{n} matches" = { one = "{n}টি মিল", other = "{n}টি মিল" }
"{n} min" = "{n} মিনিট"
"{n} minutes ago" = { one = "{n} মিনিট আগে", other = "{n} মিনিট আগে" }
"{n} months ago" = { one = "{n} মাস আগে", other = "{n} মাস আগে" }
"{n} more notes omitted from this report" = { one = "এই রিপোর্ট থেকে আরও {n}টি নোট বাদ দেওয়া হয়েছে", other = "এই রিপোর্ট থেকে আরও {n}টি নোট বাদ দেওয়া হয়েছে" }
@@ -1739,6 +1756,8 @@ status = "draft — needs native review"
"{n} styles" = { one = "{n}টি স্টাইল", other = "{n}টি স্টাইল" }
"{n} text layers" = { one = "{n}টি টেক্সট লেয়ার", other = "{n}টি টেক্সট লেয়ার" }
"{n} variables" = { one = "{n}টি ভেরিয়েবল", other = "{n}টি ভেরিয়েবল" }
"{n} votes" = { one = "{n}টি ভোট", other = "{n}টি ভোট" }
"{n} votes left" = { one = "{n}টি ভোট বাকি", other = "{n}টি ভোট বাকি" }
"{n} weeks ago" = { one = "{n} সপ্তাহ আগে", other = "{n} সপ্তাহ আগে" }
"{n} years ago" = { one = "{n} বছর আগে", other = "{n} বছর আগে" }
"{n}d" = "{n}d"
@@ -1776,6 +1795,10 @@ status = "draft — needs native review"
"board.shape" = "লেখাসহ আকৃতি"
"board.stamp" = "স্ট্যাম্প"
"board.sticky" = "স্টিকি নোট"
"board.timer" = "টাইমার"
"board.timerState" = "টাইমারের অবস্থা"
"board.vote" = "ভোটিং সেশন"
"board.voteResults" = "ভোটের ফলাফল"
"file.newBoard" = "নতুন বোর্ড"
"tool.connector" = "সংযোগকারী"
"tool.eraser" = "ইরেজার"
+23
View File
@@ -33,6 +33,7 @@ status = "draft — needs native review"
"Add a description" = "Přidat popis"
"Add a frame (F) or a shape (R) to get started" = "Začněte přidáním rámce (F) nebo tvaru (R)"
"Add a layer or component to tile first" = "Nejprve přidejte vrstvu nebo komponentu k opakování"
"Add a minute" = "Přidat minutu"
"Add a note for developers" = "Přidat poznámku pro vývojáře"
"Add action" = "Přidat akci"
"Add actions for when no condition holds" = "Přidat akce pro případ, kdy neplatí žádná podmínka"
@@ -86,6 +87,7 @@ status = "draft — needs native review"
"All comments" = "Všechny komentáře"
"All components" = "Všechny komponenty"
"All fonts" = "Všechna písma"
"All objects" = "Všechny objekty"
"All supported properties" = "Všechny podporované vlastnosti"
"All variables" = "Všechny proměnné"
"Alpha" = "Alfa"
@@ -198,6 +200,8 @@ status = "draft — needs native review"
"Checking the server…" = "Kontrola serveru…"
"Chevron" = "Lomená šipka"
"Clear from this list" = "Vymazat z tohoto seznamu"
"Clear results" = "Vymazat výsledky"
"Click objects to vote; click your badge to take a vote back" = "Kliknutím na objekty hlasujete; kliknutím na svůj odznak hlas vrátíte"
"Code or invite link" = "Kód nebo odkaz s pozvánkou"
"Color profile: {name}" = "Barevný profil: {name}"
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Barevné a textové styly, proměnné pro světlý a tmavý režim, tlačítka, vstupní pole a karty"
@@ -441,6 +445,7 @@ status = "draft — needs native review"
"Empty" = "Prázdné"
"End" = "Konec"
"End point" = "Koncový bod"
"End vote" = "Ukončit hlasování"
"Ends the session; everyone keeps their copy" = "Ukončí relaci; každý si ponechá svou kopii"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Zadejte adresu serveru figcraft-server a stisknutím Procházet zobrazte nabízené pluginy."
@@ -765,6 +770,7 @@ status = "draft — needs native review"
"Min H" = "Min. V"
"Min W" = "Min. Š"
"Minimize" = "Minimalizovat"
"Minutes" = "Minuty"
"Mirror angle" = "Zrcadlit úhel"
"Mirror angle and length" = "Zrcadlit úhel a délku"
"Mirroring" = "Zrcadlení"
@@ -859,6 +865,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "Zatím žádné textové styly. Kliknutím na + vytvoříte styl z vybraného textu."
"No variables fit here." = "Žádné vhodné proměnné."
"No variables yet. Create them in Local variables." = "Zatím žádné proměnné. Vytvořte je v Místních proměnných."
"No votes yet" = "Zatím žádné hlasy"
"Nobody else is here yet" = "Zatím tu není nikdo další"
"Noise" = "Šum"
"None" = "Žádné"
@@ -943,6 +950,7 @@ status = "draft — needs native review"
"Page deleted" = "Stránka smazána"
"Pages" = "Stránky"
"Pages added to the document" = "Stránky přidány do dokumentu"
"Pause" = "Pozastavit"
"Pink" = "Růžová"
"Placed on the current page" = "Umístěno na aktuální stránku"
"Pages: {pages}" = "Stránky: {pages}"
@@ -1120,6 +1128,8 @@ status = "draft — needs native review"
"Restore main component" = "Obnovit hlavní komponentu"
"Restore this version" = "Obnovit tuto verzi"
"Restored “{name}”" = "Obnoveno: „{name}“"
"Results" = "Výsledky"
"Resume" = "Pokračovat"
"Reverse the order" = "Obrátit pořadí"
"Reversed triangle" = "Obrácený trojúhelník"
"Review and merge" = "Zkontrolovat a sloučit"
@@ -1294,17 +1304,20 @@ status = "draft — needs native review"
"Start point" = "Počáteční bod"
"Start sharing" = "Zahájit sdílení"
"Start sharing with the Share button, then pass on the code it gives" = "Zahajte sdílení tlačítkem Sdílet a předejte získaný kód"
"Start vote" = "Zahájit hlasování"
"Starting to share…" = "Zahajování sdílení…"
"Status" = "Stav"
"Sticky" = "Přichycené"
"Sticky note" = "Lepicí poznámka"
"Sticky note colors" = "Barvy lepicích poznámek"
"Sticky notes" = "Lepicí poznámky"
"Sticky notes, shapes and connectors" = "Lepicí poznámky, tvary a spojnice"
"Stop" = "Zastavit"
"Stop following {name}" = "Přestat sledovat: {name}"
"Stop session" = "Ukončit relaci"
"Stop sharing" = "Zastavit sdílení"
"Stop spotlighting" = "Ukončit sdílení pohledu"
"Stop timer" = "Zastavit časovač"
"Stop {n} {hex}" = "Zarážka {n} {hex}"
"Straight" = "Rovná"
"Stretch" = "Roztáhnout"
@@ -1386,6 +1399,7 @@ status = "draft — needs native review"
"Tidy up" = "Uspořádat"
"Tile" = "Dlaždice"
"Tile type" = "Typ dlaždic"
"Timer" = "Časovač"
"Tint" = "Tónování"
"Title" = "Název"
"Title case" = "Velká počáteční písmena slov"
@@ -1477,6 +1491,8 @@ status = "draft — needs native review"
"Visibility" = "Viditelnost"
"Visible" = "Viditelné"
"Vivid" = "Živé"
"Votes per person" = "Hlasů na osobu"
"Voting session" = "Hlasování"
"Warm" = "Teplé"
"Watch" = "Hodinky"
"Wavy" = "Vlnitý"
@@ -1690,6 +1706,7 @@ status = "draft — needs native review"
"{n} layers selected" = { one = "Vybrána {n} vrstva", few = "Vybrány {n} vrstvy", other = "Vybráno {n} vrstev" }
"{n} layers · {parts}" = { one = "{n} vrstva · {parts}", few = "{n} vrstvy · {parts}", other = "{n} vrstev · {parts}" }
"{n} matches" = { one = "{n} shoda", few = "{n} shody", other = "{n} shod" }
"{n} min" = "{n} min"
"{n} minutes ago" = { one = "Před {n} minutou", few = "Před {n} minutami", other = "Před {n} minutami" }
"{n} months ago" = { one = "Před {n} měsícem", few = "Před {n} měsíci", other = "Před {n} měsíci" }
"{n} more notes omitted from this report" = { one = "{n} další poznámka v této zprávě vynechána", few = "{n} další poznámky v této zprávě vynechány", other = "{n} dalších poznámek v této zprávě vynecháno" }
@@ -1702,6 +1719,8 @@ status = "draft — needs native review"
"{n} rows" = { one = "{n} řádek", few = "{n} řádky", other = "{n} řádků" }
"{n} styles" = { one = "{n} styl", few = "{n} styly", other = "{n} stylů" }
"{n} variables" = { one = "{n} proměnná", few = "{n} proměnné", other = "{n} proměnných" }
"{n} votes" = { one = "{n} hlas", few = "{n} hlasy", other = "{n} hlasů" }
"{n} votes left" = { one = "Zbývá {n} hlas", few = "Zbývají {n} hlasy", other = "Zbývá {n} hlasů" }
"{n} weeks ago" = { one = "Před {n} týdnem", few = "Před {n} týdny", other = "Před {n} týdny" }
"{n} years ago" = { one = "Před {n} rokem", few = "Před {n} lety", other = "Před {n} lety" }
"{n}d" = "{n} d"
@@ -1763,6 +1782,10 @@ status = "draft — needs native review"
"board.shape" = "Tvar s textem"
"board.stamp" = "Razítko"
"board.sticky" = "Lepicí poznámka"
"board.timer" = "Časovač"
"board.timerState" = "Stav časovače"
"board.vote" = "Hlasování"
"board.voteResults" = "Výsledky hlasování"
"bool.exclude" = "Vyloučit výběr"
"bool.flatten" = "Sloučit booleovskou skupinu do vektoru"
"bool.intersect" = "Průnik výběru"
+23
View File
@@ -34,6 +34,7 @@ status = "draft — needs native review"
"Add a description" = "Beschreibung hinzufügen"
"Add a frame (F) or a shape (R) to get started" = "Fügen Sie zum Start einen Rahmen (F) oder eine Form (R) hinzu"
"Add a layer or component to tile first" = "Fügen Sie zuerst eine Ebene oder Komponente zum Kacheln hinzu"
"Add a minute" = "Eine Minute hinzufügen"
"Add a note for developers" = "Notiz für Entwickler hinzufügen"
"Add action" = "Aktion hinzufügen"
"Add actions for when no condition holds" = "Aktionen für den Fall hinzufügen, dass keine Bedingung zutrifft"
@@ -88,6 +89,7 @@ status = "draft — needs native review"
"All comments" = "Alle Kommentare"
"All components" = "Alle Komponenten"
"All fonts" = "Alle Schriften"
"All objects" = "Alle Objekte"
"All supported properties" = "Alle unterstützten Eigenschaften"
"All variables" = "Alle Variablen"
"Alpha" = "Alpha"
@@ -194,6 +196,8 @@ status = "draft — needs native review"
"Checking the server…" = "Server wird geprüft…"
"Chevron" = "Chevron"
"Clear from this list" = "Aus dieser Liste entfernen"
"Clear results" = "Ergebnisse löschen"
"Click objects to vote; click your badge to take a vote back" = "Klicken Sie auf Objekte, um abzustimmen; klicken Sie auf Ihr Abzeichen, um eine Stimme zurückzunehmen"
"Code or invite link" = "Code oder Einladungslink"
"Color profile: {name}" = "Farbprofil: {name}"
"CSS custom properties…" = "Benutzerdefinierte CSS-Eigenschaften…"
@@ -436,6 +440,7 @@ status = "draft — needs native review"
"Empty" = "Leer"
"End" = "Ende"
"End point" = "Endpunkt"
"End vote" = "Abstimmung beenden"
"Ends the session; everyone keeps their copy" = "Beendet die Sitzung; alle behalten ihre Kopie"
"Enter" = "Eingabe"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Geben Sie die Adresse eines figcraft-server ein und drücken Sie auf Durchsuchen, um die angebotenen Plugins zu sehen."
@@ -742,6 +747,7 @@ status = "draft — needs native review"
"Min H" = "Min. H"
"Min W" = "Min. B"
"Minimize" = "Minimieren"
"Minutes" = "Minuten"
"Mirror angle" = "Winkel spiegeln"
"Mirror angle and length" = "Winkel und Länge spiegeln"
"Mirroring" = "Spiegelung"
@@ -833,6 +839,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "Noch keine Textstile. Klicken Sie auf +, um einen aus dem ausgewählten Text zu erstellen."
"No variables fit here." = "Hier passen keine Variablen."
"No variables yet. Create them in Local variables." = "Noch keine Variablen. Erstellen Sie sie unter Lokale Variablen."
"No votes yet" = "Noch keine Stimmen"
"Nobody else is here yet" = "Noch niemand sonst hier"
"Noise" = "Rauschen"
"None" = "Keine"
@@ -915,6 +922,7 @@ status = "draft — needs native review"
"Page deleted" = "Seite gelöscht"
"Pages" = "Seiten"
"Pages added to the document" = "Seiten zum Dokument hinzugefügt"
"Pause" = "Pausieren"
"Pink" = "Rosa"
"Placed on the current page" = "Auf der aktuellen Seite platziert"
"Pages: {pages}" = "Seiten: {pages}"
@@ -1090,6 +1098,8 @@ status = "draft — needs native review"
"Restore main component" = "Hauptkomponente wiederherstellen"
"Restore this version" = "Diese Version wiederherstellen"
"Restored “{name}”" = "„{name}“ wiederhergestellt"
"Results" = "Ergebnisse"
"Resume" = "Fortsetzen"
"Reverse the order" = "Reihenfolge umkehren"
"Reversed triangle" = "Umgekehrtes Dreieck"
"Review and merge" = "Prüfen und zusammenführen"
@@ -1251,17 +1261,20 @@ status = "draft — needs native review"
"Start on the Home screen" = "Mit Start beginnen"
"Start point" = "Startpunkt"
"Start sharing" = "Teilen starten"
"Start vote" = "Abstimmung starten"
"Starting to share…" = "Teilen wird gestartet…"
"Status" = "Status"
"Sticky" = "Fixiert"
"Sticky note" = "Haftnotiz"
"Sticky note colors" = "Haftnotizfarben"
"Sticky notes" = "Haftnotizen"
"Sticky notes, shapes and connectors" = "Haftnotizen, Formen und Verbinder"
"Stop" = "Beenden"
"Stop following {name}" = "{name} nicht mehr folgen"
"Stop session" = "Sitzung beenden"
"Stop sharing" = "Teilen beenden"
"Stop spotlighting" = "Spotlight beenden"
"Stop timer" = "Timer stoppen"
"Stop {n} {hex}" = "Farbunterbrechung {n} {hex}"
"Straight" = "Gerade"
"Stretch" = "Strecken"
@@ -1347,6 +1360,7 @@ status = "draft — needs native review"
"Tidy up" = "Aufräumen"
"Tile" = "Kachel"
"Tile type" = "Kacheltyp"
"Timer" = "Timer"
"Tint" = "Tönung"
"Title" = "Titel"
"Title case" = "Großschreibung am Wortanfang"
@@ -1437,6 +1451,8 @@ status = "draft — needs native review"
"Visibility" = "Sichtbarkeit"
"Visible" = "Sichtbar"
"Vivid" = "Lebhaft"
"Votes per person" = "Stimmen pro Person"
"Voting session" = "Abstimmung"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND ist gesetzt und legt die Grafik-API fest; ohne diese Variable wählt FigCraft selbst."
"Warm" = "Warm"
"Watch" = "Smartwatch"
@@ -1646,6 +1662,7 @@ status = "draft — needs native review"
"{n} layers selected" = { one = "{n} Ebene ausgewählt", other = "{n} Ebenen ausgewählt" }
"{n} layers · {parts}" = { one = "{n} Ebene · {parts}", other = "{n} Ebenen · {parts}" }
"{n} matches" = { one = "{n} Treffer", other = "{n} Treffer" }
"{n} min" = "{n} Min."
"{n} minutes ago" = { one = "vor {n} Minute", other = "vor {n} Minuten" }
"{n} months ago" = { one = "vor {n} Monat", other = "vor {n} Monaten" }
"{n} more notes omitted from this report" = { one = "{n} weitere Notiz in diesem Bericht nicht aufgeführt", other = "{n} weitere Notizen in diesem Bericht nicht aufgeführt" }
@@ -1658,6 +1675,8 @@ status = "draft — needs native review"
"{n} rows" = { one = "{n} Zeile", other = "{n} Zeilen" }
"{n} styles" = { one = "{n} Stil", other = "{n} Stile" }
"{n} variables" = { one = "{n} Variable", other = "{n} Variablen" }
"{n} votes" = { one = "{n} Stimme", other = "{n} Stimmen" }
"{n} votes left" = { one = "Noch {n} Stimme", other = "Noch {n} Stimmen" }
"{n} weeks ago" = { one = "vor {n} Woche", other = "vor {n} Wochen" }
"{n} years ago" = { one = "vor {n} Jahr", other = "vor {n} Jahren" }
"{n}d" = "{n} T."
@@ -1719,6 +1738,10 @@ status = "draft — needs native review"
"board.shape" = "Form mit Text"
"board.stamp" = "Stempel"
"board.sticky" = "Haftnotiz"
"board.timer" = "Timer"
"board.timerState" = "Timer-Status"
"board.vote" = "Abstimmung"
"board.voteResults" = "Abstimmungsergebnisse"
"bool.exclude" = "Auswahl ausschließen"
"bool.flatten" = "Boolesche Gruppe zusammenfügen"
"bool.intersect" = "Schnittmenge der Auswahl"
+19
View File
@@ -36,6 +36,7 @@ name = "English"
"Add a description" = "Add a description"
"Add a frame (F) or a shape (R) to get started" = "Add a frame (F) or a shape (R) to get started"
"Add a layer or component to tile first" = "Add a layer or component to tile first"
"Add a minute" = "Add a minute"
"Add a note for developers" = "Add a note for developers"
"Add action" = "Add action"
"Add actions for when no condition holds" = "Add actions for when no condition holds"
@@ -90,6 +91,7 @@ name = "English"
"All comments" = "All comments"
"All components" = "All components"
"All fonts" = "All fonts"
"All objects" = "All objects"
"All supported properties" = "All supported properties"
"All the fonts this file uses are installed" = "All the fonts this file uses are installed"
"All variables" = "All variables"
@@ -205,6 +207,8 @@ name = "English"
"Checking the server…" = "Checking the server…"
"Chevron" = "Chevron"
"Clear from this list" = "Clear from this list"
"Clear results" = "Clear results"
"Click objects to vote; click your badge to take a vote back" = "Click objects to vote; click your badge to take a vote back"
"Code or invite link" = "Code or invite link"
"Color profile: {name}" = "Color profile: {name}"
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Colour and text styles, Light and Dark variables, buttons, inputs and cards"
@@ -457,6 +461,7 @@ name = "English"
"Empty" = "Empty"
"End" = "End"
"End point" = "End point"
"End vote" = "End vote"
"Ends the session; everyone keeps their copy" = "Ends the session; everyone keeps their copy"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Enter the address of a figcraft-server and press Browse to see the plugins it offers."
@@ -786,6 +791,7 @@ name = "English"
"Min H" = "Min H"
"Min W" = "Min W"
"Minimize" = "Minimize"
"Minutes" = "Minutes"
"Mirror angle" = "Mirror angle"
"Mirror angle and length" = "Mirror angle and length"
"Mirroring" = "Mirroring"
@@ -883,6 +889,7 @@ name = "English"
"No text styles yet. Click + to create one from the selected text." = "No text styles yet. Click + to create one from the selected text."
"No variables fit here." = "No variables fit here."
"No variables yet. Create them in Local variables." = "No variables yet. Create them in Local variables."
"No votes yet" = "No votes yet"
"Nobody else is here yet" = "Nobody else is here yet"
"Noise" = "Noise"
"None" = "None"
@@ -971,6 +978,7 @@ name = "English"
"Page deleted" = "Page deleted"
"Pages" = "Pages"
"Pages added to the document" = "Pages added to the document"
"Pause" = "Pause"
"Pink" = "Pink"
"Placed on the current page" = "Placed on the current page"
"Pages: {pages}" = "Pages: {pages}"
@@ -1154,6 +1162,8 @@ name = "English"
"Restore main component" = "Restore main component"
"Restore this version" = "Restore this version"
"Restored “{name}”" = "Restored “{name}”"
"Results" = "Results"
"Resume" = "Resume"
"Reverse the order" = "Reverse the order"
"Reversed triangle" = "Reversed triangle"
"Review and merge" = "Review and merge"
@@ -1334,17 +1344,20 @@ name = "English"
"Start point" = "Start point"
"Start sharing" = "Start sharing"
"Start sharing with the Share button, then pass on the code it gives" = "Start sharing with the Share button, then pass on the code it gives"
"Start vote" = "Start vote"
"Starting to share…" = "Starting to share…"
"Status" = "Status"
"Sticky" = "Sticky"
"Sticky note" = "Sticky note"
"Sticky note colors" = "Sticky note colors"
"Sticky notes" = "Sticky notes"
"Sticky notes, shapes and connectors" = "Sticky notes, shapes and connectors"
"Stop" = "Stop"
"Stop following {name}" = "Stop following {name}"
"Stop session" = "Stop session"
"Stop sharing" = "Stop sharing"
"Stop spotlighting" = "Stop spotlighting"
"Stop timer" = "Stop timer"
"Stop {n} {hex}" = "Stop {n} {hex}"
"Straight" = "Straight"
"Stretch" = "Stretch"
@@ -1434,6 +1447,7 @@ name = "English"
"Tidy up" = "Tidy up"
"Tile" = "Tile"
"Tile type" = "Tile type"
"Timer" = "Timer"
"Tint" = "Tint"
"Title" = "Title"
"Title case" = "Title case"
@@ -1530,6 +1544,8 @@ name = "English"
"Visibility" = "Visibility"
"Visible" = "Visible"
"Vivid" = "Vivid"
"Votes per person" = "Votes per person"
"Voting session" = "Voting session"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose."
"Warm" = "Warm"
"Watch" = "Watch"
@@ -1749,6 +1765,7 @@ name = "English"
"{n} layers selected" = { one = "{n} layer selected", other = "{n} layers selected" }
"{n} layers · {parts}" = { one = "{n} layer · {parts}", other = "{n} layers · {parts}" }
"{n} matches" = { one = "{n} match", other = "{n} matches" }
"{n} min" = "{n} min"
"{n} minutes ago" = { one = "{n} minute ago", other = "{n} minutes ago" }
"{n} months ago" = { one = "{n} month ago", other = "{n} months ago" }
"{n} more notes omitted from this report" = { one = "{n} more note omitted from this report", other = "{n} more notes omitted from this report" }
@@ -1762,6 +1779,8 @@ name = "English"
"{n} styles" = { one = "{n} style", other = "{n} styles" }
"{n} text layers" = { one = "{n} text layer", other = "{n} text layers" }
"{n} variables" = { one = "{n} variable", other = "{n} variables" }
"{n} votes" = { one = "{n} vote", other = "{n} votes" }
"{n} votes left" = { one = "{n} vote left", other = "{n} votes left" }
"{n} weeks ago" = { one = "{n} week ago", other = "{n} weeks ago" }
"{n} years ago" = { one = "{n} year ago", other = "{n} years ago" }
"{n}d" = "{n}d"
+1
View File
@@ -17,6 +17,7 @@ status = "draft — needs native review"
"Add a description" = "Agregar una descripción"
"Add a frame (F) or a shape (R) to get started" = "Agrega un marco (F) o una forma (R) para empezar"
"Add a layer or component to tile first" = "Agrega antes una capa o un componente para el mosaico"
"Add a minute" = "Agregar un minuto"
"Add a note for developers" = "Agrega una nota para desarrollo"
"Add action" = "Agregar acción"
"Add actions for when no condition holds" = "Agregar acciones para cuando no se cumpla ninguna condición"
+23
View File
@@ -8,9 +8,13 @@ name = "Español"
status = "draft — needs native review"
[ui]
"Add a minute" = "Añadir un minuto"
"All objects" = "Todos los objetos"
"Arrow at the end" = "Flecha al final"
"Check" = "Visto"
"Chevron" = "Cheurón"
"Clear results" = "Borrar resultados"
"Click objects to vote; click your badge to take a vote back" = "Haz clic en los objetos para votar; haz clic en tu insignia para retirar un voto"
"Compatibility notes" = "Notas de compatibilidad"
"Connector" = "Conector"
"Couldn't download the variables" = "No se pudieron descargar las variables"
@@ -458,6 +462,7 @@ status = "draft — needs native review"
"Empty" = "Vacía"
"End" = "Final"
"End point" = "Punto final"
"End vote" = "Terminar votación"
"Ends the session; everyone keeps their copy" = "Termina la sesión; cada persona conserva su copia"
"Enter" = "Intro"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Escribe la dirección de un figcraft-server y pulsa Explorar para ver los plugins que ofrece."
@@ -787,6 +792,7 @@ status = "draft — needs native review"
"Min H" = "Mín. H"
"Min W" = "Mín. W"
"Minimize" = "Minimizar"
"Minutes" = "Minutos"
"Mirror angle" = "Reflejar ángulo"
"Mirror angle and length" = "Reflejar ángulo y longitud"
"Mirroring" = "Reflejo"
@@ -884,6 +890,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "Aún no hay estilos de texto. Haz clic en + para crear uno a partir del texto seleccionado."
"No variables fit here." = "Ninguna variable encaja aquí."
"No variables yet. Create them in Local variables." = "Aún no hay variables. Créalas en Variables locales."
"No votes yet" = "Aún no hay votos"
"Nobody else is here yet" = "Aún no hay nadie más"
"Noise" = "Ruido"
"None" = "Ninguno"
@@ -972,6 +979,7 @@ status = "draft — needs native review"
"Page deleted" = "Página eliminada"
"Pages" = "Páginas"
"Pages added to the document" = "Páginas añadidas al documento"
"Pause" = "Pausar"
"Pink" = "Rosa"
"Placed on the current page" = "Colocado en la página actual"
"Pages: {pages}" = "Páginas: {pages}"
@@ -1155,6 +1163,8 @@ status = "draft — needs native review"
"Restore main component" = "Restaurar componente principal"
"Restore this version" = "Restaurar esta versión"
"Restored “{name}”" = "“{name}” restaurada"
"Results" = "Resultados"
"Resume" = "Reanudar"
"Reverse the order" = "Invertir el orden"
"Reversed triangle" = "Triángulo invertido"
"Review and merge" = "Revisar y fusionar"
@@ -1335,17 +1345,20 @@ status = "draft — needs native review"
"Start point" = "Punto inicial"
"Start sharing" = "Empezar a compartir"
"Start sharing with the Share button, then pass on the code it gives" = "Empieza a compartir con el botón Compartir y pasa el código que te da"
"Start vote" = "Iniciar votación"
"Starting to share…" = "Empezando a compartir…"
"Status" = "Estado"
"Sticky" = "Adherido"
"Sticky note" = "Nota adhesiva"
"Sticky note colors" = "Colores de las notas adhesivas"
"Sticky notes" = "Notas adhesivas"
"Sticky notes, shapes and connectors" = "Notas adhesivas, formas y conectores"
"Stop" = "Parar"
"Stop following {name}" = "Dejar de seguir a {name}"
"Stop session" = "Detener sesión"
"Stop sharing" = "Dejar de compartir"
"Stop spotlighting" = "Salir del foco"
"Stop timer" = "Detener temporizador"
"Stop {n} {hex}" = "Parada {n} {hex}"
"Straight" = "Recto"
"Stretch" = "Estirar"
@@ -1435,6 +1448,7 @@ status = "draft — needs native review"
"Tidy up" = "Ordenar"
"Tile" = "Mosaico"
"Tile type" = "Tipo de mosaico"
"Timer" = "Temporizador"
"Tint" = "Matiz"
"Title" = "Título"
"Title case" = "Tipo título"
@@ -1531,6 +1545,8 @@ status = "draft — needs native review"
"Visibility" = "Visibilidad"
"Visible" = "Visible"
"Vivid" = "Vívido"
"Votes per person" = "Votos por persona"
"Voting session" = "Votación"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND está definida e indica la API gráfica que se usa; si la quitas, FigCraft elige por sí mismo."
"Warm" = "Cálido"
"Watch" = "Reloj"
@@ -1750,6 +1766,7 @@ status = "draft — needs native review"
"{n} layers selected" = { one = "{n} capa seleccionada", other = "{n} capas seleccionadas" }
"{n} layers · {parts}" = { one = "{n} capa · {parts}", other = "{n} capas · {parts}" }
"{n} matches" = { one = "{n} coincidencia", other = "{n} coincidencias" }
"{n} min" = "{n} min"
"{n} minutes ago" = { one = "hace {n} minuto", other = "hace {n} minutos" }
"{n} months ago" = { one = "hace {n} mes", other = "hace {n} meses" }
"{n} more notes omitted from this report" = { one = "{n} nota más omitida de este informe", other = "{n} notas más omitidas de este informe" }
@@ -1763,6 +1780,8 @@ status = "draft — needs native review"
"{n} styles" = { one = "{n} estilo", other = "{n} estilos" }
"{n} text layers" = { one = "{n} capa de texto", other = "{n} capas de texto" }
"{n} variables" = { one = "{n} variable", other = "{n} variables" }
"{n} votes" = { one = "{n} voto", other = "{n} votos" }
"{n} votes left" = { one = "Te queda {n} voto", other = "Te quedan {n} votos" }
"{n} weeks ago" = { one = "hace {n} semana", other = "hace {n} semanas" }
"{n} years ago" = { one = "hace {n} año", other = "hace {n} años" }
"{n}d" = "{n} d"
@@ -1826,6 +1845,10 @@ status = "draft — needs native review"
"board.shape" = "Forma con texto"
"board.stamp" = "Sello"
"board.sticky" = "Nota adhesiva"
"board.timer" = "Temporizador"
"board.timerState" = "Estado del temporizador"
"board.vote" = "Votación"
"board.voteResults" = "Resultados de la votación"
"bool.exclude" = "Excluir selección"
"bool.flatten" = "Acoplar booleana"
"bool.intersect" = "Intersecar selección"
+23
View File
@@ -38,6 +38,7 @@ status = "draft — needs native review"
"Add a description" = "Lisää kuvaus"
"Add a frame (F) or a shape (R) to get started" = "Aloita lisäämällä kehys (F) tai muoto (R)"
"Add a layer or component to tile first" = "Lisää ensin laatoitettava taso tai komponentti"
"Add a minute" = "Lisää minuutti"
"Add a note for developers" = "Lisää huomautus kehittäjille"
"Add action" = "Lisää toiminto"
"Add actions for when no condition holds" = "Lisää toimintoja tilanteeseen, jossa mikään ehto ei täyty"
@@ -92,6 +93,7 @@ status = "draft — needs native review"
"All comments" = "Kaikki kommentit"
"All components" = "Kaikki komponentit"
"All fonts" = "Kaikki fontit"
"All objects" = "Kaikki objektit"
"All supported properties" = "Kaikki tuetut ominaisuudet"
"All the fonts this file uses are installed" = "Kaikki tämän tiedoston käyttämät fontit on asennettu"
"All variables" = "Kaikki muuttujat"
@@ -235,8 +237,10 @@ status = "draft — needs native review"
"Circle" = "Ympyrä"
"Clear from this list" = "Poista tästä luettelosta"
"Clear recent" = "Tyhjennä viimeisimmät"
"Clear results" = "Tyhjennä tulokset"
"Click + to replace mixed content" = "Korvaa vaihteleva sisältö napsauttamalla +"
"Click anywhere on the canvas to leave one." = "Lisää huomautus napsauttamalla mitä tahansa piirtoalueen kohtaa."
"Click objects to vote; click your badge to take a vote back" = "Äänestä napsauttamalla objekteja; peru ääni napsauttamalla merkkiäsi"
"Click or drag to merge · {alt} removes" = "Yhdistä napsauttamalla tai vetämällä · {alt} poistaa"
"Click or drag to remove · {alt} merges" = "Poista napsauttamalla tai vetämällä · {alt} yhdistää"
"Click to record a key" = "Tallenna näppäin napsauttamalla"
@@ -456,6 +460,7 @@ status = "draft — needs native review"
"Empty" = "Tyhjä"
"End" = "Loppu"
"End point" = "Loppupiste"
"End vote" = "Lopeta äänestys"
"Ends the session; everyone keeps their copy" = "Lopettaa istunnon. Kaikki säilyttävät oman kopionsa."
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Anna figcraft-server-palvelimen osoite ja näytä sen tarjoamat liitännäiset painamalla Selaa."
@@ -782,6 +787,7 @@ status = "draft — needs native review"
"Min H" = "Vähimmäiskorkeus"
"Min W" = "Vähimmäisleveys"
"Minimize" = "Pienennä"
"Minutes" = "Minuutit"
"Mirror angle" = "Peilaa kulma"
"Mirror angle and length" = "Peilaa kulma ja pituus"
"Mirroring" = "Peilaus"
@@ -879,6 +885,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "Ei vielä tekstityylejä. Luo tyyli valitusta tekstistä napsauttamalla +."
"No variables fit here." = "Tähän ei sovi muuttujia."
"No variables yet. Create them in Local variables." = "Ei vielä muuttujia. Luo niitä Paikalliset muuttujat -näkymässä."
"No votes yet" = "Ei vielä ääniä"
"Nobody else is here yet" = "Kukaan muu ei ole vielä paikalla"
"Noise" = "Kohina"
"None" = "Ei mitään"
@@ -985,6 +992,7 @@ status = "draft — needs native review"
"Pattern alignment" = "Kuvion tasaus"
"Pattern scale" = "Kuvion skaalaus"
"Pattern source" = "Kuvion lähde"
"Pause" = "Keskeytä"
"Pen" = "Kynä"
"Pencil" = "Lyijykynä"
"People in this file" = "Tämän tiedoston käyttäjät"
@@ -1146,6 +1154,8 @@ status = "draft — needs native review"
"Restore main component" = "Palauta pääkomponentti"
"Restore this version" = "Palauta tämä versio"
"Restored “{name}”" = "”{name}” palautettu"
"Results" = "Tulokset"
"Resume" = "Jatka"
"Reverse the order" = "Käännä järjestys"
"Reversed triangle" = "Käänteinen kolmio"
"Review and merge" = "Tarkista ja yhdistä"
@@ -1324,17 +1334,20 @@ status = "draft — needs native review"
"Start point" = "Alkupiste"
"Start sharing" = "Aloita jakaminen"
"Start sharing with the Share button, then pass on the code it gives" = "Aloita jakaminen Jaa-painikkeella ja välitä sen antama koodi"
"Start vote" = "Aloita äänestys"
"Starting to share…" = "Aloitetaan jakamista…"
"Status" = "Tila"
"Sticky" = "Kiinnittyvä"
"Sticky note" = "Muistilappu"
"Sticky note colors" = "Muistilappujen värit"
"Sticky notes" = "Muistilaput"
"Sticky notes, shapes and connectors" = "Muistilaput, muodot ja yhdistimet"
"Stop" = "Pysäytä"
"Stop following {name}" = "Lopeta käyttäjän {name} seuraaminen"
"Stop session" = "Lopeta istunto"
"Stop sharing" = "Lopeta jakaminen"
"Stop spotlighting" = "Lopeta esiin nostaminen"
"Stop timer" = "Pysäytä ajastin"
"Stop {n} {hex}" = "Piste {n} {hex}"
"Straight" = "Suora"
"Stretch" = "Venytys"
@@ -1420,6 +1433,7 @@ status = "draft — needs native review"
"Tidy up" = "Järjestä tasavälein"
"Tile" = "Laatoitus"
"Tile type" = "Laatan tyyppi"
"Timer" = "Ajastin"
"Tint" = "Sävytys"
"Title" = "Otsikko"
"Title case" = "Sanojen isot alkukirjaimet"
@@ -1514,6 +1528,8 @@ status = "draft — needs native review"
"Visibility" = "Näkyvyys"
"Visible" = "Näkyvä"
"Vivid" = "Kirkas"
"Votes per person" = "Ääniä henkilöä kohden"
"Voting session" = "Äänestys"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND on asetettu ja määrittää käytettävän grafiikkarajapinnan. Poista se käytöstä, jotta FigCraft voi valita."
"Warm" = "Lämmin"
"Watch" = "Seuraa"
@@ -1731,6 +1747,7 @@ status = "draft — needs native review"
"{n} layers selected" = { one = "{n} taso valittu", other = "{n} tasoa valittu" }
"{n} layers · {parts}" = { one = "{n} taso · {parts}", other = "{n} tasoa · {parts}" }
"{n} matches" = { one = "{n} osuma", other = "{n} osumaa" }
"{n} min" = "{n} min"
"{n} minutes ago" = { one = "{n} minuutti sitten", other = "{n} minuuttia sitten" }
"{n} months ago" = { one = "{n} kuukausi sitten", other = "{n} kuukautta sitten" }
"{n} more notes omitted from this report" = { one = "Tästä raportista on jätetty pois vielä {n} huomautus", other = "Tästä raportista on jätetty pois vielä {n} huomautusta" }
@@ -1744,6 +1761,8 @@ status = "draft — needs native review"
"{n} styles" = { one = "{n} tyyli", other = "{n} tyyliä" }
"{n} text layers" = { one = "{n} tekstitaso", other = "{n} tekstitasoa" }
"{n} variables" = { one = "{n} muuttuja", other = "{n} muuttujaa" }
"{n} votes" = { one = "{n} ääni", other = "{n} ääntä" }
"{n} votes left" = { one = "{n} ääni jäljellä", other = "{n} ääntä jäljellä" }
"{n} weeks ago" = { one = "{n} viikko sitten", other = "{n} viikkoa sitten" }
"{n} years ago" = { one = "{n} vuosi sitten", other = "{n} vuotta sitten" }
"{n}d" = "{n}pv"
@@ -1805,6 +1824,10 @@ status = "draft — needs native review"
"board.shape" = "Muoto ja teksti"
"board.stamp" = "Leima"
"board.sticky" = "Muistilappu"
"board.timer" = "Ajastin"
"board.timerState" = "Ajastimen tila"
"board.vote" = "Äänestys"
"board.voteResults" = "Äänestyksen tulokset"
"bool.exclude" = "Sulje valinnan päällekkäinen alue pois"
"bool.flatten" = "Yhdistä Boolen muoto vektoriksi"
"bool.intersect" = "Leikkaa valinnan yhteinen alue"
+23
View File
@@ -37,6 +37,7 @@ status = "draft — needs native review"
"Add a description" = "Ajouter une description"
"Add a frame (F) or a shape (R) to get started" = "Ajoutez un cadre (F) ou une forme (R) pour commencer"
"Add a layer or component to tile first" = "Ajoutez d’abord un calque ou un composant à répéter en mosaïque"
"Add a minute" = "Ajouter une minute"
"Add a note for developers" = "Ajouter une note pour les développeurs"
"Add action" = "Ajouter une action"
"Add actions for when no condition holds" = "Ajouter des actions pour le cas où aucune condition n’est remplie"
@@ -91,6 +92,7 @@ status = "draft — needs native review"
"All comments" = "Tous les commentaires"
"All components" = "Tous les composants"
"All fonts" = "Toutes les polices"
"All objects" = "Tous les objets"
"All supported properties" = "Toutes les propriétés prises en charge"
"All variables" = "Toutes les variables"
"Alpha" = "Alpha"
@@ -203,6 +205,8 @@ status = "draft — needs native review"
"Checking the server…" = "Vérification auprès du serveur…"
"Chevron" = "Chevron"
"Clear from this list" = "Effacer de cette liste"
"Clear results" = "Effacer les résultats"
"Click objects to vote; click your badge to take a vote back" = "Cliquez sur des objets pour voter ; cliquez sur votre badge pour retirer un vote"
"Code or invite link" = "Code ou lien d’invitation"
"Color profile: {name}" = "Profil colorimétrique : {name}"
"CSS custom properties…" = "Propriétés personnalisées CSS…"
@@ -455,6 +459,7 @@ status = "draft — needs native review"
"Empty" = "Vide"
"End" = "Fin"
"End point" = "Point final"
"End vote" = "Terminer le vote"
"Ends the session; everyone keeps their copy" = "Met fin à la session ; chacun conserve sa copie"
"Enter" = "Entrée"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Saisissez l’adresse d’un figcraft-server et appuyez sur Parcourir pour voir les plugins qu’il propose."
@@ -782,6 +787,7 @@ status = "draft — needs native review"
"Min H" = "H min."
"Min W" = "L min."
"Minimize" = "Réduire"
"Minutes" = "Minutes"
"Mirror angle" = "Symétrie d’angle"
"Mirror angle and length" = "Symétrie d’angle et de longueur"
"Mirroring" = "Symétrie"
@@ -877,6 +883,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "Aucun style de texte pour l’instant. Cliquez sur + pour en créer un à partir du texte sélectionné."
"No variables fit here." = "Aucune variable ne convient ici."
"No variables yet. Create them in Local variables." = "Aucune variable pour l’instant. Créez-en dans Variables locales."
"No votes yet" = "Aucun vote pour l’instant"
"Nobody else is here yet" = "Personne d’autre n’est encore là"
"Noise" = "Bruit"
"None" = "Aucun"
@@ -965,6 +972,7 @@ status = "draft — needs native review"
"Page deleted" = "Page supprimée"
"Pages" = "Pages"
"Pages added to the document" = "Pages ajoutées au document"
"Pause" = "Pause"
"Pick the Frame tool, then drag on the canvas or choose a device size" = "Choisissez l’outil Cadre, puis faites glisser sur le canevas ou choisissez une taille d’appareil"
"Pink" = "Rose"
"Placed on the current page" = "Placé sur la page actuelle"
@@ -1147,6 +1155,8 @@ status = "draft — needs native review"
"Restore main component" = "Restaurer le composant principal"
"Restore this version" = "Restaurer cette version"
"Restored “{name}”" = "« {name} » restaurée"
"Results" = "Résultats"
"Resume" = "Reprendre"
"Reverse the order" = "Inverser l’ordre"
"Reversed triangle" = "Triangle inversé"
"Review and merge" = "Relire et fusionner"
@@ -1323,17 +1333,20 @@ status = "draft — needs native review"
"Start point" = "Point de départ"
"Start sharing" = "Commencer le partage"
"Start sharing with the Share button, then pass on the code it gives" = "Commencez à partager avec le bouton Partager, puis communiquez le code qu’il donne"
"Start vote" = "Lancer le vote"
"Starting to share…" = "Démarrage du partage…"
"Status" = "Statut"
"Sticky" = "Collant"
"Sticky note" = "Note autocollante"
"Sticky note colors" = "Couleurs des notes autocollantes"
"Sticky notes" = "Notes autocollantes"
"Sticky notes, shapes and connectors" = "Notes autocollantes, formes et connecteurs"
"Stop" = "Arrêter"
"Stop following {name}" = "Ne plus suivre {name}"
"Stop session" = "Arrêter la session"
"Stop sharing" = "Arrêter le partage"
"Stop spotlighting" = "Quitter les projecteurs"
"Stop timer" = "Arrêter le minuteur"
"Stop {n} {hex}" = "Arrêt {n} {hex}"
"Straight" = "Droit"
"Stretch" = "Étirer"
@@ -1421,6 +1434,7 @@ status = "draft — needs native review"
"Tidy up" = "Ranger"
"Tile" = "Mosaïque"
"Tile type" = "Type de mosaïque"
"Timer" = "Minuteur"
"Tint" = "Nuance"
"Title" = "Titre"
"Title case" = "Casse de titre"
@@ -1515,6 +1529,8 @@ status = "draft — needs native review"
"Visibility" = "Visibilité"
"Visible" = "Visible"
"Vivid" = "Vif"
"Votes per person" = "Votes par personne"
"Voting session" = "Session de vote"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND est définie et impose l’API graphique ; sans elle, FigCraft choisit lui-même."
"Warm" = "Chaud"
"Watch" = "Montre"
@@ -1732,6 +1748,7 @@ status = "draft — needs native review"
"{n} layers selected" = { one = "{n} calque sélectionné", other = "{n} calques sélectionnés" }
"{n} layers · {parts}" = { one = "{n} calque · {parts}", other = "{n} calques · {parts}" }
"{n} matches" = { one = "{n} résultat", other = "{n} résultats" }
"{n} min" = "{n} min"
"{n} minutes ago" = { one = "il y a {n} minute", other = "il y a {n} minutes" }
"{n} months ago" = { one = "il y a {n} mois", other = "il y a {n} mois" }
"{n} more notes omitted from this report" = { one = "{n} autre note omise de ce rapport", other = "{n} autres notes omises de ce rapport" }
@@ -1744,6 +1761,8 @@ status = "draft — needs native review"
"{n} rows" = { one = "{n} ligne", other = "{n} lignes" }
"{n} styles" = { one = "{n} style", other = "{n} styles" }
"{n} variables" = { one = "{n} variable", other = "{n} variables" }
"{n} votes" = { one = "{n} vote", other = "{n} votes" }
"{n} votes left" = { one = "{n} vote restant", other = "{n} votes restants" }
"{n} weeks ago" = { one = "il y a {n} semaine", other = "il y a {n} semaines" }
"{n} years ago" = { one = "il y a {n} an", other = "il y a {n} ans" }
"{n}d" = "{n} j"
@@ -1806,6 +1825,10 @@ status = "draft — needs native review"
"board.shape" = "Forme avec texte"
"board.stamp" = "Tampon"
"board.sticky" = "Note autocollante"
"board.timer" = "Minuteur"
"board.timerState" = "État du minuteur"
"board.vote" = "Session de vote"
"board.voteResults" = "Résultats du vote"
"bool.exclude" = "Exclure la sélection"
"bool.flatten" = "Aplatir le booléen"
"bool.intersect" = "Intersection de la sélection"
+23
View File
@@ -37,6 +37,7 @@ status = "draft — needs native review"
"Add a description" = "Tambah deskripsi"
"Add a frame (F) or a shape (R) to get started" = "Tambahkan frame (F) atau bentuk (R) untuk memulai"
"Add a layer or component to tile first" = "Tambahkan dulu layer atau component untuk dijadikan ubin"
"Add a minute" = "Tambah satu menit"
"Add a note for developers" = "Tambah catatan untuk developer"
"Add action" = "Tambah aksi"
"Add actions for when no condition holds" = "Tambah aksi saat tidak ada kondisi yang terpenuhi"
@@ -90,6 +91,7 @@ status = "draft — needs native review"
"All comments" = "Semua komentar"
"All components" = "Semua component"
"All fonts" = "Semua font"
"All objects" = "Semua objek"
"All supported properties" = "Semua properti yang didukung"
"All variables" = "Semua variabel"
"Alpha" = "Alfa"
@@ -202,6 +204,8 @@ status = "draft — needs native review"
"Checking the server…" = "Memeriksa server…"
"Chevron" = "Chevron"
"Clear from this list" = "Hapus dari daftar ini"
"Clear results" = "Hapus hasil"
"Click objects to vote; click your badge to take a vote back" = "Klik objek untuk memberi suara; klik lencana Anda untuk menarik suara"
"Code or invite link" = "Kode atau link undangan"
"Color profile: {name}" = "Profil warna: {name}"
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Gaya warna dan teks, variabel Light dan Dark, tombol, input, dan kartu"
@@ -445,6 +449,7 @@ status = "draft — needs native review"
"Empty" = "Kosong"
"End" = "Akhir"
"End point" = "Titik akhir"
"End vote" = "Akhiri pemungutan suara"
"Ends the session; everyone keeps their copy" = "Mengakhiri sesi; semua orang tetap memiliki salinannya"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Masukkan alamat figcraft-server lalu tekan Jelajahi untuk melihat plugin yang ditawarkannya."
@@ -769,6 +774,7 @@ status = "draft — needs native review"
"Min H" = "Min T"
"Min W" = "Min L"
"Minimize" = "Minimalkan"
"Minutes" = "Menit"
"Mirror angle" = "Cerminkan sudut"
"Mirror angle and length" = "Cerminkan sudut dan panjang"
"Mirroring" = "Pencerminan"
@@ -863,6 +869,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "Belum ada gaya teks. Klik + untuk membuat dari teks yang dipilih."
"No variables fit here." = "Tidak ada variabel yang sesuai di sini."
"No variables yet. Create them in Local variables." = "Belum ada variabel. Buat di Variabel lokal."
"No votes yet" = "Belum ada suara"
"Nobody else is here yet" = "Belum ada orang lain di sini"
"Noise" = "Noise"
"None" = "Tidak ada"
@@ -947,6 +954,7 @@ status = "draft — needs native review"
"Page deleted" = "Halaman dihapus"
"Pages" = "Halaman"
"Pages added to the document" = "Halaman ditambahkan ke dokumen"
"Pause" = "Jeda"
"Pink" = "Merah muda"
"Placed on the current page" = "Ditempatkan di halaman saat ini"
"Pages: {pages}" = "Halaman: {pages}"
@@ -1124,6 +1132,8 @@ status = "draft — needs native review"
"Restore main component" = "Pulihkan main component"
"Restore this version" = "Pulihkan versi ini"
"Restored “{name}”" = "“{name}” dipulihkan"
"Results" = "Hasil"
"Resume" = "Lanjutkan"
"Reverse the order" = "Balik urutan"
"Reversed triangle" = "Segitiga terbalik"
"Review and merge" = "Tinjau dan merge"
@@ -1298,17 +1308,20 @@ status = "draft — needs native review"
"Start point" = "Titik awal"
"Start sharing" = "Mulai berbagi"
"Start sharing with the Share button, then pass on the code it gives" = "Mulai berbagi dengan tombol Bagikan, lalu teruskan kode yang diberikannya"
"Start vote" = "Mulai pemungutan suara"
"Starting to share…" = "Memulai berbagi…"
"Status" = "Status"
"Sticky" = "Menempel"
"Sticky note" = "Catatan tempel"
"Sticky note colors" = "Warna catatan tempel"
"Sticky notes" = "Catatan tempel"
"Sticky notes, shapes and connectors" = "Catatan tempel, bentuk, dan konektor"
"Stop" = "Hentikan"
"Stop following {name}" = "Berhenti mengikuti {name}"
"Stop session" = "Hentikan sesi"
"Stop sharing" = "Berhenti berbagi"
"Stop spotlighting" = "Hentikan sorotan"
"Stop timer" = "Hentikan pengatur waktu"
"Stop {n} {hex}" = "Titik {n} {hex}"
"Straight" = "Lurus"
"Stretch" = "Regangkan"
@@ -1390,6 +1403,7 @@ status = "draft — needs native review"
"Tidy up" = "Rapikan"
"Tile" = "Ubin"
"Tile type" = "Jenis ubin"
"Timer" = "Pengatur waktu"
"Tint" = "Rona"
"Title" = "Judul"
"Title case" = "Huruf Awal Kapital"
@@ -1481,6 +1495,8 @@ status = "draft — needs native review"
"Visibility" = "Visibilitas"
"Visible" = "Terlihat"
"Vivid" = "Cerah"
"Votes per person" = "Suara per orang"
"Voting session" = "Sesi pemungutan suara"
"Warm" = "Hangat"
"Watch" = "Jam tangan"
"Wavy" = "Bergelombang"
@@ -1694,6 +1710,7 @@ status = "draft — needs native review"
"{n} layers selected" = { other = "{n} layer dipilih" }
"{n} layers · {parts}" = { other = "{n} layer · {parts}" }
"{n} matches" = { other = "{n} kecocokan" }
"{n} min" = "{n} mnt"
"{n} minutes ago" = { other = "{n} menit lalu" }
"{n} months ago" = { other = "{n} bulan lalu" }
"{n} more notes omitted from this report" = { other = "{n} catatan lainnya tidak dicantumkan dalam laporan ini" }
@@ -1706,6 +1723,8 @@ status = "draft — needs native review"
"{n} rows" = { other = "{n} baris" }
"{n} styles" = { other = "{n} gaya" }
"{n} variables" = { other = "{n} variabel" }
"{n} votes" = { other = "{n} suara" }
"{n} votes left" = { other = "Sisa {n} suara" }
"{n} weeks ago" = { other = "{n} minggu lalu" }
"{n} years ago" = { other = "{n} tahun lalu" }
"{n}d" = "{n} hr"
@@ -1767,6 +1786,10 @@ status = "draft — needs native review"
"board.shape" = "Bentuk dengan teks"
"board.stamp" = "Stempel"
"board.sticky" = "Catatan tempel"
"board.timer" = "Pengatur waktu"
"board.timerState" = "Status pengatur waktu"
"board.vote" = "Sesi pemungutan suara"
"board.voteResults" = "Hasil pemungutan suara"
"bool.exclude" = "Kecualikan pilihan"
"bool.flatten" = "Satukan boolean"
"bool.intersect" = "Iriskan pilihan"
+23
View File
@@ -39,6 +39,7 @@ status = "draft — needs native review"
"Add a description" = "説明を追加"
"Add a frame (F) or a shape (R) to get started" = "フレーム(F)またはシェイプ(R)を追加して開始"
"Add a layer or component to tile first" = "先に並べるレイヤーまたはコンポーネントを追加"
"Add a minute" = "1分追加"
"Add a note for developers" = "開発者向けのメモを追加"
"Add action" = "アクションを追加"
"Add actions for when no condition holds" = "どの条件にも当てはまらないときのアクションを追加"
@@ -92,6 +93,7 @@ status = "draft — needs native review"
"All comments" = "すべてのコメント"
"All components" = "すべてのコンポーネント"
"All fonts" = "すべてのフォント"
"All objects" = "すべてのオブジェクト"
"All supported properties" = "サポートされているすべてのプロパティ"
"All variables" = "すべての変数"
"Alpha" = "アルファ"
@@ -204,6 +206,8 @@ status = "draft — needs native review"
"Checking the server…" = "サーバーを確認中…"
"Chevron" = "シェブロン"
"Clear from this list" = "このリストから消去"
"Clear results" = "結果をクリア"
"Click objects to vote; click your badge to take a vote back" = "オブジェクトをクリックして投票、自分のバッジをクリックして票を取り消し"
"Code or invite link" = "コードまたは招待リンク"
"Color profile: {name}" = "カラープロファイル:{name}"
"CSS custom properties…" = "CSS カスタムプロパティ…"
@@ -450,6 +454,7 @@ status = "draft — needs native review"
"Empty" = "空"
"End" = "終了"
"End point" = "終点"
"End vote" = "投票を終了"
"Ends the session; everyone keeps their copy" = "セッションを終了。各自のコピーは残る"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "figcraft-server のアドレスを入力し、「参照」を押すと、提供されているプラグインが表示される。"
@@ -776,6 +781,7 @@ status = "draft — needs native review"
"Min H" = "最小 H"
"Min W" = "最小 W"
"Minimize" = "最小化"
"Minutes" = "分"
"Mirror angle" = "角度を対称にする"
"Mirror angle and length" = "角度と長さを対称にする"
"Mirroring" = "対称"
@@ -870,6 +876,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "テキストスタイルはまだない。+ をクリックして選択したテキストから作成。"
"No variables fit here." = "ここに使える変数はない。"
"No variables yet. Create them in Local variables." = "変数はまだない。ローカル変数で作成。"
"No votes yet" = "まだ投票はありません"
"Nobody else is here yet" = "まだ他に誰もいない"
"Noise" = "ノイズ"
"None" = "なし"
@@ -955,6 +962,7 @@ status = "draft — needs native review"
"Page deleted" = "ページは削除済み"
"Pages" = "ページ"
"Pages added to the document" = "ページをドキュメントに追加した"
"Pause" = "一時停止"
"Pink" = "ピンク"
"Placed on the current page" = "現在のページに配置した"
"Pages: {pages}" = "ページ:{pages}"
@@ -1134,6 +1142,8 @@ status = "draft — needs native review"
"Restore main component" = "メインコンポーネントを復元"
"Restore this version" = "このバージョンを復元"
"Restored “{name}”" = "「{name}」を復元済み"
"Results" = "結果"
"Resume" = "再開"
"Reverse the order" = "順序を反転"
"Reversed triangle" = "逆三角形"
"Review and merge" = "レビューしてマージ"
@@ -1310,17 +1320,20 @@ status = "draft — needs native review"
"Start point" = "始点"
"Start sharing" = "共有を開始"
"Start sharing with the Share button, then pass on the code it gives" = "共有ボタンで共有を開始し、表示されたコードを相手に伝える"
"Start vote" = "投票を開始"
"Starting to share…" = "共有を開始中…"
"Status" = "ステータス"
"Sticky" = "固定"
"Sticky note" = "付箋"
"Sticky note colors" = "付箋の色"
"Sticky notes" = "付箋"
"Sticky notes, shapes and connectors" = "付箋、シェイプ、コネクタ"
"Stop" = "停止"
"Stop following {name}" = "{name} のフォローを停止"
"Stop session" = "セッションを停止"
"Stop sharing" = "共有を停止"
"Stop spotlighting" = "スポットライトを停止"
"Stop timer" = "タイマーを停止"
"Stop {n} {hex}" = "分岐点 {n} {hex}"
"Straight" = "直線"
"Stretch" = "伸縮"
@@ -1404,6 +1417,7 @@ status = "draft — needs native review"
"Tidy up" = "整列"
"Tile" = "タイル"
"Tile type" = "タイルの種類"
"Timer" = "タイマー"
"Tint" = "ティント"
"Title" = "タイトル"
"Title case" = "タイトルケース"
@@ -1497,6 +1511,8 @@ status = "draft — needs native review"
"Visibility" = "表示"
"Visible" = "表示"
"Vivid" = "ビビッド"
"Votes per person" = "1人あたりの票数"
"Voting session" = "投票セッション"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND が設定されていて、使うグラフィックス API を指定している。解除すると FigCraft が選ぶ。"
"Warm" = "ウォーム"
"Watch" = "ウォッチ"
@@ -1713,6 +1729,7 @@ status = "draft — needs native review"
"{n} layers selected" = { other = "{n} 個のレイヤーを選択中" }
"{n} layers · {parts}" = { other = "{n} 個のレイヤー · {parts}" }
"{n} matches" = { other = "{n} 件一致" }
"{n} min" = "{n}分"
"{n} minutes ago" = { other = "{n} 分前" }
"{n} months ago" = { other = "{n} か月前" }
"{n} more notes omitted from this report" = { other = "このレポートで省略した注記がさらに {n} 件" }
@@ -1725,6 +1742,8 @@ status = "draft — needs native review"
"{n} rows" = { other = "{n} 行" }
"{n} styles" = { other = "{n} 個のスタイル" }
"{n} variables" = { other = "{n} 個の変数" }
"{n} votes" = { other = "{n}票" }
"{n} votes left" = { other = "残り{n}票" }
"{n} weeks ago" = { other = "{n} 週間前" }
"{n} years ago" = { other = "{n} 年前" }
"{n}d" = "{n}日"
@@ -1786,6 +1805,10 @@ status = "draft — needs native review"
"board.shape" = "テキスト付きシェイプ"
"board.stamp" = "スタンプ"
"board.sticky" = "付箋"
"board.timer" = "タイマー"
"board.timerState" = "タイマーの状態"
"board.vote" = "投票セッション"
"board.voteResults" = "投票結果"
"bool.exclude" = "選択範囲を除外"
"bool.flatten" = "ブーリアンを平坦化"
"bool.intersect" = "選択範囲を交差"
+23
View File
@@ -39,6 +39,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Add a description" = "აღწერის დამატება"
"Add a frame (F) or a shape (R) to get started" = "დასაწყებად დაამატეთ ჩარჩო (F) ან ფიგურა (R)"
"Add a layer or component to tile first" = "ჯერ დაამატეთ შრე ან კომპონენტი ფილებად დასაწყობად"
"Add a minute" = "ერთი წუთის დამატება"
"Add a note for developers" = "შენიშვნის დამატება დეველოპერებისთვის"
"Add action" = "მოქმედების დამატება"
"Add actions for when no condition holds" = "მოქმედებების დამატება იმ შემთხვევისთვის, როცა არცერთი პირობა არ სრულდება"
@@ -93,6 +94,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"All comments" = "ყველა კომენტარი"
"All components" = "ყველა კომპონენტი"
"All fonts" = "ყველა შრიფტი"
"All objects" = "ყველა ობიექტი"
"All supported properties" = "ყველა მხარდაჭერილი თვისება"
"All the fonts this file uses are installed" = "ამ ფაილის ყველა შრიფტი დაინსტალირებულია"
"All variables" = "ყველა ცვლადი"
@@ -207,6 +209,8 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Checking the server…" = "სერვერის შემოწმება…"
"Chevron" = "შევრონი"
"Clear from this list" = "ამ სიიდან ამოღება"
"Clear results" = "შედეგების გასუფთავება"
"Click objects to vote; click your badge to take a vote back" = "ხმის მისაცემად დააწკაპუნეთ ობიექტებზე; ხმის დასაბრუნებლად დააწკაპუნეთ თქვენს ნიშანზე"
"Code or invite link" = "კოდი ან მოსაწვევი ბმული"
"Color profile: {name}" = "ფერის პროფილი: {name}"
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "ფერისა და ტექსტის სტილები, ღია და მუქი ცვლადები, ღილაკები, შეყვანის ველები და ბარათები"
@@ -456,6 +460,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Empty" = "ცარიელი"
"End" = "ბოლო"
"End point" = "საბოლოო წერტილი"
"End vote" = "ხმის მიცემის დასრულება"
"Ends the session; everyone keeps their copy" = "ასრულებს სესიას; ყველას თავისი ასლი რჩება"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "შეიყვანეთ figcraft-server-ის მისამართი და დააჭირეთ „დათვალიერებას“, რომ ნახოთ მის მიერ შემოთავაზებული პლაგინები."
@@ -782,6 +787,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Min H" = "მინ. H"
"Min W" = "მინ. W"
"Minimize" = "ჩაკეცვა"
"Minutes" = "წუთები"
"Mirror angle" = "კუთხის არეკვლა"
"Mirror angle and length" = "კუთხისა და სიგრძის არეკვლა"
"Mirroring" = "არეკვლა"
@@ -879,6 +885,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"No text styles yet. Click + to create one from the selected text." = "ტექსტის სტილები ჯერ არ არის. დაწკაპეთ +, რომ მონიშნული ტექსტიდან შექმნათ."
"No variables fit here." = "აქ არცერთი ცვლადი არ ერგება."
"No variables yet. Create them in Local variables." = "ცვლადები ჯერ არ არის. შექმენით ისინი ლოკალურ ცვლადებში."
"No votes yet" = "ხმები ჯერ არ არის"
"Nobody else is here yet" = "აქ ჯერ სხვა არავინ არის"
"Noise" = "ხმაური"
"None" = "არცერთი"
@@ -965,6 +972,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Page" = "გვერდი"
"Pages" = "გვერდები"
"Pages added to the document" = "გვერდები დაემატა დოკუმენტს"
"Pause" = "პაუზა"
"Pink" = "ვარდისფერი"
"Placed on the current page" = "განთავსდა მიმდინარე გვერდზე"
"Pages: {pages}" = "გვერდი: {pages}"
@@ -1145,6 +1153,8 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Restore main component" = "მთავარი კომპონენტის აღდგენა"
"Restore this version" = "ამ ვერსიის აღდგენა"
"Restored “{name}”" = "„{name}“ აღდგა"
"Results" = "შედეგები"
"Resume" = "გაგრძელება"
"Reverse the order" = "რიგის შებრუნება"
"Reversed triangle" = "შებრუნებული სამკუთხედი"
"Review and merge" = "განხილვა და შერწყმა"
@@ -1322,17 +1332,20 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Start point" = "საწყისი წერტილი"
"Start sharing" = "გაზიარების დაწყება"
"Start sharing with the Share button, then pass on the code it gives" = "გაზიარება დაიწყეთ ღილაკით „გაზიარება“, შემდეგ გადაეცით მის მიერ მოცემული კოდი"
"Start vote" = "ხმის მიცემის დაწყება"
"Starting to share…" = "გაზიარების დაწყება…"
"Status" = "სტატუსი"
"Sticky" = "მიმაგრებული"
"Sticky note" = "სტიკერი"
"Sticky note colors" = "სტიკერების ფერები"
"Sticky notes" = "სტიკერები"
"Sticky notes, shapes and connectors" = "სტიკერები, ფიგურები და შემაერთებლები"
"Stop" = "შეწყვეტა"
"Stop following {name}" = "{name}-ის მიყოლის შეწყვეტა"
"Stop session" = "სესიის შეწყვეტა"
"Stop sharing" = "გაზიარების შეწყვეტა"
"Stop spotlighting" = "ჩემი ხედის ჩვენების შეწყვეტა"
"Stop timer" = "ტაიმერის გაჩერება"
"Stop {n} {hex}" = "გადასვლის წერტილი {n} {hex}"
"Straight" = "სწორი"
"Stretch" = "გაწელვა"
@@ -1418,6 +1431,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Tidy up" = "მოწესრიგება"
"Tile" = "ფილებად"
"Tile type" = "ფილის ტიპი"
"Timer" = "ტაიმერი"
"Tint" = "ტონირება"
"Title" = "დასახელება"
"Title case" = "სათაურის რეგისტრი"
@@ -1512,6 +1526,8 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"Visibility" = "ხილვადობა"
"Visible" = "ხილული"
"Vivid" = "ცოცხალი"
"Votes per person" = "ხმები ერთ ადამიანზე"
"Voting session" = "ხმის მიცემის სესია"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND დაყენებულია და გამოსაყენებელ გრაფიკულ API-ს განსაზღვრავს; მისი მოხსნის შემდეგ FigCraft თავად აირჩევს."
"Warm" = "თბილი"
"Watch" = "საათი"
@@ -1728,6 +1744,7 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"{n} layers selected" = { one = "მონიშნული შრე: {n}", other = "მონიშნული შრე: {n}" }
"{n} layers · {parts}" = { one = "შრე: {n} · {parts}", other = "შრე: {n} · {parts}" }
"{n} matches" = { one = "დამთხვევა: {n}", other = "დამთხვევა: {n}" }
"{n} min" = "{n} წთ"
"{n} minutes ago" = { one = "{n} წუთის წინ", other = "{n} წუთის წინ" }
"{n} months ago" = { one = "{n} თვის წინ", other = "{n} თვის წინ" }
"{n} more notes omitted from this report" = { one = "ამ ანგარიშიდან გამოტოვებული სხვა შენიშვნები: {n}", other = "ამ ანგარიშიდან გამოტოვებული სხვა შენიშვნები: {n}" }
@@ -1741,6 +1758,8 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"{n} styles" = { one = "სტილი: {n}", other = "სტილი: {n}" }
"{n} text layers" = { one = "ტექსტის შრე: {n}", other = "ტექსტის შრე: {n}" }
"{n} variables" = { one = "ცვლადი: {n}", other = "ცვლადი: {n}" }
"{n} votes" = { one = "ხმა: {n}", other = "ხმა: {n}" }
"{n} votes left" = { one = "დარჩენილი ხმა: {n}", other = "დარჩენილი ხმა: {n}" }
"{n} weeks ago" = { one = "{n} კვირის წინ", other = "{n} კვირის წინ" }
"{n} years ago" = { one = "{n} წლის წინ", other = "{n} წლის წინ" }
"{n}d" = "{n} დღ"
@@ -1802,6 +1821,10 @@ status = "draft — reviewed by a native speaker, awaiting a maintainer's approv
"board.shape" = "ფიგურა ტექსტით"
"board.stamp" = "შტამპი"
"board.sticky" = "სტიკერი"
"board.timer" = "ტაიმერი"
"board.timerState" = "ტაიმერის მდგომარეობა"
"board.vote" = "ხმის მიცემის სესია"
"board.voteResults" = "ხმის მიცემის შედეგები"
"bool.exclude" = "მონიშნულის გამორიცხვა"
"bool.flatten" = "ლოგიკური ოპერაციის გაბრტყელება"
"bool.intersect" = "მონიშნულის გადაკვეთა"
+23
View File
@@ -35,6 +35,7 @@ status = "draft — needs native review"
"Add a description" = "설명 추가"
"Add a frame (F) or a shape (R) to get started" = "프레임(F) 또는 도형(R)을 추가해 시작하세요"
"Add a layer or component to tile first" = "먼저 바둑판식으로 배열할 레이어나 컴포넌트를 추가하세요"
"Add a minute" = "1분 추가"
"Add a note for developers" = "개발자용 메모 추가"
"Add action" = "액션 추가"
"Add actions for when no condition holds" = "조건에 맞지 않을 때의 액션 추가"
@@ -88,6 +89,7 @@ status = "draft — needs native review"
"All comments" = "모든 댓글"
"All components" = "모든 컴포넌트"
"All fonts" = "모든 글꼴"
"All objects" = "모든 개체"
"All supported properties" = "지원되는 모든 속성"
"All variables" = "모든 변수"
"Alpha" = "알파"
@@ -194,6 +196,8 @@ status = "draft — needs native review"
"Checking the server…" = "서버 확인 중…"
"Chevron" = "셰브런"
"Clear from this list" = "이 목록에서 지우기"
"Clear results" = "결과 지우기"
"Click objects to vote; click your badge to take a vote back" = "개체를 클릭해 투표하고, 내 배지를 클릭해 투표를 취소하세요"
"Code or invite link" = "코드 또는 초대 링크"
"Color profile: {name}" = "색상 프로필: {name}"
"CSS custom properties…" = "CSS 사용자 정의 속성…"
@@ -432,6 +436,7 @@ status = "draft — needs native review"
"Empty" = "비어 있음"
"End" = "끝"
"End point" = "끝점"
"End vote" = "투표 종료"
"Ends the session; everyone keeps their copy" = "세션을 종료함. 각자의 사본은 그대로 유지됨"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "figcraft-server 주소를 입력하고 찾아보기를 눌러 제공되는 플러그인을 확인하세요."
@@ -738,6 +743,7 @@ status = "draft — needs native review"
"Min H" = "최소 H"
"Min W" = "최소 W"
"Minimize" = "최소화"
"Minutes" = "분"
"Mirror angle" = "각도 대칭"
"Mirror angle and length" = "각도와 길이 대칭"
"Mirroring" = "대칭"
@@ -829,6 +835,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "아직 텍스트 스타일이 없음. +를 클릭해 선택한 텍스트로 만드세요."
"No variables fit here." = "여기에 맞는 변수가 없음."
"No variables yet. Create them in Local variables." = "아직 변수가 없음. 로컬 변수에서 만드세요."
"No votes yet" = "아직 투표가 없습니다"
"Nobody else is here yet" = "아직 다른 사람이 없음"
"Noise" = "노이즈"
"None" = "없음"
@@ -909,6 +916,7 @@ status = "draft — needs native review"
"Page deleted" = "페이지가 삭제됨"
"Pages" = "페이지"
"Pages added to the document" = "문서에 페이지 추가됨"
"Pause" = "일시 정지"
"Pink" = "분홍"
"Placed on the current page" = "현재 페이지에 배치됨"
"Pages: {pages}" = "페이지: {pages}"
@@ -1084,6 +1092,8 @@ status = "draft — needs native review"
"Restore main component" = "메인 컴포넌트 복원"
"Restore this version" = "이 버전 복원"
"Restored “{name}”" = "\"{name}\" 복원됨"
"Results" = "결과"
"Resume" = "재개"
"Reverse the order" = "순서 뒤집기"
"Reversed triangle" = "역삼각형"
"Review and merge" = "검토 및 병합"
@@ -1245,17 +1255,20 @@ status = "draft — needs native review"
"Start on the Home screen" = "홈에서 시작"
"Start point" = "시작점"
"Start sharing" = "공유 시작"
"Start vote" = "투표 시작"
"Starting to share…" = "공유를 시작하는 중…"
"Status" = "상태"
"Sticky" = "고정"
"Sticky note" = "스티커 메모"
"Sticky note colors" = "스티커 메모 색상"
"Sticky notes" = "스티커 메모"
"Sticky notes, shapes and connectors" = "스티커 메모, 도형, 커넥터"
"Stop" = "중지"
"Stop following {name}" = "{name} 따라가기 중지"
"Stop session" = "세션 중지"
"Stop sharing" = "공유 중지"
"Stop spotlighting" = "스포트라이트 중지"
"Stop timer" = "타이머 중지"
"Stop {n} {hex}" = "정지점 {n} {hex}"
"Straight" = "직선"
"Stretch" = "늘이기"
@@ -1338,6 +1351,7 @@ status = "draft — needs native review"
"Tidy up" = "정돈"
"Tile" = "타일"
"Tile type" = "타일 유형"
"Timer" = "타이머"
"Tint" = "색조 보정"
"Title" = "제목"
"Title case" = "제목 대소문자"
@@ -1428,6 +1442,8 @@ status = "draft — needs native review"
"Visibility" = "표시 여부"
"Visible" = "표시됨"
"Vivid" = "선명하게"
"Votes per person" = "1인당 투표 수"
"Voting session" = "투표 세션"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND가 설정되어 사용할 그래픽 API를 지정하고 있습니다. 설정을 해제하면 FigCraft가 선택합니다."
"Warm" = "따뜻하게"
"Watch" = "시계"
@@ -1637,6 +1653,7 @@ status = "draft — needs native review"
"{n} layers selected" = { other = "레이어 {n}개 선택됨" }
"{n} layers · {parts}" = { other = "레이어 {n}개 · {parts}" }
"{n} matches" = { other = "{n}개 일치" }
"{n} min" = "{n}분"
"{n} minutes ago" = { other = "{n}분 전" }
"{n} months ago" = { other = "{n}개월 전" }
"{n} more notes omitted from this report" = { other = "참고 사항 {n}개가 이 보고서에서 더 생략됨" }
@@ -1649,6 +1666,8 @@ status = "draft — needs native review"
"{n} rows" = { other = "{n}행" }
"{n} styles" = { other = "스타일 {n}개" }
"{n} variables" = { other = "변수 {n}개" }
"{n} votes" = { other = "{n}표" }
"{n} votes left" = { other = "남은 투표 {n}개" }
"{n} weeks ago" = { other = "{n}주 전" }
"{n} years ago" = { other = "{n}년 전" }
"{n}d" = "{n}일"
@@ -1710,6 +1729,10 @@ status = "draft — needs native review"
"board.shape" = "텍스트가 있는 도형"
"board.stamp" = "스탬프"
"board.sticky" = "스티커 메모"
"board.timer" = "타이머"
"board.timerState" = "타이머 상태"
"board.vote" = "투표 세션"
"board.voteResults" = "투표 결과"
"bool.exclude" = "선택 항목 제외"
"bool.flatten" = "불리언 평면화"
"bool.intersect" = "선택 항목 교차"
+23
View File
@@ -38,6 +38,7 @@ status = "draft — needs native review"
"Add a description" = "Beschrijving toevoegen"
"Add a frame (F) or a shape (R) to get started" = "Voeg een frame (F) of een vorm (R) toe om te beginnen"
"Add a layer or component to tile first" = "Voeg eerst een laag of component toe om als tegel te gebruiken"
"Add a minute" = "Een minuut toevoegen"
"Add a note for developers" = "Notitie voor ontwikkelaars toevoegen"
"Add action" = "Actie toevoegen"
"Add actions for when no condition holds" = "Voeg acties toe voor het geval geen enkele voorwaarde geldt"
@@ -92,6 +93,7 @@ status = "draft — needs native review"
"All comments" = "Alle reacties"
"All components" = "Alle components"
"All fonts" = "Alle lettertypen"
"All objects" = "Alle objecten"
"All supported properties" = "Alle ondersteunde eigenschappen"
"All the fonts this file uses are installed" = "Alle lettertypen die dit bestand gebruikt zijn geïnstalleerd"
"All variables" = "Alle variabelen"
@@ -235,8 +237,10 @@ status = "draft — needs native review"
"Circle" = "Cirkel"
"Clear from this list" = "Verwijderen uit deze lijst"
"Clear recent" = "Recenten wissen"
"Clear results" = "Resultaten wissen"
"Click + to replace mixed content" = "Klik op + om gemengde inhoud te vervangen"
"Click anywhere on the canvas to leave one." = "Klik ergens op het canvas om er een achter te laten."
"Click objects to vote; click your badge to take a vote back" = "Klik op objecten om te stemmen; klik op je badge om een stem terug te nemen"
"Click or drag to merge · {alt} removes" = "Klik of sleep om samen te voegen · {alt} verwijdert"
"Click or drag to remove · {alt} merges" = "Klik of sleep om te verwijderen · {alt} voegt samen"
"Click to record a key" = "Klik om een toets op te nemen"
@@ -456,6 +460,7 @@ status = "draft — needs native review"
"Empty" = "Leeg"
"End" = "End"
"End point" = "Eindpunt"
"End vote" = "Stemronde beëindigen"
"Ends the session; everyone keeps their copy" = "Beëindigt de sessie; iedereen houdt zijn eigen kopie"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Voer het adres van een figcraft-server in en druk op Bladeren om te zien welke plugins het aanbiedt."
@@ -782,6 +787,7 @@ status = "draft — needs native review"
"Min H" = "Min H"
"Min W" = "Min. B"
"Minimize" = "Minimaliseren"
"Minutes" = "Minuten"
"Mirror angle" = "Hoek spiegelen"
"Mirror angle and length" = "Spiegel hoek en lengte"
"Mirroring" = "Spiegeling"
@@ -879,6 +885,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "Nog geen tekststijlen. Klik op + om er een te maken vanuit de geselecteerde tekst."
"No variables fit here." = "Er passen hier geen variabelen."
"No variables yet. Create them in Local variables." = "Nog geen variabelen. Maak ze in Lokale variabelen."
"No votes yet" = "Nog geen stemmen"
"Nobody else is here yet" = "Er is nog niemand anders hier"
"Noise" = "Ruis"
"None" = "Geen"
@@ -985,6 +992,7 @@ status = "draft — needs native review"
"Pattern alignment" = "Patroonuitlijning"
"Pattern scale" = "Patroonschaal"
"Pattern source" = "Patroonbron"
"Pause" = "Pauzeren"
"Pen" = "Pen"
"Pencil" = "Potlood"
"People in this file" = "Personen in dit bestand"
@@ -1146,6 +1154,8 @@ status = "draft — needs native review"
"Restore main component" = "Hoofdcomponent terugzetten"
"Restore this version" = "Deze versie terugzetten"
"Restored “{name}”" = "“{name}” teruggezet"
"Results" = "Resultaten"
"Resume" = "Hervatten"
"Reverse the order" = "Volgorde omdraaien"
"Reversed triangle" = "Omgekeerde driehoek"
"Review and merge" = "Controleren en samenvoegen"
@@ -1324,17 +1334,20 @@ status = "draft — needs native review"
"Start point" = "Startpunt"
"Start sharing" = "Delen starten"
"Start sharing with the Share button, then pass on the code it gives" = "Begin met delen via de knop Delen en geef daarna de code door die je krijgt"
"Start vote" = "Stemronde starten"
"Starting to share…" = "Bezig met delen…"
"Status" = "Status"
"Sticky" = "Sticky"
"Sticky note" = "Plakbriefje"
"Sticky note colors" = "Kleuren van plakbriefjes"
"Sticky notes" = "Plakbriefjes"
"Sticky notes, shapes and connectors" = "Plakbriefjes, vormen en verbindingen"
"Stop" = "Stoppen"
"Stop following {name}" = "Stop met {name} te volgen"
"Stop session" = "Sessie stoppen"
"Stop sharing" = "Stoppen met delen"
"Stop spotlighting" = "Stop met uitlichten"
"Stop timer" = "Timer stoppen"
"Stop {n} {hex}" = "Stop {n} {hex}"
"Straight" = "Recht"
"Stretch" = "Uitrekken"
@@ -1420,6 +1433,7 @@ status = "draft — needs native review"
"Tidy up" = "Opruimen"
"Tile" = "Tegelen"
"Tile type" = "Tegeltype"
"Timer" = "Timer"
"Tint" = "Tint"
"Title" = "Titel"
"Title case" = "Hoofdletter per woord"
@@ -1514,6 +1528,8 @@ status = "draft — needs native review"
"Visibility" = "Zichtbaarheid"
"Visible" = "Zichtbaar"
"Vivid" = "Vivid"
"Votes per person" = "Stemmen per persoon"
"Voting session" = "Stemronde"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND is ingesteld en geeft de te gebruiken grafische API aan; als je het uitzet, kiest FigCraft zelf."
"Warm" = "Warm"
"Watch" = "Watch"
@@ -1730,6 +1746,7 @@ status = "draft — needs native review"
"{n} layers selected" = { one = "{n} laag geselecteerd", other = "{n} lagen geselecteerd" }
"{n} layers · {parts}" = { one = "{n} laag · {parts}", other = "{n} lagen · {parts}" }
"{n} matches" = { one = "{n} overeenkomst", other = "{n} overeenkomsten" }
"{n} min" = "{n} min"
"{n} minutes ago" = { one = "{n} minuut geleden", other = "{n} minuten geleden" }
"{n} months ago" = { one = "{n} maand geleden", other = "{n} maanden geleden" }
"{n} more notes omitted from this report" = { one = "{n} extra notitie weggelaten uit dit rapport", other = "{n} extra notities weggelaten uit dit rapport" }
@@ -1743,6 +1760,8 @@ status = "draft — needs native review"
"{n} styles" = { one = "{n} stijl", other = "{n} stijlen" }
"{n} text layers" = { one = "{n} tekstlaag", other = "{n} tekstlagen" }
"{n} variables" = { one = "{n} variabele", other = "{n} variabelen" }
"{n} votes" = { one = "{n} stem", other = "{n} stemmen" }
"{n} votes left" = { one = "Nog {n} stem", other = "Nog {n} stemmen" }
"{n} weeks ago" = { one = "{n} week geleden", other = "{n} weken geleden" }
"{n} years ago" = { one = "{n} jaar geleden", other = "{n} jaar geleden" }
"{n}d" = "{n}d"
@@ -1804,6 +1823,10 @@ status = "draft — needs native review"
"board.shape" = "Vorm met tekst"
"board.stamp" = "Stempel"
"board.sticky" = "Plakbriefje"
"board.timer" = "Timer"
"board.timerState" = "Timerstatus"
"board.vote" = "Stemronde"
"board.voteResults" = "Stemresultaten"
"bool.exclude" = "Exclude toepassen op selectie"
"bool.flatten" = "Boolean afvlakken"
"bool.intersect" = "Intersect toepassen op selectie"
+23
View File
@@ -35,6 +35,7 @@ status = "draft — needs native review"
"Add a description" = "Dodaj opis"
"Add a frame (F) or a shape (R) to get started" = "Dodaj ramkę (F) lub kształt (R), aby rozpocząć"
"Add a layer or component to tile first" = "Najpierw dodaj warstwę lub komponent do powielenia"
"Add a minute" = "Dodaj minutę"
"Add a note for developers" = "Dodaj notatkę dla programistów"
"Add action" = "Dodaj akcję"
"Add actions for when no condition holds" = "Dodaj akcje wykonywane, gdy żaden warunek nie jest spełniony"
@@ -89,6 +90,7 @@ status = "draft — needs native review"
"All comments" = "Wszystkie komentarze"
"All components" = "Wszystkie komponenty"
"All fonts" = "Wszystkie czcionki"
"All objects" = "Wszystkie obiekty"
"All supported properties" = "Wszystkie obsługiwane właściwości"
"All the fonts this file uses are installed" = "Wszystkie czcionki używane w tym pliku są zainstalowane"
"All variables" = "Wszystkie zmienne"
@@ -203,6 +205,8 @@ status = "draft — needs native review"
"Checking the server…" = "Sprawdzanie serwera…"
"Chevron" = "Szewron"
"Clear from this list" = "Usuń z tej listy"
"Clear results" = "Wyczyść wyniki"
"Click objects to vote; click your badge to take a vote back" = "Kliknij obiekty, aby głosować; kliknij swoją odznakę, aby cofnąć głos"
"Code or invite link" = "Kod lub link z zaproszeniem"
"Color profile: {name}" = "Profil kolorów: {name}"
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Style kolorów i tekstu, zmienne Jasne i Ciemne, przyciski, pola wprowadzania i karty"
@@ -453,6 +457,7 @@ status = "draft — needs native review"
"Empty" = "Puste"
"End" = "End"
"End point" = "Punkt końcowy"
"End vote" = "Zakończ głosowanie"
"Ends the session; everyone keeps their copy" = "Kończy sesję; każdy zachowuje swoją kopię"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Wprowadź adres figcraft-server i kliknij Przeglądaj, aby zobaczyć dostępne wtyczki."
@@ -779,6 +784,7 @@ status = "draft — needs native review"
"Min H" = "Min. wys."
"Min W" = "Min. szer."
"Minimize" = "Minimalizuj"
"Minutes" = "Minuty"
"Mirror angle" = "Kąt odbicia lustrzanego"
"Mirror angle and length" = "Kąt i długość odbicia lustrzanego"
"Mirroring" = "Odbicie lustrzane"
@@ -876,6 +882,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "Brak stylów tekstu. Kliknij +, aby utworzyć styl z zaznaczonego tekstu."
"No variables fit here." = "Żadne zmienne tutaj nie pasują."
"No variables yet. Create them in Local variables." = "Brak zmiennych. Utwórz je w Zmiennych lokalnych."
"No votes yet" = "Brak głosów"
"Nobody else is here yet" = "Nikogo innego tu jeszcze nie ma"
"Noise" = "Szum"
"None" = "Brak"
@@ -962,6 +969,7 @@ status = "draft — needs native review"
"Page" = "Strona"
"Pages" = "Strony"
"Pages added to the document" = "Strony dodane do dokumentu"
"Pause" = "Wstrzymaj"
"Pink" = "Różowy"
"Placed on the current page" = "Umieszczono na bieżącej stronie"
"Pages: {pages}" = "Strony: {pages}"
@@ -1142,6 +1150,8 @@ status = "draft — needs native review"
"Restore main component" = "Przywróć główny komponent"
"Restore this version" = "Przywróć tę wersję"
"Restored “{name}”" = "Przywrócono „{name}”"
"Results" = "Wyniki"
"Resume" = "Wznów"
"Reverse the order" = "Odwróć kolejność"
"Reversed triangle" = "Odwrócony trójkąt"
"Review and merge" = "Przejrzyj i scal"
@@ -1319,17 +1329,20 @@ status = "draft — needs native review"
"Start point" = "Punkt początkowy"
"Start sharing" = "Rozpocznij udostępnianie"
"Start sharing with the Share button, then pass on the code it gives" = "Rozpocznij udostępnianie przyciskiem Udostępnij, a następnie przekaż wygenerowany kod"
"Start vote" = "Rozpocznij głosowanie"
"Starting to share…" = "Rozpoczynanie udostępniania…"
"Status" = "Stan"
"Sticky" = "Przyklejony (sticky)"
"Sticky note" = "Karteczka"
"Sticky note colors" = "Kolory karteczek"
"Sticky notes" = "Karteczki"
"Sticky notes, shapes and connectors" = "Karteczki, kształty i łączniki"
"Stop" = "Zatrzymaj"
"Stop following {name}" = "Przestań obserwować: {name}"
"Stop session" = "Zatrzymaj sesję"
"Stop sharing" = "Zatrzymaj udostępnianie"
"Stop spotlighting" = "Zatrzymaj wyróżnianie widoku"
"Stop timer" = "Zatrzymaj minutnik"
"Stop {n} {hex}" = "Punkt przejścia {n} {hex}"
"Straight" = "Prosty"
"Stretch" = "Rozciągnięcie"
@@ -1415,6 +1428,7 @@ status = "draft — needs native review"
"Tidy up" = "Uporządkuj"
"Tile" = "Sąsiadująco"
"Tile type" = "Typ kafelka"
"Timer" = "Minutnik"
"Tint" = "Odcień barwny"
"Title" = "Tytuł"
"Title case" = "Jak W Tytule"
@@ -1509,6 +1523,8 @@ status = "draft — needs native review"
"Visibility" = "Widoczność"
"Visible" = "Widoczne"
"Vivid" = "Żywe kolory"
"Votes per person" = "Głosy na osobę"
"Voting session" = "Głosowanie"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "Zmienna WGPU_BACKEND jest ustawiona i określa używane API graficzne; jej usunięcie pozwala FigCraft dokonać wyboru."
"Warm" = "Ciepły"
"Watch" = "Zegarek"
@@ -1726,6 +1742,7 @@ status = "draft — needs native review"
"{n} layers selected" = { one = "Zaznaczono {n} warstwę", few = "Zaznaczono {n} warstwy", many = "Zaznaczono {n} warstw", other = "Zaznaczono {n} warstw" }
"{n} layers · {parts}" = { one = "{n} warstwa · {parts}", few = "{n} warstwy · {parts}", many = "{n} warstw · {parts}", other = "{n} warstw · {parts}" }
"{n} matches" = { one = "{n} dopasowanie", few = "{n} dopasowania", many = "{n} dopasowań", other = "{n} dopasowań" }
"{n} min" = "{n} min"
"{n} minutes ago" = { one = "{n} minutę temu", few = "{n} minuty temu", many = "{n} minut temu", other = "{n} minut temu" }
"{n} months ago" = { one = "{n} miesiąc temu", few = "{n} miesiące temu", many = "{n} miesięcy temu", other = "{n} miesięcy temu" }
"{n} more notes omitted from this report" = { one = "Pominięto jeszcze {n} notatkę w tym raporcie", few = "Pominięto jeszcze {n} notatki w tym raporcie", many = "Pominięto jeszcze {n} notatek w tym raporcie", other = "Pominięto jeszcze {n} notatek w tym raporcie" }
@@ -1739,6 +1756,8 @@ status = "draft — needs native review"
"{n} styles" = { one = "{n} styl", few = "{n} style", many = "{n} stylów", other = "{n} stylów" }
"{n} text layers" = { one = "{n} warstwa tekstowa", few = "{n} warstwy tekstowe", many = "{n} warstw tekstowych", other = "{n} warstw tekstowych" }
"{n} variables" = { one = "{n} zmienna", few = "{n} zmienne", many = "{n} zmiennych", other = "{n} zmiennych" }
"{n} votes" = { one = "{n} głos", few = "{n} głosy", many = "{n} głosów", other = "{n} głosu" }
"{n} votes left" = { one = "Pozostał {n} głos", few = "Pozostały {n} głosy", many = "Pozostało {n} głosów", other = "Pozostało {n} głosu" }
"{n} weeks ago" = { one = "{n} tydzień temu", few = "{n} tygodnie temu", many = "{n} tygodni temu", other = "{n} tygodni temu" }
"{n} years ago" = { one = "{n} rok temu", few = "{n} lata temu", many = "{n} lat temu", other = "{n} lat temu" }
"{n}d" = "{n} d"
@@ -1800,6 +1819,10 @@ status = "draft — needs native review"
"board.shape" = "Kształt z tekstem"
"board.stamp" = "Pieczątka"
"board.sticky" = "Karteczka"
"board.timer" = "Minutnik"
"board.timerState" = "Stan minutnika"
"board.vote" = "Głosowanie"
"board.voteResults" = "Wyniki głosowania"
"bool.exclude" = "Wyklucz zaznaczenie"
"bool.flatten" = "Spłaszcz operację logiczną"
"bool.intersect" = "Część wspólna zaznaczenia"
+23
View File
@@ -33,6 +33,7 @@ status = "draft — needs native review"
"Add a description" = "Adicionar uma descrição"
"Add a frame (F) or a shape (R) to get started" = "Adicione um quadro (F) ou uma forma (R) para começar"
"Add a layer or component to tile first" = "Adicione primeiro uma camada ou um componente para o mosaico"
"Add a minute" = "Adicionar um minuto"
"Add a note for developers" = "Adicione uma nota para desenvolvedores"
"Add action" = "Adicionar ação"
"Add actions for when no condition holds" = "Adicionar ações para quando nenhuma condição for atendida"
@@ -86,6 +87,7 @@ status = "draft — needs native review"
"All comments" = "Todos os comentários"
"All components" = "Todos os componentes"
"All fonts" = "Todas as fontes"
"All objects" = "Todos os objetos"
"All supported properties" = "Todas as propriedades compatíveis"
"All variables" = "Todas as variáveis"
"Alpha" = "Alfa"
@@ -192,6 +194,8 @@ status = "draft — needs native review"
"Checking the server…" = "Verificando o servidor…"
"Chevron" = "Divisa"
"Clear from this list" = "Limpar desta lista"
"Clear results" = "Limpar resultados"
"Click objects to vote; click your badge to take a vote back" = "Clique nos objetos para votar; clique no seu selo para retirar um voto"
"Code or invite link" = "Código ou link de convite"
"Color profile: {name}" = "Perfil de cor: {name}"
"CSS custom properties…" = "Propriedades personalizadas CSS…"
@@ -431,6 +435,7 @@ status = "draft — needs native review"
"Empty" = "Vazio"
"End" = "Fim"
"End point" = "Ponto final"
"End vote" = "Encerrar votação"
"Ends the session; everyone keeps their copy" = "Encerra a sessão; cada pessoa fica com a própria cópia"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Digite o endereço de um figcraft-server e pressione Procurar para ver os plugins que ele oferece."
@@ -737,6 +742,7 @@ status = "draft — needs native review"
"Min H" = "Alt. mín."
"Min W" = "Larg. mín."
"Minimize" = "Minimizar"
"Minutes" = "Minutos"
"Mirror angle" = "Espelhar ângulo"
"Mirror angle and length" = "Espelhar ângulo e comprimento"
"Mirroring" = "Espelhamento"
@@ -828,6 +834,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "Ainda não há estilos de texto. Clique em + para criar um a partir do texto selecionado."
"No variables fit here." = "Nenhuma variável se encaixa aqui."
"No variables yet. Create them in Local variables." = "Ainda não há variáveis. Crie-as em Variáveis locais."
"No votes yet" = "Nenhum voto ainda"
"Nobody else is here yet" = "Ainda não há mais ninguém aqui"
"Noise" = "Ruído"
"None" = "Nenhum"
@@ -908,6 +915,7 @@ status = "draft — needs native review"
"Page deleted" = "Página excluída"
"Pages" = "Páginas"
"Pages added to the document" = "Páginas adicionadas ao documento"
"Pause" = "Pausar"
"Pink" = "Rosa"
"Placed on the current page" = "Colocado na página atual"
"Pages: {pages}" = "Páginas: {pages}"
@@ -1083,6 +1091,8 @@ status = "draft — needs native review"
"Restore main component" = "Restaurar componente principal"
"Restore this version" = "Restaurar esta versão"
"Restored “{name}”" = "“{name}” restaurada"
"Results" = "Resultados"
"Resume" = "Retomar"
"Reverse the order" = "Inverter a ordem"
"Reversed triangle" = "Triângulo invertido"
"Review and merge" = "Revisar e mesclar"
@@ -1244,17 +1254,20 @@ status = "draft — needs native review"
"Start on the Home screen" = "Começar no Início"
"Start point" = "Ponto inicial"
"Start sharing" = "Começar a compartilhar"
"Start vote" = "Iniciar votação"
"Starting to share…" = "Começando a compartilhar…"
"Status" = "Status"
"Sticky" = "Fixado"
"Sticky note" = "Nota adesiva"
"Sticky note colors" = "Cores das notas adesivas"
"Sticky notes" = "Notas adesivas"
"Sticky notes, shapes and connectors" = "Notas adesivas, formas e conectores"
"Stop" = "Parar"
"Stop following {name}" = "Parar de seguir {name}"
"Stop session" = "Encerrar sessão"
"Stop sharing" = "Parar de compartilhar"
"Stop spotlighting" = "Remover o foco"
"Stop timer" = "Parar timer"
"Stop {n} {hex}" = "Parada {n} {hex}"
"Straight" = "Reto"
"Stretch" = "Esticar"
@@ -1337,6 +1350,7 @@ status = "draft — needs native review"
"Tidy up" = "Organizar"
"Tile" = "Mosaico"
"Tile type" = "Tipo de mosaico"
"Timer" = "Timer"
"Tint" = "Tonalidade"
"Title" = "Título"
"Title case" = "Iniciais maiúsculas"
@@ -1427,6 +1441,8 @@ status = "draft — needs native review"
"Visibility" = "Visibilidade"
"Visible" = "Visível"
"Vivid" = "Vívido"
"Votes per person" = "Votos por pessoa"
"Voting session" = "Sessão de votação"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND está definida e indica a API gráfica a usar; sem ela, o FigCraft escolhe sozinho."
"Warm" = "Quente"
"Watch" = "Relógio"
@@ -1636,6 +1652,7 @@ status = "draft — needs native review"
"{n} layers selected" = { one = "{n} camada selecionada", other = "{n} camadas selecionadas" }
"{n} layers · {parts}" = { one = "{n} camada · {parts}", other = "{n} camadas · {parts}" }
"{n} matches" = { one = "{n} resultado", other = "{n} resultados" }
"{n} min" = "{n} min"
"{n} minutes ago" = { one = "há {n} minuto", other = "há {n} minutos" }
"{n} months ago" = { one = "há {n} mês", other = "há {n} meses" }
"{n} more notes omitted from this report" = { one = "{n} nota a mais omitida deste relatório", other = "{n} notas a mais omitidas deste relatório" }
@@ -1648,6 +1665,8 @@ status = "draft — needs native review"
"{n} rows" = { one = "{n} linha", other = "{n} linhas" }
"{n} styles" = { one = "{n} estilo", other = "{n} estilos" }
"{n} variables" = { one = "{n} variável", other = "{n} variáveis" }
"{n} votes" = { one = "{n} voto", other = "{n} votos" }
"{n} votes left" = { one = "{n} voto restante", other = "{n} votos restantes" }
"{n} weeks ago" = { one = "há {n} semana", other = "há {n} semanas" }
"{n} years ago" = { one = "há {n} ano", other = "há {n} anos" }
"{n}d" = "{n} d"
@@ -1823,6 +1842,10 @@ status = "draft — needs native review"
"board.shape" = "Forma com texto"
"board.stamp" = "Carimbo"
"board.sticky" = "Nota adesiva"
"board.timer" = "Timer"
"board.timerState" = "Estado do timer"
"board.vote" = "Sessão de votação"
"board.voteResults" = "Resultados da votação"
"bool.exclude" = "Excluir sobreposição da seleção"
"bool.flatten" = "Nivelar booleano"
"bool.intersect" = "Interseção da seleção"
+23
View File
@@ -36,6 +36,7 @@ name = "Русский"
"Add a description" = "Добавить описание"
"Add a frame (F) or a shape (R) to get started" = "Для начала добавьте Frame (F) или фигуру (R)"
"Add a layer or component to tile first" = "Сначала добавьте слой или компонент для повторения"
"Add a minute" = "Добавить минуту"
"Add a note for developers" = "Добавить заметку для разработчиков"
"Add action" = "Добавить действие"
"Add actions for when no condition holds" = "Добавить действия на случай, если ни одно условие не выполнено"
@@ -90,6 +91,7 @@ name = "Русский"
"All comments" = "Все комментарии"
"All components" = "Все компоненты"
"All fonts" = "Все шрифты"
"All objects" = "Все объекты"
"All supported properties" = "Все поддерживаемые свойства"
"All the fonts this file uses are installed" = "Все шрифты, которые использует этот файл, установлены"
"All variables" = "Все переменные"
@@ -205,6 +207,8 @@ name = "Русский"
"Checking the server…" = "Проверка сервера…"
"Chevron" = "Шеврон"
"Clear from this list" = "Убрать из этого списка"
"Clear results" = "Очистить результаты"
"Click objects to vote; click your badge to take a vote back" = "Нажмите на объекты, чтобы проголосовать; нажмите на свой значок, чтобы забрать голос"
"Code or invite link" = "Код или ссылка приглашения"
"Color profile: {name}" = "Цветовой профиль: {name}"
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Цветовые и текстовые стили, переменные светлой и тёмной тем, кнопки, поля ввода и карточки"
@@ -455,6 +459,7 @@ name = "Русский"
"Empty" = "Пусто"
"End" = "Конец"
"End point" = "Конечная точка"
"End vote" = "Завершить голосование"
"Ends the session; everyone keeps their copy" = "Завершает сессию; у всех остаются свои копии"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Введите адрес figcraft-server и нажмите «Обзор», чтобы увидеть доступные плагины."
@@ -784,6 +789,7 @@ name = "Русский"
"Min H" = "Мин. В"
"Min W" = "Мин. Ш"
"Minimize" = "Свернуть окно"
"Minutes" = "Минуты"
"Mirror angle" = "Отражать угол"
"Mirror angle and length" = "Отражать угол и длину"
"Mirroring" = "Зеркальность"
@@ -881,6 +887,7 @@ name = "Русский"
"No text styles yet. Click + to create one from the selected text." = "Текстовых стилей пока нет. Нажмите +, чтобы создать стиль из выделенного текста."
"No variables fit here." = "Нет подходящих переменных."
"No variables yet. Create them in Local variables." = "Переменных пока нет. Создайте их в разделе «Локальные переменные»."
"No votes yet" = "Голосов пока нет"
"Nobody else is here yet" = "Здесь пока больше никого нет"
"Noise" = "Шум"
"None" = "Нет"
@@ -968,6 +975,7 @@ name = "Русский"
"Page deleted" = "Страница удалена"
"Pages" = "Страницы"
"Pages added to the document" = "Страницы добавлены в документ"
"Pause" = "Пауза"
"Pink" = "Розовый"
"Placed on the current page" = "Размещено на текущей странице"
"Pages: {pages}" = "Страницы: {pages}"
@@ -1150,6 +1158,8 @@ name = "Русский"
"Restore main component" = "Восстановить основной компонент"
"Restore this version" = "Восстановить эту версию"
"Restored “{name}”" = "«{name}» восстановлено"
"Results" = "Результаты"
"Resume" = "Продолжить"
"Reverse the order" = "Обратить порядок"
"Reversed triangle" = "Обратный треугольник"
"Review and merge" = "Проверить и объединить"
@@ -1330,17 +1340,20 @@ name = "Русский"
"Start point" = "Начальная точка"
"Start sharing" = "Включить общий доступ"
"Start sharing with the Share button, then pass on the code it gives" = "Включите общий доступ кнопкой «Поделиться», затем передайте полученный код"
"Start vote" = "Начать голосование"
"Starting to share…" = "Включение общего доступа…"
"Status" = "Статус"
"Sticky" = "Закреплённый"
"Sticky note" = "Стикер"
"Sticky note colors" = "Цвета стикеров"
"Sticky notes" = "Стикеры"
"Sticky notes, shapes and connectors" = "Стикеры, фигуры и соединители"
"Stop" = "Остановить"
"Stop following {name}" = "Перестать следить за {name}"
"Stop session" = "Завершить сессию"
"Stop sharing" = "Остановить общий доступ"
"Stop spotlighting" = "Прекратить показ всем"
"Stop timer" = "Остановить таймер"
"Stop {n} {hex}" = "Точка {n} {hex}"
"Straight" = "Прямая"
"Stretch" = "Растянуть"
@@ -1428,6 +1441,7 @@ name = "Русский"
"Tidy up" = "Упорядочить"
"Tile" = "Повторение"
"Tile type" = "Тип повторения"
"Timer" = "Таймер"
"Tint" = "Оттенок"
"Title" = "Заголовок"
"Title case" = "Каждое Слово С Прописной"
@@ -1524,6 +1538,8 @@ name = "Русский"
"Visibility" = "Видимость"
"Visible" = "Видимый"
"Vivid" = "Яркий"
"Votes per person" = "Голосов на человека"
"Voting session" = "Голосование"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "Переменная WGPU_BACKEND задана и указывает используемый графический API; если её убрать, FigCraft выберет сам."
"Warm" = "Тёплый"
"Watch" = "Часы"
@@ -1741,6 +1757,7 @@ name = "Русский"
"{n} layers selected" = { one = "Выделен {n} слой", few = "Выделены {n} слоя", many = "Выделено {n} слоёв", other = "Выделено {n} слоя" }
"{n} layers · {parts}" = { one = "{n} слой · {parts}", few = "{n} слоя · {parts}", many = "{n} слоёв · {parts}", other = "{n} слоя · {parts}" }
"{n} matches" = { one = "{n} совпадение", few = "{n} совпадения", many = "{n} совпадений", other = "{n} совпадения" }
"{n} min" = "{n} мин"
"{n} minutes ago" = { one = "{n} минуту назад", few = "{n} минуты назад", many = "{n} минут назад", other = "{n} минуты назад" }
"{n} months ago" = { one = "{n} месяц назад", few = "{n} месяца назад", many = "{n} месяцев назад", other = "{n} месяца назад" }
"{n} more notes omitted from this report" = { one = "Ещё {n} замечание не включено в этот отчёт", few = "Ещё {n} замечания не включены в этот отчёт", many = "Ещё {n} замечаний не включено в этот отчёт", other = "Ещё {n} замечания не включено в этот отчёт" }
@@ -1754,6 +1771,8 @@ name = "Русский"
"{n} styles" = { one = "{n} стиль", few = "{n} стиля", many = "{n} стилей", other = "{n} стиля" }
"{n} text layers" = { one = "{n} текстовый слой", few = "{n} текстовых слоя", many = "{n} текстовых слоёв", other = "{n} текстовых слоя" }
"{n} variables" = { one = "{n} переменная", few = "{n} переменные", many = "{n} переменных", other = "{n} переменной" }
"{n} votes" = { one = "{n} голос", few = "{n} голоса", many = "{n} голосов", other = "{n} голоса" }
"{n} votes left" = { one = "Остался {n} голос", few = "Осталось {n} голоса", many = "Осталось {n} голосов", other = "Осталось {n} голоса" }
"{n} weeks ago" = { one = "{n} неделю назад", few = "{n} недели назад", many = "{n} недель назад", other = "{n} недели назад" }
"{n} years ago" = { one = "{n} год назад", few = "{n} года назад", many = "{n} лет назад", other = "{n} года назад" }
"{n}d" = "{n} д"
@@ -1817,6 +1836,10 @@ name = "Русский"
"board.shape" = "Фигура с текстом"
"board.stamp" = "Штамп"
"board.sticky" = "Стикер"
"board.timer" = "Таймер"
"board.timerState" = "Состояние таймера"
"board.vote" = "Голосование"
"board.voteResults" = "Результаты голосования"
"bool.exclude" = "Исключить выделение"
"bool.flatten" = "Свести булеву группу"
"bool.intersect" = "Пересечь выделение"
+23
View File
@@ -36,6 +36,7 @@ name = "Svenska"
"Add a description" = "Lägg till en beskrivning"
"Add a frame (F) or a shape (R) to get started" = "Lägg till en ram (F) eller en form (R) för att komma igång"
"Add a layer or component to tile first" = "Lägg först till ett lager eller en komponent att lägga sida vid sida"
"Add a minute" = "Lägg till en minut"
"Add a note for developers" = "Lägg till en anteckning för utvecklare"
"Add action" = "Lägg till åtgärd"
"Add actions for when no condition holds" = "Lägg till åtgärder för när inget villkor uppfylls"
@@ -90,6 +91,7 @@ name = "Svenska"
"All comments" = "Alla kommentarer"
"All components" = "Alla komponenter"
"All fonts" = "Alla typsnitt"
"All objects" = "Alla objekt"
"All supported properties" = "Alla egenskaper som stöds"
"All variables" = "Alla variabler"
"Alpha" = "Alfa"
@@ -202,6 +204,8 @@ name = "Svenska"
"Checking the server…" = "Kontrollerar servern…"
"Chevron" = "Vinkel"
"Clear from this list" = "Ta bort från listan"
"Clear results" = "Rensa resultat"
"Click objects to vote; click your badge to take a vote back" = "Klicka på objekt för att rösta; klicka på din bricka för att ta tillbaka en röst"
"Code or invite link" = "Kod eller inbjudningslänk"
"Color profile: {name}" = "Färgprofil: {name}"
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Färg- och textstilar, ljusa och mörka variabler, knappar, inmatningsfält och kort"
@@ -450,6 +454,7 @@ name = "Svenska"
"Empty" = "Tom"
"End" = "Slut"
"End point" = "Slutpunkt"
"End vote" = "Avsluta omröstning"
"Ends the session; everyone keeps their copy" = "Avslutar sessionen; alla behåller sin kopia"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Ange adressen till en figcraft-server och tryck på Bläddra för att se vilka plugin-program den erbjuder."
@@ -776,6 +781,7 @@ name = "Svenska"
"Min H" = "Min H"
"Min W" = "Min B"
"Minimize" = "Minimera"
"Minutes" = "Minuter"
"Mirror angle" = "Spegla vinkel"
"Mirror angle and length" = "Spegla vinkel och längd"
"Mirroring" = "Spegling"
@@ -870,6 +876,7 @@ name = "Svenska"
"No text styles yet. Click + to create one from the selected text." = "Inga textstilar än. Klicka på + för att skapa en från den markerade texten."
"No variables fit here." = "Inga variabler passar här."
"No variables yet. Create them in Local variables." = "Inga variabler än. Skapa dem i Lokala variabler."
"No votes yet" = "Inga röster än"
"Nobody else is here yet" = "Ingen annan är här än"
"Noise" = "Brus"
"None" = "Ingen"
@@ -956,6 +963,7 @@ name = "Svenska"
"Page deleted" = "Sidan har raderats"
"Pages" = "Sidor"
"Pages added to the document" = "Sidor tillagda i dokumentet"
"Pause" = "Pausa"
"Pink" = "Rosa"
"Placed on the current page" = "Placerat på den aktuella sidan"
"Pages: {pages}" = "Sidor: {pages}"
@@ -1134,6 +1142,8 @@ name = "Svenska"
"Restore main component" = "Återställ huvudkomponent"
"Restore this version" = "Återställ den här versionen"
"Restored “{name}”" = "Återställde ”{name}”"
"Results" = "Resultat"
"Resume" = "Återuppta"
"Reverse the order" = "Vänd på ordningen"
"Reversed triangle" = "Omvänd triangel"
"Review and merge" = "Granska och sammanfoga"
@@ -1310,17 +1320,20 @@ name = "Svenska"
"Start point" = "Startpunkt"
"Start sharing" = "Börja dela"
"Start sharing with the Share button, then pass on the code it gives" = "Börja dela med knappen Dela och skicka sedan vidare koden du får"
"Start vote" = "Starta omröstning"
"Starting to share…" = "Börjar dela…"
"Status" = "Status"
"Sticky" = "Fast"
"Sticky note" = "Lapp"
"Sticky note colors" = "Lappfärger"
"Sticky notes" = "Lappar"
"Sticky notes, shapes and connectors" = "Lappar, former och kopplingar"
"Stop" = "Stoppa"
"Stop following {name}" = "Sluta följa {name}"
"Stop session" = "Avsluta session"
"Stop sharing" = "Sluta dela"
"Stop spotlighting" = "Sluta använda strålkastaren"
"Stop timer" = "Stoppa timern"
"Stop {n} {hex}" = "Stopp {n} {hex}"
"Straight" = "Rak"
"Stretch" = "Sträck"
@@ -1405,6 +1418,7 @@ name = "Svenska"
"Tidy up" = "Snygga till"
"Tile" = "Sida vid sida"
"Tile type" = "Upprepningstyp"
"Timer" = "Timer"
"Tint" = "Färgton"
"Title" = "Titel"
"Title case" = "Inledande versaler"
@@ -1498,6 +1512,8 @@ name = "Svenska"
"Visibility" = "Synlighet"
"Visible" = "Synlig"
"Vivid" = "Livfull"
"Votes per person" = "Röster per person"
"Voting session" = "Omröstning"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "WGPU_BACKEND är satt och anger vilket grafik-API som ska användas; tar du bort den väljer FigCraft själv."
"Warm" = "Varm"
"Watch" = "Bevaka"
@@ -1712,6 +1728,7 @@ name = "Svenska"
"{n} layers selected" = { one = "{n} lager markerat", other = "{n} lager markerade" }
"{n} layers · {parts}" = { one = "{n} lager · {parts}", other = "{n} lager · {parts}" }
"{n} matches" = { one = "{n} träff", other = "{n} träffar" }
"{n} min" = "{n} min"
"{n} minutes ago" = { one = "för {n} minut sedan", other = "för {n} minuter sedan" }
"{n} months ago" = { one = "för {n} månad sedan", other = "för {n} månader sedan" }
"{n} more notes omitted from this report" = { one = "{n} anteckning till utelämnades i den här rapporten", other = "{n} anteckningar till utelämnades i den här rapporten" }
@@ -1724,6 +1741,8 @@ name = "Svenska"
"{n} rows" = { one = "{n} rad", other = "{n} rader" }
"{n} styles" = { one = "{n} formatmall", other = "{n} formatmallar" }
"{n} variables" = { one = "{n} variabel", other = "{n} variabler" }
"{n} votes" = { one = "{n} röst", other = "{n} röster" }
"{n} votes left" = { one = "{n} röst kvar", other = "{n} röster kvar" }
"{n} weeks ago" = { one = "för {n} vecka sedan", other = "för {n} veckor sedan" }
"{n} years ago" = { one = "för {n} år sedan", other = "för {n} år sedan" }
"{n}d" = "{n} d"
@@ -1786,6 +1805,10 @@ name = "Svenska"
"board.shape" = "Form med text"
"board.stamp" = "Stämpel"
"board.sticky" = "Lapp"
"board.timer" = "Timer"
"board.timerState" = "Timerstatus"
"board.vote" = "Omröstning"
"board.voteResults" = "Omröstningsresultat"
"bool.exclude" = "Uteslut markering"
"bool.flatten" = "Platta till boolesk form"
"bool.intersect" = "Skärning av markering"
+23
View File
@@ -37,6 +37,7 @@ name = "Türkçe"
"Add a description" = "Açıklama ekle"
"Add a frame (F) or a shape (R) to get started" = "Başlamak için bir çerçeve (F) veya bir şekil (R) ekleyin"
"Add a layer or component to tile first" = "Önce döşenecek bir katman veya bileşen ekleyin"
"Add a minute" = "Bir dakika ekle"
"Add a note for developers" = "Geliştiriciler için not ekle"
"Add action" = "Eylem ekle"
"Add actions for when no condition holds" = "Hiçbir koşul sağlanmadığında yapılacak eylemleri ekleyin"
@@ -90,6 +91,7 @@ name = "Türkçe"
"All comments" = "Tüm yorumlar"
"All components" = "Tüm bileşenler"
"All fonts" = "Tüm yazı tipleri"
"All objects" = "Tüm nesneler"
"All supported properties" = "Desteklenen tüm özellikler"
"All variables" = "Tüm değişkenler"
"Alpha" = "Alfa"
@@ -202,6 +204,8 @@ name = "Türkçe"
"Checking the server…" = "Sunucu denetleniyor…"
"Chevron" = "Şerit ok"
"Clear from this list" = "Bu listeden kaldır"
"Clear results" = "Sonuçları temizle"
"Click objects to vote; click your badge to take a vote back" = "Oy vermek için nesnelere tıklayın; oyu geri almak için rozetinize tıklayın"
"Code or invite link" = "Kod veya davet bağlantısı"
"Color profile: {name}" = "Renk profili: {name}"
"Colour and text styles, Light and Dark variables, buttons, inputs and cards" = "Renk ve metin stilleri, Açık ve Koyu değişkenler, düğmeler, giriş alanları ve kartlar"
@@ -445,6 +449,7 @@ name = "Türkçe"
"Empty" = "Boş"
"End" = "Bitiş"
"End point" = "Bitiş noktası"
"End vote" = "Oylamayı bitir"
"Ends the session; everyone keeps their copy" = "Oturumu sonlandırır; herkes kendi kopyasını saklar"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "Bir figcraft-server adresi girin ve sunduğu eklentileri görmek için Gözat'a basın."
@@ -769,6 +774,7 @@ name = "Türkçe"
"Min H" = "En küçük Y"
"Min W" = "En küçük G"
"Minimize" = "Simge durumuna küçült"
"Minutes" = "Dakika"
"Mirror angle" = "Açıyı yansıt"
"Mirror angle and length" = "Açıyı ve uzunluğu yansıt"
"Mirroring" = "Yansıtma"
@@ -863,6 +869,7 @@ name = "Türkçe"
"No text styles yet. Click + to create one from the selected text." = "Henüz metin stili yok. Seçili metinden oluşturmak için + simgesine tıklayın."
"No variables fit here." = "Buraya uyan değişken yok."
"No variables yet. Create them in Local variables." = "Henüz değişken yok. Onları Yerel değişkenler'de oluşturun."
"No votes yet" = "Henüz oy yok"
"Nobody else is here yet" = "Burada henüz başka kimse yok"
"Noise" = "Gürültü"
"None" = "Yok"
@@ -947,6 +954,7 @@ name = "Türkçe"
"Page deleted" = "Sayfa silindi"
"Pages" = "Sayfalar"
"Pages added to the document" = "Sayfalar belgeye eklendi"
"Pause" = "Duraklat"
"Pink" = "Pembe"
"Placed on the current page" = "Geçerli sayfaya yerleştirildi"
"Pages: {pages}" = "Sayfalar: {pages}"
@@ -1124,6 +1132,8 @@ 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"
"Results" = "Sonuçlar"
"Resume" = "Sürdür"
"Reverse the order" = "Sırayı ters çevir"
"Reversed triangle" = "Ters üçgen"
"Review and merge" = "Gözden geçir ve birleştir"
@@ -1298,17 +1308,20 @@ name = "Türkçe"
"Start point" = "Başlangıç noktası"
"Start sharing" = "Paylaşmaya başla"
"Start sharing with the Share button, then pass on the code it gives" = "Paylaş düğmesiyle paylaşmaya başlayın, sonra verdiği kodu iletin"
"Start vote" = "Oylamayı başlat"
"Starting to share…" = "Paylaşım başlatılıyor…"
"Status" = "Durum"
"Sticky" = "Yapışkan"
"Sticky note" = "Yapışkan not"
"Sticky note colors" = "Yapışkan not renkleri"
"Sticky notes" = "Yapışkan notlar"
"Sticky notes, shapes and connectors" = "Yapışkan notlar, şekiller ve bağlayıcılar"
"Stop" = "Durdur"
"Stop following {name}" = "{name} takibini bırak"
"Stop session" = "Oturumu durdur"
"Stop sharing" = "Paylaşmayı durdur"
"Stop spotlighting" = "Öne çıkarmayı durdur"
"Stop timer" = "Zamanlayıcıyı durdur"
"Stop {n} {hex}" = "Durak {n} {hex}"
"Straight" = "Düz"
"Stretch" = "Uzat"
@@ -1390,6 +1403,7 @@ name = "Türkçe"
"Tidy up" = "Toparla"
"Tile" = "Döşe"
"Tile type" = "Döşeme türü"
"Timer" = "Zamanlayıcı"
"Tint" = "Renk tonu"
"Title" = "Başlık"
"Title case" = "Başlık biçimi"
@@ -1481,6 +1495,8 @@ name = "Türkçe"
"Visibility" = "Görünürlük"
"Visible" = "Görünür"
"Vivid" = "Canlı"
"Votes per person" = "Kişi başı oy"
"Voting session" = "Oylama oturumu"
"Warm" = "Sıcak"
"Watch" = "İzle"
"Wavy" = "Dalgalı"
@@ -1694,6 +1710,7 @@ name = "Türkçe"
"{n} layers selected" = { other = "{n} katman seçili" }
"{n} layers · {parts}" = { other = "{n} katman · {parts}" }
"{n} matches" = { other = "{n} eşleşme" }
"{n} min" = "{n} dk"
"{n} minutes ago" = { other = "{n} dakika önce" }
"{n} months ago" = { other = "{n} ay önce" }
"{n} more notes omitted from this report" = { other = "{n} not daha bu rapora alınmadı" }
@@ -1706,6 +1723,8 @@ name = "Türkçe"
"{n} rows" = { other = "{n} satır" }
"{n} styles" = { other = "{n} stil" }
"{n} variables" = { other = "{n} değişken" }
"{n} votes" = { other = "{n} oy" }
"{n} votes left" = { other = "{n} oy kaldı" }
"{n} weeks ago" = { other = "{n} hafta önce" }
"{n} years ago" = { other = "{n} yıl önce" }
"{n}d" = "{n} g"
@@ -1767,6 +1786,10 @@ name = "Türkçe"
"board.shape" = "Metinli şekil"
"board.stamp" = "Damga"
"board.sticky" = "Yapışkan not"
"board.timer" = "Zamanlayıcı"
"board.timerState" = "Zamanlayıcı durumu"
"board.vote" = "Oylama oturumu"
"board.voteResults" = "Oylama sonuçları"
"bool.exclude" = "Seçimi hariç tut"
"bool.flatten" = "Boolean'ı düzleştir"
"bool.intersect" = "Seçimi kesiştir"
+23
View File
@@ -36,6 +36,7 @@ status = "draft — needs native review"
"Add a description" = "添加描述"
"Add a frame (F) or a shape (R) to get started" = "添加画框(F)或形状(R)以开始"
"Add a layer or component to tile first" = "请先添加要平铺的图层或组件"
"Add a minute" = "加一分钟"
"Add a note for developers" = "为开发者添加备注"
"Add action" = "添加操作"
"Add actions for when no condition holds" = "添加所有条件都不满足时的操作"
@@ -89,6 +90,7 @@ status = "draft — needs native review"
"All comments" = "全部评论"
"All components" = "全部组件"
"All fonts" = "全部字体"
"All objects" = "所有对象"
"All supported properties" = "所有支持的属性"
"All variables" = "全部变量"
"Alpha" = "Alpha"
@@ -195,6 +197,8 @@ status = "draft — needs native review"
"Checking the server…" = "正在检查服务器…"
"Chevron" = "箭头形"
"Clear from this list" = "从此列表中清除"
"Clear results" = "清除结果"
"Click objects to vote; click your badge to take a vote back" = "点击对象投票;点击你的徽章收回一票"
"Code or invite link" = "代码或邀请链接"
"Color profile: {name}" = "颜色配置文件:{name}"
"CSS custom properties…" = "CSS 自定义属性…"
@@ -433,6 +437,7 @@ status = "draft — needs native review"
"Empty" = "空"
"End" = "末尾"
"End point" = "终点"
"End vote" = "结束投票"
"Ends the session; everyone keeps their copy" = "结束会话;每个人保留自己的副本"
"Enter" = "Enter"
"Enter the address of a figcraft-server and press Browse to see the plugins it offers." = "输入 figcraft-server 的地址,然后按“浏览”查看它提供的插件。"
@@ -739,6 +744,7 @@ status = "draft — needs native review"
"Min H" = "最小高"
"Min W" = "最小宽"
"Minimize" = "最小化"
"Minutes" = "分钟"
"Mirror angle" = "镜像角度"
"Mirror angle and length" = "镜像角度和长度"
"Mirroring" = "镜像"
@@ -830,6 +836,7 @@ status = "draft — needs native review"
"No text styles yet. Click + to create one from the selected text." = "还没有文本样式。点击 + 从所选文本创建。"
"No variables fit here." = "没有适用于此处的变量。"
"No variables yet. Create them in Local variables." = "还没有变量。请在“本地变量”中创建。"
"No votes yet" = "暂无投票"
"Nobody else is here yet" = "这里还没有其他人"
"Noise" = "杂色"
"None" = "无"
@@ -910,6 +917,7 @@ status = "draft — needs native review"
"Page deleted" = "页面已删除"
"Pages" = "页面"
"Pages added to the document" = "页面已添加到文档"
"Pause" = "暂停"
"Pink" = "粉色"
"Placed on the current page" = "已放置在当前页面"
"Pages: {pages}" = "页面:{pages}"
@@ -1085,6 +1093,8 @@ status = "draft — needs native review"
"Restore main component" = "恢复主组件"
"Restore this version" = "恢复此版本"
"Restored “{name}”" = "已恢复“{name}”"
"Results" = "结果"
"Resume" = "继续"
"Reverse the order" = "反转顺序"
"Reversed triangle" = "反向三角形"
"Review and merge" = "审阅并合并"
@@ -1246,17 +1256,20 @@ status = "draft — needs native review"
"Start on the Home screen" = "启动时显示主页"
"Start point" = "起点"
"Start sharing" = "开始分享"
"Start vote" = "开始投票"
"Starting to share…" = "正在开始分享…"
"Status" = "状态"
"Sticky" = "粘性"
"Sticky note" = "便利贴"
"Sticky note colors" = "便利贴颜色"
"Sticky notes" = "便利贴"
"Sticky notes, shapes and connectors" = "便利贴、形状和连接线"
"Stop" = "停止"
"Stop following {name}" = "停止跟随 {name}"
"Stop session" = "停止会话"
"Stop sharing" = "停止分享"
"Stop spotlighting" = "停止聚焦"
"Stop timer" = "停止计时器"
"Stop {n} {hex}" = "色标 {n} {hex}"
"Straight" = "直线"
"Stretch" = "拉伸"
@@ -1339,6 +1352,7 @@ status = "draft — needs native review"
"Tidy up" = "整理"
"Tile" = "平铺"
"Tile type" = "平铺类型"
"Timer" = "计时器"
"Tint" = "色调"
"Title" = "标题"
"Title case" = "首字母大写"
@@ -1429,6 +1443,8 @@ status = "draft — needs native review"
"Visibility" = "可见性"
"Visible" = "可见"
"Vivid" = "鲜艳"
"Votes per person" = "每人票数"
"Voting session" = "投票"
"WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose." = "已设置 WGPU_BACKEND,它指定了要使用的图形 API;取消设置后由 FigCraft 自行选择。"
"Warm" = "暖调"
"Watch" = "手表"
@@ -1638,6 +1654,7 @@ status = "draft — needs native review"
"{n} layers selected" = { other = "已选择 {n} 个图层" }
"{n} layers · {parts}" = { other = "{n} 个图层 · {parts}" }
"{n} matches" = { other = "{n} 个匹配项" }
"{n} min" = "{n} 分钟"
"{n} minutes ago" = { other = "{n} 分钟前" }
"{n} months ago" = { other = "{n} 个月前" }
"{n} more notes omitted from this report" = { other = "另有 {n} 条说明未列入此报告" }
@@ -1650,6 +1667,8 @@ status = "draft — needs native review"
"{n} rows" = { other = "{n} 行" }
"{n} styles" = { other = "{n} 个样式" }
"{n} variables" = { other = "{n} 个变量" }
"{n} votes" = { other = "{n} 票" }
"{n} votes left" = { other = "剩余 {n} 票" }
"{n} weeks ago" = { other = "{n} 周前" }
"{n} years ago" = { other = "{n} 年前" }
"{n}d" = "{n}天"
@@ -1711,6 +1730,10 @@ status = "draft — needs native review"
"board.shape" = "带文字的形状"
"board.stamp" = "印章"
"board.sticky" = "便利贴"
"board.timer" = "计时器"
"board.timerState" = "计时器状态"
"board.vote" = "投票"
"board.voteResults" = "投票结果"
"bool.exclude" = "排除所选项"
"bool.flatten" = "拼合布尔组"
"bool.intersect" = "所选项相交"
+257
View File
@@ -0,0 +1,257 @@
//! Boards in the UI: the timer and voting pill at the top right of a board, and the vote badges on
//! objects. Everything they change goes through `board.timer` and `board.vote`.
use std::time::Duration;
use egui::{Align2, Color32, CornerRadius, Rect, RichText, Stroke, Ui, pos2, vec2};
use figcraft_doc::{BoardTimer, NodeId};
use figcraft_engine::cmd::facilitate::{self, BADGE_PX};
use serde_json::{Value, json};
use crate::FigApp;
use crate::i18n::{tr, tr_noop, trf, trn};
use crate::icons::{self, Icon};
use crate::theme::{Tokens, regular, semibold};
use crate::widgets;
const CELL: f32 = 28.0;
/// The presets the timer offers (minutes).
const PRESETS: [u32; 5] = [1, 3, 5, 10, 15];
/// What a vote can be on, as `board.vote {on}` takes it.
const SCOPES: [(&str, &str); 3] = [("all", tr_noop("All objects")), ("stickies", tr_noop("Sticky notes")), ("selection", tr_noop("Selection"))];
/// What the pill shows of the vote: none, running (your votes left) or ended (the results).
enum VoteView {
Off,
Running { left: u32 },
Ended { results: Vec<(usize, NodeId, String, u32)> },
}
fn vote_view(app: &FigApp) -> VoteView {
let Ok(doc) = app.session.doc() else { return VoteView::Off };
let Some(vote) = &doc.vote else { return VoteView::Off };
if vote.ended {
let results = facilitate::ranked(doc, vote).into_iter().map(|(rank, id, n)| (rank, id, facilitate::object_name(doc, id), n)).collect();
return VoteView::Ended { results };
}
let used: u32 = facilitate::mine(doc, &facilitate::voter(&app.session, &Value::Null)).iter().map(|m| m.1).sum();
VoteView::Running { left: vote.per_person.saturating_sub(used) }
}
/// The pill at the top right of a board's canvas: the timer, then the vote.
pub fn show(app: &mut FigApp, ctx: &egui::Context, canvas: Rect) {
let Ok(doc) = app.session.doc() else { return };
if !figcraft_engine::board::is_board(doc) {
return;
}
let timer = doc.timer;
let now = facilitate::now_ms();
if let Some(left) = timer.filter(|t| matches!(t, BoardTimer::Running { .. })).map(|t| t.left(now)).filter(|l| *l > 0) {
// Wake when the shown second changes; nothing else ticks.
ctx.request_repaint_after(Duration::from_millis(left % 1000 + 5));
}
let vote = vote_view(app);
let editable = app.session.active_read_only().is_none();
let t = Tokens::get(ctx);
egui::Area::new(egui::Id::new("board_facilitate"))
.order(egui::Order::Middle)
.pivot(Align2::RIGHT_TOP)
.fixed_pos(pos2(canvas.max.x - 12.0, canvas.min.y + 12.0))
.show(ctx, |ui| {
egui::Frame::NONE
.fill(t.panel)
.stroke(Stroke::new(1.0, t.border))
.corner_radius(CornerRadius::same(10))
.shadow(egui::epaint::Shadow { offset: [0, 2], blur: 12, spread: 0, color: t.shadow })
.inner_margin(egui::Margin::same(4))
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 2.0;
timer_part(app, ui, timer, now, editable, &t);
ui.add_space(4.0);
ui.separator();
ui.add_space(4.0);
vote_part(app, ui, vote, editable, &t);
});
});
});
}
fn timer_part(app: &mut FigApp, ui: &mut Ui, timer: Option<BoardTimer>, now: u64, editable: bool, t: &Tokens) {
let Some(timer) = timer else {
let resp = icons::button(ui, Icon::Timer, CELL, false, tr("Timer"));
if editable {
timer_popup(app, &resp);
}
return;
};
let state = facilitate::timer_state(Some(timer), now);
let color = match state {
"done" => t.danger,
"paused" => t.text_secondary,
_ => t.text,
};
let (r, _) = ui.allocate_exact_size(vec2(20.0, CELL), egui::Sense::hover());
icons::paint(ui.painter(), Rect::from_center_size(r.center(), vec2(16.0, 16.0)), Icon::Timer, color);
// As wide as the same clock in zeros: the pill doesn't jitter as the seconds change.
let text = facilitate::clock(timer.left(now));
let zeros: String = text.chars().map(|c| if c.is_ascii_digit() { '0' } else { c }).collect();
let w = ui.painter().layout_no_wrap(zeros, semibold(13.0), color).size().x;
let (r, _) = ui.allocate_exact_size(vec2(w + 2.0, CELL), egui::Sense::hover());
ui.painter().text(r.left_center(), Align2::LEFT_CENTER, text, semibold(13.0), color);
ui.add_space(2.0);
if !editable {
return;
}
let (icon, tip, action) = match timer {
BoardTimer::Paused { .. } => (Icon::Play, tr("Resume"), "resume"),
BoardTimer::Running { .. } => (Icon::Pause, tr("Pause"), "pause"),
};
if state != "done" && icons::button(ui, icon, CELL, false, tip).clicked() {
app.run("board.timer", json!({"action": action}));
}
if icons::button(ui, Icon::Plus, CELL, false, tr("Add a minute")).clicked() {
app.run("board.timer", json!({"action": "add", "minutes": 1}));
}
if icons::button(ui, Icon::Close, CELL, false, tr("Stop timer")).clicked() {
app.run("board.timer", json!({"action": "stop"}));
}
}
/// The timer button's popover: presets and a custom length.
fn timer_popup(app: &mut FigApp, anchor: &egui::Response) {
crate::right::menu_popup(anchor).width(300.0).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
ui.spacing_mut().item_spacing = vec2(6.0, 8.0);
ui.add_space(2.0);
ui.label(RichText::new(tr("Timer")).font(semibold(12.0)));
let mut start = None;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 4.0;
for m in PRESETS {
if widgets::button(ui, &trf("{n} min", &[("n", &m)]), false, true).clicked() {
start = Some(m);
}
}
});
ui.horizontal(|ui| {
let id = egui::Id::new("board_timer_minutes");
let mut minutes: u32 = ui.data(|d| d.get_temp(id)).unwrap_or(5);
widgets::label(ui, tr("Minutes"), Tokens::get(ui.ctx()).text_secondary);
ui.add(egui::DragValue::new(&mut minutes).range(1..=1440).speed(0.25));
ui.data_mut(|d| d.insert_temp(id, minutes));
if widgets::button(ui, tr("Start"), true, true).clicked() {
start = Some(minutes);
}
});
if let Some(minutes) = start {
app.run("board.timer", json!({"action": "start", "minutes": minutes}));
ui.close();
}
});
}
fn vote_part(app: &mut FigApp, ui: &mut Ui, vote: VoteView, editable: bool, t: &Tokens) {
match vote {
VoteView::Off => {
let resp = icons::button(ui, Icon::Vote, CELL, false, tr("Voting session"));
if editable {
vote_popup(app, &resp);
}
}
VoteView::Running { left } => {
let (r, resp) = ui.allocate_exact_size(vec2(20.0, CELL), egui::Sense::hover());
icons::paint(ui.painter(), Rect::from_center_size(r.center(), vec2(16.0, 16.0)), Icon::Vote, t.accent_text);
let hint = tr("Click objects to vote; click your badge to take a vote back");
resp.on_hover_text(hint);
ui.label(RichText::new(trn("{n} votes left", left as usize, &[])).font(regular(12.0)).color(t.text)).on_hover_text(hint);
ui.add_space(4.0);
if editable && widgets::button(ui, tr("End vote"), true, true).clicked() {
app.run("board.vote", json!({"action": "end"}));
}
}
VoteView::Ended { results } => {
let (r, _) = ui.allocate_exact_size(vec2(20.0, CELL), egui::Sense::hover());
icons::paint(ui.painter(), Rect::from_center_size(r.center(), vec2(16.0, 16.0)), Icon::Vote, t.accent_text);
let resp = widgets::button(ui, tr("Results"), false, true);
results_popup(app, &resp, &results, editable);
}
}
}
/// The vote button's popover: votes per person, what to vote on, Start.
fn vote_popup(app: &mut FigApp, anchor: &egui::Response) {
let selected = !app.session.selection().is_empty();
crate::right::menu_popup(anchor).width(236.0).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
ui.spacing_mut().item_spacing.y = 6.0;
ui.add_space(2.0);
ui.label(RichText::new(tr("Voting session")).font(semibold(12.0)));
let (votes_id, on_id) = (egui::Id::new("board_vote_votes"), egui::Id::new("board_vote_on"));
let mut votes: u32 = ui.data(|d| d.get_temp(votes_id)).unwrap_or(3);
let mut on: usize = ui.data(|d| d.get_temp(on_id)).unwrap_or(0);
if on == 2 && !selected {
on = 0;
}
ui.horizontal(|ui| {
widgets::label(ui, tr("Votes per person"), Tokens::get(ui.ctx()).text_secondary);
ui.add(egui::DragValue::new(&mut votes).range(1..=99));
});
for (i, (_, label)) in SCOPES.iter().enumerate() {
// The selection is offered while there is one.
if (i != 2 || selected) && widgets::radio(ui, on == i, tr(label)) {
on = i;
}
}
ui.data_mut(|d| {
d.insert_temp(votes_id, votes);
d.insert_temp(on_id, on);
});
let scope = SCOPES.get(on).map_or("all", |s| s.0);
if widgets::button(ui, tr("Start vote"), true, true).clicked() {
app.run("board.vote", json!({"action": "start", "votes": votes, "on": scope}));
ui.close();
}
});
}
/// The ended vote's results, most votes first: a click on a row selects that object.
fn results_popup(app: &mut FigApp, anchor: &egui::Response, results: &[(usize, NodeId, String, u32)], editable: bool) {
crate::right::menu_popup(anchor).width(260.0).show(|ui| {
ui.add_space(2.0);
ui.label(RichText::new(tr("Results")).font(semibold(12.0)));
if results.is_empty() {
widgets::label(ui, tr("No votes yet"), Tokens::get(ui.ctx()).text_secondary);
}
for (rank, id, name, n) in results {
let label = format!("{rank}. {}", widgets::elide(ui, name, 160.0));
if crate::right::menu_item(ui, &label, &trn("{n} votes", *n as usize, &[])) {
app.run("select.set", json!({"ids": [id]}));
}
}
if editable {
ui.add_space(4.0);
if widgets::button(ui, tr("Clear results"), false, true).clicked() {
app.run("board.vote", json!({"action": "clear"}));
ui.close();
}
}
});
}
/// The vote badges on the page: your votes while the vote runs, everyone's once it ends.
pub fn badges(app: &FigApp, painter: &egui::Painter) {
let Ok(ds) = app.session.ds() else { return };
let doc = &ds.doc;
let Some(vote) = &doc.vote else { return };
let counts = if vote.ended { vote.tally(doc, &doc.votes) } else { facilitate::mine(doc, &facilitate::voter(&app.session, &Value::Null)) };
let t = Tokens::get(painter.ctx());
let r = BADGE_PX as f32;
for (id, n) in counts {
let Some(b) = doc.world_bounds(id).filter(|_| doc.page_of(id) == Some(ds.page)) else { continue };
let (c, _) = facilitate::badge(b, ds.view.zoom);
let c = app.to_screen(c.x, c.y);
painter.circle_filled(c + vec2(0.0, 1.0), r + 1.0, Color32::from_black_alpha(40));
painter.circle_filled(c, r, t.accent);
painter.circle_stroke(c, r, Stroke::new(1.5, Color32::WHITE));
painter.text(c, Align2::CENTER_CENTER, n.to_string(), semibold(11.0), t.accent_on);
}
}
+2
View File
@@ -256,8 +256,10 @@ pub fn show(app: &mut FigApp, ui: &mut Ui, worker: &mut Worker) {
}
crate::dev_mode::draw(app, &painter);
crate::handles::draw(app, &painter);
crate::board_facilitate::badges(app, &painter);
crate::board_bar::stamp_author(app, &painter, ctx.input(|i| i.pointer.hover_pos()).filter(|p| rect.contains(*p)));
crate::board_bar::show(app, &ctx);
crate::board_facilitate::show(app, &ctx, rect);
crate::selection_spacing::draw(app, &painter);
crate::comments::draw(app, &painter);
crate::live_text::draw(app, &painter);
+21
View File
@@ -204,6 +204,11 @@ icons! {
Connector,
Shapes,
Stamp,
/// Running a board session: a stopwatch (the timer), two bars (pause) and a ballot going
/// into a box (voting).
Timer,
Pause,
Vote,
}
/// Points along a circular arc from angle `a0` to `a1` (radians, y down: increasing is clockwise).
@@ -1172,6 +1177,22 @@ fn draw(p: &P, icon: Icon) {
p.circle(8.0, 8.0, 6.0);
p.poly(&[(5.0, 8.3), (7.2, 10.5), (11.0, 6.0)], false);
}
Timer => {
p.circle(8.0, 9.0, 5.5);
p.line(8.0, 9.0, 8.0, 6.0);
p.line(6.0, 1.5, 10.0, 1.5);
p.line(8.0, 1.5, 8.0, 3.5);
p.line(12.0, 4.0, 13.5, 2.5);
}
Pause => {
p.fill_rect(4.0, 3.0, 6.5, 13.0, 0.5);
p.fill_rect(9.5, 3.0, 12.0, 13.0, 0.5);
}
Vote => {
p.rect(2.5, 8.5, 13.5, 13.5, 1.0);
p.poly(&[(5.5, 8.5), (5.5, 2.5), (10.5, 2.5), (10.5, 8.5)], false);
p.poly(&[(6.6, 5.6), (7.7, 6.7), (9.4, 4.6)], false);
}
Logo => {
// FigCraft's own mark: the curled tail of the chameleon on the app icon. One bold
// stroke: a shaft along the bottom, then three half turns, each tighter than the last.
+1
View File
@@ -9,6 +9,7 @@ pub mod a11y;
pub mod assets_panel;
pub mod bidi;
pub mod board_bar;
pub mod board_facilitate;
pub mod branch_ui;
pub mod canvas;
pub mod cjk;
+9 -6
View File
@@ -1,12 +1,12 @@
# Boards parity
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-24 · **Change:** major (created: board files, the first slice of the whiteboard workflow) · **Target:** the incumbent's whiteboard product (behaviour from its public help centre)
> **Last reviewed:** 2026-10-24 · **Last updated:** 2026-10-24 · **Change:** minor (facilitation: a shared timer and voting sessions) · **Target:** the incumbent's whiteboard product (behaviour from its public help centre)
**Breadth ~35%, ready ~20% (estimated, erring low).** A board is a FigCraft file of its own kind
**Breadth ~40%, ready ~24% (estimated, erring low).** A board is a FigCraft file of its own kind
(`file.newBoard`) with sticky notes, shapes with text, connectors that follow what they join,
marker, highlighter and eraser, stamps, text and sections, edited together in the existing live
sessions. The facilitation tools (timer, voting, widgets), templates, tables, mind maps and the
rest of the tail are missing. Remaining: ~30–60 h. Tracked as area 22 of
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: ~26–54 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).
@@ -42,8 +42,10 @@ all end with the same geometry.
| Stamps sticking to the object they land on, the stamp wheel, emotes, stickers | missing | |
| Text and sections | done | the existing Text tool and sections |
| Live sessions: cursors, selections, cursor chat, spotlight | done | the existing sessions; board objects, connectors following moved objects, colours and stamps sync both ways (checked with two app instances) |
| Timer and music | missing | |
| Voting sessions | missing | |
| Timer | done | a pill at the top right of the board: presets (1, 3, 5, 10, 15 min) or any whole minutes, pause, resume, a minute more, stop; red at 0:00 until stopped; no sound. `board.timer {action, minutes?}`, `board.timerState`. Synced as its end time (or the time left while paused), so every client counts down on its own clock: nothing ticks over the network. Not an undo step |
| Timer music, timer sounds | missing | |
| 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 | |
@@ -58,4 +60,5 @@ all end with the same geometry.
| Date | Change | Summary |
|---|---|---|
| 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 | 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 |
+5 -1
View File
@@ -142,7 +142,11 @@ As built:
colour profile, image presets, annotation categories and library data (whole values); styles,
comment threads, variable collections and variables (one entry per id plus the order of ids;
the whole list when ids aren't unique); dev-mode snapshots (per layer); images (per content
hash, the encoded bytes as base64). The page order is the pages' positions.
hash, the encoded bytes as base64); a board's timer and vote (whole values, `DocProp::Timer` and
`DocProp::Vote`) and the vote's ballots (`Entry::Ballot(voter)`, one per voter, so people voting
at once write different entries). The timer travels as its end time (or, paused, the time left),
never as ticks: each client counts down on its own clock. The page order is the pages'
positions.
- **Next**: this file's published library (it embeds a snapshot document; it moves with an asset
channel, together with images, which then stop travelling inside batches); the version history
(kept beside the document, not in it) on the standalone server; finer entries for modes,
+10 -7
View File
@@ -1,6 +1,6 @@
# Where FigCraft falls short of Figma
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (gap 24: boards and their tail; Dev Mode tail: import lines in layer trees, compare with versions, an inspect-only canvas; gap 5: reviewed Turkish, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail; the 2026-10-17 landings: `.fig` import fidelity, budgets for large files, the import report, a `.fig` copy from Save as and paste from another design app (gap 1); menus, the Actions palette, editor fixes, Selection colors and smart-selection spacing (gap 4); catalog counts re-measured (gap 5); bounded canvas pan overscan; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (gap 24: a board timer and voting sessions; boards and their tail; Dev Mode tail: import lines in layer trees, compare with versions, an inspect-only canvas; gap 5: reviewed Turkish, Czech and Indonesian drafts, the Japanese draft completed; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail; the 2026-10-17 landings: `.fig` import fidelity, budgets for large files, the import report, a `.fig` copy from Save as and paste from another design app (gap 1); menus, the Actions palette, editor fixes, Selection colors and smart-selection spacing (gap 4); catalog counts re-measured (gap 5); bounded canvas pan overscan; reviewed Russian catalog and integer plurals; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7)
Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from
doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The
@@ -37,7 +37,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
| 21 | Menu items without a command: none left (landed 2026-10-11) | Feature | — | [parity-checklist](parity-checklist.md) |
| 22 | Screen readers never run; browser has no accessibility tree | UI/UX | 4–8 | [ui-parity](ui-parity.md) |
| 23 | CMYK output intent for print PDFs | File format | 2–4 | [file-format-parity](file-format-parity.md) |
| 24 | Boards: the whiteboard tail (timer, voting, widgets, templates, tables, mind maps…) | Feature | 30–60 | [boards-parity](boards-parity.md) |
| 24 | Boards: the whiteboard tail (widgets, templates, tables, mind maps…) | Feature | 26–54 | [boards-parity](boards-parity.md) |
## Entries
@@ -455,19 +455,22 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
### 24. Boards: the whiteboard tail
- **Landed (2026-10-24, M12):** board files (`file.newBoard`) with sticky notes, shapes with text,
connectors that re-route when what they join moves, marker, highlighter and eraser, stamps, text
and sections; a board toolbar and a contextual bar; live sessions sync it all.
- **Missing:** timer and music, voting sessions, board widgets, templates, tables, mind maps, washi
and sections; a board toolbar and a contextual bar; live sessions sync it all. Then (also M12) a
timer everyone on the board sees and voting sessions with per-person caps, hidden votes and
ranked results (`board.timer`, `board.vote`).
- **Missing:** timer music, board widgets, templates, tables, mind maps, washi
tape, code blocks, audio and huddles, quick-create handles, curved connectors and waypoints,
stamps sticking to the objects they land on, a dot grid, `.jam` files
([`boards-parity.md`](boards-parity.md)).
- **User impact:** boards cover a brainstorm or a flow sketch; facilitated workshops need the
timer, voting and templates.
- **Estimate:** 30–60 h.
- **User impact:** boards cover a brainstorm, a flow sketch and a timed, voted workshop round;
workshops still need templates.
- **Estimate:** 26–54 h.
## Revision history
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | minor | Gap 24: a shared board timer and voting sessions landed (M12); 30–60 h → 26–54 h, ranking unchanged |
| 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. New gap 24 (the board tail, 30–60 h); the ranking above it is unchanged |
| 2026-10-15 | minor | Gap 5: Turkish reviewed by a native speaker (#263), Czech (#469) and Indonesian (#477) drafts, the Japanese draft caught up (#288); Indonesian is no longer missing; counts re-measured (2,177 entries) and corrected: only Spanish is held to completeness, Russian included. Estimate unchanged at this rounding |
| 2026-10-17 | minor | The 2026-10-17 landings. Gap 1: a new Landed entry (community batches #727 and #728, then #745: instance identity and cached library components, overrides by key, text style precedence, grid track sizes, slot content, labels of connectors saved with their path, skip ink, paragraph options, list start numbers, vertical trim, per-point stroke joins, text on a path, image adjustments, images in the message, instance layers' size and resizing overrides, Hug where nothing is recorded; budgets from the file's size; the import report; a `.fig` copy from Save as; paste from another design app), its Missing list cut to what is still left, and the wrong "whether other apps open its files is unknown" replaced by what was checked; all on synthetic files, so the beta blocker and hours stand. Gap 2: server limits, log recovery and web join links in the evidence. Gap 4: menus, the Actions palette and the editor fixes (#733, #735, #736, #729), then Selection colors and smart-selection spacing (#753) landed, none compared side by side. Gap 5: catalog counts re-measured (2,272 entries; drafts at 95–99%; Indonesian, Czech, Turkish and Swedish listed). Gap 8: the Linux and BSD clipboard changes and Wayland window controls, not run there by the maintainers. Ranking and hours unchanged |
+34
View File
@@ -377,6 +377,40 @@ one undo step and returns the ids it made:
`{"id": 4, "text": 5}` in a new board without an author name; a sticky with its author's name takes
one more id). In a `batch`, step references (`{"from": {"$step": 1}, "to": {"$step": 2}}`, the `id`s steps 1 and 2 returned) avoid counting.
Running a session on a board: a countdown and votes. Both are part of the board, so everyone in a
live session sees the same, and neither is an undo step (undo leaves them as they are). On a
design file they refuse.
- `board.timer {action: start|pause|resume|add|stop, minutes?}` → `{state, left, text, ends}`: one
countdown per board. `start` takes `minutes` (default 5) and `add` adds them (default 1), from
one second up to 1440 minutes in all; `pause` keeps the time left, `resume` counts it down again.
The timer keeps its end time (milliseconds since 1970) or, paused, the time left, and each
client counts down on its own clock. `board.timerState {}` reads it: `state` is `off`,
`running`, `paused` or `done` (it ran out: 0:00 until someone stops it), `left` milliseconds,
`text` as `m:ss` (or `h:mm:ss`).
- `board.vote {action: start|cast|retract|end|clear, …}` → the vote as `board.voteResults` gives
it. One vote at a time: `start {votes? (per person, 1–99, default 3), on?: all|stickies|selection
(default all), ids? (with selection; default the selected layers)}` (a vote that ended may be
replaced; one still running is refused), `cast {id}` puts one of your votes on an object (several
on the same object are fine; once yours are used up it refuses), `retract {id}` takes one back,
`end` shows everyone's votes, `clear` removes the vote and its results. `voter` (any action)
votes as someone else, for scripts and tests; by default you are your live-session client, or
your name (`pref.userName`) when the file isn't shared. In the app a click on an object votes
while a vote runs, and a click on your badge (or an Alt-click) takes the vote back.
- `board.voteResults {voter?}` → `{state: off|running|ended, votesPerPerson, on, by, left, mine:
[{id, votes}], voters, results?}`: while it runs you see your own votes only (`mine`, `left`) and
how many have voted; once it ends `results` lists `[{id, name, votes, rank}]`, most votes first
(ties share a rank), with `name` the sticky's text or the layer's name. Votes on deleted objects
don't count.
```json
{"command": "board.timer", "params": {"action": "start", "minutes": 10}}
{"command": "board.vote", "params": {"action": "start", "votes": 2, "on": "stickies"}}
{"command": "board.vote", "params": {"action": "cast", "id": 2}}
{"command": "board.vote", "params": {"action": "end"}}
{"command": "board.voteResults", "params": {}}
```
### Sections, handoff and arrow keys
`run_command` with `object.wrapInSection {ids?, name?}` (Wrap in new section: layers on the page or
+3 -2
View File
@@ -1,6 +1,6 @@
# FigCraft roadmap: milestones and what's next
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (M12 Boards; 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-24 · **Change:** minor (M12 Boards: timer and voting; 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)
Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and
[`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours
@@ -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 | timer, voting, widgets, templates, tables, mind maps ([boards-parity](boards-parity.md)) | 30–60 |
| 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)) | 26–54 |
| M13 AI features (new, owner decision) | not started | local models | 30–60 |
## Next, after the current focus
@@ -75,6 +75,7 @@ blockers.
| Date | Change | Summary |
|---|---|---|
| 2026-10-24 | minor | M12: a shared board timer and voting sessions; 30–60 h → 26–54 h |
| 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. Milestone M12 Boards added; AI features move to M13 |
| 2026-10-15 | minor | Auto-save (#305, #296): Preferences ▸ Save files automatically (`pref.autoSave`, off by default; desktop only, the browser has no files to save to) saves each open file that has a path 3 s after its last change, so steady editing doesn't save every few seconds, and never during a gesture, a live text preview or while a version is shown. The file is encoded on a thread of its own and written as of that moment (edits made meanwhile stay unsaved); a failed save says why in a toast and is tried again a minute later, and version history checkpoints keep coming with it on. Numbers unchanged |
| 2026-10-15 | minor | A text box by dragging (#363, #360): a drag with the Text tool makes a fixed-size text box the size of the drag (at least one line tall); a click, or a drag shorter than the drag threshold, still makes auto-width text. Numbers unchanged |
File diff suppressed because one or more lines are too long