mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-01 06:16:46 -06:00
+3
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents)
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (`.sketch` import, best effort (Sketch 48 and newer); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (`.sketch` import, best effort (Sketch 48 and newer); Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7)
|
||||
|
||||
FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow,
|
||||
native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through
|
||||
@@ -152,6 +152,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||
- **2026-10-17** · Control channel while the window is minimised (M8, #720): a minimised or covered window answers every request that acts on the app's state (edits, `ui.inspect`, `ui.pointer`, `ui.key`, `ui.type`, `ui.set`, `app.quit`…) without drawing, and what needs a drawn frame (`ui.screenshot`, `ui.input`, `window.new`) is an error at once; before, those waited up to 60 s and held up every later request on the connection (an attached agent's `inspect_ui` or window screenshot stalled until the window came back). Measured on Windows 11, minimised: requests ~100 ms each (eframe wakes a minimised window at most ten times a second; ~2 ms on screen), a window screenshot fails in ~100 ms instead of 60 s, idle CPU 0. Numbers unchanged.
|
||||
- **2026-10-17** · Number variables on text content (M3, #629): the Typography section's variable picker also lists number variables for a text layer's content, as the engine already accepted (the text shows the number, e.g. 42.5, and follows the variable). Other text fields keep their own types. Numbers unchanged.
|
||||
- **2026-10-17** · Frames and sections convert into each other (M1, #719): `object.convertToSection` turns a frame on the page or in a section into a section in place (same box, name, fill and layers, which stay where they are; what only a frame has goes: auto layout, clipping, layout grids, guides, scrolling), and `object.convertToFrame` now also turns a section into a frame that keeps its fill and doesn't clip, so the look stays. Nested frames, instances, main components and sections holding sections are refused with the reason; one undo step each. Both are in the canvas context menu (Convert to section / Convert to frame) and in Spanish. Numbers unchanged.
|
||||
- **2026-10-16** · Adobe Fonts found on macOS (M7, #298): fonts activated through Adobe Fonts rendered in the fallback font without a warning, because Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes `~/Library/Application Support/Adobe/CoreSync/plugins/livetype`, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Regression test in the font database tests; docs/cli.md lists the folder. Numbers unchanged.
|
||||
- **2026-10-15** · Dev Mode tail (M6, gap 16): generated layer trees start with the import lines of the connected components they call, once each per language; Compare changes takes any saved version as the before; a behaviour suite made the Dev Mode canvas inspect-only (no moving, resizing, drawing or editing; menus and shortcuts refuse design edits with a note), added Changed to the ready list and refused statuses on layers inside frames; read-only plugins show their panels from the Inspect tab. Dev Mode 97% / 76%.
|
||||
- **2026-10-14** · Corner handles win over the corner-radius handles (M1, #292): a press on the inner half of a corner handle's square picked the radius handle 10 px in, so the drag neither resized nor (dragging outward) rounded anything. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the link); further in the radius handle still answers. Regression test in the transform behaviour suite. Numbers unchanged.
|
||||
- **2026-10-14** · Home screen recent files as cards (M7): each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged.
|
||||
@@ -402,6 +403,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md).
|
||||
| 2026-10-17 | minor | Community fixes from #524 up (batches #727, #728, #733, #735, #736 and #729; 55 PRs) and a crash guard. `.fig` import: explicit instance identity, overrides by key, text style precedence, grid track sizes, slot content, connector labels, skip ink, paragraph options and hanging lists, list start numbers, vertical trim, per-point stroke joins, text on a path, image adjustments and embedded images; large files open within budgets from the file's own size; an import report after opening; imported bytes stay out of the command journal. Files and paste: save a copy as `.fig`; Cmd/Ctrl+V pastes images copied in other apps and layers copied from another design app (`edit.pasteDesignLayers`); paste over and paste properties; clipboard reads on Linux and the BSDs time out after 2 s. Editor: live font preview on hover; Shift+I and Resources open Assets; Shift-drag moves the selection; thin strokes take a nearby double-click; dropped points merge; Flatten keeps open paths; the Actions palette gets tabs, ranked search, a keyboard highlight and recents; the colour picker opens on a new fill or stroke; text strokes follow their alignment; selection highlights after line breaks; a draggable pages/layers divider; variant properties reorder by drag; submenus keep their hover with a safe triangle; menu actions that need a value ask for it; File ▸ Rename edits the name in place. Engine: bad paint indexes, non-finite geometry and pages in copy/cut are refused; a param naming a layer says so when it isn't one; a CLI script is one undo step and resolves `$step`. Server and collaboration: message limits follow `max_file_mb` (client capped at 1 GiB); a damaged generation field no longer drops flushed edits; the web build joins a session from a `#join=` link. Also Swedish command labels, Nix CI through the shared workflow, paint bounds once per render, and a guard that turns a panic inside the boolean-op library into bad input (the stroke draws unclipped). Numbers unchanged. |
|
||||
| 2026-10-17 | minor | Number variables on text content (M3, #629): the Typography section's variable picker also lists number variables for a text layer's content, as the engine already accepted (the text shows the number, e.g. 42.5, and follows the variable). Other text fields keep their own types. Numbers unchanged. |
|
||||
| 2026-10-17 | minor | Frames and sections convert into each other (M1, #719): `object.convertToSection` turns a frame on the page or in a section into a section in place (same box, name, fill and layers, which stay where they are; what only a frame has goes: auto layout, clipping, layout grids, guides, scrolling), and `object.convertToFrame` now also turns a section into a frame that keeps its fill and doesn't clip, so the look stays. Nested frames, instances, main components and sections holding sections are refused with the reason; one undo step each. Both are in the canvas context menu (Convert to section / Convert to frame) and in Spanish. Numbers unchanged. |
|
||||
| 2026-10-16 | minor | Adobe Fonts found on macOS (M7, #298): fonts activated through Adobe Fonts rendered in the fallback font without a warning, because Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes `~/Library/Application Support/Adobe/CoreSync/plugins/livetype`, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Regression test in the font database tests; docs/cli.md lists the folder. Numbers unchanged |
|
||||
| 2026-10-16 | minor | Community fixes from #478 up (batches #591, #598, #602; 69 PRs) and a control-channel fix: a line that isn't JSON now ends a `--control` connection, so a web page can't drive the app through a browser request (#589). Highlights: page copies keep navigation, flows, prototype settings and guides; Find and Replace keeps line endings and case-insensitive matches; text hyperlinks survive import and export; recent colours record only what was picked; MCP screenshots respect `maxSize`; Swedish catalog (UI strings), Czech plurals; bounded canvas overscan while panning; Korean fallback at the run's weight; many vector-edit, guide, page and .fig round-trip fixes. Numbers unchanged |
|
||||
| 2026-10-15 | minor | Drop files anywhere to open them (M7): dragging files over the window shows a "Drop to open" (home) or "Drop to import" (canvas) overlay. On the home screen each supported file opens as a document of its own and other files are turned away with a toast; on the canvas the files import at the drop point. Both wait behind a small card that is on screen before a large file blocks the window. A canvas import stays with the document and page that received the drop even if the tab or page changes meanwhile, and is cancelled with a message if that document or page is gone. Numbers unchanged. |
|
||||
| 2026-10-15 | minor | M7: system font discovery skips entries observed as non-regular files before opening them, avoiding the reproduced named-pipe stall. Regular-file symlinks remain supported; directory depth, font limits and priorities are unchanged. Numbers unchanged. |
|
||||
|
||||
@@ -269,14 +269,15 @@ fn main() -> eframe::Result {
|
||||
let result = eframe::run_native(
|
||||
"FigCraft",
|
||||
options,
|
||||
Box::new(move |_cc| {
|
||||
Box::new(move |cc| {
|
||||
with_app(|fig| fig.services.set_egui_context(&cc.egui_ctx));
|
||||
Ok(Box::new(App {
|
||||
queue,
|
||||
ctx_slot,
|
||||
title: String::new(),
|
||||
recovery,
|
||||
#[cfg(target_os = "macos")]
|
||||
apple_events: apple_events.connect(&_cc.egui_ctx),
|
||||
apple_events: apple_events.connect(&cc.egui_ctx),
|
||||
}))
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ use serde_json::{Value, json};
|
||||
|
||||
pub struct Desktop {
|
||||
clipboard: Option<arboard::Clipboard>,
|
||||
egui_ctx: Option<egui::Context>,
|
||||
/// Where the preferences live ([`config_dir`]).
|
||||
dir: Option<PathBuf>,
|
||||
pub prefs: Option<Prefs>,
|
||||
@@ -61,6 +62,7 @@ impl Desktop {
|
||||
let dark = Arc::new(AtomicU8::new(DARK_UNKNOWN));
|
||||
Self {
|
||||
clipboard: arboard::Clipboard::new().ok(),
|
||||
egui_ctx: None,
|
||||
dir,
|
||||
prefs,
|
||||
reduce_motion,
|
||||
@@ -409,10 +411,13 @@ impl Services for Desktop {
|
||||
remembering(self.last_folder.clone(), pick, true)
|
||||
}
|
||||
fn clipboard_set_text(&mut self, s: &str) {
|
||||
if let Some(c) = self.clipboard.as_mut() {
|
||||
let _ = c.set_text(s.to_string());
|
||||
if let Some(ctx) = &self.egui_ctx {
|
||||
ctx.copy_text(s.to_string());
|
||||
}
|
||||
}
|
||||
fn set_egui_context(&mut self, ctx: &egui::Context) {
|
||||
self.egui_ctx = Some(ctx.clone());
|
||||
}
|
||||
fn clipboard_get_text(&mut self) -> Option<String> {
|
||||
read_clipboard(self.clipboard.as_mut()?, |c| c.get_text().ok())
|
||||
}
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
//! report it, instead of the app quitting silently (#261).
|
||||
|
||||
use figcraft_ui_egui::dialogs::window_failed as message;
|
||||
use figcraft_ui_egui::i18n::tr;
|
||||
|
||||
const ERROR: &str = "WGPU error: Failed to create surface for any enabled backend: {}";
|
||||
const BACKEND_HINT: &str = "WGPU_BACKEND is set and names the graphics API to use; unsetting it lets FigCraft choose.";
|
||||
const DRIVER_HINT: &str = if cfg!(windows) {
|
||||
"Updating the graphics driver often helps. To use OpenGL only, start FigCraft from a Command Prompt with:"
|
||||
} else {
|
||||
"Updating the graphics driver often helps. WGPU_BACKEND (vulkan, gl, metal) names another graphics API to try."
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn the_message_carries_the_error_and_where_to_report_it() {
|
||||
@@ -15,9 +22,9 @@ fn the_message_carries_the_error_and_where_to_report_it() {
|
||||
#[test]
|
||||
fn without_wgpu_backend_it_suggests_the_driver_and_another_graphics_api() {
|
||||
let text = message(ERROR, false);
|
||||
assert!(text.contains("graphics driver"), "{text}");
|
||||
assert!(text.contains(tr(DRIVER_HINT)), "{text}");
|
||||
assert!(text.contains("WGPU_BACKEND"), "another graphics API to try: {text}");
|
||||
assert!(!text.contains("unsetting"), "{text}");
|
||||
assert!(!text.contains(tr(BACKEND_HINT)), "{text}");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -30,6 +37,6 @@ fn on_windows_it_shows_how_to_start_with_opengl() {
|
||||
#[test]
|
||||
fn with_wgpu_backend_set_it_suggests_unsetting_it() {
|
||||
let text = message(ERROR, true);
|
||||
assert!(text.contains("unsetting"), "{text}");
|
||||
assert!(!text.contains("graphics driver"), "the chosen backend is the likelier cause: {text}");
|
||||
assert!(text.contains(tr(BACKEND_HINT)), "{text}");
|
||||
assert!(!text.contains(tr(DRIVER_HINT)), "the chosen backend is the likelier cause: {text}");
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ sync_github_name = true
|
||||
[people.AganzeFelicite]
|
||||
sync_github_name = true
|
||||
|
||||
[people.mojtabajafarzadeh]
|
||||
real_name = "Mojtaba Jafarzadeh"
|
||||
|
||||
[people.switch87]
|
||||
real_name = "Gert Pellin"
|
||||
display_name = "Gert Pellin"
|
||||
|
||||
@@ -24,6 +24,12 @@ pub struct NetLimits {
|
||||
pub hello_timeout: Duration,
|
||||
/// A connection with no traffic for this long is dead.
|
||||
pub idle_timeout: Duration,
|
||||
/// How long a one-shot request connection (hosting, a library, a plugin directory — and, most
|
||||
/// importantly, publishing a file) waits for the server's reply before giving up. Publishing
|
||||
/// uploads a whole file and a large one on a slow link takes the server a while to receive and
|
||||
/// answer, so this is longer than `idle_timeout` (a large project on a slow connection can
|
||||
/// easily need several minutes).
|
||||
pub request_reply_timeout: Duration,
|
||||
/// A quiet client pings this often (keeps it under `idle_timeout`).
|
||||
pub keepalive: Duration,
|
||||
/// Messages queued for one client before it counts as stuck and is disconnected.
|
||||
@@ -68,6 +74,7 @@ impl Default for NetLimits {
|
||||
burst: 400.0,
|
||||
hello_timeout: Duration::from_secs(10),
|
||||
idle_timeout: Duration::from_secs(45),
|
||||
request_reply_timeout: Duration::from_secs(300),
|
||||
keepalive: Duration::from_secs(15),
|
||||
queue: 1024,
|
||||
write_timeout: Duration::from_secs(10),
|
||||
@@ -85,3 +92,16 @@ impl Default for NetLimits {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn request_reply_wait_is_longer_than_idle() {
|
||||
let d = NetLimits::default();
|
||||
// Publishing uploads a whole file; a slow link must not cut it off at the session idle
|
||||
// timeout. Keep the reply wait comfortably above it.
|
||||
assert!(d.request_reply_timeout > d.idle_timeout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,13 +196,15 @@ pub fn plugins(server: &Endpoint, key: &str, req: PluginReq, limits: &NetLimits,
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// A WebSocket to `server` that waits for answers up to the idle timeout (a large upload takes the
|
||||
/// server a while to keep).
|
||||
/// A WebSocket to `server` that waits for answers up to the request-reply timeout (a large upload
|
||||
/// takes the server a while to keep).
|
||||
fn connect(server: &Endpoint, limits: &NetLimits, timeout: Duration) -> Result<Ws, String> {
|
||||
let (stream, _, _) = open(server, timeout, limits)?;
|
||||
let (ws, _) = tungstenite::client::client_with_config(server.url(), stream, Some(config(limits.max_server_message)))
|
||||
.map_err(|e| format!("{} doesn't answer as a FigCraft server: {e}", server.display()))?;
|
||||
ws.get_ref().set_read_timeout(Some(limits.idle_timeout)).map_err(|e| e.to_string())?;
|
||||
// A publish uploads a whole file; a large project on a slow link can take several minutes to
|
||||
// receive and answer. The wait uses `request_reply_timeout`, not the session `idle_timeout`.
|
||||
ws.get_ref().set_read_timeout(Some(limits.request_reply_timeout)).map_err(|e| e.to_string())?;
|
||||
Ok(ws)
|
||||
}
|
||||
|
||||
|
||||
+13
-5
@@ -44,6 +44,10 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Deepest descendant [`Document::world_render_bounds`] follows: as deep as the renderer draws
|
||||
/// (`figcraft_render::MAX_EXPORT_DEPTH`), so a crafted chain can't overflow the stack (#1062).
|
||||
const MAX_RENDER_DEPTH: usize = 256;
|
||||
|
||||
/// Stable node identifier, unique within a document.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
@@ -703,12 +707,16 @@ impl Document {
|
||||
/// a clipping frame's own area, else that and what its visible children show (masked
|
||||
/// children only within their mask). A group is what its children show, grown by its effects.
|
||||
pub fn world_render_bounds(&self, id: NodeId) -> Option<Rect> {
|
||||
self.render_bounds_at(id, 0)
|
||||
}
|
||||
|
||||
fn render_bounds_at(&self, id: NodeId, depth: usize) -> Option<Rect> {
|
||||
let n = self.node(id)?;
|
||||
let own = n.own_paint_bounds(self.world_transform(id));
|
||||
if n.clip && n.kind.is_frame_like() && !n.kind.bounds_follow_children() {
|
||||
if (n.clip && n.kind.is_frame_like() && !n.kind.bounds_follow_children()) || depth >= MAX_RENDER_DEPTH {
|
||||
return Some(own);
|
||||
}
|
||||
Some(match (self.children_render_bounds(n), &n.kind) {
|
||||
Some(match (self.children_render_bounds(n, depth + 1), &n.kind) {
|
||||
(Some(kids), NodeKind::Group) => n.grow_by_effects(kids),
|
||||
(Some(kids), _) => own.union(kids),
|
||||
(None, _) => own,
|
||||
@@ -716,15 +724,15 @@ impl Document {
|
||||
}
|
||||
|
||||
/// What `n`'s visible children show, together: each mask's run within the mask's bounds.
|
||||
fn children_render_bounds(&self, n: &Node) -> Option<Rect> {
|
||||
fn children_render_bounds(&self, n: &Node, depth: usize) -> Option<Rect> {
|
||||
let shown = |i: &NodeId| self.node(*i).is_some_and(|c| c.visible && !c.is_mask);
|
||||
let mut out: Option<Rect> = None;
|
||||
for run in self.paint_runs(n) {
|
||||
let ids = n.children.get(run).unwrap_or(&[]);
|
||||
let mask = ids.first().copied().filter(|m| self.node(*m).is_some_and(|c| c.is_mask && c.visible));
|
||||
let r = ids.iter().filter(|i| shown(i)).filter_map(|i| self.world_render_bounds(*i)).reduce(|a, b| a.union(b));
|
||||
let r = ids.iter().filter(|i| shown(i)).filter_map(|i| self.render_bounds_at(*i, depth)).reduce(|a, b| a.union(b));
|
||||
let r = match mask {
|
||||
Some(m) => r.zip(self.world_render_bounds(m)).map(|(a, b)| a.intersect(b)).filter(|x| x.width() > 0.0 && x.height() > 0.0),
|
||||
Some(m) => r.zip(self.render_bounds_at(m, depth)).map(|(a, b)| a.intersect(b)).filter(|x| x.width() > 0.0 && x.height() > 0.0),
|
||||
None => r,
|
||||
};
|
||||
out = match (out, r) {
|
||||
|
||||
@@ -560,6 +560,24 @@ fn is_ancestor_holds_for_very_deep_chains() {
|
||||
assert!(matches!(d.reparent(root, leaf, None), Err(DocError::Cycle(..))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_bounds_stop_where_drawing_stops_on_very_deep_chains() {
|
||||
// A crafted file nests groups far deeper than anything draws (#1062). The leaf counts while
|
||||
// it's within the renderer's depth and is left out past it, as the renderer leaves it out.
|
||||
let chain = |depth: usize| {
|
||||
let mut d = Document::new("x");
|
||||
let mut parent = page(&d);
|
||||
for _ in 0..depth {
|
||||
parent = d.create(parent, NodeKind::Group, "G", 0.0, 0.0, 10.0, 10.0).unwrap();
|
||||
}
|
||||
d.create(parent, NodeKind::Rectangle, "R", 100.0, 100.0, 10.0, 10.0).unwrap();
|
||||
let top = d.node(page(&d)).unwrap().children[0];
|
||||
d.world_render_bounds(top).unwrap()
|
||||
};
|
||||
assert_eq!(chain(MAX_RENDER_DEPTH).x1, 110.0);
|
||||
assert_eq!(chain(20_000).x1, 10.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_insert_at_run_boundary_keeps_runs_disjoint() {
|
||||
let mut t = TextData::new("0123456789");
|
||||
|
||||
@@ -258,3 +258,25 @@ fn a_nested_batch_has_its_own_names() {
|
||||
);
|
||||
assert!(msg.contains("`$ref` \"frame\" names no step"), "the outer names aren't visible inside: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failing_command_names_its_step_and_rolls_back() {
|
||||
let mut s = Session::new();
|
||||
let frame = id(&s.execute("node.create", &json!({"type": "frame", "width": 50, "height": 50})).unwrap());
|
||||
let text = id(&s.execute("node.create", &json!({"type": "text", "text": "orig"})).unwrap());
|
||||
let undo = s.ds().unwrap().history.undo.len();
|
||||
let name = s.doc().unwrap().node(s.page().unwrap()).unwrap().name.clone();
|
||||
let msg = batch_error(
|
||||
&mut s,
|
||||
json!([
|
||||
{"command": "text.set", "params": {"ids": [text.0], "text": "novo"}},
|
||||
{"command": "text.set", "params": {"ids": [frame.0], "text": "erro"}},
|
||||
{"command": "page.rename", "params": {"name": "Depois do erro"}}
|
||||
]),
|
||||
);
|
||||
// Still the command's own kind of error, its message saying which step.
|
||||
assert!(msg.starts_with("step 1: ") && msg.contains("is not text"), "{msg}");
|
||||
assert!(matches!(&s.doc().unwrap().node(text).unwrap().kind, figcraft_doc::NodeKind::Text(t) if t.characters == "orig"));
|
||||
assert_eq!(s.doc().unwrap().node(s.page().unwrap()).unwrap().name, name);
|
||||
assert_eq!(s.ds().unwrap().history.undo.len(), undo);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,18 @@ impl From<figcraft_doc::DocError> for EngineError {
|
||||
}
|
||||
}
|
||||
|
||||
impl EngineError {
|
||||
/// This error from step `step` of a batch, saying so in its message; the kind stays (errors
|
||||
/// without a message stay as they are).
|
||||
fn at_step(self, step: usize) -> Self {
|
||||
match self {
|
||||
EngineError::BadParams { cmd, msg } => EngineError::BadParams { cmd, msg: format!("step {step}: {msg}") },
|
||||
EngineError::Other(msg) => EngineError::Other(format!("step {step}: {msg}")),
|
||||
e => e,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, EngineError>;
|
||||
|
||||
pub fn bad(cmd: &str, msg: impl Into<String>) -> EngineError {
|
||||
@@ -1258,7 +1270,8 @@ impl Session {
|
||||
/// so its steps resolve references the same way, inside its own [`Session::grouped`].
|
||||
pub fn batch_step(&mut self, scope: &mut BatchScope, cmd: &str, params: &Value) -> Result<()> {
|
||||
let p = if cmd == "command.batch" { params.clone() } else { scope.resolve(params)? };
|
||||
let v = self.execute(cmd, &p)?;
|
||||
// Say which step failed; a nested batch names its own steps.
|
||||
let v = self.execute(cmd, &p).map_err(|e| if cmd == "command.batch" { e } else { e.at_step(scope.next_step()) })?;
|
||||
scope.push(v);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -272,6 +272,23 @@ fn sharing_online_publishes_to_the_server_in_preferences_and_shows_its_code() {
|
||||
assert_eq!((&r["status"], &r["code"], &r["share"]), (&json!("off"), &Value::Null, &Value::Null));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sharing_online_uses_a_separate_key_param() {
|
||||
// The Share popover sends the server key as its own field when the address (from Preferences,
|
||||
// which never holds a key) needs one. It must reach `publish` and not leak into the summary.
|
||||
let (mut s, script) = session();
|
||||
ex(&mut s, "pref.publishServer", json!({"url": "wss://share.example/"}));
|
||||
let r = ex(&mut s, "collab.share", json!({"key": "KeyKeyKeyKeyKeyKeyKey1"}));
|
||||
assert_eq!(r["status"], "connecting");
|
||||
{
|
||||
let sc = script.lock().unwrap();
|
||||
let [p] = sc.published.as_slice() else { panic!("published {}", sc.published.len()) };
|
||||
assert_eq!(p.server, "wss://share.example/");
|
||||
assert_eq!(p.key.as_deref(), Some("KeyKeyKeyKeyKeyKeyKey1"));
|
||||
}
|
||||
assert!(!r.to_string().contains("KeyKey"), "the server's key is nowhere in the summary");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_server_without_join_codes_leaves_the_links() {
|
||||
let (mut s, script) = session();
|
||||
|
||||
@@ -827,6 +827,7 @@ impl FontDb {
|
||||
}
|
||||
}
|
||||
|
||||
/// The folders this platform keeps installed and activated fonts in, walked by the font scan.
|
||||
fn system_font_dirs() -> Vec<std::path::PathBuf> {
|
||||
#[allow(unused_mut)]
|
||||
let mut v: Vec<std::path::PathBuf> = Vec::new();
|
||||
@@ -836,7 +837,11 @@ fn system_font_dirs() -> Vec<std::path::PathBuf> {
|
||||
v.push("/System/Library/Fonts".into());
|
||||
v.push("/Library/Fonts".into());
|
||||
if let Some(h) = std::env::var_os("HOME") {
|
||||
v.push(std::path::PathBuf::from(h).join("Library/Fonts"));
|
||||
let h = std::path::PathBuf::from(h);
|
||||
v.push(h.join("Library/Fonts"));
|
||||
// Adobe Fonts activates its fonts from here (hidden files named by number), without
|
||||
// installing them into a font folder.
|
||||
v.push(h.join("Library/Application Support/Adobe/CoreSync/plugins/livetype"));
|
||||
}
|
||||
// Fonts the system downloads (PingFang and other CJK faces on current versions) live in
|
||||
// its asset store; `Assets` is the older versions' store.
|
||||
@@ -886,6 +891,9 @@ fn unix_user_font_dirs(home: Option<std::ffi::OsString>, data_home: Option<std::
|
||||
dirs
|
||||
}
|
||||
|
||||
/// Adds the faces of every font file under `dir` (up to six folders deep) to `out`, keyed by
|
||||
/// lowercased family: files named `.ttf`, `.otf`, `.ttc` or `.otc`, and files without an extension
|
||||
/// that start with a font signature.
|
||||
fn scan_dir(dir: &std::path::Path, depth: usize, out: &mut BTreeMap<String, (String, Vec<Pending>)>) {
|
||||
if depth > 6 {
|
||||
return;
|
||||
@@ -909,7 +917,7 @@ fn scan_dir(dir: &std::path::Path, depth: usize, out: &mut BTreeMap<String, (Str
|
||||
continue;
|
||||
}
|
||||
let ext = path.extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase).unwrap_or_default();
|
||||
if !matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc") {
|
||||
if !matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc") && !(path.extension().is_none() && starts_like_a_font(&path)) {
|
||||
continue;
|
||||
}
|
||||
// Very large files are indexed all the same (Apple Color Emoji is ~180 MB and is where the
|
||||
@@ -930,6 +938,14 @@ fn scan_dir(dir: &std::path::Path, depth: usize, out: &mut BTreeMap<String, (Str
|
||||
}
|
||||
}
|
||||
|
||||
/// A file without an extension is a font when it starts with a font signature (font services such
|
||||
/// as Adobe Fonts keep their fonts in files named by number).
|
||||
fn starts_like_a_font(path: &std::path::Path) -> bool {
|
||||
use std::io::Read;
|
||||
let mut magic = [0u8; 4];
|
||||
std::fs::File::open(path).and_then(|mut f| f.read_exact(&mut magic)).is_ok() && matches!(&magic, &[0, 1, 0, 0] | b"OTTO" | b"true" | b"ttcf")
|
||||
}
|
||||
|
||||
/// The largest font file read (CJK collections run to ~120 MB, Apple Color Emoji ~180 MB).
|
||||
pub const MAX_FONT_FILE: u64 = 256 * 1024 * 1024;
|
||||
|
||||
|
||||
@@ -232,6 +232,35 @@ fn user_font_dirs_preserve_non_utf8_paths() {
|
||||
assert_eq!(dirs[1].as_os_str().as_bytes(), b"/data/\xfe/fonts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_scan_reads_font_files_without_an_extension() {
|
||||
// #298: Adobe Fonts keeps the fonts it activates in hidden files named by number. Any installed
|
||||
// font stands in for one (none on CI); a file that isn't a font is left alone.
|
||||
let Some(font) = system_font_dirs().into_iter().find_map(|d| {
|
||||
std::fs::read_dir(d)
|
||||
.ok()?
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.find(|p| p.extension().is_some_and(|x| x.eq_ignore_ascii_case("ttf") || x.eq_ignore_ascii_case("otf")))
|
||||
}) else {
|
||||
eprintln!("skipping: no installed font to stand in for an activated one");
|
||||
return;
|
||||
};
|
||||
let dir = std::env::temp_dir().join(format!("figcraft-font-bare-{}", std::process::id()));
|
||||
std::fs::create_dir_all(dir.join(".r")).unwrap();
|
||||
std::fs::copy(&font, dir.join(".r/.12345")).unwrap();
|
||||
std::fs::write(dir.join(".r/entitlements"), b"<xml/>").unwrap();
|
||||
let mut found = BTreeMap::new();
|
||||
scan_dir(&dir, 0, &mut found);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let families: Vec<&String> = found.values().map(|(f, _)| f).collect();
|
||||
assert_eq!(found.values().map(|(_, v)| v.len()).sum::<usize>(), faces_in(&std::fs::read(&font).unwrap()).len(), "{families:?}");
|
||||
assert!(!families.is_empty(), "{} is read as a font", font.display());
|
||||
assert!(found.values().flat_map(|(_, v)| v).all(|p| p.path.ends_with(".r/.12345")));
|
||||
#[cfg(target_os = "macos")]
|
||||
assert!(system_font_dirs().iter().any(|d| d.ends_with("Adobe/CoreSync/plugins/livetype")), "Adobe Fonts' folder is scanned");
|
||||
}
|
||||
|
||||
/// A rescan offers new families and the styles installed since, and never a file twice.
|
||||
#[test]
|
||||
fn a_rescan_offers_fonts_installed_since() {
|
||||
|
||||
@@ -1256,6 +1256,7 @@ name = "English"
|
||||
"Server address (ws://host:47900/)" = "Server address (ws://host:47900/)"
|
||||
"Server address" = "Server address"
|
||||
"Server for online sharing" = "Server for online sharing"
|
||||
"Server key" = "Server key"
|
||||
"Set as default" = "Set as default"
|
||||
"Set corner smoothing to 60%" = "Set corner smoothing to 60%"
|
||||
"Set variable" = "Set variable"
|
||||
|
||||
@@ -1250,6 +1250,7 @@ status = "draft — needs native review"
|
||||
"Server address (ws://host:47900/)" = "Dirección del servidor (ws://host:47900/)"
|
||||
"Server address" = "Dirección del servidor"
|
||||
"Server for online sharing" = "Servidor para compartir en línea"
|
||||
"Server key" = "Clave del servidor"
|
||||
"Set as default" = "Establecer como predeterminado"
|
||||
"Set variable" = "Establecer variable"
|
||||
"Set variable mode" = "Establecer modo de variable"
|
||||
|
||||
+131
-93
@@ -14,10 +14,10 @@ name = "Svenska"
|
||||
"+ Create variable" = "+ Skapa variabel"
|
||||
".fig (experimental)" = ".fig (experimentell)"
|
||||
"1…9, 0" = "1…9, 0"
|
||||
"@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PR:er, {commits} commits, +{added} / −{deleted} rader (Δ {delta}), +{bin_added} / −{bin_deleted} binära tillgångar, {first} – {last}"
|
||||
"A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "En samling i den här filen med dessa lägen och variabler: åsidosätt deras värden per läge och lägg till egna variabler"
|
||||
"@{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{bin_added} / −{bin_deleted} binary assets, {first} – {last}" = "@{login}: {prs} PR:er, {commits} commits, +{added} / −{deleted} rader (Δ {delta}), +{bin_added} / −{bin_deleted} binära resurser, {first} – {last}"
|
||||
"A collection of this file with these modes and variables: override their values per mode and add variables of its own" = "En samling i den här filen med dessa lägen och variabler: ange overrides för deras värden per läge och lägg till egna variabler"
|
||||
"A .figcraft file with this name already exists. Choose its full name to confirm replacement." = "Det finns redan en .figcraft-fil med det här namnet. Välj dess fullständiga namn för att bekräfta att den ersätts."
|
||||
"A layer on its own aligns to its frame: select more layers, or one in a frame" = "Ett ensamt lager justeras mot sin ram: markera fler lager, eller ett lager i en ram"
|
||||
"A layer on its own aligns to its frame: select more layers, or one in a frame" = "Ett ensamt lager justeras mot sin frame: markera fler lager, eller ett lager i en frame"
|
||||
"A responsive product page: hero, features, figures, a quote and a footer" = "En responsiv produktsida: toppsektion, funktioner, figurer, ett citat och en sidfot"
|
||||
"A spring plays until it settles: its mass, stiffness and damping set how long" = "En fjäder spelas tills den stannar: dess massa, styvhet och dämpning avgör hur länge"
|
||||
"A title, an agenda, a big figure, a chart and a closing slide" = "En titel, en agenda, en stor figur, ett diagram och en avslutande bild"
|
||||
@@ -34,14 +34,14 @@ name = "Svenska"
|
||||
"Add a category" = "Lägg till en kategori"
|
||||
"Add a comment" = "Lägg till en kommentar"
|
||||
"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 frame (F) or a shape (R) to get started" = "Lägg till en frame (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 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"
|
||||
"Add an annotation for developers" = "Lägg till en annotering för utvecklare"
|
||||
"Add another condition" = "Lägg till ett villkor till"
|
||||
"Add auto layout" = "Lägg till automatisk layout"
|
||||
"Add auto layout" = "Lägg till auto layout"
|
||||
"Add background behind overlay" = "Lägg till bakgrund bakom överlägg"
|
||||
"Add content to this slot" = "Lägg till innehåll i facket"
|
||||
"Add dev resource" = "Lägg till utvecklarresurs"
|
||||
@@ -92,6 +92,7 @@ name = "Svenska"
|
||||
"All components" = "Alla komponenter"
|
||||
"All fonts" = "Alla typsnitt"
|
||||
"All supported properties" = "Alla egenskaper som stöds"
|
||||
"All the fonts this file uses are installed" = "Alla typsnitt som filen använder är installerade"
|
||||
"All variables" = "Alla variabler"
|
||||
"Along the bottom" = "Längs nederkanten"
|
||||
"Along the left" = "Längs vänsterkanten"
|
||||
@@ -132,7 +133,7 @@ name = "Svenska"
|
||||
"Arrow at the end" = "Pil i slutet"
|
||||
"As typed" = "Som inskrivet"
|
||||
"Asking the server…" = "Frågar servern…"
|
||||
"Assets" = "Tillgångar"
|
||||
"Assets" = "Resurser"
|
||||
"Aug" = "aug"
|
||||
"Auto" = "Auto"
|
||||
"Auto (space around)" = "Auto (utrymme runt om)"
|
||||
@@ -142,15 +143,15 @@ name = "Svenska"
|
||||
"Auto columns" = "Automatiska kolumner"
|
||||
"Auto gap" = "Automatiskt mellanrum"
|
||||
"Auto height" = "Automatisk höjd"
|
||||
"Auto layout" = "Automatisk layout"
|
||||
"Auto layout" = "Auto layout"
|
||||
"Auto rows" = "Automatiska rader"
|
||||
"Auto width" = "Automatisk bredd"
|
||||
"Back to the FigCraft server" = "Tillbaka till FigCraft-servern"
|
||||
"Back to the font's value" = "Tillbaka till teckensnittets värde"
|
||||
"Back to the font's value" = "Tillbaka till typsnittets värde"
|
||||
"Back" = "Tillbaka"
|
||||
"Back to Ready for dev" = "Tillbaka till Redo för utveckling"
|
||||
"Back to Ready for dev: the next change compares with how it is now" = "Tillbaka till Redo för utveckling: nästa ändring jämförs med hur det ser ut nu"
|
||||
"Back to assets" = "Tillbaka till tillgångar"
|
||||
"Back to assets" = "Tillbaka till resurser"
|
||||
"Back to current" = "Tillbaka till aktuell"
|
||||
"Back to styles" = "Tillbaka till formatmallar"
|
||||
"Back to the canvas from a focused region" = "Tillbaka till arbetsytan från en fokuserad region"
|
||||
@@ -178,6 +179,7 @@ name = "Svenska"
|
||||
"Both directions" = "Båda riktningarna"
|
||||
"Bottom" = "Nederkant"
|
||||
"Bottom left radius" = "Radie nere till vänster"
|
||||
"Bottom padding" = "Nedre padding"
|
||||
"Bottom right radius" = "Radie nere till höger"
|
||||
"Branch" = "Gren"
|
||||
"Branch archived" = "Grenen har arkiverats"
|
||||
@@ -205,6 +207,7 @@ name = "Svenska"
|
||||
"CMYK colour" = "CMYK-färg"
|
||||
"Can view" = "Kan visa"
|
||||
"Check" = "Bock"
|
||||
"Checking the fonts installed on this computer…" = "Kontrollerar vilka typsnitt som är installerade på den här datorn…"
|
||||
"Checking the server…" = "Kontrollerar servern…"
|
||||
"Chevron" = "Vinkel"
|
||||
"Clear from this list" = "Ta bort från listan"
|
||||
@@ -296,7 +299,7 @@ name = "Svenska"
|
||||
"Connection lost, reconnecting (attempt {attempt})…" = "Anslutningen bröts, återansluter (försök {attempt})…"
|
||||
"Connector" = "Koppling"
|
||||
"Constrain proportions" = "Behåll proportioner"
|
||||
"Constraints" = "Begränsningar"
|
||||
"Constraints" = "Constraints"
|
||||
"Content" = "Innehåll"
|
||||
"Contents hidden" = "Innehållet är dolt"
|
||||
"Contextual alternates" = "Kontextuella alternativ"
|
||||
@@ -304,6 +307,8 @@ name = "Svenska"
|
||||
"Contributors" = "Bidragsgivare"
|
||||
"Contributors: {people} · Commits: {commits}" = "Bidragsgivare: {people} · Commits: {commits}"
|
||||
"Controller" = "Handkontroll"
|
||||
"Convert to frame" = "Konvertera till frame"
|
||||
"Convert to section" = "Konvertera till avsnitt"
|
||||
"Cool" = "Kall"
|
||||
"Copied as CSS" = "Kopierat som CSS"
|
||||
"Copied as PNG" = "Kopierat som PNG"
|
||||
@@ -420,12 +425,12 @@ name = "Svenska"
|
||||
"Drafts" = "Utkast"
|
||||
"Drag the stroke to add a width point" = "Dra i konturen för att lägga till en breddpunkt"
|
||||
"Draw" = "Rita"
|
||||
"Draw a frame" = "Rita en ram"
|
||||
"Draw a frame" = "Rita en frame"
|
||||
"Drawing tools" = "Ritverktyg"
|
||||
"Drop a .figcraft, .fig, .sketch, .json, .svg or image file." = "Släpp en .figcraft-, .fig-, .sketch-, .json- eller .svg-fil eller en bildfil."
|
||||
"Drop to import" = "Släpp för att importera"
|
||||
"Drop to open" = "Släpp för att öppna"
|
||||
"Drop shadow" = "Släppskugga"
|
||||
"Drop shadow" = "Slagskugga"
|
||||
"Dual-licensed MIT OR Apache-2.0. Made by the ArtCraft team and community." = "Dubbellicensierad under MIT OR Apache-2.0. Skapad av ArtCraft-teamet och gemenskapen."
|
||||
"Duo" = "Duo"
|
||||
"Duplicate" = "Duplicera"
|
||||
@@ -457,13 +462,13 @@ name = "Svenska"
|
||||
"End point" = "Slutpunkt"
|
||||
"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."
|
||||
"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 plugins den erbjuder."
|
||||
"Enter to save · Esc to cancel" = "Enter för att spara · Esc för att avbryta"
|
||||
"Eraser" = "Suddgummi"
|
||||
"Esc" = "Esc"
|
||||
"Essential" = "Grundläggande"
|
||||
"Every conflict has a choice." = "Alla konflikter har ett val."
|
||||
"Every top-level frame already starts a flow" = "Varje ram på översta nivån startar redan ett flöde"
|
||||
"Every top-level frame already starts a flow" = "Varje frame på översta nivån startar redan ett flöde"
|
||||
"Exact opacity (4 then 5: 45%)" = "Exakt opacitet (4 och sedan 5: 45 %)"
|
||||
"Exclude" = "Uteslut"
|
||||
"Excluded" = "Utesluten"
|
||||
@@ -474,7 +479,7 @@ name = "Svenska"
|
||||
"Expand" = "Expandera"
|
||||
"Export" = "Exportera"
|
||||
"Export all" = "Exportera alla"
|
||||
"Export frames and layers as PNG, JPG, SVG or PDF" = "Exportera ramar och lager som PNG, JPG, SVG eller PDF"
|
||||
"Export frames and layers as PNG, JPG, SVG or PDF" = "Exportera frames och lager som PNG, JPG, SVG eller PDF"
|
||||
"Export page as PNG" = "Exportera sidan som PNG"
|
||||
"Export settings" = "Exportinställningar"
|
||||
"Export variables" = "Exportera variabler"
|
||||
@@ -542,17 +547,17 @@ name = "Svenska"
|
||||
"Follow {name}" = "Följ {name}"
|
||||
"Following" = "Följer"
|
||||
"Following {name} · {key} to stop" = "Följer {name} · {key} för att sluta"
|
||||
"Font" = "Teckensnitt"
|
||||
"Font not installed" = "Teckensnittet är inte installerat"
|
||||
"Font" = "Typsnitt"
|
||||
"Font not installed" = "Typsnittet är inte installerat"
|
||||
"Font size" = "Teckenstorlek"
|
||||
"Font style" = "Teckenstil"
|
||||
"Forget on this computer" = "Glöm på den här datorn"
|
||||
"Four screens of a trail app with components and a prototype flow" = "Fyra skärmar i en vandringsapp med komponenter och ett prototypflöde"
|
||||
"Fractions" = "Bråk"
|
||||
"Frame" = "Ram"
|
||||
"Frame" = "Frame"
|
||||
"Frame selection" = "Rama in markering"
|
||||
"Frame tools" = "Ramverktyg"
|
||||
"Frames" = "Ramar"
|
||||
"Frame tools" = "Frame-verktyg"
|
||||
"Frames" = "Frames"
|
||||
"From the library \"{name}\": read-only" = "Från biblioteket \"{name}\": skrivskyddad"
|
||||
"From {name}" = "Från {name}"
|
||||
"From {server}" = "Från {server}"
|
||||
@@ -609,7 +614,7 @@ name = "Svenska"
|
||||
"Horizontal" = "Horisontell"
|
||||
"Horizontal align" = "Horisontell justering"
|
||||
"Horizontal hex" = "Horisontell hex"
|
||||
"Horizontal padding" = "Horisontell utfyllnad"
|
||||
"Horizontal padding" = "Horisontell padding"
|
||||
"Horizontal spacing" = "Horisontellt avstånd"
|
||||
"Hosted by {operator}" = "Värd: {operator}"
|
||||
"Hover a layer to see its properties" = "Hovra över ett lager för att se dess egenskaper"
|
||||
@@ -622,7 +627,7 @@ name = "Svenska"
|
||||
"If it keeps happening, please report it at {link} with this message." = "Om det fortsätter att hända kan du rapportera det på {link} och bifoga det här meddelandet."
|
||||
"If" = "Om"
|
||||
"If {condition}" = "Om {condition}"
|
||||
"Ignore auto layout (absolute position)" = "Ignorera automatisk layout (absolut position)"
|
||||
"Ignore auto layout (absolute position)" = "Ignorera auto layout (absolut position)"
|
||||
"Ignore overlapping layers" = "Ignorera överlappande lager"
|
||||
"Image" = "Bild"
|
||||
"Images" = "Bilder"
|
||||
@@ -643,6 +648,7 @@ name = "Svenska"
|
||||
"Included" = "Inkluderad"
|
||||
"Included in layout" = "Inkluderad i layouten"
|
||||
"Individual corners" = "Enskilda hörn"
|
||||
"Individual padding" = "Individuell padding"
|
||||
"Individual strokes" = "Enskilda konturer"
|
||||
"Ink" = "Bläck"
|
||||
"Inner shadow" = "Inre skugga"
|
||||
@@ -664,6 +670,7 @@ name = "Svenska"
|
||||
"Interaction" = "Interaktion"
|
||||
"Interactions" = "Interaktioner"
|
||||
"Interface design and prototyping. A free, open-source, clean-room design tool written from scratch in Rust, part of the ArtCraft Crafting Apps." = "Gränssnittsdesign och prototyper. Ett fritt clean-room-designverktyg med öppen källkod, skrivet från grunden i Rust och en del av ArtCraft Crafting Apps."
|
||||
"Interface size" = "Gränssnittsstorlek"
|
||||
"Intersect" = "Skärning"
|
||||
"Intersects" = "Skär"
|
||||
"Invalid color" = "Ogiltig färg"
|
||||
@@ -714,6 +721,7 @@ name = "Svenska"
|
||||
"Leaves" = "Löv"
|
||||
"Left" = "Vänster"
|
||||
"Left & right" = "Vänster och höger"
|
||||
"Left padding" = "Vänster padding"
|
||||
"Left side" = "Vänster sida"
|
||||
"Left side width" = "Bredd på vänster sida"
|
||||
"Letter case" = "Skiftläge"
|
||||
@@ -739,7 +747,7 @@ name = "Svenska"
|
||||
"Link" = "Länk"
|
||||
"Link copied" = "Länken har kopierats"
|
||||
"Link docs, code or tickets for developers." = "Länka dokumentation, kod eller ärenden för utvecklare."
|
||||
"Link frames on the Prototype tab, then present the flow" = "Länka ramar på fliken Prototyp och presentera sedan flödet"
|
||||
"Link frames on the Prototype tab, then present the flow" = "Länka frames på fliken Prototyp och presentera sedan flödet"
|
||||
"Link this component to its code component: instances then show your real snippet with their values." = "Länka den här komponenten till dess kodkomponent: instanserna visar då ditt riktiga kodavsnitt med sina värden."
|
||||
"List spacing" = "Listavstånd"
|
||||
"List style" = "Liststil"
|
||||
@@ -788,6 +796,7 @@ name = "Svenska"
|
||||
"Mirror angle and length" = "Spegla vinkel och längd"
|
||||
"Mirroring" = "Spegling"
|
||||
"Missing" = "Saknas"
|
||||
"Missing fonts" = "Saknade typsnitt"
|
||||
"Missing layer" = "Lager saknas"
|
||||
"Missing style" = "Stil saknas"
|
||||
"Missing variable" = "Variabel saknas"
|
||||
@@ -828,7 +837,7 @@ name = "Svenska"
|
||||
"New board" = "Ny whiteboard"
|
||||
"New design file" = "Ny designfil"
|
||||
"Newest first" = "Nyaste först"
|
||||
"Next frame" = "Nästa ram"
|
||||
"Next frame" = "Nästa frame"
|
||||
"Next or previous control in the focused region" = "Nästa eller föregående kontroll i den fokuserade regionen"
|
||||
"Next ready for dev" = "Nästa redo för utveckling"
|
||||
"Next size option" = "Nästa storleksalternativ"
|
||||
@@ -847,6 +856,7 @@ name = "Svenska"
|
||||
"No conflicts." = "Inga konflikter."
|
||||
"No contributor data was built into this copy." = "Inga uppgifter om bidragsgivare finns inbyggda i den här kopian."
|
||||
"No device" = "Ingen enhet"
|
||||
"No files or templates match your search" = "Inga filer eller mallar matchar sökningen"
|
||||
"No fonts match" = "Inga typsnitt matchar"
|
||||
"No join code here (the server is older than join codes, or this address can't be put in one): share the link instead." = "Ingen anslutningskod här (servern är äldre än anslutningskoder, eller så kan den här adressen inte göras om till en): dela länken i stället."
|
||||
"No layer changes to show." = "Inga lagerändringar att visa."
|
||||
@@ -861,9 +871,10 @@ name = "Svenska"
|
||||
"No model credits were built into this copy." = "Inga modellkrediteringar finns inbyggda i den här kopian."
|
||||
"No number variables fit here." = "Inga nummervariabler passar här."
|
||||
"No number variables yet. Create them in Local variables." = "Inga nummervariabler än. Skapa dem i Lokala variabler."
|
||||
"No other pages" = "Inga andra sidor"
|
||||
"No paints" = "Inga färger"
|
||||
"No plugins on {server}." = "Inga plugin-program på {server}."
|
||||
"No plugins yet. Import a development plugin from its figcraft-plugin.json." = "Inga plugin-program än. Importera ett plugin-program under utveckling från dess figcraft-plugin.json."
|
||||
"No plugins on {server}." = "Inga plugins på {server}."
|
||||
"No plugins yet. Import a development plugin from its figcraft-plugin.json." = "Inga plugins än. Importera ett plugin under utveckling från dess figcraft-plugin.json."
|
||||
"No preferred components" = "Inga föredragna komponenter"
|
||||
"No preset" = "Ingen förinställning"
|
||||
"No saved versions yet. Save a version to mark a point in your work you can come back to." = "Inga sparade versioner än. Spara en version för att markera en punkt i arbetet som du kan återvända till."
|
||||
@@ -885,19 +896,19 @@ name = "Svenska"
|
||||
"Normal" = "Normal"
|
||||
"Not bound" = "Inte kopplad"
|
||||
"Not in a live session" = "Inte i en livesession"
|
||||
"Not in this font" = "Finns inte i det här teckensnittet"
|
||||
"Not in this font" = "Finns inte i det här typsnittet"
|
||||
"Not properties, kept as written: {names}" = "Inte egenskaper, behölls som de skrevs: {names}"
|
||||
"Not read yet" = "Inte läst än"
|
||||
"Not shared" = "Inte delad"
|
||||
"Note {n}" = "Anteckning {n}"
|
||||
"Nothing changed since this was marked ready for dev." = "Inget har ändrats sedan detta markerades som klart för utveckling."
|
||||
"Nothing here yet. Set a frame's status to list it." = "Inget här än. Ange en rams status för att lista den."
|
||||
"Nothing here yet. Set a frame's status to list it." = "Inget här än. Ange en frames status för att lista den."
|
||||
"Nothing online yet. Use Share on a file to put it online and it is listed here." = "Inget online än. Använd Dela på en fil för att lägga ut den online, så visas den här."
|
||||
"Nothing to export" = "Inget att exportera"
|
||||
"Nov" = "nov"
|
||||
"Now" = "Nu"
|
||||
"Nudge (Shift: big nudge)" = "Knuffa (Shift: stor knuff)"
|
||||
"Nudge amount" = "Knuffavstånd"
|
||||
"Nudge (Shift: big nudge)" = "Flytta stegvis (Shift: stort steg)"
|
||||
"Nudge amount" = "Steglängd"
|
||||
"Number" = "Nummer"
|
||||
"Number ↑" = "Nummer ↑"
|
||||
"Number ↓" = "Nummer ↓"
|
||||
@@ -944,6 +955,7 @@ name = "Svenska"
|
||||
"Options" = "Alternativ"
|
||||
"Orange" = "Orange"
|
||||
"Order" = "Ordning"
|
||||
"Other" = "Annat"
|
||||
"Other apps may not open every .fig file exported here." = "Andra appar kanske inte kan öppna alla .fig-filer som exporteras här."
|
||||
"Others lose access and keep their copy; the file stays on the server for you" = "Andra förlorar åtkomst och behåller sin kopia; filen ligger kvar på servern åt dig"
|
||||
"Others type this under Share ▸ Join with a code" = "Andra skriver in den under Dela ▸ Anslut med en kod"
|
||||
@@ -955,12 +967,12 @@ name = "Svenska"
|
||||
"Overlay" = "Överlägg"
|
||||
"Overlay position" = "Överläggsposition"
|
||||
"Overridden here (the collection it extends has another value)" = "Åsidosatt här (samlingen som den utökar har ett annat värde)"
|
||||
"Overrides" = "Åsidosättningar"
|
||||
"Overrides" = "Overrides"
|
||||
"Owner token" = "Ägartoken"
|
||||
"Owner token copied" = "Ägartoken kopierad"
|
||||
"PDF print options" = "PDF-utskriftsalternativ"
|
||||
"PDF print settings" = "PDF-utskriftsinställningar"
|
||||
"Padding" = "Utfyllnad"
|
||||
"Padding" = "Padding"
|
||||
"Page" = "Sida"
|
||||
"Page deleted" = "Sidan har raderats"
|
||||
"Pages" = "Sidor"
|
||||
@@ -994,18 +1006,20 @@ name = "Svenska"
|
||||
"Phone" = "Telefon"
|
||||
"Pick color" = "Hämta färg"
|
||||
"Pick color (I)" = "Hämta färg (I)"
|
||||
"Pick one font family first" = "Välj en teckensnittsfamilj först"
|
||||
"Pick the Frame tool, then drag on the canvas or choose a device size" = "Välj ramverktyget och dra sedan på arbetsytan eller välj en enhetsstorlek"
|
||||
"Pick one font family first" = "Välj en typsnittsfamilj först"
|
||||
"Pick the Frame tool, then drag on the canvas or choose a device size" = "Välj verktyget Frame och dra sedan på arbetsytan eller välj en enhetsstorlek"
|
||||
"Pin a property" = "Fäst en egenskap"
|
||||
"Pinch" = "Nyp"
|
||||
"Pixel grid" = "Pixelrutnät"
|
||||
"Pixel preview 1x" = "Pixelförhandsgranskning 1x"
|
||||
"Pixel preview 2x" = "Pixelförhandsgranskning 2x"
|
||||
"Place a shape or frame (its tool chosen)" = "Placera en form eller ram (när dess verktyg är valt)"
|
||||
"Place a shape or frame (its tool chosen)" = "Placera en form eller frame (när dess verktyg är valt)"
|
||||
"Place automatically" = "Placera automatiskt"
|
||||
"Place image" = "Placera bild"
|
||||
"Plugins run in FigCraft's sandbox, without files, network or clock. Install only plugins you trust." = "Plugin-program körs i FigCrafts sandlåda, utan filer, nätverk eller klocka. Installera bara plugin-program du litar på."
|
||||
"Plugins" = "Plugin-program"
|
||||
"Plugin" = "Plugin"
|
||||
"Plugins & widgets" = "Plugins och widgetar"
|
||||
"Plugins run in FigCraft's sandbox, without files, network or clock. Install only plugins you trust." = "Plugins körs i FigCrafts sandlåda, utan filer, nätverk eller klocka. Installera bara plugins du litar på."
|
||||
"Plugins" = "Plugins"
|
||||
"Plus darker" = "Plus mörkare"
|
||||
"Plus lighter" = "Plus ljusare"
|
||||
"Plus one" = "Plus ett"
|
||||
@@ -1026,8 +1040,8 @@ name = "Svenska"
|
||||
"Presenting: comments" = "Presentation: kommentarer"
|
||||
"Presenting: cycle the size options" = "Presentation: växla mellan storleksalternativen"
|
||||
"Presenting: full screen" = "Presentation: helskärm"
|
||||
"Presenting: next frame" = "Presentation: nästa ram"
|
||||
"Presenting: previous frame" = "Presentation: föregående ram"
|
||||
"Presenting: next frame" = "Presentation: nästa frame"
|
||||
"Presenting: previous frame" = "Presentation: föregående frame"
|
||||
"Presenting: restart" = "Presentation: starta om"
|
||||
"Preserve scroll position" = "Bevara rullningsposition"
|
||||
"Presets" = "Förinställningar"
|
||||
@@ -1035,8 +1049,8 @@ name = "Svenska"
|
||||
"Playground" = "Testyta"
|
||||
"Preview" = "Förhandsgranskning"
|
||||
"Preview this flow" = "Förhandsgranska det här flödet"
|
||||
"Preview this frame" = "Förhandsgranska den här ramen"
|
||||
"Previous frame" = "Föregående ram"
|
||||
"Preview this frame" = "Förhandsgranska den här framen"
|
||||
"Previous frame" = "Föregående frame"
|
||||
"Previous ready for dev" = "Föregående redo för utveckling"
|
||||
"Progressive" = "Progressiv"
|
||||
"Properties" = "Egenskaper"
|
||||
@@ -1075,10 +1089,11 @@ name = "Svenska"
|
||||
"Reduce motion" = "Minska rörelse"
|
||||
"Refraction" = "Brytning"
|
||||
"Regular expression" = "Reguljärt uttryck"
|
||||
"Relative to your system's display scaling (100% is the system's own size). Document zoom is separate." = "Relativt systemets skalning av skärmen (100 % är systemets egen storlek). Dokumentets zoom är separat."
|
||||
"Remove" = "Ta bort"
|
||||
"Remove all" = "Ta bort alla"
|
||||
"Remove annotation" = "Ta bort anteckning"
|
||||
"Remove auto layout" = "Ta bort automatisk layout"
|
||||
"Remove auto layout" = "Ta bort auto layout"
|
||||
"Remove dev resource" = "Ta bort utvecklarresurs"
|
||||
"Remove export setting" = "Ta bort exportinställning"
|
||||
"Remove from recents" = "Ta bort från Senaste"
|
||||
@@ -1114,6 +1129,7 @@ name = "Svenska"
|
||||
"Rename on the server…" = "Byt namn på servern…"
|
||||
"Rename page" = "Byt namn på sida"
|
||||
"Rename to" = "Nytt namn"
|
||||
"Reorder selection" = "Ändra ordning på markeringen"
|
||||
"Replace all" = "Ersätt alla"
|
||||
"Replace mixed export settings" = "Ersätt blandade exportinställningar"
|
||||
"Replace with" = "Ersätt med"
|
||||
@@ -1124,14 +1140,14 @@ name = "Svenska"
|
||||
"Reset" = "Återställ"
|
||||
"Reset adjustments" = "Återställ justeringar"
|
||||
"Reset all changes" = "Återställ alla ändringar"
|
||||
"Reset all overrides on this layer" = "Återställ alla åsidosättningar på det här lagret"
|
||||
"Reset overrides" = "Återställ åsidosättningar"
|
||||
"Reset all overrides on this layer" = "Återställ alla overrides på det här lagret"
|
||||
"Reset overrides" = "Återställ overrides"
|
||||
"Reset slot" = "Återställ plats"
|
||||
"Reset this override" = "Återställ den här åsidosättningen"
|
||||
"Reset this override" = "Återställ den här overriden"
|
||||
"Reset to style" = "Återställ till formatmall"
|
||||
"Reset to the inherited value" = "Återställ till ärvt värde"
|
||||
"Resize by the nudge amount" = "Ändra storlek med knuffavståndet"
|
||||
"Resize frames to the device's screen (or the window without a device): their constraints and auto layout respond" = "Ändra storlek på ramar efter enhetens skärm (eller fönstret utan enhet): deras begränsningar och automatiska layout anpassas"
|
||||
"Resize by the nudge amount" = "Ändra storlek med steglängden"
|
||||
"Resize frames to the device's screen (or the window without a device): their constraints and auto layout respond" = "Ändra storlek på frames efter enhetens skärm (eller fönstret utan enhet): deras constraints och auto layout anpassas"
|
||||
"Resize to fit" = "Anpassa storlek"
|
||||
"Resizing" = "Storleksändring"
|
||||
"Resolve" = "Lös"
|
||||
@@ -1149,6 +1165,7 @@ name = "Svenska"
|
||||
"Review and merge" = "Granska och sammanfoga"
|
||||
"Ribbon" = "Verktygspanel"
|
||||
"Right" = "Höger"
|
||||
"Right padding" = "Höger padding"
|
||||
"Right side" = "Höger sida"
|
||||
"Right side width" = "Bredd på höger sida"
|
||||
"Rotate 90°" = "Rotera 90°"
|
||||
@@ -1170,6 +1187,9 @@ name = "Svenska"
|
||||
"Run" = "Kör"
|
||||
"Run {label}" = "Kör {label}"
|
||||
"Save files automatically" = "Spara filer automatiskt"
|
||||
"Search files and templates" = "Sök efter filer och mallar"
|
||||
"Search pages" = "Sök sidor"
|
||||
"Search results" = "Sökresultat"
|
||||
"Search templates" = "Sök mallar"
|
||||
"See all templates" = "Se alla mallar"
|
||||
"Select layers and they space themselves as their content changes" = "Markera lager så fördelar de avståndet själva när innehållet ändras"
|
||||
@@ -1191,10 +1211,10 @@ name = "Svenska"
|
||||
"Scroll to" = "Rulla till"
|
||||
"Scroll to layer" = "Rulla till lager"
|
||||
"Scroll with parent" = "Rulla med överordnat element"
|
||||
"Search plugins" = "Sök plugin-program"
|
||||
"Search plugins" = "Sök plugins"
|
||||
"Search" = "Sök"
|
||||
"Search actions, layers and tools" = "Sök åtgärder, lager och verktyg"
|
||||
"Search assets" = "Sök tillgångar"
|
||||
"Search assets" = "Sök resurser"
|
||||
"Search comments" = "Sök i kommentarer"
|
||||
"Search fonts" = "Sök typsnitt"
|
||||
"Search layers" = "Sök lager"
|
||||
@@ -1202,8 +1222,8 @@ name = "Svenska"
|
||||
"Section" = "Avsnitt"
|
||||
"Section color" = "Avsnittsfärg"
|
||||
"Sections" = "Avsnitt"
|
||||
"Select a frame and add a starting point to present it as a flow." = "Markera en ram och lägg till en startpunkt för att presentera den som ett flöde."
|
||||
"Select a frame to preview it" = "Markera en ram för att förhandsgranska den"
|
||||
"Select a frame and add a starting point to present it as a flow." = "Markera en frame och lägg till en startpunkt för att presentera den som ett flöde."
|
||||
"Select a frame to preview it" = "Markera en frame för att förhandsgranska den"
|
||||
"Select a layer to see its properties and code" = "Markera ett lager för att se dess egenskaper och kod"
|
||||
"Select all" = "Markera alla"
|
||||
"Select all with same" = "Markera alla med samma"
|
||||
@@ -1350,16 +1370,16 @@ name = "Svenska"
|
||||
"Subtract" = "Subtrahera"
|
||||
"Subtracts" = "Subtraktioner"
|
||||
"Suffix" = "Suffix"
|
||||
"Suspend snapping while dragging" = "Stäng av fästning medan du drar"
|
||||
"Suspend snapping while dragging" = "Stäng av fästfunktionen medan du drar"
|
||||
"Swap instance" = "Byt instans"
|
||||
"Swap library: use another library's assets of the same names" = "Byt bibliotek: använd tillgångar med samma namn från ett annat bibliotek"
|
||||
"Swap library: use another library's assets of the same names" = "Byt bibliotek: använd resurser med samma namn från ett annat bibliotek"
|
||||
"Swap orientation" = "Växla orientering"
|
||||
"Swap overlay" = "Byt överlägg"
|
||||
"Swap to {name}" = "Byt till {name}"
|
||||
"Swap width and height" = "Byt plats på bredd och höjd"
|
||||
"Swapped instance" = "Utbytt instans"
|
||||
"Swapped {n} assets" = { one = "Bytte {n} tillgång", other = "Bytte {n} tillgångar" }
|
||||
"Swapped {n} assets; {missed} had no match and still use the old library" = { one = "Bytte {n} tillgång; {missed} saknade motsvarighet och använder fortfarande det gamla biblioteket", other = "Bytte {n} tillgångar; {missed} saknade motsvarighet och använder fortfarande det gamla biblioteket" }
|
||||
"Swapped {n} assets" = { one = "Bytte {n} resurs", other = "Bytte {n} resurser" }
|
||||
"Swapped {n} assets; {missed} had no match and still use the old library" = { one = "Bytte {n} resurs; {missed} saknade motsvarighet och använder fortfarande det gamla biblioteket", other = "Bytte {n} resurser; {missed} saknade motsvarighet och använder fortfarande det gamla biblioteket" }
|
||||
"Sweep" = "Svep"
|
||||
"System" = "Enligt systemet"
|
||||
"Tab / Shift+Tab" = "Tab / Shift+Tab"
|
||||
@@ -1395,6 +1415,7 @@ name = "Svenska"
|
||||
"The selection uses several text styles: click to apply one" = "Markeringen använder flera textstilar: klicka för att använda en"
|
||||
"The server deletes this file if nobody opens it before {date}." = "Servern raderar filen om ingen öppnar den före {date}."
|
||||
"The whole file" = "Hela filen"
|
||||
"The window is too small for {size}%, so the interface is drawn at {drawn}%. Make the window larger to use {size}%." = "Fönstret är för litet för {size} %, så gränssnittet ritas i {drawn} %. Gör fönstret större för att använda {size} %."
|
||||
"Theme" = "Tema"
|
||||
"There's no destination for that action" = "Det finns inget mål för den åtgärden"
|
||||
"Thickness" = "Tjocklek"
|
||||
@@ -1406,8 +1427,10 @@ name = "Svenska"
|
||||
"This file" = "Den här filen"
|
||||
"This file has unsaved changes" = "Filen har osparade ändringar"
|
||||
"This file is on {server}, open only to you. Share it to let others in with a new code." = "Filen ligger på {server} och är bara öppen för dig. Dela den för att släppa in andra med en ny kod."
|
||||
"This frame is new on the branch." = "Den här ramen är ny på grenen."
|
||||
"This frame isn't on the branch." = "Den här ramen finns inte på grenen."
|
||||
"This file is too large to join here (up to {mb} MB)" = "Den här filen är för stor för att ansluta till här (högst {mb} MB)"
|
||||
"This file uses fonts that aren't installed on this computer. Their text is drawn in a fallback font until you install them." = "Den här filen använder typsnitt som inte är installerade på den här datorn. Deras text ritas med ett reservtypsnitt tills du installerar dem."
|
||||
"This frame is new on the branch." = "Den här framen är ny på grenen."
|
||||
"This frame isn't on the branch." = "Den här framen finns inte på grenen."
|
||||
"This server is open to the public: no key needed." = "Servern är öppen för alla: ingen nyckel behövs."
|
||||
"This server needs its key: add ?key=… to the address (`figcraft-server key` prints it on the server's machine)." = "Servern kräver sin nyckel: lägg till ?key=… i adressen (`figcraft-server key` skriver ut den på serverns dator)."
|
||||
"This server takes files only with its key: see Advanced." = "Servern tar bara emot filer med sin nyckel: se Avancerat."
|
||||
@@ -1427,6 +1450,7 @@ name = "Svenska"
|
||||
"Top" = "Överkant"
|
||||
"Top & bottom" = "Över- och nederkant"
|
||||
"Top left radius" = "Radie uppe till vänster"
|
||||
"Top padding" = "Övre padding"
|
||||
"Top right radius" = "Radie uppe till höger"
|
||||
"Transform" = "Transformera"
|
||||
"Triangle" = "Triangel"
|
||||
@@ -1437,6 +1461,7 @@ name = "Svenska"
|
||||
"Truncate with an ellipsis" = "Korta av med ellips"
|
||||
"Try again" = "Försök igen"
|
||||
"Try another search or filter." = "Prova en annan sökning eller ett annat filter."
|
||||
"Try another search, or browse templates." = "Prova en annan sökning eller bläddra bland mallar."
|
||||
"Try another word, or look at every kind." = "Prova ett annat ord, eller visa alla typer."
|
||||
"Turn a layer into a component, reuse it, and change every copy at once" = "Gör om ett lager till en komponent, återanvänd den och ändra alla kopior på en gång"
|
||||
"Two digits" = "Två siffror"
|
||||
@@ -1471,10 +1496,11 @@ name = "Svenska"
|
||||
"Use a fixed value" = "Använd ett fast värde"
|
||||
"Use an expression, e.g. count + 1" = "Använd ett uttryck, t.ex. count + 1"
|
||||
"Use as mask" = "Använd som mask"
|
||||
"Use auto layout" = "Använd automatisk layout"
|
||||
"Use auto layout" = "Använd auto layout"
|
||||
"Use number keys for opacity" = "Använd siffertangenterna för opacitet"
|
||||
"Use the .figcraft or .fig extension." = "Använd filändelsen .figcraft eller .fig."
|
||||
"Use the layer's fill" = "Använd lagrets fyllning"
|
||||
"Use your browser's zoom to change the interface size." = "Använd webbläsarens zoom för att ändra gränssnittets storlek."
|
||||
"Used in {files} · {instances}" = "Används i {files} · {instances}"
|
||||
"Username" = "Användarnamn"
|
||||
"Value" = "Värde"
|
||||
@@ -1496,7 +1522,7 @@ name = "Svenska"
|
||||
"Vertical" = "Vertikal"
|
||||
"Vertical align" = "Vertikal justering"
|
||||
"Vertical hex" = "Vertikal hex"
|
||||
"Vertical padding" = "Vertikal utfyllnad"
|
||||
"Vertical padding" = "Vertikal padding"
|
||||
"Vertical spacing" = "Vertikalt avstånd"
|
||||
"Vertical trim" = "Vertikal trimning"
|
||||
"View" = "Visa"
|
||||
@@ -1523,12 +1549,13 @@ name = "Svenska"
|
||||
"What changed since it was marked ready for dev" = "Vad som har ändrats sedan den markerades som klar för utveckling"
|
||||
"What is wrong with this file? The server's operator gets your report." = "Vad är fel med den här filen? Serverns operatör får din rapport."
|
||||
"What it's for" = "Vad den är till för"
|
||||
"What this changed from isn't known: it changed before FigCraft kept a copy of ready-for-dev frames. Resolve the change (or mark it ready again) to compare from now on." = "Vad detta ändrades från är okänt: ändringen gjordes innan FigCraft började spara en kopia av ramar som är klara för utveckling. Lös ändringen (eller markera den som klar igen) för att kunna jämföra från och med nu."
|
||||
"What this changed from isn't known: it changed before FigCraft kept a copy of ready-for-dev frames. Resolve the change (or mark it ready again) to compare from now on." = "Vad detta ändrades från är okänt: ändringen gjordes innan FigCraft började spara en kopia av frames som är klara för utveckling. Lös ändringen (eller markera den som klar igen) för att kunna jämföra från och med nu."
|
||||
"While hovering" = "Vid hovring"
|
||||
"While pressing" = "Vid nedtryckning"
|
||||
"White" = "Vit"
|
||||
"Who runs it, its terms and how long it keeps files" = "Vem som driver den, dess villkor och hur länge den sparar filer"
|
||||
"Why it changed" = "Varför det ändrades"
|
||||
"Widget" = "Widget"
|
||||
"Widgets" = "Widgetar"
|
||||
"Width" = "Bredd"
|
||||
"Width point" = "Breddpunkt"
|
||||
@@ -1579,7 +1606,7 @@ name = "Svenska"
|
||||
"connector label was skipped: text style data is missing" = "etiketten på en kopplingslinje hoppades över: textens stildata saknas"
|
||||
"connector label was skipped: text transform is invalid" = "etiketten på en kopplingslinje hoppades över: textens transformering är ogiltig"
|
||||
"connector labels were skipped" = "etiketter på kopplingslinjer hoppades över"
|
||||
"connector layers import as static frame/vector snapshots with labels when present; live endpoint bindings are not retained" = "kopplingslinjelager importeras som statiska ögonblicksbilder av ramar eller vektorer, med etiketter om de finns; dynamiska bindningar av ändpunkterna behålls inte"
|
||||
"connector layers import as static frame/vector snapshots with labels when present; live endpoint bindings are not retained" = "kopplingslinjelager importeras som statiska ögonblicksbilder av frames eller vektorer, med etiketter om de finns; dynamiska bindningar av ändpunkterna behålls inte"
|
||||
"connector snapshot vector data is missing; connector was left empty" = "vektordata för kopplingslinjens ögonblicksbild saknas; kopplingslinjen lämnades tom"
|
||||
"connectors whose ends can't be placed are empty" = "kopplingslinjer vars ändar inte kan placeras är tomma"
|
||||
"crop marks" = "skärmärken"
|
||||
@@ -1604,11 +1631,11 @@ name = "Svenska"
|
||||
"image filters are ambiguous because both filter containers are present; filter conversion was skipped" = "bildfiltren är tvetydiga eftersom båda filterbehållarna finns; filterkonverteringen hoppades över"
|
||||
"image filters use FigCraft's approximate renderer" = "bildfilter använder FigCrafts ungefärliga rendering"
|
||||
"instance swap properties whose default component isn't in the file were skipped" = "egenskaper för instansbyte vars standardkomponent inte finns i filen hoppades över"
|
||||
"instances inside their own main component were imported as frames" = "instanser inuti sin egen huvudkomponent importerades som ramar"
|
||||
"instances inside their own main component were imported as frames" = "instanser inuti sin egen huvudkomponent importerades som frames"
|
||||
"instances of components that aren't in the file show no layers" = "instanser av komponenter som inte finns i filen visar inga lager"
|
||||
"instances of components that aren't in the file were imported as frames" = "instanser av komponenter som inte finns i filen importerades som ramar"
|
||||
"instances that would nest a component inside itself were imported as frames" = "instanser som skulle nästla en komponent i sig själv importerades som ramar"
|
||||
"instances without a main component were imported as empty frames" = "instanser utan huvudkomponent importerades som tomma ramar"
|
||||
"instances of components that aren't in the file were imported as frames" = "instanser av komponenter som inte finns i filen importerades som frames"
|
||||
"instances that would nest a component inside itself were imported as frames" = "instanser som skulle nästla en komponent i sig själv importerades som frames"
|
||||
"instances without a main component were imported as empty frames" = "instanser utan huvudkomponent importerades som tomma frames"
|
||||
"invalid internal .fig text path data was ignored" = "ogiltiga interna .fig-data för text på bana ignorerades"
|
||||
"invalid or oversized vector networks import as outlines" = "ogiltiga eller för stora vektornätverk importeras som former"
|
||||
"invalid or oversized vector path data was skipped" = "ogiltiga eller för stora vektorbanedata hoppades över"
|
||||
@@ -1626,6 +1653,7 @@ name = "Svenska"
|
||||
"nobody else here yet" = "ingen annan här än"
|
||||
"non-default slot settings are not represented and were skipped" = "fackinställningar som avviker från standard återges inte och hoppades över"
|
||||
"now" = "nu"
|
||||
"only in a text style or variable" = "endast i en textstil eller variabel"
|
||||
"or" = "eller"
|
||||
"point stroke joins were preserved; dashed or variable-width strokes render with the layer join" = "punkternas hörntyper behölls; streckade konturer och konturer med variabel bredd ritas med lagrets hörntyp"
|
||||
"preferred slot component keys cannot be resolved to file nodes and were skipped" = "nycklar för föredragna komponenter i fack kan inte kopplas till lager i filen och hoppades över"
|
||||
@@ -1648,13 +1676,14 @@ name = "Svenska"
|
||||
"set-variable-mode actions on collections or modes that aren't in the variables JSON were skipped" = "åtgärder för att ange variabelläge på samlingar eller lägen som inte finns i variabel-JSON:en hoppades över"
|
||||
"slot content assignment target is missing or is not slot content" = "målet för tilldelningen av fackinnehåll saknas eller är inte fackinnehåll"
|
||||
"slot content assignment uses the empty GUID sentinel and was skipped" = "tilldelningen av fackinnehåll använder det tomma GUID:t som markörvärde och hoppades över"
|
||||
"slot layers without a matching slot property were imported as frames" = "facklager utan en motsvarande fackegenskap importerades som frames"
|
||||
"some vector points refer to point settings missing from the file; they use the layer's joins and plain corners" = "vissa vektorpunkter hänvisar till punktinställningar som saknas i filen; de använder lagrets hörntyper och vanliga hörn"
|
||||
"some vector segments refer to style settings missing from the file; they use the layer's stroke" = "vissa vektorsegment hänvisar till stilinställningar som saknas i filen; de använder lagrets kontur"
|
||||
"stale or invalid grid track sizing entries were ignored" = "inaktuella eller ogiltiga storleksposter för rutnätsspår ignorerades"
|
||||
"stars and polygons without point counts or geometry use the default point count" = "stjärnor och polygoner utan antal punkter eller geometri använder standardantalet punkter"
|
||||
"style settings of some vector segments aren't supported; those segments use the layer's stroke" = "stilinställningar för vissa vektorsegment stöds inte; de segmenten använder lagrets kontur"
|
||||
"text path underline or strikethrough decoration is not rendered" = "understrykning eller genomstrykning av text på bana ritas inte"
|
||||
"text style overrides refer to missing table entries" = "åsidosättningar av textstilar hänvisar till tabellposter som saknas"
|
||||
"text style overrides refer to missing table entries" = "overrides av textstilar hänvisar till tabellposter som saknas"
|
||||
"the file has no pages" = "filen har inga sidor"
|
||||
"the plugin" = "pluginet"
|
||||
"the variables JSON has no `variableCollections` (expected the local variables response) and was ignored" = "variabel-JSON:en har inga `variableCollections` (svaret med lokala variabler väntades) och ignorerades"
|
||||
@@ -1734,6 +1763,7 @@ name = "Svenska"
|
||||
"{n} replies" = { one = "{n} svar", other = "{n} svar" }
|
||||
"{n} rows" = { one = "{n} rad", other = "{n} rader" }
|
||||
"{n} styles" = { one = "{n} formatmall", other = "{n} formatmallar" }
|
||||
"{n} text layers" = { one = "{n} textlager", other = "{n} textlager" }
|
||||
"{n} variables" = { one = "{n} variabel", other = "{n} variabler" }
|
||||
"{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" }
|
||||
@@ -1742,6 +1772,7 @@ name = "Svenska"
|
||||
"{n}m" = "{n} min"
|
||||
"{n}w" = "{n} v"
|
||||
"{page}, nothing selected" = "{page}, inget markerat"
|
||||
"{percent}%" = "{percent} %"
|
||||
"{profile} (current)" = "{profile} (aktuell)"
|
||||
"{property} is its value; {property ? \"a\" : \"b\"} picks a string. Other {…} stays as written." = "{property} är dess värde; {property ? \"a\" : \"b\"} väljer en sträng. Andra {…} lämnas som de är skrivna."
|
||||
"{server} can't say what became of your files (it is older than this app)." = "{server} kan inte säga vad som hände med dina filer (den är äldre än den här appen)."
|
||||
@@ -1785,6 +1816,7 @@ name = "Svenska"
|
||||
"arrange.front" = "Flytta längst fram"
|
||||
"arrange.packH" = "Packa horisontellt"
|
||||
"arrange.packV" = "Packa vertikalt"
|
||||
"arrange.reorder" = "Ändra ordning på markeringen"
|
||||
"arrange.roundToPixels" = "Avrunda till pixlar"
|
||||
"arrange.spacing" = "Avstånd"
|
||||
"arrange.tidy" = "Snygga till"
|
||||
@@ -1868,7 +1900,7 @@ name = "Svenska"
|
||||
"component.pushChanges" = "Skicka ändringar till huvudkomponenten"
|
||||
"component.removeSlot" = "Ta bort plats"
|
||||
"component.resetInstance" = "Återställ alla ändringar"
|
||||
"component.resetOverride" = "Återställ åsidosättning"
|
||||
"component.resetOverride" = "Återställ override"
|
||||
"component.restoreMain" = "Återställ huvudkomponent"
|
||||
"dev.compare" = "Jämför ändringar"
|
||||
"dev.focus" = "Fokusvy"
|
||||
@@ -1920,7 +1952,7 @@ name = "Svenska"
|
||||
"file.colorProfile" = "Färgprofil"
|
||||
"file.duplicate" = "Duplicera fil"
|
||||
"file.export" = "Exportera"
|
||||
"file.exportFramesPdf" = "Exportera ramar som PDF"
|
||||
"file.exportFramesPdf" = "Exportera frames som PDF"
|
||||
"file.exportLayers" = "Exportera lager"
|
||||
"file.exportSvg" = "Exportera som SVG"
|
||||
"file.import" = "Importera"
|
||||
@@ -1943,8 +1975,8 @@ name = "Svenska"
|
||||
"fill.pattern" = "Mönster"
|
||||
"fill.remove" = "Ta bort fyllning"
|
||||
"fill.set" = "Ange fyllning"
|
||||
"frame.create" = "Skapa ram"
|
||||
"frame.presets" = "Ramförinställningar"
|
||||
"frame.create" = "Skapa frame"
|
||||
"frame.presets" = "Förinställningar för frames"
|
||||
"frame.swapOrientation" = "Växla orientering"
|
||||
"grid.add" = "Lägg till rutnät"
|
||||
"grid.move" = "Flytta rutnät"
|
||||
@@ -1963,7 +1995,7 @@ name = "Svenska"
|
||||
"image.place" = "Placera bild"
|
||||
"image.presets" = "Bildförinställningar"
|
||||
"image.savePreset" = "Spara förinställning"
|
||||
"inspect.assets" = "Tillgångar"
|
||||
"inspect.assets" = "Resurser"
|
||||
"inspect.code" = "Kod"
|
||||
"inspect.componentCode" = "Komponentkod"
|
||||
"inspect.properties" = "Egenskaper"
|
||||
@@ -1977,8 +2009,8 @@ name = "Svenska"
|
||||
"instance.swap" = "Byt instans"
|
||||
"instance.swapAt" = "Byt instans vid position"
|
||||
"layers.collapseAll" = "Fäll ihop alla"
|
||||
"layout.add" = "Lägg till automatisk layout"
|
||||
"layout.remove" = "Ta bort automatisk layout"
|
||||
"layout.add" = "Lägg till auto layout"
|
||||
"layout.remove" = "Ta bort auto layout"
|
||||
"layout.setTrack" = "Ange spår"
|
||||
"layout.suggest" = "Föreslå layout"
|
||||
"libraries.open" = "Öppna bibliotek"
|
||||
@@ -2002,15 +2034,16 @@ name = "Svenska"
|
||||
"library.updates" = "Biblioteksuppdateringar"
|
||||
"library.usage" = "Biblioteksanvändning"
|
||||
"measure.distances" = "Mät avstånd"
|
||||
"measure.padding" = "Mät utfyllnad"
|
||||
"measure.padding" = "Mät padding"
|
||||
"node.create" = "Skapa nod"
|
||||
"node.get" = "Hämta nod"
|
||||
"node.reorder" = "Ändra ordning"
|
||||
"node.reparent" = "Byt överordnad nod"
|
||||
"node.set" = "Ange nodegenskaper"
|
||||
"object.batchRename" = "Byt namn på flera"
|
||||
"object.convertToFrame" = "Konvertera till ram"
|
||||
"object.convertToFrame" = "Konvertera till frame"
|
||||
"object.convertToGroup" = "Konvertera till grupp"
|
||||
"object.convertToSection" = "Konvertera till avsnitt"
|
||||
"object.flipH" = "Vänd vågrätt"
|
||||
"object.flipV" = "Vänd lodrätt"
|
||||
"object.frameSelection" = "Rama in markering"
|
||||
@@ -2022,7 +2055,7 @@ name = "Svenska"
|
||||
"object.rasterize" = "Rastrera"
|
||||
"object.releaseMask" = "Frigör mask"
|
||||
"object.removeFill" = "Ta bort fyllning"
|
||||
"object.removeFrame" = "Ta bort ram"
|
||||
"object.removeFrame" = "Ta bort frame"
|
||||
"object.removeStroke" = "Ta bort kontur"
|
||||
"object.rename" = "Byt namn"
|
||||
"object.resizeToFit" = "Anpassa storlek"
|
||||
@@ -2051,23 +2084,24 @@ name = "Svenska"
|
||||
"plugin.directory.unpublish" = "Avpublicera plugin från katalogen"
|
||||
"plugin.event" = "Använd kontroll i plugin-panelen"
|
||||
"plugin.import" = "Importera plugin från manifest…"
|
||||
"plugin.list" = "Lista plugin-program"
|
||||
"plugin.list" = "Lista plugins"
|
||||
"plugin.remove" = "Ta bort plugin"
|
||||
"plugin.run" = "Kör plugin"
|
||||
"plugin.ui" = "Plugin-panel"
|
||||
"plugins.manage" = "Hantera plugin-program…"
|
||||
"plugins.manage" = "Hantera plugins…"
|
||||
"plugins.runLast" = "Kör senaste plugin"
|
||||
"pref.autoSave" = "Spara filer automatiskt"
|
||||
"pref.highlightHover" = "Framhäv lager vid hovring"
|
||||
"pref.keepTool" = "Behåll verktyget markerat efter användning"
|
||||
"pref.language" = "Gränssnittsspråk"
|
||||
"pref.nudge" = "Knuffavstånd"
|
||||
"pref.nudge" = "Steglängd"
|
||||
"pref.numberKeysOpacity" = "Använd siffertangenterna för opacitet"
|
||||
"pref.publishServer" = "Server för delning online"
|
||||
"pref.reduceMotion" = "Minska rörelse"
|
||||
"pref.renameDuplicates" = "Byt namn på duplicerade lager"
|
||||
"pref.showDimensions" = "Visa dimensioner på objekt"
|
||||
"pref.startOnHome" = "Börja på Start"
|
||||
"pref.uiScale" = "Gränssnittsstorlek"
|
||||
"pref.userName" = "Ditt namn i kommentarer"
|
||||
"proto.action.add" = "Lägg till prototypåtgärd"
|
||||
"proto.action.move" = "Ändra ordning på prototypåtgärd"
|
||||
@@ -2105,7 +2139,7 @@ name = "Svenska"
|
||||
"select.remove" = "Ta bort från markering"
|
||||
"select.sameEffect" = "Markera samma effekt"
|
||||
"select.sameFill" = "Markera samma fyllning"
|
||||
"select.sameFont" = "Markera samma teckensnitt"
|
||||
"select.sameFont" = "Markera samma typsnitt"
|
||||
"select.sameInstance" = "Markera samma instans"
|
||||
"select.sameProperties" = "Markera samma egenskaper"
|
||||
"select.sameStroke" = "Markera samma kontur"
|
||||
@@ -2168,7 +2202,7 @@ name = "Svenska"
|
||||
"text.edit.selectWord" = "Markera ord"
|
||||
"text.edit.selection" = "Textmarkering"
|
||||
"text.endPreview" = "Avsluta förhandsgranskning av text"
|
||||
"text.fonts" = "Lista teckensnitt"
|
||||
"text.fonts" = "Lista typsnitt"
|
||||
"text.italic" = "Kursiv"
|
||||
"text.lineHeightDown" = "Minska radhöjden"
|
||||
"text.lineHeightUp" = "Öka radhöjden"
|
||||
@@ -2176,17 +2210,21 @@ name = "Svenska"
|
||||
"text.listIndent" = "Öka listindrag"
|
||||
"text.listOutdent" = "Minska listindrag"
|
||||
"text.listStyle" = "Liststil"
|
||||
"text.loadFont" = "Läs in typsnittsfil"
|
||||
"text.maxLines" = "Max antal rader"
|
||||
"text.missingFonts" = "Saknade typsnitt"
|
||||
"text.numberList" = "Numrerad lista"
|
||||
"text.onPath" = "Text på bana"
|
||||
"text.paragraphIndent" = "Styckeindrag"
|
||||
"text.paragraphSpacing" = "Styckeavstånd"
|
||||
"text.pathOptions" = "Alternativ för textbana"
|
||||
"text.preview" = "Förhandsgranska textändring"
|
||||
"text.refit" = "Anpassa text till typsnitt"
|
||||
"text.removeFromPath" = "Ta bort text från bana"
|
||||
"text.rescanFonts" = "Sök igenom installerade typsnitt igen"
|
||||
"text.set" = "Ange text"
|
||||
"text.setFeatures" = "OpenType-funktioner"
|
||||
"text.setVariations" = "Axlar för variabla teckensnitt"
|
||||
"text.setVariations" = "Axlar för variabla typsnitt"
|
||||
"text.sizeDown" = "Minska teckenstorleken"
|
||||
"text.sizeUp" = "Öka teckenstorleken"
|
||||
"text.strikethrough" = "Genomstruken"
|
||||
@@ -2201,7 +2239,7 @@ name = "Svenska"
|
||||
"tool.connector" = "Koppling"
|
||||
"tool.ellipse" = "Ellips"
|
||||
"tool.eraser" = "Suddgummi"
|
||||
"tool.frame" = "Ram"
|
||||
"tool.frame" = "Frame"
|
||||
"tool.hand" = "Hand"
|
||||
"tool.highlighter" = "Överstrykningspenna"
|
||||
"tool.line" = "Linje"
|
||||
@@ -2226,8 +2264,8 @@ name = "Svenska"
|
||||
"tool.text" = "Text"
|
||||
"transform.move" = "Flytta"
|
||||
"transform.moveTo" = "Flytta till"
|
||||
"transform.nudge" = "Knuffa"
|
||||
"transform.nudgeSize" = "Ändra storlek genom att knuffa"
|
||||
"transform.nudge" = "Flytta stegvis"
|
||||
"transform.nudgeSize" = "Ändra storlek stegvis"
|
||||
"transform.resize" = "Ändra storlek"
|
||||
"transform.rotate" = "Rotera"
|
||||
"transform.rotationOrigin" = "Flytta rotationspunkt"
|
||||
@@ -2252,7 +2290,7 @@ name = "Svenska"
|
||||
"variable.setMode" = "Ange variabelläge"
|
||||
"variable.update" = "Redigera variabel"
|
||||
"vector.create" = "Vektor"
|
||||
"vector.deleteHeal" = "Radera och läk markering"
|
||||
"vector.deleteHeal" = "Radera och retuschera markering"
|
||||
"vector.edit.addVertex" = "Lägg till punkt"
|
||||
"vector.edit.begin" = "Redigera vektor"
|
||||
"vector.edit.bend" = "Böj segment"
|
||||
@@ -2267,7 +2305,7 @@ name = "Svenska"
|
||||
"vector.edit.mirroring" = "Handtagsspegling"
|
||||
"vector.edit.moveHandles" = "Flytta handtag"
|
||||
"vector.edit.moveVertices" = "Flytta punkter"
|
||||
"vector.edit.nudge" = "Knuffa punkter"
|
||||
"vector.edit.nudge" = "Flytta punkter stegvis"
|
||||
"vector.edit.paint" = "Färghink"
|
||||
"vector.edit.pen" = "Pennpunkt"
|
||||
"vector.edit.penEnd" = "Avsluta pennbana"
|
||||
@@ -2301,13 +2339,13 @@ name = "Svenska"
|
||||
"version.preview" = "Visa version"
|
||||
"version.rename" = "Byt namn på version"
|
||||
"version.restore" = "Återställ version"
|
||||
"view.assets" = "Panelen Tillgångar"
|
||||
"view.assets" = "Panelen Resurser"
|
||||
"view.comments" = "Kommentarer"
|
||||
"view.contextMenu" = "Visa snabbmeny"
|
||||
"view.design" = "Panelen Design"
|
||||
"view.designPrototype" = "Växla mellan Design och Prototyp"
|
||||
"view.devMode" = "Utvecklarläge"
|
||||
"view.frameOutlines" = "Konturlinjer för ramar"
|
||||
"view.frameOutlines" = "Konturlinjer för frames"
|
||||
"view.fullscreen" = "Helskärm"
|
||||
"view.get" = "Hämta vy"
|
||||
"view.layers" = "Panelen Lager"
|
||||
@@ -2315,7 +2353,7 @@ name = "Svenska"
|
||||
"view.leftSidebar" = "Visa/dölj vänster sidofält"
|
||||
"view.maskOutlines" = "Konturlinjer för masker"
|
||||
"view.multiplayerCursors" = "Samarbetsmarkörer"
|
||||
"view.nextFrame" = "Zooma till nästa ram"
|
||||
"view.nextFrame" = "Zooma till nästa frame"
|
||||
"view.nextPage" = "Nästa sida"
|
||||
"view.nextRegion" = "Fokusera nästa region"
|
||||
"view.outlines" = "Konturlinjer"
|
||||
@@ -2326,7 +2364,7 @@ name = "Svenska"
|
||||
"view.panelWidths" = "Återställ panelbredder"
|
||||
"view.pixelGrid" = "Pixelrutnät"
|
||||
"view.pixelPreview" = "Pixelförhandsgranskning"
|
||||
"view.prevFrame" = "Zooma till föregående ram"
|
||||
"view.prevFrame" = "Zooma till föregående frame"
|
||||
"view.prevPage" = "Föregående sida"
|
||||
"view.previousRegion" = "Fokusera föregående region"
|
||||
"view.proofSrgb" = "Skärmkorrektur i sRGB"
|
||||
@@ -2368,7 +2406,7 @@ name = "Svenska"
|
||||
"libraries.open" = "Öppna bibliotek"
|
||||
"select.sameEffect" = "Effekt"
|
||||
"select.sameFill" = "Fyllning"
|
||||
"select.sameFont" = "Teckensnitt"
|
||||
"select.sameFont" = "Typsnitt"
|
||||
"select.sameInstance" = "Instans"
|
||||
"select.sameProperties" = "Egenskaper"
|
||||
"select.sameStroke" = "Kontur"
|
||||
@@ -2377,7 +2415,7 @@ name = "Svenska"
|
||||
"text.alignJustify" = "Marginaljustera text"
|
||||
"text.alignLeft" = "Vänsterjustera text"
|
||||
"text.alignRight" = "Högerjustera text"
|
||||
"view.assets" = "Tillgångar"
|
||||
"view.assets" = "Resurser"
|
||||
"view.design" = "Design"
|
||||
"view.layers" = "Lager"
|
||||
"view.outlines" = "Visa konturlinjer"
|
||||
|
||||
@@ -288,6 +288,8 @@ pub trait Services {
|
||||
Box::pin(std::future::ready(None))
|
||||
}
|
||||
fn clipboard_set_text(&mut self, _s: &str) {}
|
||||
/// Give native services access to the window context for platform clipboard output.
|
||||
fn set_egui_context(&mut self, _ctx: &egui::Context) {}
|
||||
fn clipboard_get_text(&mut self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
@@ -408,9 +410,8 @@ impl Wake for RepaintWaker {
|
||||
pub struct NoServices;
|
||||
impl Services for NoServices {}
|
||||
|
||||
/// Put `text` on the system clipboard through egui's output, which the browser shell writes while
|
||||
/// it handles the copy event (the only time a page may write it). For shells without a clipboard
|
||||
/// of their own.
|
||||
/// Put `text` on the system clipboard through egui's platform output. Native integrations handle
|
||||
/// it directly; the browser shell writes it while handling the copy event.
|
||||
pub fn egui_copy_text(ctx: &egui::Context, text: &str) {
|
||||
ctx.copy_text(text.to_string());
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@ pub struct LiveUi {
|
||||
pub advanced: bool,
|
||||
pub this_computer: bool,
|
||||
pub port: String,
|
||||
/// The server key for online sharing (a server that needs one; empty: none). Passed to
|
||||
/// `collab.share` so a keyed server can be used from the Share popover without the Publish
|
||||
/// dialog.
|
||||
pub share_key: String,
|
||||
/// What is typed in the join field of the Share popover and the home screen.
|
||||
pub code: String,
|
||||
/// The join started from the home screen: what it is doing shows under the field, and its
|
||||
@@ -746,6 +750,9 @@ fn start_ui(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
if let (true, Ok(port)) = (local, app.ui.live.port.trim().parse::<u16>()) {
|
||||
params["port"] = json!(port);
|
||||
}
|
||||
if !local && !app.ui.live.share_key.trim().is_empty() {
|
||||
params["key"] = json!(app.ui.live.share_key.trim());
|
||||
}
|
||||
app.run("collab.share", params);
|
||||
}
|
||||
join_and_advanced(app, ui, t, |app, ui, t| {
|
||||
@@ -762,6 +769,15 @@ fn start_ui(app: &mut FigApp, ui: &mut Ui, t: &Tokens) {
|
||||
});
|
||||
} else {
|
||||
widgets::label(ui, tr("Another server, or one that needs its key. The usual server is set in Preferences."), t.text_secondary);
|
||||
// The server key (a server that needs one; the address in Preferences is without it).
|
||||
ui.horizontal(|ui| {
|
||||
widgets::label(ui, tr("Server key"), t.text);
|
||||
if let Some(key) =
|
||||
widgets::secret_field_named(ui, egui::Id::unique("live_share_key"), &app.ui.live.share_key, 200.0, tr("Server key"))
|
||||
{
|
||||
app.ui.live.share_key = key.trim().chars().filter(|c| !c.is_whitespace() && !c.is_control()).take(128).collect();
|
||||
}
|
||||
});
|
||||
if widgets::button(ui, tr("Publish to another server…"), false, true).clicked() {
|
||||
open_publish(app);
|
||||
ui.close();
|
||||
|
||||
@@ -232,6 +232,20 @@ fn not_sharing_the_popover_offers_one_way_to_start_and_one_to_join() {
|
||||
assert_eq!(h.app.session.live().status, Status::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_server_key_shows_as_dots_while_shown_and_typed() {
|
||||
let (mut h, _) = harness();
|
||||
let shapes = open(&mut h);
|
||||
click(&mut h, &shapes, "Advanced");
|
||||
h.app.ui.live.share_key = "s3cret-key".into();
|
||||
let shapes = h.frames(2);
|
||||
let dots = "•".repeat(10);
|
||||
assert!(has_text(&shapes, &dots) && !has_text(&shapes, "s3cret-key"));
|
||||
// Editing it keeps it hidden.
|
||||
let shapes = click(&mut h, &shapes, &dots);
|
||||
assert!(!has_text(&shapes, "s3cret-key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sharing_online_shows_the_code_copies_it_and_stops() {
|
||||
let (mut h, script) = harness();
|
||||
|
||||
@@ -59,6 +59,30 @@ fn enter_edits_a_selected_text_with_all_of_it_selected_and_escape_keeps_the_laye
|
||||
assert!(u.app.session.selection().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copying_and_cutting_selected_text_use_clipboard_service() {
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
struct Clipboard(Rc<RefCell<Vec<String>>>);
|
||||
impl crate::Services for Clipboard {
|
||||
fn clipboard_set_text(&mut self, text: &str) {
|
||||
self.0.borrow_mut().push(text.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let mut s = Session::new();
|
||||
let t = NodeId(s.execute("text.create", &json!({"text": "Hello world", "x": 20, "y": 20})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("select.set", &json!({"ids": [t]})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
let copied = Rc::new(RefCell::new(Vec::new()));
|
||||
u.app.services = Box::new(Clipboard(copied.clone()));
|
||||
press(&mut u, Key::Enter);
|
||||
u.frame_output(vec![Event::Copy]);
|
||||
u.frame_output(vec![Event::Cut]);
|
||||
assert_eq!(&*copied.borrow(), &["Hello world", "Hello world"]);
|
||||
assert_eq!(u.app.session.doc().unwrap().node(t).unwrap().kind.text().unwrap().characters, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_file_takes_the_page_colour_of_the_theme_and_keeps_it() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -486,13 +486,20 @@ pub fn bound_field(ui: &mut Ui, id: Id, lead: Lead, rect: Rect, name: &str) -> O
|
||||
/// A text field that commits on Enter / focus loss. Returns the new text when committed.
|
||||
pub fn text_field(ui: &mut Ui, id: Id, value: &str, width: f32, placeholder: &str) -> Option<String> {
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover());
|
||||
text_field_in(ui, id, value, rect, placeholder, placeholder)
|
||||
text_field_in(ui, id, value, rect, placeholder, placeholder, false)
|
||||
}
|
||||
|
||||
/// [`text_field`] without a placeholder, named `name` for screen readers (its label is beside it).
|
||||
pub fn text_field_named(ui: &mut Ui, id: Id, value: &str, width: f32, name: &str) -> Option<String> {
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover());
|
||||
text_field_in(ui, id, value, rect, "", name)
|
||||
text_field_in(ui, id, value, rect, "", name, false)
|
||||
}
|
||||
|
||||
/// [`text_field_named`] for a secret (a server key): it shows dots, typing included, and screen
|
||||
/// readers hear dots too.
|
||||
pub fn secret_field_named(ui: &mut Ui, id: Id, value: &str, width: f32, name: &str) -> Option<String> {
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover());
|
||||
text_field_in(ui, id, value, rect, "", name, true)
|
||||
}
|
||||
|
||||
/// A multi-line text field across the panel (descriptions, notes) that commits when it loses
|
||||
@@ -536,7 +543,7 @@ pub fn multiline_field(ui: &mut Ui, id: Id, value: &str, hint: &str, rows: usize
|
||||
pub fn combo_field(ui: &mut Ui, id: Id, value: &str, width: f32, options: &[String]) -> Option<String> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover());
|
||||
let mut out = text_field_in(ui, id, value, rect, "", "");
|
||||
let mut out = text_field_in(ui, id, value, rect, "", "", false);
|
||||
// The chevron sits on top of the field's right end.
|
||||
let chev = Rect::from_min_max(pos2(rect.max.x - 18.0, rect.min.y), rect.max);
|
||||
let resp = ui.interact(chev, id.with("menu"), Sense::click());
|
||||
@@ -557,13 +564,14 @@ pub fn combo_field(ui: &mut Ui, id: Id, value: &str, width: f32, options: &[Stri
|
||||
}
|
||||
|
||||
/// [`text_field`] drawn into `rect`; `name` names it for screen readers.
|
||||
fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str, name: &str) -> Option<String> {
|
||||
fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str, name: &str, secret: bool) -> Option<String> {
|
||||
cancel_retargeted_field_save(ui, id);
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let editing_id = id.with("editing");
|
||||
let mut editing: Option<String> = ui.data(|d| d.get_temp(editing_id));
|
||||
let resp = ui.interact(rect, id.with("click"), Sense::click());
|
||||
a11y::text(&resp, name, value);
|
||||
let shown = if secret { std::borrow::Cow::Owned("•".repeat(value.chars().count())) } else { std::borrow::Cow::Borrowed(value) };
|
||||
a11y::text(&resp, name, &shown);
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), t.field);
|
||||
if editing.is_some() {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, crate::theme::ACCENT), StrokeKind::Inside);
|
||||
@@ -579,7 +587,7 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str
|
||||
let mut out = None;
|
||||
match editing {
|
||||
Some(mut text) => {
|
||||
let r = field_editor(ui, rect, id.with("te"), &mut text, "");
|
||||
let r = field_editor(ui, rect, id.with("te"), &mut text, "", secret);
|
||||
a11y::own_focus(&r);
|
||||
keep_focus(&r, started);
|
||||
crate::shortcuts::release_field_for_save(&r);
|
||||
@@ -595,7 +603,7 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let (s, col) = if value.is_empty() { (placeholder, t.text_tertiary) } else { (value, t.text) };
|
||||
let (s, col) = if value.is_empty() { (placeholder, t.text_tertiary) } else { (&*shown, t.text) };
|
||||
// Long text stays inside the field.
|
||||
let painter = ui.painter().with_clip_rect(rect.shrink2(vec2(8.0, 0.0)));
|
||||
painter.text(pos2(rect.min.x + 8.0, rect.center().y), Align2::LEFT_CENTER, s, regular(11.0), col);
|
||||
@@ -605,14 +613,15 @@ fn text_field_in(ui: &mut Ui, id: Id, value: &str, rect: Rect, placeholder: &str
|
||||
}
|
||||
|
||||
/// The editor inside a field's `rect`: one line of text in the field's font, without a frame of
|
||||
/// its own (the field draws it), showing `hint` while empty.
|
||||
fn field_editor(ui: &mut Ui, rect: Rect, id: Id, text: &mut String, hint: &str) -> Response {
|
||||
/// its own (the field draws it), showing `hint` while empty; `password` shows dots.
|
||||
fn field_editor(ui: &mut Ui, rect: Rect, id: Id, text: &mut String, hint: &str, password: bool) -> Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect.shrink2(vec2(8.0, 4.0))));
|
||||
let r = child.add(
|
||||
egui::TextEdit::singleline(text)
|
||||
.id(id)
|
||||
.frame(egui::Frame::NONE)
|
||||
.password(password)
|
||||
.font(regular(11.0))
|
||||
.hint_text(egui::RichText::new(hint).font(regular(11.0)).color(t.text_tertiary))
|
||||
.desired_width(rect.width() - 16.0),
|
||||
@@ -628,7 +637,7 @@ pub fn text_input(ui: &mut Ui, id: Id, text: &mut String, width: f32, hint: &str
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(width, FIELD_H), Sense::hover());
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), t.field);
|
||||
let r = field_editor(ui, rect, id, text, hint);
|
||||
let r = field_editor(ui, rect, id, text, hint, false);
|
||||
if r.has_focus() {
|
||||
ui.painter().rect_stroke(rect, CornerRadius::same(5), Stroke::new(1.0, crate::theme::ACCENT), StrokeKind::Inside);
|
||||
} else if ui.rect_contains_pointer(rect) {
|
||||
|
||||
@@ -25,6 +25,10 @@ On Linux and other Unix systems except macOS, user fonts are read from
|
||||
`$XDG_DATA_HOME/fonts`, or `$HOME/.local/share/fonts` when `XDG_DATA_HOME` is unset,
|
||||
empty or relative. The legacy `$HOME/.fonts` directory is also scanned.
|
||||
|
||||
On macOS, fonts activated through Adobe Fonts are read where Creative Cloud keeps them
|
||||
(`~/Library/Application Support/Adobe/CoreSync/plugins/livetype`), so they're available while
|
||||
they're active there. Files without an extension are read when they start like a font file.
|
||||
|
||||
The collaboration server is a binary of its own, `figcraft-server` (`cargo build --release -p
|
||||
figcraft-server`): it keeps files with share links and serves their live sessions, and its
|
||||
`key`, `files`, `links`, `reports`, `block-ip` and `check` subcommands manage them (a server can
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
# FigCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (`.sketch` import, best effort; 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 (`.sketch` import, best effort; Adobe Fonts found on macOS (#298); 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)
|
||||
|
||||
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
|
||||
@@ -128,6 +128,7 @@ blockers.
|
||||
| 2026-10-15 | minor | Languages (#257, #263, #469, #477, #288): Russian (with one/few/many plural forms) and Turkish catalogs, reviewed by native speakers; Czech and Indonesian drafts; the Japanese draft completed. 2,177 entries now (1,587 UI strings, 559 command labels, 31 menu wordings); strings added since a catalog was written read English there. Localization 39% and 29–51 h unchanged at this rounding (Indonesian is now a draft, while the five older drafts lack 88 newer entries) |
|
||||
| 2026-10-17 | minor | Community batch #753 (10 PRs): Selection colors for mixed selections (#403); smart-selection spacing fields and canvas handles (#402); a Present tab with device and scale controls (#537); adding auto layout to an empty frame keeps its size (#369); `node.set` applies auto layout keys to frames only and warns about font styles a family lacks (#603); canvas images on whole device pixels (#359); Adwaita window controls on Wayland without server decorations (#592, not run on GNOME or KDE by the maintainers); the keyboard shortcuts panel closes on the home screen (#548); macOS paste modifiers in tests (#750); credits column tests (#347). Numbers unchanged. |
|
||||
| 2026-10-17 | minor | Community fixes from #524 up (batches #727, #728, #733, #735, #736 and #729; 55 PRs) and a crash guard. `.fig` import: explicit instance identity, overrides by key, text style precedence, grid track sizes, slot content, connector labels, skip ink, paragraph options and hanging lists, list start numbers, vertical trim, per-point stroke joins, text on a path, image adjustments and embedded images; large files open within budgets from the file's own size; an import report after opening; imported bytes stay out of the command journal. Files and paste: save a copy as `.fig`; Cmd/Ctrl+V pastes images copied in other apps and layers copied from another design app (`edit.pasteDesignLayers`); paste over and paste properties; clipboard reads on Linux and the BSDs time out after 2 s. Editor: live font preview on hover; Shift+I and Resources open Assets; Shift-drag moves the selection; thin strokes take a nearby double-click; dropped points merge; Flatten keeps open paths; the Actions palette gets tabs, ranked search, a keyboard highlight and recents; the colour picker opens on a new fill or stroke; text strokes follow their alignment; selection highlights after line breaks; a draggable pages/layers divider; variant properties reorder by drag; submenus keep their hover with a safe triangle; menu actions that need a value ask for it; File ▸ Rename edits the name in place. Engine: bad paint indexes, non-finite geometry and pages in copy/cut are refused; a param naming a layer says so when it isn't one; a CLI script is one undo step and resolves `$step`. Server and collaboration: message limits follow `max_file_mb` (client capped at 1 GiB); a damaged generation field no longer drops flushed edits; the web build joins a session from a `#join=` link. Also Swedish command labels, Nix CI through the shared workflow, paint bounds once per render, and a guard that turns a panic inside the boolean-op library into bad input (the stroke draws unclipped). Numbers unchanged. |
|
||||
| 2026-10-16 | minor | Adobe Fonts found on macOS (M7, #298): fonts activated through Adobe Fonts rendered in the fallback font without a warning, because Creative Cloud keeps them in its own folder as hidden files named by number, which the font scan never read. The macOS scan now includes `~/Library/Application Support/Adobe/CoreSync/plugins/livetype`, and files without an extension are indexed when they start with a font signature. Fonts are read in place. Regression test in the font database tests; docs/cli.md lists the folder. Numbers unchanged |
|
||||
| 2026-10-16 | minor | Community fixes from #478 up (batches #591, #598, #602; 69 PRs) and a control-channel fix: a line that isn't JSON now ends a `--control` connection, so a web page can't drive the app through a browser request (#589). Highlights: page copies keep navigation, flows, prototype settings and guides; Find and Replace keeps line endings and case-insensitive matches; text hyperlinks survive import and export; recent colours record only what was picked; MCP screenshots respect `maxSize`; Swedish catalog (UI strings), Czech plurals; bounded canvas overscan while panning; Korean fallback at the run's weight; many vector-edit, guide, page and .fig round-trip fixes. Numbers unchanged |
|
||||
| 2026-10-14 | minor | M7: the Small caps button in Type settings showed a box where the UI font has no small-capital `ɢ` (builds without Inter use egui's fonts); its sample is now a capital and a smaller capital, drawn from ordinary letters every UI font has. Numbers unchanged |
|
||||
| 2026-10-14 | minor | M7: Home screen recent files as cards: each card shows the picture its file carries on its first page's colour, the file's icon, name and "Edited ... ago"; sort by name, creation or last change either way; a table view; a click selects, a double-click or Enter opens, arrow keys move; Open, Show in folder and Remove from recents on right-click. Numbers unchanged. |
|
||||
|
||||
Reference in New Issue
Block a user