diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9328ea9..c1cb42c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,18 @@ env: CARGO_TERM_COLOR: always jobs: + product-site: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + - name: Check product assets, cache versions and download selection + run: | + python3 scripts/sync-site-assets.py --check + python3 scripts/refresh-site-versions.py --check + node --check site/site.js + node --test scripts/tests/downloads.test.mjs + python3 -m unittest discover -s scripts/tests -p 'test_*.py' + native: runs-on: ubuntu-24.04 steps: diff --git a/.github/workflows/desktop-preview.yml b/.github/workflows/desktop-preview.yml new file mode 100644 index 0000000..1110449 --- /dev/null +++ b/.github/workflows/desktop-preview.yml @@ -0,0 +1,146 @@ +name: Desktop packages + +on: + workflow_dispatch: + push: + branches: ['candidate/**'] + tags: ['v*'] + pull_request: + paths: ['packaging/**', 'scripts/package-desktop.py', 'scripts/tests/**', 'scripts/release-manifest.py', '.github/workflows/desktop-preview.yml', '.cargo/**', 'crates/**', 'src/**', 'assets/**', 'build.rs', 'LICENSE-*', 'THIRD_PARTY.md', 'Cargo.toml', 'Cargo.lock', 'rust-toolchain*'] + +permissions: + contents: read + +concurrency: + group: desktop-packages-${{ github.ref }} + cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} + +jobs: + package: + strategy: + fail-fast: false + matrix: + include: + - os: macos-15-intel + platform: macos + arch: universal + target: x86_64-apple-darwin + - os: windows-2025 + platform: windows + arch: x64 + target: x86_64-pc-windows-msvc + - os: ubuntu-24.04 + platform: linux + arch: x64 + target: x86_64-unknown-linux-gnu + runs-on: ${{ matrix.os }} + env: + RUSTUP_TOOLCHAIN: 1.95.0 + CARGO_TERM_COLOR: always + MACOSX_DEPLOYMENT_TARGET: '11.0' + CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS: '-C target-feature=+crt-static' + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Test package publication and release manifests on this OS + run: python -m unittest discover -s scripts/tests -p 'test_*.py' + - name: Toolchain + shell: bash + run: | + rustup toolchain install 1.95.0 --profile minimal + rustup target add ${{ matrix.target }} + - name: Reuse Rust dependencies and release build cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + target/release + target/${{ matrix.target }}/release + target/aarch64-apple-darwin/release + key: desktop-rust-1.95-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml') }}-${{ github.sha }} + restore-keys: | + desktop-rust-1.95-${{ runner.os }}-${{ matrix.target }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml') }}- + - name: Linux dependencies + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libxkbcommon-dev libxkbcommon-x11-0 libwayland-dev libxcb1-dev libgtk-3-dev mesa-vulkan-drivers + - name: Build and check native contracts + shell: bash + run: | + cargo build --release --locked --target ${{ matrix.target }} --bin openpaperdesign --bin paper-rs-mcp + cargo test --release --locked --target ${{ matrix.target }} --lib --bin openpaperdesign --test projects -- --test-threads=1 + - name: Restore screenshots generated by project tests + shell: bash + run: git restore --worktree -- docs/screenshots + - name: Build Apple Silicon slice and assemble universal executables + if: runner.os == 'macOS' + shell: bash + run: | + rustup target add aarch64-apple-darwin + cargo build --release --locked --target aarch64-apple-darwin --bin openpaperdesign --bin paper-rs-mcp + mkdir -p target/distribution + for binary in openpaperdesign paper-rs-mcp; do + lipo -create target/x86_64-apple-darwin/release/$binary target/aarch64-apple-darwin/release/$binary -output target/distribution/$binary + lipo target/distribution/$binary -verify_arch x86_64 arm64 + done + - name: Stage native executables + if: runner.os != 'macOS' + shell: bash + run: | + mkdir -p target/distribution + suffix='' + if [ '${{ runner.os }}' = Windows ]; then suffix=.exe; fi + cp target/${{ matrix.target }}/release/openpaperdesign$suffix target/distribution/ + cp target/${{ matrix.target }}/release/paper-rs-mcp$suffix target/distribution/ + - name: Verify actual packaged CLI executables + shell: bash + run: | + suffix='' + if [ '${{ runner.os }}' = Windows ]; then suffix=.exe; fi + ./target/distribution/openpaperdesign$suffix --help + ./target/distribution/paper-rs-mcp$suffix --url + - name: Package exact clean source and checksums + run: python scripts/package-desktop.py --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --bin-dir target/distribution --output dist --require-clean + - name: Keep exact packages for review + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: OpenPaperDesign-${{ matrix.platform }}-${{ matrix.arch }} + path: dist/* + if-no-files-found: error + retention-days: 14 + + draft-release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + needs: package + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + fetch-depth: 0 + persist-credentials: false + - name: Require a reviewed main-branch release source + shell: bash + run: git merge-base --is-ancestor "$GITHUB_SHA" origin/main + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + pattern: OpenPaperDesign-* + merge-multiple: true + path: dist + - name: Verify packages and generate manifest + run: python scripts/release-manifest.py dist --repository arcitai/openpaperdesign --tag "$GITHUB_REF_NAME" --revision "$GITHUB_SHA" + - name: Create a draft for review, never publish automatically + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + if gh release view "$GITHUB_REF_NAME" --repo arcitai/openpaperdesign --json isDraft >/dev/null 2>&1; then + echo 'Release already exists; leave its reviewed assets unchanged.' >&2 + exit 1 + fi + gh release create "$GITHUB_REF_NAME" dist/* --repo arcitai/openpaperdesign --target "$GITHUB_SHA" --draft --prerelease --title "OpenPaperDesign $GITHUB_REF_NAME" --notes 'Desktop review packages. macOS is ad-hoc signed and not notarized. See packaged BUILD.json, licences and checksums. Publication is a separate deliberate action.' + test "$(gh release view "$GITHUB_REF_NAME" --repo arcitai/openpaperdesign --json isDraft --jq .isDraft)" = true diff --git a/.gitignore b/.gitignore index 6411ea6..af1e550 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ target/ *.asar app.asar.unpacked/ .toolchain/ +dist/ +site/downloads/ +site/releases.preview.json +__pycache__/ diff --git a/Cargo.toml b/Cargo.toml index 76f6b51..c92a3ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,10 @@ path = "src/main.rs" name = "paper-rs-mcp" path = "src/bin/paper-rs-mcp.rs" +[[bin]] +name = "openpaperdesign" +path = "packaging/native.rs" + [dependencies] egui = "0.36" egui-wgpu = "0.36" diff --git a/README.md b/README.md index 111dae2..b7d422c 100644 --- a/README.md +++ b/README.md @@ -1,378 +1,67 @@ -# openpaperdesign + + + + -An independent fork of [michaelshimeles/paper-rs](https://github.com/michaelshimeles/paper-rs), -not affiliated with or endorsed by Paper (paper.design). The first slice is a local Codex -plugin with a loopback editable canvas, deliberately bound to one design project inside the -workspace. See [standalone plugin setup](docs/openpaperdesign/plugin-b2b.md#install-on-a-mac-operator-steps) -for local-build setup and limits. Installed model editing and browser save/conflict/reopen -passed on macOS with Codex 0.162.1 at `0078487` (2026-10-13). Distribution is source-only / -local build, with no packaged installer or public marketplace listing. See -[repository Releases](https://github.com/arcitai/openpaperdesign/releases) for delivery status. -There is no protection against concurrent native or external writers, and no multiplayer -or cloud service. The upstream README follows unchanged. +# OpenPaperDesign -## Why I built this - -I wanted an infinite, editable canvas inside my AI agent’s workspace, where we could work through architecture diagrams, systems and interface designs together without leaving the conversation. - -OpenPaperDesign is built on paper-rs by [Michael Shimeles](https://github.com/michaelshimeles). I reuse its editor, follow open standards where they exist and add only the integration the agent workspace needs. That keeps the extra code and maintenance small. - -In Codex, I can sketch a system and refine a layout with the agent. Project files stay local and the canvas stays editable. I’m also exploring how AI-generated images can become part of that workflow. - ---- - -# paper-rs - -A native Rust infinite canvas for planning, working and sketching. It is modeled on the -behavior of [Paper](https://paper.design) and built clean-room on `wgpu` + `winit` + `egui`. - -![paper-rs main window](docs/screenshots/app_demo.png) - -A local-first design and whiteboard tool. Projects are folders of pages; each page is an -infinite canvas with frames, groups, components and instances, auto layout and constraints, -vector paths and booleans, gradients and image fills, rich text, smart guides, and -pressure-sensitive ink — plus a whiteboard mode: hand-drawn (rough.js-style) shapes, arrows that -bind to shapes and re-route, elbow connectors, sticky notes, a laser pointer and a shape -library. Everything autosaves, and exports to PNG, SVG and PDF; Excalidraw files and SVG import. - -![Figma-core features](docs/screenshots/milestone3_figma_core.png) - -> **Clean-room note.** `analysis/PAPER_ARCHITECTURE.md` documents how Paper Desktop 0.5.15 is -> put together, from a licensed install. The Electron package turned out to be a thin shell -> around the hosted web editor. paper-rs re-implements *behavior* only. No Paper code, -> shaders, icons, fonts or assets are used or committed. The UI and canvas text use -> [Inter](https://rsms.me/inter/) 4.1 (SIL OFL 1.1), bundled in `assets/fonts/`. - -## Reverse-engineered origin - -paper-rs is an independent, clean-room-style reimplementation. It was reverse engineered from -[Paper](https://paper.design) by studying the behavior of the Paper desktop app with -[REA](https://github.com/morluto/rea) and Claude Code. It is not affiliated with or endorsed by -Paper. No original Paper code or assets are included in this repository. - -## Projects - -paper-rs opens on a **home screen** of your projects (recents grid with thumbnails, New, Import, -and Rename / Duplicate / Move to Trash from each card's ⋯ menu or right-click; restore from -Trash). Each project is a plain folder in `~/Documents/paper-rs//` holding `project.json` -and one JSON file per page in `pages/`. Work is autosaved about a second after each change with -crash-safe atomic writes; files are versioned and older versions migrate on load. Set -`PAPER_RS_HOME` to use another folder. - -A project has any number of **pages** (Pages list at the top of the left sidebar: + to add, -double-click to rename, right-click to duplicate or delete, drag to reorder). Each page is its -own infinite canvas with its own undo history and view. Copy and paste works between pages and -between projects. - -## Build and run - -You need Rust 1.95 or newer (`rust-version` in `Cargo.toml`; the latest stable is recommended). Install it with [rustup](https://rustup.rs). - -```sh -cargo build --release -./target/release/paper-rs # projects home screen -./target/release/paper-rs --demo # sample planning board (not saved) -./target/release/paper-rs ~/Documents/paper-rs/Plan # open a project folder -./target/release/paper-rs board.paper-rs # open a paper-rs 0.1 document -``` - -### macOS - -```sh -xcode-select --install # command line tools (linker) -curl https://sh.rustup.rs -sSf | sh -cargo build --release && ./target/release/paper-rs --demo -``` - -It uses Metal through wgpu. Shortcuts use ⌘, and trackpad pinch zooms. The macOS target is -type-checked in CI-style (`cargo check --target aarch64-apple-darwin`) but has not been run on -Mac hardware yet (see Known gaps). - -### Linux - -```sh -# Debian/Ubuntu runtime libraries (usually already present on desktops) -sudo apt install libxkbcommon-x11-0 libxcb-xkb1 libvulkan1 mesa-vulkan-drivers -cargo build --release && ./target/release/paper-rs --demo -``` - -It runs on X11 and Wayland through Vulkan, or GL as a fallback. Shortcuts use Ctrl. File -dialogs use the XDG desktop portal. +A free, open-source canvas for ideas, diagrams, interfaces and visual work. Work on your own, or connect an AI agent over MCP. -### Headless (servers, CI) +![New UI/UX development snapshot rendered by paper-rs --screenshot](site/assets/product-architecture.png) -No display is needed. wgpu renders offscreen, through Mesa lavapipe when there is no GPU: +Built on **paper-rs by [Michael Shimeles (Rasmic)](https://github.com/michaelshimeles)**, now developed as [Sketch](https://github.com/michaelshimeles/sketch). The core editor is his work. OpenPaperDesign adds agent-host integration, design methods and desktop packaging. This is an independent fork, not affiliated with Paper (paper.design). -```sh -paper-rs --screenshot ui.png --demo --size 1440x900 --scale 2 # full UI -paper-rs --screenshot home.png --home # projects home screen -paper-rs --export-png out.png PROJECT_DIR --page 0 --scale 2 [--transparent] -paper-rs --export-svg out.svg PROJECT_DIR -paper-rs --export-pdf out.pdf PROJECT_DIR --frames # one PDF page per frame -paper-rs --export out_dir PROJECT_DIR --format png --scope frames # a folder of PNGs -paper-rs --import board.excalidraw # new project, prints its folder -``` +The product images use the new UI/UX source snapshot from PR #9. They are development previews; they are not screenshots of an already released desktop package. -## AI agents (MCP) +## Desktop preview -![A sign-in screen written by an agent through write_html](docs/screenshots/milestone7_mcp.png) +[Downloads and releases](https://github.com/arcitai/openpaperdesign/releases) · [Installation and platform status](docs/openpaperdesign/desktop.md) -paper-rs serves the [Model Context Protocol](https://modelcontextprotocol.io) while it runs, so -agents such as Claude Code and Codex can read and design on the open canvas, the way Paper's -own MCP server works (behaviour notes: `analysis/paper-mcp.md`). Every tool call runs through -the editor like a user edit: one undo step per call, and the window repaints immediately. +The existing **v0.1.0 release is source code**, not an installer. Desktop packaging is being verified in the desktop packaging work. Do not substitute an upstream Sketch download for an OpenPaperDesign app. -* **Streamable HTTP** (built into the app): `http://127.0.0.1:29980/mcp`. Paper uses 29979, so - both can run. Loopback only; requests from browsers (with an `Origin` header) and foreign - `Host` headers are rejected. `PAPER_RS_MCP_PORT=NNNN` changes the port; `PAPER_RS_MCP=0` - turns the server off. -* **stdio**: `paper-rs-mcp` (built next to `paper-rs`) relays stdio to the running app. It - lists the tools even when the app is closed, and tells the agent to start paper-rs if a - call can't reach it. +| Platform | Current evidence | +| --- | --- | +| macOS, Apple Silicon | Integrated new-UI preview opens, edits, saves and reopens on Apple M4; ad-hoc signed, not notarized. Downloaded first launch remains unqualified. | +| Windows, x64 | Release build and ZIP packaging passed in CI; native desktop journey pending. | +| Linux, x64 | Release build, CLI smoke and tarball packaging passed in CI; native desktop journey pending. | -### Claude Code +## What you can do -```sh -# HTTP (the app must be running) -claude mcp add --transport http --scope user paper-rs http://127.0.0.1:29980/mcp -# or stdio through the bridge -claude mcp add --scope user paper-rs -- /path/to/paper-rs/target/release/paper-rs-mcp -``` +- Work through systems with editable shapes, text and bound arrows. +- Sketch with hand-drawn shapes, ink and sticky notes in six colours. +- Design screens with frames, auto layout, components, vectors and image fills. +- Keep projects locally as JSON pages and assets. Export PNG, SVG or PDF; import SVG and Excalidraw. +- Connect a compatible MCP client, including Codex or Claude Code, when you want agent help. -### Codex +The native desktop app and the Codex plugin reuse the same editor, but have different host interfaces. The newer Codex Home/workspace UI is still developed separately in [PR #9](https://github.com/arcitai/openpaperdesign/pull/9). A desktop build does not imply that branch has been accepted or released. Avoid editing the same project simultaneously in the native app and the plugin; the plugin's checked writer path does not protect against external native writers. -```sh -# HTTP -codex mcp add paper-rs --url http://127.0.0.1:29980/mcp -# or stdio -codex mcp add paper-rs -- /path/to/paper-rs/target/release/paper-rs-mcp -``` - -which writes this to `~/.codex/config.toml` (either form works): - -```toml -[mcp_servers.paper-rs] -url = "http://127.0.0.1:29980/mcp" -# command = "/path/to/paper-rs/target/release/paper-rs-mcp" -``` - -Then ask the agent something like *"In paper-rs, create a 390×844 mobile artboard and design a -sign-in screen"*, or *"draw a flowchart of our deploy process on a new page, hand-drawn -style"*. - -### Tools - -Names and arguments follow Paper's MCP server where paper-rs has an equivalent, so prompts -written for Paper work here too. +## Why I built this -| Group | Tools | -|---|---| -| Projects & pages | `list_resources`, `open_file`, `create_file`, `create_page`, `rename_pages` | -| Read | `get_basic_info`, `get_selection`, `get_node_info`, `get_children`, `get_tree_summary`, `get_computed_styles`, `get_jsx`, `find_nodes`, `get_fill_image`, `get_font_family_info`, `get_guide` | -| See | `get_screenshot` (PNG image content of a node or the page), `export` (PNG/SVG/PDF files), `export_combined_pdf` | -| Write | `create_artboard`, `write_html`, `update_styles`, `set_text_content`, `rename_nodes`, `duplicate_nodes`, `move_nodes`, `delete_nodes`, `finish_working_on_nodes` (saves) | -| paper-rs extras | `create_shape` (rectangles, ellipses, diamonds, stars, stickies, text, lines, arrows bound to shapes), `select_nodes`, `undo`, `redo` | +I wanted somewhere to sketch systems and design interfaces with an AI agent, without another paid design-tool subscription. Michael's paper-rs gave me a native, editable canvas to build on. I keep that foundation and add the integrations and methods I use in my own work. -`write_html` takes HTML with inline styles: flexbox (`display:flex`, direction, gap, padding, -alignment, `flex-grow`, wrap) and block stacking map onto auto layout with CSS's stretch rules; -text gets size, weight, italic, color, line height, letter spacing, alignment and rich-text -runs (``, ``, ``); boxes get backgrounds (colors, gradients, images), -borders, radii, opacity and blend modes; `` takes local paths or `data:` URIs and `` -is imported as vectors. `update_styles` takes the same CSS as JSON, plus the hand-drawn keys -`sketch`, `roughness`, `fillStyle`, `strokeStyle`. Node ids look like `3fa9c1:14` (page and -node); `pageId` lets an agent work on another page without switching the user's view. +## Build from source -## Tests +Rust 1.95 or later and your platform's native build dependencies are required: ```sh -cargo test # ~140 tests: unit, scenario, headless GPU, CLI -PAPER_RS_REQUIRE_GPU=1 cargo test # fail (instead of skip) when no GPU adapter exists -cargo test --release --test perf -- --nocapture # 10k-element frame timings +cargo build --release --locked --bin openpaperdesign --bin paper-rs-mcp +./target/release/openpaperdesign ``` -* **Unit tests** cover geometry, vector paths, the rough.js port (compared with real - rough.js 4.6.4 output), auto layout and constraints, booleans, rich text spans, the elbow - router, formats and migrations, atomic writes, the project store, PDF structure, Excalidraw - and SVG import, the palette's fuzzy matching and the shortcut table. -* **Scenario tests** drive the editor with synthetic pointer and keyboard events: - `src/editor/tests.rs` (core editing), `tests/projects.rs` (projects, autosave, pages), - `tests/figma.rs` (groups, components, auto layout, constraints, pen, booleans, rich text, - guides), `tests/excalidraw.rs` (hand-drawn style, arrows, quick-connect, stickies, laser, - library), `tests/workflow.rs` (layers drag, palette, shortcuts), `tests/io.rs` (export, - import, clipboard, CLI), `tests/mcp.rs` (every MCP tool on a headless session, then an rmcp - client over HTTP and through the `paper-rs-mcp` stdio bridge). -* **Headless GPU tests** render through wgpu, assert pixels and write the screenshots below. - One rasterizes the SVG export with resvg and compares it with the GPU render; one renders - the PDF export with pdftoppm when installed. +The distribution entrypoint uses the platform’s local application-data folder (`~/Library/Application Support/OpenPaperDesign/projects` on macOS). Set `PAPER_RS_HOME` to choose another library. Existing upstream libraries are not moved. The original `paper-rs` command, editor, file formats and environment variables remain available. Platform details and the optional MCP bridge are in the [desktop guide](docs/openpaperdesign/desktop.md). The original [upstream README](docs/upstream-README.md) is retained for its detailed feature and command documentation. -| Milestone | Screenshot(s) in `docs/screenshots/` | -|---|---| -| 1. Local projects | `milestone1_home.png`, `milestone1_trash.png` | -| 2. Pages | `milestone2_pages.png` | -| 3. Figma core | `milestone3_figma_core.png`, `milestone3_canvas.png` | -| 4. Excalidraw core | `milestone4_whiteboard.png` | -| 5. Inspector, palette, shortcuts | `milestone5_inspector.png`, `milestone5_palette.png`, `milestone5_shortcuts.png` | -| 6. Import / export | `milestone6_pdf_page.png` (PDF rasterized by pdftoppm), `milestone6_excalidraw_import.png`, `milestone6_svg_import.png` | -| 7. MCP server | `milestone7_mcp.png` (a screen written through `write_html`, captured with `get_screenshot`), `milestone7_mcp_live_xvfb.png` (the live app after Claude Code drew into it over the stdio bridge) | -| paper-rs 0.1 | `m1_*` … `m6_*`, `app_demo.png`, `x11_window_xvfb.png` | +## Agent integration -## Using it +- [Native desktop MCP](docs/openpaperdesign/desktop.md#optional-ai-agents): connect an MCP client to the running app. +- [Codex plugin](docs/openpaperdesign/plugin-b2b.md): the separate, project-bound browser integration. +- [Design methods](plugin/openpaperdesign/skills/design/SKILL.md): portable direction and rendered review. -![Whiteboard features](docs/screenshots/milestone4_whiteboard.png) - -The tools sit in the floating bar at the bottom of the canvas. Select a layer to edit it in -the **Design** panel on the right. The **Layers** panel on the left shows the tree: click to -select, Shift-click to add, double-click to rename, and use 👁 / 🔒 to hide or lock. The -**Up/Down** buttons reorder. - -* Draw a frame around existing shapes and it adopts them. Shapes drawn inside a frame become - its children, and dragging a layer into or out of a frame re-parents it. -* Top-level frames that contain content act as canvas regions. Drag from their body to - marquee-select inside them, and drag the frame by its name label. -* The pencil keeps a constant on-screen width at any zoom. With nothing selected, set width, - smoothing and color in the Design panel. -* Hold ⌘/Ctrl while dragging to suspend snapping. - -### Keyboard shortcuts - -⌘ on macOS, Ctrl on Linux and Windows. Press `?` or ⌘/ in the app for the full grouped list -(100+ bindings), or ⌘K to search every command. - -| Keys | Action | Keys | Action | -|---|---|---|---| -| V / 1 | Select | ⌘K | Command palette | -| H, or hold Space | Hand (pan) | ⌘Z / ⌘⇧Z | Undo / redo | -| F or A | Frame | ⌘G / ⌘⇧G | Group / ungroup | -| R / 2, O / 4, D / 3 | Rectangle, ellipse, diamond | ⌘⌥G | Frame selection | -| ⇧Y, ⇧S | Polygon, star | ⌘⌥K / ⌘⌥B | Create component / detach | -| L / 6, ⇧L / 5 | Line, arrow | ⇧A / ⌥⇧A | Add / remove auto layout | -| P, ⇧P / 7 | Pen, pencil | ⌥A ⌥H ⌥D / ⌥W ⌥V ⌥S | Align left/center/right, top/middle/bottom | -| T / 8, S | Text, sticky note | ⌥⇧H / ⌥⇧V | Distribute spacing | -| K | Laser pointer | ⌥⇧U S I E | Union, subtract, intersect, exclude | -| E / 0 | Eraser | ⌘E | Flatten | -| 9, ⌘⇧K | Place image | ⌘B / ⌘I / ⌘U / ⌘⇧X | Bold, italic, underline, strikethrough | -| Enter / ⇧Enter | Edit (text, path) / select parent | ⌘⌥H | Hand-drawn style | -| Tab / ⇧Tab | Next / previous sibling | ⇧H / ⇧V | Flip | -| ⌥-drag | Duplicate while dragging | ⌘⌥C / ⌘⌥V | Copy / paste properties | -| ⌥ hover | Measure distances | ⌘R | Rename | -| ⌘= / ⌘- / ⇧0 | Zoom in / out / 100% | ⇧1 / ⇧2 | Zoom to fit / selection | -| PageUp / PageDown | Previous / next page | ⌘\\ | Show / hide UI | - -## Feature parity: Paper / Figma / Excalidraw vs paper-rs - -Columns 2–4 say whether each product has the feature (✓) and where the behavior was learned: -Paper from its desktop shell and public docs (`analysis/PAPER_ARCHITECTURE.md`), Figma from -public docs (`analysis/FIGMA_NOTES.md`), Excalidraw from its MIT source -(`analysis/EXCALIDRAW_NOTES.md`). The last column is paper-rs: ✅ done, ⚠️ partial (see -Known gaps), ⏳ planned in a later milestone, ❌ not planned. - -| Feature | Paper | Figma | Excalidraw | paper-rs | -|---|---|---|---|---| -| Local projects, recents grid, trash | (cloud dashboard) | ✓ (cloud) | (single file) | ✅ Folders in `~/Documents/paper-rs`, thumbnails, rename/duplicate/trash/restore | -| Autosave, crash-safe, versioned files | ✓ (cloud) | ✓ (cloud) | ✓ (browser storage) | ✅ 1 s autosave, atomic writes, migration chain (page v1→v3) | -| Pages, infinite canvas per page | ✓ | ✓ | – | ✅ Per-page history and camera, reorder/duplicate/delete | -| Frames with clipping | ✓ | ✓ | ✓ | ✅ ⚠️ clip is rectangular (corner radius not clipped) | -| Groups | ✓ | ✓ | ✓ | ✅ Bounds follow children, click selects group, double-click enters, scale on resize | -| Components, instances, overrides | – | ✓ | (library only) | ✅ Live sync, per-property overrides, reset, detach, go to main | -| Auto layout (direction, gap, padding, hug/fill/fixed, wrap) | ✓ (flexbox) | ✓ | – | ✅ Drag-reorder in flow, space-between, absolute children | -| Constraints | – | ✓ | – | ✅ Left/right/stretch/center/scale per axis, ⌘ to ignore | -| Pen tool, vector point editing | ✓ | ✓ | – | ✅ Corner/smooth points, handles (⌥ breaks), close, delete, toggle | -| Boolean ops (union/subtract/intersect/exclude), flatten | (roadmap) | ✓ | – | ✅ ⚠️ destructive (result is a path, curves flattened) | -| Gradients (linear, radial) | ✓ | ✓ | – | ✅ Multi-stop, angle, radius; ⚠️ no on-canvas gradient handles | -| Image fills | ✓ | ✓ | ✓ (image element) | ✅ Fill/fit/stretch/tile, place image (⌘⇧K), drag-and-drop, stored in project | -| Strokes: width, align, dash, caps, joins | ✓ | ✓ | ✓ | ✅ | -| Corner radius (per corner), polygons, stars | ✓ | ✓ | – | ✅ | -| Opacity, blend modes | ✓ | ✓ | ✓ (opacity) | ✅ ⚠️ canvas renders 5 modes exactly, all 16 exact in SVG | -| Rich text (per-range bold/italic/size/colour) | ✓ | ✓ | – | ✅ Plus underline, strikethrough, line height, letter spacing | -| Align / distribute | ✓ | ✓ | ✓ | ✅ ⌥A/D/W/S/H/V, ⌥⇧H/V | -| Smart guides, spacing, measurement | ✓ | ✓ | – | ✅ Edge/center snap, equal spacing, ⌥-hover distances | -| Hand-drawn (rough) style per element | – | – | ✓ | ✅ rough.js 4.6.4 port, verified against rough.js output; sloppiness, fill styles | -| Bound arrows, elbow arrows, quick shapes, sticky notes | – | (FigJam) | ✓ | ✅ Orbit binding that follows shapes, straight/curved/elbow, 5 heads, diamond, quick-connect, stickies | -| Laser pointer, shape library | – | (FigJam) | ✓ | ✅ Fading laser trail; shared library with starter items | -| Inspector, layers drag-reorder, command palette (⌘K) | ✓ | ✓ | ✓ | ✅ Sectioned inspector, tree drag above/below/into, fuzzy palette over actions, pages, layers, library | -| Export PNG / SVG / PDF of selection, frame, page | ✓ | ✓ | ✓ (PNG/SVG) | ✅ Selection, each frame, page or all pages; vector PDF with gradients, images, blend modes; copy as PNG/SVG | -| Import .excalidraw / SVG, copy-paste across projects | – | ✓ (SVG) | ✓ | ✅ .excalidraw scenes, .excalidrawlib libraries, SVG (paths, gradients, images, text), system clipboard between pages, projects and apps | -| Freehand ink with pressure | – | (pencil) | ✓ | ✅ Smoothing, pressure, eraser | -| MCP server for AI agents | ✓ | ✓ (Dev Mode) | ✓ (community) | ✅ Streamable HTTP in the app + stdio bridge; Paper-compatible tool names, `write_html`, screenshots, whiteboard shapes | -| Multiplayer, cloud sync, plugins | ✓ | ✓ | ✓ | ❌ local-first by design | - -## Known gaps - -* **Paper's document model is HTML/CSS.** paper-rs uses geometric nodes with flexbox-like auto - layout on frames; there are no variables, CSS export or code generation. -* **Rendering approximations.** Frame clipping is a rectangle (scissor), so rounded frames - don't clip their corners. Group opacity multiplies into children instead of compositing the - group. Overlay, color dodge/burn, hard/soft light, difference, exclusion, hue, saturation, - color and luminosity blend as Normal on the canvas (they are exact in SVG export). -* **Booleans are destructive**: the result is a new vector path with flattened curves. - Gradients are edited in the inspector only. -* **Not run on macOS hardware yet.** The macOS build type-checks and uses only portable APIs - (Metal via wgpu, winit, rfd), but it has not been launched on a Mac. On Linux, the real - window was exercised under Xvfb; the screenshot is `x11_window_xvfb.png`. -* **Pen pressure.** winit 0.30 does not expose tablet pressure for pen events that arrive as - mouse input, which is common with macOS and Wacom tablets. Pressure comes from touch/pen - `force` when the platform reports it. Otherwise it is simulated from stroke velocity. -* **Text** uses Inter (regular, bold, italic, bold italic) with no shaping, so there are no - ligatures, complex scripts or RTL, and no font picker. IME preedit is not displayed - (committed text works). -* **No rotation.** Nodes are axis-aligned boxes; rotated imports are baked into paths. -* **Undo granularity.** Document renames from the Design panel are not undoable, and the - selection is not part of history. -* **Performance.** Nodes are tessellated once per zoom octave and cached; unchanged frames - reuse the previous item list and GPU buffers, so panning uploads nothing. With 10k visible - nodes a cached frame takes ~3 ms (release, `tests/perf.rs`); the first frame at a new zoom - octave takes ~140 ms on a CPU. Not tuned for 100k+. -* **PDF**: text is outlined (not selectable); gradient stop transparency uses the first - stop's alpha; tiled image fills are drawn as repeated images. -* **Import**: Excalidraw rotation is baked into paths (text rotation is dropped), bound text - inside shapes stays a separate text layer (except sticky notes), and fonts map to Inter. - SVG patterns, filters, masks and clip paths are not imported. -* **MCP**: one project is open at a time, so `fileId` must name the open project (call - `open_file` to switch; the user's window switches too). No design tokens, comments or AI - image generation (Paper has them). Moving or duplicating layers between pages isn't - supported. Remote `http(s)` images aren't fetched; pass a local path or a `data:` URI. - CSS margin, grid, shadows and transforms are reported back as ignored styles. -* **Linux file dialogs** need an XDG desktop portal. Without one, Save/Open dialogs return - nothing. The headless CLI is unaffected. - -## Project layout - -``` -src/geom.rs vectors, rects, camera -src/vector.rs bezier paths: flatten, hit test, SVG data, shapes, dashes -src/model/ Node / NodeKind / paints / Document arena -src/history.rs transactions, undo/redo -src/layout.rs auto layout, constraints, group bounds -src/ink.rs stroke smoothing, pressure, outline -src/text.rs rich text layout and glyph outlines -src/assets.rs image registry (content-hashed) -src/project/ project store, manifest/page formats, migrations -src/session.rs open project, autosave, thumbnails -src/editor/ tools, structure (groups/components), vector ops, pages, text editing -src/render/ draw ops → cached meshes → wgpu; headless capture -src/io/ PNG, SVG, PDF, export scopes, Excalidraw + SVG import, clipboard, - atomic writes -src/connector.rs arrow bindings, elbow router, arrowheads -src/library.rs shared shape library -src/ui/ home screen, pages, layers, inspector -src/app.rs winit application -src/mcp/ MCP server (rmcp): tools, HTML/CSS mapping, HTTP service -src/bin/paper-rs-mcp.rs stdio bridge to the running app -analysis/ reverse-engineering findings and design (raw dumps are gitignored) -docs/screenshots/ headless renders produced by `cargo test` -``` +No account or hosted backend is required. There is no cloud sync or multiplayer service. The native app and project-bound plugin have distinct access contracts; setup instructions document those boundaries. -## openpaperdesign: Codex plugin (this fork) +## Product page and provenance -This fork adds a local Codex plugin, `openpaperdesign` (`plugin/openpaperdesign/`, local -marketplace `.agents/plugins/marketplace.json`). The agent binds a design project in your -workspace, edits it over MCP, and opens the editable canvas in Codex's in-app browser on -127.0.0.1. It runs locally only and is not affiliated with Paper. To set it up, run -`./scripts/build-web.sh`, then -`cargo install --locked --path crates/openpaperdesign-serve --bin openpaperdesign-mcp`. See -`docs/openpaperdesign/plugin-b2b.md` for the steps and what is verified. +The source for the [product page](site/README.md) is in `site/`. Its screenshots are rendered by the actual native app from the public, editable [product fixtures](design/product/fixtures). [Capture and asset provenance](site/assets-sources.json) records the source and hashes. Public launch at `openpaperdesign.arcitai.com` remains separate from local preview. -## License +## Licences and credits -MIT OR Apache-2.0 for the code in this repository. Inter (Regular, Medium, SemiBold) is -© The Inter Project Authors under the SIL Open Font License 1.1; see `assets/fonts/OFL.txt`. +MIT OR Apache-2.0. Keep [LICENSE-MIT](LICENSE-MIT), [LICENSE-APACHE](LICENSE-APACHE), [THIRD_PARTY.md](THIRD_PARTY.md) and the bundled font notices with distributions. Upstream history and authorship are preserved. The product page's Geist font is also distributed under its bundled OFL notice. diff --git a/assets/brand/home-logo-dark.png b/assets/brand/home-logo-dark.png new file mode 100644 index 0000000..fad8393 Binary files /dev/null and b/assets/brand/home-logo-dark.png differ diff --git a/assets/brand/home-logo-light.png b/assets/brand/home-logo-light.png new file mode 100644 index 0000000..49e46bf Binary files /dev/null and b/assets/brand/home-logo-light.png differ diff --git a/assets/brand/openpaperdesign-dark.svg b/assets/brand/openpaperdesign-dark.svg new file mode 100644 index 0000000..c8bb07f --- /dev/null +++ b/assets/brand/openpaperdesign-dark.svg @@ -0,0 +1 @@ +OpenPaperDesign diff --git a/assets/brand/openpaperdesign.svg b/assets/brand/openpaperdesign.svg new file mode 100644 index 0000000..71eb3d1 --- /dev/null +++ b/assets/brand/openpaperdesign.svg @@ -0,0 +1 @@ +OpenPaperDesign diff --git a/design/product/fixtures/architecture.excalidraw b/design/product/fixtures/architecture.excalidraw new file mode 100644 index 0000000..11a6a2b --- /dev/null +++ b/design/product/fixtures/architecture.excalidraw @@ -0,0 +1,654 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "OpenPaperDesign public product fixture", + "elements": [ + { + "id": "item0000", + "type": "text", + "x": 0, + "y": 0, + "width": 1000, + "height": 54.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 17, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "A place to think. A system you can explain.", + "originalText": "A place to think. A system you can explain.", + "fontSize": 36, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0001", + "type": "text", + "x": 0, + "y": 62, + "width": 900, + "height": 25.5, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 18, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "OpenPaperDesign / local desktop + optional AI agents", + "originalText": "OpenPaperDesign / local desktop + optional AI agents", + "fontSize": 17, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0002", + "type": "rectangle", + "x": 0, + "y": 165, + "width": 240, + "height": 100, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "#f0f3f8", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 19, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "roundness": { + "type": 3, + "value": 8 + } + }, + { + "id": "item0003", + "type": "text", + "x": 18, + "y": 183, + "width": 204, + "height": 30.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 20, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "You", + "originalText": "You", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0004", + "type": "text", + "x": 18, + "y": 219, + "width": 204, + "height": 19.5, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 21, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Sketch / review / refine", + "originalText": "Sketch / review / refine", + "fontSize": 13, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0005", + "type": "rectangle", + "x": 360, + "y": 165, + "width": 280, + "height": 100, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "#e0e8f5", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 22, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "roundness": { + "type": 3, + "value": 8 + } + }, + { + "id": "item0006", + "type": "text", + "x": 378, + "y": 183, + "width": 244, + "height": 30.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 23, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "OpenPaperDesign", + "originalText": "OpenPaperDesign", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0007", + "type": "text", + "x": 378, + "y": 219, + "width": 244, + "height": 19.5, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 24, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "An editable native canvas", + "originalText": "An editable native canvas", + "fontSize": 13, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0008", + "type": "rectangle", + "x": 760, + "y": 165, + "width": 240, + "height": 100, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "#f0f3f8", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 25, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "roundness": { + "type": 3, + "value": 8 + } + }, + { + "id": "item0009", + "type": "text", + "x": 778, + "y": 183, + "width": 204, + "height": 30.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 26, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Your project", + "originalText": "Your project", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0010", + "type": "text", + "x": 778, + "y": 219, + "width": 204, + "height": 19.5, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 27, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Pages / JSON / assets", + "originalText": "Pages / JSON / assets", + "fontSize": 13, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0011", + "type": "arrow", + "x": 245, + "y": 215, + "width": 105, + "height": 1, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 28, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "points": [ + [ + 0, + 0 + ], + [ + 105, + 0 + ] + ], + "endArrowhead": "arrow" + }, + { + "id": "item0012", + "type": "arrow", + "x": 645, + "y": 215, + "width": 105, + "height": 1, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 29, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "points": [ + [ + 0, + 0 + ], + [ + 105, + 0 + ] + ], + "endArrowhead": "arrow" + }, + { + "id": "item0013", + "type": "rectangle", + "x": 100, + "y": 390, + "width": 240, + "height": 100, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "#f0f3f8", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 30, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "roundness": { + "type": 3, + "value": 8 + } + }, + { + "id": "item0014", + "type": "text", + "x": 118, + "y": 408, + "width": 204, + "height": 30.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 31, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Codex", + "originalText": "Codex", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0015", + "type": "text", + "x": 118, + "y": 444, + "width": 204, + "height": 19.5, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 32, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Optional MCP client", + "originalText": "Optional MCP client", + "fontSize": 13, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0016", + "type": "rectangle", + "x": 680, + "y": 390, + "width": 240, + "height": 100, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "#f0f3f8", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 33, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "roundness": { + "type": 3, + "value": 8 + } + }, + { + "id": "item0017", + "type": "text", + "x": 698, + "y": 408, + "width": 204, + "height": 30.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 34, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Claude Code", + "originalText": "Claude Code", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0018", + "type": "text", + "x": 698, + "y": 444, + "width": 204, + "height": 19.5, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 35, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Optional MCP client", + "originalText": "Optional MCP client", + "fontSize": 13, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0019", + "type": "arrow", + "x": 225, + "y": 385, + "width": 160, + "height": 110, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 36, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "points": [ + [ + 0, + 0 + ], + [ + 160, + -110 + ] + ], + "endArrowhead": "arrow" + }, + { + "id": "item0020", + "type": "arrow", + "x": 800, + "y": 385, + "width": 180, + "height": 110, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 37, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "points": [ + [ + 0, + 0 + ], + [ + -180, + -110 + ] + ], + "endArrowhead": "arrow" + }, + { + "id": "item0021", + "type": "stickynote", + "x": 385, + "y": 365, + "width": 190, + "height": 150, + "angle": 0, + "strokeColor": "#74644f", + "backgroundColor": "#ffdf6b", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 38, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0022", + "type": "text", + "x": 400, + "y": 383, + "width": 160, + "height": 27.0, + "angle": 0, + "strokeColor": "#30271f", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 39, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Design the interface\nbeside the system.", + "originalText": "Design the interface\nbeside the system.", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25, + "containerId": "item0021" + }, + { + "id": "item0023", + "type": "text", + "x": 280, + "y": 600, + "width": 800, + "height": 33.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 40, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "No account. No subscription. Your files.", + "originalText": "No account. No subscription. Your files.", + "fontSize": 22, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + } + ], + "appState": { + "viewBackgroundColor": "#fafafa" + }, + "files": {} +} diff --git a/design/product/fixtures/banner-study.svg b/design/product/fixtures/banner-study.svg new file mode 100644 index 0000000..6e80d2f --- /dev/null +++ b/design/product/fixtures/banner-study.svg @@ -0,0 +1 @@ +One visual thread across my work.gustavonline / Arc’IT AI / onlinesourdoughThe selected LinkedIn banner, brought into an editable canvas.Banner artwork is an image layer. Frames, notes and diagrams stay editable beside it. \ No newline at end of file diff --git a/design/product/fixtures/design.excalidraw b/design/product/fixtures/design.excalidraw new file mode 100644 index 0000000..7dd0476 --- /dev/null +++ b/design/product/fixtures/design.excalidraw @@ -0,0 +1,5525 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "OpenPaperDesign public product fixture", + "elements": [ + { + "id": "item0000", + "type": "text", + "x": 0, + "y": 0, + "width": 1200, + "height": 54.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 17, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Three brands. One thread.", + "originalText": "Three brands. One thread.", + "fontSize": 36, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0001", + "type": "text", + "x": 0, + "y": 65, + "width": 1200, + "height": 25.5, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 18, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Public interface studies / gustavonline \u00b7 Arc\u2019IT AI \u00b7 onlinesourdough", + "originalText": "Public interface studies / gustavonline \u00b7 Arc\u2019IT AI \u00b7 onlinesourdough", + "fontSize": 17, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0002", + "type": "rectangle", + "x": 0, + "y": 145, + "width": 410, + "height": 510, + "angle": 0, + "strokeColor": "#ccc8c0", + "backgroundColor": "#fafafa", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 19, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0003", + "type": "text", + "x": 24, + "y": 168, + "width": 360, + "height": 30.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 20, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "gustavonline", + "originalText": "gustavonline", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0004", + "type": "line", + "x": 20, + "y": 211, + "width": 370, + "height": 1, + "angle": 0, + "strokeColor": "#d9d4cb", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 21, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "points": [ + [ + 0, + 0 + ], + [ + 370, + 0 + ] + ] + }, + { + "id": "item0005", + "type": "text", + "x": 24, + "y": 245, + "width": 360, + "height": 43.5, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 22, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Work. Ideas.\nExperiments.", + "originalText": "Work. Ideas.\nExperiments.", + "fontSize": 29, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0006", + "type": "text", + "x": 24, + "y": 340, + "width": 360, + "height": 19.5, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 23, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Data, software and agentic engineering.", + "originalText": "Data, software and agentic engineering.", + "fontSize": 13, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0007", + "type": "rectangle", + "x": 25, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 24, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0008", + "type": "rectangle", + "x": 54, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 25, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0009", + "type": "rectangle", + "x": 83, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 26, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0010", + "type": "rectangle", + "x": 112, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 27, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0011", + "type": "rectangle", + "x": 141, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 28, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0012", + "type": "rectangle", + "x": 170, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 29, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0013", + "type": "rectangle", + "x": 199, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 30, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0014", + "type": "rectangle", + "x": 228, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 31, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0015", + "type": "rectangle", + "x": 257, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 32, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0016", + "type": "rectangle", + "x": 286, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 33, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0017", + "type": "rectangle", + "x": 315, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 34, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0018", + "type": "rectangle", + "x": 344, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 35, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0019", + "type": "rectangle", + "x": 25, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 36, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0020", + "type": "rectangle", + "x": 54, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 37, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0021", + "type": "rectangle", + "x": 83, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 38, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0022", + "type": "rectangle", + "x": 112, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 39, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0023", + "type": "rectangle", + "x": 141, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 40, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0024", + "type": "rectangle", + "x": 170, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 41, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0025", + "type": "rectangle", + "x": 199, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 42, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0026", + "type": "rectangle", + "x": 228, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 43, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0027", + "type": "rectangle", + "x": 257, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 44, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0028", + "type": "rectangle", + "x": 286, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 45, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0029", + "type": "rectangle", + "x": 315, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 46, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0030", + "type": "rectangle", + "x": 344, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 47, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0031", + "type": "rectangle", + "x": 25, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 48, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0032", + "type": "rectangle", + "x": 54, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 49, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0033", + "type": "rectangle", + "x": 83, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 50, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0034", + "type": "rectangle", + "x": 112, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 51, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0035", + "type": "rectangle", + "x": 141, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 52, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0036", + "type": "rectangle", + "x": 170, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 53, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0037", + "type": "rectangle", + "x": 199, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 54, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0038", + "type": "rectangle", + "x": 228, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 55, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0039", + "type": "rectangle", + "x": 257, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 56, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0040", + "type": "rectangle", + "x": 286, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 57, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0041", + "type": "rectangle", + "x": 315, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 58, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0042", + "type": "rectangle", + "x": 344, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 59, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0043", + "type": "rectangle", + "x": 25, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 60, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0044", + "type": "rectangle", + "x": 54, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 61, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0045", + "type": "rectangle", + "x": 83, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 62, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0046", + "type": "rectangle", + "x": 112, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 63, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0047", + "type": "rectangle", + "x": 141, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 64, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0048", + "type": "rectangle", + "x": 170, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 65, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0049", + "type": "rectangle", + "x": 199, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 66, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0050", + "type": "rectangle", + "x": 228, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 67, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0051", + "type": "rectangle", + "x": 257, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 68, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0052", + "type": "rectangle", + "x": 286, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 69, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0053", + "type": "rectangle", + "x": 315, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 70, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0054", + "type": "rectangle", + "x": 344, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 71, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0055", + "type": "rectangle", + "x": 25, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 72, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0056", + "type": "rectangle", + "x": 54, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 73, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0057", + "type": "rectangle", + "x": 83, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 74, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0058", + "type": "rectangle", + "x": 112, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 75, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0059", + "type": "rectangle", + "x": 141, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 76, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0060", + "type": "rectangle", + "x": 170, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 77, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0061", + "type": "rectangle", + "x": 199, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 78, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0062", + "type": "rectangle", + "x": 228, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 79, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0063", + "type": "rectangle", + "x": 257, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 80, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0064", + "type": "rectangle", + "x": 286, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 81, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0065", + "type": "rectangle", + "x": 315, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 82, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0066", + "type": "rectangle", + "x": 344, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 83, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0067", + "type": "rectangle", + "x": 25, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 84, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0068", + "type": "rectangle", + "x": 54, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 85, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0069", + "type": "rectangle", + "x": 83, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 86, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0070", + "type": "rectangle", + "x": 112, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 87, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0071", + "type": "rectangle", + "x": 141, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 88, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0072", + "type": "rectangle", + "x": 170, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 89, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0073", + "type": "rectangle", + "x": 199, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 90, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0074", + "type": "rectangle", + "x": 228, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 91, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0075", + "type": "rectangle", + "x": 257, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 92, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0076", + "type": "rectangle", + "x": 286, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 93, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0077", + "type": "rectangle", + "x": 315, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 94, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0078", + "type": "rectangle", + "x": 344, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 95, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0079", + "type": "rectangle", + "x": 25, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 96, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0080", + "type": "rectangle", + "x": 54, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 97, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0081", + "type": "rectangle", + "x": 83, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 98, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0082", + "type": "rectangle", + "x": 112, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 99, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0083", + "type": "rectangle", + "x": 141, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 100, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0084", + "type": "rectangle", + "x": 170, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 101, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0085", + "type": "rectangle", + "x": 199, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 102, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0086", + "type": "rectangle", + "x": 228, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 103, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0087", + "type": "rectangle", + "x": 257, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d37635", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 104, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0088", + "type": "rectangle", + "x": 286, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 105, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0089", + "type": "rectangle", + "x": 315, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 106, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0090", + "type": "rectangle", + "x": 344, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 107, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0091", + "type": "rectangle", + "x": 450, + "y": 145, + "width": 410, + "height": 510, + "angle": 0, + "strokeColor": "#ccc8c0", + "backgroundColor": "#f3f5f8", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 108, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0092", + "type": "text", + "x": 474, + "y": 168, + "width": 360, + "height": 30.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 109, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Arc\u2019IT AI", + "originalText": "Arc\u2019IT AI", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0093", + "type": "line", + "x": 470, + "y": 211, + "width": 370, + "height": 1, + "angle": 0, + "strokeColor": "#d9d4cb", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 110, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "points": [ + [ + 0, + 0 + ], + [ + 370, + 0 + ] + ] + }, + { + "id": "item0094", + "type": "text", + "x": 474, + "y": 245, + "width": 360, + "height": 43.5, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 111, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "AI should understand\nyour business.", + "originalText": "AI should understand\nyour business.", + "fontSize": 29, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0095", + "type": "text", + "x": 474, + "y": 340, + "width": 360, + "height": 19.5, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 112, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Your data and business context.", + "originalText": "Your data and business context.", + "fontSize": 13, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0096", + "type": "rectangle", + "x": 475, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 113, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0097", + "type": "rectangle", + "x": 504, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 114, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0098", + "type": "rectangle", + "x": 533, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 115, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0099", + "type": "rectangle", + "x": 562, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 116, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0100", + "type": "rectangle", + "x": 591, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 117, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0101", + "type": "rectangle", + "x": 620, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 118, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0102", + "type": "rectangle", + "x": 649, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 119, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0103", + "type": "rectangle", + "x": 678, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 120, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0104", + "type": "rectangle", + "x": 707, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 121, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0105", + "type": "rectangle", + "x": 736, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 122, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0106", + "type": "rectangle", + "x": 765, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 123, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0107", + "type": "rectangle", + "x": 794, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 124, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0108", + "type": "rectangle", + "x": 475, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 125, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0109", + "type": "rectangle", + "x": 504, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 126, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0110", + "type": "rectangle", + "x": 533, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 127, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0111", + "type": "rectangle", + "x": 562, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 128, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0112", + "type": "rectangle", + "x": 591, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 129, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0113", + "type": "rectangle", + "x": 620, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 130, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0114", + "type": "rectangle", + "x": 649, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 131, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0115", + "type": "rectangle", + "x": 678, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 132, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0116", + "type": "rectangle", + "x": 707, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 133, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0117", + "type": "rectangle", + "x": 736, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 134, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0118", + "type": "rectangle", + "x": 765, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 135, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0119", + "type": "rectangle", + "x": 794, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 136, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0120", + "type": "rectangle", + "x": 475, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 137, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0121", + "type": "rectangle", + "x": 504, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 138, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0122", + "type": "rectangle", + "x": 533, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 139, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0123", + "type": "rectangle", + "x": 562, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 140, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0124", + "type": "rectangle", + "x": 591, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 141, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0125", + "type": "rectangle", + "x": 620, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 142, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0126", + "type": "rectangle", + "x": 649, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 143, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0127", + "type": "rectangle", + "x": 678, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 144, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0128", + "type": "rectangle", + "x": 707, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 145, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0129", + "type": "rectangle", + "x": 736, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 146, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0130", + "type": "rectangle", + "x": 765, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 147, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0131", + "type": "rectangle", + "x": 794, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 148, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0132", + "type": "rectangle", + "x": 475, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 149, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0133", + "type": "rectangle", + "x": 504, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 150, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0134", + "type": "rectangle", + "x": 533, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 151, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0135", + "type": "rectangle", + "x": 562, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 152, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0136", + "type": "rectangle", + "x": 591, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 153, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0137", + "type": "rectangle", + "x": 620, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 154, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0138", + "type": "rectangle", + "x": 649, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 155, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0139", + "type": "rectangle", + "x": 678, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 156, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0140", + "type": "rectangle", + "x": 707, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 157, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0141", + "type": "rectangle", + "x": 736, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 158, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0142", + "type": "rectangle", + "x": 765, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 159, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0143", + "type": "rectangle", + "x": 794, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 160, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0144", + "type": "rectangle", + "x": 475, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 161, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0145", + "type": "rectangle", + "x": 504, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 162, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0146", + "type": "rectangle", + "x": 533, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 163, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0147", + "type": "rectangle", + "x": 562, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 164, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0148", + "type": "rectangle", + "x": 591, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 165, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0149", + "type": "rectangle", + "x": 620, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 166, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0150", + "type": "rectangle", + "x": 649, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 167, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0151", + "type": "rectangle", + "x": 678, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 168, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0152", + "type": "rectangle", + "x": 707, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 169, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0153", + "type": "rectangle", + "x": 736, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 170, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0154", + "type": "rectangle", + "x": 765, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 171, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0155", + "type": "rectangle", + "x": 794, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 172, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0156", + "type": "rectangle", + "x": 475, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 173, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0157", + "type": "rectangle", + "x": 504, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 174, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0158", + "type": "rectangle", + "x": 533, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 175, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0159", + "type": "rectangle", + "x": 562, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 176, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0160", + "type": "rectangle", + "x": 591, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 177, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0161", + "type": "rectangle", + "x": 620, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 178, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0162", + "type": "rectangle", + "x": 649, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 179, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0163", + "type": "rectangle", + "x": 678, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 180, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0164", + "type": "rectangle", + "x": 707, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 181, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0165", + "type": "rectangle", + "x": 736, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 182, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0166", + "type": "rectangle", + "x": 765, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 183, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0167", + "type": "rectangle", + "x": 794, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 184, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0168", + "type": "rectangle", + "x": 475, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 185, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0169", + "type": "rectangle", + "x": 504, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 186, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0170", + "type": "rectangle", + "x": 533, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 187, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0171", + "type": "rectangle", + "x": 562, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 188, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0172", + "type": "rectangle", + "x": 591, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 189, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0173", + "type": "rectangle", + "x": 620, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 190, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0174", + "type": "rectangle", + "x": 649, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 191, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0175", + "type": "rectangle", + "x": 678, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 192, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0176", + "type": "rectangle", + "x": 707, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#7697c6", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 193, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0177", + "type": "rectangle", + "x": 736, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#b1c2e0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 194, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0178", + "type": "rectangle", + "x": 765, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d8e2f2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 195, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0179", + "type": "rectangle", + "x": 794, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#4265a0", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 196, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0180", + "type": "rectangle", + "x": 900, + "y": 145, + "width": 410, + "height": 510, + "angle": 0, + "strokeColor": "#ccc8c0", + "backgroundColor": "#f8f1e5", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 197, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0181", + "type": "text", + "x": 924, + "y": 168, + "width": 360, + "height": 30.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 198, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "onlinesourdough", + "originalText": "onlinesourdough", + "fontSize": 20, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0182", + "type": "line", + "x": 920, + "y": 211, + "width": 370, + "height": 1, + "angle": 0, + "strokeColor": "#d9d4cb", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 199, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "points": [ + [ + 0, + 0 + ], + [ + 370, + 0 + ] + ] + }, + { + "id": "item0183", + "type": "text", + "x": 924, + "y": 245, + "width": 360, + "height": 43.5, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 200, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Valuable AI starts\nwith your data.", + "originalText": "Valuable AI starts\nwith your data.", + "fontSize": 29, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0184", + "type": "text", + "x": 924, + "y": 340, + "width": 360, + "height": 19.5, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 201, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "And your business context.", + "originalText": "And your business context.", + "fontSize": 13, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0185", + "type": "rectangle", + "x": 925, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 202, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0186", + "type": "rectangle", + "x": 954, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 203, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0187", + "type": "rectangle", + "x": 983, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 204, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0188", + "type": "rectangle", + "x": 1012, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 205, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0189", + "type": "rectangle", + "x": 1041, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 206, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0190", + "type": "rectangle", + "x": 1070, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 207, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0191", + "type": "rectangle", + "x": 1099, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 208, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0192", + "type": "rectangle", + "x": 1128, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 209, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0193", + "type": "rectangle", + "x": 1157, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 210, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0194", + "type": "rectangle", + "x": 1186, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 211, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0195", + "type": "rectangle", + "x": 1215, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 212, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0196", + "type": "rectangle", + "x": 1244, + "y": 420, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 213, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0197", + "type": "rectangle", + "x": 925, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 214, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0198", + "type": "rectangle", + "x": 954, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 215, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0199", + "type": "rectangle", + "x": 983, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 216, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0200", + "type": "rectangle", + "x": 1012, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 217, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0201", + "type": "rectangle", + "x": 1041, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 218, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0202", + "type": "rectangle", + "x": 1070, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 219, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0203", + "type": "rectangle", + "x": 1099, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 220, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0204", + "type": "rectangle", + "x": 1128, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 221, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0205", + "type": "rectangle", + "x": 1157, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 222, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0206", + "type": "rectangle", + "x": 1186, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 223, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0207", + "type": "rectangle", + "x": 1215, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 224, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0208", + "type": "rectangle", + "x": 1244, + "y": 444, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 225, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0209", + "type": "rectangle", + "x": 925, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 226, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0210", + "type": "rectangle", + "x": 954, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 227, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0211", + "type": "rectangle", + "x": 983, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 228, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0212", + "type": "rectangle", + "x": 1012, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 229, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0213", + "type": "rectangle", + "x": 1041, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 230, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0214", + "type": "rectangle", + "x": 1070, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 231, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0215", + "type": "rectangle", + "x": 1099, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 232, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0216", + "type": "rectangle", + "x": 1128, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 233, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0217", + "type": "rectangle", + "x": 1157, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 234, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0218", + "type": "rectangle", + "x": 1186, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 235, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0219", + "type": "rectangle", + "x": 1215, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 236, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0220", + "type": "rectangle", + "x": 1244, + "y": 468, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 237, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0221", + "type": "rectangle", + "x": 925, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 238, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0222", + "type": "rectangle", + "x": 954, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 239, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0223", + "type": "rectangle", + "x": 983, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 240, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0224", + "type": "rectangle", + "x": 1012, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 241, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0225", + "type": "rectangle", + "x": 1041, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 242, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0226", + "type": "rectangle", + "x": 1070, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 243, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0227", + "type": "rectangle", + "x": 1099, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 244, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0228", + "type": "rectangle", + "x": 1128, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 245, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0229", + "type": "rectangle", + "x": 1157, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 246, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0230", + "type": "rectangle", + "x": 1186, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 247, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0231", + "type": "rectangle", + "x": 1215, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 248, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0232", + "type": "rectangle", + "x": 1244, + "y": 492, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 249, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0233", + "type": "rectangle", + "x": 925, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 250, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0234", + "type": "rectangle", + "x": 954, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 251, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0235", + "type": "rectangle", + "x": 983, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 252, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0236", + "type": "rectangle", + "x": 1012, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 253, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0237", + "type": "rectangle", + "x": 1041, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 254, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0238", + "type": "rectangle", + "x": 1070, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 255, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0239", + "type": "rectangle", + "x": 1099, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 256, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0240", + "type": "rectangle", + "x": 1128, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 257, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0241", + "type": "rectangle", + "x": 1157, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 258, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0242", + "type": "rectangle", + "x": 1186, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 259, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0243", + "type": "rectangle", + "x": 1215, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 260, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0244", + "type": "rectangle", + "x": 1244, + "y": 516, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 261, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0245", + "type": "rectangle", + "x": 925, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 262, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0246", + "type": "rectangle", + "x": 954, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 263, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0247", + "type": "rectangle", + "x": 983, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 264, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0248", + "type": "rectangle", + "x": 1012, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 265, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0249", + "type": "rectangle", + "x": 1041, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 266, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0250", + "type": "rectangle", + "x": 1070, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 267, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0251", + "type": "rectangle", + "x": 1099, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 268, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0252", + "type": "rectangle", + "x": 1128, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 269, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0253", + "type": "rectangle", + "x": 1157, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 270, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0254", + "type": "rectangle", + "x": 1186, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 271, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0255", + "type": "rectangle", + "x": 1215, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 272, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0256", + "type": "rectangle", + "x": 1244, + "y": 540, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 273, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0257", + "type": "rectangle", + "x": 925, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 274, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0258", + "type": "rectangle", + "x": 954, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 275, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0259", + "type": "rectangle", + "x": 983, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 276, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0260", + "type": "rectangle", + "x": 1012, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 277, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0261", + "type": "rectangle", + "x": 1041, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 278, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0262", + "type": "rectangle", + "x": 1070, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 279, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0263", + "type": "rectangle", + "x": 1099, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 280, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0264", + "type": "rectangle", + "x": 1128, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 281, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0265", + "type": "rectangle", + "x": 1157, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#9c6849", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 282, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0266", + "type": "rectangle", + "x": 1186, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e5ba92", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 283, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0267", + "type": "rectangle", + "x": 1215, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#e9dfcf", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 284, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0268", + "type": "rectangle", + "x": 1244, + "y": 564, + "width": 24, + "height": 18, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "#d7b99f", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 285, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0269", + "type": "text", + "x": 0, + "y": 710, + "width": 1200, + "height": 24.0, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 286, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Layout studies, not browser screenshots.", + "originalText": "Layout studies, not browser screenshots.", + "fontSize": 16, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + } + ], + "appState": { + "viewBackgroundColor": "#fafafa" + }, + "files": {} +} diff --git a/design/product/fixtures/product-page.svg b/design/product/fixtures/product-page.svg new file mode 100644 index 0000000..cc27c3e --- /dev/null +++ b/design/product/fixtures/product-page.svg @@ -0,0 +1,22 @@ + + + + +OpenPaperDesign +OpenPaperDesign +CanvasFeaturesGitHub ↗ + +A canvas for everything. +Map a system, design a screen, think on a whiteboardor lay out a banner. One canvas for all of it. +Download free ↓ +No subscription. No account needed. +Codex · Claude Code · Local MCP + +OpenPaperDesign + +A canvas foreverything. +Map a system. Design a screen.Think on a whiteboard.Or lay out a banner. +Explore the canvas ↓ +Free. Open source. Yours. + + \ No newline at end of file diff --git a/design/product/fixtures/whiteboard.excalidraw b/design/product/fixtures/whiteboard.excalidraw new file mode 100644 index 0000000..5bac156 --- /dev/null +++ b/design/product/fixtures/whiteboard.excalidraw @@ -0,0 +1,478 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "OpenPaperDesign public product fixture", + "elements": [ + { + "id": "item0000", + "type": "text", + "x": 0, + "y": 0, + "width": 1100, + "height": 54.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 17, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "A little room for the unfinished idea.", + "originalText": "A little room for the unfinished idea.", + "fontSize": 36, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0001", + "type": "text", + "x": 0, + "y": 65, + "width": 1000, + "height": 25.5, + "angle": 0, + "strokeColor": "#626262", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 18, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Notes, sketches and decisions. All still editable.", + "originalText": "Notes, sketches and decisions. All still editable.", + "fontSize": 17, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0002", + "type": "stickynote", + "x": 0, + "y": 145, + "width": 190, + "height": 150, + "angle": 0, + "strokeColor": "#74644f", + "backgroundColor": "#ffdf6b", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 19, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0003", + "type": "text", + "x": 15, + "y": 163, + "width": 160, + "height": 27.0, + "angle": 0, + "strokeColor": "#30271f", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 20, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "What are we\ntrying to change?", + "originalText": "What are we\ntrying to change?", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25, + "containerId": "item0002" + }, + { + "id": "item0004", + "type": "stickynote", + "x": 280, + "y": 145, + "width": 190, + "height": 150, + "angle": 0, + "strokeColor": "#74644f", + "backgroundColor": "#ffb8a8", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 21, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0005", + "type": "text", + "x": 295, + "y": 163, + "width": 160, + "height": 27.0, + "angle": 0, + "strokeColor": "#30271f", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 22, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Keep the useful\nparts we have.", + "originalText": "Keep the useful\nparts we have.", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25, + "containerId": "item0004" + }, + { + "id": "item0006", + "type": "stickynote", + "x": 560, + "y": 145, + "width": 190, + "height": 150, + "angle": 0, + "strokeColor": "#74644f", + "backgroundColor": "#b5e6a2", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 23, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0007", + "type": "text", + "x": 575, + "y": 163, + "width": 160, + "height": 27.0, + "angle": 0, + "strokeColor": "#30271f", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 24, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Draw it before\nwe build it.", + "originalText": "Draw it before\nwe build it.", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25, + "containerId": "item0006" + }, + { + "id": "item0008", + "type": "stickynote", + "x": 0, + "y": 365, + "width": 190, + "height": 150, + "angle": 0, + "strokeColor": "#74644f", + "backgroundColor": "#a8d8ff", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 25, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0009", + "type": "text", + "x": 15, + "y": 383, + "width": 160, + "height": 27.0, + "angle": 0, + "strokeColor": "#30271f", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 26, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "What data\ndo we need?", + "originalText": "What data\ndo we need?", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25, + "containerId": "item0008" + }, + { + "id": "item0010", + "type": "stickynote", + "x": 280, + "y": 365, + "width": 190, + "height": 150, + "angle": 0, + "strokeColor": "#74644f", + "backgroundColor": "#d7c2ff", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 27, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0011", + "type": "text", + "x": 295, + "y": 383, + "width": 160, + "height": 27.0, + "angle": 0, + "strokeColor": "#30271f", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 28, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Review it\ntogether.", + "originalText": "Review it\ntogether.", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25, + "containerId": "item0010" + }, + { + "id": "item0012", + "type": "stickynote", + "x": 560, + "y": 365, + "width": 190, + "height": 150, + "angle": 0, + "strokeColor": "#74644f", + "backgroundColor": "#e8e8ec", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 29, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0013", + "type": "text", + "x": 575, + "y": 383, + "width": 160, + "height": 27.0, + "angle": 0, + "strokeColor": "#30271f", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 30, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "Next step:\ntry a small slice.", + "originalText": "Next step:\ntry a small slice.", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25, + "containerId": "item0012" + }, + { + "id": "item0014", + "type": "ellipse", + "x": 900, + "y": 170, + "width": 200, + "height": 120, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 1.4, + "opacity": 100, + "seed": 31, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0015", + "type": "text", + "x": 927, + "y": 210, + "width": 160, + "height": 27.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 32, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "rough idea", + "originalText": "rough idea", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0016", + "type": "rectangle", + "x": 885, + "y": 415, + "width": 230, + "height": 115, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 1.2, + "opacity": 100, + "seed": 33, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [] + }, + { + "id": "item0017", + "type": "text", + "x": 905, + "y": 455, + "width": 220, + "height": 27.0, + "angle": 0, + "strokeColor": "#202020", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 34, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "text": "a clearer direction", + "originalText": "a clearer direction", + "fontSize": 18, + "fontFamily": 2, + "textAlign": "left", + "verticalAlign": "top", + "lineHeight": 1.25 + }, + { + "id": "item0018", + "type": "arrow", + "x": 1000, + "y": 300, + "width": 1, + "height": 100, + "angle": 0, + "strokeColor": "#4265a0", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 1.5, + "roughness": 0, + "opacity": 100, + "seed": 35, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "groupIds": [], + "points": [ + [ + 0, + 0 + ], + [ + 0, + 100 + ] + ], + "endArrowhead": "arrow" + } + ], + "appState": { + "viewBackgroundColor": "#fafafa" + }, + "files": {} +} diff --git a/design/product/identity/README.md b/design/product/identity/README.md new file mode 100644 index 0000000..9eb8776 --- /dev/null +++ b/design/product/identity/README.md @@ -0,0 +1,19 @@ +# Identity exploration · 2026-10-28 + +Owner rejected the boxed canvas/plus icon. Three original SVG candidates explore open sheets, +a folded sheet and an open canvas. A was recommended provisionally before that round was rejected. Nothing copies the Paper +or Sketch mark. Observed primary references: https://paper.design/ (blocky P, sparse), +https://sketch.rasmic.xyz/ (selection outline with a freehand stroke), +https://penpot.app/media-kit (one-colour outlined tool metaphor), +https://www.sketch.com/about-us/ (compact gem silhouette). These are comparison references, +not redistributed source assets. Check 16/24/32px, inverse and native app use before selecting. + +This companion is governed by the root DESIGN.md. It does not replace it. + +Round 1 was rejected by the owner. Round 2 (forms, gesture, anchor) is a new original vector exploration, with Opus 5.5 High sparring. Same chosen geometry must apply everywhere; the suggestions are alternatives, not different marks for different hosts. Those studies were alternatives; the owner selection is recorded below. + +Selected by the owner: round-2 option 1 (overlapping forms), 2026-10-28. Replace vermilion +with Arc’IT #4265a0, and use black/white according to theme. Canonical artwork now lives in +assets/brand/openpaperdesign.svg and openpaperdesign-dark.svg. The app mark is one identity +across desktop, plugin and website; the other concepts remain rejected/exploratory history. +Optional renderer: Node + sharp 0.35.5 (development only), run scripts/render-brand-assets.cjs. diff --git a/design/product/identity/anchor-small.svg b/design/product/identity/anchor-small.svg new file mode 100644 index 0000000..4c7e7a1 --- /dev/null +++ b/design/product/identity/anchor-small.svg @@ -0,0 +1 @@ +OpenPaperDesign · 3 · O + ankerpunkt diff --git a/design/product/identity/anchor.svg b/design/product/identity/anchor.svg new file mode 100644 index 0000000..3faff03 --- /dev/null +++ b/design/product/identity/anchor.svg @@ -0,0 +1 @@ +OpenPaperDesign · 3 · O + ankerpunkt diff --git a/design/product/identity/fold.svg b/design/product/identity/fold.svg new file mode 100644 index 0000000..9ae316c --- /dev/null +++ b/design/product/identity/fold.svg @@ -0,0 +1 @@ +OpenPaperDesign · B · Fold diff --git a/design/product/identity/forms-small.svg b/design/product/identity/forms-small.svg new file mode 100644 index 0000000..2437553 --- /dev/null +++ b/design/product/identity/forms-small.svg @@ -0,0 +1 @@ +OpenPaperDesign · 1 · Former diff --git a/design/product/identity/forms.svg b/design/product/identity/forms.svg new file mode 100644 index 0000000..a96b65f --- /dev/null +++ b/design/product/identity/forms.svg @@ -0,0 +1 @@ +OpenPaperDesign · 1 · Former diff --git a/design/product/identity/gesture-small.svg b/design/product/identity/gesture-small.svg new file mode 100644 index 0000000..0ceff01 --- /dev/null +++ b/design/product/identity/gesture-small.svg @@ -0,0 +1 @@ +OpenPaperDesign · 2 · Tegnestreg diff --git a/design/product/identity/gesture.svg b/design/product/identity/gesture.svg new file mode 100644 index 0000000..7c60160 --- /dev/null +++ b/design/product/identity/gesture.svg @@ -0,0 +1 @@ +OpenPaperDesign · 2 · Tegnestreg diff --git a/design/product/identity/index.html b/design/product/identity/index.html new file mode 100644 index 0000000..8eb52c1 --- /dev/null +++ b/design/product/identity/index.html @@ -0,0 +1 @@ +OpenPaperDesign · valgt mærke

OpenPaperDesign.

Den valgte retning: overlappende canvas-former, nu med Arc’IT-blå #4265a0 og sort/hvid efter tema. Samme mærke i app, plugin og på websitet.

LysOpenPaperDesigns valgte mærke på lys baggrund
OpenPaperDesign
App-ikon32 pixel24 pixel16 pixel
MørkOpenPaperDesigns valgte mærke på mørk baggrund
OpenPaperDesign
App-ikon32 pixel24 pixel16 pixel
\ No newline at end of file diff --git a/design/product/identity/open-canvas.svg b/design/product/identity/open-canvas.svg new file mode 100644 index 0000000..f1ee6df --- /dev/null +++ b/design/product/identity/open-canvas.svg @@ -0,0 +1 @@ +OpenPaperDesign · C · Open canvas diff --git a/design/product/identity/open-sheets.svg b/design/product/identity/open-sheets.svg new file mode 100644 index 0000000..9e09c5a --- /dev/null +++ b/design/product/identity/open-sheets.svg @@ -0,0 +1 @@ +OpenPaperDesign · A · Open sheets diff --git a/design/product/identity/round1-app-icon.svg b/design/product/identity/round1-app-icon.svg new file mode 100644 index 0000000..db82c66 --- /dev/null +++ b/design/product/identity/round1-app-icon.svg @@ -0,0 +1 @@ + diff --git a/design/product/identity/round1.html b/design/product/identity/round1.html new file mode 100644 index 0000000..9f85124 --- /dev/null +++ b/design/product/identity/round1.html @@ -0,0 +1,3 @@ +OpenPaperDesign · ikonretninger

Et tydeligere OpenPaperDesign-mærke.

Originale vektorforslag undersøgt i store og små størrelser. Samme mærke i desktop, plugin og på produktsiden. Fokus: en rolig silhuet, få detaljer og tydelig tilknytning til papir og canvas.

A · Open sheets

To åbne papirflader. Min anbefaling: tydeligt, enkelt og anvendeligt i både app og plugin.

OpenPaperDesign
App · 24px · 16px
OpenPaperDesign

B · Fold

En mere kompakt papirform, med en lille fold og en stærk silhuet.

OpenPaperDesign
App · 24px · 16px
OpenPaperDesign

C · Open canvas

En åben flade og et frit forløb. Mere abstrakt og mindre dokumentagtigt.

OpenPaperDesign
App · 24px · 16px
OpenPaperDesign
\ No newline at end of file diff --git a/design/product/identity/round2.html b/design/product/identity/round2.html new file mode 100644 index 0000000..54e688e --- /dev/null +++ b/design/product/identity/round2.html @@ -0,0 +1,3 @@ +OpenPaperDesign · nye ikonretninger

Mere canvas. Mindre dokument.

Tre nye retninger efter din feedback, med sparring fra Opus 5.5 High. Formerne kan stå alene, og samme valgte mærke vil gå igen i appen, pluginet og på produktsiden.

1 · Former

To overlappende canvas-former. Den ene går ud over den anden.

OpenPaperDesign
App · 24px · 16px
OpenPaperDesign

2 · Tegnestreg

En fri streg omkring en form. Mere tegneværktøj og mindre filikon.

OpenPaperDesign
App · 24px · 16px
OpenPaperDesign

3 · O + ankerpunkt

Et O fra Open med et markeret punkt, som på en vektorkurve.

OpenPaperDesign
App · 24px · 16px
OpenPaperDesign

Jeg hælder til 1 for det direkte designværktøjsudtryk, eller 3 for den stærkeste silhuet som lille app-ikon.

\ No newline at end of file diff --git a/design/product/identity/selected-app.png b/design/product/identity/selected-app.png new file mode 100644 index 0000000..6e30a80 Binary files /dev/null and b/design/product/identity/selected-app.png differ diff --git a/design/product/identity/selected-dark.svg b/design/product/identity/selected-dark.svg new file mode 100644 index 0000000..c8bb07f --- /dev/null +++ b/design/product/identity/selected-dark.svg @@ -0,0 +1 @@ +OpenPaperDesign diff --git a/design/product/identity/selected-light.svg b/design/product/identity/selected-light.svg new file mode 100644 index 0000000..71eb3d1 --- /dev/null +++ b/design/product/identity/selected-light.svg @@ -0,0 +1 @@ +OpenPaperDesign diff --git a/docs/openpaperdesign/desktop.md b/docs/openpaperdesign/desktop.md new file mode 100644 index 0000000..bf6df44 --- /dev/null +++ b/docs/openpaperdesign/desktop.md @@ -0,0 +1,70 @@ +# OpenPaperDesign desktop preview + +The desktop package runs the existing native Rust editor, without Codex or a plugin. The main-branch packaging prototypes do not yet include the new UI/UX shown on the product page. Those images were rendered with the frozen PR #9 source at `1b04c31`, where the shared native workspace also has the new UI. Public desktop release is held until packages include that qualified UI and pass the normal app journey. Both hosts reuse the editor and project format; their access contracts differ. + +## Current status + +- **macOS / Apple Silicon:** local release builds and ad-hoc-signed bundles on an Apple M4. The newer UI integration successfully opens Home, opens a real project, accepts an edit, saves it and reopens that saved change after quitting. Native MCP was observed on this exact app process. During an earlier blocked launch, process sampling showed synchronous project enumeration under Documents before the event loop. The new entrypoint defaults to local application data instead. This fixes the observed startup block without granting blanket Documents access or moving existing projects. The owner's earlier unspecified error dialog remains unreproduced. Actual downloaded/quarantined first launch and notarization are separate and unqualified. +- **Windows / x64:** The corrected x64 build and ZIP packaging passed on the GitHub Windows runner at `04c738e` using Bash to gate both CLI commands. The static-CRT Windows package also passed at `61d94cc`. The GUI-subsystem app does not open a console from Explorer; CLI calls retain redirected output and attach to a parent console when needed. First launch and editing on a real Windows desktop remain unqualified. +- **Linux / x64:** The corrected x64 build, CLI smoke checks and tarball packaging passed on the GitHub Ubuntu runner at `04c738e`. No physical-machine pass is claimed. + +Use the release's own status and checksums. A workflow archive is not a claim that an installer was accepted by Windows/macOS security checks. The source-only v0.1.0 release is still labelled source. No automatic updater, signing credential or security setting is installed by this work. + +## Library and existing files + +The `openpaperdesign` entrypoint uses the platform's local application-data folder: +`~/Library/Application Support/OpenPaperDesign/projects` on macOS, `%LOCALAPPDATA%/OpenPaperDesign/projects` on Windows, and the XDG data location on Linux. An explicit `PAPER_RS_HOME` selects another library. Existing Documents projects, the upstream library and the plugin library are not moved or silently adopted. Native projects retain the upstream folder/JSON/assets format and `.paper-rs` compatibility. + +Desktop launches keep private local diagnostic files under the adjacent `OpenPaperDesign/logs` directory. The ordinary log is bounded to 1 MiB plus a truncation marker; an adjacent `.error` file retains the latest error (up to 64 KiB) independently of that quota. Nothing is uploaded. CLI calls use their normal stderr instead. Logs may include project paths or error content: inspect them before sharing. + + +Do not edit the same project in the native app and the Codex browser plugin at the same time. The plugin's checked save/binding path does not protect against an independent native writer. + +## Build from source + +Use Rust 1.95 or later and the platform's native compiler/linker: + +```sh +cargo build --release --locked --bin openpaperdesign --bin paper-rs-mcp +./target/release/openpaperdesign +``` + +On Windows, use `target/release/openpaperdesign.exe`. The workflow statically links the MSVC C runtime. A manual build without that flag may require the Microsoft Visual C++ runtime; the GUI executable does not create a console when launched from Explorer. On Linux, provide the normal GTK3, X11/Wayland, libxkbcommon and Vulkan/GL runtime libraries. The Ubuntu 24.04 package targets glibc 2.39 or newer. The upstream [build guide](../upstream-README.md#build-and-run) describes those dependencies. A source build does not require an account or an AI agent. + +The old `paper-rs` binary remains available for upstream-compatible commands. The thin distribution entrypoint reuses that CLI rather than adding a second editor. + +## Package an existing build + +```sh +python3 scripts/package-desktop.py --platform macos --arch arm64 --bin-dir target/release --output dist +``` + +Use `--platform windows --arch x64` or `--platform linux --arch x64` **on the matching build host** with binaries built for that target. The script refuses missing binaries. The Mac packager uses the repository icon and macOS `iconutil`, `codesign` and `ditto`; it only applies an ad-hoc signature. Archives contain both binaries, project/font notices, BUILD.json provenance and an adjacent SHA256 file. Existing archive/checksum names are never overwritten. Use a fresh output directory or move an earlier build aside deliberately. Concurrent attempts for the same target fail without replacing the winning package. Publication uses same-volume hard links (APFS, ext4 or NTFS); unsupported volumes fail safely. A process killed between publishing the archive and its checksum may leave an incomplete pair, which release-manifest validation refuses. Inspect/move that pair before retrying; do not publish it. BUILD.json marks a dirty source tree honestly. + +`Desktop packages` builds a universal macOS archive (Intel and Apple Silicon slices), Windows x64 ZIP and Linux x64 tarball. It reuses a pinned, OS/target-specific Rust cache without skipping builds or tests. It tests the native library, app entrypoint and project behavior in the release profile, restores only generated screenshot outputs, then refuses dirty distribution sources. Packages contain input binary hashes and source/component provenance; adjacent checksums describe the final archives. Candidate branches and PRs retain exact archives for review. A `v*` tag reachable from main can create a draft prerelease only after every platform succeeds and all archive checksums/provenance agree. Existing releases are never replaced. Publication is deliberate; the workflow does not deploy the site. No automatic updates are included. Public release assets must come from the reviewed exact build; do not rename another platform's executable. + +## Optional AI agents + +The native MCP server is on by default. The running app exposes the upstream local MCP endpoint at `http://127.0.0.1:29980/mcp`. Configure a compatible MCP client to use that URL, or use the bundled `paper-rs-mcp` stdio bridge. The app must be running. `PAPER_RS_MCP_PORT` changes the port in **both** app and bridge; `PAPER_RS_MCP=0` disables the app server. + +Sketch/paper-rs uses the same default port. Stop the other app or choose a unique port in both app and bridge, and verify the actual receiving library before editing. A successful connection alone does not establish which app received it. + +The existing upstream [Claude Code and Codex instructions](../upstream-README.md#ai-agents-mcp) are retained. For a Mac bundle, the bridge executable is `OpenPaperDesign.app/Contents/MacOS/paper-rs-mcp`; on other platforms it is beside the app executable. No client configuration is changed by downloading or opening the app. + +This is the **native app's MCP contract**, not the project-bound plugin's workspace/lease contract. The native app exposes its library and local file authoring operations; choose the client access you intend to grant. Loopback/origin checks are not an OS sandbox. + +## Required qualification before a public desktop release + +For each advertised platform, record the exact source revision, binary/archive hashes and actual platform/architecture. Verify normal first launch, project creation, editing, save, quit/reopen, import/export, and an upgrade with existing data retained. Qualify optional MCP on a disposable library. Test actual downloaded security handling rather than a locally copied app alone. Keep code signing/notarization and physical-device passes distinct from a CI build. + +## Upstream selection + +The actual merge base with `michaelshimeles/sketch` is `0b1b91da7caa10abae7cbf75d2ce003822c85fe9` after removing the shallow-clone limitation. Subsequent native changes are mostly the Sketch rename, packaging/updater integration and autosave while covered. Six-colour sticky notes, hand-drawn rendering, frames/components, exports and MCP already exist in this fork. + +The upstream autosave fix `66893186714a0bb5f16f1e5b9d93e69b5b37a86d` is a relevant candidate: it starts autosave from the event loop when an occluded window does not render. It is isolated on a separate port branch based on Factory PR #9, preserving this fork's `autosave_due_ui` guard. A no-frame, timed save/reopen regression passes locally. The integration candidate records its exact port revision; PR21 itself does not silently adopt Factory's whole branch. Do not merge the whole upstream branch, rename the fork, adopt upstream's signing/update keys or copy its analytics. + +macOS bundle layout was informed by Rasmic's release script. This implementation is a small packager without Sparkle, downloaded frameworks, an update feed or signing credentials. Core editor and bundled licence attribution are retained. + +## Apple notarization + +The current universal preview remains ad-hoc signed. See the [notarization runbook](notarization.md) for owner prerequisites, the signing/submission/stapling sequence and the final downloaded-archive checks. Developer ID signing and an accepted Apple submission have not been completed for OpenPaperDesign. diff --git a/docs/openpaperdesign/notarization.md b/docs/openpaperdesign/notarization.md new file mode 100644 index 0000000..c6e3a4e --- /dev/null +++ b/docs/openpaperdesign/notarization.md @@ -0,0 +1,100 @@ +# Apple signing and notarization + +The current OpenPaperDesign Mac preview is **ad-hoc signed, not Apple notarized**. +The universal archive has Intel and Apple Silicon executables; that is separate from +Apple trusting the app on a newly downloaded first launch. Do not label the website +“notarized” until the final distributed archive has passed the checks below. + +## What is needed from the owner + +- An active Apple Developer Program membership (Apple currently lists US$99 per year, + or the local equivalent). +- A **Developer ID Application** signing identity and its private key in the build + Mac's Keychain. An Apple Development identity or an ad-hoc signature is insufficient. +- A notarytool Keychain profile, created by the owner using Apple's supported + authentication. Keep passwords, private keys and API keys out of the repository, + logs, screenshots and command history. The release workflow currently has no signing + secrets and must continue to build honest ad-hoc previews without them. + +On 28 October 2026 this machine reported **zero valid code-signing identities**. +That says nothing about whether the owner has an Apple Developer account. It prevents +completing the actual Developer ID signing and submission here today. + +## Release procedure once those prerequisites exist + +Use a clean, reviewed universal build from the existing Desktop packages workflow. +Keep the original CI archive and its checksum. Work on a copy in a new local temporary +directory outside synced Documents; some file providers add Finder metadata that +invalidates signatures. Never remove quarantine or weaken Gatekeeper to qualify a build. + +1. Verify the original archive's SHA256 and `Contents/Resources/BUILD.json` revision, + component revisions, platform and architectures. Extract with `ditto`. Check both + `Contents/MacOS/openpaperdesign` and `Contents/MacOS/paper-rs-mcp` with + `lipo -verify_arch x86_64 arm64`. Preserve the bundled notices and resources. +2. If signing metadata in BUILD.json needs updating, do it **before** signing. Record + the original archive hash and new signing process in an external release receipt. + Do not change application contents after notarization; doing so invalidates the seal. +3. Sign the inner bridge executable, then the outer app with the selected Developer ID + Application identity, a secure timestamp and Hardened Runtime. Check any newly added + nested executables/frameworks inside out as well. Do not use `--deep` as a substitute + for signing nested code. There is currently no need for debugging entitlements such + as `get-task-allow`. + + ```sh + codesign --force --options runtime --timestamp --sign "$OPD_SIGNING_IDENTITY" \ + "$OPD_APP/Contents/MacOS/paper-rs-mcp" + codesign --force --options runtime --timestamp --sign "$OPD_SIGNING_IDENTITY" \ + "$OPD_APP" + codesign --verify --deep --strict --verbose=2 "$OPD_APP" + ``` + +4. Create a fresh ZIP of the signed app with `ditto -c -k --keepParent`. Submit it: + + ```sh + xcrun notarytool submit "$OPD_SUBMISSION_ZIP" \ + --keychain-profile "$OPD_NOTARY_PROFILE" --wait --output-format json + ``` + + Require **Accepted**, save the submission ID, and inspect Apple's log on any other + outcome. Signing alone, or a successfully uploaded submission, is not acceptance. +5. Attach Apple's ticket to the app and verify it: + + ```sh + xcrun stapler staple "$OPD_APP" + xcrun stapler validate "$OPD_APP" + codesign --verify --deep --strict --verbose=2 "$OPD_APP" + spctl --assess --type execute --verbose=2 "$OPD_APP" + ``` + + ZIP files cannot themselves be stapled. Recreate the final ZIP **after** stapling + the app. Compute a new SHA256; do not reuse the CI archive's checksum. +6. Extract that exact final ZIP into a second clean directory. Re-run signature, + ticket and Gatekeeper assessments there. Test a real browser download on Mac, + including first launch, Home, editing, save, quit/reopen and optional local MCP. + Keep Intel runtime testing distinct from checking universal Mach-O slices on an + Apple Silicon Mac. Do not execute unrelated third-party code as part of this test. +7. Publish only the qualified final archive and matching checksum, together with its + original source/component provenance, signing identity/team, submission receipt + and actual platform test results. Update the release manifest and install guide + to match that release. Do not silently replace a published artifact. The current + draft-release workflow and site deployment are separate steps. + +The commands above are a runbook; they have **not** been submitted using an owner +identity. CI signing automation should be added only once the owner chooses how its +certificate/profile is managed. No Apple enrollment, payment, new credential, updater +or expanded secret access has been created by this work. + +## Reference check + +Rasmic's current Sketch v0.1.0 Mac ZIP was inspected on 28 October 2026 without launching +it. It has a Developer ID Application signature, Hardened Runtime and a stapled ticket; +`stapler validate` passed and Gatekeeper reported `source=Notarized Developer ID`. +Its release script signs nested code, submits to notarytool and staples accepted results. +This is useful packaging guidance, not an identity or credential to reuse. + +- [Apple: notarizing macOS software](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution) +- [Apple: customizing the notarization workflow](https://developer.apple.com/documentation/security/customizing-the-notarization-workflow) +- [Apple: resolving notarization issues](https://developer.apple.com/documentation/security/resolving-common-notarization-issues) +- [Apple Developer Program](https://developer.apple.com/programs/) +- [Sketch v0.1.0 release](https://github.com/michaelshimeles/sketch/releases/tag/v0.1.0) +- [Sketch Mac release script](https://github.com/michaelshimeles/sketch/blob/main/scripts/release-macos.sh) diff --git a/docs/upstream-README.md b/docs/upstream-README.md new file mode 100644 index 0000000..75995d3 --- /dev/null +++ b/docs/upstream-README.md @@ -0,0 +1,355 @@ +# paper-rs + +A native Rust infinite canvas for planning, working and sketching. It is modeled on the +behavior of [Paper](https://paper.design) and built clean-room on `wgpu` + `winit` + `egui`. + +![paper-rs main window](docs/screenshots/app_demo.png) + +A local-first design and whiteboard tool. Projects are folders of pages; each page is an +infinite canvas with frames, groups, components and instances, auto layout and constraints, +vector paths and booleans, gradients and image fills, rich text, smart guides, and +pressure-sensitive ink — plus a whiteboard mode: hand-drawn (rough.js-style) shapes, arrows that +bind to shapes and re-route, elbow connectors, sticky notes, a laser pointer and a shape +library. Everything autosaves, and exports to PNG, SVG and PDF; Excalidraw files and SVG import. + +![Figma-core features](docs/screenshots/milestone3_figma_core.png) + +> **Clean-room note.** `analysis/PAPER_ARCHITECTURE.md` documents how Paper Desktop 0.5.15 is +> put together, from a licensed install. The Electron package turned out to be a thin shell +> around the hosted web editor. paper-rs re-implements *behavior* only. No Paper code, +> shaders, icons, fonts or assets are used or committed. The UI and canvas text use +> [Inter](https://rsms.me/inter/) 4.1 (SIL OFL 1.1), bundled in `assets/fonts/`. + +## Reverse-engineered origin + +paper-rs is an independent, clean-room-style reimplementation. It was reverse engineered from +[Paper](https://paper.design) by studying the behavior of the Paper desktop app with +[REA](https://github.com/morluto/rea) and Claude Code. It is not affiliated with or endorsed by +Paper. No original Paper code or assets are included in this repository. + +## Projects + +paper-rs opens on a **home screen** of your projects (recents grid with thumbnails, New, Import, +and Rename / Duplicate / Move to Trash from each card's ⋯ menu or right-click; restore from +Trash). Each project is a plain folder in `~/Documents/paper-rs//` holding `project.json` +and one JSON file per page in `pages/`. Work is autosaved about a second after each change with +crash-safe atomic writes; files are versioned and older versions migrate on load. Set +`PAPER_RS_HOME` to use another folder. + +A project has any number of **pages** (Pages list at the top of the left sidebar: + to add, +double-click to rename, right-click to duplicate or delete, drag to reorder). Each page is its +own infinite canvas with its own undo history and view. Copy and paste works between pages and +between projects. + +## Build and run + +You need Rust 1.95 or newer (`rust-version` in `Cargo.toml`; the latest stable is recommended). Install it with [rustup](https://rustup.rs). + +```sh +cargo build --release +./target/release/paper-rs # projects home screen +./target/release/paper-rs --demo # sample planning board (not saved) +./target/release/paper-rs ~/Documents/paper-rs/Plan # open a project folder +./target/release/paper-rs board.paper-rs # open a paper-rs 0.1 document +``` + +### macOS + +```sh +xcode-select --install # command line tools (linker) +curl https://sh.rustup.rs -sSf | sh +cargo build --release && ./target/release/paper-rs --demo +``` + +It uses Metal through wgpu. Shortcuts use ⌘, and trackpad pinch zooms. The macOS target is +type-checked in CI-style (`cargo check --target aarch64-apple-darwin`) but has not been run on +Mac hardware yet (see Known gaps). + +### Linux + +```sh +# Debian/Ubuntu runtime libraries (usually already present on desktops) +sudo apt install libxkbcommon-x11-0 libxcb-xkb1 libvulkan1 mesa-vulkan-drivers +cargo build --release && ./target/release/paper-rs --demo +``` + +It runs on X11 and Wayland through Vulkan, or GL as a fallback. Shortcuts use Ctrl. File +dialogs use the XDG desktop portal. + +### Headless (servers, CI) + +No display is needed. wgpu renders offscreen, through Mesa lavapipe when there is no GPU: + +```sh +paper-rs --screenshot ui.png --demo --size 1440x900 --scale 2 # full UI +paper-rs --screenshot home.png --home # projects home screen +paper-rs --export-png out.png PROJECT_DIR --page 0 --scale 2 [--transparent] +paper-rs --export-svg out.svg PROJECT_DIR +paper-rs --export-pdf out.pdf PROJECT_DIR --frames # one PDF page per frame +paper-rs --export out_dir PROJECT_DIR --format png --scope frames # a folder of PNGs +paper-rs --import board.excalidraw # new project, prints its folder +``` + +## AI agents (MCP) + +![A sign-in screen written by an agent through write_html](docs/screenshots/milestone7_mcp.png) + +paper-rs serves the [Model Context Protocol](https://modelcontextprotocol.io) while it runs, so +agents such as Claude Code and Codex can read and design on the open canvas, the way Paper's +own MCP server works (behaviour notes: `analysis/paper-mcp.md`). Every tool call runs through +the editor like a user edit: one undo step per call, and the window repaints immediately. + +* **Streamable HTTP** (built into the app): `http://127.0.0.1:29980/mcp`. Paper uses 29979, so + both can run. Loopback only; requests from browsers (with an `Origin` header) and foreign + `Host` headers are rejected. `PAPER_RS_MCP_PORT=NNNN` changes the port; `PAPER_RS_MCP=0` + turns the server off. +* **stdio**: `paper-rs-mcp` (built next to `paper-rs`) relays stdio to the running app. It + lists the tools even when the app is closed, and tells the agent to start paper-rs if a + call can't reach it. + +### Claude Code + +```sh +# HTTP (the app must be running) +claude mcp add --transport http --scope user paper-rs http://127.0.0.1:29980/mcp +# or stdio through the bridge +claude mcp add --scope user paper-rs -- /path/to/paper-rs/target/release/paper-rs-mcp +``` + +### Codex + +```sh +# HTTP +codex mcp add paper-rs --url http://127.0.0.1:29980/mcp +# or stdio +codex mcp add paper-rs -- /path/to/paper-rs/target/release/paper-rs-mcp +``` + +which writes this to `~/.codex/config.toml` (either form works): + +```toml +[mcp_servers.paper-rs] +url = "http://127.0.0.1:29980/mcp" +# command = "/path/to/paper-rs/target/release/paper-rs-mcp" +``` + +Then ask the agent something like *"In paper-rs, create a 390×844 mobile artboard and design a +sign-in screen"*, or *"draw a flowchart of our deploy process on a new page, hand-drawn +style"*. + +### Tools + +Names and arguments follow Paper's MCP server where paper-rs has an equivalent, so prompts +written for Paper work here too. + +| Group | Tools | +|---|---| +| Projects & pages | `list_resources`, `open_file`, `create_file`, `create_page`, `rename_pages` | +| Read | `get_basic_info`, `get_selection`, `get_node_info`, `get_children`, `get_tree_summary`, `get_computed_styles`, `get_jsx`, `find_nodes`, `get_fill_image`, `get_font_family_info`, `get_guide` | +| See | `get_screenshot` (PNG image content of a node or the page), `export` (PNG/SVG/PDF files), `export_combined_pdf` | +| Write | `create_artboard`, `write_html`, `update_styles`, `set_text_content`, `rename_nodes`, `duplicate_nodes`, `move_nodes`, `delete_nodes`, `finish_working_on_nodes` (saves) | +| paper-rs extras | `create_shape` (rectangles, ellipses, diamonds, stars, stickies, text, lines, arrows bound to shapes), `select_nodes`, `undo`, `redo` | + +`write_html` takes HTML with inline styles: flexbox (`display:flex`, direction, gap, padding, +alignment, `flex-grow`, wrap) and block stacking map onto auto layout with CSS's stretch rules; +text gets size, weight, italic, color, line height, letter spacing, alignment and rich-text +runs (``, ``, ``); boxes get backgrounds (colors, gradients, images), +borders, radii, opacity and blend modes; `` takes local paths or `data:` URIs and `` +is imported as vectors. `update_styles` takes the same CSS as JSON, plus the hand-drawn keys +`sketch`, `roughness`, `fillStyle`, `strokeStyle`. Node ids look like `3fa9c1:14` (page and +node); `pageId` lets an agent work on another page without switching the user's view. + +## Tests + +```sh +cargo test # ~140 tests: unit, scenario, headless GPU, CLI +PAPER_RS_REQUIRE_GPU=1 cargo test # fail (instead of skip) when no GPU adapter exists +cargo test --release --test perf -- --nocapture # 10k-element frame timings +``` + +* **Unit tests** cover geometry, vector paths, the rough.js port (compared with real + rough.js 4.6.4 output), auto layout and constraints, booleans, rich text spans, the elbow + router, formats and migrations, atomic writes, the project store, PDF structure, Excalidraw + and SVG import, the palette's fuzzy matching and the shortcut table. +* **Scenario tests** drive the editor with synthetic pointer and keyboard events: + `src/editor/tests.rs` (core editing), `tests/projects.rs` (projects, autosave, pages), + `tests/figma.rs` (groups, components, auto layout, constraints, pen, booleans, rich text, + guides), `tests/excalidraw.rs` (hand-drawn style, arrows, quick-connect, stickies, laser, + library), `tests/workflow.rs` (layers drag, palette, shortcuts), `tests/io.rs` (export, + import, clipboard, CLI), `tests/mcp.rs` (every MCP tool on a headless session, then an rmcp + client over HTTP and through the `paper-rs-mcp` stdio bridge). +* **Headless GPU tests** render through wgpu, assert pixels and write the screenshots below. + One rasterizes the SVG export with resvg and compares it with the GPU render; one renders + the PDF export with pdftoppm when installed. + +| Milestone | Screenshot(s) in `docs/screenshots/` | +|---|---| +| 1. Local projects | `milestone1_home.png`, `milestone1_trash.png` | +| 2. Pages | `milestone2_pages.png` | +| 3. Figma core | `milestone3_figma_core.png`, `milestone3_canvas.png` | +| 4. Excalidraw core | `milestone4_whiteboard.png` | +| 5. Inspector, palette, shortcuts | `milestone5_inspector.png`, `milestone5_palette.png`, `milestone5_shortcuts.png` | +| 6. Import / export | `milestone6_pdf_page.png` (PDF rasterized by pdftoppm), `milestone6_excalidraw_import.png`, `milestone6_svg_import.png` | +| 7. MCP server | `milestone7_mcp.png` (a screen written through `write_html`, captured with `get_screenshot`), `milestone7_mcp_live_xvfb.png` (the live app after Claude Code drew into it over the stdio bridge) | +| paper-rs 0.1 | `m1_*` … `m6_*`, `app_demo.png`, `x11_window_xvfb.png` | + +## Using it + +![Whiteboard features](docs/screenshots/milestone4_whiteboard.png) + +The tools sit in the floating bar at the bottom of the canvas. Select a layer to edit it in +the **Design** panel on the right. The **Layers** panel on the left shows the tree: click to +select, Shift-click to add, double-click to rename, and use 👁 / 🔒 to hide or lock. The +**Up/Down** buttons reorder. + +* Draw a frame around existing shapes and it adopts them. Shapes drawn inside a frame become + its children, and dragging a layer into or out of a frame re-parents it. +* Top-level frames that contain content act as canvas regions. Drag from their body to + marquee-select inside them, and drag the frame by its name label. +* The pencil keeps a constant on-screen width at any zoom. With nothing selected, set width, + smoothing and color in the Design panel. +* Hold ⌘/Ctrl while dragging to suspend snapping. + +### Keyboard shortcuts + +⌘ on macOS, Ctrl on Linux and Windows. Press `?` or ⌘/ in the app for the full grouped list +(100+ bindings), or ⌘K to search every command. + +| Keys | Action | Keys | Action | +|---|---|---|---| +| V / 1 | Select | ⌘K | Command palette | +| H, or hold Space | Hand (pan) | ⌘Z / ⌘⇧Z | Undo / redo | +| F or A | Frame | ⌘G / ⌘⇧G | Group / ungroup | +| R / 2, O / 4, D / 3 | Rectangle, ellipse, diamond | ⌘⌥G | Frame selection | +| ⇧Y, ⇧S | Polygon, star | ⌘⌥K / ⌘⌥B | Create component / detach | +| L / 6, ⇧L / 5 | Line, arrow | ⇧A / ⌥⇧A | Add / remove auto layout | +| P, ⇧P / 7 | Pen, pencil | ⌥A ⌥H ⌥D / ⌥W ⌥V ⌥S | Align left/center/right, top/middle/bottom | +| T / 8, S | Text, sticky note | ⌥⇧H / ⌥⇧V | Distribute spacing | +| K | Laser pointer | ⌥⇧U S I E | Union, subtract, intersect, exclude | +| E / 0 | Eraser | ⌘E | Flatten | +| 9, ⌘⇧K | Place image | ⌘B / ⌘I / ⌘U / ⌘⇧X | Bold, italic, underline, strikethrough | +| Enter / ⇧Enter | Edit (text, path) / select parent | ⌘⌥H | Hand-drawn style | +| Tab / ⇧Tab | Next / previous sibling | ⇧H / ⇧V | Flip | +| ⌥-drag | Duplicate while dragging | ⌘⌥C / ⌘⌥V | Copy / paste properties | +| ⌥ hover | Measure distances | ⌘R | Rename | +| ⌘= / ⌘- / ⇧0 | Zoom in / out / 100% | ⇧1 / ⇧2 | Zoom to fit / selection | +| PageUp / PageDown | Previous / next page | ⌘\\ | Show / hide UI | + +## Feature parity: Paper / Figma / Excalidraw vs paper-rs + +Columns 2–4 say whether each product has the feature (✓) and where the behavior was learned: +Paper from its desktop shell and public docs (`analysis/PAPER_ARCHITECTURE.md`), Figma from +public docs (`analysis/FIGMA_NOTES.md`), Excalidraw from its MIT source +(`analysis/EXCALIDRAW_NOTES.md`). The last column is paper-rs: ✅ done, ⚠️ partial (see +Known gaps), ⏳ planned in a later milestone, ❌ not planned. + +| Feature | Paper | Figma | Excalidraw | paper-rs | +|---|---|---|---|---| +| Local projects, recents grid, trash | (cloud dashboard) | ✓ (cloud) | (single file) | ✅ Folders in `~/Documents/paper-rs`, thumbnails, rename/duplicate/trash/restore | +| Autosave, crash-safe, versioned files | ✓ (cloud) | ✓ (cloud) | ✓ (browser storage) | ✅ 1 s autosave, atomic writes, migration chain (page v1→v3) | +| Pages, infinite canvas per page | ✓ | ✓ | – | ✅ Per-page history and camera, reorder/duplicate/delete | +| Frames with clipping | ✓ | ✓ | ✓ | ✅ ⚠️ clip is rectangular (corner radius not clipped) | +| Groups | ✓ | ✓ | ✓ | ✅ Bounds follow children, click selects group, double-click enters, scale on resize | +| Components, instances, overrides | – | ✓ | (library only) | ✅ Live sync, per-property overrides, reset, detach, go to main | +| Auto layout (direction, gap, padding, hug/fill/fixed, wrap) | ✓ (flexbox) | ✓ | – | ✅ Drag-reorder in flow, space-between, absolute children | +| Constraints | – | ✓ | – | ✅ Left/right/stretch/center/scale per axis, ⌘ to ignore | +| Pen tool, vector point editing | ✓ | ✓ | – | ✅ Corner/smooth points, handles (⌥ breaks), close, delete, toggle | +| Boolean ops (union/subtract/intersect/exclude), flatten | (roadmap) | ✓ | – | ✅ ⚠️ destructive (result is a path, curves flattened) | +| Gradients (linear, radial) | ✓ | ✓ | – | ✅ Multi-stop, angle, radius; ⚠️ no on-canvas gradient handles | +| Image fills | ✓ | ✓ | ✓ (image element) | ✅ Fill/fit/stretch/tile, place image (⌘⇧K), drag-and-drop, stored in project | +| Strokes: width, align, dash, caps, joins | ✓ | ✓ | ✓ | ✅ | +| Corner radius (per corner), polygons, stars | ✓ | ✓ | – | ✅ | +| Opacity, blend modes | ✓ | ✓ | ✓ (opacity) | ✅ ⚠️ canvas renders 5 modes exactly, all 16 exact in SVG | +| Rich text (per-range bold/italic/size/colour) | ✓ | ✓ | – | ✅ Plus underline, strikethrough, line height, letter spacing | +| Align / distribute | ✓ | ✓ | ✓ | ✅ ⌥A/D/W/S/H/V, ⌥⇧H/V | +| Smart guides, spacing, measurement | ✓ | ✓ | – | ✅ Edge/center snap, equal spacing, ⌥-hover distances | +| Hand-drawn (rough) style per element | – | – | ✓ | ✅ rough.js 4.6.4 port, verified against rough.js output; sloppiness, fill styles | +| Bound arrows, elbow arrows, quick shapes, sticky notes | – | (FigJam) | ✓ | ✅ Orbit binding that follows shapes, straight/curved/elbow, 5 heads, diamond, quick-connect, stickies | +| Laser pointer, shape library | – | (FigJam) | ✓ | ✅ Fading laser trail; shared library with starter items | +| Inspector, layers drag-reorder, command palette (⌘K) | ✓ | ✓ | ✓ | ✅ Sectioned inspector, tree drag above/below/into, fuzzy palette over actions, pages, layers, library | +| Export PNG / SVG / PDF of selection, frame, page | ✓ | ✓ | ✓ (PNG/SVG) | ✅ Selection, each frame, page or all pages; vector PDF with gradients, images, blend modes; copy as PNG/SVG | +| Import .excalidraw / SVG, copy-paste across projects | – | ✓ (SVG) | ✓ | ✅ .excalidraw scenes, .excalidrawlib libraries, SVG (paths, gradients, images, text), system clipboard between pages, projects and apps | +| Freehand ink with pressure | – | (pencil) | ✓ | ✅ Smoothing, pressure, eraser | +| MCP server for AI agents | ✓ | ✓ (Dev Mode) | ✓ (community) | ✅ Streamable HTTP in the app + stdio bridge; Paper-compatible tool names, `write_html`, screenshots, whiteboard shapes | +| Multiplayer, cloud sync, plugins | ✓ | ✓ | ✓ | ❌ local-first by design | + +## Known gaps + +* **Paper's document model is HTML/CSS.** paper-rs uses geometric nodes with flexbox-like auto + layout on frames; there are no variables, CSS export or code generation. +* **Rendering approximations.** Frame clipping is a rectangle (scissor), so rounded frames + don't clip their corners. Group opacity multiplies into children instead of compositing the + group. Overlay, color dodge/burn, hard/soft light, difference, exclusion, hue, saturation, + color and luminosity blend as Normal on the canvas (they are exact in SVG export). +* **Booleans are destructive**: the result is a new vector path with flattened curves. + Gradients are edited in the inspector only. +* **Not run on macOS hardware yet.** The macOS build type-checks and uses only portable APIs + (Metal via wgpu, winit, rfd), but it has not been launched on a Mac. On Linux, the real + window was exercised under Xvfb; the screenshot is `x11_window_xvfb.png`. +* **Pen pressure.** winit 0.30 does not expose tablet pressure for pen events that arrive as + mouse input, which is common with macOS and Wacom tablets. Pressure comes from touch/pen + `force` when the platform reports it. Otherwise it is simulated from stroke velocity. +* **Text** uses Inter (regular, bold, italic, bold italic) with no shaping, so there are no + ligatures, complex scripts or RTL, and no font picker. IME preedit is not displayed + (committed text works). +* **No rotation.** Nodes are axis-aligned boxes; rotated imports are baked into paths. +* **Undo granularity.** Document renames from the Design panel are not undoable, and the + selection is not part of history. +* **Performance.** Nodes are tessellated once per zoom octave and cached; unchanged frames + reuse the previous item list and GPU buffers, so panning uploads nothing. With 10k visible + nodes a cached frame takes ~3 ms (release, `tests/perf.rs`); the first frame at a new zoom + octave takes ~140 ms on a CPU. Not tuned for 100k+. +* **PDF**: text is outlined (not selectable); gradient stop transparency uses the first + stop's alpha; tiled image fills are drawn as repeated images. +* **Import**: Excalidraw rotation is baked into paths (text rotation is dropped), bound text + inside shapes stays a separate text layer (except sticky notes), and fonts map to Inter. + SVG patterns, filters, masks and clip paths are not imported. +* **MCP**: one project is open at a time, so `fileId` must name the open project (call + `open_file` to switch; the user's window switches too). No design tokens, comments or AI + image generation (Paper has them). Moving or duplicating layers between pages isn't + supported. Remote `http(s)` images aren't fetched; pass a local path or a `data:` URI. + CSS margin, grid, shadows and transforms are reported back as ignored styles. +* **Linux file dialogs** need an XDG desktop portal. Without one, Save/Open dialogs return + nothing. The headless CLI is unaffected. + +## Project layout + +``` +src/geom.rs vectors, rects, camera +src/vector.rs bezier paths: flatten, hit test, SVG data, shapes, dashes +src/model/ Node / NodeKind / paints / Document arena +src/history.rs transactions, undo/redo +src/layout.rs auto layout, constraints, group bounds +src/ink.rs stroke smoothing, pressure, outline +src/text.rs rich text layout and glyph outlines +src/assets.rs image registry (content-hashed) +src/project/ project store, manifest/page formats, migrations +src/session.rs open project, autosave, thumbnails +src/editor/ tools, structure (groups/components), vector ops, pages, text editing +src/render/ draw ops → cached meshes → wgpu; headless capture +src/io/ PNG, SVG, PDF, export scopes, Excalidraw + SVG import, clipboard, + atomic writes +src/connector.rs arrow bindings, elbow router, arrowheads +src/library.rs shared shape library +src/ui/ home screen, pages, layers, inspector +src/app.rs winit application +src/mcp/ MCP server (rmcp): tools, HTML/CSS mapping, HTTP service +src/bin/paper-rs-mcp.rs stdio bridge to the running app +analysis/ reverse-engineering findings and design (raw dumps are gitignored) +docs/screenshots/ headless renders produced by `cargo test` +``` + +## openpaperdesign: Codex plugin (this fork) + +This fork adds a local Codex plugin, `openpaperdesign` (`plugin/openpaperdesign/`, local +marketplace `.agents/plugins/marketplace.json`). The agent binds a design project in your +workspace, edits it over MCP, and opens the editable canvas in Codex's in-app browser on +127.0.0.1. It runs locally only and is not affiliated with Paper. To set it up, run +`./scripts/build-web.sh`, then +`cargo install --locked --path crates/openpaperdesign-serve --bin openpaperdesign-mcp`. See +`docs/openpaperdesign/plugin-b2b.md` for the steps and what is verified. + +## License + +MIT OR Apache-2.0 for the code in this repository. Inter (Regular, Medium, SemiBold) is +© The Inter Project Authors under the SIL Open Font License 1.1; see `assets/fonts/OFL.txt`. diff --git a/packaging/desktop.json b/packaging/desktop.json new file mode 100644 index 0000000..f2d5455 --- /dev/null +++ b/packaging/desktop.json @@ -0,0 +1,9 @@ +{ + "name": "OpenPaperDesign", + "identifier": "com.arcitai.openpaperdesign", + "version": "0.1.0", + "release": "v0.1.0-desktop.1", + "minimumMacOS": "11.0", + "signing": "ad-hoc preview; not Apple notarized", + "upstream": "paper-rs by Michael Shimeles (Rasmic)" +} diff --git a/packaging/diagnostics.rs b/packaging/diagnostics.rs new file mode 100644 index 0000000..bae57be --- /dev/null +++ b/packaging/diagnostics.rs @@ -0,0 +1,211 @@ +//! Local, private, bounded diagnostics for desktop launches without a console. +use std::io::{self, Seek, Write}; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +const LOG_LIMIT: usize = 1024 * 1024; +const ERROR_LIMIT: usize = 64 * 1024; + +struct BoundedLog { + file: std::fs::File, + remaining: usize, + truncated: bool, +} +impl Write for BoundedLog { + fn write(&mut self, bytes: &[u8]) -> io::Result { + let count = bytes.len().min(self.remaining); + self.file.write_all(&bytes[..count])?; + self.remaining -= count; + if count < bytes.len() && !self.truncated { + self.file.write_all( + b"\n[Diagnostic log limit reached; latest error is in the adjacent .error file.]\n", + )?; + self.truncated = true; + } + Ok(bytes.len()) + } + fn flush(&mut self) -> io::Result<()> { + self.file.flush() + } +} +struct SharedLog(Arc>); +impl Write for SharedLog { + fn write(&mut self, bytes: &[u8]) -> io::Result { + self.0 + .lock() + .unwrap_or_else(|p| p.into_inner()) + .write(bytes) + } + fn flush(&mut self) -> io::Result<()> { + self.0.lock().unwrap_or_else(|p| p.into_inner()).flush() + } +} + +/// Replaces only this launch's owned error handle, never a path opened by a later write. +fn last_error(file: &mut std::fs::File, message: &[u8]) -> io::Result<()> { + file.rewind()?; + file.write_all(&message[..message.len().min(ERROR_LIMIT)])?; + file.set_len(message.len().min(ERROR_LIMIT) as u64)?; + file.flush() +} + +fn create_log(directory: &Path) -> io::Result<(PathBuf, std::fs::File)> { + let mut builder = std::fs::DirBuilder::new(); + builder.recursive(true); + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + builder.mode(0o700); + } + builder.create(directory)?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + let path = directory.join(format!("app-{now}-{}.log", std::process::id())); + Ok((path.clone(), private_file(&path)?)) +} +fn private_file(path: &Path) -> io::Result { + let mut options = std::fs::OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + options.open(path) +} + +pub fn start(gui: bool) -> Option { + let filter = if gui { + "warn,paper_rs=info,openpaperdesign=info" + } else { + "warn" + }; + let mut logger = + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(filter)); + let mut path = None; + let mut error_sink = None; + if gui { + let files = dirs::data_local_dir() + .ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "No local application-data folder") + }) + .and_then(|root| create_log(&root.join("OpenPaperDesign/logs"))); + match files { + Ok((name, file)) => { + let error = private_file(&name.with_extension("error")); + match error { + Ok(file) => error_sink = Some(Arc::new(Mutex::new(file))), + Err(error) => eprintln!("Cannot create latest-error log: {error}"), + } + logger.target(env_logger::Target::Pipe(Box::new(SharedLog(Arc::new( + Mutex::new(BoundedLog { + file, + remaining: LOG_LIMIT, + truncated: false, + }), + ))))); + path = Some(name); + } + Err(error) => eprintln!("Cannot create a local diagnostic log: {error}"), + } + } + if let Some(sink) = error_sink.clone() { + logger.format(move |buffer, record| { + let message = format!( + "{} {}: {}\n", + buffer.timestamp_millis(), + record.level(), + record.args() + ); + if record.level() == log::Level::Error { + let _ = last_error( + &mut sink.lock().unwrap_or_else(|p| p.into_inner()), + message.as_bytes(), + ); + } + buffer.write_all(message.as_bytes()) + }); + } + if logger.try_init().is_err() { + return None; + } + if let Some(sink) = error_sink { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + // Independent from the general logger, so its quota or lock cannot hide a panic. + let message = format!( + "Unrecoverable application error: {info}\n{}", + std::backtrace::Backtrace::force_capture() + ); + if let Ok(mut file) = sink.try_lock() { + let _ = last_error(&mut file, message.as_bytes()); + } + previous(info); + })); + } + log::info!( + "OpenPaperDesign {} starting on {}/{} (process {})", + env!("CARGO_PKG_VERSION"), + std::env::consts::OS, + std::env::consts::ARCH, + std::process::id() + ); + path +} + +#[cfg(test)] +mod tests { + use super::*; + struct Scratch(PathBuf); + impl Drop for Scratch { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + fn scratch() -> Scratch { + let root = + std::env::temp_dir().join(format!("opd-log-test-{}", paper_rs::project::new_id())); + std::fs::create_dir(&root).unwrap(); + Scratch(root) + } + #[test] + fn exhaustion_keeps_latest_error_and_reports_full_writes() { + let root = scratch(); + let (path, file) = create_log(&root.0).unwrap(); + let error_path = path.with_extension("error"); + let mut error = private_file(&error_path).unwrap(); + let mut log = BoundedLog { + file, + remaining: 4, + truncated: false, + }; + assert_eq!(log.write(b"first").unwrap(), 5); + assert_eq!(log.write(b"more").unwrap(), 4); + last_error(&mut error, b"first fatal error").unwrap(); + last_error(&mut error, b"last error").unwrap(); + log.flush().unwrap(); + let text = std::fs::read_to_string(path).unwrap(); + assert!(text.starts_with("firs")); + assert_eq!(text.matches("limit reached").count(), 1); + assert_eq!(std::fs::read(error_path).unwrap(), b"last error"); + } + #[test] + fn creates_private_new_files_without_replacing_existing_files() { + let root = scratch(); + let unrelated = root.0.join("existing.log"); + std::fs::write(&unrelated, b"keep").unwrap(); + assert!(private_file(&unrelated).is_err()); + let (path, _) = create_log(&root.0).unwrap(); + assert_eq!(std::fs::read(unrelated).unwrap(), b"keep"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!( + std::fs::metadata(path).unwrap().permissions().mode() & 0o777, + 0o600 + ); + } + } +} diff --git a/packaging/icons/icon-1024.png b/packaging/icons/icon-1024.png new file mode 100644 index 0000000..6e30a80 Binary files /dev/null and b/packaging/icons/icon-1024.png differ diff --git a/packaging/icons/icon.svg b/packaging/icons/icon.svg new file mode 100644 index 0000000..92881b1 --- /dev/null +++ b/packaging/icons/icon.svg @@ -0,0 +1 @@ + diff --git a/packaging/native.rs b/packaging/native.rs new file mode 100644 index 0000000..79aacaf --- /dev/null +++ b/packaging/native.rs @@ -0,0 +1,76 @@ +//! Distribution entrypoint. Reuse the native editor and CLI without a second UI. +#![cfg_attr(target_os = "windows", windows_subsystem = "windows")] +mod diagnostics; + +#[allow(dead_code)] // The shared CLI's main is retained in its original source. +mod upstream { + include!("../src/main.rs"); + + pub fn launch(args: Vec) -> Result<(), String> { + run(args) + } +} + +fn main() -> std::process::ExitCode { + let args: Vec = std::env::args().skip(1).collect(); + let gui = args.is_empty() + || args.as_slice() == ["--demo"] + || (args.len() == 1 && !args[0].starts_with('-')); + attach_parent_console(!gui); + // A new app must not silently adopt the upstream app's existing library. Explicit + // PAPER_RS_HOME continues to work for operators and disposable verification. + if std::env::var_os("PAPER_RS_HOME").is_none() { + let Some(data) = dirs::data_local_dir() else { + eprintln!( + "Cannot find your local application-data folder. Set PAPER_RS_HOME to a chosen project library." + ); + return std::process::ExitCode::FAILURE; + }; + // Protected Documents access can block macOS before the native event loop is + // running. The app's own local store avoids that first-window dependency. + // Existing Documents projects are left in place; an explicit root is honoured. + let root = data.join("OpenPaperDesign/projects"); + // The process is still single-threaded, before the editor/logger/runtime starts. + unsafe { std::env::set_var("PAPER_RS_HOME", root) }; + } + let diagnostic = diagnostics::start(gui); + match upstream::launch(args) { + Ok(()) => std::process::ExitCode::SUCCESS, + Err(error) => { + log::error!("Application stopped: {error}"); + eprintln!("error: {error}"); + if gui { + let detail = diagnostic.map_or_else( + || "A local diagnostic log could not be created.".to_string(), + |path| format!("Details were saved locally in:\n{}", path.display()), + ); + rfd::MessageDialog::new() + .set_level(rfd::MessageLevel::Error) + .set_title("OpenPaperDesign could not continue") + .set_description(format!("{error}\n\n{detail}")) + .show(); + } + std::process::ExitCode::FAILURE + } + } +} + +// Windows GUI builds have no automatic parent console. Reattach only for CLI calls; +// redirected stdout/stderr are already usable and must stay connected to their pipes. +#[cfg(target_os = "windows")] +fn attach_parent_console(cli: bool) { + #[link(name = "kernel32")] + unsafe extern "system" { + fn AttachConsole(process_id: u32) -> i32; + fn GetStdHandle(which: u32) -> *mut std::ffi::c_void; + } + if cli { + unsafe { + if GetStdHandle(-11i32 as u32).is_null() { + AttachConsole(u32::MAX); + } + } + } +} +#[cfg(not(target_os = "windows"))] +fn attach_parent_console(_: bool) {} diff --git a/plugin/openpaperdesign/assets/composer-icon-dark.svg b/plugin/openpaperdesign/assets/composer-icon-dark.svg new file mode 100644 index 0000000..c8bb07f --- /dev/null +++ b/plugin/openpaperdesign/assets/composer-icon-dark.svg @@ -0,0 +1 @@ +OpenPaperDesign diff --git a/plugin/openpaperdesign/assets/composer-icon.svg b/plugin/openpaperdesign/assets/composer-icon.svg new file mode 100644 index 0000000..71eb3d1 --- /dev/null +++ b/plugin/openpaperdesign/assets/composer-icon.svg @@ -0,0 +1 @@ +OpenPaperDesign diff --git a/plugin/openpaperdesign/assets/logo-dark.svg b/plugin/openpaperdesign/assets/logo-dark.svg new file mode 100644 index 0000000..c8bb07f --- /dev/null +++ b/plugin/openpaperdesign/assets/logo-dark.svg @@ -0,0 +1 @@ +OpenPaperDesign diff --git a/plugin/openpaperdesign/assets/logo.svg b/plugin/openpaperdesign/assets/logo.svg new file mode 100644 index 0000000..71eb3d1 --- /dev/null +++ b/plugin/openpaperdesign/assets/logo.svg @@ -0,0 +1 @@ +OpenPaperDesign diff --git a/plugin/openpaperdesign/plugin.json b/plugin/openpaperdesign/plugin.json index d297f77..70c4943 100644 --- a/plugin/openpaperdesign/plugin.json +++ b/plugin/openpaperdesign/plugin.json @@ -3,11 +3,20 @@ "name": "openpaperdesign", "version": "0.1.0", "description": "Edit a design project in your workspace with an agent and in an editable canvas in the in-app browser (paper-rs, local only).", - "author": { "name": "Arc’IT AI", "url": "https://github.com/arcitai" }, + "author": { + "name": "Arc’IT AI", + "url": "https://github.com/arcitai" + }, "license": "MIT OR Apache-2.0", "repository": "https://github.com/arcitai/openpaperdesign", "homepage": "https://github.com/arcitai/openpaperdesign", - "keywords": ["design", "canvas", "whiteboard", "mcp", "paper-rs"], + "keywords": [ + "design", + "canvas", + "whiteboard", + "mcp", + "paper-rs" + ], "extensions": { "com.openai": { "interface": { @@ -15,7 +24,11 @@ "shortDescription": "Agent + human design canvas", "longDescription": "Binds one paper-rs design project in the workspace, lets the agent edit it over MCP, and opens the editable canvas in the in-app browser on a loopback URL. Local only: no cloud, no telemetry. Not affiliated with Paper (paper.design).", "developerName": "Arc’IT AI", - "category": "Design" + "category": "Design", + "logo": "./assets/logo.svg", + "logoDark": "./assets/logo-dark.svg", + "composerIcon": "./assets/composer-icon.svg", + "composerIconDark": "./assets/composer-icon-dark.svg" }, "onboardingSkill": "./skills/setup" } diff --git a/scripts/export-product-site.py b/scripts/export-product-site.py new file mode 100644 index 0000000..e5c6e55 --- /dev/null +++ b/scripts/export-product-site.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Export the committed static site; ignored local previews never enter a deployment.""" +import argparse +import io +from pathlib import Path, PurePosixPath +import subprocess +import tarfile + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--output', required=True, type=Path) +args = parser.parse_args() +root = Path(__file__).resolve().parents[1] +if args.output.exists(): + parser.error('Choose a new output directory; existing files are never overwritten.') +archive = subprocess.check_output(['git', 'archive', '--format=tar', 'HEAD', 'site'], cwd=root) +with tarfile.open(fileobj=io.BytesIO(archive)) as source: + files = [] + for member in source.getmembers(): + path = PurePosixPath(member.name) + if member.isdir(): + continue + if not member.isfile() or path.parts[0] != 'site' or '..' in path.parts: + raise ValueError('Unexpected site archive entry') + relative = Path(*path.parts[1:]) + if relative.name == 'releases.preview.json' or relative.parts[0] == 'downloads': + raise ValueError('A local preview was committed; refuse deployment export') + files.append((member, relative)) + args.output.mkdir(parents=True) + for member, relative in files: + target = args.output / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(source.extractfile(member).read()) +print(f'Exported {len(files)} committed files to {args.output}') diff --git a/scripts/make-product-fixtures.py b/scripts/make-product-fixtures.py new file mode 100644 index 0000000..2cca7d3 --- /dev/null +++ b/scripts/make-product-fixtures.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Generate public, editable Excalidraw fixtures for the native app's own renderer.""" +import json +from pathlib import Path +ROOT=Path(__file__).resolve().parents[1] +out=ROOT/'design/product/fixtures';out.mkdir(parents=True,exist_ok=True) +class Scene: + def __init__(self):self.items=[] + def shape(self,kind,x,y,w,h,fill='transparent',stroke='#4265a0',**extra): + ident=f'item{len(self.items):04d}' + self.items.append(dict(id=ident,type=kind,x=x,y=y,width=w,height=h,angle=0,strokeColor=stroke,backgroundColor=fill,fillStyle='solid',strokeWidth=1.5,roughness=0,opacity=100,seed=len(self.items)+17,version=1,versionNonce=1,isDeleted=False,groupIds=[]));self.items[-1].update(extra);return ident + def text(self,s,x,y,w=240,size=20,color='#202020',**kw):return self.shape('text',x,y,w,size*1.5,stroke=color,text=s,originalText=s,fontSize=size,fontFamily=2,textAlign='left',verticalAlign='top',lineHeight=1.25,**kw) + def box(self,title,detail,x,y,w=240,fill='#f0f3f8'): + self.shape('rectangle',x,y,w,100,fill,roundness={'type':3,'value':8});self.text(title,x+18,y+18,w-36,20);self.text(detail,x+18,y+54,w-36,13,'#626262') + def arrow(self,x,y,dx,dy):self.shape('arrow',x,y,abs(dx) or 1,abs(dy) or 1,points=[[0,0],[dx,dy]],endArrowhead='arrow') + def sticky(self,text,x,y,color): + ident=self.shape('stickynote',x,y,190,150,color,'#74644f');self.text(text,x+15,y+18,160,18,'#30271f',containerId=ident) + def save(self,name): + (out/f'{name}.excalidraw').write_text(json.dumps({'type':'excalidraw','version':2,'source':'OpenPaperDesign public product fixture','elements':self.items,'appState':{'viewBackgroundColor':'#fafafa'},'files':{}},indent=2)+'\n') +s=Scene();s.text('A place to think. A system you can explain.',0,0,1000,36);s.text('OpenPaperDesign / local desktop + optional AI agents',0,62,900,17,'#626262') +s.box('You','Sketch / review / refine',0,165);s.box('OpenPaperDesign','An editable native canvas',360,165,280,'#e0e8f5');s.box('Your project','Pages / JSON / assets',760,165) +s.arrow(245,215,105,0);s.arrow(645,215,105,0);s.box('Codex','Optional MCP client',100,390);s.box('Claude Code','Optional MCP client',680,390);s.arrow(225,385,160,-110);s.arrow(800,385,-180,-110) +s.sticky('Design the interface\nbeside the system.',385,365,'#ffdf6b');s.text('No account. No subscription. Your files.',280,600,800,22) +s.save('architecture') +s=Scene();s.text('A little room for the unfinished idea.',0,0,1100,36);s.text('Notes, sketches and decisions. All still editable.',0,65,1000,17,'#626262') +for i,(label,color) in enumerate([('What are we\ntrying to change?','#ffdf6b'),('Keep the useful\nparts we have.','#ffb8a8'),('Draw it before\nwe build it.','#b5e6a2'),('What data\ndo we need?','#a8d8ff'),('Review it\ntogether.','#d7c2ff'),('Next step:\ntry a small slice.','#e8e8ec')]):s.sticky(label,(i%3)*280,(i//3)*220+145,color) +s.shape('ellipse',900,170,200,120,'transparent',roughness=1.4);s.text('rough idea',927,210,160,18);s.shape('rectangle',885,415,230,115,'transparent',roughness=1.2);s.text('a clearer direction',905,455,220,18);s.arrow(1000,300,0,100) +s.save('whiteboard') +s=Scene();s.text('Three brands. One thread.',0,0,1200,36);s.text('Public interface studies / gustavonline · Arc’IT AI · onlinesourdough',0,65,1200,17,'#626262') +for i,(brand,heading,body,bg,accent) in enumerate([('gustavonline','Work. Ideas.\nExperiments.','Data, software and agentic engineering.','#fafafa','#d37635'),('Arc’IT AI','AI should understand\nyour business.','Your data and business context.','#f3f5f8','#4265a0'),('onlinesourdough','Valuable AI starts\nwith your data.','And your business context.','#f8f1e5','#9c6849')]): + x=i*450;s.shape('rectangle',x,145,410,510,bg,'#ccc8c0');s.text(brand,x+24,168,360,20);s.shape('line',x+20,211,370,1,points=[[0,0],[370,0]],stroke='#d9d4cb');s.text(heading,x+24,245,360,29);s.text(body,x+24,340,360,13,'#626262') + for row in range(7): + for col in range(12): + levels=['#e9dfcf','#e5ba92',accent,'#d7b99f'] if i!=1 else ['#d8e2f2','#b1c2e0','#7697c6','#4265a0'];s.shape('rectangle',x+25+col*29,420+row*24,24,18,levels[(row*7+col*3)%4],'transparent') +s.text('Layout studies, not browser screenshots.',0,710,1200,16,'#626262');s.save('design') +print(out) diff --git a/scripts/package-desktop.py b/scripts/package-desktop.py new file mode 100644 index 0000000..6075110 --- /dev/null +++ b/scripts/package-desktop.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Package existing, target-built native binaries. No downloads, signing credentials or publish. + +Usage: package-desktop.py --platform macos|windows|linux --arch arm64|x64 + --bin-dir PATH --output PATH +macOS packaging runs on a Mac and uses ad-hoc signing only. Release signing/notarization +is deliberately separate; a successful archive does not prove end-user installation. +An existing archive or checksum is never replaced: move earlier packages aside first. +The output directory must support hard links (APFS, ext4, NTFS and similar). +""" +import argparse +import hashlib +import json +import os +import plistlib +import platform as host +import shutil +import subprocess +import tarfile +import tempfile +import zipfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def publish(files, output): + """Hard-link each finished file into output. A link never replaces an existing name, so of + two runs packaging the same target exactly one publishes. On failure, withdraw only the + names this run linked.""" + published = [] + try: + for path in files: + target = output / path.name + os.link(path, target) + published.append((target, path)) + except BaseException: + for target, path in reversed(published): + if os.path.samefile(target, path): + target.unlink() + raise + + +def package(platform, arch, binaries, output, require_clean=False): + revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip() + dirty = bool(subprocess.check_output(["git", "status", "--porcelain"], cwd=ROOT, text=True)) + if require_clean and dirty: + raise ValueError("Refusing a distribution package from an uncommitted source tree") + expected = {"macos": "Darwin", "windows": "Windows", "linux": "Linux"}[platform] + if host.system() != expected: + raise ValueError(f"Package {platform} on its matching build host, not {host.system()}") + machine = host.machine().lower() + actual_arch = "arm64" if machine in ("arm64", "aarch64") else "x64" if machine in ("x86_64", "amd64") else machine + if actual_arch != arch and not (platform == "macos" and arch == "universal"): + raise ValueError(f"Requested {arch} but the build host is {actual_arch}") + config = json.loads((ROOT / "packaging/desktop.json").read_text(encoding="utf-8")) + suffix = ".exe" if platform == "windows" else "" + source = binaries / ("openpaperdesign" + suffix) + bridge = binaries / ("paper-rs-mcp" + suffix) + for required in (source, bridge): + if not required.is_file(): + raise FileNotFoundError(f"Missing built executable: {required}") + if platform == "macos": + architectures = ["arm64", "x86_64"] if arch == "universal" else ["arm64" if arch == "arm64" else "x86_64"] + subprocess.run(["lipo", str(required), "-verify_arch", *architectures], check=True) + output.mkdir(parents=True, exist_ok=True) + name = f"OpenPaperDesign-{config['version']}-{platform}-{arch}" + archive = output / (name + (".tar.gz" if platform == "linux" else ".zip")) + checksum = archive.with_name(archive.name + ".sha256") + for existing in (archive, checksum): + if os.path.lexists(existing): + raise FileExistsError(f"Refusing to replace {existing}; move earlier packages aside first") + # The product is built and signed in the system temporary directory: a synced output folder + # (Documents) can attach Finder/file-provider metadata that fails strict signature checks. + # Only the finished archive and checksum are staged privately beside the output, on its + # volume, so publish() can link them into place. A failed or overlapping run then leaves + # the output as it was. + with tempfile.TemporaryDirectory(prefix="opd-package-") as temporary, \ + tempfile.TemporaryDirectory(prefix=f".{name}.", suffix=".partial", dir=output) as finished: + stage, ready = Path(temporary), Path(finished) + if platform == "macos": + product = stage / "OpenPaperDesign.app" + executable_dir = product / "Contents/MacOS" + resources = product / "Contents/Resources" + executable_dir.mkdir(parents=True) + resources.mkdir(parents=True) + plist = {"CFBundleName": config["name"], "CFBundleDisplayName": config["name"], + "CFBundleIdentifier": config["identifier"], "CFBundleExecutable": "openpaperdesign", + "CFBundlePackageType": "APPL", "CFBundleShortVersionString": config["version"], + "CFBundleVersion": config["version"], "LSMinimumSystemVersion": config["minimumMacOS"], + "NSHighResolutionCapable": True, "LSApplicationCategoryType": "public.app-category.graphics-design", + "NSHumanReadableCopyright": "OpenPaperDesign / Arc’IT AI. Built on paper-rs by Michael Shimeles.", + "CFBundleIconFile": "AppIcon", + "NSDocumentsFolderUsageDescription": "OpenPaperDesign opens and saves the projects you choose in your Documents folder."} + (product / "Contents/Info.plist").write_bytes(plistlib.dumps(plist)) + iconset = stage / "AppIcon.iconset" + iconset.mkdir() + icon = ROOT / "packaging/icons/icon-1024.png" + if not icon.is_file(): + raise FileNotFoundError("Render packaging/icons/icon-1024.png before packaging") + for size in (16, 32, 128, 256, 512): + for factor in (1, 2): + target = iconset / f"icon_{size}x{size}{'@2x' if factor == 2 else ''}.png" + subprocess.run(["sips", "-z", str(size * factor), str(size * factor), str(icon), "--out", str(target)], check=True, stdout=subprocess.DEVNULL) + subprocess.run(["iconutil", "-c", "icns", str(iconset), "-o", str(resources / "AppIcon.icns")], check=True) + else: + product = stage / name + executable_dir = resources = product + product.mkdir() + for path in (source, bridge): + destination = executable_dir / path.name + shutil.copy2(path, destination) + destination.chmod(0o755) + for license_file in ("LICENSE-MIT", "LICENSE-APACHE", "THIRD_PARTY.md"): + shutil.copy2(ROOT / license_file, resources / license_file) + shutil.copy2(ROOT / "assets/fonts/OFL.txt", resources / "Inter-OFL.txt") + # The newer workspace also embeds Geist Mono. Carry every additional + # bundled font notice when packaging that source, not only Inter's. + if list((ROOT / "assets/fonts").glob("*GeistMono*")) and not (ROOT / "assets/fonts/OFL-GeistMono.txt").is_file(): + raise FileNotFoundError("The bundled Geist Mono font requires OFL-GeistMono.txt") + for notice in sorted((ROOT / "assets/fonts").glob("OFL-*.txt")): + shutil.copy2(notice, resources / notice.name) + (resources / "BUILD.json").write_text(json.dumps({**config, "platform": platform, "architecture": arch, + "revision": revision, "dirty": dirty, + "inputBinaries": {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in (source, bridge)}, + "components": json.loads((ROOT / "packaging/components.json").read_text()) if (ROOT / "packaging/components.json").is_file() else {}, + "automaticUpdates": False}, indent=2) + "\n") + built = ready / archive.name + if platform == "macos": + subprocess.run(["codesign", "--force", "--sign", "-", str(executable_dir / bridge.name)], check=True) + subprocess.run(["codesign", "--force", "--sign", "-", str(product)], check=True) + subprocess.run(["codesign", "--verify", "--deep", "--strict", str(product)], check=True) + subprocess.run(["ditto", "-c", "-k", "--keepParent", str(product), str(built)], check=True) + elif platform == "windows": + with zipfile.ZipFile(built, "w", zipfile.ZIP_DEFLATED) as z: + for path in sorted(product.rglob("*")): + if path.is_file(): + z.write(path, path.relative_to(stage)) + else: + with tarfile.open(built, "w:gz") as t: + t.add(product, arcname=product.name) + digest = hashlib.sha256(built.read_bytes()).hexdigest() + (ready / checksum.name).write_text(f"{digest} {archive.name}\n") + publish([built, ready / checksum.name], output) + print(archive) + return archive + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--platform", choices=("macos", "windows", "linux"), required=True) + parser.add_argument("--arch", choices=("arm64", "x64", "universal"), required=True) + parser.add_argument("--bin-dir", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--require-clean", action="store_true") + args = parser.parse_args() + package(args.platform, args.arch, args.bin_dir.resolve(), args.output.resolve(), args.require_clean) diff --git a/scripts/refresh-site-versions.py b/scripts/refresh-site-versions.py new file mode 100644 index 0000000..1ed5389 --- /dev/null +++ b/scripts/refresh-site-versions.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Keep plain-static preview HTML/modules bound to their actual stylesheet/JS bytes.""" +import argparse +import hashlib +import re +from pathlib import Path + +site = Path(__file__).resolve().parents[1] / 'site' +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--check', action='store_true') +args = parser.parse_args() + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest()[:12] + +module = site / 'site.js' +before = module.read_text() +after = re.sub(r"from './downloads\.js(?:\?v=[0-9a-f]+)?'", f"from './downloads.js?v={digest(site / 'downloads.js')}'", before) +if args.check: + if before != after: + raise SystemExit('Refresh the download module version') +else: + module.write_text(after) + +index = site / 'index.html' +before = index.read_text() +after = before +for name in ('styles.css', 'site.js'): + after = re.sub(r'(["\'])/' + re.escape(name) + r'(?:\?v=[0-9a-f]+)?(["\'])', lambda m: f'{m[1]}/{name}?v={digest(site / name)}{m[2]}', after) +if args.check: + if before != after: + raise SystemExit('Refresh the stylesheet and script versions') +else: + index.write_text(after) +print('Site asset versions match content') diff --git a/scripts/release-manifest.py b/scripts/release-manifest.py new file mode 100644 index 0000000..7f2c34b --- /dev/null +++ b/scripts/release-manifest.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Verify all three desktop packages before writing a release download manifest.""" +import argparse +import hashlib +import json +import re +import tarfile +import zipfile +from pathlib import Path + +PACKAGE = re.compile(r'OpenPaperDesign-([0-9][A-Za-z0-9.-]*)-(macos-universal|windows-x64|linux-x64)\.(zip|tar\.gz)\Z') + + +def digest(path): + with path.open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + + +def build_info(path): + if path.suffix == '.zip': + with zipfile.ZipFile(path) as archive: + entries = [entry for entry in archive.infolist() if entry.filename.endswith('/BUILD.json')] + if len(entries) != 1 or entries[0].file_size > 65536: + raise ValueError('Expected exactly one bounded BUILD.json') + return json.loads(archive.read(entries[0])) + with tarfile.open(path, 'r:gz') as archive: + entries = [entry for entry in archive if entry.name.endswith('/BUILD.json')] + if len(entries) != 1 or not entries[0].isfile() or entries[0].size > 65536: + raise ValueError('Expected exactly one bounded BUILD.json') + return json.load(archive.extractfile(entries[0])) + + +def manifest(directory, repository, tag, revision): + if repository != 'arcitai/openpaperdesign' or not re.fullmatch(r'v[0-9][A-Za-z0-9.-]*', tag): + raise ValueError('Unexpected release destination or tag') + if not re.fullmatch(r'[0-9a-f]{40}', revision): + raise ValueError('Expected an exact source revision') + platforms = {} + version = None + for path in sorted(directory.iterdir()): + match = PACKAGE.fullmatch(path.name) + if not match: + continue + if path.is_symlink() or not path.is_file() or path.stat().st_size == 0: + raise ValueError('Invalid package file') + package_version, target, extension = match.groups() + platform, arch = target.split('-') + if extension != ('tar.gz' if platform == 'linux' else 'zip') or platform in platforms: + raise ValueError('Unexpected or duplicate platform package') + if version is not None and package_version != version: + raise ValueError('Mixed package versions') + version = package_version + checksum = digest(path) + if path.with_name(path.name+'.sha256').read_text().strip() != f'{checksum} {path.name}': + raise ValueError(f'Checksum mismatch: {path.name}') + info = build_info(path) + if (info.get('dirty') is not False or info.get('revision') != revision + or info.get('platform') != platform or info.get('architecture') != arch + or info.get('version') != version): + raise ValueError(f'Unqualified package provenance: {path.name}') + platforms[platform] = { + 'url': f'https://github.com/{repository}/releases/download/{tag}/{path.name}', + 'architecture': 'Intel + Apple Silicon' if arch == 'universal' else arch, + 'status': 'Desktop preview · ad-hoc signed, not notarized' if platform == 'macos' else 'Desktop preview', + 'sha256': checksum, 'bytes': path.stat().st_size, + } + if set(platforms) != {'macos', 'windows', 'linux'}: + raise ValueError('All three verified platform packages are required') + if not (tag == 'v'+version or tag.startswith('v'+version+'-')): + raise ValueError('Tag does not match package version') + return {'version': version, 'revision': revision, 'tag': tag, 'platforms': platforms, + 'notice': 'Desktop preview. Check platform notes, packaged licences and checksums before use.'} + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('directory', type=Path) + parser.add_argument('--repository', required=True) + parser.add_argument('--tag', required=True) + parser.add_argument('--revision', required=True) + args = parser.parse_args() + value = manifest(args.directory, args.repository, args.tag, args.revision) + (args.directory/'releases.json').write_text(json.dumps(value, indent=2)+'\n') diff --git a/scripts/render-brand-assets.cjs b/scripts/render-brand-assets.cjs new file mode 100644 index 0000000..05825c3 --- /dev/null +++ b/scripts/render-brand-assets.cjs @@ -0,0 +1,31 @@ +#!/usr/bin/env node +// Optional maintainer tool. The application and CI use the committed PNG/SVG outputs. +// Requires sharp (development only); source geometry remains in assets/brand/*.svg. +const fs=require('node:fs/promises'); +const path=require('node:path'); +const sharp=require('sharp'); +const root=path.resolve(__dirname,'..'); +async function render(){ + const light=await fs.readFile(path.join(root,'assets/brand/openpaperdesign.svg'),'utf8'); + const dark=await fs.readFile(path.join(root,'assets/brand/openpaperdesign-dark.svg'),'utf8'); + const homePath=path.join(root,'crates/openpaperdesign-serve/src/home.html'); + const home=require('node:fs').existsSync(homePath)?await fs.readFile(homePath,'utf8'):null; + // The compact Home consumer is present on the new-UI integration branch. + if(home && home.includes('')+1,light.lastIndexOf('')).replace(/.*?<\/title>/,''); + await fs.writeFile(homePath,home.replace(/<span class="brand-mark"[^>]*>.*?<\/span>/, + `<span class="brand-mark" aria-hidden="true"><svg viewBox="0 0 64 64">${body}</svg></span>`)); + } + for(const [theme,source] of [['light',light],['dark',dark]]){ + await sharp(Buffer.from(source)).resize(96,96).png().toFile(path.join(root,`assets/brand/home-logo-${theme}.png`)); + } + // App icons have an opaque tile; the mark itself is the same inverse artwork. + const body=dark.slice(dark.indexOf('>')+1,dark.lastIndexOf('</svg>')).replace(/<title>.*?<\/title>/,''); + const icon=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><rect x="40" y="40" width="944" height="944" rx="212" fill="#202124"/><g color="#fafafa" transform="translate(115 115) scale(12.4)">${body}</g></svg>`; + await fs.writeFile(path.join(root,'packaging/icons/icon.svg'),icon+'\n'); + await sharp(Buffer.from(icon)).resize(1024,1024).png().toFile(path.join(root,'packaging/icons/icon-1024.png')); + for(const [target,source] of [['logo.svg',light],['composer-icon.svg',light],['logo-dark.svg',dark],['composer-icon-dark.svg',dark]]){ + await fs.writeFile(path.join(root,'plugin/openpaperdesign/assets',target),source); + } +} +render().catch(error=>{console.error(error);process.exitCode=1;}); diff --git a/scripts/sync-site-assets.py b/scripts/sync-site-assets.py new file mode 100644 index 0000000..5aa0f8e --- /dev/null +++ b/scripts/sync-site-assets.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Copy/check pinned repository-owned marketing assets. Never modifies source assets.""" +import argparse,hashlib,json +from pathlib import Path +root=Path(__file__).resolve().parents[1];site=root/'site' +parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--check',action='store_true');args=parser.parse_args() +manifest=json.loads((site/'assets-sources.json').read_text()) +if manifest['repository']!='arcitai/openpaperdesign':raise ValueError('Wrong asset repository') +for asset in manifest['assets']: + source=(root/asset['path']).resolve();target=(site/asset['output']).resolve() + if not source.is_relative_to(root) or not target.is_relative_to(site/'assets'):raise ValueError('Asset path leaves the repository or site/assets') + data=source.read_bytes() + if hashlib.sha256(data).hexdigest()!=asset['sha256']:raise ValueError(f"Source hash differs: {asset['path']}") + if args.check: + if not target.is_file() or target.read_bytes()!=data:raise ValueError(f"Site asset differs: {target.name}") + elif source!=target: + target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(data) + print(asset['output']) diff --git a/scripts/tests/downloads.test.mjs b/scripts/tests/downloads.test.mjs new file mode 100644 index 0000000..03912b6 --- /dev/null +++ b/scripts/tests/downloads.test.mjs @@ -0,0 +1,71 @@ +import {test} from 'node:test'; +import assert from 'node:assert/strict'; +import {detectPlatform,selectAsset,allowedDownload,appDownloadCount,publishedManifest,downloadChoice} from '../../site/downloads.js'; +test('mobile never receives a desktop installer, including iPad desktop mode',()=>{ + assert.equal(detectPlatform('MacIntel','iPhone',1),'mobile'); + assert.equal(detectPlatform('MacIntel','Mozilla Safari',5),'mobile'); + assert.equal(detectPlatform('Linux armv8','Android',5),'mobile'); + assert.equal(selectAsset({platforms:{macos:{url:'/downloads/app.zip',architecture:'arm64',status:'preview'}}},'mobile'),null); +}); +test('download totals count app archives across releases, not sources, checksums or drafts',()=>{ + const asset=(name,download_count)=>({name,download_count}); + assert.equal(appDownloadCount([{assets:[asset('OpenPaperDesign-0.1.0-macos-arm64.zip',3),asset('OpenPaperDesign-0.1.0-macos-arm64.zip.sha256',90),asset('source.tar.gz',88)]},{assets:[asset('OpenPaperDesign-0.1.1-linux-x64.tar.gz',5)]},{draft:true,assets:[asset('OpenPaperDesign-0.1.0-windows-x64.zip',50)]}]),8); + assert.throws(()=>appDownloadCount([{assets:[asset('OpenPaperDesign-0.1.0-macos-arm64.zip',-1)]}])); + assert.throws(()=>appDownloadCount({error:'rate limit'})); + assert.equal(appDownloadCount([{assets:[asset('source.tar.gz',88)]}]),null); + assert.equal(appDownloadCount([{assets:[asset('OpenPaperDesign-0.1.0-windows-x64.zip',0)]}]),0); +}); +test('desktop OS detection has an honest unknown fallback',()=>{ + assert.equal(detectPlatform('MacIntel','Safari',0),'macos'); + assert.equal(detectPlatform('Win32'),'windows');assert.equal(detectPlatform('Linux x86_64'),'linux');assert.equal(detectPlatform('Other'),'unknown'); +}); +test('only reviewed release or local-preview assets become download actions',()=>{ + assert.equal(allowedDownload('https://github.com/arcitai/openpaperdesign/releases/download/v1/app.zip'),true); + for(const u of ['javascript:alert(1)','https://github.com/other/repo/releases/download/v1/a','https://github.com.evil.test/arcitai/openpaperdesign/releases/download/v1/a','/downloads/../secret','https://evil.test/app.zip'])assert.equal(allowedDownload(u),false); + assert.equal(selectAsset({platforms:{windows:{url:'https://evil.test/app.zip',architecture:'x64',status:'built'}}},'windows'),null); + assert.equal(selectAsset({},'linux'),null); + assert.equal(allowedDownload('/downloads/app.zip'),false); + assert.equal(allowedDownload('/downloads/app.zip',true),true); +}); + +test('only a complete published three-platform release with checksum assets supplies downloads',()=>{ + const tag='v0.1.0-desktop.1';const base=`https://github.com/arcitai/openpaperdesign/releases/download/${tag}/`; + const assets=['macos-universal.zip','windows-x64.zip','linux-x64.tar.gz'].flatMap(target=>{ + const name=`OpenPaperDesign-0.1.0-${target}`; + return [{name,size:500,browser_download_url:base+name},{name:name+'.sha256',size:100,browser_download_url:base+name+'.sha256'}]; + }); + const release={tag_name:tag,assets}; + assert.equal(publishedManifest([release]).platforms.macos.architecture,'Intel + Apple Silicon'); + assert.match(downloadChoice(publishedManifest([release]),'macos').meta,/Apple notarization not verified/); + assert.match(publishedManifest([release]).notice,/notarization is not verified/); + assert.equal(publishedManifest([{...release,draft:true}]),null); + assert.equal(publishedManifest([{...release,tag_name:'candidate-abc'}]),null); + assert.equal(publishedManifest([{...release,assets:assets.slice(0,-1)}]),null); + assert.equal(publishedManifest([{...release,assets:assets.map((a,i)=>i===0?{...a,browser_download_url:'https://evil.test/app.zip'}:a)}]),null); + assert.equal(appDownloadCount([{assets:[{name:'OpenPaperDesign-0.1.0-macos-universal.zip',download_count:4}]}]),4); +}); + +test('the primary action directly downloads the validated file for the detected desktop OS',()=>{ + const manifest={platforms:Object.fromEntries(['macos','windows','linux'].map(os=>[os,{url:`/downloads/${os}.zip`,architecture:os==='macos'?'Intel + Apple Silicon':'x64',status:'Preview · not notarized'}]))}; + for(const os of ['macos','windows','linux']){ + const choice=downloadChoice(manifest,os,true); + assert.equal(choice.href,`/downloads/${os}.zip`); + assert.ok(choice.asset); + assert.match(choice.label,/^Download for /); + } + assert.match(downloadChoice(manifest,'macos',true).meta,/Not Apple notarized/); + const unknown={platforms:{macos:{...manifest.platforms.macos,status:'Preview'}}}; + assert.match(downloadChoice(unknown,'macos',true).meta,/Apple notarization not verified/); + unknown.platforms.macos.notarized=true; + assert.doesNotMatch(downloadChoice(unknown,'macos',true).meta,/notarization not verified/); + for(const os of ['mobile','unknown']){ + assert.equal(downloadChoice(manifest,os,true).href,'#download'); + assert.equal(downloadChoice(manifest,os,true).asset,null); + } + assert.equal(downloadChoice(manifest,'macos',false).asset,null,'local files never become public downloads'); + assert.equal(downloadChoice({platforms:{}},'windows').href,'#download'); +}); +test('unsupported desktop hardware goes to platform choices rather than an x64 installer',()=>{ + for(const [platform,ua] of [['Linux aarch64','Firefox'],['Linux armv7l','Firefox'],['Win32','Windows NT 10.0; ARM64'],['Linux x86_64','Mozilla/5.0 (X11; CrOS x86_64 1)']])assert.equal(detectPlatform(platform,ua),'unknown'); + assert.equal(detectPlatform('MacIntel','Mozilla Safari',0),'macos','the universal Mac app needs no CPU guess'); +}); diff --git a/scripts/tests/test_package_publication.py b/scripts/tests/test_package_publication.py new file mode 100644 index 0000000..fefd601 --- /dev/null +++ b/scripts/tests/test_package_publication.py @@ -0,0 +1,274 @@ +"""Desktop package publication: exact files, no replacement, safe failure, overlapping runs. + +Linux and Windows packages are built from stand-in executables on any host: only the host +check is patched, the archives, notices and file system publication are real. The macOS cases +also run the real sips/iconutil/codesign/ditto and need a Mac (lipo is skipped for stand-ins). +""" +import contextlib +import errno +import hashlib +import importlib.util +import json +import os +import re +import shutil +import subprocess +import sys +import tarfile +import tempfile +import threading +import unittest +import zipfile +from pathlib import Path +from unittest import mock + +SCRIPTS = Path(__file__).parents[1] + + +def load(name, filename): + spec = importlib.util.spec_from_file_location(name, SCRIPTS/filename) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +packager = load('package_desktop', 'package-desktop.py') +release_manifest = load('release_manifest', 'release-manifest.py') +VERSION = json.loads((SCRIPTS.parent/'packaging/desktop.json').read_text(encoding='utf-8'))['version'] +HOSTS = {'linux': 'Linux', 'windows': 'Windows'} +WRITERS = {'linux': tarfile.TarFile, 'windows': zipfile.ZipFile} +MAC_TOOLS = sys.platform == 'darwin' and all(map(shutil.which, ('sips', 'iconutil', 'codesign', 'ditto'))) + + +@contextlib.contextmanager +def after_archive(platform, hook, commands=None): + """Call hook once a run has completely written its archive, before anything else happens. + On macOS, commands (a list) receives every command the run executes.""" + if platform == 'macos': + run = subprocess.run + def written(command, *args, **kwargs): + if commands is not None: commands.append(command) + if command[0] == 'lipo': # stand-in executables are not the requested slices + return subprocess.CompletedProcess(command, 0) + result = run(command, *args, **kwargs) + if command[0] == 'ditto': hook() + return result + with mock.patch.object(packager.subprocess, 'run', written): yield + return + close = WRITERS[platform].close + def written(self): + close(self) + if self.mode == 'w' and not getattr(self, 'hooked', False): # ZipFile.__del__ closes again + self.hooked = True + hook() + with mock.patch.object(WRITERS[platform], 'close', written): yield + + +class PackagePublication(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.output = self.root/'dist' + + @contextlib.contextmanager + def host(self, platform): + # Patched once around all runs: the platform module is shared by every thread. + with mock.patch.object(packager.host, 'system', return_value=HOSTS[platform]), \ + mock.patch.object(packager.host, 'machine', return_value='x86_64'): + yield + + def run_package(self, platform, label, arch='x64'): + """Package the stand-in executables named after label, creating them on first use.""" + executables = self.root/label + if not executables.exists(): + executables.mkdir() + for name in ('openpaperdesign', 'paper-rs-mcp'): + (executables/(name+('.exe' if platform == 'windows' else ''))).write_bytes(f'{label} {name}\n'.encode()*512) + return packager.package(platform, arch, executables, self.output) + + def mac_executables(self, label): + """Signable stand-ins: copies of /usr/bin/<label>.""" + (self.root/label).mkdir() + for name in ('openpaperdesign', 'paper-rs-mcp'): + shutil.copy(f'/usr/bin/{label}', self.root/label/name) + + def expected(self, platform, arch='x64'): + return self.output/f'OpenPaperDesign-{VERSION}-{platform}-{arch}{".tar.gz" if platform == "linux" else ".zip"}' + + def snapshot(self): + return {path.name: (path.read_bytes(), path.stat().st_ino) for path in self.output.iterdir()} + + def staged(self): + """Every path under the output, relative to it.""" + return sorted(path.relative_to(self.output).as_posix() for path in self.output.rglob('*')) + + def assert_staging(self, platform, label, arch='x64', commands=None): + """Package label, checking that only its finished archive, then its checksum, are ever under + the output (in one private directory, gone afterwards). The product is built elsewhere.""" + seen, publish = [], packager.publish + def published(files, output): + seen.append(self.staged()) + return publish(files, output) + with after_archive(platform, lambda: seen.append(self.staged()), commands), \ + mock.patch.object(packager, 'publish', published): + archive = self.run_package(platform, label, arch) + private = seen[0][0] + self.assertRegex(private, rf'^{re.escape(f".OpenPaperDesign-{VERSION}-{platform}-{arch}.")}[^/]+\.partial$') + self.assertEqual(seen, [[private, f'{private}/{archive.name}'], + [private, f'{private}/{archive.name}', f'{private}/{archive.name}.sha256']]) + self.assertEqual(self.staged(), [archive.name, archive.name+'.sha256']) + self.assert_package(archive, label) + return archive + + def assert_package(self, archive, label): + """archive and its checksum are complete, consistent and entirely the work of run label.""" + self.assertTrue(release_manifest.PACKAGE.fullmatch(archive.name), archive.name) + self.assertEqual(archive.with_name(archive.name+'.sha256').read_text(), + f'{release_manifest.digest(archive)} {archive.name}\n') + info = release_manifest.build_info(archive) + self.assertEqual(archive.name, f"OpenPaperDesign-{info['version']}-{info['platform']}-{info['architecture']}" + + ('.tar.gz' if info['platform'] == 'linux' else '.zip')) + self.assertIs(info['automaticUpdates'], False) + executables = sorted((self.root/label).iterdir()) + self.assertEqual(info['inputBinaries'], {path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in executables}) + if archive.suffix == '.zip': + with zipfile.ZipFile(archive) as z: + members = {Path(name).name: z.read(name) for name in z.namelist() if not name.endswith('/')} + else: + with tarfile.open(archive, 'r:gz') as t: + members = {Path(member.name).name: t.extractfile(member).read() for member in t if member.isfile()} + for notice in ('LICENSE-MIT', 'LICENSE-APACHE', 'THIRD_PARTY.md', 'Inter-OFL.txt'): + self.assertEqual(members[notice], (SCRIPTS.parent/('assets/fonts/OFL.txt' if notice == 'Inter-OFL.txt' else notice)).read_bytes()) + if info['platform'] != 'macos': # macOS executables are re-signed inside the bundle + for path in executables: + self.assertEqual(members[path.name], path.read_bytes()) + + def overlap(self, platform, labels, arch='x64'): + """Run one package per label at once. Each waits until all have written their archive.""" + barrier = threading.Barrier(len(labels), timeout=120) + results = {} + def run(label): + try: + results[label] = self.run_package(platform, label, arch) + except BaseException as error: + results[label] = error + workers = [threading.Thread(target=run, args=(label,)) for label in labels] + with after_archive(platform, barrier.wait): + for worker in workers: worker.start() + for worker in workers: worker.join() + published = {label: result for label, result in results.items() if isinstance(result, Path)} + refused = [result for result in results.values() if isinstance(result, FileExistsError)] + self.assertEqual((len(published), len(refused)), (1, len(labels)-1), results) + (winner, archive), = published.items() + self.assertEqual(archive, self.expected(platform, arch)) + self.assert_package(archive, winner) + self.assertEqual(sorted(os.listdir(self.output)), [archive.name, archive.name+'.sha256']) + + def test_distinct_packages_publish_side_by_side(self): + for platform in ('linux', 'windows'): + with self.host(platform): + self.assertEqual(self.run_package(platform, platform), self.expected(platform)) + for platform in ('linux', 'windows'): + self.assert_package(self.expected(platform), platform) + self.assertEqual(len(os.listdir(self.output)), 4) + + def test_overlapping_runs_publish_exactly_one_complete_package(self): + for platform in ('linux', 'windows'): + self.output = self.root/f'dist-{platform}' + with self.subTest(platform=platform), self.host(platform): + self.overlap(platform, [f'{platform}-first', f'{platform}-second', f'{platform}-third']) + + @unittest.skipUnless(MAC_TOOLS, 'needs a Mac with sips, iconutil, codesign and ditto') + def test_overlapping_macos_runs_publish_exactly_one_complete_package(self): + for label in ('true', 'false'): + self.mac_executables(label) + self.overlap('macos', ['true', 'false'], 'universal') + + def test_only_finished_files_are_staged_beside_the_output(self): + for platform in ('linux', 'windows'): + self.output = self.root/f'dist-{platform}' + with self.subTest(platform=platform), self.host(platform): + self.assert_staging(platform, platform) + + @unittest.skipUnless(MAC_TOOLS, 'needs a Mac with sips, iconutil, codesign and ditto') + def test_macos_app_is_signed_in_the_system_temporary_directory_not_the_output(self): + self.mac_executables('true') + commands = [] + archive = self.assert_staging('macos', 'true', 'universal', commands) + system, output = Path(tempfile.gettempdir()).resolve(), self.output.resolve() + *_, product, built = next(command for command in commands if command[0] == 'ditto') + product, built = Path(product).resolve(), Path(built).resolve() + self.assertEqual((product.name, product.parent.parent), ('OpenPaperDesign.app', system)) + self.assertTrue(product.parent.name.startswith('opd-package-'), product) + self.assertFalse(product.is_relative_to(output), product) + self.assertFalse(product.parent.exists()) + signed = [Path(command[-1]).resolve() for command in commands if command[0] == 'codesign'] + self.assertEqual(len(signed), 3) # the bridge, the bundle, then its strict verification + for path in signed: + self.assertTrue(path.is_relative_to(product), path) + self.assertEqual((built.name, built.parent.parent), (archive.name, output)) + with tempfile.TemporaryDirectory() as extracted: + subprocess.run(['ditto', '-x', '-k', str(archive), extracted], check=True) + subprocess.run(['codesign', '--verify', '--deep', '--strict', f'{extracted}/OpenPaperDesign.app'], check=True) + + def test_published_package_is_never_replaced(self): + with self.host('linux'): + archive = self.run_package('linux', 'first') + before = self.snapshot() + with self.assertRaises(FileExistsError): + self.run_package('linux', 'second') + self.assertEqual(self.snapshot(), before) + self.assert_package(archive, 'first') + + def test_unpaired_file_blocks_publication_and_stays_untouched(self): + archive = self.expected('linux') + self.output.mkdir() + with self.host('linux'): + for existing in (archive, archive.with_name(archive.name+'.sha256')): + with self.subTest(existing=existing.name): + existing.write_bytes(b'kept') + before = self.snapshot() + with self.assertRaises(FileExistsError): + self.run_package('linux', 'run') + self.assertEqual(self.snapshot(), before) + existing.unlink() + + def test_checksum_appearing_during_the_run_withdraws_only_this_runs_archive(self): + checksum = self.expected('linux').with_name(self.expected('linux').name+'.sha256') + with self.host('linux'), after_archive('linux', lambda: checksum.write_bytes(b'other')): + with self.assertRaises(FileExistsError): + self.run_package('linux', 'run') + self.assertEqual(sorted(os.listdir(self.output)), [checksum.name]) + self.assertEqual(checksum.read_bytes(), b'other') + + def test_failed_run_changes_nothing_and_a_retry_publishes(self): + with self.host('windows'): + other = self.run_package('windows', 'other') + before = self.snapshot() + addfile, calls = tarfile.TarFile.addfile, [] + def full_disk(self, *args, **kwargs): + calls.append(1) + if len(calls) > 2: raise OSError(errno.ENOSPC, 'No space left on device') + return addfile(self, *args, **kwargs) + with self.host('linux'): + with mock.patch.object(tarfile.TarFile, 'addfile', full_disk), self.assertRaises(OSError) as failure: + self.run_package('linux', 'run') + self.assertEqual(failure.exception.errno, errno.ENOSPC) + self.assertEqual(self.snapshot(), before) + archive = self.run_package('linux', 'run') + self.assert_package(archive, 'run') + self.assert_package(other, 'other') + + def test_leftovers_of_a_killed_run_neither_block_a_retry_nor_get_removed(self): + leftover = self.output/f'.OpenPaperDesign-{VERSION}-linux-x64.killed.partial' + leftover.mkdir(parents=True) + (leftover/'half').write_bytes(b'half') + with self.host('linux'): + archive = self.run_package('linux', 'retry') + self.assert_package(archive, 'retry') + self.assertEqual((leftover/'half').read_bytes(), b'half') + self.assertEqual(sorted(os.listdir(self.output)), sorted([leftover.name, archive.name, archive.name+'.sha256'])) + + +if __name__ == '__main__': unittest.main() diff --git a/scripts/tests/test_release_manifest.py b/scripts/tests/test_release_manifest.py new file mode 100644 index 0000000..291e83e --- /dev/null +++ b/scripts/tests/test_release_manifest.py @@ -0,0 +1,46 @@ +import hashlib +import importlib.util +import io +import json +import tarfile +import tempfile +import unittest +import zipfile +from pathlib import Path + +spec = importlib.util.spec_from_file_location('release_manifest', Path(__file__).parents[1]/'release-manifest.py') +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +REV = 'a'*40 + + +class ReleaseManifest(unittest.TestCase): + def packages(self, root): + for platform, arch in [('macos','universal'), ('windows','x64'), ('linux','x64')]: + info = json.dumps({'dirty': False, 'revision': REV, 'platform': platform, + 'architecture': arch, 'version': '0.1.0'}).encode() + name = f'OpenPaperDesign-0.1.0-{platform}-{arch}' + path = root/(name+('.tar.gz' if platform == 'linux' else '.zip')) + if platform == 'linux': + with tarfile.open(path, 'w:gz') as archive: + member = tarfile.TarInfo(name+'/BUILD.json'); member.size=len(info) + archive.addfile(member, io.BytesIO(info)) + else: + with zipfile.ZipFile(path, 'w') as archive: archive.writestr(name+'/BUILD.json',info) + path.with_name(path.name+'.sha256').write_text(f'{hashlib.sha256(path.read_bytes()).hexdigest()} {path.name}\n') + return root + + def test_complete_exact_archives_and_damaged_or_incomplete_inputs(self): + with tempfile.TemporaryDirectory() as temporary: + root=self.packages(Path(temporary)) + value=module.manifest(root,'arcitai/openpaperdesign','v0.1.0-desktop.1',REV) + self.assertEqual(set(value['platforms']),{'macos','windows','linux'}) + self.assertEqual(value['platforms']['macos']['architecture'],'Intel + Apple Silicon') + with self.assertRaises(ValueError): module.manifest(root,'arcitai/openpaperdesign','v0.1.0-desktop.1','b'*40) + path=root/'OpenPaperDesign-0.1.0-windows-x64.zip' + path.write_bytes(b'damaged') + with self.assertRaises(ValueError): module.manifest(root,'arcitai/openpaperdesign','v0.1.0-desktop.1',REV) + path.unlink() + with self.assertRaises(ValueError): module.manifest(root,'arcitai/openpaperdesign','v0.1.0-desktop.1',REV) + +if __name__ == '__main__': unittest.main() diff --git a/site/DESIGN.md b/site/DESIGN.md new file mode 100644 index 0000000..343221e --- /dev/null +++ b/site/DESIGN.md @@ -0,0 +1,63 @@ +# OpenPaperDesign product page + +Revision 5 · 2026-10-28 · Owner’s latest direction supersedes the first Arc’IT-heavy page. + +## Job and direction +A canvas for everything. A free, open-source visual workspace for ideas, diagrams, interfaces, whiteboards and banners, usable alone or with a coding agent. Primary action: a clearly labelled desktop download. The page has its own neutral product identity; family relationships remain in Offers and the footer. + +## References, preserve and change +Sketch (https://sketch.rasmic.xyz/) supplies the product-first sequence: real editor demonstration, readable feature pairs, source and clear download status. Paper (https://paper.design/, inspected 28 October) supplies the confidence of large readable typography, generous media, coding-tool marks and visible product outcomes. Neither reference supplies code, screenshots, testimonials, branding, HTML/code export or two-way sync claims. + +Preserve the family’s calm navbar/footer, real Offers destinations, full mobile overlay and light/dark capability. Use neutral headlines and buttons with the owner-selected blue/ink canvas mark, use the family moon/sun in the navbar, remove tiny metadata from the hero, and use actual new UI/UX captures. Latest owner correction: use the same moon/sun as Arc’IT in the navbar; no footer theme control. + +## Sequence and hierarchy +Quiet header → broad headline and concrete uses → download and source → Codex/Claude Code compatibility → five real canvas examples (this website, system, interface, whiteboard, banner) → three feature pairs → actual local MCP explanation → files and origin → one primary platform download with visible text-link alternatives → grouped family footer and upstream credit. Product media up to 1280px; reading column 1120px. Desktop H1 up to 78px, body 18–21px, captions 14–16px. No gratuitous labels or fake customer proof. + +## Real content and limits +The captures use the new shared native workspace, rendered on Apple M4/Metal. Four retain the frozen PR9 source 1b04c31; the own-site study was recaptured at integrated 3286238 with the selected brand mark. They are development previews; the local archive preview uses integrated 3286238; no packaged public release of that UI exists yet. The site itself is an SVG design study imported into the real canvas with desktop/mobile compositions, editable vector layers and cropped native screenshot image layers, not a running HTML page. The banner is the owner-selected real LinkedIn artwork as an image layer, surrounded by editable vectors. Remaining samples are editable public illustrative diagrams, notes and layout studies. Exact source/fixture/binary hashes are in assets-sources.json. Never present these as a publicly released app before qualification. + +## Responsive and interaction choices +Above 800px use the quiet product header; at intermediate widths suppress the optional Features shortcut before crowding the brand. At 800px and below show an icon-only three-line menu and a full-viewport modal, Offers first. Escape/close restores focus; in-page navigation moves focus to the destination; lock background scroll while open. Stack feature/media/story pairs and platform actions. Gallery controls sit below the image, wrap visibly on compact views and keep the page free of horizontal overflow; the image opens at full size. No hover-only essential actions. Touch/mobile devices see a desktop explanation. A validated download is the primary action itself for a known desktop platform. The Mac archive is universal; never guess Apple CPU architecture. Mobile, ChromeOS, known unsupported ARM desktops and unknown devices get labelled choices. Missing or untrusted assets never become direct downloads. Show the same-origin manifest before reconciling GitHub metadata so an external lookup cannot delay a known download. + +## Acceptance +Inspect narrow/intermediate/wide layouts, both themes, relevant keyboard/menu states, actual files and all gallery choices. Check no accidental horizontal page overflow, adequate contrast and readable captions. The preview is not hardware certification. Preserve the source/upstream licences and individual brand ownership. Keep local ignored downloads out of deployment by exporting committed files with scripts/export-product-site.py. Domain/indexing/public downloads stay held until the exact latest-UI app and deployed destination are qualified. + +## Selected identity · owner, 2026-10-28 + +The owner selected overlapping canvas forms (round 2, option 1) with Arc’IT blue #4265a0 +and theme-aware black/white. Preserve the overlap and separation at small sizes. The +canonical light/dark SVGs are `assets/brand/openpaperdesign*.svg`; native Home PNGs, +plugin assets and the app icon derive from them through `scripts/render-brand-assets.cjs`. +The blue is the existing Arc’IT family accent; keep the page's neutral typography and +buttons. The app icon uses the inverse mark on a dark tile, while Home and the website +use the version suited to their theme. One selected mark across surfaces, never a different +logo for each host. Previous rejected explorations remain under `design/product/identity/`. +The root DESIGN.md remains canonical for the app and the relationship between surfaces. + +## Final landing-page refinement · 2026-10-28 + +Compared the actual Sketch and Paper pages, consulted Opus 5.5 High on the source and +kept the selected identity and real five-example gallery. Use one detected-OS action, visible alternative platform links and a short, visible +preview/install note. No download dropdowns or footer theme selector. Keep the current Mac +notarization limitation visible near its button. A small counter near the hero action +reports observed GitHub app-archive downloads, including repeats, excluding sources, +drafts and local CI previews. A successfully fetched source-only list means zero app +downloads; an API failure means unavailable, never a fabricated zero. + +The header uses normal three-column layout and an SVG Offers chevron, with keyboard, +Escape and outside/focus dismissal. Inline the selected canonical mark geometry once per placement, inheriting theme ink. +Do not pair light/dark image tags or swap their URLs after page load. Appearance starts with the system theme and the navbar moon/sun switches light/dark. The two-tier footer gives product, family links, project links and legal +credit their own room. Reuse the actual owner bread mark and portrait from the approved +family footer, plus Arc’IT's existing mark. Do not invent a replacement gustavonline logo. + +The observed preview regression mixed new HTML with cached pre-refinement CSS. Bind +stylesheets, the entry module and its download module to content-hash query versions +using scripts/refresh-site-versions.py; CI checks those bindings. The user's screenshot +of cramped footer links and two logo dots was a real receiving-tab failure even when a +fresh tab looked correct. Verify a reload of an existing tab as well as a fresh one. +Obsidian's download page supplies the additional reference for one platform action and +plain visible alternative downloads; do not copy its imagery, colour or features. + +GitHub asset metadata alone does not prove Apple notarization. Published Mac metadata therefore carries a visible “Apple notarization not verified” qualification, while the exact local preview states its known “Not Apple notarized” status. Do not remove either limitation during metadata reconciliation. + +The owner’s final correction supersedes the earlier footer appearance proposal: reuse Arc’IT’s exact CSS moon/sun in the navbar, beside the mobile menu. Keep both buttons at 44px targets and verify the brand still fits at 320px. diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..6a4d6af --- /dev/null +++ b/site/README.md @@ -0,0 +1,33 @@ +# Product page + +Static HTML/CSS/modules, locally served Geist, no framework, analytics or build step. Matches the Arc’IT / gustavonline / onlinesourdough family with its own blue canvas mark. + +```sh +python3 -m http.server 4388 --bind 127.0.0.1 --directory site +node --test scripts/tests/downloads.test.mjs +python3 scripts/sync-site-assets.py --check +``` + +Serve `site/` at a domain root. The intended public address remains **openpaperdesign.arcitai.com**, tracked by issue #20. Production currently remains noindex until a real release/domain is reviewed. This folder never serves the owner's editor or project library. + +## Downloads + +`releases.json` contains only reviewed actual release assets, with explicit platform/architecture and qualification. No executable means a source-build link, not a fake download. The primary action detects the OS and leads to explicitly labelled architecture choices. Mobile/iPad visitors see the desktop availability message. An optional ignored `releases.preview.json` is fetched **only on localhost** for actual files copied into ignored `site/downloads/`. It must never be deployed. The local preview can therefore demonstrate a built package without advertising it as a public release. + +A null download count is hidden. The counter reads measured GitHub release-asset totals and stays hidden until an app asset exists; checksums and source archives are not app downloads. A network failure keeps an explicit GitHub release fallback. Publication is separate from preparing artifacts. + +## Product images + +New UI/UX development-preview captures from `design/product/fixtures/` (`.excalidraw` and `.svg`). `scripts/make-product-fixtures.py` generates the editable public examples. Import them with `PAPER_RS_HOME` pointed at a disposable library, then render with the build's `--screenshot` command. See `assets-sources.json` for source revision, hashes and exact limitations. Use the new UI/UX source snapshot requested by the owner, with its revision and unreleased status explicit. Never present it as a publicly available desktop release. + +The shared SVG identity lives in `assets/brand/`. Site and plugin assets match the canonical source. Font and upstream licences are retained. The original product-page screenshot remains in Git history. + +## Review and publication + +Verify compact/intermediate/wide views, both themes, mobile overlay/focus/scroll-lock, Offers, preview tabs and real file downloads. Validate release and source destinations, current screenshot provenance, canonical/social metadata, HTTPS and indexing when publishing. Do not deploy the ignored local package directory or preview manifest. No hosting credentials or installed plugins are changed by this page. + +Export deployments with `python3 scripts/export-product-site.py --output /new/output/path`. This exports committed files from Git, refuses preview entries, and never uploads ignored local binaries. Do not upload the working `site/` directory directly. + +The download counter contacts `api.github.com` in the visitor’s browser; it sends no canvas data. Brand marks and owner artwork are credited separately and are not relicensed by the product’s MIT/Apache code licences. + +The review page links to the immutable integrated source revision `3286238`, which contains the actual new Home/workspace and native packaging, so guides work before merge and after branch deletion. When publishing a qualified release, update these guide URLs to that release tag. diff --git a/site/assets-sources.json b/site/assets-sources.json new file mode 100644 index 0000000..29bbb58 --- /dev/null +++ b/site/assets-sources.json @@ -0,0 +1,160 @@ +{ + "repository": "arcitai/openpaperdesign", + "source": "Pinned local captures, owner artwork and credited open-source assets; verified by SHA256.", + "assets": [ + { + "path": "assets/brand/openpaperdesign.svg", + "output": "assets/logo.svg", + "sha256": "0a2f7f605557aabc7826cbd9111f81a75f471cec4b4b567b258b0cbe134aa454", + "source": "Owner-selected overlapping canvas forms, 2026-10-28. Arc\u2019IT blue #4265a0 with theme-aware black/white. Original vector mark developed with Opus 5.5 High sparring." + }, + { + "path": "assets/brand/openpaperdesign-dark.svg", + "output": "assets/logo-dark.svg", + "sha256": "bfa7117c9c23ec3e06647a9233186236fd36efd1ff6ba78da75189d26a58cf6f", + "source": "Owner-selected overlapping canvas forms, 2026-10-28. Arc\u2019IT blue #4265a0 with theme-aware black/white. Original vector mark developed with Opus 5.5 High sparring." + }, + { + "path": "site/assets/product-architecture.png", + "output": "assets/product-architecture.png", + "sha256": "0e537d41cd1a36572da530f23649128d55857e24d36c904b3f94d6eb3623ade3", + "capture": { + "renderer": "native paper-rs / Apple M4 / Metal", + "sourceRevision": "1b04c312f90526a0daaf83e5ad6a8dfe47fde795", + "fixture": "design/product/fixtures/architecture.excalidraw", + "command": "paper-rs --screenshot OUTPUT PROJECT --size 1600x1000 --scale 1", + "limitations": "New UI/UX at the frozen PR9 source revision, rendered by the native Metal renderer. Development preview, not a released desktop app or proof of a full manual app journey.", + "fixtureSha256": "4b405225f8867be96a8f95954945f73f23063ef22c2ce1e1cdf8b62f7d37cd93", + "binarySha256": "384cccce85561625d2c6c55daf695f2949fbf6890f33d8a0a9ec50af11341060", + "importCommand": "PAPER_RS_HOME=DISPOSABLE_LIBRARY paper-rs --import design/product/fixtures/architecture.excalidraw" + } + }, + { + "path": "site/assets/product-design.png", + "output": "assets/product-design.png", + "sha256": "7973025656efd91957751092c990b54504dbb6a012197b8d2650492de15885c0", + "capture": { + "renderer": "native paper-rs / Apple M4 / Metal", + "sourceRevision": "1b04c312f90526a0daaf83e5ad6a8dfe47fde795", + "fixture": "design/product/fixtures/design.excalidraw", + "command": "paper-rs --screenshot OUTPUT PROJECT --size 1600x1000 --scale 1", + "limitations": "New UI/UX at the frozen PR9 source revision, rendered by the native Metal renderer. Development preview, not a released desktop app or proof of a full manual app journey.", + "fixtureSha256": "e9ddd63c7d11d2c775e99e98d60f4981144fa1684bfd330a7950a5bbfd694c13", + "binarySha256": "384cccce85561625d2c6c55daf695f2949fbf6890f33d8a0a9ec50af11341060", + "importCommand": "PAPER_RS_HOME=DISPOSABLE_LIBRARY paper-rs --import design/product/fixtures/design.excalidraw" + } + }, + { + "path": "site/assets/product-whiteboard.png", + "output": "assets/product-whiteboard.png", + "sha256": "d3346f053db63324628b68e1cba820003e27a926df04b1c5ba686a405fd2d3eb", + "capture": { + "renderer": "native paper-rs / Apple M4 / Metal", + "sourceRevision": "1b04c312f90526a0daaf83e5ad6a8dfe47fde795", + "fixture": "design/product/fixtures/whiteboard.excalidraw", + "command": "paper-rs --screenshot OUTPUT PROJECT --size 1600x1000 --scale 1", + "limitations": "New UI/UX at the frozen PR9 source revision, rendered by the native Metal renderer. Development preview, not a released desktop app or proof of a full manual app journey.", + "fixtureSha256": "b18b8fbf454335d65c8551626bfea636ff871544560401efb2fa43739e3d3ecf", + "binarySha256": "384cccce85561625d2c6c55daf695f2949fbf6890f33d8a0a9ec50af11341060", + "importCommand": "PAPER_RS_HOME=DISPOSABLE_LIBRARY paper-rs --import design/product/fixtures/whiteboard.excalidraw" + } + }, + { + "path": "site/assets/product-banner.png", + "output": "assets/product-banner.png", + "sha256": "42f02c84ef8bec80ea5dbcecef3a054924e08dadeac6428bf489a5bec2a0e55c", + "capture": { + "renderer": "native paper-rs / Apple M4 / Metal", + "sourceRevision": "1b04c312f90526a0daaf83e5ad6a8dfe47fde795", + "fixture": "design/product/fixtures/banner-study.svg", + "command": "paper-rs --screenshot OUTPUT PROJECT --size 1600x1000 --scale 1", + "limitations": "New UI/UX development snapshot. Banner artwork is a raster image layer; surrounding text is imported as vectors. Not a released desktop build.", + "artwork": "Owner-selected banner-B-matte, publicly used on LinkedIn; original owner artwork, no third-party UI copied.", + "fixtureSha256": "1d5d61b414bedeb2fd53930cd80c3dd7e98d106605c465ea5eea46b2f4e40bcf", + "binarySha256": "384cccce85561625d2c6c55daf695f2949fbf6890f33d8a0a9ec50af11341060", + "importCommand": "PAPER_RS_HOME=DISPOSABLE_LIBRARY paper-rs --import design/product/fixtures/banner-study.svg" + } + }, + { + "path": "site/assets/arcitai.svg", + "output": "assets/arcitai.svg", + "sha256": "e2e7db4b96105e5b2f7a87d4295adbe788a9af6577faa03aac980c239306cd93", + "source": "Canonical owner mark from arcitai-website/public/assets/arcitai-mark-final.svg" + }, + { + "path": "site/assets/codex.svg", + "output": "assets/codex.svg", + "sha256": "4a2f43ce46b5b6e3722c95088f88d26ef91e6a8c2e598e70642a1c54367386e4", + "source": "Lobe Icons static SVG collection, MIT; brand marks remain with their respective owners; used for compatibility, not endorsement.", + "sourceUrl": "https://github.com/lobehub/lobe-icons/blob/8b7bbc433d38437fa2b0cca58ae896917162b812/packages/static-svg/icons/codex-color.svg", + "sourceRevision": "8b7bbc433d38437fa2b0cca58ae896917162b812" + }, + { + "path": "site/assets/claude-code.svg", + "output": "assets/claude-code.svg", + "sha256": "670b3d8d749d0815ad8f7e62a59d51cb5e2053cb19403f3541a8aed7036a877f", + "source": "Lobe Icons static SVG collection, MIT; brand marks remain with their respective owners; used for compatibility, not endorsement.", + "sourceUrl": "https://github.com/lobehub/lobe-icons/blob/8b7bbc433d38437fa2b0cca58ae896917162b812/packages/static-svg/icons/claudecode-color.svg", + "sourceRevision": "8b7bbc433d38437fa2b0cca58ae896917162b812" + }, + { + "path": "site/assets/product-own-site.png", + "output": "assets/product-own-site.png", + "sha256": "1c05ed15c07646d6d4b861545bf04518b5230ece0a7e6b659b0b00c53cebda32", + "capture": { + "renderer": "native paper-rs / Apple M4 / Metal", + "sourceRevision": "328623852a062023348275ecb3c51dea11abf5ad", + "fixture": "design/product/fixtures/product-page.svg", + "fixtureSha256": "c6b8c683a0d3bfdabeb15299087a5a4660a4f09b07cfee38eee50255cdf50396", + "binarySha256": "87aaa0e109d3865083948ea2f5663cf5d0a9457d228336e51f48c8ba32670d2d", + "limitations": "Native Metal capture of the imported editable SVG study at the integrated new-UI revision, including the selected brand mark. Source project and capture are development previews; the native product UI surrounds the study.", + "importCommand": "PAPER_RS_HOME=DISPOSABLE_LIBRARY paper-rs --import design/product/fixtures/product-page.svg", + "command": "openpaperdesign --screenshot OUTPUT PROJECT --size 1920x1200 --scale 1" + } + }, + { + "path": "site/assets/licenses/Lobe-Icons-LICENSE", + "output": "assets/licenses/Lobe-Icons-LICENSE", + "sha256": "add9d7531d1b21646317a8958e38fc727506fa39d24bdecb44154d943c82753a" + }, + { + "path": "site/assets/fonts/Geist-LICENSE.txt", + "output": "assets/fonts/Geist-LICENSE.txt", + "sha256": "930853ee1daa68554d9e35c8a9175affb74f699fad9a5da6ee5ebe76379d9137" + }, + { + "path": "site/assets/fonts/Inter-LICENSE.txt", + "output": "assets/fonts/Inter-LICENSE.txt", + "sha256": "3b0a5fca3d17942cde889069889dedbbbd075e9b599968c82a95f4d944e9b345" + }, + { + "path": "site/assets/NOTICE.md", + "output": "assets/NOTICE.md", + "sha256": "759d33b17e4811929dc399d8d2fe9a28438f9d3735a63d73f60b44cbe003e445" + }, + { + "path": "assets/brand/home-logo-light.png", + "output": "assets/brand/home-logo-light.png", + "sha256": "555ec6275fe6562395cfeb62bde8bf2303360c0f1325206938c2e800a23fe81f", + "source": "Rendered from the canonical SVG for the shared native Home. Same mark, not a separate identity." + }, + { + "path": "assets/brand/home-logo-dark.png", + "output": "assets/brand/home-logo-dark.png", + "sha256": "a24e82288d732edd92571aa94bba8ec45bcc8176fb6ce23a5d7b1ad7ccd53b20", + "source": "Rendered from the canonical SVG for the shared native Home. Same mark, not a separate identity." + }, + { + "path": "site/assets/onlinesourdough.svg", + "output": "assets/onlinesourdough.svg", + "sha256": "9d6fc30b46611e4eb91bcdb1573f8192ee5652ec860979cf4ead605d28532f34", + "source": "Approved owner large-pixel bread mark, reused unchanged from gustavonline/public/assets/brand-marks/onlinesourdough.svg (production snapshot 2026-10-25). Rendered as a theme-aware mask; geometry unchanged." + }, + { + "path": "site/assets/gustavonline.jpg", + "output": "assets/gustavonline.jpg", + "sha256": "85f1ef31fd1243277ffee1e4ad78db187be1275d242f399b4dfad20bd7f847dd", + "source": "Approved owner portrait used by FamilyMark in the onlinesourdough family footer, production snapshot 2026-10-25. Reused unchanged from onlinesourdough/public/assets/gustav-linkedin-portrait.jpg." + } + ] +} diff --git a/site/assets/NOTICE.md b/site/assets/NOTICE.md new file mode 100644 index 0000000..6909117 --- /dev/null +++ b/site/assets/NOTICE.md @@ -0,0 +1,7 @@ +# Product-page assets + +OpenPaperDesign is an independent fork of paper-rs / Sketch by Michael Shimeles. The website uses original OpenPaperDesign workspace captures, not Paper.design or Sketch marketing images. The newer UI is a development snapshot, identified in assets-sources.json. + +The Codex and Claude Code SVGs come from Lobe Icons (Copyright 2023 LobeHub, MIT), with the licence in licenses/Lobe-Icons-LICENSE. Their product names and marks belong to OpenAI and Anthropic respectively. They identify optional local MCP clients, with no partnership or endorsement implied. + +Arc’IT AI, onlinesourdough, gustavonline and the LinkedIn banner are the owner's existing brand assets/artwork. The MIT/Apache code licences do not grant trademark rights in any brand displayed here. Inter and Geist font notices are retained alongside their corresponding assets. diff --git a/site/assets/arcitai.svg b/site/assets/arcitai.svg new file mode 100644 index 0000000..e15bc69 --- /dev/null +++ b/site/assets/arcitai.svg @@ -0,0 +1,12 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="4 -4 60 60" role="img" aria-labelledby="title description"> + <title id="title">Arc’It AI Cut Six Refined + The selected Arc’It mark: three sheared rail modules and three offset square terminals. + + + + + + + + + diff --git a/site/assets/brand/home-logo-dark.png b/site/assets/brand/home-logo-dark.png new file mode 100644 index 0000000..fad8393 Binary files /dev/null and b/site/assets/brand/home-logo-dark.png differ diff --git a/site/assets/brand/home-logo-light.png b/site/assets/brand/home-logo-light.png new file mode 100644 index 0000000..49e46bf Binary files /dev/null and b/site/assets/brand/home-logo-light.png differ diff --git a/site/assets/canvas-v0.1.0.jpg b/site/assets/canvas-v0.1.0.jpg new file mode 100644 index 0000000..9e3a599 Binary files /dev/null and b/site/assets/canvas-v0.1.0.jpg differ diff --git a/site/assets/claude-code.svg b/site/assets/claude-code.svg new file mode 100644 index 0000000..ed1293c --- /dev/null +++ b/site/assets/claude-code.svg @@ -0,0 +1 @@ +Claude Code \ No newline at end of file diff --git a/site/assets/codex.svg b/site/assets/codex.svg new file mode 100644 index 0000000..c77ccfd --- /dev/null +++ b/site/assets/codex.svg @@ -0,0 +1 @@ +Codex \ No newline at end of file diff --git a/site/assets/fonts/Geist-LICENSE.txt b/site/assets/fonts/Geist-LICENSE.txt new file mode 100644 index 0000000..8d003fe --- /dev/null +++ b/site/assets/fonts/Geist-LICENSE.txt @@ -0,0 +1,92 @@ +Copyright (c) 2023 Vercel, in collaboration with basement.studio + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/site/assets/fonts/Geist-SOURCE.txt b/site/assets/fonts/Geist-SOURCE.txt new file mode 100644 index 0000000..3664a37 --- /dev/null +++ b/site/assets/fonts/Geist-SOURCE.txt @@ -0,0 +1,7 @@ +Geist Sans variable font, reused unchanged from Arc’IT’s bundled font asset. +Repository: https://github.com/arcitai/arcitai-website +Revision: 46e5566e5f4d1f89fc36df43cd5a35c79ff504f5 +Path: public/assets/fonts/geist-sans-variable.woff2 +Upstream: https://github.com/vercel/geist-font +Licence: SIL Open Font License 1.1; see Geist-LICENSE.txt. +Served locally; no external font service is used. diff --git a/site/assets/fonts/Inter-LICENSE.txt b/site/assets/fonts/Inter-LICENSE.txt new file mode 100644 index 0000000..40589da --- /dev/null +++ b/site/assets/fonts/Inter-LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/site/assets/fonts/Inter-SOURCE.txt b/site/assets/fonts/Inter-SOURCE.txt new file mode 100644 index 0000000..104e089 --- /dev/null +++ b/site/assets/fonts/Inter-SOURCE.txt @@ -0,0 +1,7 @@ +Inter variable font, Latin subset, normal style, weights 100–900. +Source: @fontsource-variable/inter 5.3.0 +https://www.npmjs.com/package/@fontsource-variable/inter/v/5.3.0 +Original file: files/inter-latin-wght-normal.woff2 +Upstream: https://github.com/rsms/inter +License: SIL Open Font License 1.1; see Inter-LICENSE.txt. +Served locally; no external font service is used. diff --git a/site/assets/fonts/geist-sans-variable.woff2 b/site/assets/fonts/geist-sans-variable.woff2 new file mode 100644 index 0000000..b2f0121 Binary files /dev/null and b/site/assets/fonts/geist-sans-variable.woff2 differ diff --git a/site/assets/fonts/inter-latin-variable.woff2 b/site/assets/fonts/inter-latin-variable.woff2 new file mode 100644 index 0000000..d15208d Binary files /dev/null and b/site/assets/fonts/inter-latin-variable.woff2 differ diff --git a/site/assets/gustavonline.jpg b/site/assets/gustavonline.jpg new file mode 100644 index 0000000..e40d0de Binary files /dev/null and b/site/assets/gustavonline.jpg differ diff --git a/site/assets/licenses/LICENSE-APACHE b/site/assets/licenses/LICENSE-APACHE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/site/assets/licenses/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/site/assets/licenses/LICENSE-MIT b/site/assets/licenses/LICENSE-MIT new file mode 100644 index 0000000..57ae8c3 --- /dev/null +++ b/site/assets/licenses/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Michael Shimeles + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/site/assets/licenses/Lobe-Icons-LICENSE b/site/assets/licenses/Lobe-Icons-LICENSE new file mode 100644 index 0000000..1dd53d2 --- /dev/null +++ b/site/assets/licenses/Lobe-Icons-LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 LobeHub + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/site/assets/licenses/THIRD_PARTY.md b/site/assets/licenses/THIRD_PARTY.md new file mode 100644 index 0000000..158a2d2 --- /dev/null +++ b/site/assets/licenses/THIRD_PARTY.md @@ -0,0 +1,58 @@ +# Third-party code and assets + +paper-rs is MIT OR Apache-2.0. It ports a few algorithms from MIT-licensed projects (listed +below with their copyright notices and license text) and bundles the Inter font (SIL OFL 1.1, +`assets/fonts/OFL.txt`). Figma and Paper are proprietary: only their *behavior* was studied +(`analysis/FIGMA_NOTES.md`, `analysis/PAPER_ARCHITECTURE.md`), and no code, shaders, icons or +assets from them are used. + +## Ported code + +| Project | Copyright | What was ported | Where | +|---|---|---|---| +| [rough.js](https://github.com/rough-stuff/rough) 4.6.4 | Copyright (c) 2019 Preet Shihn | Seeded RNG, `_line` / `_doubleLine`, curves, ellipses, `svgPath`, solid / hachure / cross-hatch / zigzag / dots fills, generator option handling. Verified against rough.js output (`tests/fixtures/rough/`). | `src/render/rough.rs` | +| [hachure-fill](https://github.com/pshihn/hachure-fill) 0.5.2 | Copyright (c) 2023 Preet Shihn | Scan-line hachure algorithm | `src/render/rough.rs` | +| [points-on-curve](https://github.com/pshihn/bezier-points) 0.2.0, [points-on-path](https://github.com/pshihn/points-on-path) 0.2.1 | Copyright (c) 2020 Preet Shihn | Bezier flattening by flatness, Ramer–Douglas–Peucker, `curveToBezier`, path point sets | `src/render/rough.rs` | +| [Excalidraw](https://github.com/excalidraw/excalidraw) | Copyright (c) 2020 Excalidraw | Element → rough options (`adjustRoughness`, dash patterns, fill weights), binding gap and fixed-point rules, elbow-arrow grid/A* router with its segment-count heuristic, elbow corner rounding, arrowhead geometry, laser trail decay constants, sticky-note sizing, `.excalidraw` / `.excalidrawlib` formats | `src/render/rough.rs`, `src/connector.rs`, `src/editor/whiteboard.rs`, `src/io/excalidraw.rs` | + +Each ported module starts with a comment naming its source. + +## Dependencies + +Rust crates are pulled from crates.io under their own licenses (see `Cargo.lock`); notable ones +are wgpu, winit, egui, lyon, ttf-parser, image, i_overlay (MIT), serde and rfd. + +## MIT License (applies to each project listed above, with its copyright line) + +``` +MIT License + +Copyright (c) 2019 Preet Shihn (rough.js) +Copyright (c) 2020 Preet Shihn (points-on-curve, points-on-path, path-data-parser) +Copyright (c) 2023 Preet Shihn (hachure-fill) +Copyright (c) 2020 Excalidraw (Excalidraw) +Copyright (c) 2023 Excalidraw (@excalidraw/laser-pointer) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +## Fonts + +Inter 4.1 by Rasmus Andersson — SIL Open Font License 1.1 (`assets/fonts/OFL.txt`). Regular, +Medium, SemiBold, Bold, Italic and Bold Italic are bundled unmodified. diff --git a/site/assets/logo-dark.svg b/site/assets/logo-dark.svg new file mode 100644 index 0000000..c8bb07f --- /dev/null +++ b/site/assets/logo-dark.svg @@ -0,0 +1 @@ +OpenPaperDesign diff --git a/site/assets/logo.svg b/site/assets/logo.svg new file mode 100644 index 0000000..71eb3d1 --- /dev/null +++ b/site/assets/logo.svg @@ -0,0 +1 @@ +OpenPaperDesign diff --git a/site/assets/onlinesourdough.svg b/site/assets/onlinesourdough.svg new file mode 100644 index 0000000..e88fbfa --- /dev/null +++ b/site/assets/onlinesourdough.svg @@ -0,0 +1,80 @@ + + onlinesourdough large-pixel boule + + A wide sourdough boule assembled from large square pixels with three diagonal + score cuts. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/site/assets/product-architecture.png b/site/assets/product-architecture.png new file mode 100644 index 0000000..d0fbf5e Binary files /dev/null and b/site/assets/product-architecture.png differ diff --git a/site/assets/product-banner.png b/site/assets/product-banner.png new file mode 100644 index 0000000..ba88d67 Binary files /dev/null and b/site/assets/product-banner.png differ diff --git a/site/assets/product-design.png b/site/assets/product-design.png new file mode 100644 index 0000000..4591e42 Binary files /dev/null and b/site/assets/product-design.png differ diff --git a/site/assets/product-own-site.png b/site/assets/product-own-site.png new file mode 100644 index 0000000..19b68f9 Binary files /dev/null and b/site/assets/product-own-site.png differ diff --git a/site/assets/product-whiteboard.png b/site/assets/product-whiteboard.png new file mode 100644 index 0000000..1883391 Binary files /dev/null and b/site/assets/product-whiteboard.png differ diff --git a/site/downloads.js b/site/downloads.js new file mode 100644 index 0000000..c8e8c08 --- /dev/null +++ b/site/downloads.js @@ -0,0 +1,57 @@ +export function detectPlatform(platform='',ua='',touch=0){ + if(/iPhone|iPad|iPod|Android/i.test(ua)||(/Mac/i.test(platform)&&touch>1))return 'mobile'; + if(/CrOS/i.test(ua)||(/Linux|Win/i.test(platform)&&/aarch64|arm(?:64|v[5-9])/i.test(platform+' '+ua)))return 'unknown'; + if(/Win/i.test(platform))return 'windows';if(/Mac/i.test(platform))return 'macos';if(/Linux/i.test(platform))return 'linux';return 'unknown'; +} +export function allowedDownload(url,local=false){ + if(typeof url!=='string')return false; + if(local&&/^\/downloads\/[A-Za-z0-9_.-]+$/.test(url))return true; + try{const u=new URL(url);return u.protocol==='https:'&&u.hostname==='github.com'&&u.pathname.startsWith('/arcitai/openpaperdesign/releases/download/')&&!u.username&&!u.password;}catch{return false;} +} +export function selectAsset(manifest,platform,local=false){ + if(!['macos','windows','linux'].includes(platform))return null; + const asset=manifest?.platforms?.[platform];return asset&&allowedDownload(asset.url,local)&&typeof asset.architecture==='string'&&typeof asset.status==='string'?asset:null; +} +// A known, validated asset is the primary action itself, not an anchor to a +// second download chooser. Mobile, unsupported CPUs and missing builds stay safe. +export function downloadChoice(manifest,platform,local=false){ + const asset=selectAsset(manifest,platform,local); + if(!asset)return {asset:null,label:platform==='mobile'?'Choose your desktop version ↓':'Choose your version ↓',href:'#download',meta:''}; + const name={macos:'Mac',windows:'Windows',linux:'Linux'}[platform]; + const caution=platform!=='macos'?'':/not(?: Apple)? notarized/i.test(asset.status)?' · Not Apple notarized':asset.notarized===true?'':' · Apple notarization not verified'; + return {asset,label:`Download for ${name} ↓`,href:asset.url,meta:`Preview · ${asset.architecture}${caution}`}; +} +export function appDownloadCount(releases){ + if(!Array.isArray(releases))throw new Error('Invalid release list'); + let count=0,matched=0; + for(const release of releases){ + if(release.draft)continue; + for(const asset of release.assets||[]){ + if(!/^OpenPaperDesign-[0-9][A-Za-z0-9.-]*-(?:macos-(?:arm64|x64|universal)|windows-x64|linux-x64)\.(?:zip|dmg|tar\.gz)$/.test(asset.name))continue; + if(!Number.isSafeInteger(asset.download_count)||asset.download_count<0)throw new Error('Invalid download count'); + count+=asset.download_count; + matched++; + if(!Number.isSafeInteger(count))throw new Error('Download count overflow'); + } + } + return matched?count:null; +} +// GitHub already supplies the published asset metadata. A draft, source-only release, +// candidate branch or incomplete platform set must never become an install button. +export function publishedManifest(releases){ + if(!Array.isArray(releases))return null; + for(const release of releases){ + if(release.draft||!/^v[0-9][A-Za-z0-9.-]*$/.test(release.tag_name||''))continue; + const assets=release.assets||[], platforms={};let version=null,invalid=false; + for(const [platform,target,extension] of [['macos','macos-universal','zip'],['windows','windows-x64','zip'],['linux','linux-x64','tar.gz']]){ + const candidates=assets.filter(a=>typeof a.name==='string'&&a.name.startsWith('OpenPaperDesign-')&&a.name.endsWith(`-${target}.${extension}`)); + if(candidates.length!==1){invalid=true;break;} + const asset=candidates[0],current=asset.name.slice(16,-(`-${target}.${extension}`.length)); + if(!/^[0-9][A-Za-z0-9.-]*$/.test(current)||(version&¤t!==version)||!(release.tag_name===`v${current}`||release.tag_name.startsWith(`v${current}-`))||!Number.isSafeInteger(asset.size)||asset.size<=0||!allowedDownload(asset.browser_download_url)||!assets.some(a=>a.name===asset.name+'.sha256'&&a.size>0&&allowedDownload(a.browser_download_url))){invalid=true;break;} + version=current; + platforms[platform]={url:asset.browser_download_url,architecture:platform==='macos'?'Intel + Apple Silicon':'x64',status:platform==='macos'?'Desktop preview · Apple notarization not verified':'Desktop preview · see release notes for test status'}; + } + if(!invalid)return {version,platforms,notice:'Desktop preview. Apple notarization is not verified by GitHub asset metadata. Check the release notes for signing, installation and test status, and verify the archive checksum.'}; + } + return null; +} diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..8d53882 --- /dev/null +++ b/site/index.html @@ -0,0 +1,22 @@ + +OpenPaperDesign | A canvas for everything + + + +

A canvas for everything.

Map a system, design a screen, think on a whiteboard or lay out a banner. One free, open-source canvas for all of it.

No subscription. No account needed.

Checking downloads…
Works with your coding toolsCodexClaude Code
+
OpenPaperDesign’s own website design, with desktop and mobile layouts, inside the new workspace.
A design study of this website, in OpenPaperDesign. New UI/UX preview.
+

One canvas. Room to think.

From a rough idea
to something real.

Keep the sketch, the system and the screen together. Refine the same objects as the idea takes shape.

Six-colour sticky notes and hand-drawn shapes in the real editor.

Start with the messy part.

Notes in six colours, freehand drawing and hand-drawn shapes. Get the idea down before you worry about the finish.

An editable architecture diagram with named components and arrows.

Make the system visible.

Map services, data and how the parts of a system fit together. Shapes, text and arrows stay editable as your understanding changes.

Public brand and interface studies on the native canvas.

Design the screen, too.

Frames, layouts, components, vector paths and image fills. Work on the interface beside the diagram that explains it.

+

You + your AI agent

Your agent. Your canvas.

Tell Codex or Claude Code what you want to make. Through local MCP, your agent can read and change the same canvas you edit by hand. Sketch a flow, explore a layout and refine it together.

Connect an agent ↗
CodexClaude Code
Your canvasLocal MCP · editable layers
+

Your files stay yours

Save it. Open it.
Keep working.

Projects are folders of JSON and assets on your computer. Export PNG, SVG or PDF, and bring in SVG and Excalidraw files.

Use the standalone app or open a canvas alongside your coding agent. Keep your projects on your computer, ready to come back to.

Why I built this

I wanted a canvas.
Not another subscription.

I wanted somewhere to sketch systems and design interfaces with an AI agent, without paying for another design tool.

Michael Shimeles (Rasmic) built the native canvas I could start from. OpenPaperDesign builds on his paper-rs / Sketch work, with the integrations and design workflow I use in my own projects.

Gustav Anderson · gustavonline ↗
+
+

Free & open source

Your next canvas starts here.

+

A desktop app for Mac, Windows and Linux. No subscription. No account needed.

+ + +

The screenshots show our new UI/UX development build. Packaged previews are being qualified. Check release notes before installing.

+
+ diff --git a/site/releases.json b/site/releases.json new file mode 100644 index 0000000..32bddc0 --- /dev/null +++ b/site/releases.json @@ -0,0 +1 @@ +{"version":"0.1.0","platforms":{},"downloads":null,"countedAt":null,"notice":"Desktop preview packaging is in progress. The original v0.1.0 release is source code, not an app installer. Available builds will be listed here with their actual test status."} diff --git a/site/site.js b/site/site.js new file mode 100644 index 0000000..ab3e686 --- /dev/null +++ b/site/site.js @@ -0,0 +1,85 @@ +import { detectPlatform, selectAsset, downloadChoice, appDownloadCount, publishedManifest } from './downloads.js?v=351db479d439'; +const root=document.documentElement; +const themeButton=document.querySelector('#theme-toggle'); +const system=matchMedia('(prefers-color-scheme: dark)'); +let preference; +try {preference=localStorage.getItem('openpaperdesign-site-theme');}catch{} +if(!['dark','light'].includes(preference))preference=null; +function applyTheme(){root.dataset.theme=preference||(system.matches?'dark':'light');const next=root.dataset.theme==='dark'?'light':'dark';themeButton.setAttribute('aria-label',`Switch to ${next} mode`);themeButton.title=`Switch to ${next} theme`;} +applyTheme(); +themeButton.addEventListener('click',()=>{preference=root.dataset.theme==='dark'?'light':'dark';applyTheme();try{localStorage.setItem('openpaperdesign-site-theme',preference);}catch{}}); +system.addEventListener('change',()=>{if(!preference)applyTheme();}); +const dialog=document.querySelector('#mobile-navigation'); +const menuButton=document.querySelector('#menu-toggle'); +let previousOverflow='',navigationTarget=null; +menuButton.addEventListener('click',()=>{previousOverflow=document.body.style.overflow;dialog.showModal();document.body.style.overflow='hidden';menuButton.setAttribute('aria-expanded','true');}); +function closeMenu(){if(dialog.open)dialog.close();} +document.querySelector('#menu-close').addEventListener('click',closeMenu); +dialog.addEventListener('close',()=>{document.body.style.overflow=previousOverflow;menuButton.setAttribute('aria-expanded','false');if(navigationTarget){navigationTarget.setAttribute('tabindex','-1');navigationTarget.focus({preventScroll:true});navigationTarget=null;}else if(menuButton.getClientRects().length){menuButton.focus();}}); +dialog.querySelectorAll('a').forEach(a=>a.addEventListener('click',()=>{navigationTarget=a.origin===location.origin&&a.pathname===location.pathname&&a.hash?document.getElementById(decodeURIComponent(a.hash.slice(1))):null;closeMenu();})); +matchMedia('(min-width:800.02px)').addEventListener('change',e=>{if(e.matches)closeMenu();}); +const offers=document.querySelector('.offers'); +document.addEventListener('click',e=>{if(!offers.contains(e.target))offers.open=false;}); +document.addEventListener('keydown',e=>{if(e.key==='Escape'&&offers.open){offers.open=false;offers.querySelector('summary').focus();}}); +offers.addEventListener('focusout',e=>{if(!offers.contains(e.relatedTarget))offers.open=false;}); +const previews={'own-site':['A design study of this website, in OpenPaperDesign. New UI/UX preview.','OpenPaperDesign’s own website, with desktop and mobile layout studies, inside the new workspace.'],architecture:['New UI/UX preview · development build. Real canvas, editable objects.','The new OpenPaperDesign workspace with an editable system diagram.'],design:['Interface studies in the new workspace. Development preview.','Public website and tile studies in the new OpenPaperDesign UI.'],whiteboard:['Six colours of sticky notes and hand-drawn shapes. New UI/UX development preview.','Coloured sticky notes and hand-drawn shapes in the new workspace.'],banner:['A tile banner as an image layer, beside editable vector layers. New UI/UX preview.','The selected gustavonline tile banner in the new OpenPaperDesign workspace.']}; +document.querySelectorAll('[data-preview]').forEach(button=>button.addEventListener('click',()=>{const key=button.dataset.preview;if(!previews[key])return;document.querySelectorAll('[data-preview]').forEach(x=>x.setAttribute('aria-pressed',String(x===button)));const image=document.querySelector('#product-screenshot');image.src=`/assets/product-${key}.png`;document.querySelector('#preview-open').href=image.src;image.alt=previews[key][1];document.querySelector('#preview-caption').textContent=previews[key][0];})); +const platform=detectPlatform(navigator.userAgentData?.platform||navigator.platform,navigator.userAgent,navigator.maxTouchPoints); +const local=['127.0.0.1','localhost'].includes(location.hostname); +const platformLabels={macos:['Mac','Intel + Apple Silicon'],windows:['Windows','x64'],linux:['Linux','x64']}; +const buildGuide='https://github.com/arcitai/openpaperdesign/blob/328623852a062023348275ecb3c51dea11abf5ad/docs/openpaperdesign/desktop.md#build-from-source'; +function choiceForCurrentPlatform(key,manifest){return key===platform&&selectAsset(manifest,platform,local);} +function renderDownloads(manifest){ + const list=document.querySelector('#platform-downloads');list.replaceChildren(); + for(const [key,[name,arch]] of Object.entries(platformLabels)){ + const asset=selectAsset(manifest,key,local); + const action=document.createElement('a');action.href=asset?asset.url:buildGuide;action.hidden=Boolean(choiceForCurrentPlatform(key,manifest));action.append(document.createTextNode(name)); + const detail=document.createElement('span');detail.textContent=asset?`${asset.architecture} ↓`:`Build from source · ${arch} ↗`;action.append(detail); + action.setAttribute('aria-label',asset?`Download ${name} preview for ${asset.architecture}`:`Build ${name} from source`);action.title=asset?.status||'No packaged release yet';list.append(action); + } + const choice=downloadChoice(manifest,platform,local),primary=document.querySelector('#primary-download'); + primary.textContent=choice.label;primary.href=choice.href; + const selected=document.querySelector('#download-primary');selected.hidden=!choice.asset; + if(choice.asset){ + const action=document.querySelector('#desktop-download');action.textContent=choice.label;action.href=choice.href; + document.querySelector('#download-meta').textContent=choice.meta; + document.querySelector('#release-note').textContent=choice.meta; + }else document.querySelector('#release-note').textContent=platform==='mobile'?'For Mac, Windows and Linux. Choose your desktop version below.':'Free and open source. No subscription or account needed.'; + if(typeof manifest.notice==='string')document.querySelector('#download-help').textContent=manifest.notice; +} +async function loadDownloads(){ + let manifest={platforms:{}}; + try{ + const response=await fetch('/releases.json');if(!response.ok)throw new Error('Release manifest unavailable'); + manifest=await response.json(); + }catch{manifest.notice='Release information could not be loaded. Check GitHub releases for the latest downloads.';} + if(local){ + try{const preview=await fetch('/releases.preview.json');if(preview.ok)manifest=await preview.json();}catch{/* The committed manifest remains the safe fallback. */} + } + // Show the same-origin manifest first. A slow/rate-limited GitHub lookup must not + // delay a known download. Reconcile published assets independently afterwards. + renderDownloads(manifest); + if(!local){const published=publishedManifest(await releasesLookup);if(published)renderDownloads(published);} +} +async function publishedReleases(){ + const releases=[]; + for(let page=1;page<=5;page++){ + const response=await fetch(`https://api.github.com/repos/arcitai/openpaperdesign/releases?per_page=100&page=${page}`,{headers:{Accept:'application/vnd.github+json'},signal:AbortSignal.timeout(6000)}); + if(!response.ok)throw new Error('GitHub release metadata unavailable'); + const batch=await response.json();if(!Array.isArray(batch))throw new Error('Invalid release list'); + releases.push(...batch);if(batch.length<100)return releases; + } + throw new Error('Release list exceeds the bounded lookup'); +} +const releasesLookup=publishedReleases().catch(()=>null); +async function showObservedDownloads(){ + const count=document.querySelector('#download-count'); + const releases=await releasesLookup; + if(!releases){count.textContent='Download count unavailable';return;} + const total=appDownloadCount(releases)??0; + count.textContent=`${total.toLocaleString()} ${total===1?'download':'downloads'}`; + count.title='App archives downloaded from GitHub releases, including repeat downloads. Source code and local previews are excluded.'; + count.setAttribute('aria-label',`${count.textContent} from GitHub app releases, including repeat downloads`); +} +showObservedDownloads().catch(()=>{document.querySelector('#download-count').textContent='Download count unavailable';}); +loadDownloads().catch(()=>{document.querySelector('#download-help').textContent='Release information could not be loaded. Check GitHub releases for the latest downloads.';}); diff --git a/site/styles.css b/site/styles.css new file mode 100644 index 0000000..70cf60f --- /dev/null +++ b/site/styles.css @@ -0,0 +1,55 @@ +@font-face{font-family:Geist;src:url('/assets/fonts/geist-sans-variable.woff2') format('woff2');font-weight:100 900;font-display:swap} +:root{color-scheme:light;--paper:#fbfaf8;--surface:#fff;--ink:#232321;--muted:#656560;--line:#deded8;--wash:#f0efeb;font:18px/1.55 Geist,Arial,sans-serif;color:var(--ink);background:var(--paper);scroll-padding-top:100px} +:root[data-theme=dark]{color-scheme:dark;--paper:#181817;--surface:#222220;--ink:#f4f3ee;--muted:#b8b7ae;--line:#3b3b35;--wash:#22221f} +*{box-sizing:border-box}body{margin:0}[hidden]{display:none!important}button{font:inherit;cursor:pointer;color:inherit}a{color:inherit;text-decoration:none;text-underline-offset:4px}a:hover{text-decoration:underline}img{max-width:100%;height:auto}h1,h2,h3,p,figure{margin:0}h1,h2,h3{font-weight:500;letter-spacing:-.045em;line-height:1.09}h2{font-size:clamp(34px,4vw,48px)}h3{font-size:32px}p{color:var(--muted)}:focus-visible{outline:2px solid var(--ink);outline-offset:5px}.page-width,.header-inner{width:calc(100% - 64px);max-width:1120px;margin-inline:auto} +.site-header{position:sticky;top:0;background:var(--paper);border-bottom:1px solid var(--line);z-index:20}.header-inner{min-height:80px;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:24px;max-width:1280px}.wordmark:hover{text-decoration:none}.wordmark{font-size:21px;display:inline-flex;align-items:center;gap:11px;letter-spacing:-.04em;white-space:nowrap}.product-mark{display:inline-block;flex:none;vertical-align:middle;color:var(--ink)}.header-links{justify-self:end;display:flex;gap:28px;font-size:16px;align-items:center}.header-end{display:flex;align-items:center;justify-content:flex-end;gap:20px}.header-actions{display:flex;gap:4px}.icon-button{height:44px;width:44px;background:none;border:0;border-radius:4px;display:grid;place-content:center} +.offers{position:relative;font-size:16px}.offers summary{cursor:pointer;list-style:none;min-height:44px;display:flex;align-items:center;gap:9px}.offers summary::-webkit-details-marker{display:none}.chevron{flex:none}.offers[open] .chevron{transform:rotate(180deg)}.offers nav{position:absolute;top:51px;left:50%;transform:translateX(-50%);width:285px;padding:12px;background:var(--surface);border:1px solid var(--line);border-radius:8px;box-shadow:0 12px 30px #0001}.offers nav a{padding:10px 12px;display:block}.offers nav a:hover{background:var(--wash);text-decoration:none}.offers nav small{display:block;font-size:14px;color:var(--muted)}.menu-toggle{display:none;gap:5px}.menu-toggle span{width:24px;height:1.5px;background:currentColor}.mobile-navigation{display:none} +.eyebrow{font-size:16px;letter-spacing:0;margin-bottom:20px}.hero{text-align:center;padding:80px 0 38px}h1{font-size:clamp(44px,6vw,78px);letter-spacing:-.06em;line-height:1.03}.hero-description{max-width:680px;margin:26px auto 0;font-size:21px;line-height:1.5}.hero-actions{display:flex;justify-content:center;gap:28px;align-items:center;flex-wrap:wrap;margin-top:30px}.button{display:inline-flex;justify-content:center;gap:22px;align-items:center;border:1px solid var(--ink);padding:13px 22px;min-height:50px;color:var(--paper);background:var(--ink);border-radius:6px;font-size:16px}.button:hover{opacity:.85;text-decoration:none}.text-link{display:inline-flex;align-items:center;min-height:44px;font-size:16px;border-bottom:1px solid var(--line)}.release-note{font-size:16px;margin-top:18px}.download-count{display:inline-block;font-size:15px;line-height:1.6;color:var(--muted);padding:10px 0;margin-top:12px;min-height:44px}.compatibility{display:flex;align-items:center;justify-content:center;gap:26px;flex-wrap:wrap;margin-top:27px;font-size:16px;color:var(--muted)}.compatibility a{display:inline-flex;align-items:center;gap:10px;color:var(--ink)}.compatibility img{width:30px;height:30px;object-fit:contain} +.product-view{padding-top:20px;max-width:1280px}.preview-tabs{display:flex;justify-content:center;gap:32px;margin:20px 0 0}.preview-tabs button{border:0;background:none;font-size:16px;padding:10px 0;color:var(--muted);border-bottom:2px solid transparent;min-height:44px}.preview-tabs [aria-pressed=true]{color:var(--ink);border-color:var(--ink)}.app-preview{border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--surface);box-shadow:0 18px 60px #00000008}.app-preview img{width:100%;display:block;aspect-ratio:1.6;object-fit:contain}.app-preview figcaption{padding:15px 22px;font-size:15px;color:var(--muted);text-align:center;border-top:1px solid var(--line)} +.features{padding:110px 0 100px}.section-heading{max-width:700px;margin-bottom:56px}.section-heading>p:last-child{max-width:52ch;margin-top:23px}.feature-row{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);gap:64px;align-items:center;margin-top:80px}.feature-row.reverse{grid-template-columns:minmax(0,1fr) minmax(0,1.4fr)}.feature-row.reverse .feature-media{order:2}.feature-media{border:1px solid var(--line);border-radius:8px;background:var(--surface);overflow:hidden}.feature-media img{display:block;width:100%;aspect-ratio:1.6;object-fit:contain}.feature-row p{margin-top:22px;font-size:18px;max-width:39ch} +.agents-band{padding:80px 0;background:var(--wash)}.agents-inner{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);gap:80px;align-items:center}.agents-inner p:not(.eyebrow){margin:24px 0 13px;max-width:47ch}.agent-path{text-align:center}.agent-path>div:first-child{display:flex;justify-content:center;gap:28px}.agent-client{display:flex;flex-direction:column;align-items:center;gap:12px;font-size:17px}.agent-client img{width:48px;height:48px;object-fit:contain}.path-line{font-size:27px;color:var(--muted);display:block;padding:22px}.path-canvas{border:1px solid var(--line);padding:28px;max-width:300px;margin:auto;background:var(--paper);border-radius:8px;font-size:21px}.path-canvas>.product-mark{display:block;margin:0 auto 14px}.path-canvas small{display:block;color:var(--muted);font-size:15px;margin-top:7px} +.files-and-story{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:90px;padding:100px 0;border-bottom:1px solid var(--line)}.files-and-story h2{font-size:36px}.files-and-story article>p:not(.eyebrow){margin-top:24px;font-size:18px}.origin .text-link{margin-top:22px}.origin p a{text-decoration:underline} +.downloads{padding:100px 0;text-align:center}.download-intro{margin:24px auto 0;max-width:52ch}.download-primary{margin-top:30px}.download-meta{font-size:15px;margin-top:13px}.platform-links{display:flex;justify-content:center;align-items:center;flex-wrap:wrap;gap:14px 32px;margin:26px auto 0;max-width:700px}.platform-links a{display:inline-flex;align-items:center;gap:8px;min-height:44px;font-size:16px;text-decoration:underline;text-underline-offset:5px}.platform-links a span{font-size:14px;color:var(--muted);text-decoration:none}.download-notes{max-width:720px;margin:26px auto 0}.download-notes p{font-size:15px;line-height:1.65}.download-links{display:flex;justify-content:center;flex-wrap:wrap;gap:8px 26px;margin-top:18px;font-size:15px}.download-links a{display:inline-flex;align-items:center;min-height:44px;text-decoration:underline} +footer{border-top:1px solid var(--line);padding-block:48px 32px}.footer-top{display:grid;grid-template-columns:1fr auto;gap:40px;align-items:start}.footer-product p{font-size:15px;line-height:1.6;margin-top:15px}.family-links{display:flex;flex-wrap:wrap;gap:16px 28px;align-items:center;padding-top:1px}.family-links a{display:inline-flex;align-items:center;gap:9px;min-height:44px;font-size:15px;white-space:nowrap}.family-links a>span:last-child{color:var(--muted)}.arc-mark{display:inline-grid;place-content:center;width:29px;height:29px;border-radius:3px;background:#4265a0}.bread-mark{display:inline-block;width:34px;height:25px;background:currentColor;-webkit-mask:url('/assets/onlinesourdough.svg') center/contain no-repeat;mask:url('/assets/onlinesourdough.svg') center/contain no-repeat}.owner-mark{width:29px;height:29px;object-fit:cover;border-radius:3px} +.footer-bottom{display:flex;align-items:center;justify-content:space-between;gap:24px;margin-top:40px;padding-top:24px;border-top:1px solid var(--line);font-size:15px}.footer-bottom nav{display:flex;flex-wrap:wrap;gap:10px 25px}.footer-bottom nav a{display:inline-flex;align-items:center;min-height:44px}.footer-credit{max-width:87ch;font-size:14px;line-height:1.65;margin-top:24px}.footer-credit a{text-decoration:underline}.skip-link{position:fixed;top:-100px;left:20px;padding:12px;background:var(--paper);z-index:100}.skip-link:focus{top:10px} +@media(max-width:1100px){.header-links a:first-child{display:none}.header-links{gap:20px}.feature-row{gap:38px}.agents-inner,.files-and-story{gap:50px}.family-links{gap:16px 20px}.footer-top{gap:28px}} +@media(max-width:800px){ + :root{scroll-padding-top:84px}.header-inner,.page-width{width:calc(100% - 40px)}.header-inner{display:flex;min-height:70px;gap:8px}.wordmark{font-size:20px;gap:9px}.wordmark>.product-mark{width:29px;height:29px}.header-links,.offers{display:none}.header-end{margin-left:auto;gap:0}.menu-toggle{display:grid} + .mobile-navigation[open]{display:flex;flex-direction:column;position:fixed;inset:0;width:100%;max-width:none;height:100dvh;max-height:100dvh;margin:0;padding:max(14px,env(safe-area-inset-top)) 24px max(24px,env(safe-area-inset-bottom));border:0;background:var(--paper);color:var(--ink);overflow-y:auto;overscroll-behavior:contain}.mobile-navigation::backdrop{background:var(--paper)}.mobile-top{display:flex;align-items:center;justify-content:space-between;min-height:52px;margin-bottom:28px;flex:none}.mobile-top .icon-button{font-size:32px}.mobile-navigation nav{border-top:1px solid var(--line);padding:22px 0}.mobile-navigation nav a{display:block;padding:11px 0;font-size:24px;line-height:1.4}.mobile-navigation nav small{display:block;font-size:15px;color:var(--muted);margin-top:3px}.mobile-navigation .eyebrow{margin-bottom:8px}.mobile-newsletter{font-size:17px;margin-top:8px;min-height:44px} + .hero{padding:54px 0 28px}h1{font-size:clamp(43px,8vw,62px)}.hero-description{font-size:19px;margin-top:24px}.hero-actions{gap:22px;margin-top:28px}.compatibility{gap:16px 24px;font-size:15px}.compatibility>span{flex-basis:100%}.compatibility img{width:28px;height:28px}.preview-tabs{gap:24px;justify-content:center;overflow-x:auto;white-space:nowrap;padding-bottom:5px;scrollbar-width:thin}.preview-tabs button{font-size:15px;flex:none}.app-preview{border-radius:8px}.app-preview figcaption{font-size:14px;padding:14px} + .features{padding:75px 0 70px}.section-heading{margin-bottom:35px}.feature-row,.feature-row.reverse{grid-template-columns:1fr;gap:27px;margin-top:54px}.feature-row.reverse .feature-media{order:0}.feature-row h3{font-size:32px}.feature-row p{max-width:52ch;margin-top:17px}.agents-band{padding:60px 0}.agents-inner{grid-template-columns:1fr;gap:38px}.agent-path{padding-top:10px}.path-canvas{max-width:340px}.files-and-story{grid-template-columns:1fr;gap:58px;padding:75px 0}.files-and-story h2{font-size:34px}.downloads{padding:75px 0}.download-links{gap:8px 20px}.footer-top{grid-template-columns:1fr;gap:30px}.family-links{gap:12px 26px}.footer-bottom{margin-top:28px;flex-wrap:wrap;gap:12px 24px}footer{padding-block:36px 28px} +} +@media(max-width:560px){.family-links{display:grid;grid-template-columns:1fr;gap:10px}.footer-bottom{align-items:flex-start;flex-direction:column;gap:16px}.footer-bottom nav{gap:8px 22px}.platform-links{gap:8px 24px}.platform-links a{flex-wrap:wrap;justify-content:center}} +@media(max-width:370px){.wordmark{font-size:18px}.header-inner{width:calc(100% - 28px)}.hero-actions{align-items:stretch;flex-direction:column;gap:12px}.hero-actions .text-link{align-self:center}h1{font-size:41px}} +@media(prefers-reduced-motion:no-preference){a,button{transition:background-color .15s,color .15s,opacity .15s}.chevron{transition:transform .15s}} + +@media(hover:hover){.icon-button:hover{background:var(--wash)}} +@media(max-width:560px){.preview-tabs{flex-wrap:wrap;justify-content:center;overflow:visible;gap:0 22px}} + +/* Existing family moon/sun, reused from Arc’IT. */ +.theme-toggle { + width: 44px; + height: 44px; + display: grid; + place-items: center; +} +.theme-toggle-icon { + display: block; + width: 17px; + height: 17px; + border-radius: 999px; + box-shadow: inset -6px -4px 0 0 currentColor; +} +:root[data-theme="dark"] .theme-toggle-icon { + background: currentColor; + box-shadow: + 0 -8px 0 -6px currentColor, + 0 8px 0 -6px currentColor, + 8px 0 0 -6px currentColor, + -8px 0 0 -6px currentColor, + 6px 6px 0 -6px currentColor, + -6px -6px 0 -6px currentColor, + 6px -6px 0 -6px currentColor, + -6px 6px 0 -6px currentColor; +} +.theme-toggle{color:var(--muted)}