diff --git a/apps/pdftools/README.md b/apps/pdftools/README.md index 6d951b05..d90f47fc 100644 --- a/apps/pdftools/README.md +++ b/apps/pdftools/README.md @@ -2,32 +2,69 @@ English | [简体中文](README.zh-CN.md) -PDF Tools (`os.pdftools`) is OctoSense's PDF app: the PDFs in its own -storage, a viewer with page thumbnails, merge, split and text extraction, -and PDFs opened from the device. The shell's `pdf` engine service (pdfcraft, -[ADR 0013](../../docs/adr/0013-craft-engines-as-pinned-services.md)) does -the work, in the app's own storage; the app is a contained Splash program in -`bundle/` and never reads a PDF itself. It ships in the desktop shell only, -because the ten craft engines are desktop only (`craft-engines`). +PDF Tools (`os.pdftools`) is OctoSense's PDF app for the desktop: read, +search, review, fill and sign, organize pages, combine and edit the PDFs in +its own storage, and open PDFs from the device. The shell's `pdf` engine +service (pdfcraft, [ADR 0013](../../docs/adr/0013-craft-engines-as-pinned-services.md)) +does the work in the app's own storage, through the methods in +[design/SERVICE.md](design/SERVICE.md); the app is a contained Splash program +in `bundle/` and never reads a PDF itself. It ships in the desktop shell +only, because the craft engines are desktop only (`craft-engines`). It declares `storage`, `files` and `pdf`, and no `storage.max_bytes`, so its storage is App Hub's system ceiling, 64 MiB. -## Screens +Version 2 was rebuilt through OctoSense App Flow from nine approved generated +designs: the product requirements in [design/BRIEF.md](design/BRIEF.md), the +untouched images and their measurements in `design/source/`, and one pixel +map per screen in `design/map/` that ties every measured region to the +widget that rebuilds it, its geometry, colours, type size and the review +decision that applies. -| Screen | What it shows | Its one action | +## The window + +| Region | What it holds | +| --- | --- | +| Document tabs | Home while it shows, one tab per open PDF (its name, a dot while it has unsaved changes, a close cross), and Open, which goes to Home | +| Mode bar | Read · Comment · Fill & Sign · Pages · Combine · Edit, the current one underlined; Search in document; Undo, Redo; Save | +| Rail and left panel | Pages (thumbnails), Outline (the PDF's bookmarks), Comments, Search (the results); the panel folds away | +| Canvas | The pages, white on the desk: a run of up to eight pages from the page you went to, each the engine's render at screen resolution | +| Pill | Page "3 / 24", previous and next, zoom out, zoom, zoom in, fit width, fit page | +| Right panel | Comment: the threads. Fill & Sign: the form's fields and Add text, Add date, Add initials. Edit: the paragraph being edited | +| Status line | Saved or Edited, and the storage the PDFs use ("14.6 of 64 MB used", "37 KB of 64 MB used") | + +The designs are drawn at 1536 x 1024, but the desktop opens every app's +window at 72% of the desk's width and 76% of its height, at most 1000 x 720 +points (`crates/shell/src/desktop_layout.rs`); in the runs here PDF Tools got +990 x 603 points. An app has no way to ask for a larger window; the person +can resize or maximize it. Below 1180 points wide the mode tabs and Save trim +their padding, both right panels take 320 points, a right panel takes the +left panel's place, and until the person zooms, the page fits the canvas, up +to 100%. + +## Modes + +| Mode | What it does | Engine | | --- | --- | --- | -| Library | The PDFs in `accounts/device/library/`: first page, name, pages and size; a damaged file says it can't be opened; "Open a PDF from this device" with the largest file it takes; a card when a file couldn't be added, or when storage is full | **Merge PDFs** (an empty library: **Open a PDF from this device**) | -| Document › Pages | Page thumbnails, 24 at a time; tap one for the page view | **Split into files** | -| Document › Text | The text of each page in reading order, with Find in the text | – | -| Document › Info | File, pages, page size, file size, title, author, subject, keywords, creator, producer, PDF version, security, fonts, bookmarks | – | -| Document › Remove | Asks once more: the PDF is deleted from the app's storage; PDFs made from it stay | **Remove** / Keep it | -| Page view | One page, large; Previous and Next, or swipe | – | -| Merge | Three steps: Choose (numbered picks, 2 to 16 files), Order (move up or down, remove, name), Done (the result, Open) | Next / **Merge** / Open | -| Split | Every few pages (a stepper) or Where I choose (tap the pages that start a new file); a live list of the files it makes, and which existing files it replaces; then Done | **Split into N files** | - -Merged and split PDFs stay in the app's storage: exporting them is not part -of this version. +| Home | Recent PDFs as cards with their first page, pages, size and when they were opened; Open a PDF from this device (up to 64 MB); remove a PDF (asks once more); an empty state on a first run | `pdf.open`, `pdf.info`, `pdf.page`, `pdf.close` | +| Read | Pages render one at a time at 72 dpi x zoom x 2 and are kept; thumbnails fill in one by one; the outline jumps to a section | `pdf.open`, `pdf.page` | +| Find | Search in document, then Return: "12 matches on 7 pages", the results grouped by page with the match in bold, every match highlighted on its page, the current one stronger, previous and next | `pdf.find` | +| Comment | Highlight, Underline or Strike out the line you tap (tap twice for its paragraph), Note and Text box where you tap, in one of four colours; threads with replies, a status, delete. Comments carry the name kept under "Comment as" | `pdf.lines`, `pdf.comments`, `pdf.comment` | +| Fill & Sign | The form's fields outlined (required empty ones in red) and listed with their values; choose one to type its value; Add text, Add date and Add initials place a mark where you tap; initials are kept | `pdf.fields`, `pdf.fill`, `pdf.fill_sign` | +| Pages | Every page as a large thumbnail: choose pages, Rotate left or right, Delete, Extract (a new PDF in the library), Insert from file (another PDF here), drag a page to move it | `pdf.pages` | +| Combine | This PDF and others in order (drag the grip to reorder), each with its pages ("1-4, 9" or All), "One PDF of 31 pages", a name, Combine; the new PDF opens | `pdf.merge` | +| Edit | Click a paragraph: its text becomes editable in place, the right panel shows its font and size; Apply or Cancel | `pdf.lines`, `pdf.edit_text` | + +Undo and Redo use the engine's history; Save writes the PDF back +(`pdf.undo`, `pdf.redo`, `pdf.save`, `pdf.state`). Closing a tab with unsaved +changes asks: Save, Don't save, or Cancel. + +States every screen handles: an empty library, a page still rendering, a +damaged PDF ("Couldn't open this PDF" and the engine's reason), a protected +PDF ("This PDF is protected with a password": OctoSense keeps passwords on +host sheets and there is none for PDFs yet, so it is not opened), storage +full ("Remove a PDF to make room"), eight PDFs already open, unsaved changes +when closing, and no engine on the device. ## Opening a PDF from this device @@ -35,149 +72,138 @@ of this version. file the person chooses in the host's own dialog into the library. The app names the new file first, because `files.import` takes its destination before the dialog opens: `Imported PDF.pdf`, then `Imported PDF 2.pdf` and so -on (the import refuses an existing destination). The copy then opens like -any other PDF here: the engine's `info`, `render` and `text` on the same -relative path. - -The host also reports the chosen file's display name (`name`, cleaned of -folders, control characters and direction marks), and that becomes the -PDF's title, without `.pdf`: "Lease 2026", or "Lease 2026 (2)" when another -PDF here already has that title. The file keeps the name the app gave it -(app storage has no rename), and the title lives in `library.json`. Without -`name` (on Android, or when nothing was left of it after cleaning) the title -is the file name, "Imported PDF 2". Parts split from a titled PDF are titled -after it ("Lease 2026-part2"). The library lists files in their storage -order. - -`files.status` (asked once, without opening anything) gives the largest file -an import takes, 64 MiB on the desktop, which the library shows; free storage -bounds it too. A refusal shows as a card at the top of the library, in plain -words with the host's own message below: the app is not in the foreground, -storage is full, too many files, the file is too large, another transfer is -running, or no files service. +on. The host reports the chosen file's display name (`name`), which becomes +the PDF's title without `.pdf` ("Lease 2026", or "Lease 2026 (2)" when that +title is taken); the file keeps the name the app gave it and the title lives +in `library.json`. Then it opens in a tab. A refusal shows on Home in plain +words with the host's own message below it. ## How it works -Every path below is relative to the app's storage, the folder `fs.*` sees. -The engine works in that same folder (ADR 0013, "Engines work in the -caller's own folder"): a call made while the app is open may replace a file, -and what the engine writes counts against the app's storage. +Every path is relative to the app's storage, the folder `fs.*` sees; the +engine works in the same folder. | Path | Holds | | --- | --- | -| `accounts/device/library/*.pdf` | The PDFs (ADR 0004 §11: a system app's data is its `device` account folder), including imported ones and what merge and split write | -| `accounts/device/library.json` | What the engine said about each file (pages and size), and its title when that is not its file name | -| `cache/covers/.png` | Each file's first page (evictable) | -| `cache/pages//p.png` | The open document's thumbnails; other documents' are removed when one opens, since app storage holds at most 256 entries | -| `cache/view.png` | The page view's image | - -| Call | When | Arguments | -| --- | --- | --- | -| `pdf.info` | a file is listed for the first time, a document opens | `{path}` | -| `pdf.render` | covers, thumbnails (`max_side` 360) and the page view (`max_side` 1400) | `{path, page, out, max_side}` | -| `pdf.text` | the Text tab | `{path}`, or the first 512 `pages` | -| `pdf.merge` | Merge | `{paths, out}` | -| `pdf.split` | Split | `{path, out_dir, every}` or `{path, out_dir, before}` | -| `files.status` | the app starts | `{}` | -| `files.import` | Open a PDF from this device | `{path: "accounts/device/library/Imported PDF.pdf"}`; the result's `name`, when present, is the title | - -The app never reads `pdf.info`'s `document.path`: until #434 it is the -engine's absolute host path, and every path the app uses is its own relative -one. - -When the engine has no room to write (`… more than the N bytes left in this -storage`), the library, the pages and the merge and split screens say that -PDF Tools' storage is full, and Merge or Split says why it failed. Removing a -PDF frees its room and lets the app try its page pictures again. +| `accounts/device/library/*.pdf` | The PDFs, including imported, extracted and combined ones | +| `accounts/device/library.json` | Per file: title (when not its file name), pages, size, when last opened, the page and zoom it was left at, its cover's render; and `who`: the name comments carry and the initials Fill & Sign places | +| `.cache/pages//@.png` | The engine's page renders: its cache, at most 16 MiB and 64 files | Notes for whoever changes the script: -- The calls go one at a time: the service runs on the UI thread (#399). -- A refusal answers inside `host.request` itself. Before the runtime overlay - that came with #413, a UI call made in such a callback lost the rest of the - callback; `engine()` and the files calls hand every answer to a later turn - (`start_timeout`). -- Page images are read with `fs.read_bytes` and drawn with - `binary_resource` (runtime `platform/src/script/res.rs`, reachable through - the widgets prelude), one stored handle per image, so a redraw reuses the - decoded texture. Each screen has its own scroller, declared in the page - and filled by an explicit `render()`. Two things observed in `card-host` - shaped this: a scroller made inside another view's `on_render` exists only - after the next draw, and its automatic first render came out empty when it - held stored images; and a view with `on_render` that starts with - `visible: false` stayed hidden after `set_visible(true)`. So the scrollers - sit in plain `View`s, which are shown and hidden instead. -- One handler, timer or callback has a 64 ms budget; the steps after a merge - or split (forgetting stale caches, rescanning, the next screen) run on - separate turns. +- **Engine work runs on the shell's UI thread (#399).** The app keeps one + background render out at a time (`pump()`): the current page and the two + after it, then the visible thumbnails, then the rest of the run, then the + other thumbnails, then Home's covers. A change drops only the renders of + the pages it touched; Undo and Redo, which don't say what changed, redraw + the run on show. +- **No scroll position for script apps.** The runtime gives a script app no + way to read or set where a scroll view is. So the canvas is a static + `ScrollXYView` that keeps its place while renders land, and going to a page + (a thumbnail, the outline, a find result, the pill) draws the canvas empty + for one frame, which clamps its scroll to the top, then fills it with a run + of pages that starts at that page. Pages before it come back with the + pill's previous. +- **No keyboard events for script apps.** Every shortcut in the brief (⌘F, + ⌘+ and ⌘−, ⌘0, the arrows, ⌘Z, ⇧⌘Z, ⌘S) is a button instead; Search in + document runs on Return, which a text field does report. +- **State lives on `mod.pdftools`.** A light/dark switch runs the script + again (#440); every `let` starts over, `mod` survives, and `resume()` draws + every region with the new look. +- **Labels draw their own margin and padding twice** on this runtime + (`widgets/src/label.rs` passes the padded walk to both its turtle and its + text): give a Label no margin or padding and put a View around it instead. +- **A view whose `on_render` starts hidden stays hidden**: regions that hide + are wrapped in plain Views that are shown and hidden instead. +- One handler, timer or callback has a 64 ms budget of wall-clock time; page + images are read with `fs.read_bytes` and shown with `binary_resource`, one + stored handle per image. `library.json` is written by the heartbeat (at most + every 2 s) when a page turn or an open changed it, so no tap waits on the + disk; a new title, a removal or the "Comment as" name is written at once. ## Testing -The pdf service's `pdftools_fixture` example writes four sample PDFs and a -damaged file into PDF Tools' storage under an apps root (an OctoSense home's -`apps/`, or a `card-host --app-data` folder), before the app starts. With -`--import-sample ` it writes one more PDF, a three-page garden plan. -It never calls the engine; it refuses storage that already holds data. +The library's rules are pure functions in `main.splash`, tested from Rust by +evaluating the bundle's functions in a script VM +(`crates/shell/src/pdftools_model_tests.rs`): an imported PDF's title, a split +part's title, what the library index keeps across a restart, the pages a +range names, a find snippet's match in bold, an avatar's initials and a +comment's date. ```sh -cargo run --locked -p octosense-pdf-service --example pdftools_fixture -- -cargo run --locked -p octosense-pdf-service --example pdftools_fixture -- --import-sample +cargo test --locked -p octosense-shell --lib pdftools_model ``` -`tests/ui.py` runs the app end to end in a hidden desktop shell built from -this checkout, with the real engine: it makes a fresh `OCTOSENSE_HOME` for -each run, writes the samples into `apps/os.pdftools/`, starts -`target/debug/octosense` from a Terminal.app tab (`MAKEPAD_HIDE_WINDOWS=1`, -`MAKEPAD_REMOTE`, `MAKEPAD_WM_TEST_APP=pdftools`), drives it over the remote -bridge, saves every grab and ends with `/quit`. A lock folder keeps it to one -hidden shell on the machine, and it refuses to start while another OctoSense -runs. +`tests/ui.py` runs the app end to end. With `--shell` it uses a hidden +desktop shell built from this checkout, with the real engine, on the sample +PDFs the pdf service's `pdftools_fixture` example writes (it makes a fresh +`OCTOSENSE_HOME` for each run, starts `target/debug/octosense` from a +Terminal.app tab with `MAKEPAD_HIDE_WINDOWS=1`, `MAKEPAD_REMOTE` and +`MAKEPAD_WM_TEST_APP=pdftools`, drives it over the remote bridge, saves every +grab and ends with `/quit`; a lock folder keeps it to one hidden shell on the +machine, and it waits while another OctoSense runs). With `--card-host` it +uses App Hub's `card-host`, which serves no host services. ```sh -cargo build --locked -p octosense +cargo build --locked -p octosense --no-default-features --features app-hub,craft-engines python3 apps/pdftools/tests/ui.py --shell target/debug/octosense --lock --output target/pdftools-ui -python3 apps/pdftools/tests/ui.py --card-host /target/release/card-host --only missing --output target/pdftools-ui +python3 apps/pdftools/tests/ui.py --card-host /target/release/card-host --output target/pdftools-ui ``` +The runtime gives one handler 64 ms of wall-clock time, so on a busy machine +a run can fail on a handler that waited for the CPU (`script time budget +exceeded` in its log); `--budget-ms 1000` raises the hosts' budget +(`MAKEPAD_SPLASH_BUDGET_MS`) for such a machine. + | Run | What it covers | | --- | --- | -| `shell` | Every screen on the samples in light, then dark through the shell's own style menu: covers, pages, the page view, text and find, info, both splits (one replaces a file), merge, open, remove, the damaged file, and "Open a PDF from this device" | -| `restart` | The same home again: what merge and split wrote is still there, and `Imported PDF.pdf`, placed in the library between the runs as an import leaves one, is read, drawn and opened | -| `full` | Storage filled to 8 KB short of its 64 MiB: the engine's refusal on the library and on a merge, then a removal | -| `empty` | No PDFs: the empty library and its Open button, in light and dark | -| `missing` | App Hub's `card-host`, which serves no host services: no engine and no files service | -The library's rules are pure functions in `main.splash`, tested from Rust by -evaluating the bundle's functions in a script VM -(`crates/shell/src/pdftools_model_tests.rs`, as Maps' and Photos' are): an -imported PDF's title with a name, without one, for a name another PDF -already shows, and when nothing is left of the name; a split part's title; -and what the library index keeps across a restart. +| `shell` | Every mode on the samples in the shell's own window, light, then dark through the shell's own style menu (the dark pass works on what the light pass left): Home and its covers, "Open a PDF from this device" refused, reading and thumbnails, find ("9 matches on 4 pages"), a highlight and its reply, a rotation and its undo, combine with a page range ("One PDF of 5 pages"), initials placed by Fill & Sign, a text edit, save, and the damaged file | +| `restart` | The same home again: the library, its titles, the page a PDF was left at, and `Imported PDF.pdf` placed as an import leaves one | +| `full` | Storage filled to 8 KB short of its 64 MiB: the refusal on Home, then a removal, which asks once more | +| `empty` | No PDFs: the empty library and its Open button, light and dark | +| `missing` | `card-host` with the shipped bundle: no engine and no files service | +| `fixture` | `card-host` with the dev fixture: every designed screen at the designs' 1536 x 1024, light and dark, each grab beside its design in `compare/`, and the damaged, protected and unsaved states | + +The dev fixture never ships. `dev-fixture/engine.splash` is a stand-in for +the pdf engine that answers SERVICE.md's methods; `tests/ui.py` puts it in +place of `engine()` in a scratch copy of the bundle. `dev-fixture/make_fixture.py` +fills the app's storage with sample documents drawn from the approved +designs' own sample text, their pages as pictures, and what the stand-in +answers (outlines, text lines and paragraphs with their boxes, form fields, +comments). ```sh -cargo test --locked -p octosense-shell --lib pdftools_model +python3 apps/pdftools/dev-fixture/make_fixture.py /os.pdftools ``` The host's dialog cannot be driven in a hidden shell, and a hidden window never has focus, so the files service refuses `files.import` there before the dialog opens (`foreground_required: …`); the runs check that the app shows -that refusal. What follows an import: the title rules above, and the `restart` -run, which takes a library file the index has never seen, read by the real -engine and opened by the app. +that refusal. ## Status -- **Verified** in a hidden desktop shell on macOS (Apple silicon), with the - real engine: every screen above in light and dark, a restart on the same - storage with a PDF placed as an import leaves one, full storage, the empty - library, and `card-host` without host services. The grabs, digests and - checks are in [tests/evidence/shell-20261009](tests/evidence/shell-20261009/README.md). - The titles of imported PDFs are verified by the Rust tests of the library's - rules above. -- **Not verified**: the host's file dialog and a real import through it - (`imported()` in the script, which applies the tested title rule), the - import's other refusals, window sizes other than the shell's default, - Linux, Windows, phones. -- **Known**: a light/dark switch runs the script again (the runtime's style - reapply), so the app returns to its library. The shell evidence's dark - frames keep two columns of pages: they were made before the shell sent - hosted apps their size again after a restyle (9d7a386e). +- **Verified on 10 Oct 2026** (macOS, Apple silicon; the frames, side-by-sides + and receipt are in [tests/evidence/v2-20261010](tests/evidence/v2-20261010/README.md)): + the model tests; the `shell`, `restart`, `full` and `empty` runs in a hidden + desktop shell with the real engine, at the runtime's 64 ms script budget + (and again with `--budget-ms 1000` while other builds loaded the machine); + the `missing` and `fixture` runs in card-host, light and dark. `fixture` + ran with `--budget-ms 1000`: its stand-in engine answers in script, and at + 64 ms one of its answers overran in the dark pass. +- **Not verified**: the host's file dialog and a real import (a hidden window + cannot open it); with the real engine, form fields, outlines, Redo, the + other comment and page operations, password-protected PDFs, the eight-PDF + cap and the unsaved-changes question (the dev fixture covers their screens); + window sizes other than the shell's default and 1536 x 1024; Linux, + Windows, phones. +- **Known**: a comment's date is the engine's UTC time read as local time, so + a comment made in the evening west of UTC shows the next day's date, until + the pdf service converts it. The status line counts only the PDFs in the + library: the files service does not report the storage's use. On a heavily + loaded machine a tap can overrun the 64 ms budget and be lost (seen at load + averages of 60 to 150 before `library.json` moved off the tap path). +- **Not part of this version**: export (no approved design), opening + password-protected PDFs, keyboard shortcuts (the runtime gives script apps no + key events), the Edit panel's Alignment and Colour (`pdf.edit_text` takes + only text). diff --git a/apps/pdftools/README.zh-CN.md b/apps/pdftools/README.zh-CN.md index 9588cba3..d56f6eac 100644 --- a/apps/pdftools/README.zh-CN.md +++ b/apps/pdftools/README.zh-CN.md @@ -2,155 +2,169 @@ [English](README.md) | 简体中文 -PDF Tools(`os.pdftools`)是 OctoSense 的 PDF 应用:管理它自己存储中的 PDF, -提供带页面缩略图的查看器、合并、拆分和文字提取,并可从设备打开 PDF。实际工作由 -Shell 的 `pdf` 引擎服务(pdfcraft,[ADR 0013](../../docs/adr/0013-craft-engines-as-pinned-services.zh-CN.md)) -在应用自己的存储中完成;应用本身是 `bundle/` 中一个隔离运行的 Splash 程序,从不 -自己读取 PDF。它只随桌面 Shell 发布,因为十个 craft 引擎只在桌面版中提供 -(`craft-engines`)。 +PDF Tools(`os.pdftools`)是 OctoSense 桌面版的 PDF 应用:阅读、搜索、审阅、填写与签名、 +整理页面、合并和编辑它自己存储中的 PDF,并可从设备打开 PDF。实际工作由 Shell 的 `pdf` +引擎服务(pdfcraft,[ADR 0013](../../docs/adr/0013-craft-engines-as-pinned-services.zh-CN.md)) +在应用自己的存储中完成,所用方法见 [design/SERVICE.md](design/SERVICE.md);应用本身是 +`bundle/` 中一个隔离运行的 Splash 程序,从不自己读取 PDF。它只随桌面 Shell 发布,因为 +craft 引擎只在桌面版中提供(`craft-engines`)。 -它申请 `storage`、`files` 和 `pdf`,不声明 `storage.max_bytes`,因此其存储上限是 -App Hub 的系统上限 64 MiB。 +它申请 `storage`、`files` 和 `pdf`,不声明 `storage.max_bytes`,因此其存储上限是 App Hub +的系统上限 64 MiB。 -## 界面 +第 2 版通过 OctoSense App Flow,依据九张已获批准的生成设计图重建:产品需求见 +[design/BRIEF.md](design/BRIEF.md),未经修改的图像及其测量数据在 `design/source/`, +`design/map/` 中每个界面一份像素映射,把每个测得的区域与重建它的组件、它的几何尺寸、 +颜色、字号以及适用的评审决定一一对应。 -| 界面 | 显示内容 | 唯一的主要操作 | -| --- | --- | --- | -| 资料库 | `accounts/device/library/` 中的 PDF:首页、名称、页数和大小;损坏的文件会说明无法打开;“Open a PDF from this device”及可添加的最大文件;文件无法添加或存储已满时显示一张卡片 | **Merge PDFs**(资料库为空时:**Open a PDF from this device**) | -| 文档 › Pages | 页面缩略图,每次 24 页;点按一页进入单页视图 | **Split into files** | -| 文档 › Text | 按阅读顺序显示每页文字,可用 Find in the text 查找 | – | -| 文档 › Info | 文件、页数、页面尺寸、文件大小、标题、作者、主题、关键词、创建程序、生成程序、PDF 版本、安全性、字体、书签 | – | -| 文档 › Remove | 再确认一次:PDF 会从应用的存储中删除;由它生成的 PDF 保留 | **Remove** / Keep it | -| 单页视图 | 放大显示一页;Previous、Next 或滑动翻页 | – | -| 合并 | 三步:Choose(带序号的选择,2 到 16 个文件)、Order(上移或下移、移除、命名)、Done(结果,可 Open) | Next / **Merge** / Open | -| 拆分 | Every few pages(步进器)或 Where I choose(点按每个新文件起始的页面);实时列出将生成的文件,以及会替换哪些已有文件;然后 Done | **Split into N files** | +## 窗口 -合并和拆分得到的 PDF 保留在应用的存储中:这个版本不提供导出。 +| 区域 | 内容 | +| --- | --- | +| 文档标签 | 显示主页时有 Home 标签,每个打开的 PDF 一个标签(名称、有未保存更改时的圆点、关闭叉号),以及前往主页的 Open | +| 模式栏 | Read · Comment · Fill & Sign · Pages · Combine · Edit,当前模式带下划线;Search in document;Undo、Redo;Save | +| 侧栏与左侧面板 | Pages(缩略图)、Outline(PDF 的书签)、Comments、Search(结果);面板可以收起 | +| 画布 | 页面以白色显示在桌面背景上:从你前往的那一页开始的至多八页,每页都是引擎按屏幕分辨率渲染的结果 | +| 浮动控制条 | 页码“3 / 24”、上一页和下一页、缩小、缩放比例、放大、适合宽度、适合页面 | +| 右侧面板 | Comment:评论线程。Fill & Sign:表单字段以及 Add text、Add date、Add initials。Edit:正在编辑的段落 | +| 状态栏 | Saved 或 Edited,以及 PDF 占用的存储(“14.6 of 64 MB used”、“37 KB of 64 MB used”) | + +设计图按 1536 x 1024 绘制,但桌面版打开每个应用的窗口时,宽度取桌面的 72%、高度取 76%, +最大 1000 x 720 点(`crates/shell/src/desktop_layout.rs`);在这里的运行中 PDF Tools 得到 +990 x 603 点。应用没有办法请求更大的窗口;用户可以调整窗口大小或将其最大化。窗口宽度小于 +1180 点时,模式标签和 Save 会收紧内边距,两种右侧面板都取 320 点宽,右侧面板会取代左侧面板 +的位置;在用户自己缩放之前,页面会适合画布宽度,最大 100%。 + +## 模式 + +| 模式 | 作用 | 引擎 | +| --- | --- | --- | +| 主页 | 最近的 PDF 以卡片显示首页、页数、大小和打开时间;Open a PDF from this device(最大 64 MB);移除 PDF(会再确认一次);首次运行时的空状态 | `pdf.open`、`pdf.info`、`pdf.page`、`pdf.close` | +| Read | 页面按 72 dpi × 缩放 × 2 逐页渲染并保留;缩略图逐个填入;大纲可跳转到章节 | `pdf.open`、`pdf.page` | +| 查找 | 在 Search in document 中输入后按 Return:“12 matches on 7 pages”,结果按页分组、匹配处加粗,每处匹配在页面上高亮,当前匹配更醒目,可前后切换 | `pdf.find` | +| Comment | 对点按的那一行做高亮、下划线或删除线(点按两次作用于整段),在点按处添加 Note 和 Text box,四种颜色可选;带回复、状态和删除的评论线程。评论使用在“Comment as”中保存的名字 | `pdf.lines`、`pdf.comments`、`pdf.comment` | +| Fill & Sign | 用框线标出表单字段(未填的必填字段为红色)并列出其值;选中一个字段即可输入值;Add text、Add date 和 Add initials 会在点按处放置标记;姓名缩写会被保存 | `pdf.fields`、`pdf.fill`、`pdf.fill_sign` | +| Pages | 每页一张大缩略图:选择页面,向左或向右旋转、删除、提取(在资料库中生成新 PDF)、从文件插入(这里的另一个 PDF),拖动页面以移动 | `pdf.pages` | +| Combine | 当前 PDF 与其他 PDF 按顺序排列(拖动手柄调整顺序),每个都可指定页面(“1-4, 9”或 All),“One PDF of 31 pages”,名称,Combine;新 PDF 随即打开 | `pdf.merge` | +| Edit | 点击一个段落:它的文字可以就地编辑,右侧面板显示字体和字号;Apply 或 Cancel | `pdf.lines`、`pdf.edit_text` | + +Undo 和 Redo 使用引擎的历史记录;Save 把 PDF 写回(`pdf.undo`、`pdf.redo`、`pdf.save`、 +`pdf.state`)。关闭有未保存更改的标签时会询问:Save、Don't save 或 Cancel。 + +每个界面都处理的状态:资料库为空、页面仍在渲染、PDF 损坏(“Couldn't open this PDF”以及 +引擎给出的原因)、PDF 受密码保护(“This PDF is protected with a password”:OctoSense 只在 +宿主自己的面板上处理密码,目前还没有 PDF 的密码面板,因此不打开它)、存储已满(“Remove a +PDF to make room”)、已打开八个 PDF、关闭时有未保存的更改,以及设备上没有引擎。 ## 从设备打开 PDF -“Open a PDF from this device”请 Shell 的文件服务把用户在宿主自己的对话框中选择的 -一个文件复制到资料库中。应用要先为新文件命名,因为 `files.import` 在对话框打开前 -就接收目标路径:依次为 `Imported PDF.pdf`、`Imported PDF 2.pdf` 等(目标已存在时 -导入会被拒绝)。复制进来的文件随后像这里的其他 PDF 一样打开:引擎在同一个相对路径上 -执行 `info`、`render` 和 `text`。 - -宿主还会报告所选文件的显示名称(`name`,已去掉文件夹、控制字符和方向标记),它会成为 -这个 PDF 的标题,并去掉 `.pdf`:例如“Lease 2026”;如果这里已有同名标题,则为 -“Lease 2026 (2)”。文件本身保留应用为它取的名字(应用存储不支持重命名),标题记录在 -`library.json` 中。没有 `name` 时(在 Android 上,或清理后什么都不剩),标题就是 -文件名,例如“Imported PDF 2”。从有标题的 PDF 拆分出的部分按它的标题命名 -(“Lease 2026-part2”)。资料库按存储中的顺序列出文件。 - -`files.status`(只问一次,不打开任何东西)给出一次导入可接受的最大文件:桌面上是 -64 MiB,资料库会显示这个值;剩余存储空间同样会限制导入。导入被拒绝时,资料库顶部 -会显示一张卡片,先用平实的话说明原因,下面附上宿主自己的消息:应用不在前台、存储 -已满、文件太多、文件太大、另一个传输正在进行,或者没有文件服务。 +“Open a PDF from this device”请 Shell 的文件服务把用户在宿主自己的对话框中选择的一个文件 +复制到资料库中。应用要先为新文件命名,因为 `files.import` 在对话框打开前就接收目标路径: +依次为 `Imported PDF.pdf`、`Imported PDF 2.pdf` 等。宿主会报告所选文件的显示名称 +(`name`),它去掉 `.pdf` 后成为 PDF 的标题(“Lease 2026”;若该标题已被使用则为 +“Lease 2026 (2)”);文件本身保留应用为它取的名字,标题记录在 `library.json` 中。随后它在 +一个标签中打开。若被拒绝,主页会用通俗的文字说明原因,下面附上宿主自己的消息。 -## 工作方式 +## 工作原理 -下文所有路径都相对于应用的存储,即 `fs.*` 看到的目录。引擎也在同一目录中工作 -(ADR 0013“Engines work in the caller's own folder”):应用打开时发出的调用可以 -替换文件,引擎写入的内容计入应用的存储。 +所有路径都相对于应用的存储,即 `fs.*` 看到的文件夹;引擎也在同一个文件夹中工作。 | 路径 | 内容 | | --- | --- | -| `accounts/device/library/*.pdf` | PDF 文件(ADR 0004 §11:系统应用的数据位于其 `device` 账户目录),包括导入的文件以及合并和拆分写入的文件 | -| `accounts/device/library.json` | 引擎对每个文件给出的信息(页数和大小),以及与文件名不同时的标题 | -| `cache/covers/.png` | 每个文件的首页(可清除) | -| `cache/pages//p.png` | 当前打开文档的缩略图;打开一个文档时会删除其他文档的缩略图,因为应用存储最多容纳 256 个条目 | -| `cache/view.png` | 单页视图的图片 | - -| 调用 | 何时调用 | 参数 | -| --- | --- | --- | -| `pdf.info` | 首次列出某个文件、打开文档时 | `{path}` | -| `pdf.render` | 首页图、缩略图(`max_side` 360)和单页视图(`max_side` 1400) | `{path, page, out, max_side}` | -| `pdf.text` | Text 标签页 | `{path}`,或前 512 页的 `pages` | -| `pdf.merge` | 合并 | `{paths, out}` | -| `pdf.split` | 拆分 | `{path, out_dir, every}` 或 `{path, out_dir, before}` | -| `files.status` | 应用启动时 | `{}` | -| `files.import` | Open a PDF from this device | `{path: "accounts/device/library/Imported PDF.pdf"}`;结果中的 `name`(如有)就是标题 | - -应用从不读取 `pdf.info` 的 `document.path`:在 #434 之前,它是引擎在宿主上的绝对 -路径,而应用使用的每个路径都是它自己的相对路径。 - -当引擎没有空间写入时(`… more than the N bytes left in this storage`),资料库、 -页面以及合并和拆分界面会说明 PDF Tools 的存储已满,Merge 或 Split 也会说明失败 -原因。移除一个 PDF 会释放它占用的空间,并让应用重新尝试生成页面图片。 - -修改脚本时请注意: - -- 调用逐个进行:服务在 UI 线程上运行(#399)。 -- 拒绝会在 `host.request` 内部直接回调。在随 #413 而来的运行时补丁之前,在这样的 - 回调里调用 UI 会丢失回调的其余部分;`engine()` 和文件调用总是把结果交给下一轮 - 处理(`start_timeout`)。 -- 页面图片用 `fs.read_bytes` 读取,用 `binary_resource` 绘制(运行时 - `platform/src/script/res.rs`,可通过 widgets prelude 使用),每张图保存一个句柄, - 重绘时会复用已解码的纹理。每个界面有自己的滚动视图,在页面中声明,并通过显式 - `render()` 填充。这样设计是因为在 `card-host` 中观察到两点:在另一个视图的 - `on_render` 中创建的滚动视图要到下一次绘制后才存在,而它自动进行的首次渲染在 - 持有已保存的图片时是空的;另外,带 `on_render` 且初始为 `visible: false` 的视图 - 在 `set_visible(true)` 之后仍然隐藏。因此这些滚动视图放在普通 `View` 中,通过 - 显示和隐藏外层视图来切换。 -- 每个处理函数、定时器或回调有 64 ms 的预算;合并或拆分之后的步骤(清除过期缓存、 - 重新扫描、进入下一个界面)分在不同的轮次中执行。 +| `accounts/device/library/*.pdf` | 所有 PDF,包括导入、提取和合并得到的 | +| `accounts/device/library.json` | 每个文件的标题(与文件名不同时)、页数、大小、上次打开时间、离开时所在的页面和缩放比例、封面的渲染结果;以及 `who`:评论使用的名字和 Fill & Sign 放置的姓名缩写 | +| `.cache/pages//@.png` | 引擎的页面渲染结果:它的缓存,最多 16 MiB、64 个文件 | + +给修改脚本的人的说明: + +- **引擎工作在 Shell 的 UI 线程上运行(#399)。** 应用同时只发出一个后台渲染 + (`pump()`):先是当前页及其后两页,然后是可见的缩略图,然后是这一组页面中的其余页面, + 再是其他缩略图,最后是主页的封面。一处更改只丢弃它所涉及页面的渲染结果;Undo 和 Redo + 不说明改了什么,因此重新绘制当前显示的那一组页面。 +- **脚本应用拿不到滚动位置。** 运行时不给脚本应用读取或设置滚动视图位置的方法。所以画布是 + 一个静态的 `ScrollXYView`,在渲染结果陆续到达时保持位置不变;前往某一页(缩略图、大纲、 + 查找结果、浮动控制条)时,先把画布绘制成空的一帧,使其滚动位置被限制回顶部,再填入从该页 + 开始的一组页面。该页之前的页面可以用浮动控制条的上一页回去。 +- **脚本应用收不到键盘事件。** 需求中列出的每个快捷键(⌘F、⌘+ 和 ⌘−、⌘0、方向键、⌘Z、 + ⇧⌘Z、⌘S)都改为按钮;Search in document 在按 Return 时执行,这是文本框会报告的事件。 +- **状态保存在 `mod.pdftools` 上。** 切换浅色/深色会重新运行脚本(#440);每个 `let` 都会 + 重新开始,`mod` 保留下来,`resume()` 用新的外观重新绘制每个区域。 +- **在这个运行时中,Label 会把自己的 margin 和 padding 绘制两次** + (`widgets/src/label.rs` 把加了内边距的 walk 同时交给它的 turtle 和文字):不要给 Label + 设置 margin 或 padding,改为在外面套一个 View。 +- **`on_render` 一开始就隐藏的视图会一直隐藏**:需要隐藏的区域都套在普通的 View 中,改为 + 显示和隐藏外层 View。 +- 每个处理函数、定时器或回调有 64 ms 的挂钟时间预算;页面图像用 `fs.read_bytes` 读取并用 + `binary_resource` 显示,每张图像保存一个句柄。翻页或打开文件改动了 `library.json` 时, + 由心跳定时器写入(至多每 2 秒一次),因此点按从不等待磁盘;新的标题、移除操作或 + “Comment as”中的名字会立即写入。 ## 测试 -pdf 服务的 `pdftools_fixture` 示例会在应用启动前,把四个示例 PDF 和一个损坏的文件 -写入某个应用根目录(OctoSense home 的 `apps/`,或 `card-host --app-data` 目录)下 -PDF Tools 的存储中。加上 `--import-sample ` 时,它再写出一个 PDF:一份三页的 -花园计划。它从不调用引擎;如果存储中已有数据,它会拒绝写入。 +资料库的规则是 `main.splash` 中的纯函数,由 Rust 在脚本虚拟机中执行包内函数来测试 +(`crates/shell/src/pdftools_model_tests.rs`):导入的 PDF 的标题、拆分出的部分的标题、 +资料库索引在重启后保留的内容、页面范围所指的页面、查找摘要中加粗的匹配、头像中的姓名缩写, +以及评论的日期。 ```sh -cargo run --locked -p octosense-pdf-service --example pdftools_fixture -- -cargo run --locked -p octosense-pdf-service --example pdftools_fixture -- --import-sample +cargo test --locked -p octosense-shell --lib pdftools_model ``` -`tests/ui.py` 在由本仓库构建的隐藏桌面 Shell 中端到端运行应用,使用真实引擎:每次 -运行都创建一个新的 `OCTOSENSE_HOME`,把示例写入 `apps/os.pdftools/`,在 -Terminal.app 标签页中启动 `target/debug/octosense`(`MAKEPAD_HIDE_WINDOWS=1`、 -`MAKEPAD_REMOTE`、`MAKEPAD_WM_TEST_APP=pdftools`),通过远程桥驱动它,保存每张 -截图,最后以 `/quit` 结束。一个锁目录保证本机同时只运行一个隐藏 Shell;如果另一个 -OctoSense 正在运行,它会拒绝启动。 +`tests/ui.py` 端到端地运行应用。使用 `--shell` 时,它使用由本检出构建的隐藏桌面 Shell 和 +真实引擎,在 pdf 服务的 `pdftools_fixture` 示例写入的示例 PDF 上运行(每次运行都新建一个 +`OCTOSENSE_HOME`,从 Terminal.app 的标签页以 `MAKEPAD_HIDE_WINDOWS=1`、`MAKEPAD_REMOTE` +和 `MAKEPAD_WM_TEST_APP=pdftools` 启动 `target/debug/octosense`,通过远程桥驱动它,保存 +每一张截图并以 `/quit` 结束;一个锁文件夹保证机器上同一时间只有一个隐藏 Shell,有其他 +OctoSense 在运行时它会等待)。使用 `--card-host` 时,它使用 App Hub 的 `card-host`,后者 +不提供任何宿主服务。 ```sh -cargo build --locked -p octosense +cargo build --locked -p octosense --no-default-features --features app-hub,craft-engines python3 apps/pdftools/tests/ui.py --shell target/debug/octosense --lock --output target/pdftools-ui -python3 apps/pdftools/tests/ui.py --card-host /target/release/card-host --only missing --output target/pdftools-ui +python3 apps/pdftools/tests/ui.py --card-host /target/release/card-host --output target/pdftools-ui ``` +运行时给每个处理函数 64 ms 的挂钟时间,因此在繁忙的机器上,一次运行可能因为某个处理函数 +等待 CPU 而失败(日志中出现 `script time budget exceeded`);对这样的机器, +`--budget-ms 1000` 会提高宿主的预算(`MAKEPAD_SPLASH_BUDGET_MS`)。 + | 运行 | 覆盖内容 | | --- | --- | -| `shell` | 在示例上先以浅色、再通过 Shell 自己的样式菜单切换到深色走完每个界面:首页图、页面、单页视图、文字和查找、信息、两种拆分(其中一次替换已有文件)、合并、打开、移除、损坏的文件,以及“Open a PDF from this device” | -| `restart` | 再次使用同一个 home:合并和拆分写入的文件仍在;在两次运行之间按导入留下文件的方式放入资料库的 `Imported PDF.pdf` 会被读取、绘制并打开 | -| `full` | 存储被填到距 64 MiB 上限只差 8 KB:资料库和一次合并上显示引擎的拒绝,然后移除一个文件 | -| `empty` | 没有 PDF:空资料库及其 Open 按钮,浅色和深色各一次 | -| `missing` | App Hub 的 `card-host`,它不提供任何宿主服务:没有引擎,也没有文件服务 | -资料库的规则是 `main.splash` 中的纯函数,由 Rust 在脚本虚拟机中求值应用包的函数 -来测试(`crates/shell/src/pdftools_model_tests.rs`,与 Maps 和 Photos 的做法相同): -导入的 PDF 在有名称、没有名称、名称已被其他 PDF 使用、名称清理后什么都不剩时的标题; -拆分部分的标题;以及资料库索引在重启后保留的内容。 +| `shell` | 在 Shell 自己的窗口中、在示例上覆盖每种模式,先浅色,再通过 Shell 自己的样式菜单切到深色(深色这一轮在浅色一轮留下的状态上进行):主页及其封面、“Open a PDF from this device”被拒绝、阅读和缩略图、查找(“9 matches on 4 pages”)、一处高亮及其回复、一次旋转及其撤销、带页面范围的合并(“One PDF of 5 pages”)、Fill & Sign 放置的姓名缩写、一次文字编辑、保存,以及损坏的文件 | +| `restart` | 再次使用同一个主目录:资料库、其中的标题、PDF 离开时所在的页面,以及按导入方式放入的 `Imported PDF.pdf` | +| `full` | 存储被填到距 64 MiB 只差 8 KB:主页上的拒绝提示,然后移除一个 PDF(会再确认一次) | +| `empty` | 没有 PDF:空资料库及其 Open 按钮,浅色和深色 | +| `missing` | 使用发布包的 `card-host`:没有引擎,也没有文件服务 | +| `fixture` | 使用开发夹具的 `card-host`:每个设计过的界面按设计图的 1536 x 1024 显示,浅色和深色,每张截图都与它的设计图并排放在 `compare/` 中,另有损坏、受保护和未保存三种状态 | + +开发夹具从不随应用发布。`dev-fixture/engine.splash` 是 pdf 引擎的替身,回答 SERVICE.md 中 +的方法;`tests/ui.py` 在包的临时副本中用它替换 `engine()`。`dev-fixture/make_fixture.py` +用取自已批准设计图示例文字的示例文档填充应用的存储:页面以图像形式保存,并附上替身要回答的 +内容(大纲、带位置框的文字行和段落、表单字段、评论)。 ```sh -cargo test --locked -p octosense-shell --lib pdftools_model +python3 apps/pdftools/dev-fixture/make_fixture.py /os.pdftools ``` -隐藏的 Shell 无法驱动宿主的对话框,而且隐藏窗口从不获得焦点,因此文件服务会在 -对话框打开前拒绝 `files.import`(`foreground_required: …`);这些运行会检查应用 -显示了这个拒绝。导入之后的流程:上面的标题规则,以及 `restart` 运行——它走的是一个 -索引从未见过的资料库文件,由真实引擎读取,再由应用打开。 +隐藏的 Shell 中无法操作宿主的对话框,而且隐藏窗口永远不会获得焦点,所以文件服务会在对话框 +打开前就拒绝 `files.import`(`foreground_required: …`);各次运行会检查应用是否显示了这个 +拒绝。 ## 状态 -- **已验证**:在 macOS(Apple silicon)上的隐藏桌面 Shell 中使用真实引擎:上文的 - 每个界面(浅色和深色)、在同一存储上重启并按导入留下文件的方式放入一个 PDF、 - 存储已满、空资料库,以及不提供宿主服务的 `card-host`。截图、摘要和检查记录在 - [tests/evidence/shell-20261009](tests/evidence/shell-20261009/README.zh-CN.md)。 - 导入的 PDF 的标题由上面资料库规则的 Rust 测试验证。 -- **未验证**:宿主的文件对话框以及通过它进行的真实导入(脚本中的 `imported()`,它 - 使用经过测试的标题规则)、导入的其他拒绝情况、Shell 默认尺寸以外的窗口尺寸、Linux、 - Windows 和手机。 -- **已知问题**:切换浅色和深色会重新运行脚本(运行时的样式重新应用),所以应用会 - 回到资料库。Shell 证据中的深色截图页面网格保持两列:它们是在 Shell 于样式切换后 - 重新向托管应用发送尺寸(9d7a386e)之前生成的。 +- **已于 2026 年 10 月 10 日验证**(macOS,Apple 芯片;截图、并排对比图和记录见 + [tests/evidence/v2-20261010](tests/evidence/v2-20261010/README.zh-CN.md)):模型测试;在 + 隐藏的桌面 Shell 中使用真实引擎、按运行时 64 ms 的脚本预算进行的 `shell`、`restart`、 + `full` 和 `empty` 运行(在其他构建占用机器时又用 `--budget-ms 1000` 运行了一次);在 + card-host 中进行的 `missing` 和 `fixture` 运行,浅色和深色。`fixture` 使用了 + `--budget-ms 1000`:它的替身引擎在脚本中作答,按 64 ms 运行时,深色一轮中有一次作答超时。 +- **未验证**:宿主的文件对话框和真实的导入(隐藏窗口无法打开它);使用真实引擎时的表单 + 字段、大纲、Redo、其他评论和页面操作、受密码保护的 PDF、八个 PDF 的上限以及未保存更改 + 的询问(开发夹具覆盖了它们的界面);Shell 默认窗口和 1536 x 1024 以外的窗口尺寸; + Linux、Windows、手机。 +- **已知问题**:评论的日期是引擎的 UTC 时间被当作本地时间读取,因此在 UTC 以西的晚上做的 + 评论会显示第二天的日期,直到 pdf 服务完成转换。状态栏只统计资料库中的 PDF:文件服务不 + 报告存储的用量。在负载很高的机器上,一次点按可能超出 64 ms 预算而丢失(在 `library.json` + 移出点按路径之前,于 60 到 150 的平均负载下见到过)。 +- **不在这个版本中**:导出(没有已批准的设计)、打开受密码保护的 PDF、键盘快捷键(运行时 + 不给脚本应用按键事件)、Edit 面板中的 Alignment 和 Colour(`pdf.edit_text` 只接收文字)。 diff --git a/apps/pdftools/bundle/icons/arrow-down.svg b/apps/pdftools/bundle/icons/arrow-down.svg new file mode 100644 index 00000000..26a7f2fa --- /dev/null +++ b/apps/pdftools/bundle/icons/arrow-down.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/arrow-up.svg b/apps/pdftools/bundle/icons/arrow-up.svg new file mode 100644 index 00000000..3c98eeba --- /dev/null +++ b/apps/pdftools/bundle/icons/arrow-up.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/bookmark.svg b/apps/pdftools/bundle/icons/bookmark.svg new file mode 100644 index 00000000..564f138f --- /dev/null +++ b/apps/pdftools/bundle/icons/bookmark.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/calendar.svg b/apps/pdftools/bundle/icons/calendar.svg new file mode 100644 index 00000000..cd18cde8 --- /dev/null +++ b/apps/pdftools/bundle/icons/calendar.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/check.svg b/apps/pdftools/bundle/icons/check.svg new file mode 100644 index 00000000..6ebebcc6 --- /dev/null +++ b/apps/pdftools/bundle/icons/check.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/chevron-down.svg b/apps/pdftools/bundle/icons/chevron-down.svg new file mode 100644 index 00000000..305f70b6 --- /dev/null +++ b/apps/pdftools/bundle/icons/chevron-down.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/chevron-left.svg b/apps/pdftools/bundle/icons/chevron-left.svg new file mode 100644 index 00000000..98509cdf --- /dev/null +++ b/apps/pdftools/bundle/icons/chevron-left.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/chevron-right.svg b/apps/pdftools/bundle/icons/chevron-right.svg new file mode 100644 index 00000000..038fbefc --- /dev/null +++ b/apps/pdftools/bundle/icons/chevron-right.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/chevron-up.svg b/apps/pdftools/bundle/icons/chevron-up.svg new file mode 100644 index 00000000..4abd82d5 --- /dev/null +++ b/apps/pdftools/bundle/icons/chevron-up.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/close.svg b/apps/pdftools/bundle/icons/close.svg new file mode 100644 index 00000000..06c7b0a5 --- /dev/null +++ b/apps/pdftools/bundle/icons/close.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/combine.svg b/apps/pdftools/bundle/icons/combine.svg new file mode 100644 index 00000000..445083c5 --- /dev/null +++ b/apps/pdftools/bundle/icons/combine.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/comment.svg b/apps/pdftools/bundle/icons/comment.svg new file mode 100644 index 00000000..68c2187e --- /dev/null +++ b/apps/pdftools/bundle/icons/comment.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/extract.svg b/apps/pdftools/bundle/icons/extract.svg new file mode 100644 index 00000000..1a7a0120 --- /dev/null +++ b/apps/pdftools/bundle/icons/extract.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/file-pdf.svg b/apps/pdftools/bundle/icons/file-pdf.svg new file mode 100644 index 00000000..c9c96670 --- /dev/null +++ b/apps/pdftools/bundle/icons/file-pdf.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/filter.svg b/apps/pdftools/bundle/icons/filter.svg new file mode 100644 index 00000000..2525fb09 --- /dev/null +++ b/apps/pdftools/bundle/icons/filter.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/fit-page.svg b/apps/pdftools/bundle/icons/fit-page.svg new file mode 100644 index 00000000..f1130d61 --- /dev/null +++ b/apps/pdftools/bundle/icons/fit-page.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/fit-width.svg b/apps/pdftools/bundle/icons/fit-width.svg new file mode 100644 index 00000000..8f98209f --- /dev/null +++ b/apps/pdftools/bundle/icons/fit-width.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/folder.svg b/apps/pdftools/bundle/icons/folder.svg new file mode 100644 index 00000000..803877e3 --- /dev/null +++ b/apps/pdftools/bundle/icons/folder.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/grip.svg b/apps/pdftools/bundle/icons/grip.svg new file mode 100644 index 00000000..b3889d48 --- /dev/null +++ b/apps/pdftools/bundle/icons/grip.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/highlight.svg b/apps/pdftools/bundle/icons/highlight.svg new file mode 100644 index 00000000..d678ab93 --- /dev/null +++ b/apps/pdftools/bundle/icons/highlight.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/home.svg b/apps/pdftools/bundle/icons/home.svg new file mode 100644 index 00000000..2946d9ff --- /dev/null +++ b/apps/pdftools/bundle/icons/home.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/info.svg b/apps/pdftools/bundle/icons/info.svg new file mode 100644 index 00000000..e98c21aa --- /dev/null +++ b/apps/pdftools/bundle/icons/info.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/insert.svg b/apps/pdftools/bundle/icons/insert.svg new file mode 100644 index 00000000..e0b88806 --- /dev/null +++ b/apps/pdftools/bundle/icons/insert.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/lock.svg b/apps/pdftools/bundle/icons/lock.svg new file mode 100644 index 00000000..f291a8b3 --- /dev/null +++ b/apps/pdftools/bundle/icons/lock.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/minus.svg b/apps/pdftools/bundle/icons/minus.svg new file mode 100644 index 00000000..5f5e05d7 --- /dev/null +++ b/apps/pdftools/bundle/icons/minus.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/more.svg b/apps/pdftools/bundle/icons/more.svg new file mode 100644 index 00000000..656d8761 --- /dev/null +++ b/apps/pdftools/bundle/icons/more.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/move.svg b/apps/pdftools/bundle/icons/move.svg new file mode 100644 index 00000000..e987f4c3 --- /dev/null +++ b/apps/pdftools/bundle/icons/move.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/note.svg b/apps/pdftools/bundle/icons/note.svg new file mode 100644 index 00000000..285e7590 --- /dev/null +++ b/apps/pdftools/bundle/icons/note.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/outline.svg b/apps/pdftools/bundle/icons/outline.svg new file mode 100644 index 00000000..c22edd96 --- /dev/null +++ b/apps/pdftools/bundle/icons/outline.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/page.svg b/apps/pdftools/bundle/icons/page.svg new file mode 100644 index 00000000..9734c708 --- /dev/null +++ b/apps/pdftools/bundle/icons/page.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/pdf-hero.svg b/apps/pdftools/bundle/icons/pdf-hero.svg new file mode 100644 index 00000000..3d94831e --- /dev/null +++ b/apps/pdftools/bundle/icons/pdf-hero.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/plus.svg b/apps/pdftools/bundle/icons/plus.svg new file mode 100644 index 00000000..505f9503 --- /dev/null +++ b/apps/pdftools/bundle/icons/plus.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/redo.svg b/apps/pdftools/bundle/icons/redo.svg new file mode 100644 index 00000000..342340fa --- /dev/null +++ b/apps/pdftools/bundle/icons/redo.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/rotate-left.svg b/apps/pdftools/bundle/icons/rotate-left.svg new file mode 100644 index 00000000..b12e2a5d --- /dev/null +++ b/apps/pdftools/bundle/icons/rotate-left.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/rotate-right.svg b/apps/pdftools/bundle/icons/rotate-right.svg new file mode 100644 index 00000000..dab9d236 --- /dev/null +++ b/apps/pdftools/bundle/icons/rotate-right.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/save.svg b/apps/pdftools/bundle/icons/save.svg new file mode 100644 index 00000000..32897b42 --- /dev/null +++ b/apps/pdftools/bundle/icons/save.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/search.svg b/apps/pdftools/bundle/icons/search.svg new file mode 100644 index 00000000..59e53aff --- /dev/null +++ b/apps/pdftools/bundle/icons/search.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/strike.svg b/apps/pdftools/bundle/icons/strike.svg new file mode 100644 index 00000000..7b7c40d3 --- /dev/null +++ b/apps/pdftools/bundle/icons/strike.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/text.svg b/apps/pdftools/bundle/icons/text.svg new file mode 100644 index 00000000..599a0cf6 --- /dev/null +++ b/apps/pdftools/bundle/icons/text.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/textbox.svg b/apps/pdftools/bundle/icons/textbox.svg new file mode 100644 index 00000000..b39bb613 --- /dev/null +++ b/apps/pdftools/bundle/icons/textbox.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/trash.svg b/apps/pdftools/bundle/icons/trash.svg new file mode 100644 index 00000000..81158a6e --- /dev/null +++ b/apps/pdftools/bundle/icons/trash.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/triangle-down.svg b/apps/pdftools/bundle/icons/triangle-down.svg new file mode 100644 index 00000000..5eee17ab --- /dev/null +++ b/apps/pdftools/bundle/icons/triangle-down.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/triangle-right.svg b/apps/pdftools/bundle/icons/triangle-right.svg new file mode 100644 index 00000000..2b52e048 --- /dev/null +++ b/apps/pdftools/bundle/icons/triangle-right.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/underline.svg b/apps/pdftools/bundle/icons/underline.svg new file mode 100644 index 00000000..a03e1e64 --- /dev/null +++ b/apps/pdftools/bundle/icons/underline.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/undo.svg b/apps/pdftools/bundle/icons/undo.svg new file mode 100644 index 00000000..e979dac0 --- /dev/null +++ b/apps/pdftools/bundle/icons/undo.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/icons/warning.svg b/apps/pdftools/bundle/icons/warning.svg new file mode 100644 index 00000000..8593b8f5 --- /dev/null +++ b/apps/pdftools/bundle/icons/warning.svg @@ -0,0 +1 @@ + diff --git a/apps/pdftools/bundle/main.splash b/apps/pdftools/bundle/main.splash index 7998d455..54b690de 100644 --- a/apps/pdftools/bundle/main.splash +++ b/apps/pdftools/bundle/main.splash @@ -35,110 +35,76 @@ let UiCard = RoundedView{width: Fill height: Fit flow: Down spacing: 12 padding: let UiCaption = Label{padding: 0 flow: Flow.Right{wrap: true} width: Fill draw_text +: {color: ui_muted text_style: theme.font_regular{font_size: 13 line_spacing: 1.4}}} let UiTitle = Label{padding: 0 flow: Flow.Right{wrap: true} width: Fill draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 28 line_spacing: 1.2}}} // END shared app interface -// PDF Tools: the PDFs in this app's own storage, a viewer with page -// thumbnails, merge, split and text extraction. The shell's `pdf` engine -// service (pdfcraft, ADR 0013) does the work through host.request; this -// script never reads a PDF itself. +// PDF Tools v2: read, find, review, fill and sign, organize, combine and edit +// the PDFs in this app's own storage, on the desktop's pdf engine (pdfcraft, +// ADR 0013). Rebuilt from the approved designs in apps/pdftools/design (BRIEF.md, +// the generated screens in source/, the pixel maps in map/) through OctoSense App +// Flow: every region, size and colour below comes from those maps. // -// Storage (ADR 0004 §11): the PDFs in accounts/device/library/, an index of -// what the engine said about them in accounts/device/library.json, and the -// page images the engine renders in cache/. The engine works in this storage -// (ADR 0013's per-caller areas): every path given to it is relative to the -// folder fs.* sees, a call made while the app is open may replace a file, -// and what it writes counts against this app's storage. +// The engine: every call is host.request("pdf.") on the contract in +// apps/pdftools/design/SERVICE.md, and nothing else. A PDF opens once per tab (pdf.open +// gives a handle); renders, finds, comments, fields, page changes and text +// edits name that handle, Undo and Redo use the engine's history, and Save +// writes the PDF back. This script never reads a PDF itself. // -// Opening a PDF from this device: files.import copies the file the person -// chooses into a new library file (`Imported PDF.pdf`, `Imported PDF 2.pdf`, -// …) and reports the chosen file's name, which becomes its title (kept in -// the index: app storage has no rename); without one the title is the file -// name. Then it opens like any other PDF here: the same engine calls on the -// same relative path. +// Storage (64 MiB, the system ceiling): the PDFs in accounts/device/library/, +// the library index in accounts/device/library.json (title, pages, size, when +// it was last opened, its page and zoom, its cover), and the page renders the +// engine writes in its own cache (.cache/pages/, at most 16 MiB). +// +// Engine work runs on the shell's UI thread until #399 lands, so pages render +// one at a time at screen resolution and are kept, thumbnails fill in one by +// one, and a change re-renders only the pages on show. +// +// The runtime gives script apps no scroll position and no keyboard events. +// So the canvas shows a run of pages starting at the page you went to (a +// thumbnail, the outline, a find result, ‹ and ›), and every shortcut the +// brief lists is a button. let LIBRARY = "accounts/device/library" let INDEX = "accounts/device/library.json" -let COVERS = "cache/covers" -let PAGES = "cache/pages" -let VIEW_FILE = "cache/view.png" +let CACHE = ".cache/pages" // What an imported PDF is called: the import names its destination first. let IMPORT_NAME = "Imported PDF" -// The longest edge, in pixels, of the page images the engine renders. -let THUMB_SIDE = 360 -let VIEW_SIDE = 1400 -// What the pdf service accepts: 2 to 16 files per merge; text of at most -// 512 pages per call. +// The app's storage: App Hub's system ceiling (no storage.max_bytes). +let QUOTA = 67108864 +// What the pdf service allows: 8 open documents, 2 to 16 files per merge, +// 512 pages named in one call. +let MAX_OPEN = 8 let MAX_MERGE = 16 -let MAX_TEXT = 512 -let PAGE_BATCH = 24 -let TEXT_BATCH = 8 - -// Icons from the theme's icon font (Font Awesome solid). -let ICON_NEXT = "\u{f054}" -let ICON_WARN = "\u{f071}" -let ICON_DONE = "\u{f058}" -let ICON_UP = "\u{f062}" -let ICON_DOWN = "\u{f063}" -let ICON_REMOVE = "\u{f00d}" -let ICON_CUT = "\u{f0c4}" -let ICON_OPEN = "\u{f56f}" -let ICON_TRASH = "\u{f1f8}" -// A done mark, not text: the same green in both appearances. -let GREEN = #x2e9d5b - -let route = "library" // library | doc | page | merge | split -let tab = "pages" // the document's: pages | text | info -let docs = [] // {name path title pages bytes error} -let index = {} // name -> {pages bytes} -let engine_state = "" // "" until the engine answers, then ok | missing -let engine_note = "" // why it is missing: the host's refusal -let space_full = false // the engine had no room left in this app's storage -let files_known = false // files.status answered -let files_ok = false // files.status: this device can copy a file in -let max_import = 0 // the largest file it copies in, in bytes -let importing = false // a file is being chosen or copied in -let lib_note = nil // {head detail note}: why a file couldn't be added -let confirm_remove = false - -let cover_src = {} // name -> image of page 1 -let cover_failed = {} -let lib_busy = false - -let current = nil // the open document -let info = nil // pdf.info's answer for it -let doc_error = "" -let gen = 0 // bumped when a document opens or closes -let thumb_src = {} // "p" -> image -let thumb_failed = {} -let thumbs_busy = false -let shown_pages = PAGE_BATCH -let columns = 2 -let tile_w = 184 -let page_no = 1 -let view_src = nil -let view_note = "" -let view_seq = 0 -let text_pages = nil // [{page text}] -let text_state = "" // "" | loading | error -let text_note = "" -let query = "" -let shown_text = TEXT_BATCH - -let merge_step = 1 // 1 choose, 2 order, 3 done -let picked = [] // names, in merge order -let merge_name = "" -let merge_busy = false -let merge_error = "" -let merge_result = nil // the merged document - -let split_mode = "every" // every | at -let split_every = 2 -let split_cuts = [] // pages that start a new file, ascending -let split_busy = false -let split_error = "" -let split_result = nil // [document] - -let banner_n = 0 - -// ---- small helpers ----------------------------------------------------- +let MAX_NAMED = 512 +// Pages in the canvas's run, and how many render ahead of the current one. +let RUN = 8 +let AHEAD = 2 +// Render pixels per point at 100%: the designs' 100% is one PDF point per +// layout point, drawn at twice that for a high-density screen. +let DENSITY = 2 +let THUMB_DPI = 24 +let GRID_DPI = 48 +let COVER_DPI = 48 +// The thumbnails the Pages panel and the Pages grid list at a time. +let THUMB_WINDOW = 40 +let ZOOMS = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 3.0] +// The window's regions, in points, as measured on the approved designs +// (design/map/02-reading.json and the others). +let TAB_H = 53 +let MODE_H = 54 +let RAIL_W = 84 +let PANEL_W = 204 +let LIST_W = 288 +let RIGHT_W = 360 +let THREAD_W = 424 +let STATUS_H = 44 +let PAGE_TOP = 13 +let PAGE_GAP = 13 +// Comment colours: the four dots of the Comment tool row. +let MARKS = ["yellow", "green", "blue", "pink"] +let MARK_HEX = {yellow: "#ffd84d" green: "#5cb85c" blue: "#2d7ff9" pink: "#f0609e"} +let MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] +let MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] + +// ---- the library's rules: pure functions, tested from Rust by +// crates/shell/src/pdftools_model_tests.rs (everything up to start_timeout) -- // A missing key reads as `fallback` (as News, Photos and Wasm Lab do it). fn optional(it, key, fallback){ @@ -150,12 +116,6 @@ fn pick(c, a, b){ if c { return a } b } fn plural(n, one, many){ if n == 1 { return "1 " + one } "" + n + " " + many } fn is_pdf(name){ regex("\\.pdf$", "i").test(name) } fn stem(name){ name.replace(regex("\\.pdf$", "i"), "") } -fn size_text(bytes){ - if bytes == nil { return "" } - if bytes < 1024 { return plural(bytes, "byte", "bytes") } - if bytes < 1048576 { return "" + round(bytes / 1024) + " KB" } - "" + floor(bytes / 104857.6) / 10 + " MB" -} fn has(list, x){ for v in list { if v == x { return true } } false @@ -164,122 +124,33 @@ fn position(list, x){ for i v in list { if v == x { return i } } -1 } -fn same_list(a, b){ - if a.len() != b.len() { return false } - for i v in a { if v != b[i] { return false } } - true -} -fn rows_of(items, n){ - let rows = [] - let row = [] - for it in items { - row.push(it) - if row.len() == n { rows.push(row) row = [] } - } - if row.len() > 0 { rows.push(row) } - rows -} -fn first_n(items, n){ - let out = [] - for it in items { if out.len() < n { out.push(it) } } - out -} -fn range_list(n){ - let out = [] - for i in n { out.push(i + 1) } - out -} -let specials = ["\\" "." "*" "+" "?" "(" ")" "[" "]" "{" "}" "|" "^" "$"] -fn escape_re(text){ - let out = text - for c in specials { out = out.replace(c, "\\" + c) } - out -} -fn remove_tree(path){ - if !fs.exists(path) { return } - for entry in fs.list(path) { - let child = path + "/" + entry.replace("/", "") - if entry.search("/") >= 0 { remove_tree(child) } else { fs.remove(child) } - } - fs.remove(path) -} - -// ---- the engine ---------------------------------------------------------- - -fn missing(error){ - if !error.is_string() { return false } - error.search("was not granted") >= 0 || error.search("no service answers") >= 0 -} - -// One call to the pdf engine. `done` gets {is_ok data error}, always on a -// later turn: a refusal answers inside host.request itself, where UI calls -// lost the rest of the callback on runtimes before #413's overlay. -fn engine(method, args, done){ - host.request("pdf." + method, args, fn(r){ - let answer = {is_ok: r.is_ok data: r.data error: r.error} - start_timeout(0.01, || done(answer)) - }) -} - -// True when `a` failed because the engine is not there for this app. -fn engine_gone(a){ - if a.is_ok { engine_state = "ok" return false } - if missing(a.error) { - engine_state = "missing" - engine_note = a.error - return true - } - false -} -fn engine_headline(){ - if engine_note.search("was not granted") >= 0 { return "PDF Tools can't use the PDF engine" } - "This device has no PDF engine" -} -fn engine_detail(){ - let why = "This OctoSense doesn't let PDF Tools use it." - if engine_note.search("was not granted") < 0 { why = "PDF Tools needs the OctoSense desktop, which has it." } - "Pages, merging, splitting and text all come from the engine. " + why + " Your PDFs stay safe in this app's storage." -} -// The storage quota or its file count refused a write: the engine's own -// words (`… more than the N bytes left in this storage`) or the host's. -fn storage_full(error){ - let e = "" + error - e.search("bytes left in this storage") >= 0 || e.search("app storage is full") >= 0 || e.search("too many files") >= 0 -} -// An engine error without the "pdf.text: " prefix the service adds. -fn engine_error(error){ - if !error.is_string() { return "The PDF engine gave no reason." } - if storage_full(error) { return "There isn't room for it in PDF Tools' storage. Remove a PDF you no longer need, then try again." } - error.replace(regex("^pdf\\.[a-z]+: "), "") -} -// Every engine write: a refusal for room shows on the library and the pages -// until a write succeeds or a PDF is removed. -fn wrote(a){ - if a.is_ok { space_full = false return } - if storage_full(a.error) { space_full = true } -} - -// ---- the library --------------------------------------------------------- - -fn load_index(){ - if !fs.exists(INDEX) { return } - let v = try { fs.read(INDEX).parse_json() } catch { nil } - if v != nil && optional(v, "version", 0) == 1 { index = optional(v, "docs", {}) } +fn size_text(bytes){ + if bytes == nil { return "" } + if bytes < 1024 { return plural(bytes, "byte", "bytes") } + if bytes < 1048576 { return "" + round(bytes / 1024) + " KB" } + "" + floor(bytes / 104857.6) / 10 + " MB" } -fn save_index(){ - try { fs.write(INDEX, {version: 1 docs: index}.to_json()) } catch {} +// "14.6 of 64 MB used": the PDFs, against the app's storage. +// Under 1 MB it counts in kilobytes ("90 KB of 64 MB used"). +fn used_text(bytes){ + let quota_part = " of " + round(QUOTA / 1048576) + " MB used" + if bytes < 1048576 { return size_text(bytes) + quota_part } + "" + floor(bytes / 104857.6) / 10 + quota_part } -// ---- the library's rules: pure functions, tested from Rust by -// crates/shell/src/pdftools_model_tests.rs ------------------------------------ // A library row: the file, and what the index kept about it (`known`, or -// nil): its pages and size, and its title, which is otherwise its name. +// nil): its pages and size, its title (otherwise its name), when it was last +// opened, the page and zoom it was left at, and its cover. fn doc_from(name, known){ - let d = {name: name path: LIBRARY + "/" + name title: stem(name) pages: nil bytes: nil error: ""} + let d = {name: name path: LIBRARY + "/" + name title: stem(name) pages: nil bytes: nil opened: 0 page: 1 zoom: nil cover: "" error: "" locked: false} if known != nil { d.pages = optional(known, "pages", nil) d.bytes = optional(known, "bytes", nil) d.title = optional(known, "title", d.title) + d.opened = optional(known, "opened", 0) + d.page = optional(known, "page", 1) + d.zoom = optional(known, "zoom", nil) + d.cover = optional(known, "cover", "") } d } @@ -310,98 +181,573 @@ fn title_for_part(title, name, part){ if title == stem(name) { return nil } title + stem(part).replace(stem(name), "") } +// The pages a range such as "1-4, 9" names in a PDF of `total` pages, in +// its order, or nil when it names no page or one the PDF doesn't have. +// Empty, or "all", is every page. +fn range_pages(text, total){ + let t = ("" + text).trim().replace(regex("[–—]", "g"), "-") + let out = [] + if t == "" || regex("^all$", "i").test(t) { + for i in total { out.push(i + 1) } + return out + } + // No return inside the loop: an else-if chain returning from a loop + // inside a for broke the script VM's stack ("pop_stack_value on empty + // stack") on this runtime. + let bad = false + for part in t.split(",") { + let p = part.trim() + if p != "" { + let ends = p.split("-") + let a = ends[0].trim().to_f64() + let b = a + if ends.len() == 2 { b = ends[1].trim().to_f64() } + if ends.len() > 2 || !(a >= 1) || !(b >= a) || b > total || floor(a) != a || floor(b) != b { bad = true } + if !bad { for k in b - a + 1 { out.push(a + k) } } + } + } + if bad || out.len() == 0 { return nil } + out +} +// A range as the engine takes it: "" (every page) stays "", "1–4" becomes "1-4". +fn range_arg(text){ ("" + text).trim().replace(regex("[–—]", "g"), "-") } +// A find snippet ("… strong [[revenue]] …") as HTML, its match in bold. +fn snippet_html(s){ + let safe = ("" + s).replace(regex("&", "g"), "&").replace(regex("<", "g"), "<").replace(regex(">", "g"), ">") + safe.replace(regex("\\[\\[", "g"), "").replace(regex("\\]\\]", "g"), "") +} +// Two letters for an avatar: "Jun Park" → "JP" (the first letters of the +// first and last words). +fn first_letter(word){ + let m = regex("^(.)").exec(word) + if m == nil { return "" } + m.captures[1] +} +fn initials_of(name){ + let words = [] + for w in ("" + name).trim().split(" ") { if w != "" { words.push(w) } } + if words.len() == 0 { return "?" } + if words.len() == 1 { return first_letter(words[0]) } + first_letter(words[0]) + first_letter(words[words.len() - 1]) +} +// When something happened, as the designs say it: "today", "yesterday", +// "3 Oct", or the year for an older date. +fn day_text(t, now_t){ + if t == nil || t <= 0 { return "" } + let a = local_time(t) + let b = local_time(now_t) + if a.year == b.year && a.month == b.month && a.day == b.day { return "today" } + let y = local_time(now_t - 86400) + if a.year == y.year && a.month == y.month && a.day == y.day { return "yesterday" } + if a.year == b.year { return "" + a.day + " " + MONTHS[a.month - 1] } + "" + a.day + " " + MONTHS[a.month - 1] + " " + a.year +} +fn two(n){ pick(n < 10, "0" + n, "" + n) } +// A comment's date as the engine reports it (a PDF date "D:20261010101200", +// or ISO "2026-10-10T10:12"), as Unix seconds in local time, or nil. +fn comment_time(date){ + let s = "" + date + let m = regex("^D?:?([0-9][0-9][0-9][0-9])-?([0-9][0-9])-?([0-9][0-9])[T ]?([0-9][0-9])?:?([0-9][0-9])?").exec(s) + if m == nil { return nil } + {year: m.captures[1].to_f64() month: m.captures[2].to_f64() day: m.captures[3].to_f64() + hour: optional(m.captures, 4, "0").to_f64() minute: optional(m.captures, 5, "0").to_f64()} +} +// "Today 10:12", "Yesterday", "3 Oct": a comment's date beside its author. +fn comment_when(date, now_t){ + let c = comment_time(date) + if c == nil { return "" + date } + let b = local_time(now_t) + let y = local_time(now_t - 86400) + let hm = pick(c.hour >= 0, " " + two(c.hour) + ":" + two(c.minute), "") + if c.year == b.year && c.month == b.month && c.day == b.day { return "Today" + hm } + if c.year == y.year && c.month == y.month && c.day == y.day { return "Yesterday" } + "" + c.day + " " + MONTHS[c.month - 1] +} +// Today, as Fill & Sign's date: "10 October 2026". +fn date_text(t){ + let a = local_time(t) + "" + a.day + " " + MONTH_NAMES[a.month - 1] + " " + a.year +} -fn doc_of(name){ doc_from(name, optional(index, name, nil)) } -fn remember(d){ - index[d.name] = index_entry(d.pages, d.bytes, d.title, d.name) - save_index() +start_timeout(0.05, || boot()) + +// ---- appearance ----------------------------------------------------------- +// The shared interface above decides light or dark (`ui_light`, from the +// theme the shell sets; a switch runs this file again). The values are the +// designs': light from the light screens, dark from 09-dark-reading, as +// design/map/ records them. Pages stay white in both. +let assets = "{{assets}}" +let pt_accent = if ui_light { #x116cdc } else { #x2f75d8 } +let pt_accent_hover = if ui_light { #x2a7de6 } else { #x3d82e0 } +let pt_accent_down = if ui_light { #x0c5bbd } else { #x2565c2 } +let pt_link = if ui_light { #x1c6ce0 } else { #x4d91e3 } +let pt_tabs = if ui_light { #xf4f4f5 } else { #x191e21 } +let pt_tab_on = if ui_light { #xffffff } else { #x232a2e } +let pt_bar = if ui_light { #xffffff } else { #x232a2e } +let pt_rail = if ui_light { #xffffff } else { #x1e252a } +let pt_panel = if ui_light { #xffffff } else { #x192025 } +let pt_head = if ui_light { #xffffff } else { #x1f262b } +let pt_desk = if ui_light { #xeceded } else { #x191f24 } +let pt_status = if ui_light { #xffffff } else { #x1b2226 } +let pt_pill = if ui_light { #xffffff } else { #x282d32 } +let pt_card = if ui_light { #xffffff } else { #x232a2e } +let pt_card_top = if ui_light { #xf7f8f9 } else { #x1e252a } +let pt_home = if ui_light { #xf7f8f9 } else { #x191f24 } +let pt_line = if ui_light { #xe1e2e3 } else { #x383d41 } +let pt_line_soft = if ui_light { #xececee } else { #x2c3236 } +let pt_thumb_line = if ui_light { #xd3d5d9 } else { #x464b50 } +let pt_tint = if ui_light { #xe9f2fe } else { #x275294 } +let pt_tint_line = if ui_light { #xcfe0fa } else { #x34619f } +let pt_tint_ink = if ui_light { #x2f6fd0 } else { #xbac7dd } +let pt_select = if ui_light { #xeff5fc } else { #x22344d } +let pt_field = if ui_light { #xffffff } else { #x1e2429 } +let pt_field_line = if ui_light { #xdcdcde } else { #x3c4043 } +let pt_hover = if ui_light { #xf1f2f3 } else { #x2b3237 } +let pt_ink = if ui_light { #x37393c } else { #xd5d7d8 } +let pt_ink2 = if ui_light { #x55575b } else { #xb0b2b4 } +let pt_muted = if ui_light { #x76787c } else { #x9a9da0 } +let pt_faint = if ui_light { #x9c9da0 } else { #x7d8184 } +let pt_icon = if ui_light { #x5b5d61 } else { #xa9acae } +let pt_green = if ui_light { #x49b039 } else { #x38a540 } +let pt_green_tint = if ui_light { #xeffbf4 } else { #x1f3326 } +let pt_orange = if ui_light { #xf2a20d } else { #xe39a1c } +let pt_red = if ui_light { #xe5534f } else { #xf06a66 } +let pt_red_tint = if ui_light { #xfef3f2 } else { #x3a2426 } +let pt_shadow = if ui_light { #x00000022 } else { #x00000070 } +let pt_scrim = #x00000059 +let pt_hit = #xffd84d6b +let pt_hit_on = #xffa62a8c +let pt_field_box = if ui_light { #x2d7ff914 } else { #x2d7ff922 } + +// ---- templates ------------------------------------------------------------ + +let Text = Label{padding: 0 text: "" draw_text +: {color: pt_ink text_style: theme.font_regular{font_size: 12}}} +let TextB = Label{padding: 0 text: "" draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 12}}} +let Small = Label{padding: 0 text: "" draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11}}} +let Wrap = Label{padding: 0 width: Fill padding: 0 flow: Flow.Right{wrap: true} text: "" draw_text +: {color: pt_ink text_style: theme.font_regular{font_size: 12 line_spacing: 1.45}}} +let Code = Label{padding: 0 width: Fill padding: 0 flow: Flow.Right{wrap: true} text: "" draw_text +: {color: pt_muted text_style: theme.font_code{font_size: 10}}} +// A plain View paints no background in the contained host: lines are SolidViews. +let Line = SolidView{width: Fill height: 1 draw_bg.color: pt_line} +let VLine = SolidView{width: 1 height: Fill draw_bg.color: pt_line} +let Gap = View{width: Fill height: 1} +let Ico = Svg{width: 18 height: 18 animating: false draw_svg +: {color: pt_icon}} +// Buttons: flat text and icon buttons, the primary blue one, the outlined one. +let Flat = ButtonFlat{height: 32 margin: 0 padding: Inset{left: 10 right: 10} spacing: 8 + icon_walk: Walk{width: 18 height: 18} + draw_bg +: {border_radius: 6.0 border_size: 0.0 color: #x00000000 color_hover: pt_hover color_down: pt_line_soft color_focus: #x00000000} + draw_text +: {color: pt_ink2 color_hover: pt_ink color_down: pt_ink color_focus: pt_ink2 text_style: theme.font_regular{font_size: 12}} + draw_icon +: {color: pt_icon} +} +let IconBtn = Flat{width: 34 height: 34 padding: 0 spacing: 0 text: ""} +let Primary = Flat{height: 34 padding: Inset{left: 16 right: 16} + draw_bg +: {border_radius: 5.0 color: pt_accent color_hover: pt_accent_hover color_down: pt_accent_down color_focus: pt_accent} + draw_text +: {color: #xffffff color_hover: #xffffff color_down: #xffffff color_focus: #xffffff} + draw_icon +: {color: #xffffff} +} +let Outline = Flat{height: 34 padding: Inset{left: 14 right: 14} + draw_bg +: {border_radius: 5.0 border_size: 1.0 border_color: pt_line color: pt_card color_hover: pt_hover color_down: pt_line_soft color_focus: pt_card} + draw_text +: {color: pt_ink color_hover: pt_ink color_down: pt_ink color_focus: pt_ink} +} +let LinkBtn = Flat{padding: Inset{left: 6 right: 6} draw_text +: {color: pt_link color_hover: pt_link color_down: pt_link color_focus: pt_link}} +let DangerBtn = Primary{draw_bg +: {color: #xd23a4f color_hover: #xdc4b5f color_down: #xb52d40 color_focus: #xd23a4f}} +let Field = TextInput{width: Fill height: 36 margin: 0 padding: Inset{left: 10 right: 10 top: 8 bottom: 8} + draw_bg +: {color: pt_field color_hover: pt_field color_focus: pt_field color_empty: pt_field + border_color: pt_field_line border_color_hover: pt_accent border_color_focus: pt_accent border_color_empty: pt_field_line border_radius: 5.0} + draw_text +: {color: pt_ink color_hover: pt_ink color_focus: pt_ink color_empty: pt_faint color_empty_hover: pt_faint color_empty_focus: pt_faint text_style: theme.font_regular{font_size: 12}} + draw_selection.color: #x557dcc55 } -fn title_of(name){ - let d = doc_named(name) - if d == nil { return stem(name) } - d.title +let Card = RoundedView{width: Fill height: Fit flow: Down show_bg: true + draw_bg +: {color: pt_card border_size: 1.0 border_color: pt_line border_radius: 6.0}} +let Dot = RoundedView{width: 10 height: 10 show_bg: true draw_bg +: {color: pt_green border_radius: 5.0}} + +// ---- state ---------------------------------------------------------------- +// Everything PDF Tools remembers is one object kept on `mod`. A switch +// between light and dark runs this file again to restyle it: the runtime +// keeps the `mod` entries this file made, but every `let` starts over (#440). +let st = try { mod.pdftools } catch { nil } +if st == nil { + st = { + booted: false + screen: "home" // home | doc + docs: [] // the library, most recently opened first + index: {} // file name -> what library.json keeps + tabs: [] // the open documents (new_tab) + cur: -1 // the tab on show; -1 on Home + engine: "" // "" until the engine answers; ok | missing | old + engine_note: "" // the host's words when it isn't there + files_known: false // files.status answered + files_ok: false // this device copies a chosen file in + max_import: 0 + importing: false + alert: nil // {head detail note}: why something failed, on Home + space_full: false // the last write found no room + width: 1536 // the app's size: on_app_resize (card-host sends none) + height: 1024 + busy: false // a background render is out + covers: {} // file name -> image of its first page + cover_tried: {} + blank: false // the canvas's first half of a jump (go_page) + jump_n: 0 + confirm: nil // the tab whose unsaved changes the closing question is about + note_n: 0 + resize_n: 0 + icons: {} // icon name -> its handle, made once + who: {author: "" initials: ""} // who comments and signs (library.json) + naming: false // the Comment as field is open + name_draft: "" + removing: "" // the PDF whose removal Home asks about + busy_at: 0 // when the background render went out + heartbeat: nil // the interval below, made once + index_dirty: false // library.json waits for the heartbeat + } + mod.pdftools = st } -fn scan_library(){ - let out = [] - if fs.exists(LIBRARY) { - for name in fs.list(LIBRARY) { if is_pdf(name) { out.push(doc_of(name)) } } +// The heartbeat (below), started again by every run of this file. +if st.heartbeat != nil { stop_timer(st.heartbeat) } +st.heartbeat = start_interval(2, || heartbeat()) + +// ---- small helpers that need the state ----------------------------------- + +fn icon(name){ + let h = optional(st.icons, name, nil) + if h == nil { + h = http_resource(assets + "/icons/" + name + ".svg") + st.icons[name] = h } - docs = out + h +} +fn cur_tab(){ + if st.screen != "doc" || st.cur < 0 || st.cur >= st.tabs.len() { return nil } + st.tabs[st.cur] +} +fn tab_named(name){ + for t in st.tabs { if t.name == name { return t } } + nil +} +fn tab_index(t){ + for i x in st.tabs { if x.name == t.name { return i } } + -1 } fn doc_named(name){ - for d in docs { if d.name == name { return d } } + for d in st.docs { if d.name == name { return d } } nil } -// Forget what the engine said about a file that was just written again. +fn shown_name(title){ title + ".pdf" } +fn note(text){ + st.note_n = st.note_n + 1 + let n = st.note_n + ui.banner_text.set_text(text) + ui.banner.set_visible(text != "") + if text != "" { start_timeout(8, || { if n == st.note_n { ui.banner.set_visible(false) } }) } +} + +// ---- the engine ------------------------------------------------------------- + +// One call to the pdf engine. `done` gets {is_ok data error}, always on a +// later turn: a refusal answers inside host.request itself, where only the +// first widget call of the callback lands. +fn engine(method, args, done){ + host.request("pdf." + method, args, fn(r){ + let answer = {is_ok: r.is_ok data: r.data error: r.error} + start_timeout(0.01, || done(answer)) + }) +} +fn missing(error){ + let e = "" + error + e.search("was not granted") >= 0 || e.search("no service answers") >= 0 +} +// The engine is there but older than this contract: the shells refuse the +// v2 methods until the service lands. +fn outdated(error){ ("" + error).search("is not a method") >= 0 } +// True when `a` failed because the engine is not there for this app. +fn engine_gone(a){ + if a.is_ok { st.engine = "ok" return false } + if missing(a.error) { + st.engine = "missing" + st.engine_note = "" + a.error + return true + } + false +} +fn outdated_open(a){ + if a.is_ok || !outdated(a.error) { return false } + st.engine = "old" + st.engine_note = "" + a.error + true +} +// An error without the "pdf.open: " the service may put first; then its +// stable code (SERVICE.md: not_found, damaged, protected, storage_full, …) +// and the words for a person after it. +fn err_text(e){ ("" + e).replace(regex("^pdf\\.[a-z_]+: "), "") } +fn err_code(e){ + let m = regex("^([a-z_]+): ").exec(err_text(e)) + if m == nil { return "" } + m.captures[1] +} +fn err_words(e){ + let s = err_text(e) + if err_code(e) == "" { return s } + s.replace(regex("^[a-z_]+: "), "") +} +fn storage_full(e){ + let s = "" + e + err_code(e) == "storage_full" || s.search("bytes left in this storage") >= 0 || s.search("app storage is full") >= 0 || s.search("too many files") >= 0 +} +// Every engine write: a refusal for room shows until a write succeeds or a +// PDF is removed. +fn wrote(a){ + if a.is_ok { st.space_full = false return } + if storage_full(a.error) { st.space_full = true } +} +fn engine_headline(){ + if st.engine == "old" { return "This OctoSense's PDF engine is older than PDF Tools" } + if st.engine_note.search("was not granted") >= 0 { return "PDF Tools can't use the PDF engine" } + "This device has no PDF engine" +} +fn engine_detail(){ + if st.engine == "old" { return "Reading, search, comments, forms and page changes need the engine's newer methods. Your PDFs stay safe in PDF Tools' storage." } + if st.engine_note.search("was not granted") >= 0 { return "This OctoSense doesn't let PDF Tools use it. Your PDFs stay safe in PDF Tools' storage." } + "Pages come from the engine, and PDF Tools needs the OctoSense desktop, which has it. Your PDFs stay safe in this app's storage." +} +// What went wrong with a call a person made, in words, for the banner. +fn action_failed(what, e){ + if missing(e) { return engine_headline() + "." } + if outdated(e) { return "This OctoSense's PDF engine can't " + what + " yet." } + if storage_full(e) { return "There isn't room in PDF Tools' storage. Remove a PDF to make room." } + let code = err_code(e) + if code == "unknown_doc" { return "This PDF was closed by the engine. Close its tab and open it again." } + "Couldn't " + what + ": " + err_words(e) +} + +// ---- the library ---------------------------------------------------------- + +fn load_index(){ + if !fs.exists(INDEX) { return } + let v = try { fs.read(INDEX).parse_json() } catch { nil } + if v != nil && optional(v, "version", 0) == 1 { + st.index = optional(v, "docs", {}) + let who = optional(v, "who", nil) + if who != nil { st.who = {author: optional(who, "author", "") initials: optional(who, "initials", "")} } + } +} +fn save_index(){ + st.index_dirty = false + try { fs.write(INDEX, {version: 1 docs: st.index who: st.who}.to_json()) } catch {} +} +fn entry_of(d){ + let e = index_entry(d.pages, d.bytes, d.title, d.name) + if d.opened > 0 { e.opened = d.opened } + if d.page > 1 { e.page = d.page } + if d.zoom != nil { e.zoom = d.zoom } + if d.cover != "" { e.cover = d.cover } + e +} +fn remember(d){ + st.index[d.name] = entry_of(d) + // The heartbeat writes it (at most every 2 s), so a tap or a page turn + // never waits on the disk: one handler has 64 ms of wall-clock time. + st.index_dirty = true +} fn forget(name){ let kept = {} - for k v in index { if k != name { kept[k] = v } } - index = kept + for k v in st.index { if k != name { kept[k] = v } } + st.index = kept save_index() - let cover = COVERS + "/" + name + ".png" - if fs.exists(cover) { fs.remove(cover) } - remove_tree(PAGES + "/" + name) - cover_src[name] = nil - cover_failed[name] = nil + let covers = {} + for k v in st.covers { if k != name { covers[k] = v } } + st.covers = covers + let tried = {} + for k v in st.cover_tried { if k != name { tried[k] = v } } + st.cover_tried = tried } - -// Read, one file at a time, each PDF's page count and its first page. -fn library_next(){ - if lib_busy || engine_state == "missing" { return } - for d in docs { - if d.pages == nil && d.error == "" { read_info(d) return } +fn scan_library(){ + let found = [] + if fs.exists(LIBRARY) { + for name in fs.list(LIBRARY) { + if is_pdf(name) { + let d = doc_from(name, optional(st.index, name, nil)) + let old = doc_named(name) + if old != nil { + d.error = old.error + d.locked = old.locked + } + found.push(d) + } + } } - for d in docs { - if d.pages != nil && optional(cover_src, d.name, nil) == nil && optional(cover_failed, d.name, nil) == nil { read_cover(d) return } + // Most recently opened first; the runtime has no sort. + let out = [] + for d in found { + let at = out.len() + for i x in out { if at == out.len() && d.opened > x.opened { at = i } } + let next = [] + for i x in out { + if i == at { next.push(d) } + next.push(x) + } + if at == out.len() { next.push(d) } + out = next } + st.docs = out } -fn read_info(d){ - lib_busy = true - engine("info", {path: d.path}, fn(a){ - lib_busy = false - if engine_gone(a) { refresh() return } - if a.is_ok { - d.pages = a.data.pages.len() - d.bytes = optional(a.data, "file_size", nil) - remember(d) - } else { d.error = engine_error(a.error) } - refresh() - library_next() - }) +fn library_bytes(){ + let total = 0 + for d in st.docs { if d.bytes != nil { total = total + d.bytes } } + total } -fn read_cover(d){ - let file = COVERS + "/" + d.name + ".png" - if fs.exists(file) { - cover_src[d.name] = binary_resource(fs.read_bytes(file)) - refresh() - start_timeout(0.01, || library_next()) +fn unique_name(base){ + if !fs.exists(LIBRARY + "/" + base + ".pdf") { return base } + let n = 2 + loop { + if !fs.exists(LIBRARY + "/" + base + " " + n + ".pdf") { return base + " " + n } + n = n + 1 + } +} +// Removing a PDF from PDF Tools asks once more, on its card. One with +// unsaved changes in a tab stays until that tab is closed. +fn ask_remove(d){ + st.removing = d.name + render_home() +} +fn remove_doc(d){ + st.removing = "" + let t = tab_named(d.name) + if t != nil && t.edited { + note("Close “" + shown_name(d.title) + "” first: it has changes that aren't saved.") + render_home() return } - lib_busy = true - engine("render", {path: d.path page: 1 out: file max_side: THUMB_SIDE}, fn(a){ - lib_busy = false - if engine_gone(a) { refresh() return } - wrote(a) - if a.is_ok { cover_src[d.name] = binary_resource(fs.read_bytes(a.data.out)) } else { cover_failed[d.name] = true } - refresh() - library_next() - }) + let finish = || { + try { fs.remove(d.path) } catch {} + if fs.exists(d.path) { + note("Couldn't remove “" + shown_name(d.title) + "”.") + render_home() + return + } + forget(d.name) + st.space_full = false + scan_library() + render_home() + render_status() + note("Removed “" + shown_name(d.title) + "” from PDF Tools.") + } + if t != nil && t.doc != nil { + engine("close", {doc: t.doc discard: true}, fn(a){ + let kept = [] + for x in st.tabs { if x.name != t.name { kept.push(x) } } + st.tabs = kept + t.state = "closed" + render_tabs() + finish() + }) + return + } + finish() +} +fn doc_failed(d, e){ + if err_code(e) == "protected" { d.locked = true return } + d.error = err_words(e) } fn doc_meta(d){ - if d.error != "" { return "Can't open this file" } - if d.pages == nil { return pick(engine_state == "missing", "PDF", "Reading…") } + if d.locked { return "Protected with a password" } + if d.error != "" { return "Couldn't open this PDF" } + if d.pages == nil { return pick(st.engine == "missing" || st.engine == "old", "PDF", "Reading…") } let line = plural(d.pages, "page", "pages") if d.bytes != nil { line = line + " · " + size_text(d.bytes) } line } -fn library_caption(){ - if docs.len() == 0 { return "Your PDFs live in this app's storage." } - let total = 0 - let known = true - for d in docs { if d.bytes == nil { known = false } else { total = total + d.bytes } } - let line = plural(docs.len(), "PDF", "PDFs") - if known { line = line + " · " + size_text(total) } - line +// Today and yesterday, once per Home render: local_time is not free, and a +// render has 64 ms for every card. +fn days_now(){ + let now_t = time_now() + {today: local_time(now_t) yesterday: local_time(now_t - 86400)} +} +fn same_day(a, b){ a.year == b.year && a.month == b.month && a.day == b.day } +fn opened_text(d, days){ + if d.opened == nil || d.opened <= 0 { return "Not opened yet" } + let a = local_time(d.opened) + if same_day(a, days.today) { return "Opened today" } + if same_day(a, days.yesterday) { return "Opened yesterday" } + if a.year == days.today.year { return "Opened " + a.day + " " + MONTHS[a.month - 1] } + "Opened " + a.day + " " + MONTHS[a.month - 1] + " " + a.year +} + +// Each PDF's pages, size and first page, one engine call at a time. +fn cover_next(){ + if st.busy || st.engine == "missing" || st.engine == "old" { return false } + for d in st.docs { + if optional(st.covers, d.name, nil) == nil && optional(st.cover_tried, d.name, nil) == nil && d.error == "" && !d.locked { + read_cover(d) + return true + } + } + false +} +fn read_cover(d){ + st.cover_tried[d.name] = true + if d.cover != "" && d.pages != nil && fs.exists(d.cover) { + let src = try { binary_resource(fs.read_bytes(d.cover)) } catch { nil } + if src != nil { + st.covers[d.name] = src + render_home() + start_timeout(0.02, || pump()) + return + } + } + let t = tab_named(d.name) + if t != nil && t.doc != nil { + cover_from(d, t.doc, false) + return + } + st.busy = true + st.busy_at = time_now() + engine("open", {path: d.path}, fn(a){ + if engine_gone(a) || outdated_open(a) { + st.busy = false + render_home() + return + } + if !a.is_ok { + st.busy = false + if err_code(a.error) != "too_many_open" { doc_failed(d, a.error) } else { st.cover_tried[d.name] = nil } + render_home() + start_timeout(0.02, || pump()) + return + } + d.pages = optional(a.data, "pages", d.pages) + cover_from(d, a.data.doc, true) + }) +} +fn cover_from(d, doc, close_after){ + st.busy = true + st.busy_at = time_now() + engine("info", {doc: doc}, fn(i){ + if i.is_ok { + d.bytes = optional(i.data, "file_size", d.bytes) + if d.pages == nil { d.pages = optional(i.data, "pages", []).len() } + } + engine("page", {doc: doc page: 1 dpi: COVER_DPI}, fn(a){ + wrote(a) + if a.is_ok { + let path = optional(a.data, "path", "") + let src = try { binary_resource(fs.read_bytes(path)) } catch { nil } + if src != nil { + st.covers[d.name] = src + d.cover = path + } + } + remember(d) + if close_after { + engine("close", {doc: doc discard: true}, fn(c){ cover_done() }) + } else { cover_done() } + }) + }) +} +fn cover_done(){ + st.busy = false + render_home() + render_status() + start_timeout(0.02, || pump()) } // ---- opening a PDF from this device --------------------------------------- @@ -411,22 +757,22 @@ fn check_files(){ host.request("files.status", {}, fn(r){ let answer = {is_ok: r.is_ok data: r.data} start_timeout(0.01, || { - files_known = true + st.files_known = true if answer.is_ok { - files_ok = optional(answer.data, "import_supported", false) && optional(answer.data, "storage_granted", false) - max_import = optional(answer.data, "max_import_bytes", 0) + st.files_ok = optional(answer.data, "import_supported", false) && optional(answer.data, "storage_granted", false) + st.max_import = optional(answer.data, "max_import_bytes", 0) } - refresh() + render_home() }) }) } -fn import_limit(){ pick(max_import > 0, "PDFs up to " + size_text(max_import), "") } +fn import_limit(){ pick(st.max_import > 0, "Up to " + size_text(st.max_import), "") } // The host's refusal, said for a person; its own words go below it. fn import_refusal(error){ let e = "" + error if e.search("foreground_required") >= 0 { return "Bring PDF Tools to the front, then choose the file again." } if e.search("busy") >= 0 { return "Another file is still being added. Try again in a moment." } - if storage_full(e) { return "PDF Tools' storage is full. Remove a PDF you no longer need, then try again." } + if storage_full(e) { return "PDF Tools' storage is full. Remove a PDF to make room, then try again." } if e.search("too large") >= 0 || e.search("exceeds the byte limit") >= 0 { return "That file is too big to add. " + import_limit() + " can be added." } if e.search("permission_denied") >= 0 { return "OctoSense didn't let PDF Tools open files from this device." } if e.search("unsupported_platform") >= 0 { return "This device can't add files to PDF Tools." } @@ -434,1133 +780,2656 @@ fn import_refusal(error){ "OctoSense couldn't copy the file in." } // The person chooses a file in the host's own dialog; it is copied into a -// new library file the app named first, and opens. +// new library file the app named first, titled with the chosen file's name, +// and opens in a tab. fn open_from_device(){ - if importing { return } - if !files_ok { - banner("This OctoSense can't open files from the device for PDF Tools.") + if st.importing { return } + if !st.files_ok { + note("This OctoSense can't open files from the device for PDF Tools.") return } let name = unique_name(IMPORT_NAME) + ".pdf" - importing = true - lib_note = nil - refresh() + st.importing = true + st.alert = nil + render_home() host.request("files.import", {path: LIBRARY + "/" + name}, fn(r){ let answer = {is_ok: r.is_ok data: r.data error: r.error} start_timeout(0.01, || imported(name, answer)) }) } fn imported(name, a){ - importing = false + st.importing = false if !a.is_ok { - lib_note = {head: "Couldn't open a PDF from this device" detail: import_refusal(a.error) note: "" + a.error} - refresh() + st.alert = {head: "Couldn't open a PDF from this device" detail: import_refusal(a.error) note: "" + a.error} + if storage_full(a.error) { st.space_full = true } + show() return } - if optional(a.data, "cancelled", false) { refresh() return } - // From here the file is one of the library's: the engine reads it on - // its relative path, as for any PDF the library finds. It keeps the - // name the app gave it, and shows the chosen file's. + if optional(a.data, "cancelled", false) { render_home() return } forget(name) let taken = [] - for d in docs { taken.push(d.title) } + for d in st.docs { taken.push(d.title) } let title = title_for_import(optional(a.data, "name", nil), name, taken) - if title != stem(name) { - index[name] = {title: title} - save_index() - } + st.index[name] = {title: title bytes: optional(a.data, "bytes", nil)} + save_index() scan_library() let d = doc_named(name) - if d == nil { refresh() return } - if route == "library" { open_doc(d) } else { refresh() } + if d == nil { render_home() return } + open_doc(d) } -// ---- a document ---------------------------------------------------------- -fn open_doc(d){ - gen = gen + 1 - current = d - info = nil - doc_error = "" - thumb_src = {} - thumb_failed = {} - thumbs_busy = false - shown_pages = PAGE_BATCH - text_pages = nil - text_state = "" - text_note = "" - query = "" - shown_text = TEXT_BATCH - confirm_remove = false - ui.find.set_text("") - route = "doc" - tab = "pages" - // Keep only this document's page images (storage holds 256 entries). - if fs.exists(PAGES) { - for entry in fs.list(PAGES) { - if entry.replace("/", "") != d.name { remove_tree(PAGES + "/" + entry.replace("/", "")) } - } - } - show(true) - let g = gen - engine("info", {path: d.path}, fn(a){ - if g != gen { return } - if engine_gone(a) { refresh() return } - if !a.is_ok { doc_error = engine_error(a.error) refresh() return } - info = a.data - d.pages = info.pages.len() - d.bytes = optional(info, "file_size", d.bytes) - remember(d) - refresh() - thumbs_next() - }) +// ---- documents -------------------------------------------------------------- + +fn new_tab(d){ + {name: d.name path: d.path title: d.title doc: nil state: "opening" error: "" + pages: 0 sizes: [] outline: [] can_edit: true + edited: false can_undo: false can_redo: false saving: false + mode: "read" panel: "pages" panel_kept: "pages" zoom: 1.0 fit: "" zoom_auto: true + anchor: 1 page: 1 + renders: {} failed: {} thumbs: {} thumb_failed: {} grid: {} grid_failed: {} tile: 1.0 + find: nil finding: false + comments: nil comments_state: "" thread: nil tool: "highlight" color: "yellow" reply: "" status_menu: false head_menu: false pending: nil + fields: nil fields_state: "" field: nil field_text: "" field_on: false show_fields: true place: nil place_text: "" + selected: [] drag: nil inserting: false + lines: {} edit: nil + outline_open: {} + combine: nil} } -fn page_count(){ if info == nil { return 0 } info.pages.len() } -fn page_label(p){ - if info == nil { return "" + p } - let label = optional(info.pages[p - 1], "label", "") - if label == "" || label == "" + p { return "" + p } - label + " (" + p + ")" -} -fn thumb_of(p){ optional(thumb_src, "p" + p, nil) } - -// Render, one page at a time, the thumbnails of the pages on screen. -fn thumbs_next(){ - if thumbs_busy || current == nil || info == nil || engine_state == "missing" { return } - let want = pick(route == "split", page_count(), min(page_count(), shown_pages)) - for i in want { - let p = i + 1 - if thumb_of(p) == nil && optional(thumb_failed, "p" + p, nil) == nil { render_thumb(p) return } - } -} -fn render_thumb(p){ - let g = gen - let folder = PAGES + "/" + current.name - let file = folder + "/p" + p + ".png" - if fs.exists(file) { - thumb_src["p" + p] = binary_resource(fs.read_bytes(file)) - refresh_pages() - start_timeout(0.01, || thumbs_next()) +fn open_doc(d){ + let t = tab_named(d.name) + if t != nil { + show_tab(tab_index(t)) return } - thumbs_busy = true - engine("render", {path: current.path page: p out: file max_side: THUMB_SIDE}, fn(a){ - thumbs_busy = false - if g != gen { return } - if engine_gone(a) { refresh() return } - wrote(a) - if a.is_ok { thumb_src["p" + p] = binary_resource(fs.read_bytes(a.data.out)) } else { thumb_failed["p" + p] = true } - refresh_pages() - thumbs_next() + if st.tabs.len() >= MAX_OPEN { + note("PDF Tools keeps up to " + MAX_OPEN + " PDFs open. Close one to open another.") + return + } + t = new_tab(d) + st.tabs.push(t) + d.opened = time_now() + remember(d) + scan_library() + show_tab(st.tabs.len() - 1) + load_tab(t) +} +fn load_tab(t){ + t.state = "opening" + engine("open", {path: t.path}, fn(a){ + if tab_index(t) < 0 { + if a.is_ok { engine("close", {doc: a.data.doc discard: true}, fn(c){}) } + return + } + if engine_gone(a) || outdated_open(a) { + t.state = "missing" + refresh_doc() + return + } + if !a.is_ok { + let code = err_code(a.error) + if code == "protected" { t.state = "protected" } + else if code == "too_many_open" { t.state = "full" } + else { + t.state = "damaged" + t.error = err_words(a.error) + } + let d = doc_named(t.name) + if d != nil && code != "too_many_open" { doc_failed(d, a.error) } + refresh_doc() + return + } + let v = a.data + t.doc = v.doc + t.pages = optional(v, "pages", 0) + t.sizes = optional(v, "sizes", []) + t.outline = optional(v, "outline", []) + t.can_edit = optional(v, "can_edit", true) + t.state = "ready" + let d = doc_named(t.name) + t.page = 1 + t.zoom = start_zoom(t) + if d != nil { + d.pages = t.pages + d.error = "" + d.locked = false + t.page = clamp(d.page, 1, max(1, t.pages)) + if d.zoom != nil { + t.zoom = d.zoom + t.zoom_auto = false + } + remember(d) + } + refresh_doc() + go_page(t, t.page) }) } -fn refresh_pages(){ if route == "doc" || route == "split" || route == "page" { refresh() } } -fn more_pages(){ - shown_pages = shown_pages + PAGE_BATCH - refresh() - thumbs_next() +fn show_tab(i){ + st.screen = "doc" + st.cur = i + show() + pump() } -fn tile_h(){ floor(tile_w * 1.3) } -// The shell calls this when the app's window changes size (Photos does too). -fn on_app_resize(width, height){ - if width <= 0 { return } - let w = min(width, 880) - 32 - let n = max(2, min(5, floor((w + 12) / 170))) - tile_w = floor((w - (n - 1) * 12) / n) - if n != columns { - columns = n - // Called from the shell's draw pass: render on the next turn. - if route == "doc" || route == "split" { start_timeout(0.01, || refresh()) } - } -} - -fn set_tab(t){ - tab = t - if t == "text" && text_pages == nil && text_state == "" { load_text() } - show(true) -} -fn load_text(){ - let g = gen - text_state = "loading" - let args = {path: current.path} - if page_count() > MAX_TEXT { - let pages = [] - for i in MAX_TEXT { pages.push(i + 1) } - args = {path: current.path pages: pages} +fn go_home(){ + st.screen = "home" + scan_library() + show() + pump() +} +// Closing a tab with unsaved changes asks first (st.confirm). +fn ask_close(t){ + if t.edited { + st.confirm = t.name + render_dialog() + return } - engine("text", args, fn(a){ - if g != gen { return } - if engine_gone(a) { text_state = "" show(false) return } - if a.is_ok { - text_pages = a.data.pages - text_state = "" - } else { - text_state = "error" - text_note = engine_error(a.error) + close_tab(t, false) +} +fn close_tab(t, discard){ + st.confirm = nil + render_dialog() + let finish = || { + let i = tab_index(t) + if i < 0 { return } + let kept = [] + for x in st.tabs { if x.name != t.name { kept.push(x) } } + st.tabs = kept + t.state = "closed" + if st.tabs.len() == 0 { go_home() return } + if st.screen == "doc" { show_tab(min(i, st.tabs.len() - 1)) } else { render_tabs() } + } + if t.doc == nil { finish() return } + engine("close", {doc: t.doc discard: discard}, fn(a){ + if !a.is_ok && err_code(a.error) == "unsaved" { + st.confirm = t.name + render_dialog() + return } - show(false) + finish() }) } -fn text_hits(){ - if text_pages == nil { return [] } - if query == "" { return text_pages } - let re = regex(escape_re(query), "i") - let out = [] - for t in text_pages { if re.test(t.text) { out.push(t) } } - out -} -fn text_is_empty(){ - if text_pages == nil { return false } - for t in text_pages { if t.text.trim() != "" { return false } } - true -} -fn set_query(text){ - query = ("" + text).trim() - shown_text = TEXT_BATCH - refresh() +fn save_and_close(t){ + st.confirm = nil + render_dialog() + save(t, || close_tab(t, false)) } -fn paper_name(w, h){ - let a = min(w, h) - let b = max(w, h) - if abs(a - 595) < 3 && abs(b - 842) < 3 { return "A4" } - if abs(a - 612) < 3 && abs(b - 792) < 3 { return "Letter" } - if abs(a - 612) < 3 && abs(b - 1008) < 3 { return "Legal" } - if abs(a - 420) < 3 && abs(b - 595) < 3 { return "A5" } - "" -} -fn page_size_text(){ - let first = info.pages[0] - for p in info.pages { - if abs(p.width - first.width) > 1 || abs(p.height - first.height) > 1 { return "Mixed sizes" } - } - let mm = "" + round(first.width * 0.3528) + " × " + round(first.height * 0.3528) + " mm" - let name = paper_name(first.width, first.height) - if name == "" { return mm } - name + ", " + mm -} -fn info_rows(){ - // Commas between the pairs: `] [` would read as indexing. - let rows = [["File", current.name], ["Pages", "" + page_count()], ["Page size", page_size_text()]] - let bytes = optional(info, "file_size", nil) - if bytes != nil { rows.push(["File size", size_text(bytes)]) } - for pair in [["Title", "title"], ["Author", "author"], ["Subject", "subject"], ["Keywords", "keywords"], ["Made with", "creator"], ["Produced by", "producer"]] { - let value = optional(info, pair[1], "") - if value.is_string() && value.trim() != "" { rows.push([pair[0], value.trim()]) } - } - let version = optional(info, "pdf_version", "") - if version != "" { rows.push(["PDF version", "" + version]) } - let protected = optional(optional(info, "security", {}), "protected", false) - rows.push(["Security", pick(protected, "Protected", "Not protected")]) - let fonts = optional(info, "fonts", []) - if fonts.len() > 0 { - let names = "" - for f in first_n(fonts, 6) { names = names + pick(names == "", "", ", ") + f.name } - if fonts.len() > 6 { names = names + " and " + (fonts.len() - 6) + " more" } - rows.push(["Fonts", names]) - } - let outline = optional(info, "outline", []) - if outline.len() > 0 { rows.push(["Bookmarks", "" + outline.len()]) } - rows -} - -fn open_page(p){ - page_no = p - route = "page" - show(true) - load_view() -} -fn load_view(){ - view_seq = view_seq + 1 - let seq = view_seq - let p = page_no - view_src = nil - view_note = "" - // A new file each time: the shown picture is already decoded, and the - // engine then needs no leave to replace one. - try { if fs.exists(VIEW_FILE) { fs.remove(VIEW_FILE) } } catch {} - engine("render", {path: current.path page: p out: VIEW_FILE max_side: VIEW_SIDE}, fn(a){ - if seq != view_seq || route != "page" { return } - if engine_gone(a) { view_note = engine_headline() refresh() return } - wrote(a) - if a.is_ok { view_src = binary_resource(fs.read_bytes(a.data.out)) } else { view_note = "Couldn't show this page: " + engine_error(a.error) } - refresh() +// ---- the canvas: a run of pages from the page you went to ------------------ + +fn page_pt(t, p){ + let s = optional(t.sizes, p - 1, nil) + if s == nil { return [612, 792] } + [s[0], s[1]] +} +fn page_box(t, p){ + let s = page_pt(t, p) + [round(s[0] * t.zoom), round(s[1] * t.zoom)] +} +fn run_end(t){ min(t.pages, t.anchor + RUN - 1) } +// The dpi a page renders at for the zoom: screen resolution, within the +// service's 24 to 300 dpi and 16 megapixels. +fn render_dpi(t){ + let s = page_pt(t, t.page) + let cap = floor(72 * sqrt(16000000 / max(1, s[0] * s[1]))) + clamp(min(round(72 * t.zoom * DENSITY), cap), 24, 300) +} +fn page_src(t, p){ + let r = optional(t.renders, "p" + p, nil) + if r == nil { return nil } + r.src +} +fn needs_render(t, p){ + let r = optional(t.renders, "p" + p, nil) + let dpi = render_dpi(t) + if optional(t.failed, "p" + p, nil) == dpi { return false } + r == nil || r.dpi != dpi +} +// Show page `p`: the canvas starts its run there. The runtime gives script +// apps no scroll position, so the run is drawn empty first (the scroll clamps +// to the top on that frame), then filled. +fn go_page(t, p){ + if t.pages < 1 { return } + let n = clamp(p, 1, t.pages) + t.page = n + t.anchor = n + remember_place(t) + st.blank = true + render_canvas() + render_pill() + render_panel() + st.jump_n = st.jump_n + 1 + let g = st.jump_n + start_timeout(0.12, || { + if g != st.jump_n { return } + st.blank = false + render_canvas() + pump() }) } -fn step_page(d){ - let p = page_no + d - if p < 1 || p > page_count() { return } - page_no = p - refresh() - load_view() -} -fn back_to_pages(){ - view_seq = view_seq + 1 - route = "doc" - show(false) -} - -// Removing a PDF asks once more; what was made from it stays. -fn ask_remove(){ confirm_remove = true show(false) } -fn keep_doc(){ confirm_remove = false show(false) } -fn remove_doc(){ - let d = current - confirm_remove = false +fn remember_place(t){ + let d = doc_named(t.name) if d == nil { return } - try { fs.remove(d.path) } catch {} - if fs.exists(d.path) { - banner("Couldn't remove “" + d.title + "”.") - show(false) - return - } - forget(d.name) - // The room it held may let page pictures be saved again. - space_full = false - cover_failed = {} - go_library() - banner("Removed “" + d.title + "” from PDF Tools.") -} - -// ---- merge --------------------------------------------------------------- - -fn start_merge(){ - if docs.len() < 2 { return } - route = "merge" - merge_step = 1 - picked = [] - merge_busy = false - merge_error = "" - merge_result = nil - show(true) -} -fn toggle_pick(d){ - if d.error != "" { banner("“" + d.title + "” can't be opened, so it can't be merged.") return } - if has(picked, d.name) { picked.retain(|n| n != d.name) } - else if picked.len() < MAX_MERGE { picked.push(d.name) } - else { banner("PDF Tools merges up to " + MAX_MERGE + " PDFs at a time.") } - refresh() + d.page = t.page + // A zoom the person chose is kept; the automatic one is worked out again. + d.zoom = nil + if !t.zoom_auto { d.zoom = t.zoom } + remember(d) } -fn unique_name(base){ - if !fs.exists(LIBRARY + "/" + base + ".pdf") { return base } - let n = 2 - loop { - if !fs.exists(LIBRARY + "/" + base + " " + n + ".pdf") { return base + " " + n } - n = n + 1 +fn step_page(t, delta){ go_page(t, t.page + delta) } + +// ---- zoom ----------------------------------------------------------------- + +fn left_open(t){ t.panel != "" && t.mode != "pages" && t.mode != "combine" && !(narrow() && right_open(t)) } +fn right_open(t){ t.mode == "comment" || t.mode == "fill" || t.mode == "edit" } +fn panel_w(t){ pick(t.panel == "pages", PANEL_W, LIST_W) } +// In a narrow window (the desktop's default) both right panels take 320, +// so the page keeps its room. +fn right_w(t){ pick(narrow(), 320, pick(t.mode == "comment", THREAD_W, RIGHT_W)) } +// Below this width a right panel takes the left panel's place. +fn narrow(){ st.width < 1180 } +fn canvas_w(){ + let t = cur_tab() + let w = st.width - RAIL_W - 1 + if t != nil { + if left_open(t) { w = w - panel_w(t) - 1 } + if right_open(t) { w = w - right_w(t) - 1 } } -} -fn merge_next(){ - if picked.len() < 2 { return } - merge_step = 2 - merge_error = "" - merge_name = unique_name("Merged") - ui.merge_field.set_text(merge_name) - update_name_note() - show(true) -} -fn set_merge_name(text){ - merge_name = ("" + text).trim() - update_name_note() -} -fn merge_file(){ LIBRARY + "/" + stem(merge_name) + ".pdf" } -fn update_name_note(){ - let note = "" - if merge_name == "" { note = "Give the merged PDF a name." } - else if merge_name.search("/") >= 0 { note = "A name can't contain “/”." } - else if fs.exists(merge_file()) { note = "A PDF with this name is in your library; merging replaces it." } - ui.name_note.set_text(note) - ui.name_note.set_visible(note != "") -} -fn move_pick(i, d){ - let j = i + d - if j < 0 || j >= picked.len() { return } - let out = [] - for k v in picked { - if k == i { out.push(picked[j]) } else if k == j { out.push(picked[i]) } else { out.push(v) } + max(240, w) +} +fn canvas_h(){ max(240, st.height - TAB_H - MODE_H - STATUS_H - 2) } +fn fit_width_zoom(t){ + let s = page_pt(t, t.page) + (canvas_w() - 48) / s[0] +} +fn fit_page_zoom(t){ + let s = page_pt(t, t.page) + min((canvas_w() - 48) / s[0], (canvas_h() - PAGE_TOP - 80) / s[1]) +} +fn start_zoom(t){ clamp(floor(fit_width_zoom(t) * 100) / 100, 0.25, 1.0) } +fn set_zoom(t, z, fit){ + t.zoom = clamp(floor(z * 100 + 0.5) / 100, 0.25, 4.0) + t.fit = fit + t.zoom_auto = false + go_page(t, t.page) +} +// In Pages mode the pill's zoom is the tiles' size. +let TILES = [0.6, 0.8, 1.0, 1.2, 1.4, 1.6] +fn zoom_step(t, dir){ + if t.mode == "pages" { + let i = position(TILES, t.tile) + if i < 0 { i = 2 } + t.tile = TILES[clamp(i + dir, 0, TILES.len() - 1)] + render_grid() + render_pill() + return } - picked = out - refresh() -} -fn remove_pick(name){ - picked.retain(|n| n != name) - if picked.len() < 2 { merge_step = 1 show(true) return } - refresh() -} -fn merge_pages(){ - let total = 0 - for name in picked { - let d = doc_named(name) - if d == nil || d.pages == nil { return nil } - total = total + d.pages + let next = t.zoom + if dir > 0 { + let found = false + for v in ZOOMS { if !found && v > t.zoom + 0.001 { next = v found = true } } + } else { + for v in ZOOMS { if v < t.zoom - 0.001 { next = v } } } - total + set_zoom(t, next, "") } -fn do_merge(){ - if merge_busy || picked.len() < 2 { return } - if merge_name == "" || merge_name.search("/") >= 0 { update_name_note() return } - let out = merge_file() - let paths = [] - for name in picked { paths.push(LIBRARY + "/" + name) } - merge_busy = true - merge_error = "" - refresh() - engine("merge", {paths: paths out: out}, fn(a){ - merge_busy = false - if engine_gone(a) { merge_error = engine_headline() refresh() return } - wrote(a) - if !a.is_ok { merge_error = "Couldn't merge: " + engine_error(a.error) refresh() return } - let name = stem(merge_name) + ".pdf" - forget(name) - // The rescan and the next screen on a later turn: one handler has a - // 64 ms budget. Someone who left the flow stays where they are. - start_timeout(0.01, || { - scan_library() - merge_result = doc_named(name) - if route == "merge" { merge_step = 3 show(true) } else { refresh() } - start_timeout(0.01, || library_next()) - }) - }) +fn zoom_text(t){ "" + round(pick(t.mode == "pages", t.tile, t.zoom) * 100) + "%" } +// A new window size keeps a fitted zoom fitted. +fn refit(t){ + if t.fit == "width" { t.zoom = clamp(floor(fit_width_zoom(t) * 100) / 100, 0.25, 4.0) } + if t.fit == "page" { t.zoom = clamp(floor(fit_page_zoom(t) * 100) / 100, 0.25, 4.0) } } -// ---- split --------------------------------------------------------------- - -fn start_split(){ - if info == nil || page_count() < 2 { return } - route = "split" - split_mode = "every" - split_every = pick(page_count() > 2, 2, 1) - split_cuts = [] - split_busy = false - split_error = "" - split_result = nil - show(true) - thumbs_next() -} -fn set_split_mode(m){ - split_mode = m - show(true) - thumbs_next() -} -fn set_every(n){ - if n < 1 || n >= page_count() { return } - split_every = n - refresh() -} -fn toggle_cut(p){ - if p < 2 { banner("Page 1 always starts the first file. Tap another page to start a new file there.") return } - let out = [] - let found = false - for c in split_cuts { if c == p { found = true } else { out.push(c) } } - if !found { - out = [] - let placed = false - for c in split_cuts { - if !placed && p < c { out.push(p) placed = true } - out.push(c) - } - if !placed { out.push(p) } - } - split_cuts = out - refresh() -} -// The files a split makes: [first last] page ranges, as the engine cuts them. -fn split_ranges(){ - let total = page_count() - let out = [] - let start = 1 - if split_mode == "every" { - while start <= total { - let end = min(start + split_every - 1, total) - out.push([start, end]) - start = end + 1 +// ---- the render pump: one engine render at a time --------------------------- + +fn pump(){ + // A render that never answered (its callback lost to a script budget + // abort under load) frees the pump after 30 s. + if st.busy && time_now() - st.busy_at > 30 { st.busy = false } + if st.busy || st.engine == "missing" || st.engine == "old" { return } + let t = cur_tab() + if t != nil && t.state == "ready" && t.doc != nil && !st.blank { + if t.mode == "pages" { + if grid_next(t) { return } + } else if t.mode != "combine" { + if run_next(t, AHEAD + 1) { return } + if left_open(t) && t.panel == "pages" && thumb_next(t, 8) { return } + if run_next(t, RUN) { return } + if left_open(t) && t.panel == "pages" && thumb_next(t, THUMB_WINDOW) { return } } - return out } - for c in split_cuts { - if c > start && c <= total { out.push([start, c - 1]) start = c } + if st.screen == "home" { cover_next() } +} +fn run_next(t, n){ + for i in n { + let p = t.anchor + i + if p <= run_end(t) && needs_render(t, p) { + render_page(t, p) + return true + } + } + false +} +fn render_page(t, p){ + let dpi = render_dpi(t) + st.busy = true + st.busy_at = time_now() + engine("page", {doc: t.doc page: p dpi: dpi}, fn(a){ + st.busy = false + if engine_gone(a) { + t.state = "missing" + refresh_doc() + return + } + wrote(a) + if a.is_ok { + let src = try { binary_resource(fs.read_bytes(optional(a.data, "path", ""))) } catch { nil } + if src != nil { t.renders["p" + p] = {src: src dpi: dpi} } else { t.failed["p" + p] = dpi } + } else { t.failed["p" + p] = dpi } + trim_renders(t) + if cur_tab() == t && t.edit == nil && !st.blank { render_canvas() } + if st.space_full { render_status() } + start_timeout(0.02, || pump()) + }) +} +// Keep the run's renders and a page either side; the rest go. +fn trim_renders(t){ + let kept = {} + let n = 0 + for k v in t.renders { + let p = k.replace("p", "").to_f64() + if p >= t.anchor - 1 && p <= run_end(t) + 1 { + kept[k] = v + n = n + 1 + } + } + t.renders = kept +} +// Everything drawn of a document is stale after a change the engine made. +fn invalidate(t){ + t.renders = {} + t.failed = {} + t.thumbs = {} + t.thumb_failed = {} + t.grid = {} + t.grid_failed = {} + t.lines = {} + t.find = nil + start_timeout(0.05, || pump()) +} +// Only page `p` changed (a comment, a filled field, an edited paragraph). +fn invalidate_page(t, p){ + let keep = |m| { + let out = {} + for k v in m { if k != "p" + p { out[k] = v } } + out + } + t.renders = keep(t.renders) + t.failed = keep(t.failed) + t.thumbs = keep(t.thumbs) + t.thumb_failed = keep(t.thumb_failed) + t.grid = keep(t.grid) + t.grid_failed = keep(t.grid_failed) + t.lines = keep(t.lines) + start_timeout(0.05, || pump()) +} + +// ---- thumbnails: the Pages panel and the Pages grid --------------------------- + +fn window_start(t){ max(1, min(t.page - 8, t.pages - THUMB_WINDOW + 1)) } +fn window_end(t){ min(t.pages, window_start(t) + THUMB_WINDOW - 1) } +fn thumb_next(t, n){ + let first = max(window_start(t), t.page - 2) + for i in n { + let p = first + i + if p <= window_end(t) && optional(t.thumbs, "p" + p, nil) == nil && optional(t.thumb_failed, "p" + p, nil) == nil { + render_thumb(t, p, false) + return true + } + } + for i in window_end(t) - window_start(t) + 1 { + let p = window_start(t) + i + if i < n && optional(t.thumbs, "p" + p, nil) == nil && optional(t.thumb_failed, "p" + p, nil) == nil { + render_thumb(t, p, false) + return true + } + } + false +} +fn grid_next(t){ + for i in window_end(t) - window_start(t) + 1 { + let p = window_start(t) + i + if optional(t.grid, "p" + p, nil) == nil && optional(t.grid_failed, "p" + p, nil) == nil { + render_thumb(t, p, true) + return true + } + } + false +} +fn render_thumb(t, p, big){ + st.busy = true + st.busy_at = time_now() + engine("page", {doc: t.doc page: p dpi: pick(big, GRID_DPI, THUMB_DPI)}, fn(a){ + st.busy = false + if engine_gone(a) { + t.state = "missing" + refresh_doc() + return + } + wrote(a) + let src = nil + if a.is_ok { src = try { binary_resource(fs.read_bytes(optional(a.data, "path", ""))) } catch { nil } } + if big { + if src != nil { t.grid["p" + p] = src } else { t.grid_failed["p" + p] = true } + if cur_tab() == t && t.mode == "pages" { render_grid() } + } else { + if src != nil { t.thumbs["p" + p] = src } else { t.thumb_failed["p" + p] = true } + if cur_tab() == t { ui.thumbs.render() } + } + start_timeout(0.02, || pump()) + }) +} + +// ---- find ----------------------------------------------------------------- + +fn find_text(text){ + let t = cur_tab() + let q = ("" + text).trim() + if t == nil || t.state != "ready" { return } + if q == "" { + end_find(t) + return + } + t.finding = true + t.find = {query: q total: 0 matches: [] cur: 0 error: "" by_page: {} pages: []} + t.panel = "search" + let refit = auto_fit(t) + show() + if refit { go_page(t, t.page) } + engine("find", {doc: t.doc query: q limit: 200}, fn(a){ + t.finding = false + if !a.is_ok { + t.find.error = action_failed("search this PDF", a.error) + show() + return + } + t.find.total = optional(a.data, "total", 0) + t.find.matches = optional(a.data, "matches", []) + // Matches by page, once: every page drawn looks up its own. + let by_page = {} + let pages = [] + for i m in t.find.matches { + let key = "p" + m.page + if optional(by_page, key, nil) == nil { + by_page[key] = [] + pages.push(m.page) + } + by_page[key].push(i) + } + t.find.by_page = by_page + t.find.pages = pages + show() + if t.find.matches.len() > 0 { go_match(t, 0) } + }) +} +fn end_find(t){ + t.find = nil + t.finding = false + if t.panel == "search" { t.panel = "pages" } + let refit = auto_fit(t) + show() + if refit { go_page(t, t.page) } +} +fn go_match(t, i){ + let n = t.find.matches.len() + if n == 0 { return } + t.find.cur = pick(i < 0, n - 1, pick(i >= n, 0, i)) + let m = t.find.matches[t.find.cur] + render_toolrow() + ui.results.render() + if m.page != t.page || m.page != t.anchor { go_page(t, m.page) } else { render_canvas() } +} +fn find_pages(t){ t.find.pages } +fn page_matches(t, p){ optional(t.find.by_page, "p" + p, []) } +fn find_summary(t){ + if t.finding { return "Searching…" } + if t.find.error != "" { return t.find.error } + if t.find.total == 0 { return "No matches for “" + t.find.query + "”" } + plural(t.find.total, "match", "matches") + " on " + plural(find_pages(t).len(), "page", "pages") +} + +// ---- changes the engine keeps: state, undo, redo, save ------------------- + +// After a change: what the engine now says about edits and history. +fn changed(t){ + t.edited = true + t.can_undo = true + t.can_redo = false + engine("state", {doc: t.doc}, fn(a){ + if a.is_ok { + t.edited = optional(a.data, "edited", t.edited) + t.can_undo = optional(a.data, "can_undo", t.can_undo) + t.can_redo = optional(a.data, "can_redo", t.can_redo) + let pages = optional(a.data, "pages", t.pages) + if pages != t.pages { t.pages = pages } + } + render_actions() + render_status() + render_tabs() + }) + render_actions() + render_status() +} +fn undo(t, redo){ + if t == nil || t.doc == nil { return } + if redo && !t.can_redo { return } + if !redo && !t.can_undo { return } + engine(pick(redo, "redo", "undo"), {doc: t.doc}, fn(a){ + if !a.is_ok { + note(action_failed(pick(redo, "redo", "undo"), a.error)) + return + } + t.edited = optional(a.data, "edited", true) + t.can_undo = optional(a.data, "can_undo", false) + t.can_redo = optional(a.data, "can_redo", false) + // Which pages changed isn't said: draw the run again, and reread + // what the panels show. + invalidate(t) + t.comments = nil + t.fields = nil + t.edit = nil + reload_shape(t) + }) +} +// The document's pages and sizes again after a change that may move them. +fn reload_shape(t){ + engine("info", {doc: t.doc}, fn(a){ + if a.is_ok { + let pages = optional(a.data, "pages", []) + if pages.len() > 0 { + let sizes = [] + for pg in pages { sizes.push([optional(pg, "width", 612), optional(pg, "height", 792)]) } + t.sizes = sizes + t.pages = pages.len() + } + } + if t.mode == "comment" { load_comments(t) } + if t.mode == "fill" { load_fields(t) } + show() + go_page(t, min(t.page, max(1, t.pages))) + }) +} +fn save(t, then){ + if t == nil || t.doc == nil || t.saving { return } + t.saving = true + render_status() + engine("save", {doc: t.doc}, fn(a){ + t.saving = false + wrote(a) + if !a.is_ok { + note(action_failed("save", a.error)) + render_status() + return + } + t.edited = false + let d = doc_named(t.name) + if d != nil { + d.bytes = optional(a.data, "bytes", d.bytes) + remember(d) + } + render_status() + render_tabs() + render_actions() + if then != nil { then() } + }) +} + +// ---- taps on a page --------------------------------------------------------- + +// `x` and `y` are in the page's points (origin top left, y down, rotation +// applied), as the engine's geometry is. +fn page_tap(t, p, x, y){ + t.page = p + render_pill() + if t.mode == "comment" { comment_tap(t, p, x, y, false) return } + if t.mode == "fill" { fill_tap(t, p, x, y) return } + if t.mode == "edit" { edit_tap(t, p, x, y) return } + render_panel() +} +fn page_double_tap(t, p, x, y){ + if t.mode == "comment" { comment_tap(t, p, x, y, true) } +} +fn inside(r, x, y){ x >= r[0] && x <= r[0] + r[2] && y >= r[1] && y <= r[1] + r[3] } +// The text lines and paragraphs of page `p`, read once (pdf.lines). +fn with_lines(t, p, done){ + let known = optional(t.lines, "p" + p, nil) + if known != nil { + done(known) + return + } + engine("lines", {doc: t.doc page: p}, fn(a){ + if !a.is_ok { + note(action_failed("read this page's text", a.error)) + return + } + t.lines["p" + p] = a.data + done(a.data) + }) +} + +// ---- comments --------------------------------------------------------------- + +fn load_comments(t){ + t.comments_state = "loading" + render_right() + engine("comments", {doc: t.doc}, fn(a){ + if !a.is_ok { + t.comments_state = action_failed("read the comments", a.error) + render_right() + return + } + t.comments = optional(a.data, "comments", []) + t.comments_state = "" + if t.thread == nil && t.comments.len() > 0 { t.thread = t.comments[0].id } + render_right() + ui.notes.render() + render_canvas() + }) +} +fn comment_named(t, id){ + if t.comments == nil { return nil } + for c in t.comments { if c.id == id { return c } } + nil +} +// Highlight, underline or strike out the line under the pointer (a double +// tap takes its paragraph); a note or a text box waits for its words. +fn comment_tap(t, p, x, y, paragraph){ + if st.who.author == "" { + st.naming = true + render_right() + note("Type the name your comments show, under Comment as, first.") + return + } + if t.tool == "note" || t.tool == "textbox" { + t.pending = {page: p at: [x, y] type: t.tool} + t.reply = "" + render_right() + return + } + with_lines(t, p, |lines| { + let hit = nil + let list = pick(paragraph, optional(lines, "paragraphs", []), optional(lines, "lines", [])) + for l in list { if hit == nil && inside(l.box, x, y) { hit = l } } + if hit == nil { + note("Tap a line of text to " + pick(t.tool == "highlight", "highlight", pick(t.tool == "underline", "underline", "strike out")) + " it.") + return + } + let args = {doc: t.doc op: "add" page: p type: pick(t.tool == "strike", "strikeout", t.tool) rects: [hit.box] color: optional(MARK_HEX, t.color, "#ffd84d") author: st.who.author} + engine("comment", args, fn(a){ + wrote(a) + if !a.is_ok { + note(action_failed("add the comment", a.error)) + return + } + t.thread = optional(a.data, "id", nil) + invalidate_page(t, p) + changed(t) + load_comments(t) + render_canvas() + }) + }) +} +fn post_pending(t){ + let text = t.reply.trim() + if t.pending == nil || text == "" { return } + let p = t.pending + engine("comment", {doc: t.doc op: "add" page: p.page type: p.type at: p.at color: optional(MARK_HEX, t.color, "#ffd84d") text: text author: st.who.author}, fn(a){ + wrote(a) + if !a.is_ok { + note(action_failed("add the note", a.error)) + return + } + t.pending = nil + t.reply = "" + t.thread = optional(a.data, "id", nil) + invalidate_page(t, p.page) + changed(t) + load_comments(t) + render_canvas() + }) +} +fn post_reply(t){ + let text = t.reply.trim() + let c = comment_named(t, t.thread) + if c == nil || text == "" { return } + // A highlight with no words yet takes them as its own text. + let edit = optional(c, "text", "") == "" && optional(c, "replies", []).len() == 0 + let args = {doc: t.doc op: "reply" id: c.id text: text author: st.who.author} + if edit { args = {doc: t.doc op: "edit" id: c.id text: text} } + engine("comment", args, fn(a){ + wrote(a) + if !a.is_ok { + note(action_failed("post the reply", a.error)) + return + } + t.reply = "" + changed(t) + load_comments(t) + }) +} +fn set_status(t, status){ + let c = comment_named(t, t.thread) + if c == nil { return } + t.status_menu = false + engine("comment", {doc: t.doc op: "status" id: c.id status: status}, fn(a){ + if !a.is_ok { + note(action_failed("change the status", a.error)) + render_right() + return + } + changed(t) + load_comments(t) + }) +} +fn delete_comment(t){ + let c = comment_named(t, t.thread) + t.head_menu = false + if c == nil { render_right() return } + engine("comment", {doc: t.doc op: "delete" id: c.id}, fn(a){ + if !a.is_ok { + note(action_failed("delete the comment", a.error)) + render_right() + return + } + t.thread = nil + invalidate_page(t, c.page) + changed(t) + load_comments(t) + render_canvas() + }) +} +fn open_thread(t, c){ + t.thread = c.id + t.status_menu = false + t.pending = nil + if t.mode != "comment" { set_mode(t, "comment") } + if c.page != t.page { go_page(t, c.page) } + render_right() + render_canvas() +} +fn set_author(text){ + let name = ("" + text).trim() + if name == "" { return } + st.who.author = name + st.naming = false + save_index() + render_right() +} +fn status_label(s){ + if s == "accepted" { return "Accepted" } + if s == "rejected" { return "Rejected" } + if s == "cancelled" { return "Cancelled" } + if s == "completed" { return "Completed" } + "" +} + +// ---- Fill & Sign -------------------------------------------------------------- + +fn load_fields(t){ + t.fields_state = "loading" + render_right() + render_toolrow() + engine("fields", {doc: t.doc}, fn(a){ + if !a.is_ok { + t.fields_state = action_failed("read the form", a.error) + render_right() + render_toolrow() + return + } + t.fields = optional(a.data, "fields", []) + t.fields_state = "" + render_right() + render_toolrow() + render_canvas() + }) +} +fn field_named(t, name){ + if t.fields == nil { return nil } + for f in t.fields { if f.name == name { return f } } + nil +} +fn field_empty(f){ + let v = optional(f, "value", nil) + v == nil || v == false || ("" + v).trim() == "" || v == "Off" +} +fn field_shown(f){ + let v = optional(f, "value", nil) + if v == true { return "Checked" } + if v == false || v == nil { return "" } + "" + v +} +fn read_only(f){ optional(f, "read_only", false) == true } +fn empty_required(t){ + let n = 0 + if t.fields == nil { return 0 } + for f in t.fields { if optional(f, "required", false) && !read_only(f) && field_empty(f) { n = n + 1 } } + n +} +fn form_summary(t){ + if t.fields_state == "loading" { return "Reading the form…" } + if t.fields_state != "" { return t.fields_state } + if t.place != nil { return place_hint(t) } + if t.fields == nil || t.fields.len() == 0 { return "This PDF has no form fields. Add text, a date or your initials where they go." } + let line = "This form has " + plural(t.fields.len(), "field", "fields") + "." + let empty = empty_required(t) + if empty > 0 { line = line + " " + plural(empty, "required field is", "required fields are") + " empty." } + line +} +fn select_field(t, f){ + if read_only(f) { + note("“" + f.name + "” can't be changed: the form keeps it read-only.") + return + } + t.field = f.name + t.field_text = field_shown(f) + t.field_on = optional(f, "value", false) == true + t.place = nil + if f.page != t.page && (f.page < t.anchor || f.page > run_end(t)) { go_page(t, f.page) } + render_right() + render_canvas() + render_toolrow() +} +fn fill_field(t){ + let f = field_named(t, t.field) + if f == nil { return } + let values = {} + let kind = "" + optional(f, "type", "text") + if kind == "checkbox" || kind == "check" { values[f.name] = t.field_on } else { values[f.name] = t.field_text } + engine("fill", {doc: t.doc values: values}, fn(a){ + wrote(a) + if !a.is_ok { + note(action_failed("fill the field", a.error)) + return + } + t.field = nil + invalidate_page(t, f.page) + changed(t) + load_fields(t) + }) +} +fn fill_tap(t, p, x, y){ + if t.place != nil { + place_mark(t, p, x, y) + return + } + if t.fields == nil { return } + for f in t.fields { + if f.page == p && inside(f.rect, x, y) { + select_field(t, f) + return + } + } +} +fn start_place(t, kind){ + t.field = nil + t.place = {kind: kind} + if kind == "initials" { t.place_text = st.who.initials } else { t.place_text = "" } + render_right() + render_toolrow() +} +fn place_hint(t){ + if t.place.kind == "initials" { return "Tap the page where your initials go." } + if t.place.kind == "date" { return "Tap the page where the date goes." } + "Tap the page where the text goes." +} +fn place_mark(t, p, x, y){ + let text = t.place_text.trim() + if text == "" && t.place.kind != "date" { + note(pick(t.place.kind == "initials", "Type your initials first.", "Type the text first.")) + return + } + if t.place.kind == "initials" { + st.who.initials = text + save_index() + } + let args = {doc: t.doc page: p kind: t.place.kind at: [x, y] text: text} + if t.place.kind == "date" { args = {doc: t.doc page: p kind: "date" at: [x, y]} } + engine("fill_sign", args, fn(a){ + wrote(a) + if !a.is_ok { + note(action_failed("place it", a.error)) + return + } + t.place = nil + invalidate_page(t, p) + changed(t) + render_right() + render_toolrow() + render_canvas() + pump() + }) +} + +// ---- Pages: rotate, delete, move, insert, extract -------------------------- + +fn toggle_select(t, p){ + if has(t.selected, p) { t.selected.retain(|x| x != p) } else { t.selected.push(p) } + t.page = p + render_grid() + render_toolrow() + render_pill() +} +// The selection in page order, at most what one call may name. +fn chosen(t){ + let out = [] + for i in t.pages { if has(t.selected, i + 1) && out.len() < MAX_NAMED { out.push(i + 1) } } + out +} +fn page_op(t, args, what, done){ + args.doc = t.doc + engine("pages", args, fn(a){ + wrote(a) + if !a.is_ok { + note(action_failed(what, a.error)) + return + } + changed(t) + done(a.data) + }) +} +fn rotate(t, angle){ + let pages = chosen(t) + if pages.len() == 0 { note("Choose pages to rotate.") return } + page_op(t, {op: "rotate" pages: pages angle: angle}, "rotate the pages", |v| { + for p in pages { invalidate_page(t, p) } + reload_shape(t) + }) +} +fn delete_pages(t){ + let pages = chosen(t) + if pages.len() == 0 { note("Choose pages to delete.") return } + if pages.len() >= t.pages { note("A PDF keeps at least one page.") return } + page_op(t, {op: "delete" pages: pages}, "delete the pages", |v| { + invalidate(t) + t.selected = [] + reload_shape(t) + }) +} +fn extract_pages(t){ + let pages = chosen(t) + if pages.len() == 0 { note("Choose pages to extract.") return } + let name = unique_name(t.title + " pages") + ".pdf" + page_op(t, {op: "extract" pages: pages out: LIBRARY + "/" + name}, "extract the pages", |v| { + scan_library() + let d = doc_named(name) + if d != nil { + d.pages = pages.len() + remember(d) + } + note("Extracted " + plural(pages.len(), "page", "pages") + " into “" + stem(name) + "”. It's on Home.") + }) +} +fn insert_file(t, d){ + let after = t.pages + let pages = chosen(t) + if pages.len() > 0 { after = pages[pages.len() - 1] } + t.inserting = false + page_op(t, {op: "insert_file" path: d.path at: after + 1}, "insert the pages", |v| { + invalidate(t) + t.selected = [] + reload_shape(t) + note("Inserted “" + d.title + "” after page " + after + ".") + }) +} +// Drag a page tile to move it: the pan's offset, in tiles, decides where. +fn tile_w(t){ round(190 * t.tile) } +fn tile_h(t){ round(tile_w(t) * 1.32) } +fn grid_cols(t){ max(1, floor((canvas_w() - 40 + 30) / (tile_w(t) + 36))) } +fn drag_target(t, p, dx, dy){ + let cols = grid_cols(t) + let col = (p - window_start(t)) % cols + let row = floor((p - window_start(t)) / cols) + let c = clamp(round(col + dx / (tile_w(t) + 30)), 0, cols - 1) + let r = max(0, row + round(dy / (tile_h(t) + 46))) + clamp(window_start(t) + r * cols + c, 1, t.pages) +} +fn tile_pan(t, p, dx, dy, phase){ + if phase == 0 { + t.drag = {page: p to: p} + render_toolrow() + return + } + if t.drag == nil { return } + let to = drag_target(t, p, dx, dy) + if phase == 1 { + if to != t.drag.to { + t.drag.to = to + render_grid() + } + return + } + t.drag = nil + if to == p { + render_grid() + return + } + let pages = pick(has(t.selected, p), chosen(t), [p]) + page_op(t, {op: "move" pages: pages to: to}, "move the pages", |v| { + invalidate(t) + t.selected = [] + reload_shape(t) + }) +} + +// ---- Combine ------------------------------------------------------------------ + +fn start_combine(t){ + if t.combine != nil { return } + let d = doc_named(t.name) + t.combine = {items: [] name: unique_name(t.title + " combined") busy: false error: "" adding: false editing: -1} + if d != nil { t.combine.items.push({name: d.name title: d.title pages: t.pages range: ""}) } +} +fn combine_add(t, d){ + let c = t.combine + if c.items.len() >= MAX_MERGE { + note("PDF Tools combines up to " + MAX_MERGE + " PDFs at a time.") + return + } + c.items.push({name: d.name title: d.title pages: d.pages range: ""}) + c.adding = false + render_combine() +} +fn combine_remove(t, i){ + let c = t.combine + let kept = [] + for k it in c.items { if k != i { kept.push(it) } } + c.items = kept + render_combine() +} +fn combine_move(t, i, dy){ + let c = t.combine + let j = clamp(i + round(dy / 113), 0, c.items.len() - 1) + if j == i { return } + let moving = c.items[i] + let rest = [] + for k it in c.items { if k != i { rest.push(it) } } + let out = [] + for k it in rest { + if k == j { out.push(moving) } + out.push(it) + } + if j >= rest.len() { out.push(moving) } + c.items = out + render_combine() +} +fn item_pages(it){ + let total = it.pages + if total == nil { + let d = doc_named(it.name) + if d != nil { total = d.pages } + } + if total == nil { return nil } + range_pages(it.range, total) +} +fn item_count(it){ + let pages = item_pages(it) + if pages == nil { return nil } + pages.len() +} +fn combine_total(t){ + let total = 0 + for it in t.combine.items { + let n = item_count(it) + if n == nil { return nil } + total = total + n + } + total +} +fn combine_problem(t){ + let c = t.combine + if c.items.len() < 2 { return "Add another PDF to combine." } + for it in c.items { if item_count(it) == nil { return "Check the pages of “" + it.title + "”: use numbers and ranges such as 1-4, 9." } } + if c.name.trim() == "" { return "Give the new PDF a name." } + if c.name.search("/") >= 0 { return "A name can't contain “/”." } + if fs.exists(LIBRARY + "/" + c.name.trim() + ".pdf") { return "A PDF with this name is already in PDF Tools." } + "" +} +fn do_combine(t){ + let c = t.combine + if c.busy { return } + let problem = combine_problem(t) + if problem != "" { + c.error = problem + render_combine() + return + } + let paths = [] + for it in c.items { + let path = LIBRARY + "/" + it.name + if range_arg(it.range) == "" { paths.push(path) } else { paths.push({path: path pages: range_arg(it.range)}) } + } + let name = c.name.trim() + ".pdf" + c.busy = true + c.error = "" + render_combine() + engine("merge", {paths: paths out: LIBRARY + "/" + name}, fn(a){ + c.busy = false + wrote(a) + if !a.is_ok { + c.error = action_failed("combine them", a.error) + render_combine() + return + } + let total = combine_total(t) + st.index[name] = {pages: total bytes: optional(a.data, "bytes", nil)} + save_index() + scan_library() + let d = doc_named(name) + t.combine = nil + set_mode(t, "read") + note("Combined into “" + stem(name) + "”, " + plural(total, "page", "pages") + ".") + if d != nil { open_doc(d) } + }) +} + +// ---- Edit ------------------------------------------------------------------- + +fn edit_tap(t, p, x, y){ + if !t.can_edit { + note("This PDF doesn't allow editing.") + return + } + with_lines(t, p, |lines| { + let hit = nil + for g in optional(lines, "paragraphs", []) { if hit == nil && inside(g.box, x, y) { hit = g } } + if hit == nil { + note("Click a paragraph of text to edit it.") + return + } + t.edit = {page: p n: hit.n box: hit.box text: hit.text draft: hit.text font: optional(hit, "font", "") size: optional(hit, "size", 0)} + render_canvas() + render_right() + }) +} +fn apply_edit(t){ + let e = t.edit + if e == nil { return } + if e.draft == e.text { + t.edit = nil + render_canvas() + render_right() + return + } + engine("edit_text", {doc: t.doc page: e.page paragraph: e.n text: e.draft}, fn(a){ + wrote(a) + if !a.is_ok { + note(action_failed("change the text", a.error)) + return + } + t.edit = nil + invalidate_page(t, e.page) + changed(t) + render_right() + render_canvas() + pump() + }) +} +fn cancel_edit(t){ + t.edit = nil + render_canvas() + render_right() +} +fn font_text(e){ + let size = pick(e.size > 0, "" + round(e.size * 10) / 10 + " pt", "") + if e.font == "" { return size } + if size == "" { return e.font } + e.font + " · " + size +} + +// ---- modes and panels --------------------------------------------------------- + +fn set_mode(t, m){ + if t.mode == m { return } + // Comment and Edit start with the left panel folded (04-comment, + // 08-edit); leaving them brings it back. + let wide = |x| x == "comment" || x == "edit" + if wide(m) && !wide(t.mode) { + t.panel_kept = t.panel + t.panel = "" + } + if !wide(m) && wide(t.mode) && t.panel == "" { t.panel = t.panel_kept } + t.mode = m + t.place = nil + t.pending = nil + t.drag = nil + if m != "edit" { t.edit = nil } + if m == "comment" && t.comments == nil { load_comments(t) } + if m == "fill" && t.fields == nil { load_fields(t) } + if m == "combine" { start_combine(t) } + // Until the person zooms, the page fits the canvas the mode leaves it, + // up to 100%. + if t.zoom_auto && t.fit == "" { t.zoom = start_zoom(t) } + show() + if m != "pages" && m != "combine" { go_page(t, t.page) } + pump() +} +fn set_panel(t, p){ + t.panel = pick(t.panel == p, "", p) + if t.panel == "comments" && t.comments == nil { load_comments(t) } + show() + if auto_fit(t) { go_page(t, t.page) } + pump() +} +// The automatic zoom worked out again for the canvas there is now: whether +// it changed. +fn auto_fit(t){ + if !t.zoom_auto || t.fit != "" || t.state != "ready" { return false } + let z = start_zoom(t) + if z == t.zoom { return false } + t.zoom = z + true +} + +// ---- size -------------------------------------------------------------------- + +// The shell calls this when the app's window changes size, on every frame +// of its open animation too (Photos does the same): settle, then lay out. +fn on_app_resize(width, height){ + if width <= 0 { return } + if abs(width - st.width) < 2 && abs(height - st.height) < 2 { return } + st.width = width + st.height = height + st.resize_n = st.resize_n + 1 + let n = st.resize_n + start_timeout(0.3, || { + if n != st.resize_n || !st.booted { return } + let t = cur_tab() + if t != nil && t.state == "ready" && t.fit != "" { + refit(t) + go_page(t, t.page) + } + if t != nil && auto_fit(t) { go_page(t, t.page) } + show() + }) +} + +// ---- drawing: each region renders on its own ---------------------------------- + +fn render_tabs(){ ui.tabs.render() } +fn render_home(){ if st.screen == "home" { ui.home_pane.render() render_status() } } +fn render_modebar(){ ui.modebar.render() } +fn render_actions(){ ui.modebar.render() } +fn render_canvas(){ ui.canvas.render() } +fn render_pill(){ ui.pill.render() } +fn render_toolrow(){ ui.toolrow.render() } +fn render_grid(){ ui.grid.render() } +fn render_combine(){ ui.combine.render() } +fn render_status(){ ui.status.render() } +fn render_dialog(){ + ui.dialog_box.set_visible(st.confirm != nil) + ui.dialog.render() +} +fn render_right(){ + ui.right_thread.render() + ui.right_side.render() +} +fn render_panel(){ + ui.panel_head.render() + ui.thumbs.render() + ui.list_head.render() + ui.outline.render() + ui.notes.render() + ui.results.render() +} +fn refresh_doc(){ + if st.screen != "doc" { render_tabs() return } + show() +} +// Which regions show, then every region drawn again. +fn show(){ + let t = cur_tab() + let doc = t != nil + ui.home.set_visible(!doc) + ui.doc.set_visible(doc) + if doc { + let ready = t.state == "ready" + ui.panel_narrow.set_visible(ready && left_open(t) && t.panel == "pages") + ui.panel_wide.set_visible(ready && left_open(t) && t.panel != "pages") + ui.line_left.set_visible(ready && left_open(t)) + ui.thumbs.set_visible(t.panel == "pages") + ui.outline_box.set_visible(t.panel == "outline") + ui.notes_box.set_visible(t.panel == "comments") + ui.results_box.set_visible(t.panel == "search") + ui.right_thread_box.set_visible(ready && t.mode == "comment") + ui.right_side_box.set_visible(ready && (t.mode == "fill" || t.mode == "edit")) + ui.line_right.set_visible(ready && right_open(t)) + ui.canvas.set_visible(ready && t.mode != "pages" && t.mode != "combine") + ui.grid_box.set_visible(ready && t.mode == "pages") + ui.combine_box.set_visible(ready && t.mode == "combine") + ui.notice_box.set_visible(!ready) + ui.pill.set_visible(ready && t.mode != "combine") + ui.notice.render() + render_modebar() + ui.rail.render() + render_panel() + render_toolrow() + render_right() + if ready && t.mode == "pages" { render_grid() } + if ready && t.mode == "combine" { render_combine() } + if ready && t.mode != "pages" && t.mode != "combine" { render_canvas() } + render_pill() + } else { + ui.home_pane.render() + } + render_tabs() + render_status() + render_dialog() +} + +// Every 2 s: a boot that a script budget abort cut short starts again, and +// the pump picks up work an aborted callback left (Quick Deck needed the +// same under machine load). Made once: a restyle runs this file again. +fn heartbeat(){ + if !st.booted { boot() return } + if optional(st, "index_dirty", false) { save_index() } + pump() +} +fn boot(){ + if st.booted { + resume() + return + } + load_index() + scan_library() + st.booted = true + show() + check_files() + pump() +} +// After a restyle: the same screen, every region drawn with the new look. +fn resume(){ + show() + pump() +} + +// ---- views: each returns one widget, drawn by a region's on_render ---------- +// Sizes and spacing are the maps' (design/map/NN-*.json), in points. + +// Each mode's column width: its label plus 16 pt either side (02-reading). +let MODES = [{id: "read" label: "Read" w: 69}, {id: "comment" label: "Comment" w: 101}, {id: "fill" label: "Fill & Sign" w: 100}, {id: "pages" label: "Pages" w: 78}, {id: "combine" label: "Combine" w: 99}, {id: "edit" label: "Edit" w: 61}] + +// The document tabs: Home while it shows, one tab per open PDF, then Open. +fn tabs_view(){ + View{width: Fill height: Fill flow: Overlay + View{width: Fill height: Fill flow: Down align: Align{y: 1.0} + Line{} + } + View{width: Fill height: Fill flow: Right padding: Inset{left: 13 right: 13 top: 11} spacing: 2 + if st.screen == "home" { home_tab() } + for i t in st.tabs { doc_tab(i, t) } + View{width: Fit height: 42 flow: Right align: Align{y: 0.5} padding: Inset{left: 6} + Outline{height: 36 padding: Inset{left: 12 right: 14} spacing: 6 text: "Open" icon_walk: Walk{width: 16 height: 16} + draw_icon +: {svg: icon("plus") color: pt_ink2} + draw_bg +: {color: pick(st.screen == "home", pt_tabs, pt_bar)} + on_click: || go_home()} + } + } + } +} +// A tab is 48 pt tall from the row's top padding: the row clips its last 6, +// so the open tab's rounded bottom and border disappear into the mode bar. +let Tab = RoundedView{width: Fit height: 48 flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 8 bottom: 6} spacing: 9 show_bg: true + draw_bg +: {color: #x00000000 border_size: 0.0 border_color: pt_line border_radius: 6.0}} +fn home_tab(){ + Tab{width: 188 padding: Inset{left: 16 right: 12 bottom: 6} spacing: 10 flow: Overlay draw_bg +: {color: pt_tab_on border_size: 1.0} + View{width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 10 + Ico{width: 18 height: 18 draw_svg +: {svg: icon("home") color: pt_ink}} + Label{padding: 0 text: "Home" draw_text +: {color: pt_ink text_style: theme.font_regular{font_size: 12}}} + } + View{width: Fill height: Fill flow: Down align: Align{y: 1.0} padding: Inset{left: -10 right: -6 bottom: 2} + SolidView{width: Fill height: 2 draw_bg.color: pt_accent} + } + } +} +// The title's room: the tabs share the row, at most 190 pt each. +// A tab's title width: what the row leaves after its padding (26), Open +// (100) and the Home tab (190), shared by the open tabs, less each tab's +// icon, close cross and padding (86), so Open always shows. +fn tab_text_w(){ clamp(floor((st.width - 126 - pick(st.screen == "home", 190, 0)) / max(1, st.tabs.len())) - 86, 20, 190) } +fn doc_tab(i, t){ + let on = st.screen == "doc" && i == st.cur + let title = shown_name(t.title) + pick(t.edited, " •", "") + let long = title.len() * 6.7 > tab_text_w() + GestureView{width: Fit height: 48 cursor: MouseCursor.Hand on_tap: |x, y| show_tab(i) + Tab{draw_bg +: {color: pick(on, pt_tab_on, #x00000000) border_size: pick(on, 1.0, 0.0)} + Ico{width: 16 height: 16 draw_svg +: {svg: icon("file-pdf") color: pick(on, pt_ink2, pt_muted)}} + if long { Label{padding: 0 width: tab_text_w() max_lines: 1 text_overflow: TextOverflow.Ellipsis text: title draw_text +: {color: pick(on, pt_ink, pt_muted) text_style: theme.font_regular{font_size: 12}}} } + if !long { Label{padding: 0 text: title draw_text +: {color: pick(on, pt_ink, pt_muted) text_style: theme.font_regular{font_size: 12}}} } + IconBtn{width: 26 height: 26 icon_walk: Walk{width: 13 height: 13} draw_icon +: {svg: icon("close") color: pt_muted} + on_click: || ask_close(t)} + } + } +} + +// The mode bar: modes, search, Undo, Redo, Save. +fn modebar_view(){ + let t = cur_tab() + View{width: Fill height: Fill flow: Right align: Align{y: 0.5} padding: Inset{left: 15 right: 19} + for m in MODES { mode_tab(t, m) } + // The search field takes what the row leaves, at most 344 (02-reading), + // on the right. + View{width: Fill height: Fill flow: Right align: Align{x: 1.0 y: 0.5} padding: Inset{left: 16} + if t != nil && t.state == "ready" { search_field(t) } + } + View{width: 14 height: 1} + IconBtn{icon_walk: Walk{width: 20 height: 20} draw_icon +: {svg: icon("undo") color: pick(t != nil && t.can_undo, pt_icon, pt_faint)} on_click: || undo(cur_tab(), false)} + IconBtn{margin: Inset{left: 4} icon_walk: Walk{width: 20 height: 20} draw_icon +: {svg: icon("redo") color: pick(t != nil && t.can_redo, pt_icon, pt_faint)} on_click: || undo(cur_tab(), true)} + SolidView{width: 1 height: 26 margin: Inset{left: 12 right: 14} draw_bg.color: pt_line} + Primary{width: pick(narrow(), 100, 128) height: 33 spacing: 9 text: pick(t != nil && t.saving, "Saving…", "Save") icon_walk: Walk{width: 17 height: 17} + draw_icon +: {svg: icon("save")} draw_text +: {text_style: theme.font_regular{font_size: 11.5}} + on_click: || save(cur_tab(), nil)} + } +} +fn mode_tab(t, m){ + let on = t != nil && t.mode == m.id + // A narrow window (the desktop's default) trims each tab's padding, + // so the search field keeps room for its words. + View{width: m.w - pick(narrow(), 16, 0) height: Fill flow: Down align: Align{x: 0.5} + Flat{width: Fill height: Fill padding: Inset{left: 6 right: 6} text: m.label + draw_bg +: {color_hover: #x00000000 color_down: #x00000000} + draw_text +: {color: pick(on, pt_link, pt_ink2) color_hover: pick(on, pt_link, pt_ink) color_down: pt_link color_focus: pick(on, pt_link, pt_ink2) text_style: theme.font_regular{font_size: 12}} + on_click: || { let x = cur_tab() if x != nil && x.state == "ready" { set_mode(x, m.id) } }} + SolidView{width: Fill height: 3 margin: pick(narrow(), Inset{left: 8 right: 8 bottom: 1}, Inset{left: 16 right: 16 bottom: 1}) draw_bg.color: pick(on, pt_accent, #x00000000)} + } +} +fn search_field(t){ + let q = "" + if t.find != nil { q = t.find.query } + RoundedView{width: Fill max_width: 344 height: 35 flow: Overlay show_bg: true + draw_bg +: {color: pt_field border_size: 1.0 border_color: pt_field_line border_radius: 5.0} + View{width: Fill height: Fill flow: Right align: Align{y: 0.5} padding: Inset{left: 12 right: 6} + Ico{width: 18 height: 18 draw_svg +: {svg: icon("search") color: pt_muted}} + Gap{} + if q != "" { + IconBtn{width: 26 height: 26 icon_walk: Walk{width: 13 height: 13} draw_icon +: {svg: icon("close") color: pt_muted} on_click: || end_find(cur_tab())} + } + } + search := TextInput{width: Fill height: Fill margin: Inset{left: 36 right: pick(q != "", 34, 8)} padding: Inset{left: 4 right: 4 top: 9 bottom: 8} + text: q empty_text: "Search in document" + on_return: |text| find_text(text) + draw_bg +: {color: #x00000000 color_hover: #x00000000 color_focus: #x00000000 color_empty: #x00000000 + border_color: #x00000000 border_color_hover: #x00000000 border_color_focus: #x00000000 border_color_empty: #x00000000 + border_size: 0.0} + draw_text +: {color: pt_ink color_hover: pt_ink color_focus: pt_ink color_empty: pt_faint color_empty_hover: pt_faint color_empty_focus: pt_faint text_style: theme.font_regular{font_size: 12}} + draw_selection.color: #x557dcc55} + } +} + +// The rail: Pages, Outline, Comments, Search. +fn rail_view(){ + let t = cur_tab() + View{width: Fill height: Fill flow: Down align: Align{x: 0.5} padding: Inset{top: 13} spacing: 8 + rail_item(t, "pages", "page", "Pages") + rail_item(t, "outline", "outline", "Outline") + rail_item(t, "comments", "comment", "Comments") + rail_item(t, "search", "search", "Search") + } +} +fn rail_on(t, id){ + if t == nil { return false } + if t.mode == "pages" { return id == "pages" } + left_open(t) && t.panel == id +} +fn rail_tap(id){ + let t = cur_tab() + if t == nil || t.state != "ready" { return } + if t.mode == "pages" || t.mode == "combine" { + t.panel = id + set_mode(t, "read") + return + } + if narrow() && right_open(t) { set_mode(t, "read") } + set_panel(t, id) +} +fn rail_item(t, id, ic, label){ + let on = rail_on(t, id) + GestureView{width: 69 height: 77 cursor: MouseCursor.Hand on_tap: |x, y| rail_tap(id) + RoundedView{width: Fill height: Fill flow: Down align: Align{x: 0.5 y: 0.5} spacing: 7 show_bg: true + draw_bg +: {color: pick(on, pt_tint, #x00000000) border_radius: 6.0} + Ico{width: 26 height: 26 draw_svg +: {svg: icon(ic) color: pick(on, pt_tint_ink, pt_icon)}} + Label{padding: 0 text: label draw_text +: {color: pick(on, pt_tint_ink, pt_muted) text_style: theme.font_regular{font_size: 10.5}}} + } + } +} + +// The left panel's head: its title and a cross that folds it away. +fn head_row(title, line){ + View{width: Fill height: Fit flow: Down + View{width: Fill height: 46 flow: Right align: Align{y: 0.5} padding: Inset{left: 15 right: 8} + Label{padding: 0 width: Fill max_lines: 1 text_overflow: TextOverflow.Ellipsis text: title draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 13}}} + IconBtn{width: 30 height: 30 icon_walk: Walk{width: 14 height: 14} draw_icon +: {svg: icon("close") color: pt_muted} + on_click: || { let t = cur_tab() if t != nil { set_panel(t, t.panel) } }} + } + if line { Line{draw_bg.color: pt_line_soft} } + } +} +fn panel_head_view(){ head_row("Pages", false) } +fn list_head_view(){ + let t = cur_tab() + if t == nil { return View{width: Fill height: 1} } + if t.panel == "outline" { return head_row("Outline", true) } + if t.panel == "comments" { return head_row("Comments", true) } + if t.find == nil { return head_row("Search", true) } + head_row(find_summary(t), true) +} + +// Pages panel: thumbnails, filled in one at a time. +fn thumbs_view(){ + let t = cur_tab() + if t == nil || t.state != "ready" { return View{width: Fill height: 1} } + let first = window_start(t) + let last = window_end(t) + View{width: Fill height: Fit flow: Down padding: Inset{top: 2 bottom: 20} spacing: 14 + if first > 1 { + LinkBtn{margin: Inset{left: 46} text: "‹ Pages 1–" + (first - 1) on_click: || go_page(cur_tab(), max(1, window_start(cur_tab()) - 20))} + } + for i in last - first + 1 { thumb_row(t, first + i) } + if last < t.pages { + LinkBtn{margin: Inset{left: 46} text: "Pages " + (last + 1) + "–" + t.pages + " ›" on_click: || go_page(cur_tab(), window_end(cur_tab()) + 1)} + } + } +} +fn thumb_row(t, p){ + let s = page_pt(t, p) + let h = round(91 * s[1] / max(1, s[0])) + let on = p == t.page + let src = optional(t.thumbs, "p" + p, nil) + GestureView{width: Fill height: Fit flow: Right align: Align{y: 1.0} padding: Inset{left: 52} spacing: 4 cursor: MouseCursor.Hand + on_tap: |x, y| go_page(cur_tab(), p) + RoundedView{width: 99 height: h + 8 padding: 3 show_bg: true + draw_bg +: {color: #x00000000 border_size: pick(on, 2.0, 0.0) border_color: pt_accent border_radius: 3.0} + RoundedView{width: 91 height: h padding: 1 show_bg: true draw_bg +: {color: #xffffff border_size: 1.0 border_color: pt_thumb_line border_radius: 0.5} + if src != nil { Image{width: Fill height: Fill fit: ImageFit.Stretch src: src} } + } + } + Label{padding: 0 text: "" + p draw_text +: {color: pick(on, pt_accent, pt_muted) text_style: theme.font_regular{font_size: 11.5}}} + } +} + +// Outline panel: the document's bookmarks as a tree. +fn flat_outline(t, items, depth, out){ + for it in items { + let kids = optional(it, "children", []) + let key = "" + depth + "/" + optional(it, "title", "") + "/" + optional(it, "page", 0) + out.push({title: optional(it, "title", "") page: optional(it, "page", 1) depth: depth key: key kids: kids.len()}) + if kids.len() > 0 && optional(t.outline_open, key, true) { flat_outline(t, kids, depth + 1, out) } + } + out +} +fn outline_view(){ + let t = cur_tab() + if t == nil || t.state != "ready" { return View{width: Fill height: 1} } + let rows = flat_outline(t, t.outline, 0, []) + let reached = 0 + for r in rows { if r.page <= t.page && r.page > reached { reached = r.page } } + let at = -1 + for i r in rows { if at < 0 && r.page == reached { at = i } } + View{width: Fill height: Fit flow: Down padding: Inset{left: 6 right: 8 top: 8 bottom: 20} spacing: 0 + if rows.len() == 0 { View{width: Fill height: Fit padding: Inset{left: 10 top: 8} Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: "This PDF has no outline." draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11.5}}}} } + for i r in rows { outline_row(t, r, i == at) } + } +} +fn outline_row(t, r, on){ + let open = optional(t.outline_open, r.key, true) + GestureView{width: Fill height: 42 cursor: MouseCursor.Hand on_tap: |x, y| go_page(cur_tab(), r.page) + RoundedView{width: Fill height: Fill flow: Right align: Align{y: 0.5} padding: Inset{left: 2 + r.depth * 36 right: 8} spacing: 6 show_bg: true + draw_bg +: {color: pick(on, pt_accent, #x00000000) border_radius: 4.0} + if r.kids > 0 { + IconBtn{width: 16 height: 24 icon_walk: Walk{width: 11 height: 11} + draw_icon +: {svg: icon(pick(open, "triangle-down", "triangle-right")) color: pick(on, #xffffff, pt_muted)} + on_click: || { let x = cur_tab() x.outline_open[r.key] = !open ui.outline.render() }} + } + if r.kids == 0 { View{width: 16 height: 1} } + Ico{width: 18 height: 18 draw_svg +: {svg: icon("bookmark") color: pick(on, #xffffff, pt_icon)}} + Label{padding: 0 width: Fill max_lines: 1 text_overflow: TextOverflow.Ellipsis text: r.title draw_text +: {color: pick(on, #xffffff, pt_ink2) text_style: theme.font_regular{font_size: 12}}} + } + } +} + +// Comments panel: every comment, newest page order as the engine lists them. +fn notes_view(){ + let t = cur_tab() + if t == nil || t.state != "ready" { return View{width: Fill height: 1} } + View{width: Fill height: Fit flow: Down padding: Inset{left: 8 right: 8 top: 8 bottom: 20} spacing: 6 + if t.comments == nil || t.comments_state != "" { + View{width: Fill height: Fit padding: Inset{left: 8 top: 6} Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: pick(t.comments_state == "" || t.comments_state == "loading", "Reading the comments…", t.comments_state) draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11.5}}}} + } + if t.comments != nil && t.comments.len() == 0 { + View{width: Fill height: Fit padding: Inset{left: 8 top: 6} Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: "No comments yet. Choose Comment to highlight text or add a note." draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11.5}}}} + } + if t.comments != nil { for c in t.comments { note_row(t, c) } } } - out.push([start, total]) - out } -fn part_name(k){ stem(current.name) + "-part" + k + ".pdf" } -// What part k is called on screen: after the document's title. -fn part_title(k){ current.title + "-part" + k } -fn split_step(){ pick(split_result == nil, 1, 2) } -fn range_text(r){ - if r[0] == r[1] { return "Page " + r[0] } - "Pages " + r[0] + "–" + r[1] +fn note_row(t, c){ + let on = t.thread == c.id + GestureView{width: Fill height: Fit cursor: MouseCursor.Hand on_tap: |x, y| open_thread(cur_tab(), c) + RoundedView{width: Fill height: Fit flow: Down padding: Inset{left: 10 right: 10 top: 9 bottom: 10} spacing: 4 show_bg: true + draw_bg +: {color: pick(on, pt_select, #x00000000) border_size: pick(on, 1.0, 0.0) border_color: pt_tint_line border_radius: 4.0} + View{width: Fill height: Fit flow: Right align: Align{y: 0.5} spacing: 8 + RoundedView{width: 10 height: 10 show_bg: true draw_bg +: {color: mark_color(optional(c, "color", "")) border_radius: 5.0}} + Label{padding: 0 text: "Page " + c.page draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 11.5}}} + Gap{} + Label{padding: 0 text: optional(c, "author", "") max_lines: 1 text_overflow: TextOverflow.Ellipsis draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11}}} + } + if optional(c, "text", "") != "" { + Label{padding: 0 width: Fill max_lines: 2 flow: Flow.Right{wrap: true} text: c.text draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 11.5 line_spacing: 1.35}}} + } + } + } } -fn split_replaces(){ - let n = 0 - for k in split_ranges().len() { if fs.exists(LIBRARY + "/" + part_name(k + 1)) { n = n + 1 } } - n +// A comment's colour as the engine reports it ("#RRGGBB"), as the nearest of +// the four dots (a comment made elsewhere can have any colour). +// "#1A2B3C" → the byte at `at` (0, 2 or 4), or -1. +let HEX_DIGITS = "0123456789abcdef0123456789ABCDEF" +fn hex_byte(s, at){ + let m = regex("^#?([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])$").exec(s) + if m == nil { return -1 } + (HEX_DIGITS.search(m.captures[at + 1]) % 16) * 16 + HEX_DIGITS.search(m.captures[at + 2]) % 16 +} +fn mark_of(c){ + let s = ("" + c).trim() + if s == "yellow" || s == "green" || s == "blue" || s == "pink" { return s } + let r = hex_byte(s, 0) + let g = hex_byte(s, 2) + let b = hex_byte(s, 4) + if r < 0 { return "yellow" } + if b > r && b > g { return "blue" } + if g > r && g >= b { return "green" } + if r > 200 && g > 170 && b < 170 { return "yellow" } + if r > g && b > g { return "pink" } + "yellow" +} +fn mark_color(c){ + let m = mark_of(c) + if m == "green" { return #x5cb85c } + if m == "blue" { return #x2d7ff9 } + if m == "pink" { return #xf0609e } + #xf5c518 } -fn do_split(){ - let parts = split_ranges() - if split_busy || parts.len() < 2 { return } - let args = {path: current.path out_dir: LIBRARY every: split_every} - if split_mode == "at" { args = {path: current.path out_dir: LIBRARY before: split_cuts} } - split_busy = true - split_error = "" - refresh() - let source = current - engine("split", args, fn(a){ - split_busy = false - if engine_gone(a) { split_error = engine_headline() refresh() return } - wrote(a) - if !a.is_ok { split_error = "Couldn't split: " + engine_error(a.error) refresh() return } - let names = [] - for f in a.data.files { - let parts = f.path.split("/") - names.push({name: parts[parts.len() - 1] first: f.first_page last: f.last_page}) - } - split_result = names - finish_split(source, names, 0) - }) + +// Search panel: the matches, grouped by page, the current one marked. +fn results_view(){ + let t = cur_tab() + if t == nil || t.state != "ready" { return View{width: Fill height: 1} } + if t.find == nil { + return View{width: Fill height: Fit padding: Inset{left: 16 right: 16 top: 10} + Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: "Type a word or phrase in Search in document, then press Return." draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11.5 line_spacing: 1.4}}} + } + } + let current = nil + if t.find.matches.len() > 0 { current = t.find.matches[t.find.cur].page } + View{width: Fill height: Fit flow: Down padding: Inset{left: 4 right: 16 top: 0 bottom: 24} spacing: 0 + for p in find_pages(t) { result_group(t, p, p == current) } + } } -// One written file per turn (each forget writes the index), then the rescan -// and the next screen: one handler has a 64 ms budget. A part of a PDF with -// a title of its own is titled after it ("Lease-part2"). -fn finish_split(source, names, i){ - if i < names.len() { - let part = names[i].name - forget(part) - let title = title_for_part(source.title, source.name, part) - if title != nil { - index[part] = {title: title} - save_index() - } - start_timeout(0.01, || finish_split(source, names, i + 1)) - return +fn result_group(t, p, on){ + let count = page_matches(t, p).len() + RoundedView{width: Fill height: Fit flow: Down padding: Inset{left: 12 right: 12 top: 12 bottom: 12} spacing: 8 show_bg: true + draw_bg +: {color: pick(on, pt_select, #x00000000) border_size: pick(on, 1.5, 0.0) border_color: pt_accent border_radius: 4.0} + View{width: Fill height: Fit flow: Right align: Align{y: 0.5} + Label{padding: 0 text: "Page " + p draw_text +: {color: pt_ink text_style: theme.font_regular{font_size: 12}}} + Gap{} + Label{padding: 0 text: plural(count, "match", "matches") draw_text +: {color: pt_faint text_style: theme.font_regular{font_size: 10.5}}} + } + for i in page_matches(t, p) { + if i > 0 && t.find.matches[i - 1].page == p { Line{draw_bg.color: pt_line_soft} } + if true { + GestureView{width: Fill height: Fit flow: Right align: Align{y: 0.5} cursor: MouseCursor.Hand on_tap: |x, y| go_match(cur_tab(), i) + Html{width: Fill height: Fit padding: 0 body: snippet_html(optional(t.find.matches[i], "snippet", "")) + font_color: pick(i == t.find.cur, pt_ink, pt_ink2) font_size: 11.5 + draw_text +: {color: pick(i == t.find.cur, pt_ink, pt_ink2)} + text_style_normal +: {font_size: 11.5} text_style_bold +: {font_size: 11.5}} + if on && i == t.find.cur { Ico{width: 16 height: 16 margin: Inset{left: 6} draw_svg +: {svg: icon("chevron-right") color: pt_muted}} } + } + } + } + if !on { Line{margin: Inset{top: 4} draw_bg.color: pt_line_soft} } } - scan_library() - if route == "split" { show(true) } else { refresh() } - start_timeout(0.01, || library_next()) } -// ---- screens ------------------------------------------------------------- - -fn banner(text){ - banner_n = banner_n + 1 - let n = banner_n - ui.banner_text.set_text(text) - ui.banner.set_visible(text != "") - if text != "" { start_timeout(8, || { if n == banner_n { ui.banner.set_visible(false) } }) } +// Above the canvas: the find bar, the Comment tools, the form banner, the +// Pages actions. +fn toolrow_view(){ + let t = cur_tab() + if t == nil || t.state != "ready" { return View{width: Fill height: 0} } + if t.mode == "comment" { return comment_tools(t) } + if t.mode == "fill" { return form_banner(t) } + if t.mode == "pages" { return pages_actions(t) } + if t.find != nil && t.mode != "combine" { return find_bar(t) } + View{width: Fill height: 0} +} +fn bar_row(height, color){ + SolidView{width: Fill height: height flow: Right align: Align{y: 0.5} draw_bg.color: color} +} +fn find_bar(t){ + let n = t.find.matches.len() + View{width: Fill height: Fit flow: Down + SolidView{width: Fill height: 50 flow: Right align: Align{x: 0.5 y: 0.5} spacing: 8 draw_bg.color: pt_bar + RoundedView{width: 104 height: 38 align: Align{x: 0.5 y: 0.5} show_bg: true draw_bg +: {color: pt_field border_size: 1.0 border_color: pt_field_line border_radius: 5.0} + Label{padding: 0 text: pick(n == 0, pick(t.finding, "…", "0 of 0"), "" + (t.find.cur + 1) + " of " + n) draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + } + Outline{width: 38 height: 38 padding: 0 spacing: 0 text: "" icon_walk: Walk{width: 18 height: 18} draw_icon +: {svg: icon("arrow-up") color: pt_ink2} on_click: || go_match(cur_tab(), cur_tab().find.cur - 1)} + Outline{width: 38 height: 38 padding: 0 spacing: 0 text: "" icon_walk: Walk{width: 18 height: 18} draw_icon +: {svg: icon("arrow-down") color: pt_ink2} on_click: || go_match(cur_tab(), cur_tab().find.cur + 1)} + Outline{height: 38 margin: Inset{left: 4} text: "Done" on_click: || end_find(cur_tab())} + } + Line{} + } } -fn go_library(){ - gen = gen + 1 - view_seq = view_seq + 1 - current = nil - route = "library" - scan_library() - show(true) - library_next() -} -fn back_from_split(){ - route = "doc" - show(true) -} - -// Each screen has its own scroller, shown here and always filled by an -// explicit render: a scroller made in another view's on_render came out -// empty with stored page images. `fresh` marks a new screen. -fn show(fresh){ - ui.top.render() - ui.findbar.set_visible(route == "doc" && tab == "text" && text_pages != nil) - ui.namebar.set_visible(route == "merge" && merge_step == 2) - ui.lib_box.set_visible(route == "library") - ui.pages_box.set_visible(route == "doc" && tab == "pages") - ui.text_box.set_visible(route == "doc" && tab == "text") - ui.info_box.set_visible(route == "doc" && tab == "info") - ui.merge_box.set_visible(route == "merge") - ui.split_box.set_visible(route == "split") - ui.stage_box.set_visible(route == "page") - render_screen() - ui.bottom.render() -} -fn render_screen(){ - if route == "library" { ui.lib_pane.render() } - if route == "doc" && tab == "pages" { ui.pages_pane.render() } - if route == "doc" && tab == "text" { ui.text_pane.render() } - if route == "doc" && tab == "info" { ui.info_pane.render() } - if route == "merge" { ui.merge_pane.render() } - if route == "split" { ui.split_pane.render() } - if route == "page" { ui.stage.render() } -} -// The same screen with new data, in place. -fn refresh(){ - ui.top.render() - render_screen() - ui.bottom.render() +fn tool_btn(t, id, ic, label){ + let on = t.tool == id + let compact = canvas_w() < 900 + Flat{height: 37 padding: pick(compact, Inset{left: 9 right: 9}, Inset{left: 12 right: 14}) spacing: pick(compact, 0, 9) text: pick(compact, "", label) icon_walk: Walk{width: 20 height: 20} + draw_icon +: {svg: icon(ic) color: pick(on, pt_link, pt_icon)} + draw_bg +: {color: pick(on, pt_tint, #x00000000) border_size: pick(on, 1.0, 0.0) border_color: pt_tint_line color_hover: pick(on, pt_tint, pt_hover)} + draw_text +: {color: pick(on, pt_link, pt_ink2) color_hover: pick(on, pt_link, pt_ink) color_focus: pick(on, pt_link, pt_ink2)} + on_click: || { let x = cur_tab() x.tool = id x.pending = nil render_toolrow() render_right() }} +} +fn comment_tools(t){ + View{width: Fill height: Fit flow: Down + SolidView{width: Fill height: 61 flow: Right align: Align{y: 0.5} padding: Inset{left: 22 right: 22} spacing: pick(canvas_w() < 900, 8, 22) draw_bg.color: pt_bar + tool_btn(t, "highlight", "highlight", "Highlight") + tool_btn(t, "underline", "underline", "Underline") + tool_btn(t, "strike", "strike", "Strike out") + tool_btn(t, "note", "note", "Note") + tool_btn(t, "textbox", "textbox", "Text box") + View{width: 40 height: 1} + View{width: Fit height: Fit flow: Right spacing: 12 + for m in MARKS { mark_dot(t, m) } + } + } + Line{} + } } - -fn boot(){ - load_index() - scan_library() - show(true) - check_files() - library_next() +fn mark_dot(t, m){ + let on = t.color == m + GestureView{width: 30 height: 30 align: Align{x: 0.5 y: 0.5} cursor: MouseCursor.Hand on_tap: |x, y| { let c = cur_tab() c.color = m render_toolrow() } + RoundedView{width: 30 height: 30 align: Align{x: 0.5 y: 0.5} show_bg: true + draw_bg +: {color: #x00000000 border_size: pick(on, 2.0, 0.0) border_color: pt_accent border_radius: 15.0} + RoundedView{width: 19 height: 19 show_bg: true draw_bg +: {color: mark_color(m) border_radius: 9.5}} + } + } +} +fn form_banner(t){ + View{width: Fill height: Fit flow: Down + SolidView{width: Fill height: 48 flow: Right align: Align{y: 0.5} padding: Inset{left: 22 right: 22} spacing: 12 draw_bg.color: pt_select + RoundedView{width: 22 height: 22 align: Align{x: 0.5 y: 0.5} show_bg: true draw_bg +: {color: pt_accent border_radius: 11.0} + Label{padding: 0 text: "i" draw_text +: {color: #xffffff text_style: theme.font_bold{font_size: 11}}} + } + Label{padding: 0 width: Fill max_lines: 1 text_overflow: TextOverflow.Ellipsis text: form_summary(t) draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + if t.place != nil { Outline{height: 30 text: "Cancel" on_click: || { let x = cur_tab() x.place = nil render_toolrow() render_right() }} } + if t.place == nil && t.fields != nil && t.fields.len() > 0 { + Label{padding: 0 text: "Highlight fields" draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + Toggle{text: "" active: t.show_fields on_click: |active| { let x = cur_tab() x.show_fields = active render_canvas() } + draw_bg +: {size: 22.0 border_size: 0.0 border_radius: 5.0 color: pt_field_line color_hover: pt_field_line color_active: pt_accent color_focus: pt_field_line + mark_color: #xffffff mark_color_hover: #xffffff mark_color_active: #xffffff mark_color_active_hover: #xffffff}} + } + } + Line{} + } +} +fn pages_actions(t){ + let n = chosen(t).len() + let hint = "Drag pages to reorder" + if t.drag != nil { hint = "Drop to move page " + t.drag.page } + View{width: Fill height: Fit flow: Down + SolidView{width: Fill height: 52 flow: Right align: Align{y: 0.5} padding: Inset{left: 26 right: 26} spacing: 14 draw_bg.color: pt_bar + Label{padding: 0 width: 110 text: pick(n == 0, "None selected", "" + n + " selected") draw_text +: {color: pick(n == 0, pt_muted, pt_link) text_style: theme.font_bold{font_size: 12}}} + Flat{text: "Rotate left" draw_icon +: {svg: icon("rotate-left")} on_click: || rotate(cur_tab(), -90)} + Flat{text: "Rotate right" draw_icon +: {svg: icon("rotate-right")} on_click: || rotate(cur_tab(), 90)} + SolidView{width: 1 height: 24 draw_bg.color: pt_line} + Flat{text: "Delete" draw_icon +: {svg: icon("trash")} on_click: || delete_pages(cur_tab())} + Flat{text: "Extract" draw_icon +: {svg: icon("extract")} on_click: || extract_pages(cur_tab())} + Flat{text: "Insert from file" draw_icon +: {svg: icon("insert")} on_click: || { let x = cur_tab() x.inserting = !x.inserting render_toolrow() }} + Gap{} + // In a narrow window the hint gives the buttons its room. + if canvas_w() >= 1000 || t.drag != nil { + Label{padding: 0 text: hint draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12}}} + } + } + if t.inserting { insert_strip(t) } + Line{} + } +} +fn insert_strip(t){ + let ch = chosen(t) + let after = "the last page" + if ch.len() > 0 { after = "page " + ch[ch.len() - 1] } + View{width: Fill height: Fit flow: Down + Line{draw_bg.color: pt_line_soft} + SolidView{width: Fill height: Fit flow: Flow.Right{wrap: true} align: Align{y: 0.5} padding: Inset{left: 26 right: 26 top: 8 bottom: 8} spacing: 8 draw_bg.color: pt_bar + Label{padding: 0 text: "Insert after " + after + ":" draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + for d in st.docs { + if d.name != t.name && d.error == "" && !d.locked { + Outline{height: 30 text: shown_name(d.title) draw_icon +: {svg: icon("file-pdf")} icon_walk: Walk{width: 15 height: 15} on_click: || insert_file(cur_tab(), d)} + } + } + if st.docs.len() < 2 { Label{padding: 0 text: "Open another PDF first: it appears here." draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12}}} } + } + } } -start_timeout(0.05, || boot()) -// ---- widgets ------------------------------------------------------------- - -let Head = Label{width: Fill padding: 0 text: "" draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 16}}} -let Body = Label{width: Fill padding: 0 flow: Flow.Right{wrap: true} text: "" draw_text +: {color: ui_ink text_style: theme.font_regular{font_size: 14 line_spacing: 1.4}}} -let Glyph = Label{padding: 0 text: "" draw_text +: {color: ui_muted text_style: theme.font_icons{font_size: 12}}} -let Back = UiButton{padding: Inset{left: 10 right: 14}} -let Quiet = UiPill{width: Fill} -let Muted = UiButton{width: Fill - draw_bg +: {color: ui_field color_hover: ui_field color_down: ui_field color_focus: ui_field} - draw_text +: {color: ui_muted color_hover: ui_muted color_down: ui_muted color_focus: ui_muted} -} -let Primary = UiPrimary{width: Fill} -let Danger = UiPrimary{width: Fill draw_bg +: {color: #xd23a4f color_hover: #xdc4b5f color_down: #xb52d40 color_focus: #xb52d40}} -let Subtle = UiButton{draw_text +: {color: ui_muted color_hover: ui_ink color_down: ui_ink color_focus: ui_muted}} -let IconAct = UiButton{width: 36 padding: 0 draw_text +: {color: ui_muted color_hover: ui_ink color_down: ui_ink text_style: theme.font_icons{font_size: 14}}} -let List = RoundedView{width: Fill height: Fit flow: Down show_bg: true draw_bg +: {color: ui_surface border_radius: 14.0}} -let Cover = RoundedView{width: 46 height: 60 align: Align{x: 0.5 y: 0.5} padding: 1 show_bg: true - draw_bg +: {color: #xffffff border_size: 1.0 border_color: ui_border border_radius: 3.0}} -let RowText = View{width: Fill height: Fit flow: Down spacing: 3 - name := Label{width: Fill padding: 0 max_lines: 1 text: "" draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 15}}} - meta := Label{width: Fill padding: 0 max_lines: 1 text: "" draw_text +: {color: ui_muted text_style: theme.font_regular{font_size: 13}}} +// The canvas: the run of pages, each the engine's render, with what the +// mode puts over it. +fn canvas_view(){ + let t = cur_tab() + if t == nil || t.state != "ready" || st.blank { return View{width: 10 height: 1} } + View{width: Fit height: Fit flow: Down spacing: PAGE_GAP align: Align{x: 0.5} + for i in run_end(t) - t.anchor + 1 { page_view(t, t.anchor + i) } + if run_end(t) < t.pages { + View{width: Fit height: Fit padding: Inset{top: 8} + Outline{text: "Continue with page " + (run_end(t) + 1) icon_walk: Walk{width: 16 height: 16} draw_icon +: {svg: icon("arrow-down")} on_click: || go_page(cur_tab(), run_end(cur_tab()) + 1)} + } + } + } } -// A plain View paints no background in the contained host: lines are SolidViews. -let RowLine = SolidView{width: Fill height: 1 margin: Inset{left: 76} draw_bg.color: ui_border} -let InfoLine = SolidView{width: Fill height: 1 margin: Inset{left: 18} draw_bg.color: ui_border} -let StepLine = SolidView{width: 18 height: 1 draw_bg.color: ui_border} -let Mark = RoundedView{width: 26 height: 26 align: Align{x: 0.5 y: 0.5} show_bg: true - draw_bg +: {color: #x00000000 border_size: 2.0 border_color: ui_border border_radius: 13.0} - n := Label{padding: 0 text: "" draw_text +: {color: #xffffff text_style: theme.font_bold{font_size: 12}}} -} -let MarkOn = Mark{draw_bg +: {color: ui_primary border_color: ui_primary}} -let Segment = UiButton{width: Fill height: 36 padding: 4 - draw_bg +: {color: #x00000000 color_hover: ui_hover color_down: ui_hover color_focus: ui_hover border_radius: 10.0} - draw_text +: {color: ui_muted color_hover: ui_ink color_down: ui_ink color_focus: ui_ink text_style: theme.font_bold{font_size: 13}} -} -// The selected segment: white on the light track, a lighter grey on the dark one. -let ui_selected = if ui_light { #xffffff } else { mix(ui_page, ui_ink, 0.22) } -let SegmentOn = Segment{ - draw_bg +: {color: ui_selected color_hover: ui_selected color_down: ui_selected color_focus: ui_selected} - draw_text +: {color: ui_ink color_hover: ui_ink color_down: ui_ink color_focus: ui_ink} -} -let Segments = RoundedView{width: Fill height: Fit flow: Right padding: 3 spacing: 3 show_bg: true - draw_bg +: {color: ui_field border_radius: 12.0}} -let Dot = RoundedView{width: 22 height: 22 align: Align{x: 0.5 y: 0.5} show_bg: true - draw_bg +: {color: ui_field border_radius: 11.0} - n := Label{padding: 0 text: "" draw_text +: {color: ui_muted text_style: theme.font_bold{font_size: 11}}} -} -let DotOn = RoundedView{width: 22 height: 22 align: Align{x: 0.5 y: 0.5} show_bg: true - draw_bg +: {color: ui_primary border_radius: 11.0} - n := Label{padding: 0 text: "" draw_text +: {color: #xffffff text_style: theme.font_bold{font_size: 11}}} -} -let StepName = Label{padding: 0 text: "" draw_text +: {color: ui_muted text_style: theme.font_regular{font_size: 13}}} -let StepNameOn = Label{padding: 0 text: "" draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 13}}} -let Tile = RoundedView{width: Fill height: 230 align: Align{x: 0.5 y: 0.5} padding: 8 show_bg: true - draw_bg +: {color: ui_field border_radius: 8.0}} -let TileCut = Tile{draw_bg +: {border_size: 2.5 border_color: ui_primary}} -let TileLabel = Label{padding: 0 text: "" draw_text +: {color: ui_muted text_style: theme.font_regular{font_size: 12}}} -let Alert = UiCard{flow: Right spacing: 14 align: Align{y: 0.0}} -let EngineAlert = Alert{ - Glyph{text: ICON_WARN draw_text +: {color: ui_danger text_style: theme.font_icons{font_size: 18}}} - text := View{width: Fill height: Fit flow: Down spacing: 6 - head := Head{} - detail := UiCaption{text: ""} - note := Label{width: Fill padding: 0 flow: Flow.Right{wrap: true} text: "" draw_text +: {color: ui_muted text_style: theme.font_code{font_size: 11}}} - } -} -let SpaceAlert = Alert{ - Glyph{text: ICON_WARN draw_text +: {color: ui_danger text_style: theme.font_icons{font_size: 18}}} - View{width: Fill height: Fit flow: Down spacing: 6 - Head{text: "PDF Tools' storage is full"} - UiCaption{text: "Page pictures and new PDFs can't be saved until there's room. Open a PDF you no longer need and remove it."} - } -} -// One scroller per screen (and per document tab), each keeping its place. -let Pane = ScrollYView{width: Fill height: Fill flow: Down spacing: 14 padding: Inset{left: 16 right: 16 top: 2 bottom: 24}} - -SolidView{width: Fill height: Fill flow: Overlay align: Align{x: 0.5} draw_bg.color: ui_page - main := View{width: Fill max_width: 880 height: Fill flow: Down - - top := View{width: Fill height: Fit flow: Down on_render: || { - View{width: Fill height: 1} - if route == "library" { - View{width: Fill height: Fit flow: Down spacing: 4 padding: Inset{left: 20 right: 20 top: 20 bottom: 12} - UiTitle{text: "PDF Tools"} - UiCaption{text: library_caption()} +fn page_view(t, p){ + let box = page_box(t, p) + let k = t.zoom + let src = page_src(t, p) + let failed = optional(t.failed, "p" + p, nil) != nil && src == nil + RectShadowView{width: box[0] height: box[1] flow: Overlay + draw_bg +: {color: #xffffff shadow_color: pt_shadow shadow_radius: 7.0 shadow_offset: vec2(0.0, 1.5)} + if src != nil { Image{width: Fill height: Fill fit: ImageFit.Stretch src: src} } + if src == nil { + View{width: Fill height: Fill flow: Down align: Align{x: 0.5 y: 0.5} spacing: 10 + if !failed { LoadingSpinner{width: 26 height: 26} } + Label{padding: 0 text: pick(failed, "Couldn't draw page " + p, "Rendering page " + p + "…") draw_text +: {color: #x8a8c90 text_style: theme.font_regular{font_size: 11.5}}} + } + } + if t.find != nil { for i in page_matches(t, p) { for r in t.find.matches[i].rects { hit_box(r, k, i == t.find.cur) } } } + if t.mode == "comment" { comment_marks(t, p, k) } + if t.mode == "fill" && t.show_fields && t.fields != nil { for f in t.fields { if f.page == p { field_box(t, f, k) } } } + GestureView{width: Fill height: Fill cursor: pick(t.mode == "read", MouseCursor.Default, MouseCursor.Hand) + on_tap: |x, y| page_tap(cur_tab(), p, x / k, y / k) + on_double_tap: |x, y| page_double_tap(cur_tab(), p, x / k, y / k)} + if t.mode == "edit" && t.edit != nil && t.edit.page == p { edit_box(t, k) } + } +} +fn hit_box(r, k, on){ + RoundedView{margin: Inset{left: r[0] * k - 1 top: r[1] * k - 1} width: r[2] * k + 2 height: r[3] * k + 2 show_bg: true + draw_bg +: {color: pick(on, pt_hit_on, pt_hit) border_size: pick(on, 1.5, 0.0) border_color: #xf0a020 border_radius: 2.0}} +} +fn comment_marks(t, p, k){ + View{width: Fill height: Fill flow: Overlay + if t.comments != nil { + for c in t.comments { + if c.page == p && c.id == t.thread { + for r in optional(c, "rects", []) { + RoundedView{margin: Inset{left: r[0] * k - 3 top: r[1] * k - 3} width: r[2] * k + 6 height: r[3] * k + 6 show_bg: true + draw_bg +: {color: #x00000000 border_size: 1.5 border_color: pt_accent border_radius: 3.0}} + } } } - if route == "doc" && current != nil { - View{width: Fill height: Fit flow: Right align: Align{y: 0.5} padding: Inset{left: 6 right: 10 top: 8} - Back{text: "‹ Library" on_click: || go_library()} - View{width: Fill height: 1} - if !confirm_remove { Subtle{text: "Remove" on_click: || ask_remove()} } + } + if t.pending != nil && t.pending.page == p { + RoundedView{margin: Inset{left: t.pending.at[0] * k - 13 top: t.pending.at[1] * k - 13} width: 26 height: 26 align: Align{x: 0.5 y: 0.5} show_bg: true + draw_bg +: {color: #xf5c518 border_size: 1.0 border_color: #xc79a00 border_radius: 4.0} + Ico{width: 16 height: 16 draw_svg +: {svg: icon("note") color: #x5a4600}} + } + } + } +} +fn field_box(t, f, k){ + let r = f.rect + let on = t.field == f.name + let need = optional(f, "required", false) && field_empty(f) + RoundedView{margin: Inset{left: r[0] * k top: r[1] * k} width: r[2] * k height: r[3] * k show_bg: true + draw_bg +: {color: pick(need, #xe5534f12, pt_field_box) border_size: pick(on, 2.0, 1.0) border_color: pick(need, #xe5534f, pick(on, #x116cdc, #x7fa6dd)) border_radius: 2.0}} +} +// The editor is at least the paragraph's box, and grows down to hold its +// text: the app's sans is wider than most PDF faces, so it wraps into more +// lines (the line count is estimated from an average glyph width). +fn edit_height(e, k){ + let em = max(8, e.size * k * 0.75) * 1.32 + let inner = max(40, e.box[2] * k) + let lines = ceil(e.draft.len() * em * 0.56 / inner) + e.draft.split("\n").len() - 1 + max(e.box[3] * k + 16, lines * em * 1.3 + 22) +} +fn edit_box(t, k){ + let e = t.edit + let b = e.box + View{margin: Inset{left: b[0] * k - 8 top: b[1] * k - 8} width: b[2] * k + 16 height: edit_height(e, k) flow: Overlay + RoundedView{width: Fill height: Fill show_bg: true draw_bg +: {color: #xffffff border_size: 1.5 border_color: #x2171ef border_radius: 0.0}} + TextInput{width: Fill height: Fill margin: 6 padding: 2 is_multiline: true text: e.draft + on_change: |text| { let x = cur_tab() if x != nil && x.edit != nil { x.edit.draft = text } } + draw_bg +: {color: #xffffff color_hover: #xffffff color_focus: #xffffff color_empty: #xffffff + border_color: #x00000000 border_color_hover: #x00000000 border_color_focus: #x00000000 border_color_empty: #x00000000 + border_size: 0.0} + draw_text +: {color: #x111111 color_hover: #x111111 color_focus: #x111111 color_empty: #x888888 text_style: theme.font_regular{font_size: max(8, e.size * k * 0.75) line_spacing: 1.3}} + draw_selection.color: #x557dcc55} + handle(0, 0) handle(0.5, 0) handle(1, 0) handle(0, 1) handle(0.5, 1) handle(1, 1) + } +} +fn handle(ax, ay){ + View{width: Fill height: Fill align: Align{x: ax y: ay} + RoundedView{width: 8 height: 8 show_bg: true draw_bg +: {color: #xffffff border_size: 1.5 border_color: #x2171ef border_radius: 1.0}} + } +} + +// The floating pill: page, previous, next, zoom, fit width, fit page. +fn pill_view(){ + let t = cur_tab() + if t == nil || t.state != "ready" || t.mode == "combine" { return View{width: 1 height: 1} } + RoundedShadowView{width: Fit height: 53 flow: Right align: Align{y: 0.5} padding: Inset{left: 14 right: 14} spacing: 4 + draw_bg +: {color: pt_pill border_size: 1.0 border_color: pt_line border_radius: 26.5 shadow_color: pt_shadow shadow_radius: 10.0 shadow_offset: vec2(0.0, 2.0)} + IconBtn{icon_walk: Walk{width: 18 height: 18} draw_icon +: {svg: icon("chevron-left") color: pt_icon} on_click: || step_page(cur_tab(), -1)} + View{width: 78 height: Fit align: Align{x: 0.5} + Label{padding: 0 text: "" + t.page + " / " + t.pages draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + } + IconBtn{icon_walk: Walk{width: 18 height: 18} draw_icon +: {svg: icon("chevron-right") color: pt_icon} on_click: || step_page(cur_tab(), 1)} + SolidView{width: 1 height: 28 margin: Inset{left: 12 right: 12} draw_bg.color: pt_line} + IconBtn{icon_walk: Walk{width: 18 height: 18} draw_icon +: {svg: icon("minus") color: pt_icon} on_click: || zoom_step(cur_tab(), -1)} + View{width: 66 height: Fit align: Align{x: 0.5} + Label{padding: 0 text: zoom_text(t) draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + } + IconBtn{icon_walk: Walk{width: 18 height: 18} draw_icon +: {svg: icon("plus") color: pt_icon} on_click: || zoom_step(cur_tab(), 1)} + if t.mode != "pages" { + SolidView{width: 1 height: 28 margin: Inset{left: 12 right: 12} draw_bg.color: pt_line} + IconBtn{icon_walk: Walk{width: 21 height: 21} draw_icon +: {svg: icon("fit-width") color: pick(t.fit == "width", pt_accent, pt_icon)} on_click: || { let x = cur_tab() set_zoom(x, fit_width_zoom(x), "width") }} + IconBtn{margin: Inset{left: 6} icon_walk: Walk{width: 21 height: 21} draw_icon +: {svg: icon("fit-page") color: pick(t.fit == "page", pt_accent, pt_icon)} on_click: || { let x = cur_tab() set_zoom(x, fit_page_zoom(x), "page") }} + } + } +} + +// Pages mode: every page as a large thumbnail; tap to choose, drag to move. +fn grid_view(){ + let t = cur_tab() + if t == nil || t.state != "ready" { return View{width: Fill height: 1} } + let cols = grid_cols(t) + let first = window_start(t) + let last = window_end(t) + let rows = ceil((last - first + 1) / cols) + View{width: Fill height: Fit flow: Down spacing: 26 align: Align{x: 0.5} + if first > 1 { LinkBtn{text: "‹ Pages 1–" + (first - 1) on_click: || { let x = cur_tab() x.page = max(1, window_start(x) - 20) render_grid() pump() }} } + for r in rows { + View{width: Fit height: Fit flow: Right spacing: 30 + for c in cols { + if first + r * cols + c <= last { tile_view(t, first + r * cols + c) } } - View{width: Fill height: Fit flow: Down spacing: 3 padding: Inset{left: 20 right: 20 top: 2 bottom: 12} - Label{width: Fill padding: 0 max_lines: 2 text: current.title draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 24 line_spacing: 1.2}}} - UiCaption{text: doc_meta(current)} + } + } + if last < t.pages { LinkBtn{text: "Pages " + (last + 1) + "–" + t.pages + " ›" on_click: || { let x = cur_tab() x.page = window_end(x) + 1 render_grid() pump() }} } + } +} +fn tile_view(t, p){ + let s = page_pt(t, p) + let w = tile_w(t) + let h = round(w * s[1] / max(1, s[0])) + let on = has(t.selected, p) + let src = optional(t.grid, "p" + p, nil) + let marker = t.drag != nil && t.drag.to == p && t.drag.page != p + View{width: Fit height: Fit flow: Right spacing: 0 + if marker { drop_marker(h) } + GestureView{width: w + 6 height: Fit flow: Down align: Align{x: 0.5} spacing: 8 cursor: MouseCursor.Hand + on_tap: |x, y| toggle_select(cur_tab(), p) + on_pan: |dx, dy, phase| tile_pan(cur_tab(), p, dx, dy, phase) + RoundedView{width: w + 6 height: h + 6 padding: 2 flow: Overlay show_bg: true + draw_bg +: {color: #x00000000 border_size: pick(on, 2.0, 0.0) border_color: pt_accent border_radius: 3.0} + RectShadowView{width: w height: h margin: 1 draw_bg +: {color: #xffffff shadow_color: pt_shadow shadow_radius: 4.0 shadow_offset: vec2(0.0, 1.0)} + if src != nil { Image{width: Fill height: Fill fit: ImageFit.Stretch src: src} } } - if confirm_remove { - View{width: Fill height: Fit padding: Inset{left: 16 right: 16 bottom: 10} - Alert{ - Glyph{text: ICON_TRASH draw_text +: {color: ui_danger text_style: theme.font_icons{font_size: 18}}} - View{width: Fill height: Fit flow: Down spacing: 6 - Head{text: "Remove this PDF from PDF Tools?"} - UiCaption{text: "It is deleted from this app's storage. PDFs you made from it stay."} - } + if on { + View{width: Fill height: Fill align: Align{x: 1.0 y: 0.0} padding: 10 + RoundedView{width: 22 height: 22 align: Align{x: 0.5 y: 0.5} show_bg: true draw_bg +: {color: pt_accent border_size: 1.5 border_color: #xffffff border_radius: 11.0} + Ico{width: 13 height: 13 draw_svg +: {svg: icon("check") color: #xffffff}} } } } - View{width: Fill height: Fit padding: Inset{left: 16 right: 16 bottom: 10} - Segments{ - if tab == "pages" { SegmentOn{text: "Pages"} } else { Segment{text: "Pages" on_click: || set_tab("pages")} } - if tab == "text" { SegmentOn{text: "Text"} } else { Segment{text: "Text" on_click: || set_tab("text")} } - if tab == "info" { SegmentOn{text: "Info"} } else { Segment{text: "Info" on_click: || set_tab("info")} } - } + } + Label{padding: 0 text: "" + p draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12.5}}} + } + } +} +fn drop_marker(h){ + View{width: 16 height: h + 6 flow: Down align: Align{x: 0.5} margin: Inset{left: -23} + SolidView{width: 9 height: 2 draw_bg.color: pt_accent} + View{width: 2 height: Fill flow: Down spacing: 4 + for i in floor((h - 4) / 10) { SolidView{width: 2 height: 6 draw_bg.color: pt_accent} } + } + SolidView{width: 9 height: 2 draw_bg.color: pt_accent} + } +} + +// Combine: PDFs in order, each with its pages, into one new PDF. +fn combine_view(){ + let t = cur_tab() + if t == nil || t.combine == nil { return View{width: Fill height: 1} } + let c = t.combine + let total = combine_total(t) + RoundedShadowView{width: Fill max_width: 717 height: Fit flow: Down padding: Inset{left: 24 right: 24 top: 34 bottom: 24} + draw_bg +: {color: pt_card border_size: 1.0 border_color: pt_line border_radius: 7.0 shadow_color: pt_shadow shadow_radius: 8.0 shadow_offset: vec2(0.0, 1.0)} + View{width: Fit height: Fit padding: Inset{left: 16 bottom: 22} Label{padding: 0 text: "Combine files" draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 16}}}} + RoundedView{width: Fill height: Fit flow: Down show_bg: true draw_bg +: {color: pt_card border_size: 1.0 border_color: pt_line border_radius: 3.0} + for i it in c.items { + combine_row(t, i, it) + Line{draw_bg.color: pt_line_soft} + } + GestureView{width: Fill height: 60 padding: Inset{left: 8 right: 8 top: 9 bottom: 9} cursor: MouseCursor.Hand on_tap: |x, y| { let x = cur_tab() x.combine.adding = !x.combine.adding render_combine() } + dashed_box() + } + if c.adding { add_list(t) } + } + Line{margin: Inset{top: 18 bottom: 20}} + Label{padding: 0 text: pick(total == nil, "Check the pages of each PDF", "One PDF of " + plural(total, "page", "pages")) draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 13}}} + View{width: Fill height: Fit flow: Right align: Align{y: 0.5} margin: Inset{top: 22} spacing: 18 + Label{padding: 0 text: "Name" draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + Field{width: 310 height: 40 text: c.name empty_text: "Name of the new PDF" on_change: |text| { let x = cur_tab() x.combine.name = text }} + } + if c.error != "" { + View{width: Fill height: Fit padding: Inset{top: 14} Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: c.error draw_text +: {color: pt_red text_style: theme.font_regular{font_size: 12}}}} + } + View{width: Fill height: Fit flow: Right margin: Inset{top: 14} + Gap{} + Primary{width: 140 height: 46 spacing: 10 text: pick(c.busy, "Combining…", "Combine") icon_walk: Walk{width: 18 height: 18} draw_icon +: {svg: icon("combine")} + draw_text +: {text_style: theme.font_regular{font_size: 13}} + on_click: || do_combine(cur_tab())} + } + } +} +fn dashed_box(){ + View{width: Fill height: Fill flow: Overlay + View{width: Fill height: Fill flow: Down + View{width: Fill height: 1 flow: Right spacing: 4 for i in 120 { SolidView{width: 5 height: 1 draw_bg.color: pt_accent} }} + View{width: Fill height: Fill flow: Right + View{width: 1 height: Fill flow: Down spacing: 4 for i in 8 { SolidView{width: 1 height: 5 draw_bg.color: pt_accent} }} + Gap{} + View{width: 1 height: Fill flow: Down spacing: 4 for i in 8 { SolidView{width: 1 height: 5 draw_bg.color: pt_accent} }} + } + View{width: Fill height: 1 flow: Right spacing: 4 for i in 120 { SolidView{width: 5 height: 1 draw_bg.color: pt_accent} }} + } + View{width: Fill height: Fill flow: Right align: Align{x: 0.5 y: 0.5} spacing: 10 + Ico{width: 17 height: 17 draw_svg +: {svg: icon("plus") color: pt_link}} + Label{padding: 0 text: "Add files" draw_text +: {color: pt_link text_style: theme.font_regular{font_size: 13}}} + } + } +} +fn add_list(t){ + View{width: Fill height: Fit flow: Down padding: Inset{left: 12 right: 12 bottom: 10} spacing: 2 + for d in st.docs { + if !combine_has(t, d.name) && d.error == "" && !d.locked { + GestureView{width: Fill height: 38 flow: Right align: Align{y: 0.5} padding: Inset{left: 8 right: 8} spacing: 10 cursor: MouseCursor.Hand on_tap: |x, y| combine_add(cur_tab(), d) + Ico{width: 16 height: 16 draw_svg +: {svg: icon("file-pdf") color: pt_red}} + Label{padding: 0 width: Fill max_lines: 1 text_overflow: TextOverflow.Ellipsis text: shown_name(d.title) draw_text +: {color: pt_ink text_style: theme.font_regular{font_size: 12}}} + Label{padding: 0 text: doc_meta(d) draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11}}} } } - if route == "page" && current != nil { - View{width: Fill height: Fit flow: Right align: Align{y: 0.5} padding: Inset{left: 6 right: 20 top: 8 bottom: 8} - Back{text: "‹ Pages" on_click: || back_to_pages()} - View{width: Fill height: 1} - Label{padding: 0 text: "Page " + page_label(page_no) + " of " + page_count() draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 15}}} + } + GestureView{width: Fill height: 38 flow: Right align: Align{y: 0.5} padding: Inset{left: 8 right: 8} spacing: 10 cursor: MouseCursor.Hand on_tap: |x, y| open_from_device() + Ico{width: 16 height: 16 draw_svg +: {svg: icon("folder") color: pt_link}} + Label{padding: 0 text: "Open a PDF from this device" draw_text +: {color: pt_link text_style: theme.font_regular{font_size: 12}}} + } + } +} +fn combine_has(t, name){ + for it in t.combine.items { if it.name == name { return true } } + false +} +fn combine_row(t, i, it){ + let src = optional(st.covers, it.name, nil) + let n = item_count(it) + View{width: Fill height: 113 flow: Right align: Align{y: 0.5} padding: Inset{left: 10 right: 18} spacing: 18 + GestureView{width: 26 height: 60 align: Align{x: 0.5 y: 0.5} cursor: MouseCursor.Grab + on_pan: |dx, dy, phase| { if phase == 2 { combine_move(cur_tab(), i, dy) } } + Ico{width: 20 height: 20 draw_svg +: {svg: icon("grip") color: pt_icon}} + } + RoundedView{width: 70 height: 94 padding: 1 align: Align{x: 0.5 y: 0.5} show_bg: true draw_bg +: {color: #xffffff border_size: 1.0 border_color: pt_thumb_line border_radius: 0.5} + if src != nil { Image{width: Fill height: Fill fit: ImageFit.Smallest src: src} } + } + Label{padding: 0 width: Fill max_lines: 1 text_overflow: TextOverflow.Ellipsis text: shown_name(it.title) draw_text +: {color: pt_ink text_style: theme.font_regular{font_size: 13}}} + RoundedView{width: 178 height: 42 flow: Overlay show_bg: true draw_bg +: {color: pt_field border_size: 1.0 border_color: pick(n == nil, pt_red, pt_field_line) border_radius: 4.0} + TextInput{width: Fill height: Fill padding: Inset{left: 12 right: 76 top: 11 bottom: 10} text: it.range empty_text: "All" + on_change: |text| { let x = cur_tab() x.combine.items[i].range = text start_timeout(0.6, || render_combine()) } + draw_bg +: {color: #x00000000 color_hover: #x00000000 color_focus: #x00000000 color_empty: #x00000000 + border_color: #x00000000 border_color_hover: #x00000000 border_color_focus: #x00000000 border_color_empty: #x00000000 + border_size: 0.0} + draw_text +: {color: pt_ink2 color_hover: pt_ink2 color_focus: pt_ink color_empty: pt_ink2 color_empty_hover: pt_ink2 color_empty_focus: pt_faint text_style: theme.font_regular{font_size: 12}} + draw_selection.color: #x557dcc55} + View{width: Fill height: Fill flow: Right align: Align{x: 1.0 y: 0.5} padding: Inset{right: 12} + Label{padding: 0 text: pick(n == nil, "(?)", "(" + plural(n, "page", "pages") + ")") draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11.5}}} + } + } + IconBtn{icon_walk: Walk{width: 16 height: 16} draw_icon +: {svg: icon("close") color: pt_icon} on_click: || combine_remove(cur_tab(), i)} + } +} + +// The right panel in Comment mode: the threads. +fn thread_panel(){ + let t = cur_tab() + if t == nil || t.state != "ready" { return View{width: Fill height: 1} } + let count = 0 + if t.comments != nil { count = t.comments.len() } + View{width: right_w(t) height: Fill flow: Down + View{width: Fill height: 60 flow: Right align: Align{y: 0.5} padding: Inset{left: 20 right: 12} spacing: 10 + Label{padding: 0 text: "Comments" draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 14}}} + RoundedView{width: Fit height: 24 align: Align{x: 0.5 y: 0.5} padding: Inset{left: 8 right: 8} show_bg: true draw_bg +: {color: pt_hover border_radius: 12.0} + Label{padding: 0 text: "" + count draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 11.5}}} + } + Gap{} + IconBtn{icon_walk: Walk{width: 20 height: 20} draw_icon +: {svg: icon("more") color: pt_icon} on_click: || { let x = cur_tab() x.head_menu = !x.head_menu render_right() }} + } + Line{draw_bg.color: pt_line_soft} + if st.who.author == "" || st.naming { author_card() } + if t.head_menu { + View{width: Fill height: Fit flow: Flow.Right{wrap: true} padding: Inset{left: 16 right: 16 top: 8 bottom: 8} spacing: 8 + if st.who.author != "" { + Outline{height: 30 text: "Comment as " + st.who.author + "…" on_click: || { let x = cur_tab() x.head_menu = false st.naming = true st.name_draft = st.who.author render_right() }} } + if t.thread != nil { DangerBtn{height: 30 text: "Delete this comment" on_click: || delete_comment(cur_tab())} } + Outline{height: 30 text: "Cancel" on_click: || { let x = cur_tab() x.head_menu = false render_right() }} + } + } + ScrollYView{width: Fill height: Fill flow: Down padding: Inset{left: 12 right: 12 top: 16 bottom: 24} spacing: 14 + if t.pending != nil { pending_card(t) } + if t.comments == nil { + View{width: Fill height: Fit padding: Inset{left: 8} Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: pick(t.comments_state == "" || t.comments_state == "loading", "Reading the comments…", t.comments_state) draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12}}}} + } + if t.comments != nil && t.comments.len() == 0 && t.pending == nil { + View{width: Fill height: Fit padding: Inset{left: 8} Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: comment_hint(t) draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12 line_spacing: 1.4}}}} + } + if t.comments != nil { for c in t.comments { thread_card(t, c) } } + } + } +} +// Comments carry the person's name; asked once, kept in library.json. +fn author_card(){ + View{width: Fill height: Fit flow: Down padding: Inset{left: 16 right: 16 top: 14 bottom: 14} spacing: 8 + Label{padding: 0 text: "Comment as" draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + View{width: Fill height: Fit flow: Right align: Align{y: 0.5} spacing: 8 + Field{text: st.name_draft empty_text: "Your name" on_change: |text| { st.name_draft = text } on_return: |text| set_author(text)} + Primary{height: 36 text: "Save" on_click: || set_author(st.name_draft)} + } + Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: "Your comments and replies show this name." draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11.5}}} + } +} +fn comment_hint(t){ + if t.tool == "note" || t.tool == "textbox" { return "No comments yet. Tap the page where the " + pick(t.tool == "note", "note", "text box") + " goes." } + "No comments yet. Tap a line of text to " + pick(t.tool == "highlight", "highlight", pick(t.tool == "underline", "underline", "strike out")) + " it; tap twice for its paragraph." +} +fn avatar(name){ + RoundedView{width: 34 height: 34 align: Align{x: 0.5 y: 0.5} show_bg: true draw_bg +: {color: avatar_color(name) border_radius: 17.0} + Label{padding: 0 text: initials_of(name) draw_text +: {color: #x2a3140 text_style: theme.font_regular{font_size: 11.5}}} + } +} +fn avatar_color(name){ + let n = ("" + name).len() % 4 + if n == 0 { return #xc6def0 } + if n == 1 { return #xe9e0f6 } + if n == 2 { return #xf4dadf } + #xd9ecd9 +} +fn message(author, date, text, indent){ + View{width: Fill height: Fit flow: Right padding: Inset{left: indent} spacing: 16 + avatar(author) + View{width: Fill height: Fit flow: Down spacing: 8 padding: Inset{top: 6} + View{width: Fill height: Fit flow: Right align: Align{y: 0.5} + Label{padding: 0 width: Fill max_lines: 1 text_overflow: TextOverflow.Ellipsis text: pick(author == "", "Comment", author) draw_text +: {color: pt_ink text_style: theme.font_regular{font_size: 13}}} + Label{padding: 0 text: comment_when(date, time_now()) draw_text +: {color: pt_faint text_style: theme.font_regular{font_size: 11.5}}} + } + if text != "" { Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: text draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12.5 line_spacing: 1.45}}} } + } + } +} +fn thread_card(t, c){ + let open = t.thread == c.id + let st_label = status_label(optional(c, "status", "")) + RoundedView{width: Fill height: Fit flow: Down show_bg: true draw_bg +: {color: pt_card border_size: 1.0 border_color: pt_line border_radius: 7.0} + GestureView{width: Fill height: 50 flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 14} spacing: 12 cursor: MouseCursor.Hand + on_tap: |x, y| { let x = cur_tab() if x.thread == c.id { x.thread = nil render_right() render_canvas() } else { open_thread(x, c) } } + RoundedView{width: 16 height: 16 show_bg: true draw_bg +: {color: mark_color(optional(c, "color", "")) border_radius: 8.0}} + Label{padding: 0 text: "Page " + c.page draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12.5}}} + Gap{} + if !open { Label{padding: 0 text: comment_when(optional(c, "date", ""), time_now()) draw_text +: {color: pt_faint text_style: theme.font_regular{font_size: 11.5}}} } + Ico{width: 18 height: 18 draw_svg +: {svg: icon(pick(open, "chevron-up", "chevron-down")) color: pt_icon}} + } + if !open { + View{width: Fill height: Fit flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 18 bottom: 14} spacing: 16 + avatar(optional(c, "author", "")) + Label{padding: 0 width: Fill max_lines: 1 text_overflow: TextOverflow.Ellipsis text: pick(optional(c, "author", "") == "", "Comment", optional(c, "author", "")) draw_text +: {color: pt_ink text_style: theme.font_regular{font_size: 13}}} } - if route == "merge" { - View{width: Fill height: Fit flow: Right align: Align{y: 0.5} padding: Inset{left: 6 right: 10 top: 8} - if merge_step == 3 { Back{text: "‹ Library" on_click: || go_library()} } - else { Back{text: "Cancel" on_click: || go_library()} } + } + if open { + Line{draw_bg.color: pt_line_soft} + View{width: Fill height: Fit flow: Down padding: Inset{left: 16 right: 18 top: 14 bottom: 16} spacing: 18 + message(optional(c, "author", ""), optional(c, "date", ""), optional(c, "text", ""), 0) + for r in optional(c, "replies", []) { message(optional(r, "author", ""), optional(r, "date", ""), optional(r, "text", ""), 50) } + View{width: Fill height: Fit flow: Right padding: Inset{left: 50} spacing: 8 + Flat{height: 28 padding: Inset{left: 10 right: 10} text: pick(st_label == "", "Set status", st_label) + draw_bg +: {color: pick(st_label == "", #x00000000, pt_green_tint) border_size: 1.0 border_color: pick(st_label == "", pt_line, pt_green) border_radius: 3.0} + draw_text +: {color: pick(st_label == "", pt_muted, pt_green) color_hover: pick(st_label == "", pt_ink, pt_green) text_style: theme.font_regular{font_size: 11.5}} + on_click: || { let x = cur_tab() x.status_menu = !x.status_menu render_right() }} } - View{width: Fill height: Fit flow: Down spacing: 10 padding: Inset{left: 20 right: 20 top: 2 bottom: 14} - UiTitle{text: "Merge PDFs" draw_text +: {text_style: theme.font_bold{font_size: 24}}} - View{width: Fill height: Fit flow: Right spacing: 8 align: Align{y: 0.5} - for i name in ["Choose", "Order", "Done"] { - if i + 1 <= merge_step { DotOn{n.text: "" + (i + 1)} } else { Dot{n.text: "" + (i + 1)} } - if i + 1 == merge_step { StepNameOn{text: name} } else { StepName{text: name} } - if i < 2 { StepLine{} } + if t.status_menu { + View{width: Fill height: Fit flow: Flow.Right{wrap: true} padding: Inset{left: 50} spacing: 6 + for s in ["accepted", "rejected", "completed", "cancelled", "none"] { + Outline{height: 28 padding: Inset{left: 10 right: 10} text: pick(s == "none", "None", status_label(s)) draw_text +: {text_style: theme.font_regular{font_size: 11.5}} on_click: || set_status(cur_tab(), s)} } } } + reply_box(t, "Reply...", || post_reply(cur_tab())) + } + } + } +} +fn reply_box(t, hint, post){ + RoundedView{width: Fill height: 66 flow: Right align: Align{y: 0.5} padding: Inset{left: 4 right: 12} show_bg: true + draw_bg +: {color: pt_field border_size: 1.0 border_color: pt_field_line border_radius: 5.0} + TextInput{width: Fill height: Fill padding: Inset{left: 10 right: 10 top: 14 bottom: 12} text: t.reply empty_text: hint + on_change: |text| { let x = cur_tab() x.reply = text } + on_return: |text| { let x = cur_tab() x.reply = text post() } + draw_bg +: {color: #x00000000 color_hover: #x00000000 color_focus: #x00000000 color_empty: #x00000000 + border_color: #x00000000 border_color_hover: #x00000000 border_color_focus: #x00000000 border_color_empty: #x00000000 + border_size: 0.0} + draw_text +: {color: pt_ink color_hover: pt_ink color_focus: pt_ink color_empty: pt_faint color_empty_hover: pt_faint color_empty_focus: pt_faint text_style: theme.font_regular{font_size: 12.5}} + draw_selection.color: #x557dcc55} + Primary{width: 62 height: 35 padding: 0 text: "Post" on_click: || post()} + } +} +fn pending_card(t){ + RoundedView{width: Fill height: Fit flow: Down padding: 16 spacing: 14 show_bg: true draw_bg +: {color: pt_card border_size: 1.5 border_color: pt_accent border_radius: 7.0} + View{width: Fill height: Fit flow: Right align: Align{y: 0.5} spacing: 12 + RoundedView{width: 16 height: 16 show_bg: true draw_bg +: {color: mark_color(t.color) border_radius: 8.0}} + Label{padding: 0 width: Fill text: "New " + pick(t.pending.type == "note", "note", "text box") + " on page " + t.pending.page draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12.5}}} + IconBtn{width: 28 height: 28 icon_walk: Walk{width: 13 height: 13} draw_icon +: {svg: icon("close") color: pt_muted} on_click: || { let x = cur_tab() x.pending = nil render_right() render_canvas() }} + } + reply_box(t, pick(t.pending.type == "note", "Write the note...", "Write the text..."), || post_pending(cur_tab())) + } +} + +// The right panel in Fill & Sign and Edit. +fn side_panel(){ + let t = cur_tab() + if t == nil || t.state != "ready" { return View{width: Fill height: 1} } + if t.mode == "edit" { return edit_panel(t) } + fields_panel(t) +} +fn fields_panel(t){ + View{width: right_w(t) height: Fill flow: Down + View{width: Fill height: 50 flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 12} + Label{padding: 0 width: Fill text: "Fields" draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 13}}} + } + Line{draw_bg.color: pt_line_soft} + View{width: Fill height: 46 flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 16} + Label{padding: 0 width: 162 text: "Field name" draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11}}} + Label{padding: 0 text: "Value" draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11}}} + } + Line{draw_bg.color: pt_line_soft} + ScrollYView{width: Fill height: Fill flow: Down padding: Inset{top: 4 bottom: 8} + if t.fields_state != "" { + View{width: Fill height: Fit padding: Inset{left: 16 top: 10 right: 16} Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: pick(t.fields_state == "loading", "Reading the form…", t.fields_state) draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12}}}} + } + if t.fields != nil && t.fields.len() == 0 { + View{width: Fill height: Fit padding: Inset{left: 16 top: 10 right: 16} Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: "This PDF has no form fields." draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12}}}} } - if route == "split" && current != nil { - View{width: Fill height: Fit flow: Right align: Align{y: 0.5} padding: Inset{left: 6 right: 10 top: 8} - if split_result != nil { Back{text: "‹ Library" on_click: || go_library()} } - else { Back{text: "Cancel" on_click: || back_from_split()} } + if t.fields != nil { for f in t.fields { field_row(t, f) } } + } + Line{draw_bg.color: pt_line_soft} + View{width: Fill height: Fit flow: Down padding: Inset{left: 20 right: 20 top: 28 bottom: 28} spacing: 16 + if t.place != nil { place_editor(t) } + place_btn(t, "text", "text", "Add text") + place_btn(t, "date", "calendar", "Add date") + place_btn(t, "initials", "", "Add initials") + } + } +} +fn field_row(t, f){ + let on = t.field == f.name + let need = optional(f, "required", false) + let empty = field_empty(f) + View{width: Fill height: Fit flow: Down + GestureView{width: Fill height: 47 flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 16} cursor: MouseCursor.Hand on_tap: |x, y| select_field(cur_tab(), f) + Label{padding: 0 width: 162 max_lines: 1 text_overflow: TextOverflow.Ellipsis text: optional(f, "name", "") + pick(need, " *", "") draw_text +: {color: pick(need && empty, pt_red, pt_ink2) text_style: theme.font_regular{font_size: 12.5}}} + Label{padding: 0 width: Fill max_lines: 1 text_overflow: TextOverflow.Ellipsis text: pick(empty, "Empty", field_shown(f)) + pick(read_only(f), " (read-only)", "") draw_text +: {color: pick(empty, pt_faint, pt_ink2) text_style: theme.font_regular{font_size: 12.5}}} + } + if on { + View{width: Fill height: Fit flow: Down padding: Inset{left: 16 right: 16 bottom: 12} spacing: 8 + field_editor(t, f) + View{width: Fill height: Fit flow: Right spacing: 8 + Primary{height: 32 text: "Apply" on_click: || fill_field(cur_tab())} + Outline{height: 32 text: "Cancel" on_click: || { let x = cur_tab() x.field = nil render_right() render_canvas() }} } - View{width: Fill height: Fit flow: Down spacing: 10 padding: Inset{left: 20 right: 20 top: 2 bottom: 14} - UiTitle{text: "Split " + current.title draw_text +: {text_style: theme.font_bold{font_size: 24}}} - View{width: Fill height: Fit flow: Right spacing: 8 align: Align{y: 0.5} - for i name in ["Choose where", "Done"] { - if i + 1 <= split_step() { DotOn{n.text: "" + (i + 1)} } else { Dot{n.text: "" + (i + 1)} } - if i + 1 == split_step() { StepNameOn{text: name} } else { StepName{text: name} } - if i < 1 { StepLine{} } - } + } + } + } +} +fn field_editor(t, f){ + let kind = "" + optional(f, "type", "text") + let options = optional(f, "options", []) + if kind == "checkbox" || kind == "check" { + return View{width: Fill height: Fit flow: Right spacing: 8 + Outline{height: 32 text: "Checked" draw_bg +: {color: pick(t.field_on, pt_tint, pt_card)} on_click: || { let x = cur_tab() x.field_on = true render_right() }} + Outline{height: 32 text: "Not checked" draw_bg +: {color: pick(!t.field_on, pt_tint, pt_card)} on_click: || { let x = cur_tab() x.field_on = false render_right() }} + } + } + if options.len() > 0 { + return View{width: Fill height: Fit flow: Flow.Right{wrap: true} spacing: 6 + for o in options { + Outline{height: 30 text: "" + optional(o, "label", optional(o, "value", "")) draw_bg +: {color: pick(t.field_text == "" + optional(o, "value", ""), pt_tint, pt_card)} + on_click: || { let x = cur_tab() x.field_text = "" + optional(o, "value", "") render_right() }} + } + } + } + Field{text: t.field_text empty_text: "Type the value" on_change: |text| { let x = cur_tab() x.field_text = text } on_return: |text| { let x = cur_tab() x.field_text = text fill_field(x) }} +} + +fn place_btn(t, kind, ic, label){ + let on = t.place != nil && t.place.kind == kind + View{width: Fill height: 43 flow: Overlay + Outline{width: Fill height: 43 padding: Inset{left: 14 right: 14} spacing: 12 text: pick(kind == "initials", "", label) icon_walk: Walk{width: 20 height: 20} + draw_icon +: {svg: pick(ic == "", nil, icon(pick(ic == "", "text", ic))) color: pt_ink2} + draw_bg +: {color: pick(on, pt_tint, pt_card) border_color: pick(on, pt_accent, #x7fa6dd) border_size: 1.0} + draw_text +: {text_style: theme.font_regular{font_size: 12.5}} + align: Align{x: 0.0 y: 0.5} + on_click: || start_place(cur_tab(), kind)} + if kind == "initials" { + View{width: Fill height: Fill flow: Right align: Align{y: 0.5} padding: Inset{left: 14} spacing: 12 + if st.who.initials == "" { Ico{width: 20 height: 20 draw_svg +: {svg: icon("text") color: pt_ink2}} } + if st.who.initials != "" { Label{padding: 0 text: st.who.initials draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 13.5}}} } + Label{padding: 0 text: label draw_text +: {color: pt_ink text_style: theme.font_regular{font_size: 12.5}}} + } + } + } +} +fn place_editor(t){ + if t.place.kind == "date" { + return View{width: Fill height: Fit flow: Down spacing: 8 + Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: "The engine writes today's date where you tap." draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + } + } + View{width: Fill height: Fit flow: Down spacing: 8 + Label{padding: 0 text: pick(t.place.kind == "initials", "Your initials", "The text") draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + Field{text: t.place_text empty_text: pick(t.place.kind == "initials", "Initials", "Text") on_change: |text| { let x = cur_tab() x.place_text = text }} + Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: place_hint(t) draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11.5}}} + } +} +fn edit_panel(t){ + View{width: right_w(t) height: Fill flow: Down padding: 14 + RoundedView{width: Fill height: Fit flow: Down show_bg: true draw_bg +: {color: pt_card border_size: 1.0 border_color: pt_line border_radius: 6.0} + View{width: Fill height: 54 flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 8} + Label{padding: 0 width: Fill text: "Edit text" draw_text +: {color: pt_ink text_style: theme.font_regular{font_size: 12.5}}} + if t.edit != nil { IconBtn{icon_walk: Walk{width: 15 height: 15} draw_icon +: {svg: icon("close") color: pt_icon} on_click: || cancel_edit(cur_tab())} } + } + Line{draw_bg.color: pt_line_soft} + if t.edit == nil { + View{width: Fill height: Fit padding: 16 Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: pick(t.can_edit, "Click a paragraph on the page to edit its text.", "This PDF doesn't allow editing.") draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12 line_spacing: 1.4}}}} + } + if t.edit != nil { + View{width: Fill height: 52 flow: Right align: Align{y: 0.5} padding: Inset{left: 16} + Label{padding: 0 text: "Paragraph " + t.edit.n draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12.5}}} + } + Line{draw_bg.color: pt_line_soft} + View{width: Fill height: Fit flow: Down padding: Inset{left: 16 right: 16 top: 18 bottom: 20} spacing: 10 + Label{padding: 0 text: "Font" draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + RoundedView{width: Fill height: 40 flow: Right align: Align{y: 0.5} padding: Inset{left: 12 right: 12} show_bg: true draw_bg +: {color: pt_field border_size: 1.0 border_color: pt_field_line border_radius: 4.0} + Label{padding: 0 width: Fill max_lines: 1 text_overflow: TextOverflow.Ellipsis text: font_text(t.edit) draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 12}}} + } + View{width: Fill height: Fit flow: Right margin: Inset{top: 22} spacing: 14 + Primary{width: Fill height: 37 text: "Apply" on_click: || apply_edit(cur_tab())} + Outline{width: Fill height: 37 text: "Cancel" on_click: || cancel_edit(cur_tab())} } } } - }} - - findbar := View{visible: false width: Fill height: Fit padding: Inset{left: 16 right: 16 bottom: 10} - find := UiField{empty_text: "Find in the text" on_change: |text| set_query(text)} - } - - lib_box := View{width: Fill height: Fill - lib_pane := Pane{on_render: || { - View{width: Fill height: 1} - if engine_state == "missing" { EngineAlert{text.head.text: engine_headline() text.detail.text: engine_detail() text.note.text: engine_note} } - if lib_note != nil { - Alert{ - Glyph{text: ICON_WARN draw_text +: {color: ui_danger text_style: theme.font_icons{font_size: 18}}} - View{width: Fill height: Fit flow: Down spacing: 6 - Head{text: lib_note.head} - UiCaption{text: lib_note.detail} - Label{width: Fill padding: 0 flow: Flow.Right{wrap: true} text: lib_note.note draw_text +: {color: ui_muted text_style: theme.font_code{font_size: 11}}} - } - UiButton{text: "OK" on_click: || { lib_note = nil refresh() }} + } + } +} + +// What a tab shows when its PDF isn't open: opening, damaged, protected, +// no engine, too many open. +fn notice_view(){ + let t = cur_tab() + if t == nil || t.state == "ready" { return View{width: 1 height: 1} } + if t.state == "opening" { + return View{width: Fit height: Fit flow: Down align: Align{x: 0.5} spacing: 12 + LoadingSpinner{width: 30 height: 30} + Label{padding: 0 text: "Opening " + shown_name(t.title) + "…" draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12.5}}} + } + } + let head = "Couldn't open this PDF" + let detail = t.error + let ic = "warning" + if t.state == "protected" { + head = "This PDF is protected with a password" + detail = "PDF Tools doesn't open password-protected PDFs yet: OctoSense keeps passwords on its own sheets, and there isn't one for PDFs." + ic = "lock" + } + if t.state == "missing" { + head = engine_headline() + detail = engine_detail() + } + if t.state == "full" { + head = "PDF Tools has " + MAX_OPEN + " PDFs open" + detail = "Close another PDF's tab, then open this one again." + } + RoundedShadowView{width: 460 height: Fit flow: Down padding: 28 spacing: 12 + draw_bg +: {color: pt_card border_size: 1.0 border_color: pt_line border_radius: 8.0 shadow_color: pt_shadow shadow_radius: 8.0 shadow_offset: vec2(0.0, 1.0)} + Ico{width: 30 height: 30 draw_svg +: {svg: icon(ic) color: pick(ic == "lock", pt_icon, pt_red)}} + Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: head draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 15}}} + if t.state == "damaged" { Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: "It may be damaged, or not a PDF at all. The engine said:" draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12 line_spacing: 1.4}}} } + if detail != "" { Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: detail draw_text +: {color: pick(t.state == "damaged", pt_ink2, pt_muted) text_style: pick(t.state == "damaged", theme.font_code{font_size: 11}, theme.font_regular{font_size: 12 line_spacing: 1.4})}} } + if t.state == "missing" && st.engine_note != "" { Code{text: st.engine_note} } + View{width: Fill height: Fit flow: Right margin: Inset{top: 8} spacing: 10 + Outline{text: "Close this tab" on_click: || close_tab(cur_tab(), true)} + if t.state == "full" { Primary{text: "Try again" on_click: || load_tab(cur_tab())} } + } + } +} + +// Home: open a PDF from this device, and the recent ones. +fn home_cols(){ pick(st.width < 940, 2, 3) } +fn home_view(){ + let cols = home_cols() + let days = days_now() + View{width: Fill height: Fit flow: Down align: Align{x: 0.5} padding: Inset{top: 31 bottom: 48 left: 24 right: 24} + Ico{width: 46 height: 54 draw_svg +: {svg: icon("pdf-hero") color: pt_accent}} + View{width: Fit height: Fit padding: Inset{top: 13} Label{padding: 0 text: "PDF Tools" draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 28}}}} + View{width: Fit height: Fit flow: Right align: Align{y: 0.5} margin: Inset{top: 22} spacing: 20 + Primary{height: 51 padding: Inset{left: 28 right: 34} spacing: 14 icon_walk: Walk{width: 24 height: 24} draw_icon +: {svg: icon("folder")} + text: pick(st.importing, "Choosing a PDF…", "Open a PDF from this device") + draw_text +: {text_style: theme.font_regular{font_size: 13.5}} + on_click: || open_from_device()} + if st.files_ok && import_limit() != "" { Label{padding: 0 text: import_limit() draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11.5}}} } + if st.files_known && !st.files_ok { Label{padding: 0 text: "Not available here" draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11.5}}} } + } + if st.engine == "missing" || st.engine == "old" { home_alert(engine_headline(), engine_detail(), st.engine_note, false) } + if st.alert != nil { home_alert(st.alert.head, st.alert.detail, st.alert.note, true) } + if st.space_full { home_alert("PDF Tools' storage is full", "Remove a PDF to make room: page pictures and new PDFs can't be saved until there's space.", "", false) } + if st.docs.len() == 0 { + RoundedView{width: 520 height: Fit flow: Down align: Align{x: 0.5} margin: Inset{top: 40} padding: 28 spacing: 10 show_bg: true + draw_bg +: {color: pt_card border_size: 1.0 border_color: pt_line border_radius: 6.0} + Label{padding: 0 text: "No PDFs here yet" draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 15}}} + Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} align: Align{x: 0.5} text: "Open a PDF from this device to read it, search it, comment on it, fill and sign it, or rearrange its pages. PDF Tools keeps its own copy." draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12 line_spacing: 1.45}}} + } + } + if st.docs.len() > 0 { + View{width: cols * 268 + (cols - 1) * 23 height: Fit flow: Down margin: Inset{top: 34} spacing: 13 + View{width: Fit height: Fit padding: Inset{left: 2 bottom: 3} Label{padding: 0 text: "Recent" draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 15}}}} + for r in ceil(st.docs.len() / cols) { + View{width: Fill height: Fit flow: Right spacing: 23 + for c in cols { if r * cols + c < st.docs.len() { doc_card(st.docs[r * cols + c], days) } } } } - if space_full { SpaceAlert{} } - if docs.len() == 0 { - UiCard{align: Align{x: 0.5} padding: 28 spacing: 14 - AppIcon{width: 64 height: 64 name: "pdf"} - Label{padding: 0 text: "No PDFs here yet" draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 20}}} - Label{width: Fill padding: 0 flow: Flow.Right{wrap: true} align: Align{x: 0.5} - text: "Open a PDF from this device to read it, split it, merge it with others or copy its text. PDF Tools keeps its own copy." - draw_text +: {color: ui_muted text_style: theme.font_regular{font_size: 14 line_spacing: 1.4}}} - if files_ok { UiPrimary{text: pick(importing, "Choosing a PDF…", "Open a PDF from this device") on_click: || open_from_device()} } - if files_ok && max_import > 0 { UiCaption{width: Fit text: import_limit()} } - if files_known && !files_ok { - Label{width: Fill padding: 0 flow: Flow.Right{wrap: true} align: Align{x: 0.5} - text: "This OctoSense can't open files from the device for PDF Tools." - draw_text +: {color: ui_muted text_style: theme.font_regular{font_size: 13 line_spacing: 1.4}}} - } - } - } - if docs.len() > 0 { - GestureView{width: Fill height: Fit flow: Right align: Align{y: 0.5} spacing: 12 padding: Inset{left: 18 right: 16 top: 6 bottom: 6} on_tap: |x, y| open_from_device() - Glyph{text: ICON_OPEN draw_text +: {color: ui_link text_style: theme.font_icons{font_size: 15}}} - Label{width: Fill padding: 0 text: pick(importing, "Choosing a PDF…", "Open a PDF from this device") draw_text +: {color: ui_link text_style: theme.font_bold{font_size: 14}}} - if files_ok && !importing && max_import > 0 { Label{padding: 0 text: import_limit() draw_text +: {color: ui_muted text_style: theme.font_regular{font_size: 12}}} } - if files_known && !files_ok { - RoundedView{width: Fit height: Fit padding: Inset{left: 8 right: 8 top: 3 bottom: 3} show_bg: true draw_bg +: {color: ui_field border_radius: 8.0} - Label{padding: 0 text: "Not available here" draw_text +: {color: ui_muted text_style: theme.font_bold{font_size: 11}}} - } - } - } - List{ - for i d in docs { - GestureView{width: Fill height: Fit flow: Right align: Align{y: 0.5} spacing: 14 padding: Inset{left: 16 right: 14 top: 10 bottom: 10} on_tap: |x, y| open_doc(d) - Cover{ - if optional(cover_src, d.name, nil) != nil { Image{width: Fill height: Fill fit: ImageFit.Smallest src: optional(cover_src, d.name, nil)} } - else { AppIcon{width: 26 height: 26 name: "file-pdf"} } - } - if d.error != "" { RowText{name.text: d.title meta.text: doc_meta(d) meta.draw_text.color: ui_danger} } - else { RowText{name.text: d.title meta.text: doc_meta(d)} } - Glyph{text: ICON_NEXT} - } - if i < docs.len() - 1 { RowLine{} } - } + } + } + } +} +fn home_alert(head, detail, code, dismiss){ + RoundedView{width: 640 height: Fit flow: Right margin: Inset{top: 22} padding: 18 spacing: 14 show_bg: true + draw_bg +: {color: pt_card border_size: 1.0 border_color: pt_line border_radius: 6.0} + Ico{width: 22 height: 22 draw_svg +: {svg: icon("warning") color: pt_red}} + View{width: Fill height: Fit flow: Down spacing: 6 + Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: head draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 13}}} + Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: detail draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12 line_spacing: 1.4}}} + if code != "" { Code{text: code} } + } + if dismiss { Outline{height: 30 text: "OK" on_click: || { st.alert = nil render_home() }} } + } +} +fn doc_card(d, days){ + let src = optional(st.covers, d.name, nil) + GestureView{width: 268 height: Fit cursor: MouseCursor.Hand on_tap: |x, y| open_doc(d) + RoundedView{width: Fill height: Fit flow: Down show_bg: true draw_bg +: {color: pt_card border_size: 1.0 border_color: pt_line border_radius: 3.0} + View{width: Fill height: 207 flow: Down align: Align{x: 0.5} padding: Inset{top: 19} + RoundedView{width: 209 height: 188 padding: 1 show_bg: true draw_bg +: {color: #xffffff border_size: 1.0 border_color: pt_thumb_line border_radius: 0.0} + if src != nil { Image{width: 207 height: Fit fit: ImageFit.Horizontal src: src} } + if src == nil { + View{width: Fill height: Fill align: Align{x: 0.5 y: 0.5} + Ico{width: 34 height: 34 draw_svg +: {svg: icon(pick(d.locked, "lock", pick(d.error != "", "warning", "file-pdf"))) color: #xb0b3b8}} } } - }} - } - pages_box := View{visible: false width: Fill height: Fill - pages_pane := Pane{on_render: || { - View{width: Fill height: 1} - if engine_state == "missing" { EngineAlert{text.head.text: engine_headline() text.detail.text: engine_detail() text.note.text: engine_note} } - if space_full { SpaceAlert{} } - if engine_state != "missing" { - if doc_error != "" { - Alert{ - Glyph{text: ICON_WARN draw_text +: {color: ui_danger text_style: theme.font_icons{font_size: 18}}} - View{width: Fill height: Fit flow: Down spacing: 6 - Head{text: "Couldn't open this PDF"} - UiCaption{text: "It may be damaged, or not a PDF at all."} - Label{width: Fill padding: 0 flow: Flow.Right{wrap: true} text: doc_error draw_text +: {color: ui_muted text_style: theme.font_code{font_size: 11}}} - } - } - } - if doc_error == "" && info == nil { UiCaption{text: "Opening…" padding: Inset{left: 4}} } - } - if engine_state != "missing" && info != nil { - for row in rows_of(first_n(range_list(page_count()), shown_pages), columns) { - View{width: Fill height: Fit flow: Right spacing: 12 - for p in row { - GestureView{width: Fill height: Fit flow: Down spacing: 6 align: Align{x: 0.5} on_tap: |x, y| open_page(p) - Tile{height: tile_h() - if thumb_of(p) != nil { Image{width: Fill height: Fill fit: ImageFit.Smallest src: thumb_of(p)} } - else { TileLabel{text: pick(optional(thumb_failed, "p" + p, nil) != nil, "Can't draw this page", "…")} } - } - TileLabel{text: page_label(p)} - } - } - for k in columns - row.len() { View{width: Fill height: 1} } - } - } - if page_count() > shown_pages { - UiPill{text: "Show " + min(PAGE_BATCH, page_count() - shown_pages) + " more pages" on_click: || more_pages()} - } } - }} - } - text_box := View{visible: false width: Fill height: Fill - text_pane := Pane{on_render: || { - View{width: Fill height: 1} - if engine_state == "missing" { EngineAlert{text.head.text: engine_headline() text.detail.text: engine_detail() text.note.text: engine_note} } - if engine_state != "missing" { - if doc_error != "" { - Alert{ - Glyph{text: ICON_WARN draw_text +: {color: ui_danger text_style: theme.font_icons{font_size: 18}}} - View{width: Fill height: Fit flow: Down spacing: 6 - Head{text: "Couldn't open this PDF"} - UiCaption{text: "It may be damaged, or not a PDF at all."} - Label{width: Fill padding: 0 flow: Flow.Right{wrap: true} text: doc_error draw_text +: {color: ui_muted text_style: theme.font_code{font_size: 11}}} - } - } - } - if doc_error == "" && info == nil { UiCaption{text: "Opening…" padding: Inset{left: 4}} } + } + Line{} + View{width: Fill height: Fit flow: Down padding: Inset{left: 14 right: 12 top: 13 bottom: 12} spacing: 6 + View{width: Fill height: Fit flow: Right align: Align{y: 0.5} spacing: 10 + Ico{width: 18 height: 18 draw_svg +: {svg: icon("file-pdf") color: pt_red}} + Label{padding: 0 width: Fill max_lines: 1 text_overflow: TextOverflow.Ellipsis text: shown_name(d.title) draw_text +: {color: pt_ink text_style: theme.font_regular{font_size: 12}}} } - if engine_state != "missing" && info != nil { - if text_state == "loading" { UiCaption{text: "Reading the text…" padding: Inset{left: 4}} } - if text_state == "error" { - Alert{ - Glyph{text: ICON_WARN draw_text +: {color: ui_danger text_style: theme.font_icons{font_size: 18}}} - View{width: Fill height: Fit flow: Down spacing: 6 - Head{text: "Couldn't read the text"} - UiCaption{text: text_note} - } - } - } - if text_pages != nil { - if text_is_empty() { - UiCard{ - Head{text: "This PDF has no text to copy"} - UiCaption{text: "Its pages are probably pictures, such as a scan."} - } - } - if !text_is_empty() { - if query != "" { UiCaption{padding: Inset{left: 4} text: pick(text_hits().len() == 0, "No page contains “" + query + "”.", plural(text_hits().len(), "page contains", "pages contain") + " “" + query + "”.")} } - if page_count() > MAX_TEXT { UiCaption{padding: Inset{left: 4} text: "Showing the text of the first " + MAX_TEXT + " pages."} } - for t in first_n(text_hits(), shown_text) { - UiCard{spacing: 8 - Label{padding: 0 text: "PAGE " + page_label(t.page) draw_text +: {color: ui_muted text_style: theme.font_bold{font_size: 11}}} - Body{text: pick(t.text.trim() == "", "No text on this page.", t.text.trim())} - } - } - if text_hits().len() > shown_text { - UiPill{text: "Show more pages" on_click: || { shown_text = shown_text + TEXT_BATCH refresh() }} - } - } - } + View{width: Fit height: Fit padding: Inset{left: 28} Label{padding: 0 max_lines: 1 text_overflow: TextOverflow.Ellipsis text: doc_meta(d) draw_text +: {color: pick(d.error != "", pt_red, pt_muted) text_style: theme.font_regular{font_size: 11}}}} + View{width: Fill height: Fit flow: Right align: Align{y: 0.5} padding: Inset{left: 28} + Label{padding: 0 width: Fill text: opened_text(d, days) draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11}}} + IconBtn{width: 24 height: 22 icon_walk: Walk{width: 15 height: 15} draw_icon +: {svg: icon("trash") color: pt_faint} on_click: || ask_remove(d)} } - }} - } - info_box := View{visible: false width: Fill height: Fill - info_pane := Pane{on_render: || { - View{width: Fill height: 1} - if engine_state == "missing" { EngineAlert{text.head.text: engine_headline() text.detail.text: engine_detail() text.note.text: engine_note} } - if engine_state != "missing" { - if doc_error != "" { - Alert{ - Glyph{text: ICON_WARN draw_text +: {color: ui_danger text_style: theme.font_icons{font_size: 18}}} - View{width: Fill height: Fit flow: Down spacing: 6 - Head{text: "Couldn't open this PDF"} - UiCaption{text: "It may be damaged, or not a PDF at all."} - Label{width: Fill padding: 0 flow: Flow.Right{wrap: true} text: doc_error draw_text +: {color: ui_muted text_style: theme.font_code{font_size: 11}}} - } - } + if st.removing == d.name { + View{width: Fill height: Fit flow: Down spacing: 8 padding: Inset{top: 6} + Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: "Remove it from PDF Tools' storage?" draw_text +: {color: pt_ink2 text_style: theme.font_regular{font_size: 11.5}}} + View{width: Fill height: Fit flow: Right spacing: 8 + DangerBtn{height: 28 text: "Remove" on_click: || remove_doc(d)} + Outline{height: 28 text: "Keep" on_click: || { st.removing = "" render_home() }} } - if doc_error == "" && info == nil { UiCaption{text: "Opening…" padding: Inset{left: 4}} } + } + } + } + } + } +} + +// The status line: whether the open PDF is saved, and the storage used. +fn status_view(){ + let t = cur_tab() + let used = used_text(library_bytes()) + if t == nil { + return View{width: Fill height: Fill flow: Right align: Align{y: 0.5} padding: Inset{left: 20 right: 29} spacing: 20 + Label{padding: 0 text: used draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11}}} + RoundedView{width: Fill max_width: 1110 height: 6 flow: Right show_bg: true draw_bg +: {color: pt_line_soft border_radius: 3.0} + RoundedView{width: max(6, floor(min(1, library_bytes() / QUOTA) * min(1110, st.width - 230))) height: Fill show_bg: true draw_bg +: {color: pick(st.space_full, pt_red, pt_accent) border_radius: 3.0}} + } + } + } + let state = pick(t.saving, "Saving…", pick(t.edited, "Edited", "Saved")) + if t.state != "ready" { state = "" } + View{width: Fill height: Fill flow: Right align: Align{y: 0.5} padding: Inset{left: 33 right: 29} spacing: 11 + if state != "" { RoundedView{width: 12 height: 12 show_bg: true draw_bg +: {color: pick(t.edited || t.saving, pt_orange, pt_green) border_radius: 6.0}} } + Label{padding: 0 text: state draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11}}} + if st.space_full { View{width: Fit height: Fit padding: Inset{left: 12} Label{padding: 0 text: "Storage is full: remove a PDF to make room" draw_text +: {color: pt_red text_style: theme.font_regular{font_size: 11}}}} } + Gap{} + Label{padding: 0 text: used draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 11}}} + } +} + +// Closing a tab with unsaved changes. +fn dialog_view(){ + let t = nil + if st.confirm != nil { t = tab_named(st.confirm) } + if t == nil { return View{width: 1 height: 1} } + View{width: Fill height: Fill flow: Overlay + GestureView{width: Fill height: Fill on_tap: |x, y| {} + SolidView{width: Fill height: Fill draw_bg.color: pt_scrim} + } + View{width: Fill height: Fill align: Align{x: 0.5 y: 0.4} + RoundedShadowView{width: 440 height: Fit flow: Down padding: 26 spacing: 12 + draw_bg +: {color: pt_card border_size: 1.0 border_color: pt_line border_radius: 8.0 shadow_color: #x00000044 shadow_radius: 18.0 shadow_offset: vec2(0.0, 4.0)} + Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: "Save the changes to “" + shown_name(t.title) + "”?" draw_text +: {color: pt_ink text_style: theme.font_bold{font_size: 14}}} + Label{padding: 0 width: Fill flow: Flow.Right{wrap: true} text: "Your changes are in PDF Tools' open copy. If you close without saving, they're lost." draw_text +: {color: pt_muted text_style: theme.font_regular{font_size: 12 line_spacing: 1.4}}} + View{width: Fill height: Fit flow: Right margin: Inset{top: 10} spacing: 10 + Outline{text: "Don't save" on_click: || close_tab(tab_named(st.confirm), true)} + Gap{} + Outline{text: "Cancel" on_click: || { st.confirm = nil render_dialog() }} + Primary{text: "Save" on_click: || save_and_close(tab_named(st.confirm))} } - if engine_state != "missing" && info != nil { - List{padding: Inset{top: 6 bottom: 6} - for i row in info_rows() { - View{width: Fill height: Fit flow: Right spacing: 12 padding: Inset{left: 18 right: 18 top: 10 bottom: 10} - Label{width: 112 padding: 0 text: row[0] draw_text +: {color: ui_muted text_style: theme.font_regular{font_size: 14}}} - Label{width: Fill padding: 0 flow: Flow.Right{wrap: true} text: row[1] draw_text +: {color: ui_ink text_style: theme.font_regular{font_size: 14 line_spacing: 1.35}}} - } - if i < info_rows().len() - 1 { InfoLine{} } - } - } + } + } + } +} + +// ---- the window ------------------------------------------------------------- + +SolidView{width: Fill height: Fill flow: Overlay draw_bg.color: pt_desk + View{width: Fill height: Fill flow: Down + tabs := SolidView{width: Fill height: TAB_H draw_bg.color: pt_tabs on_render: || { tabs_view() }} + home := SolidView{width: Fill height: Fill flow: Down draw_bg.color: pt_home + home_pane := ScrollYView{width: Fill height: Fill flow: Down on_render: || { home_view() }} + } + doc := View{visible: false width: Fill height: Fill flow: Down + modebar := SolidView{width: Fill height: MODE_H draw_bg.color: pt_bar on_render: || { modebar_view() }} + Line{} + View{width: Fill height: Fill flow: Right + rail := SolidView{width: RAIL_W height: Fill draw_bg.color: pt_rail on_render: || { rail_view() }} + VLine{} + panel_narrow := SolidView{visible: false width: PANEL_W height: Fill flow: Down draw_bg.color: pt_panel + panel_head := View{width: Fill height: Fit on_render: || { panel_head_view() }} + thumbs := ScrollYView{width: Fill height: Fill flow: Down on_render: || { thumbs_view() }} } - }} - } - merge_box := View{visible: false width: Fill height: Fill - merge_pane := Pane{on_render: || { - View{width: Fill height: 1} - if engine_state == "missing" { EngineAlert{text.head.text: engine_headline() text.detail.text: engine_detail() text.note.text: engine_note} } - if space_full { SpaceAlert{} } - if merge_step == 1 { - UiCaption{padding: Inset{left: 4} text: "Choose the PDFs to put together, in the order they should go. You can change the order next."} - List{ - for i d in docs { - GestureView{width: Fill height: Fit flow: Right align: Align{y: 0.5} spacing: 14 padding: Inset{left: 16 right: 16 top: 10 bottom: 10} on_tap: |x, y| toggle_pick(d) - Cover{ - if optional(cover_src, d.name, nil) != nil { Image{width: Fill height: Fill fit: ImageFit.Smallest src: optional(cover_src, d.name, nil)} } - else { AppIcon{width: 26 height: 26 name: "file-pdf"} } - } - if d.error != "" { RowText{name.text: d.title meta.text: doc_meta(d) meta.draw_text.color: ui_danger} } - else { RowText{name.text: d.title meta.text: doc_meta(d)} } - if has(picked, d.name) { MarkOn{n.text: "" + (position(picked, d.name) + 1)} } - else if d.error == "" { Mark{} } - } - if i < docs.len() - 1 { RowLine{} } - } - } + panel_wide := SolidView{visible: false width: LIST_W height: Fill flow: Down draw_bg.color: pt_panel + list_head := View{width: Fill height: Fit on_render: || { list_head_view() }} + // A view whose on_render starts hidden stays hidden: each + // region shows and hides through a wrapper. + outline_box := View{visible: false width: Fill height: Fill + outline := ScrollYView{width: Fill height: Fill flow: Down on_render: || { outline_view() }} } - if merge_step == 2 { - UiCaption{padding: Inset{left: 4} text: "The pages go in this order. Move a file up or down to change it."} - List{ - for i name in picked { - View{width: Fill height: Fit flow: Right align: Align{y: 0.5} spacing: 10 padding: Inset{left: 12 right: 6 top: 10 bottom: 10} - Label{width: 16 padding: 0 align: Align{x: 0.5} text: "" + (i + 1) draw_text +: {color: ui_muted text_style: theme.font_bold{font_size: 14}}} - Cover{ - if optional(cover_src, name, nil) != nil { Image{width: Fill height: Fill fit: ImageFit.Smallest src: optional(cover_src, name, nil)} } - else { AppIcon{width: 26 height: 26 name: "file-pdf"} } - } - RowText{name.text: title_of(name) meta.text: doc_meta(doc_named(name))} - View{width: Fit height: Fit flow: Right spacing: 0 - if i > 0 { IconAct{text: ICON_UP on_click: || move_pick(i, -1)} } else { View{width: 36 height: 1} } - if i < picked.len() - 1 { IconAct{text: ICON_DOWN on_click: || move_pick(i, 1)} } else { View{width: 36 height: 1} } - IconAct{text: ICON_REMOVE on_click: || remove_pick(name)} - } - } - if i < picked.len() - 1 { RowLine{} } - } - } - if merge_pages() != nil { UiCaption{padding: Inset{left: 4} text: "One PDF of " + plural(merge_pages(), "page", "pages") + ", saved in your library."} } - if merge_error != "" { UiCaption{padding: Inset{left: 4} text: merge_error draw_text +: {color: ui_danger}} } + notes_box := View{visible: false width: Fill height: Fill + notes := ScrollYView{width: Fill height: Fill flow: Down on_render: || { notes_view() }} } - if merge_step == 3 && merge_result != nil { - UiCard{align: Align{x: 0.5} padding: 28 spacing: 14 - Glyph{text: ICON_DONE draw_text +: {color: GREEN text_style: theme.font_icons{font_size: 36}}} - Label{padding: 0 text: "Merged into one PDF" draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 20}}} - Cover{width: 120 height: 156 - if optional(cover_src, merge_result.name, nil) != nil { Image{width: Fill height: Fill fit: ImageFit.Smallest src: optional(cover_src, merge_result.name, nil)} } - else { AppIcon{width: 40 height: 40 name: "file-pdf"} } - } - Label{padding: 0 text: merge_result.title draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 16}}} - UiCaption{width: Fit text: doc_meta(merge_result)} - } + results_box := View{visible: false width: Fill height: Fill + results := ScrollYView{width: Fill height: Fill flow: Down on_render: || { results_view() }} } - }} - } - split_box := View{visible: false width: Fill height: Fill - split_pane := Pane{on_render: || { - View{width: Fill height: 1} - if space_full { SpaceAlert{} } - if current != nil && split_result == nil { - Segments{ - if split_mode == "every" { SegmentOn{text: "Every few pages"} } else { Segment{text: "Every few pages" on_click: || set_split_mode("every")} } - if split_mode == "at" { SegmentOn{text: "Where I choose"} } else { Segment{text: "Where I choose" on_click: || set_split_mode("at")} } - } - if split_mode == "every" { - UiCard{flow: Right align: Align{y: 0.5} spacing: 12 - View{width: Fill height: Fit flow: Down spacing: 4 - Head{text: "Pages in each file"} - UiCaption{text: "The last file takes what is left."} - } - IconAct{text: "\u{f068}" on_click: || set_every(split_every - 1)} - Label{width: 36 padding: 0 align: Align{x: 0.5} text: "" + split_every draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 20}}} - IconAct{text: "\u{f067}" on_click: || set_every(split_every + 1)} - } - } - if split_mode == "at" { - UiCaption{padding: Inset{left: 4} text: "Tap each page that should start a new file. Tap it again to undo."} - for row in rows_of(range_list(page_count()), columns) { - View{width: Fill height: Fit flow: Right spacing: 12 - for p in row { - GestureView{width: Fill height: Fit flow: Down spacing: 6 align: Align{x: 0.5} on_tap: |x, y| toggle_cut(p) - if has(split_cuts, p) { - TileCut{height: tile_h() - if thumb_of(p) != nil { Image{width: Fill height: Fill fit: ImageFit.Smallest src: thumb_of(p)} } - else { TileLabel{text: "…"} } - } - RoundedView{width: Fit height: Fit flow: Right spacing: 6 align: Align{y: 0.5} padding: Inset{left: 9 right: 9 top: 3 bottom: 3} show_bg: true draw_bg +: {color: ui_primary border_radius: 9.0} - Glyph{text: ICON_CUT draw_text +: {color: #xffffff text_style: theme.font_icons{font_size: 9}}} - Label{padding: 0 text: "File " + (position(split_cuts, p) + 2) + " starts here" draw_text +: {color: #xffffff text_style: theme.font_bold{font_size: 11}}} - } - } - if !has(split_cuts, p) { - Tile{height: tile_h() - if thumb_of(p) != nil { Image{width: Fill height: Fill fit: ImageFit.Smallest src: thumb_of(p)} } - else { TileLabel{text: "…"} } - } - TileLabel{text: page_label(p)} - } - } - } - for k in columns - row.len() { View{width: Fill height: 1} } - } - } - } - List{padding: Inset{top: 4 bottom: 4} - View{width: Fill height: Fit padding: Inset{left: 18 right: 18 top: 10 bottom: 4} - Head{text: pick(split_ranges().len() < 2, "Makes no new files yet", "Makes " + split_ranges().len() + " files")} - } - for k r in split_ranges() { - View{width: Fill height: Fit flow: Right spacing: 12 padding: Inset{left: 18 right: 18 top: 8 bottom: 8} - Label{width: Fill padding: 0 max_lines: 1 text: part_title(k + 1) + ".pdf" draw_text +: {color: ui_ink text_style: theme.font_regular{font_size: 14}}} - Label{padding: 0 text: range_text(r) draw_text +: {color: ui_muted text_style: theme.font_regular{font_size: 13}}} - } - } - } - if split_replaces() > 0 { UiCaption{padding: Inset{left: 4} text: "Replaces " + plural(split_replaces(), "file", "files") + " of the same name in your library."} } - if split_error != "" { UiCaption{padding: Inset{left: 4} text: split_error draw_text +: {color: ui_danger}} } } - if split_result != nil { - UiCard{align: Align{x: 0.5} padding: 24 spacing: 10 - Glyph{text: ICON_DONE draw_text +: {color: GREEN text_style: theme.font_icons{font_size: 36}}} - Label{padding: 0 text: "Split into " + split_result.len() + " files" draw_text +: {color: ui_ink text_style: theme.font_bold{font_size: 20}}} - UiCaption{width: Fit text: "They are in your library."} + line_left := VLine{visible: false} + View{width: Fill height: Fill flow: Down + toolrow := View{width: Fill height: Fit flow: Down on_render: || { toolrow_view() }} + View{width: Fill height: Fill flow: Overlay + canvas := ScrollXYView{width: Fill height: Fill flow: Down align: Align{x: 0.5} padding: Inset{top: PAGE_TOP bottom: 92 left: 24 right: 24} on_render: || { canvas_view() }} + grid_box := View{visible: false width: Fill height: Fill + grid := ScrollYView{width: Fill height: Fill flow: Down align: Align{x: 0.5} padding: Inset{left: 20 right: 20 top: 14 bottom: 92} on_render: || { grid_view() }} } - List{ - for i f in split_result { - GestureView{width: Fill height: Fit flow: Right align: Align{y: 0.5} spacing: 14 padding: Inset{left: 16 right: 14 top: 10 bottom: 10} on_tap: |x, y| { let d = doc_named(f.name) if d != nil { open_doc(d) } } - Cover{ - if optional(cover_src, f.name, nil) != nil { Image{width: Fill height: Fill fit: ImageFit.Smallest src: optional(cover_src, f.name, nil)} } - else { AppIcon{width: 26 height: 26 name: "file-pdf"} } - } - RowText{name.text: title_of(f.name) meta.text: range_text([f.first, f.last])} - Glyph{text: ICON_NEXT} - } - if i < split_result.len() - 1 { RowLine{} } - } + combine_box := View{visible: false width: Fill height: Fill + combine := ScrollYView{width: Fill height: Fill flow: Down align: Align{x: 0.5} padding: Inset{top: 42 bottom: 40 left: 24 right: 24} on_render: || { combine_view() }} } - } - }} - } - - stage_box := View{visible: false width: Fill height: Fill - stage := View{width: Fill height: Fill flow: Overlay padding: Inset{left: 12 right: 12} on_render: || { - RoundedView{width: Fill height: Fill show_bg: true draw_bg +: {color: ui_field border_radius: 14.0}} - GestureView{width: Fill height: Fill align: Align{x: 0.5 y: 0.5} padding: 18 - on_swipe: |dx, dy| { if dx < 0 { step_page(1) } else if dx > 0 { step_page(-1) } } - if view_src != nil { Image{width: Fill height: Fill fit: ImageFit.Smallest src: view_src} } - else if thumb_of(page_no) != nil { Image{width: Fill height: Fill fit: ImageFit.Smallest src: thumb_of(page_no)} } - else { UiCaption{width: Fit text: pick(view_note == "", "Drawing the page…", view_note)} } - } - if view_src == nil && view_note != "" && thumb_of(page_no) != nil { - View{width: Fill height: Fill align: Align{x: 0.5 y: 1.0} padding: 16 - UiCaption{width: Fit text: view_note} + notice_box := View{visible: false width: Fill height: Fill + notice := View{width: Fill height: Fill flow: Down align: Align{x: 0.5 y: 0.4} padding: 24 on_render: || { notice_view() }} + } + pill := View{width: Fill height: Fill flow: Down align: Align{x: 0.5 y: 1.0} padding: Inset{bottom: 12} on_render: || { pill_view() }} } } - }} - } - - namebar := View{visible: false width: Fill height: Fit flow: Down spacing: 6 padding: Inset{left: 16 right: 16 top: 4} - Label{padding: Inset{left: 4} text: "Name" draw_text +: {color: ui_muted text_style: theme.font_bold{font_size: 13}}} - merge_field := UiField{empty_text: "Merged" on_change: |text| set_merge_name(text)} - name_note := UiCaption{visible: false text: "" padding: Inset{left: 4}} - } - bottom := View{width: Fill height: Fit flow: Right spacing: 10 padding: Inset{left: 16 right: 16 top: 8 bottom: 16} align: Align{y: 0.5} on_render: || { - View{width: 0 height: 1} - if route == "library" && docs.len() >= 2 { - if engine_state == "missing" { Muted{text: "Merge PDFs" on_click: || banner(engine_headline())} } - else { Primary{text: "Merge PDFs" on_click: || start_merge()} } - } - if route == "doc" && current != nil && confirm_remove { - Quiet{text: "Keep it" on_click: || keep_doc()} - Danger{text: "Remove" on_click: || remove_doc()} - } - if route == "doc" && tab == "pages" && current != nil && !confirm_remove { - if info != nil && page_count() >= 2 { Primary{text: "Split into files" on_click: || start_split()} } - if engine_state == "missing" { Muted{text: "Split into files" on_click: || banner(engine_headline())} } - } - if route == "page" { - if page_no > 1 { Quiet{text: "Previous" on_click: || step_page(-1)} } else { Muted{text: "Previous"} } - if page_no < page_count() { Quiet{text: "Next" on_click: || step_page(1)} } else { Muted{text: "Next"} } - } - if route == "merge" { - if merge_step == 1 { - if picked.len() >= 2 { Primary{text: "Next: order " + picked.len() + " files" on_click: || merge_next()} } - else { Muted{text: pick(picked.len() == 1, "Choose one more", "Choose 2 or more PDFs")} } - } - if merge_step == 2 { - Quiet{width: Fit text: "Back" on_click: || { merge_step = 1 show(true) }} - if merge_busy { Muted{text: "Merging…"} } - else if engine_state == "missing" { Muted{text: "Merge " + picked.len() + " PDFs" on_click: || banner(engine_headline())} } - else { Primary{text: "Merge " + picked.len() + " PDFs" on_click: || do_merge()} } + line_right := VLine{visible: false} + // Each right panel is as wide as its content says (right_w). + right_thread_box := View{visible: false width: Fit height: Fill + right_thread := SolidView{width: Fit height: Fill flow: Down draw_bg.color: pt_panel on_render: || { thread_panel() }} } - if merge_step == 3 && merge_result != nil { - Quiet{text: "Done" on_click: || go_library()} - Primary{text: "Open" on_click: || open_doc(merge_result)} + right_side_box := View{visible: false width: Fit height: Fill + right_side := SolidView{width: Fit height: Fill flow: Down draw_bg.color: pt_panel on_render: || { side_panel() }} } } - if route == "split" && current != nil { - if split_result != nil { Primary{text: "Done" on_click: || go_library()} } - else if split_busy { Muted{text: "Splitting…"} } - else if engine_state == "missing" { Muted{text: "Split" on_click: || banner(engine_headline())} } - else if split_ranges().len() < 2 { Muted{text: pick(split_mode == "at", "Tap a page to split there", "Split")} } - else { Primary{text: "Split into " + split_ranges().len() + " files" on_click: || do_split()} } - } - }} + } + Line{} + status := SolidView{width: Fill height: STATUS_H draw_bg.color: pt_status on_render: || { status_view() }} } banner := View{visible: false width: Fill height: Fill flow: Down align: Align{x: 0.5 y: 1.0} padding: Inset{left: 16 right: 16 bottom: 84} RoundedView{width: Fill max_width: 560 height: Fit flow: Right align: Align{y: 0.5} padding: Inset{left: 16 right: 6 top: 6 bottom: 6} - new_batch: true show_bg: true draw_bg +: {color: #x1c1c1eee border_radius: 14.0} - banner_text := Label{width: Fill padding: 0 flow: Flow.Right{wrap: true} text: "" draw_text +: {color: #xffffff text_style: theme.font_regular{font_size: 13 line_spacing: 1.35}}} - ButtonFlat{height: 44 padding: Inset{left: 12 right: 12} text: "OK" on_click: || banner("") + new_batch: true show_bg: true draw_bg +: {color: #x1c1c1eee border_radius: 10.0} + banner_text := Label{padding: 0 width: Fill padding: 0 flow: Flow.Right{wrap: true} text: "" draw_text +: {color: #xffffff text_style: theme.font_regular{font_size: 12 line_spacing: 1.35}}} + ButtonFlat{height: 40 padding: Inset{left: 12 right: 12} text: "OK" on_click: || note("") draw_bg +: {color: #x00000000 color_focus: #x00000000 color_hover: #xffffff22 color_down: #xffffff33 border_size: 0.0} - draw_text +: {color: #x64d2ff color_focus: #x64d2ff color_hover: #x64d2ff color_down: #x64d2ff text_style: theme.font_bold{font_size: 13}}} + draw_text +: {color: #x64d2ff color_focus: #x64d2ff color_hover: #x64d2ff color_down: #x64d2ff text_style: theme.font_bold{font_size: 12}}} } } + dialog_box := View{visible: false width: Fill height: Fill + dialog := View{width: Fill height: Fill flow: Overlay on_render: || { dialog_view() }} + } } diff --git a/apps/pdftools/bundle/manifest.json b/apps/pdftools/bundle/manifest.json index 660f102c..a5807e48 100644 --- a/apps/pdftools/bundle/manifest.json +++ b/apps/pdftools/bundle/manifest.json @@ -1,7 +1,7 @@ { "schema": 1, "id": "os.pdftools", - "version": "0.1.0", + "version": "0.2.0", "name": "PDF Tools", "integrity": { "bundle_blake3": "" }, "capabilities": ["storage", "files", "pdf"] diff --git a/apps/pdftools/design/BRIEF.md b/apps/pdftools/design/BRIEF.md new file mode 100644 index 00000000..75fa1a52 --- /dev/null +++ b/apps/pdftools/design/BRIEF.md @@ -0,0 +1,160 @@ +# PDF Tools v2: product requirements + +Status: draft for review, 10 Oct 2026. This brief starts the OctoSense App +Flow for a new design. The order is: product requirements (this file), UX +design by image generation (`source/`), pixel mapping to Splash widgets, +then the logic wired to the pdf engine. It replaces v1's design; v1 stays on +`main` until v2 is accepted. + +## Why a new design + +v1 (#430) is a phone-style flow on a desktop: a library, then one page at a +time, then step-by-step merge and split. It reaches 5 of the engine's 132 +commands, and a page is a static picture. Someone used to a desktop PDF app +finds nothing they expect: no document view to scroll and zoom, no search +highlights, no comments, no page organizing, no forms. + +## Who it is for, and what they do + +People who receive, read and send PDFs on an OctoSense desktop. Their jobs, +in order of frequency: + +1. **Read** a long document: scroll through it, zoom, jump to a page or a + section, keep their place. +2. **Find** a word or phrase and see every match on the page. +3. **Review**: highlight, underline and comment, reply to others' comments, + mark them resolved. +4. **Fill and sign** a form: type into its fields, or type text, a date and + their initials onto a form that has no fields. +5. **Organize pages**: rotate, delete, reorder, insert pages from another PDF, + extract pages into a new file. +6. **Combine** several PDFs into one, choosing the pages from each. +7. **Edit** a line or paragraph of existing text (a typo, a date). +8. **Export** pages as images or the text as a file, inside the app's storage. + +## Platform and constraints + +- OctoSense desktop only: the pdf engine is behind the desktop-only + `craft-engines` feature. A contained Splash system app, id `os.pdftools`, + declaring `storage`, `files` and `pdf`. +- Files live in the app's own storage (64 MiB). A PDF comes in through + `files.import` (up to 64 MiB, titled with its own name). Exports stay in the + app's storage for v2's first release (`files.export` still reads at most + 1 MiB). +- Engine calls run on the shell's UI thread until #399 lands. The design + must stay usable with that: pages render one at a time at screen + resolution and are cached, thumbnails fill in progressively, and nothing + re-renders the whole document on a change. +- No network, no device features. A document's own scripts never run. +- Password-protected PDFs: OctoSense rule 3 keeps secrets on host-owned + sheets, and there is no PDF password sheet yet. v2 shows "This PDF is + protected with a password" and does not open it. + +## Window and layout + +The page is the hero; tools appear where they act. Regions, top to bottom +and left to right: + +| Region | What it holds | +| --- | --- | +| Document tabs | One tab per open PDF, its title and a close button; "Open" at the end | +| Mode bar | Read · Comment · Fill & Sign · Pages · Combine · Edit, the current mode marked; search field; Undo, Redo, Save | +| Left rail and panel | Rail icons: Pages (thumbnails), Outline (bookmarks), Comments, Search results. The panel shows the selected one and can collapse | +| Canvas | The document as white pages on a quiet desk, scrolled continuously; the page under the pointer is the current page | +| Canvas controls | A floating pill: page "3 / 24", previous and next, zoom out, zoom "100%", zoom in, fit width, fit page | +| Right panel | Appears only when a mode needs it: a comment thread, the form's field list, tool options, document properties | +| Status line | Save state ("Saved", "Edited"), storage used ("18.4 of 64 MB") | + +Keyboard: ⌘F find, ⌘+ and ⌘− zoom, ⌘0 fit page, arrows and Page Up and Down +to move, ⌘Z and ⇧⌘Z undo and redo, ⌘S save. + +## Screens + +1. **Home**: recent PDFs as cards with their first page, pages and size; + "Open a PDF from this device (up to 64 MB)"; storage used; empty state for + a first run. +2. **Reading**: thumbnails panel, a 24-page report at 100%, canvas controls. +3. **Find**: a query, "12 matches on 7 pages", the results list with + snippets, matches highlighted on the page, the current one stronger. +4. **Comment**: highlight tool active, a highlighted sentence, the right + panel showing a thread with two replies, a reply box and a status menu. +5. **Pages**: every page as a large thumbnail; two selected; actions Rotate, + Delete, Extract, Insert from file, and drag to reorder. +6. **Combine**: three PDFs in order with drag handles and page ranges + ("all", "1–4, 9"), "One PDF of 31 pages", Combine. +7. **Fill & Sign**: a lease with its fields outlined, some filled; the right + panel lists the fields with required ones marked; "Add initials" places + typed initials. +8. **Edit**: one paragraph selected for editing, its new text typed in place, + font and size shown in the right panel. +9. **Dark appearance** of Reading, with the Outline panel open. + +States each screen must design for: empty library; a page still rendering; +a damaged PDF ("Couldn't open this PDF" and the engine's reason); a +protected PDF; storage full ("Remove a PDF to make room"); unsaved changes +when closing a tab. + +## Actions and the engine + +| Action | Engine command | Class | App method to add | +| --- | --- | --- | --- | +| Open, page sizes, outline, fields, properties | `doc_open` (scripts off), `doc_info`, `bookmark_list` | safe after open | `pdf.open`, `pdf.info` (extend) | +| Render a page at screen resolution | `page_render {dpi}` | safe | `pdf.page` | +| Find with match rectangles | `text_find` | safe | `pdf.find` | +| Text lines and paragraphs (selection, edit) | `text_lines`, `text_paragraphs` | safe | `pdf.lines` | +| Comments: list, add, edit, delete, reply, status | `comment_*` | safe | `pdf.comments`, `pdf.comment` | +| Fill fields, Fill & Sign text, date, initials | `form_fields`, `form_fill`, `fill_sign_add` | file/code, needs review | `pdf.fields`, `pdf.fill` | +| Rotate, delete, move, duplicate pages | `page_rotate`, `page_delete`, `page_move`, `page_duplicate` | safe | `pdf.pages` | +| Insert from file, extract | `page_insert_file`, `page_extract` | file (inside storage) | `pdf.pages` | +| Edit a line or paragraph | `text_edit` | safe | `pdf.edit_text` | +| Undo, redo, save | `edit_undo`, `edit_redo`, `doc_save` | file (inside storage) | `pdf.undo`, `pdf.redo`, `pdf.save` | +| Combine with page ranges | `pdf.merge` (extend with ranges) | file | `pdf.merge` | +| Export images, text | `doc_export_images`, `doc_export_text` | file (inside storage) | `pdf.export` | + +The app's service grows with the design: each method is reviewed the way the +system agent's doors were, works in the app's storage, and keeps a document +open across calls (an open-document id per tab), so an edit doesn't reopen +the file. + +## Data + +- The library index in the app's storage: file, title, pages, size, last + opened, last page and zoom. +- Comments, form values and page changes live in the PDF itself, written by + Save. Unsaved edits stay in the engine's open document; Undo and Redo use the + engine's history. + +## Visual direction for the image prompts + +A precise, calm desktop tool that looks like it belongs to OctoSense: the +document is the brightest thing on screen, chrome is quiet and thin, and one +accent marks the current mode and primary actions. Neutral greys with a slight +cool bias, white pages with a soft shadow on a light grey desk, generous but +not loose spacing, crisp small type in the chrome and real document content on +the pages. Highlights use the familiar yellow; status uses green and red +sparingly. A dark appearance with the same structure. It must not copy Adobe +Acrobat's look or iconography. + +## Out of scope for v2's first release + +OCR, redaction, digital signatures with certificates, comparing documents, +accessibility fixing, printing, password-protected PDFs, measuring, stamps, +links, cloud services and sharing outside the app. + +## Done when + +- Every job above works end to end with the real engine in the hidden + desktop shell, in light and dark, with the states above. +- A 100-page PDF opens to its first page in under a second, and scrolling never + freezes the shell for more than a page render. A smooth canvas waits for + #399. +- Side-by-side comparisons with the approved generated designs pass a + person's visual review. +- Rebuilt from the approved images through the App Flow pixel mapping, not + hand-drawn in another style. + +## Open questions + +1. Is "Edit text" in v2's first release, or the next one? +2. Should Combine and Export also offer saving outside the app once + `files.export` takes large files? diff --git a/apps/pdftools/design/SERVICE.md b/apps/pdftools/design/SERVICE.md new file mode 100644 index 00000000..d460b33c --- /dev/null +++ b/apps/pdftools/design/SERVICE.md @@ -0,0 +1,134 @@ +# PDF Tools v2: pdf service contract + +The app-facing methods PDF Tools v2 calls over `host.request`. The service +(`apps/pdf/host-service`) implements them; the app (`apps/pdftools/bundle`) +calls nothing else. Both sides build against this file, and a change to it +goes to both. Derived from `BRIEF.md` (Actions and the engine). + +## Ground rules + +- **Callers:** system apps only (`may_call`: `os.*`), as today. The system + agent's five `pdf.*` tools (`tools.json`) do not change. +- **Where:** every path is relative to the caller's area, which for an app's + own request is its storage (`octosense_engine_area::Slot::area`), resolved + and contained as today. A write never replaces a file the call didn't open + for editing. +- **Open documents:** `pdf.open` returns a document handle; later calls name it + as `doc`. The service keeps the engine's open document (its edits and undo + history) between calls, keyed by the caller app, its storage scope and the + handle. A handle from another app or scope is refused as unknown. A caller has + at most 8 open documents; the 9th open is refused with `too_many_open`. + `pdf.close` and the app closing (its storage scope going away) release them. +- **Scripts:** a document's own JavaScript never runs: every session turns the + engine's JavaScript off before opening, as `session()` does today. +- **Speed:** engine work still runs on the shell's UI thread (#399). Every + method must answer a typical page in well under 100 ms; caps below keep the + worst case bounded. +- **Errors:** `Err` strings start with a stable code, then a colon and plain + words for a person: `not_found:`, `damaged:` (the engine's reason after it), + `protected:` (needs a password; v2 does not open these), `storage_full:`, + `too_many_open:`, `unknown_doc:`, `unsaved:`, `invalid:`, `too_large:`. + `invalid:` also covers a path outside storage, a taken output name, nothing + to undo or redo, and editing a read-only document. +- **Geometry:** points, origin at the top-left of the displayed page, y down, + as the engine's `text_find` and `comment_add` use. Rotation is already + applied. Every rectangle is `[x, y, w, h]` in points: the `lines` boxes, + the `comments` rects, the `fields` rect and the `rects` input of a comment + add. Find's match rects can go straight into a highlight. +- **Handles:** `doc` is a string (lowercase hex). + +## Documents + +| Method | Arguments | Answer | +| --- | --- | --- | +| `pdf.open` | `{path}` | `{doc, path, pages, sizes: [[w, h], …], title, outline: [{title, page, children}], fields, comments, can_edit}`; `fields` and `comments` are counts | +| `pdf.close` | `{doc}` | `{closed: true}`; refuses `unsaved:` unless `{discard: true}` | +| `pdf.state` | `{doc}` | `{edited, can_undo, can_redo, pages}` | +| `pdf.info` | `{path}` (as today) or `{doc}` | as today, plus `{doc}` works on the open document | + +Caps: the file is at most 128 MiB (`MAX_PDF_BYTES`). + +## Reading + +| Method | Arguments | Answer | +| --- | --- | --- | +| `pdf.page` | `{doc, page, dpi?}` | `{path, width, height, dpi}`: the page rendered as a PNG written to `.cache/pages//@.png` in storage | +| `pdf.find` | `{doc, query, limit?}` | `{total, matches: [{page, rects: [[x, y, w, h], …], snippet}]}`, snippet ≤ 120 characters with the match marked `[[…]]` | +| `pdf.lines` | `{doc, page}` | `{lines: [{n, text, box, font, size}], paragraphs: [{n, text, box, lines, font, size}]}` | +| `pdf.text` | `{path, pages?}` (as today) or `{doc, pages?}` | as today | + +Caps: `dpi` 24 to 300 (default 96), and at most 16 megapixels per render. A +fractional `dpi` is rounded; one out of range is `invalid:`, not clamped. A +single render whose PNG is over 16 MiB is `too_large:`. `limit` at most 500 +(default 200); `total` saturates at 10,000. The render cache is at most 16 MiB +and 64 files per app (app storage allows 256 entries in all, and imports and +library saves need room): the oldest renders go first, and it is cleared +before a write would fail with `storage_full:`. A render of an edited +document reflects its unsaved edits. + +## Comments + +| Method | Arguments | Answer | +| --- | --- | --- | +| `pdf.comments` | `{doc}` | `{comments: [{id, page, type, author, text, date, color, status, rects, replies: [{id, author, text, date}]}]}` | +| `pdf.comment` | `{doc, op: "add", page, type, rects?, at?, color?, text?, author?}` | `{id}`; `type` is `highlight`, `underline`, `strikeout`, `note` or `textbox` | +| `pdf.comment` | `{doc, op: "edit", id, text?, color?}` | `{id}` | +| `pdf.comment` | `{doc, op: "delete", id}` | `{deleted: true}` | +| `pdf.comment` | `{doc, op: "reply", id, text, author?}` | `{id}` (the reply's) | +| `pdf.comment` | `{doc, op: "status", id, status}` | `{id}`; `status` is `accepted`, `rejected`, `cancelled`, `completed` or `none` | + +A comment's `date` is local time without a zone or seconds +(`2026-10-10T22:21`), and its `rects` is one bounding box. An unnamed comment's +id is `@page-index`. Fill & Sign marks appear in the list. Without `author` +the engine writes "PdfCraft": always pass the person's name (PDF Tools asks +once, "Comment as", and keeps it in its storage). + +## Fill & Sign + +| Method | Arguments | Answer | +| --- | --- | --- | +| `pdf.fields` | `{doc}` | `{fields: [{name, type, value, required, read_only, page, rect, options}]}`; `options` are always `{value, label}`; `read_only` is being added (until then a missing value means false) | +| `pdf.fill` | `{doc, values: {name: value}}` | `{filled: n}` (one undo step) | +| `pdf.fill_sign` | `{doc, page, kind, at: [x, y], text?}` | `{added: true}`; `kind` is `text`, `date`, `initials`, `check` or `cross` | + +`pdf.fill` uses the engine's `form_fill` with JavaScript off. The service +review must confirm that no field script runs. + +## Pages + +| Method | Arguments | Answer | +| --- | --- | --- | +| `pdf.pages` | `{doc, op: "rotate", pages, angle}` | `{pages}` (the new count); `angle` is ±90, 180 or −180 | +| `pdf.pages` | `{doc, op: "delete", pages}` | `{pages}`; deleting every page is refused | +| `pdf.pages` | `{doc, op: "move", pages, to}` | `{pages}` | +| `pdf.pages` | `{doc, op: "duplicate", pages}` | `{pages}` | +| `pdf.pages` | `{doc, op: "insert_file", path, at, pages?}` | `{pages}`; `path` another PDF in storage | +| `pdf.pages` | `{doc, op: "extract", pages, out}` | `{path}`; `out` a new file in storage | + +Caps: at most 512 pages named in one call. + +## Edit + +| Method | Arguments | Answer | +| --- | --- | --- | +| `pdf.edit_text` | `{doc, page, paragraph?, line?, text}` | `{edited: true}`; exactly one of `paragraph` or `line`, numbers from `pdf.lines` | + +## History and saving + +| Method | Arguments | Answer | +| --- | --- | --- | +| `pdf.undo` | `{doc}` | `{edited, can_undo, can_redo}` | +| `pdf.redo` | `{doc}` | `{edited, can_undo, can_redo}` | +| `pdf.save` | `{doc, path?}` | `{path, bytes}`: an incremental save to the document's own file, or a full save to a new `path` (never over an existing file), which then becomes the open document's file. A plain save from a background surface answers `invalid:` | + +## Combine, split, export + +| Method | Arguments | Answer | +| --- | --- | --- | +| `pdf.merge` | `{paths: [path or {path, pages}], out}` | as today; `pages` is a range string such as `"1-4, 9"`, and accepts `"all"`, an empty string and typed dashes | +| `pdf.split` | as today | as today | +| `pdf.export` | `{doc, kind: "images", out_dir, dpi?, pages?}` or `{doc, kind: "text", out, pages?}` | `{paths}` | + +Caps: `pdf.merge` 2 to 16 inputs, as today. `pdf.export` images at most 300 dpi, +64 pages and 256 megapixels in total per call, each page at most 16 MP, +writing new files only. diff --git a/apps/pdftools/design/map/01-home.json b/apps/pdftools/design/map/01-home.json new file mode 100644 index 00000000..a3d90b67 --- /dev/null +++ b/apps/pdftools/design/map/01-home.json @@ -0,0 +1,1026 @@ +{ + "schema": 1, + "screen": "01-home", + "state": "Home with five recent PDFs", + "image": "source/01-home.png", + "image_sha256": "7f52b77e4b72a1421bb54fa35b1e8d6d7df1a6da08fbc2e71089db4e1a02ad74", + "measured": "source/01-home.measured.json", + "measured_sha256": "66cac77df8274b51b6dd84a93fdb5fbbfd36cf1384cc64f47085d3f49e5cc94c", + "units": "1 image pixel = 1 layout point (the designs are a 1536 x 1024 window)", + "built_in": "apps/pdftools/bundle/main.splash (v2)", + "runtime": "OctoSense-org/makepad d653bc44 with runtime-patches.lock.json, as tools/setup.py prepares .sources/makepad", + "type_rule": "size in theme units: the ink height for runs without descenders, ink / 1.23 with them (calibrated in card-host on the runtime's own text); the build may round", + "colours": "light from this screen (or the light screens), dark from 09-dark-reading; tokens below, selected by the shared interface's ui_light", + "tokens": { + "pt_accent": { + "light": "#116cdc", + "dark": "#2f75d8", + "from": "02 s19 Save fill #116cdc; 09 s16 Save #246acf, s08 outline selection #2970d7" + }, + "pt_link": { + "light": "#1c6ce0", + "dark": "#4d91e3", + "from": "02 t00 'Read' ink #3c7ae8 (anti-aliased); 09 t00 #468bd8" + }, + "pt_tabs": { + "light": "#f4f4f5", + "dark": "#191e21", + "from": "02 s04 tab row #f4f4f5; 09 tab row sample (1350,30) #191e21" + }, + "pt_tab_on": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 active tab; 09 active tab ground #22282c" + }, + "pt_bar": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 mode bar #fefefe; 09 t07 ground #22292d" + }, + "pt_rail": { + "light": "#ffffff", + "dark": "#1e252a", + "from": "02 rail #fefefe; 09 s02 #1e252a" + }, + "pt_panel": { + "light": "#ffffff", + "dark": "#192025", + "from": "02 s02 #fefefe; 09 s01 #192025" + }, + "pt_desk": { + "light": "#eceded", + "dark": "#191f24", + "from": "02 s00 #eceded; 09 desk sample (420,500) #191f24" + }, + "pt_home": { + "light": "#f7f8f9", + "dark": "#191f24", + "from": "01 s00 #f7f8f9; dark Home not designed: 09's desk" + }, + "pt_status": { + "light": "#ffffff", + "dark": "#1b2226", + "from": "02 status #fefefe; 09 status sample (700,1000) #1b2226" + }, + "pt_pill": { + "light": "#ffffff", + "dark": "#282d32", + "from": "02 pill #fefefe; 09 s04 #282d32" + }, + "pt_card": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "04/06/08 cards #fefefe; dark: 09 bar colour (cards not in 09)" + }, + "pt_line": { + "light": "#e1e2e3", + "dark": "#383d41", + "from": "02 d00 #e1e2e3; 09 d24 #383d41" + }, + "pt_line_soft": { + "light": "#ececee", + "dark": "#2c3236", + "from": "02 d02 #ecedee; 09 d07 #303437" + }, + "pt_thumb_line": { + "light": "#d3d5d9", + "dark": "#464b50", + "from": "02 thumbnails' border (s15 #e0e1f0 on the page edge, darker at 1x); dark derived" + }, + "pt_tint": { + "light": "#e9f2fe", + "dark": "#275294", + "from": "02 s16 rail selection #e9f2fe; 09 s14 #275293" + }, + "pt_tint_line": { + "light": "#cfe0fa", + "dark": "#34619f", + "from": "04 s27 Highlight tool border #dfebfc; dark derived from pt_tint" + }, + "pt_tint_ink": { + "light": "#2f6fd0", + "dark": "#bac7dd", + "from": "02 t01 rail 'Pages' ink #5973a6 (anti-aliased blue); 09 t03 #bac7dd" + }, + "pt_select": { + "light": "#eff5fc", + "dark": "#22344d", + "from": "03 s06 result group fill #eff5fc; dark derived" + }, + "pt_field": { + "light": "#ffffff", + "dark": "#1e2429", + "from": "02 s06 search #fefefe; 09 s10 #1e2429" + }, + "pt_field_line": { + "light": "#dcdcde", + "dark": "#3c4043", + "from": "02 d01 #dcdcde; 09 d02 #3c4043" + }, + "pt_hover": { + "light": "#f1f2f3", + "dark": "#2b3237", + "from": "hover: derived" + }, + "pt_ink": { + "light": "#37393c", + "dark": "#d5d7d8", + "from": "02 t16 'Pages' title #38393b, t05 tab #414142; 09 t06 tab #d1d3d4, t09 #d7d9da" + }, + "pt_ink2": { + "light": "#55575b", + "dark": "#b0b2b4", + "from": "02 mode labels #5c5d5e-#6d6d6e; 09 mode labels #a9abad-#b3b4b4" + }, + "pt_muted": { + "light": "#76787c", + "dark": "#9a9da0", + "from": "02 t57 'Saved' #79797b, t58 #6d6d6e; 09 t59 #929698, t60 #9b9d9f" + }, + "pt_faint": { + "light": "#9c9da0", + "dark": "#7d8184", + "from": "02 t15 placeholder #9c9d9f; 09 dimmer than t25 #9fa2a5" + }, + "pt_icon": { + "light": "#5b5d61", + "dark": "#a9acae", + "from": "02 icons; 09 icons" + }, + "pt_green": { + "light": "#49b039", + "dark": "#38a540", + "from": "02 'Saved' dot sample #49b039; 09 #38a540" + }, + "pt_orange": { + "light": "#f2a20d", + "dark": "#e39a1c", + "from": "08 'Edited' dot (31,984)" + }, + "pt_red": { + "light": "#e5534f", + "dark": "#f06a66", + "from": "07 t74 'Monthly rent *' #e85652, t75 #ed4f4a" + }, + "pt_shadow": { + "light": "#00000022", + "dark": "#00000070", + "from": "page and pill shadows: soft, from 02 page edge #e9eaeb" + } + }, + "regions": [ + { + "id": "renders1", + "role": "Home card covers: the engine's renders", + "page_content": true, + "widget": "Image (binary_resource of a pdf.page PNG) inside its frame", + "cite": [ + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": {}, + "colors": {}, + "decision": "Page contents are sample art: every page image is the engine's render (pdf.page).", + "image_rect": [ + 370, + 352, + 214, + 190 + ], + "status": "built", + "source": { + "text": [ + 4, + 5, + 6, + 7, + 8, + 9 + ], + "surfaces": [ + 1, + 7 + ], + "dividers": [ + 11 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "renders2", + "role": "Home card covers: the engine's renders", + "page_content": true, + "widget": "Image (binary_resource of a pdf.page PNG) inside its frame", + "cite": [ + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": {}, + "colors": {}, + "decision": "Page contents are sample art: every page image is the engine's render (pdf.page).", + "image_rect": [ + 661, + 352, + 214, + 190 + ], + "status": "built", + "source": { + "text": [ + 30, + 31, + 32, + 33, + 34, + 35 + ], + "surfaces": [ + 3 + ], + "dividers": [ + 16, + 17 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "renders3", + "role": "Home card covers: the engine's renders", + "page_content": true, + "widget": "Image (binary_resource of a pdf.page PNG) inside its frame", + "cite": [ + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": {}, + "colors": {}, + "decision": "Page contents are sample art: every page image is the engine's render (pdf.page).", + "image_rect": [ + 943, + 352, + 218, + 190 + ], + "status": "built", + "source": { + "text": [ + 45, + 46, + 47, + 48, + 49 + ], + "surfaces": [ + 2 + ], + "dividers": [], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "renders4", + "role": "Home card covers: the engine's renders", + "page_content": true, + "widget": "Image (binary_resource of a pdf.page PNG) inside its frame", + "cite": [ + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": {}, + "colors": {}, + "decision": "Page contents are sample art: every page image is the engine's render (pdf.page).", + "image_rect": [ + 370, + 656, + 214, + 174 + ], + "status": "built", + "source": { + "text": [ + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 24, + 25 + ], + "surfaces": [ + 4, + 16, + 17 + ], + "dividers": [], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "renders5", + "role": "Home card covers: the engine's renders", + "page_content": true, + "widget": "Image (binary_resource of a pdf.page PNG) inside its frame", + "cite": [ + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": {}, + "colors": {}, + "decision": "Page contents are sample art: every page image is the engine's render (pdf.page).", + "image_rect": [ + 651, + 656, + 224, + 174 + ], + "status": "built", + "source": { + "text": [ + 39, + 40 + ], + "surfaces": [ + 5, + 12, + 14 + ], + "dividers": [], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "tabs", + "role": "document tabs", + "widget": "SolidView tabs (on_render tabs_view): Tab = RoundedView per open PDF (Svg file-pdf 16, Label title with ellipsis, IconBtn close 13), Outline button 'Open' (Svg plus) after the last tab; Home tab only on Home", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256" + ], + "geometry": { + "height": 53, + "x": 0, + "width": "Fill", + "tab": "Fit, 48 tall from y 11; the row clips 6 so the open tab joins the mode bar", + "tab_padding": [ + 16, + 8 + ], + "gap": 2, + "open": "Outline 36 tall, 6 after the last tab" + }, + "colors": { + "light": { + "row": "#f4f4f5", + "tab_on": "#ffffff", + "border": "#e1e2e3", + "text_on": "#37393c", + "text": "#76787c" + }, + "dark": { + "row": "#191e21", + "tab_on": "#232a2e", + "border": "#383d41", + "text_on": "#d5d7d8", + "text": "#9a9da0" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": "The report tab shows two close crosses; the rebuild has one.", + "notes": "Home tab first (accent underline), then the open PDFs, then Open", + "image_rect": [ + 0, + 0, + 1536, + 50 + ], + "status": "built", + "source": { + "text": [ + { + "i": 0, + "text": "Home", + "ocr_height": 13.4, + "ink": "#383a3c", + "ground": "#f4f4f4", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 1, + "text": "Q3 2026 Board Report.pdf x", + "ocr_height": 16, + "ink": "#63666b", + "ground": "#f3f4f4", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 2, + "text": "+ Open", + "ocr_height": 16, + "ink": "#404246", + "ground": "#f2f3f4", + "ink_px": 14, + "size": 11.5 + } + ], + "surfaces": [ + { + "i": 13, + "bounds": [ + 203, + 11, + 266, + 37 + ], + "fill": "#f6f7f7", + "radius": 1 + }, + { + "i": 15, + "bounds": [ + 15, + 11, + 186, + 39 + ], + "fill": "#fcfcfd", + "radius": 1 + }, + { + "i": 18, + "bounds": [ + 478, + 11, + 81, + 37 + ], + "fill": "#f6f7f8", + "radius": 1 + } + ], + "dividers": [ + 0 + ] + } + }, + { + "id": "hero", + "role": "Home header: PDF mark, title", + "widget": "Svg pdf-hero 46 x 54 (accent), Label 'PDF Tools' bold 28", + "cite": [ + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "top": 31, + "icon": [ + 46, + 54 + ], + "title_gap": 0 + }, + "colors": { + "light": { + "icon": "#116cdc", + "title": "#37393c" + }, + "dark": { + "icon": "#2f75d8", + "title": "#d5d7d8" + } + }, + "font": "theme.font_bold", + "size": 28, + "image_rect": [ + 600, + 60, + 340, + 140 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 27, + "text": "PDF", + "ocr_height": 17.8, + "ink": "#2061c5", + "ground": "#eef3f9", + "ink_px": 24, + "size": 24.0 + }, + { + "i": 28, + "text": "PDF Tools", + "ocr_height": 34.7, + "ink": "#2d3339", + "ground": "#f4f5f6", + "ink_px": 27, + "size": 27.0 + } + ], + "surfaces": [ + { + "i": 19, + "bounds": [ + 745, + 84, + 46, + 54 + ], + "fill": "#f6f7f9", + "radius": 0 + }, + { + "i": 20, + "bounds": [ + 749, + 88, + 38, + 46 + ], + "fill": "#f6f7f9", + "radius": 1 + } + ], + "dividers": [] + } + }, + { + "id": "open", + "role": "primary action: Open a PDF from this device, its limit", + "widget": "Primary 'Open a PDF from this device' (Svg folder 24) 51 tall, Label 'Up to 64 MB' from files.status max_import_bytes", + "cite": [ + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "App Flow docs/SCRIPT-API.md 'Host services: host.request'; widgets/src/splash_host.rs" + ], + "geometry": { + "button": [ + 315, + 51 + ], + "gap": 20, + "radius": 5 + }, + "colors": { + "light": { + "button": "#116cdc", + "text": "#ffffff", + "limit": "#76787c" + }, + "dark": { + "button": "#2f75d8", + "text": "#ffffff", + "limit": "#9a9da0" + } + }, + "font": "theme.font_regular", + "size": 13.5, + "image_rect": [ + 560, + 205, + 460, + 70 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 29, + "text": "Open a PDF from this device", + "ocr_height": 18, + "ink": "#d4e3f5", + "ground": "#0a61cb", + "ink_px": 17, + "size": 14.0 + }, + { + "i": 44, + "text": "Up to 64 MB", + "ocr_height": 16.0, + "ink": "#737980", + "ground": "#f3f4f5", + "ink_px": 14, + "size": 11.5 + } + ], + "surfaces": [ + { + "i": 11, + "bounds": [ + 584, + 214, + 315, + 51 + ], + "fill": "#015dca", + "radius": 4 + } + ], + "dividers": [ + 1, + 2, + 3, + 4 + ] + } + }, + { + "id": "recent", + "role": "Recent: heading", + "widget": "Label 'Recent' bold 15", + "cite": [ + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "left": 344, + "top_gap": 34 + }, + "colors": { + "light": { + "text": "#37393c" + }, + "dark": { + "text": "#d5d7d8" + } + }, + "font": "theme.font_bold", + "size": 15, + "image_rect": [ + 330, + 285, + 120, + 40 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 3, + "text": "Recent", + "ocr_height": 15.6, + "ink": "#31363e", + "ground": "#eceeef", + "ink_px": 16, + "size": 16.0 + } + ], + "surfaces": [], + "dividers": [] + } + }, + { + "id": "cards", + "role": "recent PDF cards: first page, name, pages and size, when opened", + "widget": "GestureView 268 wide per PDF (open_doc): RoundedView card, 207 tall top with a 209 x 188 RoundedView page box clipping an Image (pdf.page 1 at 48 dpi, ImageFit.Horizontal), SolidView line, Svg file-pdf (red), Label name (ellipsis), Label ' pages · ', Label 'Opened today'", + "cite": [ + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "card": [ + 268, + 292 + ], + "columns": "3, or 2 below 940 pt wide", + "gap": [ + 23, + 13 + ], + "page_box": [ + 209, + 188 + ], + "page_top": 19 + }, + "colors": { + "light": { + "card": "#ffffff", + "border": "#e1e2e3", + "name": "#37393c", + "meta": "#76787c", + "icon": "#e5534f" + }, + "dark": { + "card": "#232a2e", + "border": "#383d41", + "name": "#d5d7d8", + "meta": "#9a9da0", + "icon": "#f06a66" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": "Card covers are sample art: each is the engine's render of page 1.", + "image_rect": [ + 330, + 325, + 880, + 600 + ], + "status": "built", + "source": { + "text": [ + { + "i": 10, + "text": "Q3 2026 Board Report.pdf", + "ocr_height": 15.6, + "ink": "#353639", + "ground": "#f3f4f4", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 11, + "text": "24 pages • 2.8 MB", + "ocr_height": 13.4, + "ink": "#797d83", + "ground": "#f6f6f7", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 12, + "text": "Opened today", + "ocr_height": 14, + "ink": "#7e8289", + "ground": "#f7f7f7", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 21, + "text": "Invoice INV-2041.pdf", + "ocr_height": 16.0, + "ink": "#3c3d3f", + "ground": "#f5f5f6", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 22, + "text": "1 page • 84 KB", + "ocr_height": 17.5, + "ink": "#7c8086", + "ground": "#f9f9fa", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 23, + "text": "Opened 1 Oct", + "ocr_height": 13.4, + "ink": "#757a81", + "ground": "#f7f7f8", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 36, + "text": "Riverside Lease 2026.pdf", + "ocr_height": 15.6, + "ink": "#313336", + "ground": "#f3f4f4", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 37, + "text": "12 pages • 1.1 MB", + "ocr_height": 13.4, + "ink": "#797d85", + "ground": "#f6f6f7", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 38, + "text": "Opened yesterday", + "ocr_height": 16.0, + "ink": "#7d8087", + "ground": "#f8f8f9", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 41, + "text": "Site Survey Photos.pdf", + "ocr_height": 16.0, + "ink": "#2c2d2f", + "ground": "#f2f2f3", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 42, + "text": "18 pages • 9.6 MB", + "ocr_height": 13.4, + "ink": "#747980", + "ground": "#f7f7f8", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 43, + "text": "Opened 28 Sep", + "ocr_height": 14, + "ink": "#757a81", + "ground": "#f6f6f7", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 50, + "text": "Field Guide to Garden Birds.pdf", + "ocr_height": 15.6, + "ink": "#343537", + "ground": "#f3f3f4", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 51, + "text": "7 pages • 16 KB", + "ocr_height": 15.6, + "ink": "#7d8288", + "ground": "#f9f9f9", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 52, + "text": "Opened 3 Oct", + "ocr_height": 16.4, + "ink": "#7f848a", + "ground": "#f9f9fa", + "ink_px": 13, + "size": 10.5 + } + ], + "surfaces": [ + { + "i": 8, + "bounds": [ + 925, + 538, + 261, + 84 + ], + "fill": "#fdfdfd", + "radius": 0 + }, + { + "i": 9, + "bounds": [ + 636, + 541, + 261, + 81 + ], + "fill": "#fdfdfd", + "radius": 1 + }, + { + "i": 10, + "bounds": [ + 345, + 829, + 261, + 80 + ], + "fill": "#fdfdfd", + "radius": 0 + } + ], + "dividers": [ + 12, + 13, + 14, + 15, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + } + }, + { + "id": "status", + "role": "status line: save state, storage used", + "widget": "SolidView status (on_render status_view): RoundedView 12 dot (green Saved, orange Edited/Saving), Label state, Label ' of 64 MB used' (the library's PDFs against the 64 MiB storage); on Home the label and a RoundedView bar", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "height": 44, + "padding": [ + 33, + 29 + ], + "dot": 12, + "gap": 11 + }, + "colors": { + "light": { + "status": "#ffffff", + "text": "#76787c", + "saved": "#49b039", + "edited": "#f2a20d" + }, + "dark": { + "status": "#1b2226", + "text": "#9a9da0", + "saved": "#38a540", + "edited": "#e39a1c" + } + }, + "font": "theme.font_regular", + "size": 11.5, + "state": "storage only", + "notes": "'14.6 of 64 MB used' and a bar (164..1274) on Home", + "image_rect": [ + 0, + 978, + 1536, + 46 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 26, + "text": "14.6 of 64 MB used", + "ocr_height": 13.4, + "ink": "#72757b", + "ground": "#f3f4f5", + "ink_px": 11, + "size": 11.0 + } + ], + "surfaces": [ + { + "i": 0, + "bounds": [ + 0, + 7, + 1536, + 1017 + ], + "fill": "#f7f8f9", + "radius": 0 + }, + { + "i": 6, + "bounds": [ + 2, + 982, + 1533, + 41 + ], + "fill": "#f7f8f8", + "radius": 0 + } + ], + "dividers": [ + 5, + 6, + 7, + 8, + 9, + 10 + ] + } + } + ], + "coverage": { + "text": 53, + "surfaces": 21, + "dividers": 26, + "unmapped": 0 + } +} diff --git a/apps/pdftools/design/map/02-reading.json b/apps/pdftools/design/map/02-reading.json new file mode 100644 index 00000000..2496a825 --- /dev/null +++ b/apps/pdftools/design/map/02-reading.json @@ -0,0 +1,1073 @@ +{ + "schema": 1, + "screen": "02-reading", + "state": "Reading page 3 of 24 at 100%, Pages panel", + "image": "source/02-reading.png", + "image_sha256": "a1b56c1e0f54c0b0db9a5d1f4e465041b6d67a44efa36180f7da1d57f79c0ef2", + "measured": "source/02-reading.measured.json", + "measured_sha256": "4dad02e0b1e0b9f56908233bfe0f1664fc67c153a9e0c8da977ef2fc16fa0ecf", + "units": "1 image pixel = 1 layout point (the designs are a 1536 x 1024 window)", + "built_in": "apps/pdftools/bundle/main.splash (v2)", + "runtime": "OctoSense-org/makepad d653bc44 with runtime-patches.lock.json, as tools/setup.py prepares .sources/makepad", + "type_rule": "size in theme units: the ink height for runs without descenders, ink / 1.23 with them (calibrated in card-host on the runtime's own text); the build may round", + "colours": "light from this screen (or the light screens), dark from 09-dark-reading; tokens below, selected by the shared interface's ui_light", + "tokens": { + "pt_accent": { + "light": "#116cdc", + "dark": "#2f75d8", + "from": "02 s19 Save fill #116cdc; 09 s16 Save #246acf, s08 outline selection #2970d7" + }, + "pt_link": { + "light": "#1c6ce0", + "dark": "#4d91e3", + "from": "02 t00 'Read' ink #3c7ae8 (anti-aliased); 09 t00 #468bd8" + }, + "pt_tabs": { + "light": "#f4f4f5", + "dark": "#191e21", + "from": "02 s04 tab row #f4f4f5; 09 tab row sample (1350,30) #191e21" + }, + "pt_tab_on": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 active tab; 09 active tab ground #22282c" + }, + "pt_bar": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 mode bar #fefefe; 09 t07 ground #22292d" + }, + "pt_rail": { + "light": "#ffffff", + "dark": "#1e252a", + "from": "02 rail #fefefe; 09 s02 #1e252a" + }, + "pt_panel": { + "light": "#ffffff", + "dark": "#192025", + "from": "02 s02 #fefefe; 09 s01 #192025" + }, + "pt_desk": { + "light": "#eceded", + "dark": "#191f24", + "from": "02 s00 #eceded; 09 desk sample (420,500) #191f24" + }, + "pt_home": { + "light": "#f7f8f9", + "dark": "#191f24", + "from": "01 s00 #f7f8f9; dark Home not designed: 09's desk" + }, + "pt_status": { + "light": "#ffffff", + "dark": "#1b2226", + "from": "02 status #fefefe; 09 status sample (700,1000) #1b2226" + }, + "pt_pill": { + "light": "#ffffff", + "dark": "#282d32", + "from": "02 pill #fefefe; 09 s04 #282d32" + }, + "pt_card": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "04/06/08 cards #fefefe; dark: 09 bar colour (cards not in 09)" + }, + "pt_line": { + "light": "#e1e2e3", + "dark": "#383d41", + "from": "02 d00 #e1e2e3; 09 d24 #383d41" + }, + "pt_line_soft": { + "light": "#ececee", + "dark": "#2c3236", + "from": "02 d02 #ecedee; 09 d07 #303437" + }, + "pt_thumb_line": { + "light": "#d3d5d9", + "dark": "#464b50", + "from": "02 thumbnails' border (s15 #e0e1f0 on the page edge, darker at 1x); dark derived" + }, + "pt_tint": { + "light": "#e9f2fe", + "dark": "#275294", + "from": "02 s16 rail selection #e9f2fe; 09 s14 #275293" + }, + "pt_tint_line": { + "light": "#cfe0fa", + "dark": "#34619f", + "from": "04 s27 Highlight tool border #dfebfc; dark derived from pt_tint" + }, + "pt_tint_ink": { + "light": "#2f6fd0", + "dark": "#bac7dd", + "from": "02 t01 rail 'Pages' ink #5973a6 (anti-aliased blue); 09 t03 #bac7dd" + }, + "pt_select": { + "light": "#eff5fc", + "dark": "#22344d", + "from": "03 s06 result group fill #eff5fc; dark derived" + }, + "pt_field": { + "light": "#ffffff", + "dark": "#1e2429", + "from": "02 s06 search #fefefe; 09 s10 #1e2429" + }, + "pt_field_line": { + "light": "#dcdcde", + "dark": "#3c4043", + "from": "02 d01 #dcdcde; 09 d02 #3c4043" + }, + "pt_hover": { + "light": "#f1f2f3", + "dark": "#2b3237", + "from": "hover: derived" + }, + "pt_ink": { + "light": "#37393c", + "dark": "#d5d7d8", + "from": "02 t16 'Pages' title #38393b, t05 tab #414142; 09 t06 tab #d1d3d4, t09 #d7d9da" + }, + "pt_ink2": { + "light": "#55575b", + "dark": "#b0b2b4", + "from": "02 mode labels #5c5d5e-#6d6d6e; 09 mode labels #a9abad-#b3b4b4" + }, + "pt_muted": { + "light": "#76787c", + "dark": "#9a9da0", + "from": "02 t57 'Saved' #79797b, t58 #6d6d6e; 09 t59 #929698, t60 #9b9d9f" + }, + "pt_faint": { + "light": "#9c9da0", + "dark": "#7d8184", + "from": "02 t15 placeholder #9c9d9f; 09 dimmer than t25 #9fa2a5" + }, + "pt_icon": { + "light": "#5b5d61", + "dark": "#a9acae", + "from": "02 icons; 09 icons" + }, + "pt_green": { + "light": "#49b039", + "dark": "#38a540", + "from": "02 'Saved' dot sample #49b039; 09 #38a540" + }, + "pt_orange": { + "light": "#f2a20d", + "dark": "#e39a1c", + "from": "08 'Edited' dot (31,984)" + }, + "pt_red": { + "light": "#e5534f", + "dark": "#f06a66", + "from": "07 t74 'Monthly rent *' #e85652, t75 #ed4f4a" + }, + "pt_shadow": { + "light": "#00000022", + "dark": "#00000070", + "from": "page and pill shadows: soft, from 02 page edge #e9eaeb" + } + }, + "regions": [ + { + "id": "pill", + "role": "canvas controls: page n / N, previous, next, zoom out, zoom %, zoom in, fit width, fit page", + "widget": "View pill (overlay, bottom centre; on_render pill_view): RoundedShadowView radius 26.5 with IconBtn chevron-left, Label 'p / N' (78 wide), IconBtn chevron-right, SolidView 1x28, IconBtn minus, Label zoom % (66 wide), IconBtn plus, SolidView, IconBtn fit-width, IconBtn fit-page", + "cite": [ + "makepad widgets/src/view_ui.rs:227 (RoundedShadowView)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'" + ], + "geometry": { + "height": 53, + "radius": 26.5, + "bottom": 12, + "padding": 14 + }, + "colors": { + "light": { + "pill": "#ffffff", + "line": "#e1e2e3", + "text": "#55575b", + "icon": "#5b5d61", + "shadow": "#00000022" + }, + "dark": { + "pill": "#282d32", + "line": "#383d41", + "text": "#b0b2b4", + "icon": "#a9acae", + "shadow": "#00000070" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 576, + 912, + 530, + 56 + ], + "status": "built", + "source": { + "text": [ + { + "i": 53, + "text": "3 / 24", + "ocr_height": 15.6, + "ink": "#626364", + "ground": "#f9f9f9", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 54, + "text": "-", + "ocr_height": 11.2, + "ink": "#777779", + "ground": "#fdfdfd", + "ink_px": 2, + "size": 2.0 + }, + { + "i": 55, + "text": "100%", + "ocr_height": 13.4, + "ink": "#6a6b6c", + "ground": "#f8f8f8", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 56, + "text": "+", + "ocr_height": 20.1, + "ink": "#767778", + "ground": "#fdfdfd", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [], + "dividers": [ + 11, + 12 + ] + } + }, + { + "id": "page1", + "role": "page: the engine's render", + "page_content": true, + "widget": "RectShadowView (white, shadow 7) with Image (binary_resource of pdf.page's PNG at 72 x zoom x 2 dpi), LoadingSpinner + 'Rendering page n…' until it lands; overlays placed by margin in points x zoom; GestureView over it (on_tap/on_double_tap in page points)", + "cite": [ + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/loading_spinner.rs:7 (LoadingSpinner); App Flow docs/SCRIPT-API.md 'Widgets available'", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": { + "size": "the engine's page size x zoom (100% = 1 pt per layout point)" + }, + "colors": { + "light": { + "page": "#ffffff", + "shadow": "#00000022" + }, + "dark": { + "page": "#ffffff", + "shadow": "#00000070" + } + }, + "decision": "Page contents are sample art: every page is the engine's render (pdf.page); highlights, matches and fields are overlays from the engine's rectangles.", + "image_rect": [ + 561, + 120, + 588, + 633 + ], + "status": "built", + "source": { + "text": [ + 21, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47 + ], + "surfaces": [ + 0, + 1, + 14, + 17, + 20, + 23 + ], + "dividers": [ + 4, + 5, + 6, + 7, + 8, + 17 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "page2", + "role": "page: the engine's render", + "page_content": true, + "widget": "RectShadowView (white, shadow 7) with Image (binary_resource of pdf.page's PNG at 72 x zoom x 2 dpi), LoadingSpinner + 'Rendering page n…' until it lands; overlays placed by margin in points x zoom; GestureView over it (on_tap/on_double_tap in page points)", + "cite": [ + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/loading_spinner.rs:7 (LoadingSpinner); App Flow docs/SCRIPT-API.md 'Widgets available'", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": { + "size": "the engine's page size x zoom (100% = 1 pt per layout point)" + }, + "colors": { + "light": { + "page": "#ffffff", + "shadow": "#00000022" + }, + "dark": { + "page": "#ffffff", + "shadow": "#00000070" + } + }, + "decision": "Page contents are sample art: every page is the engine's render (pdf.page); highlights, matches and fields are overlays from the engine's rectangles.", + "image_rect": [ + 564, + 760, + 581, + 218 + ], + "status": "built", + "source": { + "text": [ + 48, + 50, + 51, + 52 + ], + "surfaces": [ + 3 + ], + "dividers": [ + 9, + 10, + 13 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "renders", + "role": "Pages panel thumbnails: the engine's renders", + "page_content": true, + "widget": "Image (binary_resource of a pdf.page PNG) inside its frame", + "cite": [ + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": {}, + "colors": {}, + "decision": "Page contents are sample art: every page image is the engine's render (pdf.page).", + "image_rect": [ + 135, + 148, + 100, + 806 + ], + "status": "built", + "source": { + "text": [ + 17, + 18, + 19, + 20, + 24 + ], + "surfaces": [ + 2, + 5, + 7, + 8, + 9, + 10, + 11, + 12, + 15, + 18, + 24, + 25, + 26 + ], + "dividers": [], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "tabs", + "role": "document tabs", + "widget": "SolidView tabs (on_render tabs_view): Tab = RoundedView per open PDF (Svg file-pdf 16, Label title with ellipsis, IconBtn close 13), Outline button 'Open' (Svg plus) after the last tab; Home tab only on Home", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256" + ], + "geometry": { + "height": 53, + "x": 0, + "width": "Fill", + "tab": "Fit, 48 tall from y 11; the row clips 6 so the open tab joins the mode bar", + "tab_padding": [ + 16, + 8 + ], + "gap": 2, + "open": "Outline 36 tall, 6 after the last tab" + }, + "colors": { + "light": { + "row": "#f4f4f5", + "tab_on": "#ffffff", + "border": "#e1e2e3", + "text_on": "#37393c", + "text": "#76787c" + }, + "dark": { + "row": "#191e21", + "tab_on": "#232a2e", + "border": "#383d41", + "text_on": "#d5d7d8", + "text": "#9a9da0" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "02 puts Open at the far right; 01 and 03-08 put it after the last tab, which the build follows", + "image_rect": [ + 0, + 0, + 1536, + 53 + ], + "status": "built", + "source": { + "text": [ + { + "i": 5, + "text": "Q3 2026 Board Report.pdf", + "ocr_height": 15.6, + "ink": "#414142", + "ground": "#f4f4f4", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 6, + "text": "Riverside Lease 2026.pdf x", + "ocr_height": 15.6, + "ink": "#898a8c", + "ground": "#f3f3f3", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 7, + "text": "Field Guide to Garden Birds.pdf x", + "ocr_height": 15.6, + "ink": "#898a8b", + "ground": "#f2f2f3", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 8, + "text": "Invoice INV-2041.pdf", + "ocr_height": 15.6, + "ink": "#858688", + "ground": "#f2f2f2", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 9, + "text": "Site Survey Photos.pdf", + "ocr_height": 15.6, + "ink": "#8a8b8d", + "ground": "#f2f3f3", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 22, + "text": "+ Open", + "ocr_height": 16, + "ink": "#525254", + "ground": "#f5f5f5", + "ink_px": 13, + "size": 10.5 + } + ], + "surfaces": [ + { + "i": 4, + "bounds": [ + 1, + 1, + 1535, + 53 + ], + "fill": "#f4f4f5", + "radius": 3 + }, + { + "i": 21, + "bounds": [ + 1429, + 11, + 93, + 37 + ], + "fill": "#f8f8f8", + "radius": 0 + }, + { + "i": 22, + "bounds": [ + 1431, + 13, + 88, + 32 + ], + "fill": "#f8f8f8", + "radius": 0 + } + ], + "dividers": [ + 0 + ] + } + }, + { + "id": "modebar", + "role": "mode bar: Read · Comment · Fill & Sign · Pages · Combine · Edit, search field, Undo, Redo, Save", + "widget": "SolidView modebar (on_render modebar_view): per mode a fixed-width column (Flat text button + 3 pt SolidView underline, accent when current); RoundedView search box with Svg search and TextInput (on_return find), IconBtn undo/redo (faint when unavailable), SolidView 1x26 divider, Primary 'Save' with Svg save", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": { + "height": 54, + "padding": [ + 15, + 19 + ], + "mode_columns": [ + 69, + 101, + 100, + 78, + 99, + 61 + ], + "underline": "3 pt, inset 16, 1 pt above the bar's line", + "search": "Fill, at most 344, right-aligned, x 35, radius 5", + "undo_redo": "34 x 34, icons 20", + "save": "128 x 33, radius 5" + }, + "colors": { + "light": { + "bar": "#ffffff", + "mode": "#55575b", + "current": "#1c6ce0", + "underline": "#116cdc", + "field": "#ffffff", + "field_line": "#dcdcde", + "placeholder": "#9c9da0", + "save": "#116cdc" + }, + "dark": { + "bar": "#232a2e", + "mode": "#b0b2b4", + "current": "#4d91e3", + "underline": "#2f75d8", + "field": "#1e2429", + "field_line": "#3c4043", + "placeholder": "#7d8184", + "save": "#2f75d8" + } + }, + "font": "theme.font_regular", + "size": 12.5, + "current_mode": "read", + "notes": "", + "image_rect": [ + 0, + 53, + 1536, + 54 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 0, + "text": "Read", + "ocr_height": 13.4, + "ink": "#3c7ae8", + "ground": "#f2f6fd", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 10, + "text": "Comment", + "ocr_height": 13.7, + "ink": "#636465", + "ground": "#f7f7f7", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 11, + "text": "Fill & Sign", + "ocr_height": 16.2, + "ink": "#5c5d5e", + "ground": "#f6f6f7", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 12, + "text": "Pages", + "ocr_height": 15.6, + "ink": "#666667", + "ground": "#f8f8f8", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 13, + "text": "Combine", + "ocr_height": 14.7, + "ink": "#636464", + "ground": "#f6f6f7", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 14, + "text": "Edit", + "ocr_height": 13.4, + "ink": "#6d6d6e", + "ground": "#f7f7f7", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 15, + "text": "Q Search in document", + "ocr_height": 16.0, + "ink": "#9c9d9f", + "ground": "#fafafa", + "ink_px": 16, + "size": 16.0 + }, + { + "i": 23, + "text": "Save", + "ocr_height": 11.2, + "ink": "#b3d5f6", + "ground": "#156edf", + "ink_px": 11, + "size": 11.0 + } + ], + "surfaces": [ + { + "i": 6, + "bounds": [ + 932, + 62, + 344, + 35 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 13, + "bounds": [ + 935, + 66, + 337, + 28 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 19, + "bounds": [ + 1389, + 64, + 128, + 33 + ], + "fill": "#116cdc", + "radius": 4 + } + ], + "dividers": [ + 1, + 2, + 3 + ] + } + }, + { + "id": "rail", + "role": "left rail: Pages, Outline, Comments, Search", + "widget": "SolidView rail (on_render rail_view): GestureView 69 x 77 per item holding a RoundedView (tint when selected), Svg 26 and Label", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 84, + "item": [ + 69, + 77 + ], + "first_top": 13, + "pitch": 85, + "radius": 6 + }, + "colors": { + "light": { + "rail": "#ffffff", + "selected": "#e9f2fe", + "selected_ink": "#2f6fd0", + "icon": "#5b5d61", + "label": "#76787c", + "line": "#e1e2e3" + }, + "dark": { + "rail": "#1e252a", + "selected": "#275294", + "selected_ink": "#bac7dd", + "icon": "#a9acae", + "label": "#9a9da0", + "line": "#383d41" + } + }, + "font": "theme.font_regular", + "size": 10.5, + "selected": "pages", + "notes": "", + "image_rect": [ + 0, + 107, + 84, + 871 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 1, + "text": "Pages", + "ocr_height": 15.6, + "ink": "#5973a6", + "ground": "#e7f0fb", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 2, + "text": "Outline", + "ocr_height": 13.4, + "ink": "#6c6d6e", + "ground": "#f6f6f6", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 3, + "text": "Comments", + "ocr_height": 11.2, + "ink": "#808082", + "ground": "#f7f7f7", + "ink_px": 9, + "size": 9.0 + }, + { + "i": 4, + "text": "Search", + "ocr_height": 11.2, + "ink": "#6c6d6f", + "ground": "#f4f4f4", + "ink_px": 9, + "size": 9.0 + } + ], + "surfaces": [ + { + "i": 16, + "bounds": [ + 7, + 120, + 69, + 77 + ], + "fill": "#e9f2fe", + "radius": 4 + } + ], + "dividers": [ + 14 + ] + } + }, + { + "id": "panel", + "role": "left panel: Pages (thumbnails)", + "widget": "SolidView panel_narrow: head (Label 'Pages' bold 13, IconBtn close), ScrollYView thumbs (static: keeps its scroll) with per page a GestureView row: RoundedView frame (accent 2 pt when current), RoundedView page box 91 wide with Image (pdf.page at 24 dpi), Label page number", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:805 (ScrollYView); a view keeps its scroll position across its own render(): view.rs:296-306", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 204, + "head": 46, + "thumb_width": 91, + "thumb_left": 52, + "gap": 14, + "window": "40 pages around the current one" + }, + "colors": { + "light": { + "panel": "#ffffff", + "frame_on": "#116cdc", + "thumb_line": "#d3d5d9", + "number": "#76787c", + "number_on": "#116cdc" + }, + "dark": { + "panel": "#192025", + "frame_on": "#2f75d8", + "thumb_line": "#464b50", + "number": "#9a9da0", + "number_on": "#2f75d8" + } + }, + "font": "theme.font_bold", + "size": 13, + "notes": "no close cross in 02; 07 and 09 have one, which the build keeps", + "image_rect": [ + 84, + 107, + 205, + 871 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 16, + "text": "Pages", + "ocr_height": 18.3, + "ink": "#38393b", + "ground": "#f3f3f3", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 25, + "text": "3", + "ocr_height": 17.8, + "ink": "#296fe7", + "ground": "#f4f8fd", + "ink_px": 22, + "size": 22.0 + }, + { + "i": 49, + "text": "5", + "ocr_height": 15.6, + "ink": "#757679", + "ground": "#f7f8f8", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [], + "dividers": [ + 15, + 16 + ] + } + }, + { + "id": "canvas", + "role": "canvas: the desk behind the pages", + "widget": "ScrollXYView canvas (on_render canvas_view): a run of up to 8 pages from the page gone to (go_page: drawn empty for one frame so the scroll clamps to the top, then filled), 'Continue with page n' after the run", + "cite": [ + "makepad widgets/src/view_ui.rs:790 (ScrollXYView); scroll clamped to content each draw: scroll_bar.rs:1264-1267", + "makepad widgets/src/view.rs:904-929 (render), a fn returning a widget may be called in on_render and inside literals (card-host probe, pdftools2-spike/scope)" + ], + "geometry": { + "width": "Fill", + "page_top": 13, + "page_gap": 13, + "bottom": 92, + "sides": 24, + "align": "centre" + }, + "colors": { + "light": { + "desk": "#eceded" + }, + "dark": { + "desk": "#191f24" + } + }, + "notes": "", + "image_rect": [ + 289, + 107, + 1247, + 871 + ], + "status": "built", + "decision": null, + "source": { + "text": [], + "surfaces": [], + "dividers": [] + } + }, + { + "id": "status", + "role": "status line: save state, storage used", + "widget": "SolidView status (on_render status_view): RoundedView 12 dot (green Saved, orange Edited/Saving), Label state, Label ' of 64 MB used' (the library's PDFs against the 64 MiB storage); on Home the label and a RoundedView bar", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "height": 44, + "padding": [ + 33, + 29 + ], + "dot": 12, + "gap": 11 + }, + "colors": { + "light": { + "status": "#ffffff", + "text": "#76787c", + "saved": "#49b039", + "edited": "#f2a20d" + }, + "dark": { + "status": "#1b2226", + "text": "#9a9da0", + "saved": "#38a540", + "edited": "#e39a1c" + } + }, + "font": "theme.font_regular", + "size": 11.5, + "state": "Saved", + "notes": "", + "image_rect": [ + 0, + 978, + 1536, + 46 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 57, + "text": "Saved", + "ocr_height": 13.4, + "ink": "#79797b", + "ground": "#f8f8f8", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 58, + "text": "14.6 of 64 MB used", + "ocr_height": 13.4, + "ink": "#6d6d6e", + "ground": "#f6f6f6", + "ink_px": 11, + "size": 11.0 + } + ], + "surfaces": [], + "dividers": [] + } + } + ], + "coverage": { + "text": 59, + "surfaces": 27, + "dividers": 18, + "unmapped": 0 + } +} diff --git a/apps/pdftools/design/map/03-find.json b/apps/pdftools/design/map/03-find.json new file mode 100644 index 00000000..75f8b446 --- /dev/null +++ b/apps/pdftools/design/map/03-find.json @@ -0,0 +1,2028 @@ +{ + "schema": 1, + "screen": "03-find", + "state": "Find 'revenue': 12 matches on 7 pages, the second current", + "image": "source/03-find.png", + "image_sha256": "aec86c4a1cd6531b029285d31084665b45d4cdf2af465a12c795d152575f7d69", + "measured": "source/03-find.measured.json", + "measured_sha256": "a2c6a93fe1447181860c2f4eb3414ec17c3d56ea43f208a4ac9abac63efaf598", + "units": "1 image pixel = 1 layout point (the designs are a 1536 x 1024 window)", + "built_in": "apps/pdftools/bundle/main.splash (v2)", + "runtime": "OctoSense-org/makepad d653bc44 with runtime-patches.lock.json, as tools/setup.py prepares .sources/makepad", + "type_rule": "size in theme units: the ink height for runs without descenders, ink / 1.23 with them (calibrated in card-host on the runtime's own text); the build may round", + "colours": "light from this screen (or the light screens), dark from 09-dark-reading; tokens below, selected by the shared interface's ui_light", + "tokens": { + "pt_accent": { + "light": "#116cdc", + "dark": "#2f75d8", + "from": "02 s19 Save fill #116cdc; 09 s16 Save #246acf, s08 outline selection #2970d7" + }, + "pt_link": { + "light": "#1c6ce0", + "dark": "#4d91e3", + "from": "02 t00 'Read' ink #3c7ae8 (anti-aliased); 09 t00 #468bd8" + }, + "pt_tabs": { + "light": "#f4f4f5", + "dark": "#191e21", + "from": "02 s04 tab row #f4f4f5; 09 tab row sample (1350,30) #191e21" + }, + "pt_tab_on": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 active tab; 09 active tab ground #22282c" + }, + "pt_bar": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 mode bar #fefefe; 09 t07 ground #22292d" + }, + "pt_rail": { + "light": "#ffffff", + "dark": "#1e252a", + "from": "02 rail #fefefe; 09 s02 #1e252a" + }, + "pt_panel": { + "light": "#ffffff", + "dark": "#192025", + "from": "02 s02 #fefefe; 09 s01 #192025" + }, + "pt_desk": { + "light": "#eceded", + "dark": "#191f24", + "from": "02 s00 #eceded; 09 desk sample (420,500) #191f24" + }, + "pt_home": { + "light": "#f7f8f9", + "dark": "#191f24", + "from": "01 s00 #f7f8f9; dark Home not designed: 09's desk" + }, + "pt_status": { + "light": "#ffffff", + "dark": "#1b2226", + "from": "02 status #fefefe; 09 status sample (700,1000) #1b2226" + }, + "pt_pill": { + "light": "#ffffff", + "dark": "#282d32", + "from": "02 pill #fefefe; 09 s04 #282d32" + }, + "pt_card": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "04/06/08 cards #fefefe; dark: 09 bar colour (cards not in 09)" + }, + "pt_line": { + "light": "#e1e2e3", + "dark": "#383d41", + "from": "02 d00 #e1e2e3; 09 d24 #383d41" + }, + "pt_line_soft": { + "light": "#ececee", + "dark": "#2c3236", + "from": "02 d02 #ecedee; 09 d07 #303437" + }, + "pt_thumb_line": { + "light": "#d3d5d9", + "dark": "#464b50", + "from": "02 thumbnails' border (s15 #e0e1f0 on the page edge, darker at 1x); dark derived" + }, + "pt_tint": { + "light": "#e9f2fe", + "dark": "#275294", + "from": "02 s16 rail selection #e9f2fe; 09 s14 #275293" + }, + "pt_tint_line": { + "light": "#cfe0fa", + "dark": "#34619f", + "from": "04 s27 Highlight tool border #dfebfc; dark derived from pt_tint" + }, + "pt_tint_ink": { + "light": "#2f6fd0", + "dark": "#bac7dd", + "from": "02 t01 rail 'Pages' ink #5973a6 (anti-aliased blue); 09 t03 #bac7dd" + }, + "pt_select": { + "light": "#eff5fc", + "dark": "#22344d", + "from": "03 s06 result group fill #eff5fc; dark derived" + }, + "pt_field": { + "light": "#ffffff", + "dark": "#1e2429", + "from": "02 s06 search #fefefe; 09 s10 #1e2429" + }, + "pt_field_line": { + "light": "#dcdcde", + "dark": "#3c4043", + "from": "02 d01 #dcdcde; 09 d02 #3c4043" + }, + "pt_hover": { + "light": "#f1f2f3", + "dark": "#2b3237", + "from": "hover: derived" + }, + "pt_ink": { + "light": "#37393c", + "dark": "#d5d7d8", + "from": "02 t16 'Pages' title #38393b, t05 tab #414142; 09 t06 tab #d1d3d4, t09 #d7d9da" + }, + "pt_ink2": { + "light": "#55575b", + "dark": "#b0b2b4", + "from": "02 mode labels #5c5d5e-#6d6d6e; 09 mode labels #a9abad-#b3b4b4" + }, + "pt_muted": { + "light": "#76787c", + "dark": "#9a9da0", + "from": "02 t57 'Saved' #79797b, t58 #6d6d6e; 09 t59 #929698, t60 #9b9d9f" + }, + "pt_faint": { + "light": "#9c9da0", + "dark": "#7d8184", + "from": "02 t15 placeholder #9c9d9f; 09 dimmer than t25 #9fa2a5" + }, + "pt_icon": { + "light": "#5b5d61", + "dark": "#a9acae", + "from": "02 icons; 09 icons" + }, + "pt_green": { + "light": "#49b039", + "dark": "#38a540", + "from": "02 'Saved' dot sample #49b039; 09 #38a540" + }, + "pt_orange": { + "light": "#f2a20d", + "dark": "#e39a1c", + "from": "08 'Edited' dot (31,984)" + }, + "pt_red": { + "light": "#e5534f", + "dark": "#f06a66", + "from": "07 t74 'Monthly rent *' #e85652, t75 #ed4f4a" + }, + "pt_shadow": { + "light": "#00000022", + "dark": "#00000070", + "from": "page and pill shadows: soft, from 02 page edge #e9eaeb" + } + }, + "regions": [ + { + "id": "pill", + "role": "canvas controls: page n / N, previous, next, zoom out, zoom %, zoom in, fit width, fit page", + "widget": "View pill (overlay, bottom centre; on_render pill_view): RoundedShadowView radius 26.5 with IconBtn chevron-left, Label 'p / N' (78 wide), IconBtn chevron-right, SolidView 1x28, IconBtn minus, Label zoom % (66 wide), IconBtn plus, SolidView, IconBtn fit-width, IconBtn fit-page", + "cite": [ + "makepad widgets/src/view_ui.rs:227 (RoundedShadowView)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'" + ], + "geometry": { + "height": 53, + "radius": 26.5, + "bottom": 12, + "padding": 14 + }, + "colors": { + "light": { + "pill": "#ffffff", + "line": "#e1e2e3", + "text": "#55575b", + "icon": "#5b5d61", + "shadow": "#00000022" + }, + "dark": { + "pill": "#282d32", + "line": "#383d41", + "text": "#b0b2b4", + "icon": "#a9acae", + "shadow": "#00000070" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 546, + 927, + 446, + 54 + ], + "status": "built", + "source": { + "text": [ + { + "i": 84, + "text": "<", + "ocr_height": 13.4, + "ink": "#4b4c52", + "ground": "#f8f8f9", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 85, + "text": "3 / 24", + "ocr_height": 13.4, + "ink": "#5e6065", + "ground": "#f8f8f8", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 86, + "text": ">", + "ocr_height": 15.6, + "ink": "#484a51", + "ground": "#fafafa", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 87, + "text": "- 100%", + "ocr_height": 14.1, + "ink": "#5e6064", + "ground": "#f9f9fa", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 88, + "text": "+", + "ocr_height": 17.8, + "ink": "#676970", + "ground": "#fafbfb", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 89, + "text": "ГОлRС СОС ОООIШC ЗUЗоОВCCKоIlK Cotoranc", + "ocr_height": 5, + "ink": "#818285", + "ground": "#ededee", + "ink_px": 4, + "size": 4.0 + } + ], + "surfaces": [], + "dividers": [ + 18 + ] + } + }, + { + "id": "page1", + "role": "page: the engine's render", + "page_content": true, + "widget": "RectShadowView (white, shadow 7) with Image (binary_resource of pdf.page's PNG at 72 x zoom x 2 dpi), LoadingSpinner + 'Rendering page n…' until it lands; overlays placed by margin in points x zoom; GestureView over it (on_tap/on_double_tap in page points)", + "cite": [ + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/loading_spinner.rs:7 (LoadingSpinner); App Flow docs/SCRIPT-API.md 'Widgets available'", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": { + "size": "the engine's page size x zoom (100% = 1 pt per layout point)" + }, + "colors": { + "light": { + "page": "#ffffff", + "shadow": "#00000022" + }, + "dark": { + "page": "#ffffff", + "shadow": "#00000070" + } + }, + "decision": "Page contents are sample art: every page is the engine's render (pdf.page); highlights, matches and fields are overlays from the engine's rectangles.", + "image_rect": [ + 504, + 162, + 573, + 677 + ], + "status": "built", + "source": { + "text": [ + 45, + 46, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80 + ], + "surfaces": [ + 1, + 2, + 3, + 12, + 16, + 22, + 29, + 35, + 38, + 42, + 44 + ], + "dividers": [ + 4, + 5, + 6, + 7, + 8, + 9, + 12, + 14, + 15, + 23, + 24, + 25, + 26, + 27 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "page2", + "role": "page: the engine's render", + "page_content": true, + "widget": "RectShadowView (white, shadow 7) with Image (binary_resource of pdf.page's PNG at 72 x zoom x 2 dpi), LoadingSpinner + 'Rendering page n…' until it lands; overlays placed by margin in points x zoom; GestureView over it (on_tap/on_double_tap in page points)", + "cite": [ + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/loading_spinner.rs:7 (LoadingSpinner); App Flow docs/SCRIPT-API.md 'Widgets available'", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": { + "size": "the engine's page size x zoom (100% = 1 pt per layout point)" + }, + "colors": { + "light": { + "page": "#ffffff", + "shadow": "#00000022" + }, + "dark": { + "page": "#ffffff", + "shadow": "#00000070" + } + }, + "decision": "Page contents are sample art: every page is the engine's render (pdf.page); highlights, matches and fields are overlays from the engine's rectangles.", + "image_rect": [ + 504, + 846, + 573, + 139 + ], + "status": "built", + "source": { + "text": [ + 47, + 81, + 82, + 83 + ], + "surfaces": [], + "dividers": [ + 16, + 17 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "tabs", + "role": "document tabs", + "widget": "SolidView tabs (on_render tabs_view): Tab = RoundedView per open PDF (Svg file-pdf 16, Label title with ellipsis, IconBtn close 13), Outline button 'Open' (Svg plus) after the last tab; Home tab only on Home", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256" + ], + "geometry": { + "height": 53, + "x": 0, + "width": "Fill", + "tab": "Fit, 48 tall from y 11; the row clips 6 so the open tab joins the mode bar", + "tab_padding": [ + 16, + 8 + ], + "gap": 2, + "open": "Outline 36 tall, 6 after the last tab" + }, + "colors": { + "light": { + "row": "#f4f4f5", + "tab_on": "#ffffff", + "border": "#e1e2e3", + "text_on": "#37393c", + "text": "#76787c" + }, + "dark": { + "row": "#191e21", + "tab_on": "#232a2e", + "border": "#383d41", + "text_on": "#d5d7d8", + "text": "#9a9da0" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 0, + 0, + 1536, + 48 + ], + "status": "built", + "source": { + "text": [ + { + "i": 6, + "text": "Q3 2026 Board Report.pdf x", + "ocr_height": 16, + "ink": "#5a5c60", + "ground": "#f7f8f8", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 7, + "text": "11.", + "ocr_height": 13.4, + "ink": "#ae6260", + "ground": "#f0dfde", + "ink_px": 19, + "size": 19.0 + }, + { + "i": 8, + "text": "Riverside Lease 2026.pdf", + "ocr_height": 14, + "ink": "#686a6f", + "ground": "#f2f3f4", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 48, + "text": "Field Guide to Garden Birds.pdf", + "ocr_height": 14, + "ink": "#686a6f", + "ground": "#f3f3f5", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 49, + "text": "Invoice INV-2041.pdf", + "ocr_height": 14, + "ink": "#6a6c71", + "ground": "#f3f4f5", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 50, + "text": "Site Survey Photos.pdf", + "ocr_height": 14, + "ink": "#6a6b70", + "ground": "#f2f3f5", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 51, + "text": "X", + "ocr_height": 11.2, + "ink": "#8a8d94", + "ground": "#f6f7f8", + "ink_px": 8, + "size": 8.0 + }, + { + "i": 115, + "text": "+ Open", + "ocr_height": 16.3, + "ink": "#585a5f", + "ground": "#f5f6f7", + "ink_px": 15, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 10, + "bounds": [ + 539, + 9, + 284, + 37 + ], + "fill": "#f8f8fa", + "radius": 1 + }, + { + "i": 13, + "bounds": [ + 278, + 9, + 255, + 37 + ], + "fill": "#f8f8fa", + "radius": 1 + }, + { + "i": 14, + "bounds": [ + 1070, + 9, + 246, + 37 + ], + "fill": "#f8f8fa", + "radius": 0 + }, + { + "i": 15, + "bounds": [ + 825, + 9, + 238, + 37 + ], + "fill": "#f8f8fa", + "radius": 1 + }, + { + "i": 33, + "bounds": [ + 1442, + 11, + 82, + 33 + ], + "fill": "#f8f9fa", + "radius": 0 + } + ], + "dividers": [] + } + }, + { + "id": "modebar", + "role": "mode bar: Read · Comment · Fill & Sign · Pages · Combine · Edit, search field, Undo, Redo, Save", + "widget": "SolidView modebar (on_render modebar_view): per mode a fixed-width column (Flat text button + 3 pt SolidView underline, accent when current); RoundedView search box with Svg search and TextInput (on_return find), IconBtn undo/redo (faint when unavailable), SolidView 1x26 divider, Primary 'Save' with Svg save", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": { + "height": 54, + "padding": [ + 15, + 19 + ], + "mode_columns": [ + 69, + 101, + 100, + 78, + 99, + 61 + ], + "underline": "3 pt, inset 16, 1 pt above the bar's line", + "search": "Fill, at most 344, right-aligned, x 35, radius 5", + "undo_redo": "34 x 34, icons 20", + "save": "128 x 33, radius 5" + }, + "colors": { + "light": { + "bar": "#ffffff", + "mode": "#55575b", + "current": "#1c6ce0", + "underline": "#116cdc", + "field": "#ffffff", + "field_line": "#dcdcde", + "placeholder": "#9c9da0", + "save": "#116cdc" + }, + "dark": { + "bar": "#232a2e", + "mode": "#b0b2b4", + "current": "#4d91e3", + "underline": "#2f75d8", + "field": "#1e2429", + "field_line": "#3c4043", + "placeholder": "#7d8184", + "save": "#2f75d8" + } + }, + "font": "theme.font_regular", + "size": 12.5, + "current_mode": "read", + "notes": "the search field shows the query and a clear cross", + "image_rect": [ + 0, + 48, + 1536, + 54 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 0, + "text": "Read", + "ocr_height": 13.4, + "ink": "#3e7ce0", + "ground": "#f0f5fc", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 9, + "text": "Comment", + "ocr_height": 13.4, + "ink": "#535559", + "ground": "#f5f5f6", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 10, + "text": "Fill & Sign", + "ocr_height": 16.1, + "ink": "#515355", + "ground": "#f5f6f6", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 11, + "text": "Pages", + "ocr_height": 15.6, + "ink": "#4f5055", + "ground": "#f5f5f5", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 12, + "text": "Combine", + "ocr_height": 13.4, + "ink": "#4f5155", + "ground": "#f3f3f4", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 44, + "text": "Edit", + "ocr_height": 13.4, + "ink": "#484a50", + "ground": "#f1f2f2", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 52, + "text": "Q", + "ocr_height": 20.1, + "ink": "#5c5e65", + "ground": "#f8f8f8", + "ink_px": 17, + "size": 17.0 + }, + { + "i": 53, + "text": "revenue", + "ocr_height": 12.1, + "ink": "#54555a", + "ground": "#f5f5f5", + "ink_px": 8, + "size": 8.0 + }, + { + "i": 116, + "text": "Save", + "ocr_height": 13.4, + "ink": "#c1daf5", + "ground": "#1569d9", + "ink_px": 11, + "size": 11.0 + } + ], + "surfaces": [ + { + "i": 9, + "bounds": [ + 974, + 59, + 353, + 37 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 11, + "bounds": [ + 977, + 62, + 346, + 30 + ], + "fill": "#fefefe", + "radius": 1 + }, + { + "i": 34, + "bounds": [ + 1442, + 60, + 79, + 34 + ], + "fill": "#1167d7", + "radius": 4 + } + ], + "dividers": [ + 0, + 1 + ] + } + }, + { + "id": "rail", + "role": "left rail: Pages, Outline, Comments, Search", + "widget": "SolidView rail (on_render rail_view): GestureView 69 x 77 per item holding a RoundedView (tint when selected), Svg 26 and Label", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 84, + "item": [ + 69, + 77 + ], + "first_top": 13, + "pitch": 85, + "radius": 6 + }, + "colors": { + "light": { + "rail": "#ffffff", + "selected": "#e9f2fe", + "selected_ink": "#2f6fd0", + "icon": "#5b5d61", + "label": "#76787c", + "line": "#e1e2e3" + }, + "dark": { + "rail": "#1e252a", + "selected": "#275294", + "selected_ink": "#bac7dd", + "icon": "#a9acae", + "label": "#9a9da0", + "line": "#383d41" + } + }, + "font": "theme.font_regular", + "size": 10.5, + "selected": "search", + "notes": "", + "image_rect": [ + 0, + 102, + 100, + 883 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 1, + "text": "Pages", + "ocr_height": 13.4, + "ink": "#989ba1", + "ground": "#f7f8f8", + "ink_px": 12, + "size": 10.0 + }, + { + "i": 2, + "text": ":=", + "ocr_height": 17.8, + "ink": "#7c8086", + "ground": "#f8f9f9", + "ink_px": 17, + "size": 17.0 + }, + { + "i": 3, + "text": "Outline", + "ocr_height": 13.4, + "ink": "#9ea0a7", + "ground": "#f9f9f9", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 4, + "text": "Comments", + "ocr_height": 11.2, + "ink": "#a0a2a7", + "ground": "#f8f8f9", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 5, + "text": "Search", + "ocr_height": 13.4, + "ink": "#508ae0", + "ground": "#e6f0fb", + "ink_px": 10, + "size": 10.0 + } + ], + "surfaces": [ + { + "i": 17, + "bounds": [ + 9, + 359, + 84, + 69 + ], + "fill": "#e8f1fb", + "radius": 4 + } + ], + "dividers": [ + 20 + ] + } + }, + { + "id": "results", + "role": "left panel: Search results grouped by page", + "widget": "SolidView panel_wide (288): head (Label ' matches on pages' bold 13, IconBtn close), ScrollYView results: per page a RoundedView group (Label 'Page n', Label ' match(es)'; selected: accent border 1.5 and tint), per match a GestureView (go_match) with Html snippet (the match in ), hairlines between snippets", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:805 (ScrollYView); a view keeps its scroll position across its own render(): view.rs:296-306", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/html.rs:75 (Html), body html.rs:196; apps/mail/bundle/main.splash:268", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256" + ], + "geometry": { + "width": 288, + "group_padding": 12, + "snippet": "Html 11.5" + }, + "colors": { + "light": { + "panel": "#ffffff", + "selected": "#eff5fc", + "border": "#116cdc", + "page": "#37393c", + "count": "#9c9da0", + "snippet": "#55575b" + }, + "dark": { + "panel": "#192025", + "selected": "#22344d", + "border": "#2f75d8", + "page": "#d5d7d8", + "count": "#7d8184", + "snippet": "#b0b2b4" + } + }, + "font": "theme.font_regular", + "size": 11.5, + "notes": "the filter icon beside the count is not built: nothing in the brief filters results", + "image_rect": [ + 100, + 102, + 290, + 883 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 13, + "text": "12 matches on 7 pages", + "ocr_height": 15.6, + "ink": "#888b91", + "ground": "#f8f9f9", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 14, + "text": "Page 1", + "ocr_height": 16, + "ink": "#444649", + "ground": "#f6f6f6", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 15, + "text": "1 match", + "ocr_height": 11.4, + "ink": "#adafb5", + "ground": "#fafafa", + "ink_px": 9, + "size": 9.0 + }, + { + "i": 16, + "text": "... strong start to Q3. Total revenue", + "ocr_height": 15.6, + "ink": "#727478", + "ground": "#f8f8f9", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 17, + "text": "increased 12% year over year...", + "ocr_height": 15.6, + "ink": "#8a8d92", + "ground": "#f8f8f9", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 18, + "text": "Page 3", + "ocr_height": 16.1, + "ink": "#484d56", + "ground": "#ecf0f6", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 19, + "text": "Revenue grew 12% year over year,", + "ocr_height": 16.1, + "ink": "#60656f", + "ground": "#ecf1f8", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 20, + "text": "led by the Northwest region.", + "ocr_height": 16.0, + "ink": "#6e747f", + "ground": "#ecf1f8", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 21, + "text": "2 matches", + "ocr_height": 13.9, + "ink": "#a4acba", + "ground": "#eef4fb", + "ink_px": 9, + "size": 9.0 + }, + { + "i": 22, + "text": "... continued strength in revenue", + "ocr_height": 16.0, + "ink": "#585d67", + "ground": "#ebf0f7", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 23, + "text": "across all regions...", + "ocr_height": 15.6, + "ink": "#787e87", + "ground": "#eef3f9", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 24, + "text": "Page 4", + "ocr_height": 15.6, + "ink": "#515356", + "ground": "#f8f8f8", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 25, + "text": "1 match i", + "ocr_height": 11.2, + "ink": "#aaacb2", + "ground": "#f9f9fa", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 26, + "text": "Operating expenses rose 4% while", + "ocr_height": 18, + "ink": "#808288", + "ground": "#f8f8f9", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 27, + "text": "revenue outpaced costs...", + "ocr_height": 13.4, + "ink": "#707277", + "ground": "#f6f6f7", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 28, + "text": "Page 9", + "ocr_height": 15.6, + "ink": "#55565b", + "ground": "#f7f7f7", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 29, + "text": "1 match i", + "ocr_height": 11.2, + "ink": "#adb0b6", + "ground": "#fafbfb", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 30, + "text": "...strategic investments to drive", + "ocr_height": 16.0, + "ink": "#83858a", + "ground": "#f8f8f9", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 31, + "text": "future revenue growth ...", + "ocr_height": 18.0, + "ink": "#64666a", + "ground": "#f8f8f9", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 32, + "text": "Page 15", + "ocr_height": 15.6, + "ink": "#4e4f54", + "ground": "#f6f7f7", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 33, + "text": "1 match i", + "ocr_height": 11.3, + "ink": "#aaacb2", + "ground": "#f9f9fa", + "ink_px": 9, + "size": 9.0 + }, + { + "i": 34, + "text": "Appendix B: detailed revenue by", + "ocr_height": 15.6, + "ink": "#76787c", + "ground": "#f8f8f8", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 35, + "text": "region and product line.", + "ocr_height": 16.0, + "ink": "#8c8e93", + "ground": "#f8f9f9", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 36, + "text": "Page 16", + "ocr_height": 16, + "ink": "#545559", + "ground": "#f7f7f8", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 37, + "text": "1 match", + "ocr_height": 14, + "ink": "#a9acb2", + "ground": "#f9fafa", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 38, + "text": "Year-over-year revenue comparison", + "ocr_height": 16, + "ink": "#6d6f74", + "ground": "#f7f7f8", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 39, + "text": "shows consistent momentum ...", + "ocr_height": 13.4, + "ink": "#8f9197", + "ground": "#f7f8f8", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 40, + "text": "Page 21", + "ocr_height": 15.6, + "ink": "#3a3c3f", + "ground": "#f3f4f4", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 41, + "text": "1 match", + "ocr_height": 11.2, + "ink": "#a9acb2", + "ground": "#f9fafa", + "ink_px": 9, + "size": 9.0 + }, + { + "i": 42, + "text": "Risks to revenue include market", + "ocr_height": 16.0, + "ink": "#6e7075", + "ground": "#f7f7f8", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 43, + "text": "volatility and supply constraints.", + "ocr_height": 13.5, + "ink": "#8d8f94", + "ground": "#f6f7f8", + "ink_px": 14, + "size": 11.5 + } + ], + "surfaces": [ + { + "i": 5, + "bounds": [ + 101, + 155, + 287, + 829 + ], + "fill": "#fdfefe", + "radius": 0 + }, + { + "i": 6, + "bounds": [ + 105, + 258, + 265, + 155 + ], + "fill": "#eff5fc", + "radius": 3 + }, + { + "i": 8, + "bounds": [ + 101, + 104, + 286, + 49 + ], + "fill": "#fefefe", + "radius": 0 + } + ], + "dividers": [ + 21, + 22 + ] + } + }, + { + "id": "findbar", + "role": "find bar: 'n of N', previous, next, Done", + "widget": "SolidView 50 tall (toolrow, find_bar): RoundedView 104 x 38 with Label 'n of N', Outline 38 x 38 arrow-up / arrow-down (go_match ±1), Outline 'Done' (end_find)", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "height": 50, + "align": "centre", + "gap": 8 + }, + "colors": { + "light": { + "bar": "#ffffff", + "box": "#ffffff", + "line": "#dcdcde", + "text": "#55575b" + }, + "dark": { + "bar": "#232a2e", + "box": "#1e2429", + "line": "#3c4043", + "text": "#b0b2b4" + } + }, + "font": "theme.font_regular", + "size": 12, + "image_rect": [ + 390, + 102, + 805, + 52 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 54, + "text": "2 of 12", + "ocr_height": 13.4, + "ink": "#595b5f", + "ground": "#f7f7f7", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 55, + "text": "Done", + "ocr_height": 13.4, + "ink": "#454649", + "ground": "#f3f3f4", + "ink_px": 11, + "size": 11.0 + } + ], + "surfaces": [ + { + "i": 7, + "bounds": [ + 390, + 104, + 803, + 50 + ], + "fill": "#fafbfc", + "radius": 0 + }, + { + "i": 18, + "bounds": [ + 646, + 109, + 148, + 38 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 31, + "bounds": [ + 650, + 114, + 101, + 29 + ], + "fill": "#fdfdfd", + "radius": 1 + }, + { + "i": 36, + "bounds": [ + 844, + 111, + 62, + 35 + ], + "fill": "#fdfefe", + "radius": 0 + }, + { + "i": 39, + "bounds": [ + 795, + 111, + 43, + 36 + ], + "fill": "#fdfdfe", + "radius": 0 + }, + { + "i": 40, + "bounds": [ + 847, + 115, + 55, + 28 + ], + "fill": "#fcfdfd", + "radius": 0 + }, + { + "i": 41, + "bounds": [ + 752, + 113, + 38, + 31 + ], + "fill": "#fefefe", + "radius": 1 + }, + { + "i": 43, + "bounds": [ + 798, + 114, + 37, + 29 + ], + "fill": "#fdfdfd", + "radius": 0 + } + ], + "dividers": [ + 2, + 3 + ] + } + }, + { + "id": "matches", + "role": "match highlights on the page (overlay)", + "widget": "RoundedView per match rectangle (pdf.find rects, [x, y, w, h] points x zoom) inside the page's Overlay: yellow 42%, the current one orange 55% with a 1.5 pt border", + "cite": [ + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": { + "placement": "margin left x*zoom-1, top y*zoom-1; size w*zoom+2, h*zoom+2" + }, + "colors": { + "light": { + "match": "#ffd84d6b", + "current": "#ffa62a8c", + "current_border": "#f0a020" + }, + "dark": { + "match": "#ffd84d6b", + "current": "#ffa62a8c", + "current_border": "#f0a020" + } + }, + "image_rect": [ + 0, + 0, + 0, + 0 + ], + "status": "built", + "decision": null, + "source": { + "text": [], + "surfaces": [], + "dividers": [] + } + }, + { + "id": "document_panel", + "role": "right panel: Document properties, tags, signatures, reviewers", + "widget": "not built", + "status": "omitted", + "cite": [], + "geometry": {}, + "colors": {}, + "decision": "Find: drop the invented right 'Document' panel (tags, signatures, reviewers). Find has no right panel.", + "image_rect": [ + 1195, + 104, + 341, + 880 + ], + "source": { + "text": [ + { + "i": 90, + "text": "Document", + "ocr_height": 13.9, + "ink": "#3b3e41", + "ground": "#f2f2f2", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 91, + "text": "File name", + "ocr_height": 14.1, + "ink": "#9b9ea4", + "ground": "#f8f8f9", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 92, + "text": "Q3 2026 Board Report.pdf", + "ocr_height": 15.6, + "ink": "#5e5f63", + "ground": "#f6f6f7", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 93, + "text": "Pages", + "ocr_height": 15.6, + "ink": "#94969e", + "ground": "#f9f9fa", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 94, + "text": "24", + "ocr_height": 11.2, + "ink": "#505258", + "ground": "#f4f4f4", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 95, + "text": "File size", + "ocr_height": 13.4, + "ink": "#9c9fa5", + "ground": "#f8f8f9", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 96, + "text": "2.8 MB", + "ocr_height": 14, + "ink": "#63656a", + "ground": "#f7f7f8", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 97, + "text": "Created", + "ocr_height": 11.2, + "ink": "#a1a3aa", + "ground": "#f8f8f9", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 98, + "text": "6 Oct 2026, 09:14", + "ocr_height": 15.6, + "ink": "#585a5e", + "ground": "#f5f5f6", + "ink_px": 12, + "size": 10.0 + }, + { + "i": 99, + "text": "Modified", + "ocr_height": 11.3, + "ink": "#9b9da2", + "ground": "#f7f7f8", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 100, + "text": "6 Oct 2026, 09:14", + "ocr_height": 13.4, + "ink": "#55575b", + "ground": "#f3f4f4", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 101, + "text": "Tags", + "ocr_height": 15.6, + "ink": "#47484d", + "ground": "#f6f7f7", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 102, + "text": "• Add tag", + "ocr_height": 16.0, + "ink": "#626468", + "ground": "#f7f7f8", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 103, + "text": "Signatures", + "ocr_height": 15.6, + "ink": "#434448", + "ground": "#f5f5f5", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 104, + "text": "All signatures are valid", + "ocr_height": 15.6, + "ink": "#5c5d60", + "ground": "#f6f6f6", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 105, + "text": "Signed by: Jun Park", + "ocr_height": 15.6, + "ink": "#94959a", + "ground": "#f8f8f9", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 106, + "text": "Signed on: 6 Oct 2026, 09:10", + "ocr_height": 15.6, + "ink": "#93959b", + "ground": "#f8f9f9", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 107, + "text": "View signatures", + "ocr_height": 13.4, + "ink": "#787a7e", + "ground": "#f7f8f8", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 108, + "text": "Reviewers", + "ocr_height": 13.4, + "ink": "#424348", + "ground": "#f2f2f3", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 109, + "text": "MC", + "ocr_height": 15.6, + "ink": "#798699", + "ground": "#f1f4f8", + "ink_px": 21, + "size": 21.0 + }, + { + "i": 110, + "text": "Maya Chen (You)", + "ocr_height": 13.4, + "ink": "#65676b", + "ground": "#f5f5f6", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 111, + "text": "Jun Park", + "ocr_height": 13.4, + "ink": "#6c6d70", + "ground": "#f8f8f8", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 112, + "text": "AR", + "ocr_height": 11.2, + "ink": "#c36875", + "ground": "#fbf0f2", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 113, + "text": "Ana Ruiz", + "ocr_height": 14.5, + "ink": "#636568", + "ground": "#f6f7f7", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 114, + "text": "Add reviewer", + "ocr_height": 11.2, + "ink": "#787a7e", + "ground": "#f6f6f7", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 117, + "text": "No status", + "ocr_height": 13.6, + "ink": "#95989e", + "ground": "#f9f9f9", + "ink_px": 9, + "size": 9.0 + }, + { + "i": 118, + "text": "Reviewed", + "ocr_height": 13.4, + "ink": "#79ad89", + "ground": "#f5faf7", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 119, + "text": "Changes requested v", + "ocr_height": 14, + "ink": "#dd8484", + "ground": "#fdf8f8", + "ink_px": 11, + "size": 9.0 + } + ], + "surfaces": [ + { + "i": 4, + "bounds": [ + 1195, + 104, + 340, + 880 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 19, + "bounds": [ + 1210, + 887, + 125, + 37 + ], + "fill": "#fdfefe", + "radius": 0 + }, + { + "i": 20, + "bounds": [ + 1400, + 798, + 118, + 38 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 21, + "bounds": [ + 1400, + 753, + 118, + 38 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 23, + "bounds": [ + 1210, + 665, + 114, + 36 + ], + "fill": "#fdfdfd", + "radius": 0 + }, + { + "i": 24, + "bounds": [ + 1400, + 843, + 118, + 33 + ], + "fill": "#fefdfd", + "radius": 0 + }, + { + "i": 25, + "bounds": [ + 1213, + 890, + 119, + 31 + ], + "fill": "#fdfdfd", + "radius": 1 + }, + { + "i": 26, + "bounds": [ + 1403, + 801, + 112, + 32 + ], + "fill": "#fdfefe", + "radius": 1 + }, + { + "i": 27, + "bounds": [ + 1403, + 756, + 112, + 32 + ], + "fill": "#fdfefe", + "radius": 1 + }, + { + "i": 28, + "bounds": [ + 1213, + 668, + 107, + 30 + ], + "fill": "#fcfdfd", + "radius": 1 + }, + { + "i": 30, + "bounds": [ + 1403, + 846, + 114, + 27 + ], + "fill": "#fefcfd", + "radius": 0 + }, + { + "i": 32, + "bounds": [ + 1209, + 497, + 87, + 33 + ], + "fill": "#fcfcfd", + "radius": 0 + }, + { + "i": 37, + "bounds": [ + 1213, + 500, + 80, + 27 + ], + "fill": "#fbfbfb", + "radius": 0 + } + ], + "dividers": [ + 10, + 11, + 13, + 19, + 28, + 29 + ] + } + }, + { + "id": "canvas", + "role": "canvas: the desk behind the pages", + "widget": "ScrollXYView canvas (on_render canvas_view): a run of up to 8 pages from the page gone to (go_page: drawn empty for one frame so the scroll clamps to the top, then filled), 'Continue with page n' after the run", + "cite": [ + "makepad widgets/src/view_ui.rs:790 (ScrollXYView); scroll clamped to content each draw: scroll_bar.rs:1264-1267", + "makepad widgets/src/view.rs:904-929 (render), a fn returning a widget may be called in on_render and inside literals (card-host probe, pdftools2-spike/scope)" + ], + "geometry": { + "width": "Fill", + "page_top": 13, + "page_gap": 13, + "bottom": 92, + "sides": 24, + "align": "centre" + }, + "colors": { + "light": { + "desk": "#eceded" + }, + "dark": { + "desk": "#191f24" + } + }, + "notes": "", + "image_rect": [ + 388, + 154, + 807, + 831 + ], + "status": "built", + "decision": null, + "source": { + "text": [], + "surfaces": [], + "dividers": [] + } + }, + { + "id": "status", + "role": "status line: save state, storage used", + "widget": "SolidView status (on_render status_view): RoundedView 12 dot (green Saved, orange Edited/Saving), Label state, Label ' of 64 MB used' (the library's PDFs against the 64 MiB storage); on Home the label and a RoundedView bar", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "height": 44, + "padding": [ + 33, + 29 + ], + "dot": 12, + "gap": 11 + }, + "colors": { + "light": { + "status": "#ffffff", + "text": "#76787c", + "saved": "#49b039", + "edited": "#f2a20d" + }, + "dark": { + "status": "#1b2226", + "text": "#9a9da0", + "saved": "#38a540", + "edited": "#e39a1c" + } + }, + "font": "theme.font_regular", + "size": 11.5, + "state": "Saved", + "notes": "", + "image_rect": [ + 0, + 985, + 1536, + 39 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 120, + "text": "• Saved", + "ocr_height": 14, + "ink": "#66977f", + "ground": "#f7f8f8", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 121, + "text": "14.6 of 64 MB used", + "ocr_height": 14, + "ink": "#8f9197", + "ground": "#f9f9fa", + "ink_px": 11, + "size": 11.0 + } + ], + "surfaces": [ + { + "i": 0, + "bounds": [ + 0, + 5, + 1536, + 1019 + ], + "fill": "#fdfefe", + "radius": 0 + } + ], + "dividers": [] + } + } + ], + "coverage": { + "text": 122, + "surfaces": 45, + "dividers": 30, + "unmapped": 0 + } +} diff --git a/apps/pdftools/design/map/04-comment.json b/apps/pdftools/design/map/04-comment.json new file mode 100644 index 00000000..7e0e78f4 --- /dev/null +++ b/apps/pdftools/design/map/04-comment.json @@ -0,0 +1,1592 @@ +{ + "schema": 1, + "screen": "04-comment", + "state": "Comment mode, Highlight tool, the page 3 thread open", + "image": "source/04-comment.png", + "image_sha256": "4a87dcc47d9bb98ccc5f4bf04058fd407f847b2c4cc862e3fa32d7fb6337eaf3", + "measured": "source/04-comment.measured.json", + "measured_sha256": "5231ecd6ecf25d2ad426ebb507e11d97d4a206214c0868cf5e0ca75eceda5b75", + "units": "1 image pixel = 1 layout point (the designs are a 1536 x 1024 window)", + "built_in": "apps/pdftools/bundle/main.splash (v2)", + "runtime": "OctoSense-org/makepad d653bc44 with runtime-patches.lock.json, as tools/setup.py prepares .sources/makepad", + "type_rule": "size in theme units: the ink height for runs without descenders, ink / 1.23 with them (calibrated in card-host on the runtime's own text); the build may round", + "colours": "light from this screen (or the light screens), dark from 09-dark-reading; tokens below, selected by the shared interface's ui_light", + "tokens": { + "pt_accent": { + "light": "#116cdc", + "dark": "#2f75d8", + "from": "02 s19 Save fill #116cdc; 09 s16 Save #246acf, s08 outline selection #2970d7" + }, + "pt_link": { + "light": "#1c6ce0", + "dark": "#4d91e3", + "from": "02 t00 'Read' ink #3c7ae8 (anti-aliased); 09 t00 #468bd8" + }, + "pt_tabs": { + "light": "#f4f4f5", + "dark": "#191e21", + "from": "02 s04 tab row #f4f4f5; 09 tab row sample (1350,30) #191e21" + }, + "pt_tab_on": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 active tab; 09 active tab ground #22282c" + }, + "pt_bar": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 mode bar #fefefe; 09 t07 ground #22292d" + }, + "pt_rail": { + "light": "#ffffff", + "dark": "#1e252a", + "from": "02 rail #fefefe; 09 s02 #1e252a" + }, + "pt_panel": { + "light": "#ffffff", + "dark": "#192025", + "from": "02 s02 #fefefe; 09 s01 #192025" + }, + "pt_desk": { + "light": "#eceded", + "dark": "#191f24", + "from": "02 s00 #eceded; 09 desk sample (420,500) #191f24" + }, + "pt_home": { + "light": "#f7f8f9", + "dark": "#191f24", + "from": "01 s00 #f7f8f9; dark Home not designed: 09's desk" + }, + "pt_status": { + "light": "#ffffff", + "dark": "#1b2226", + "from": "02 status #fefefe; 09 status sample (700,1000) #1b2226" + }, + "pt_pill": { + "light": "#ffffff", + "dark": "#282d32", + "from": "02 pill #fefefe; 09 s04 #282d32" + }, + "pt_card": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "04/06/08 cards #fefefe; dark: 09 bar colour (cards not in 09)" + }, + "pt_line": { + "light": "#e1e2e3", + "dark": "#383d41", + "from": "02 d00 #e1e2e3; 09 d24 #383d41" + }, + "pt_line_soft": { + "light": "#ececee", + "dark": "#2c3236", + "from": "02 d02 #ecedee; 09 d07 #303437" + }, + "pt_thumb_line": { + "light": "#d3d5d9", + "dark": "#464b50", + "from": "02 thumbnails' border (s15 #e0e1f0 on the page edge, darker at 1x); dark derived" + }, + "pt_tint": { + "light": "#e9f2fe", + "dark": "#275294", + "from": "02 s16 rail selection #e9f2fe; 09 s14 #275293" + }, + "pt_tint_line": { + "light": "#cfe0fa", + "dark": "#34619f", + "from": "04 s27 Highlight tool border #dfebfc; dark derived from pt_tint" + }, + "pt_tint_ink": { + "light": "#2f6fd0", + "dark": "#bac7dd", + "from": "02 t01 rail 'Pages' ink #5973a6 (anti-aliased blue); 09 t03 #bac7dd" + }, + "pt_select": { + "light": "#eff5fc", + "dark": "#22344d", + "from": "03 s06 result group fill #eff5fc; dark derived" + }, + "pt_field": { + "light": "#ffffff", + "dark": "#1e2429", + "from": "02 s06 search #fefefe; 09 s10 #1e2429" + }, + "pt_field_line": { + "light": "#dcdcde", + "dark": "#3c4043", + "from": "02 d01 #dcdcde; 09 d02 #3c4043" + }, + "pt_hover": { + "light": "#f1f2f3", + "dark": "#2b3237", + "from": "hover: derived" + }, + "pt_ink": { + "light": "#37393c", + "dark": "#d5d7d8", + "from": "02 t16 'Pages' title #38393b, t05 tab #414142; 09 t06 tab #d1d3d4, t09 #d7d9da" + }, + "pt_ink2": { + "light": "#55575b", + "dark": "#b0b2b4", + "from": "02 mode labels #5c5d5e-#6d6d6e; 09 mode labels #a9abad-#b3b4b4" + }, + "pt_muted": { + "light": "#76787c", + "dark": "#9a9da0", + "from": "02 t57 'Saved' #79797b, t58 #6d6d6e; 09 t59 #929698, t60 #9b9d9f" + }, + "pt_faint": { + "light": "#9c9da0", + "dark": "#7d8184", + "from": "02 t15 placeholder #9c9d9f; 09 dimmer than t25 #9fa2a5" + }, + "pt_icon": { + "light": "#5b5d61", + "dark": "#a9acae", + "from": "02 icons; 09 icons" + }, + "pt_green": { + "light": "#49b039", + "dark": "#38a540", + "from": "02 'Saved' dot sample #49b039; 09 #38a540" + }, + "pt_orange": { + "light": "#f2a20d", + "dark": "#e39a1c", + "from": "08 'Edited' dot (31,984)" + }, + "pt_red": { + "light": "#e5534f", + "dark": "#f06a66", + "from": "07 t74 'Monthly rent *' #e85652, t75 #ed4f4a" + }, + "pt_shadow": { + "light": "#00000022", + "dark": "#00000070", + "from": "page and pill shadows: soft, from 02 page edge #e9eaeb" + } + }, + "regions": [ + { + "id": "pill", + "role": "canvas controls: page n / N, previous, next, zoom out, zoom %, zoom in, fit width, fit page", + "widget": "View pill (overlay, bottom centre; on_render pill_view): RoundedShadowView radius 26.5 with IconBtn chevron-left, Label 'p / N' (78 wide), IconBtn chevron-right, SolidView 1x28, IconBtn minus, Label zoom % (66 wide), IconBtn plus, SolidView, IconBtn fit-width, IconBtn fit-page", + "cite": [ + "makepad widgets/src/view_ui.rs:227 (RoundedShadowView)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'" + ], + "geometry": { + "height": 53, + "radius": 26.5, + "bottom": 12, + "padding": 14 + }, + "colors": { + "light": { + "pill": "#ffffff", + "line": "#e1e2e3", + "text": "#55575b", + "icon": "#5b5d61", + "shadow": "#00000022" + }, + "dark": { + "pill": "#282d32", + "line": "#383d41", + "text": "#b0b2b4", + "icon": "#a9acae", + "shadow": "#00000070" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 526, + 890, + 488, + 57 + ], + "status": "built", + "source": { + "text": [ + { + "i": 65, + "text": "<", + "ocr_height": 15.6, + "ink": "#5e5e60", + "ground": "#f9f9f9", + "ink_px": 15, + "size": 15.0 + }, + { + "i": 66, + "text": "3 / 24", + "ocr_height": 16.0, + "ink": "#595a5c", + "ground": "#f8f9f9", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 70, + "text": "-", + "ocr_height": 11.2, + "ink": "#2d2d30", + "ground": "#fbfbfb", + "ink_px": 2, + "size": 2.0 + }, + { + "i": 71, + "text": "100%", + "ocr_height": 15.6, + "ink": "#5d5f62", + "ground": "#f9f9f9", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 72, + "text": "+", + "ocr_height": 17.8, + "ink": "#313234", + "ground": "#fafafa", + "ink_px": 14, + "size": 14.0 + } + ], + "surfaces": [ + { + "i": 7, + "bounds": [ + 213, + 844, + 799, + 120 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 11, + "bounds": [ + 528, + 892, + 484, + 53 + ], + "fill": "#fefefe", + "radius": 0 + } + ], + "dividers": [ + 33, + 34 + ] + } + }, + { + "id": "page1", + "role": "page: the engine's render", + "page_content": true, + "widget": "RectShadowView (white, shadow 7) with Image (binary_resource of pdf.page's PNG at 72 x zoom x 2 dpi), LoadingSpinner + 'Rendering page n…' until it lands; overlays placed by margin in points x zoom; GestureView over it (on_tap/on_double_tap in page points)", + "cite": [ + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/loading_spinner.rs:7 (LoadingSpinner); App Flow docs/SCRIPT-API.md 'Widgets available'", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": { + "size": "the engine's page size x zoom (100% = 1 pt per layout point)" + }, + "colors": { + "light": { + "page": "#ffffff", + "shadow": "#00000022" + }, + "dark": { + "page": "#ffffff", + "shadow": "#00000070" + } + }, + "decision": "Page contents are sample art: every page is the engine's render (pdf.page); highlights, matches and fields are overlays from the engine's rectangles.", + "image_rect": [ + 209, + 177, + 778, + 656 + ], + "status": "built", + "source": { + "text": [ + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 62, + 63, + 69 + ], + "surfaces": [ + 1, + 2, + 21, + 25, + 28, + 29 + ], + "dividers": [ + 10, + 12, + 13, + 14, + 15, + 25, + 26, + 27, + 28, + 29, + 30, + 37, + 38, + 39, + 40, + 41, + 42 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "page2", + "role": "page: the engine's render", + "page_content": true, + "widget": "RectShadowView (white, shadow 7) with Image (binary_resource of pdf.page's PNG at 72 x zoom x 2 dpi), LoadingSpinner + 'Rendering page n…' until it lands; overlays placed by margin in points x zoom; GestureView over it (on_tap/on_double_tap in page points)", + "cite": [ + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/loading_spinner.rs:7 (LoadingSpinner); App Flow docs/SCRIPT-API.md 'Widgets available'", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": { + "size": "the engine's page size x zoom (100% = 1 pt per layout point)" + }, + "colors": { + "light": { + "page": "#ffffff", + "shadow": "#00000022" + }, + "dark": { + "page": "#ffffff", + "shadow": "#00000070" + } + }, + "decision": "Page contents are sample art: every page is the engine's render (pdf.page); highlights, matches and fields are overlays from the engine's rectangles.", + "image_rect": [ + 211, + 838, + 801, + 124 + ], + "status": "built", + "source": { + "text": [ + 64, + 67, + 68 + ], + "surfaces": [], + "dividers": [ + 31, + 32 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "tabs", + "role": "document tabs", + "widget": "SolidView tabs (on_render tabs_view): Tab = RoundedView per open PDF (Svg file-pdf 16, Label title with ellipsis, IconBtn close 13), Outline button 'Open' (Svg plus) after the last tab; Home tab only on Home", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256" + ], + "geometry": { + "height": 53, + "x": 0, + "width": "Fill", + "tab": "Fit, 48 tall from y 11; the row clips 6 so the open tab joins the mode bar", + "tab_padding": [ + 16, + 8 + ], + "gap": 2, + "open": "Outline 36 tall, 6 after the last tab" + }, + "colors": { + "light": { + "row": "#f4f4f5", + "tab_on": "#ffffff", + "border": "#e1e2e3", + "text_on": "#37393c", + "text": "#76787c" + }, + "dark": { + "row": "#191e21", + "tab_on": "#232a2e", + "border": "#383d41", + "text_on": "#d5d7d8", + "text": "#9a9da0" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 0, + 0, + 1536, + 47 + ], + "status": "built", + "source": { + "text": [ + { + "i": 0, + "text": "Q3 2026 Board Report.pdf", + "ocr_height": 16, + "ink": "#5e5e61", + "ground": "#f6f6f7", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 1, + "text": "Riverside Lease 2026.pdf X", + "ocr_height": 18.2, + "ink": "#6b6c70", + "ground": "#eeeff0", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 2, + "text": "Field Guide to Garden Birds.pdf X", + "ocr_height": 16, + "ink": "#717274", + "ground": "#eeeff1", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 3, + "text": "Invoice INV-2041.pdf", + "ocr_height": 18, + "ink": "#6a6b6e", + "ground": "#edeef0", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 39, + "text": "Site Survey Photos.pdf", + "ocr_height": 18, + "ink": "#6a6b6e", + "ground": "#edeeef", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 40, + "text": "+ Open", + "ocr_height": 16.4, + "ink": "#757577", + "ground": "#f6f7f7", + "ink_px": 16, + "size": 13.0 + } + ], + "surfaces": [ + { + "i": 16, + "bounds": [ + 490, + 5, + 259, + 42 + ], + "fill": "#f0f1f2", + "radius": 0 + }, + { + "i": 18, + "bounds": [ + 255, + 5, + 235, + 42 + ], + "fill": "#f0f0f2", + "radius": 0 + }, + { + "i": 19, + "bounds": [ + 963, + 5, + 215, + 42 + ], + "fill": "#f0f1f2", + "radius": 1 + }, + { + "i": 20, + "bounds": [ + 752, + 5, + 211, + 42 + ], + "fill": "#f0f1f2", + "radius": 1 + }, + { + "i": 30, + "bounds": [ + 1185, + 9, + 83, + 36 + ], + "fill": "#f9f9f9", + "radius": 1 + } + ], + "dividers": [ + 0, + 1 + ] + } + }, + { + "id": "modebar", + "role": "mode bar: Read · Comment · Fill & Sign · Pages · Combine · Edit, search field, Undo, Redo, Save", + "widget": "SolidView modebar (on_render modebar_view): per mode a fixed-width column (Flat text button + 3 pt SolidView underline, accent when current); RoundedView search box with Svg search and TextInput (on_return find), IconBtn undo/redo (faint when unavailable), SolidView 1x26 divider, Primary 'Save' with Svg save", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": { + "height": 54, + "padding": [ + 15, + 19 + ], + "mode_columns": [ + 69, + 101, + 100, + 78, + 99, + 61 + ], + "underline": "3 pt, inset 16, 1 pt above the bar's line", + "search": "Fill, at most 344, right-aligned, x 35, radius 5", + "undo_redo": "34 x 34, icons 20", + "save": "128 x 33, radius 5" + }, + "colors": { + "light": { + "bar": "#ffffff", + "mode": "#55575b", + "current": "#1c6ce0", + "underline": "#116cdc", + "field": "#ffffff", + "field_line": "#dcdcde", + "placeholder": "#9c9da0", + "save": "#116cdc" + }, + "dark": { + "bar": "#232a2e", + "mode": "#b0b2b4", + "current": "#4d91e3", + "underline": "#2f75d8", + "field": "#1e2429", + "field_line": "#3c4043", + "placeholder": "#7d8184", + "save": "#2f75d8" + } + }, + "font": "theme.font_regular", + "size": 12.5, + "current_mode": "comment", + "notes": "", + "image_rect": [ + 0, + 47, + 1536, + 59 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 4, + "text": "Read", + "ocr_height": 15.6, + "ink": "#4d4d50", + "ground": "#f3f3f4", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 5, + "text": "Comment", + "ocr_height": 16.8, + "ink": "#3974d5", + "ground": "#f5f8fc", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 6, + "text": "Fill & Sign", + "ocr_height": 19.8, + "ink": "#545557", + "ground": "#f8f8f9", + "ink_px": 17, + "size": 14.0 + }, + { + "i": 7, + "text": "Pages", + "ocr_height": 17.8, + "ink": "#515254", + "ground": "#f6f6f6", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 8, + "text": "Combine", + "ocr_height": 15.8, + "ink": "#505154", + "ground": "#f6f6f6", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 9, + "text": "Edit", + "ocr_height": 15.6, + "ink": "#484a4d", + "ground": "#f4f4f4", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 41, + "text": "Q Search in document", + "ocr_height": 18.2, + "ink": "#a2a2a4", + "ground": "#fafafa", + "ink_px": 19, + "size": 19.0 + }, + { + "i": 42, + "text": "Save", + "ocr_height": 15.6, + "ink": "#d1e4f9", + "ground": "#2070e0", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 15, + "bounds": [ + 973, + 57, + 299, + 42 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 17, + "bounds": [ + 976, + 60, + 293, + 35 + ], + "fill": "#fefefe", + "radius": 1 + }, + { + "i": 26, + "bounds": [ + 1416, + 58, + 108, + 40 + ], + "fill": "#206edf", + "radius": 6 + } + ], + "dividers": [ + 2, + 3, + 4, + 5, + 6, + 7 + ] + } + }, + { + "id": "rail", + "role": "left rail: Pages, Outline, Comments, Search", + "widget": "SolidView rail (on_render rail_view): GestureView 69 x 77 per item holding a RoundedView (tint when selected), Svg 26 and Label", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 84, + "item": [ + 69, + 77 + ], + "first_top": 13, + "pitch": 85, + "radius": 6 + }, + "colors": { + "light": { + "rail": "#ffffff", + "selected": "#e9f2fe", + "selected_ink": "#2f6fd0", + "icon": "#5b5d61", + "label": "#76787c", + "line": "#e1e2e3" + }, + "dark": { + "rail": "#1e252a", + "selected": "#275294", + "selected_ink": "#bac7dd", + "icon": "#a9acae", + "label": "#9a9da0", + "line": "#383d41" + } + }, + "font": "theme.font_regular", + "size": 10.5, + "selected": "pages", + "notes": "Comment starts with the left panel folded, as here; the rail opens it", + "image_rect": [ + 0, + 106, + 84, + 856 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 15, + "text": "Pages", + "ocr_height": 15.6, + "ink": "#487eda", + "ground": "#e9f2fc", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 16, + "text": ":=", + "ocr_height": 22.3, + "ink": "#686869", + "ground": "#f7f7f7", + "ink_px": 21, + "size": 21.0 + }, + { + "i": 17, + "text": "Outline", + "ocr_height": 17.8, + "ink": "#939395", + "ground": "#fafafa", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 18, + "text": "Comments", + "ocr_height": 13.7, + "ink": "#868788", + "ground": "#f7f7f8", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 19, + "text": "Search", + "ocr_height": 13.4, + "ink": "#7e7f80", + "ground": "#f6f6f6", + "ink_px": 11, + "size": 11.0 + } + ], + "surfaces": [ + { + "i": 8, + "bounds": [ + 2, + 170, + 81, + 792 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 23, + "bounds": [ + 9, + 179, + 67, + 74 + ], + "fill": "#edf5fd", + "radius": 4 + }, + { + "i": 24, + "bounds": [ + 2, + 108, + 81, + 60 + ], + "fill": "#fefefe", + "radius": 1 + } + ], + "dividers": [] + } + }, + { + "id": "tools", + "role": "Comment tools: Highlight, Underline, Strike out, Note, Text box, four colours", + "widget": "SolidView 61 tall (toolrow, comment_tools): Flat per tool (Svg 20 + label; current: tint fill, tint border, accent), then four GestureView colour dots (RoundedView 19 inside a 30 ring, accent ring when chosen); below 900 pt of canvas the tools drop their labels", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23" + ], + "geometry": { + "height": 61, + "padding": 22, + "gap": 22, + "tool": "37 tall, radius 6", + "dots": "30 apart 42, dot 19" + }, + "colors": { + "light": { + "bar": "#ffffff", + "tool_on": "#e9f2fe", + "tool_on_line": "#cfe0fa", + "tool_on_ink": "#1c6ce0", + "tool": "#55575b", + "yellow": "#f5c518", + "green": "#5cb85c", + "blue": "#2d7ff9", + "pink": "#f0609e" + }, + "dark": { + "bar": "#232a2e", + "tool_on": "#275294", + "tool_on_line": "#34619f", + "tool_on_ink": "#4d91e3", + "tool": "#b0b2b4", + "yellow": "#f5c518", + "green": "#5cb85c", + "blue": "#2d7ff9", + "pink": "#f0609e" + } + }, + "font": "theme.font_regular", + "size": 12, + "image_rect": [ + 84, + 106, + 1028, + 62 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 10, + "text": "Highlight", + "ocr_height": 17.8, + "ink": "#3a6fd3", + "ground": "#e8f1fc", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 11, + "text": "T Underline", + "ocr_height": 22.4, + "ink": "#636365", + "ground": "#f9f9f9", + "ink_px": 21, + "size": 21.0 + }, + { + "i": 12, + "text": "S Strike out", + "ocr_height": 21.4, + "ink": "#545557", + "ground": "#f8f8f8", + "ink_px": 20, + "size": 20.0 + }, + { + "i": 13, + "text": "Note", + "ocr_height": 13.4, + "ink": "#4c4c4e", + "ground": "#f3f3f4", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 14, + "text": "IT Text box", + "ocr_height": 23, + "ink": "#646566", + "ground": "#f9f9f9", + "ink_px": 20, + "size": 20.0 + } + ], + "surfaces": [ + { + "i": 9, + "bounds": [ + 85, + 106, + 1026, + 62 + ], + "fill": "#fefefe", + "radius": 3 + }, + { + "i": 22, + "bounds": [ + 106, + 117, + 116, + 43 + ], + "fill": "#eef6fe", + "radius": 6 + }, + { + "i": 27, + "bounds": [ + 109, + 120, + 110, + 37 + ], + "fill": "#edf5fe", + "radius": 1 + } + ], + "dividers": [ + 8, + 9 + ] + } + }, + { + "id": "threads", + "role": "right panel: Comments, threads with replies, status chip, reply box", + "widget": "SolidView right_thread (424): head (Label 'Comments' bold 14, RoundedView count chip, IconBtn more: Comment as / Delete this comment), 'Comment as' card until a name is kept, ScrollYView of RoundedView thread cards: GestureView header (colour dot, 'Page n', date, chevron), messages (RoundedView 34 avatar with initials, Label author, Label date, Label text), Flat status chip, status choices, RoundedView reply box (TextInput 'Reply...' + Primary 'Post')", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:805 (ScrollYView); a view keeps its scroll position across its own render(): view.rs:296-306", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 424, + "head": 60, + "card_margin": 12, + "avatar": 34, + "reply_indent": 50, + "reply_box": 66 + }, + "colors": { + "light": { + "panel": "#ffffff", + "card": "#ffffff", + "border": "#e1e2e3", + "author": "#37393c", + "date": "#9c9da0", + "text": "#55575b", + "chip": "#49b039", + "chip_fill": "#effbf4" + }, + "dark": { + "panel": "#192025", + "card": "#232a2e", + "border": "#383d41", + "author": "#d5d7d8", + "date": "#7d8184", + "text": "#b0b2b4", + "chip": "#38a540", + "chip_fill": "#1f3326" + } + }, + "font": "theme.font_regular", + "size": 13, + "notes": "text, authors and dates come from pdf.comments; the author is the name the person keeps under 'Comment as' (SERVICE.md)", + "image_rect": [ + 1112, + 106, + 424, + 856 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 43, + "text": "Comments 3", + "ocr_height": 18.2, + "ink": "#3c3e43", + "ground": "#f4f4f5", + "ink_px": 15, + "size": 15.0 + }, + { + "i": 44, + "text": "Page 3", + "ocr_height": 20.3, + "ink": "#8d8d8f", + "ground": "#f9f9f9", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 45, + "text": "^", + "ocr_height": 13.4, + "ink": "#7d7d7f", + "ground": "#fbfbfc", + "ink_px": 8, + "size": 8.0 + }, + { + "i": 46, + "text": "JP", + "ocr_height": 15.6, + "ink": "#253143", + "ground": "#c6def0", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 47, + "text": "Jun Park", + "ocr_height": 16.0, + "ink": "#3e4146", + "ground": "#f3f4f4", + "ink_px": 14, + "size": 14.0 + }, + { + "i": 48, + "text": "Today 10:12", + "ocr_height": 20.4, + "ink": "#9d9ea0", + "ground": "#fafafb", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 49, + "text": "Can we add the regional breakdown", + "ocr_height": 18.3, + "ink": "#505256", + "ground": "#f5f5f6", + "ink_px": 17, + "size": 14.0 + }, + { + "i": 50, + "text": "table here?", + "ocr_height": 18.4, + "ink": "#585a5f", + "ground": "#f8f8f8", + "ink_px": 14, + "size": 14.0 + }, + { + "i": 51, + "text": "MC", + "ocr_height": 15.6, + "ink": "#4a425e", + "ground": "#e9e0f6", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 52, + "text": "Maya Chen", + "ocr_height": 21.8, + "ink": "#43454a", + "ground": "#f7f7f8", + "ink_px": 18, + "size": 14.5 + }, + { + "i": 53, + "text": "Added on page 4.", + "ocr_height": 23.8, + "ink": "#494b4f", + "ground": "#f7f7f8", + "ink_px": 18, + "size": 14.5 + }, + { + "i": 54, + "text": "Today 10:20", + "ocr_height": 22.3, + "ink": "#959699", + "ground": "#fafafa", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 55, + "text": "Accepted", + "ocr_height": 17.8, + "ink": "#54a973", + "ground": "#effbf4", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 56, + "text": "Reply...", + "ocr_height": 20.1, + "ink": "#949496", + "ground": "#f9f9fa", + "ink_px": 17, + "size": 14.0 + }, + { + "i": 57, + "text": "Post", + "ocr_height": 17.8, + "ink": "#dae7f8", + "ground": "#2172e0", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 58, + "text": "Page 7", + "ocr_height": 18.0, + "ink": "#8a8a8c", + "ground": "#f9f9f9", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 59, + "text": "AR", + "ocr_height": 17.8, + "ink": "#43373f", + "ground": "#f4dadf", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 60, + "text": "Ana Ruiz", + "ocr_height": 17.4, + "ink": "#3c3d41", + "ground": "#f2f2f3", + "ink_px": 14, + "size": 14.0 + }, + { + "i": 61, + "text": "Yesterday v", + "ocr_height": 20.5, + "ink": "#969798", + "ground": "#fbfbfb", + "ink_px": 15, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 3, + "bounds": [ + 1113, + 170, + 422, + 792 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 4, + "bounds": [ + 1124, + 185, + 400, + 399 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 5, + "bounds": [ + 1127, + 238, + 393, + 343 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 10, + "bounds": [ + 1124, + 598, + 400, + 104 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 12, + "bounds": [ + 1113, + 108, + 422, + 60 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 13, + "bounds": [ + 1137, + 494, + 372, + 68 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 14, + "bounds": [ + 1127, + 188, + 393, + 47 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 31, + "bounds": [ + 1242, + 440, + 81, + 32 + ], + "fill": "#f1fcf5", + "radius": 4 + }, + { + "i": 32, + "bounds": [ + 1431, + 512, + 62, + 35 + ], + "fill": "#1e6edf", + "radius": 4 + }, + { + "i": 33, + "bounds": [ + 1245, + 444, + 75, + 25 + ], + "fill": "#eefcf4", + "radius": 1 + } + ], + "dividers": [ + 11, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 35, + 36, + 43, + 44, + 45, + 46 + ] + } + }, + { + "id": "comment_overlay", + "role": "the open comment's mark on the page, a pending note", + "widget": "RoundedView outline (accent 1.5) around the open thread's rect; a pending note marker; the engine draws the annotation itself in the render", + "cite": [ + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": {}, + "colors": { + "light": { + "outline": "#116cdc" + }, + "dark": { + "outline": "#2f75d8" + } + }, + "image_rect": [ + 860, + 450, + 60, + 50 + ], + "status": "built", + "decision": null, + "source": { + "text": [], + "surfaces": [], + "dividers": [] + } + }, + { + "id": "canvas", + "role": "canvas: the desk behind the pages", + "widget": "ScrollXYView canvas (on_render canvas_view): a run of up to 8 pages from the page gone to (go_page: drawn empty for one frame so the scroll clamps to the top, then filled), 'Continue with page n' after the run", + "cite": [ + "makepad widgets/src/view_ui.rs:790 (ScrollXYView); scroll clamped to content each draw: scroll_bar.rs:1264-1267", + "makepad widgets/src/view.rs:904-929 (render), a fn returning a widget may be called in on_render and inside literals (card-host probe, pdftools2-spike/scope)" + ], + "geometry": { + "width": "Fill", + "page_top": 13, + "page_gap": 13, + "bottom": 92, + "sides": 24, + "align": "centre" + }, + "colors": { + "light": { + "desk": "#eceded" + }, + "dark": { + "desk": "#191f24" + } + }, + "notes": "", + "image_rect": [ + 84, + 168, + 1028, + 794 + ], + "status": "built", + "decision": null, + "source": { + "text": [], + "surfaces": [], + "dividers": [] + } + }, + { + "id": "status", + "role": "status line: save state, storage used", + "widget": "SolidView status (on_render status_view): RoundedView 12 dot (green Saved, orange Edited/Saving), Label state, Label ' of 64 MB used' (the library's PDFs against the 64 MiB storage); on Home the label and a RoundedView bar", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "height": 44, + "padding": [ + 33, + 29 + ], + "dot": 12, + "gap": 11 + }, + "colors": { + "light": { + "status": "#ffffff", + "text": "#76787c", + "saved": "#49b039", + "edited": "#f2a20d" + }, + "dark": { + "status": "#1b2226", + "text": "#9a9da0", + "saved": "#38a540", + "edited": "#e39a1c" + } + }, + "font": "theme.font_regular", + "size": 11.5, + "state": "Saved", + "notes": "", + "image_rect": [ + 0, + 962, + 1536, + 62 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 73, + "text": "• Saved", + "ocr_height": 19.9, + "ink": "#508464", + "ground": "#f9fafa", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 74, + "text": "14.6 of 64 MB used", + "ocr_height": 13.4, + "ink": "#6c6d6f", + "ground": "#f5f5f6", + "ink_px": 13, + "size": 13.0 + } + ], + "surfaces": [ + { + "i": 0, + "bounds": [ + 0, + 4, + 1536, + 1020 + ], + "fill": "#fefefe", + "radius": 10 + }, + { + "i": 6, + "bounds": [ + 2, + 956, + 1533, + 67 + ], + "fill": "#fefefe", + "radius": 0 + } + ], + "dividers": [] + } + } + ], + "coverage": { + "text": 75, + "surfaces": 34, + "dividers": 47, + "unmapped": 0 + } +} diff --git a/apps/pdftools/design/map/05-pages.json b/apps/pdftools/design/map/05-pages.json new file mode 100644 index 00000000..b9e3df8d --- /dev/null +++ b/apps/pdftools/design/map/05-pages.json @@ -0,0 +1,1348 @@ +{ + "schema": 1, + "screen": "05-pages", + "state": "Pages mode, pages 5 and 6 selected, dragging 9", + "image": "source/05-pages.png", + "image_sha256": "8c20451b5e17f7cbf663e89c94971dcb90cf3ef0e91b81f473247ebea99b0e31", + "measured": "source/05-pages.measured.json", + "measured_sha256": "af3bffefa0658ac0bea513a33c0bd86a61b64f749d7bf8de6b345416ca9e4a1b", + "units": "1 image pixel = 1 layout point (the designs are a 1536 x 1024 window)", + "built_in": "apps/pdftools/bundle/main.splash (v2)", + "runtime": "OctoSense-org/makepad d653bc44 with runtime-patches.lock.json, as tools/setup.py prepares .sources/makepad", + "type_rule": "size in theme units: the ink height for runs without descenders, ink / 1.23 with them (calibrated in card-host on the runtime's own text); the build may round", + "colours": "light from this screen (or the light screens), dark from 09-dark-reading; tokens below, selected by the shared interface's ui_light", + "tokens": { + "pt_accent": { + "light": "#116cdc", + "dark": "#2f75d8", + "from": "02 s19 Save fill #116cdc; 09 s16 Save #246acf, s08 outline selection #2970d7" + }, + "pt_link": { + "light": "#1c6ce0", + "dark": "#4d91e3", + "from": "02 t00 'Read' ink #3c7ae8 (anti-aliased); 09 t00 #468bd8" + }, + "pt_tabs": { + "light": "#f4f4f5", + "dark": "#191e21", + "from": "02 s04 tab row #f4f4f5; 09 tab row sample (1350,30) #191e21" + }, + "pt_tab_on": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 active tab; 09 active tab ground #22282c" + }, + "pt_bar": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 mode bar #fefefe; 09 t07 ground #22292d" + }, + "pt_rail": { + "light": "#ffffff", + "dark": "#1e252a", + "from": "02 rail #fefefe; 09 s02 #1e252a" + }, + "pt_panel": { + "light": "#ffffff", + "dark": "#192025", + "from": "02 s02 #fefefe; 09 s01 #192025" + }, + "pt_desk": { + "light": "#eceded", + "dark": "#191f24", + "from": "02 s00 #eceded; 09 desk sample (420,500) #191f24" + }, + "pt_home": { + "light": "#f7f8f9", + "dark": "#191f24", + "from": "01 s00 #f7f8f9; dark Home not designed: 09's desk" + }, + "pt_status": { + "light": "#ffffff", + "dark": "#1b2226", + "from": "02 status #fefefe; 09 status sample (700,1000) #1b2226" + }, + "pt_pill": { + "light": "#ffffff", + "dark": "#282d32", + "from": "02 pill #fefefe; 09 s04 #282d32" + }, + "pt_card": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "04/06/08 cards #fefefe; dark: 09 bar colour (cards not in 09)" + }, + "pt_line": { + "light": "#e1e2e3", + "dark": "#383d41", + "from": "02 d00 #e1e2e3; 09 d24 #383d41" + }, + "pt_line_soft": { + "light": "#ececee", + "dark": "#2c3236", + "from": "02 d02 #ecedee; 09 d07 #303437" + }, + "pt_thumb_line": { + "light": "#d3d5d9", + "dark": "#464b50", + "from": "02 thumbnails' border (s15 #e0e1f0 on the page edge, darker at 1x); dark derived" + }, + "pt_tint": { + "light": "#e9f2fe", + "dark": "#275294", + "from": "02 s16 rail selection #e9f2fe; 09 s14 #275293" + }, + "pt_tint_line": { + "light": "#cfe0fa", + "dark": "#34619f", + "from": "04 s27 Highlight tool border #dfebfc; dark derived from pt_tint" + }, + "pt_tint_ink": { + "light": "#2f6fd0", + "dark": "#bac7dd", + "from": "02 t01 rail 'Pages' ink #5973a6 (anti-aliased blue); 09 t03 #bac7dd" + }, + "pt_select": { + "light": "#eff5fc", + "dark": "#22344d", + "from": "03 s06 result group fill #eff5fc; dark derived" + }, + "pt_field": { + "light": "#ffffff", + "dark": "#1e2429", + "from": "02 s06 search #fefefe; 09 s10 #1e2429" + }, + "pt_field_line": { + "light": "#dcdcde", + "dark": "#3c4043", + "from": "02 d01 #dcdcde; 09 d02 #3c4043" + }, + "pt_hover": { + "light": "#f1f2f3", + "dark": "#2b3237", + "from": "hover: derived" + }, + "pt_ink": { + "light": "#37393c", + "dark": "#d5d7d8", + "from": "02 t16 'Pages' title #38393b, t05 tab #414142; 09 t06 tab #d1d3d4, t09 #d7d9da" + }, + "pt_ink2": { + "light": "#55575b", + "dark": "#b0b2b4", + "from": "02 mode labels #5c5d5e-#6d6d6e; 09 mode labels #a9abad-#b3b4b4" + }, + "pt_muted": { + "light": "#76787c", + "dark": "#9a9da0", + "from": "02 t57 'Saved' #79797b, t58 #6d6d6e; 09 t59 #929698, t60 #9b9d9f" + }, + "pt_faint": { + "light": "#9c9da0", + "dark": "#7d8184", + "from": "02 t15 placeholder #9c9d9f; 09 dimmer than t25 #9fa2a5" + }, + "pt_icon": { + "light": "#5b5d61", + "dark": "#a9acae", + "from": "02 icons; 09 icons" + }, + "pt_green": { + "light": "#49b039", + "dark": "#38a540", + "from": "02 'Saved' dot sample #49b039; 09 #38a540" + }, + "pt_orange": { + "light": "#f2a20d", + "dark": "#e39a1c", + "from": "08 'Edited' dot (31,984)" + }, + "pt_red": { + "light": "#e5534f", + "dark": "#f06a66", + "from": "07 t74 'Monthly rent *' #e85652, t75 #ed4f4a" + }, + "pt_shadow": { + "light": "#00000022", + "dark": "#00000070", + "from": "page and pill shadows: soft, from 02 page edge #e9eaeb" + } + }, + "regions": [ + { + "id": "pill", + "role": "canvas controls: page n / N, previous, next, zoom out, zoom %, zoom in, fit width, fit page", + "widget": "View pill (overlay, bottom centre; on_render pill_view): RoundedShadowView radius 26.5 with IconBtn chevron-left, Label 'p / N' (78 wide), IconBtn chevron-right, SolidView 1x28, IconBtn minus, Label zoom % (66 wide), IconBtn plus, SolidView, IconBtn fit-width, IconBtn fit-page", + "cite": [ + "makepad widgets/src/view_ui.rs:227 (RoundedShadowView)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'" + ], + "geometry": { + "height": 53, + "radius": 26.5, + "bottom": 12, + "padding": 14 + }, + "colors": { + "light": { + "pill": "#ffffff", + "line": "#e1e2e3", + "text": "#55575b", + "icon": "#5b5d61", + "shadow": "#00000022" + }, + "dark": { + "pill": "#282d32", + "line": "#383d41", + "text": "#b0b2b4", + "icon": "#a9acae", + "shadow": "#00000070" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "in Pages mode the zoom sizes the tiles; fit width and fit page are left out (no page is fitted)", + "image_rect": [ + 620, + 918, + 402, + 47 + ], + "status": "built", + "source": { + "text": [ + { + "i": 167, + "text": "<", + "ocr_height": 15.6, + "ink": "#565a62", + "ground": "#fafafa", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 168, + "text": "3 / 24", + "ocr_height": 13.4, + "ink": "#84878d", + "ground": "#fafafa", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 169, + "text": ">", + "ocr_height": 17.8, + "ink": "#545861", + "ground": "#fbfbfb", + "ink_px": 14, + "size": 14.0 + }, + { + "i": 170, + "text": "-", + "ocr_height": 11.2, + "ink": "#70747b", + "ground": "#fbfbfb", + "ink_px": 2, + "size": 2.0 + }, + { + "i": 171, + "text": "100%", + "ocr_height": 11.2, + "ink": "#7c7f84", + "ground": "#f5f5f6", + "ink_px": 8, + "size": 8.0 + }, + { + "i": 172, + "text": "+", + "ocr_height": 20.1, + "ink": "#6d7177", + "ground": "#fbfbfb", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 20, + "bounds": [ + 623, + 921, + 396, + 41 + ], + "fill": "#fdfdfd", + "radius": 4 + } + ], + "dividers": [ + 3, + 4, + 5 + ] + } + }, + { + "id": "tabs", + "role": "document tabs", + "widget": "SolidView tabs (on_render tabs_view): Tab = RoundedView per open PDF (Svg file-pdf 16, Label title with ellipsis, IconBtn close 13), Outline button 'Open' (Svg plus) after the last tab; Home tab only on Home", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256" + ], + "geometry": { + "height": 53, + "x": 0, + "width": "Fill", + "tab": "Fit, 48 tall from y 11; the row clips 6 so the open tab joins the mode bar", + "tab_padding": [ + 16, + 8 + ], + "gap": 2, + "open": "Outline 36 tall, 6 after the last tab" + }, + "colors": { + "light": { + "row": "#f4f4f5", + "tab_on": "#ffffff", + "border": "#e1e2e3", + "text_on": "#37393c", + "text": "#76787c" + }, + "dark": { + "row": "#191e21", + "tab_on": "#232a2e", + "border": "#383d41", + "text_on": "#d5d7d8", + "text": "#9a9da0" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 0, + 0, + 1536, + 44 + ], + "status": "built", + "source": { + "text": [ + { + "i": 0, + "text": "Q3 2026 Board Report.pdf", + "ocr_height": 15.6, + "ink": "#58595c", + "ground": "#f6f6f6", + "ink_px": 12, + "size": 10.0 + }, + { + "i": 1, + "text": "Riverside Lease 2026.pdf", + "ocr_height": 15.6, + "ink": "#898b8f", + "ground": "#f2f2f3", + "ink_px": 11, + "size": 9.0 + }, + { + "i": 2, + "text": "Field Guide to Garden Birds.pdf", + "ocr_height": 13.5, + "ink": "#878a8e", + "ground": "#f1f1f2", + "ink_px": 12, + "size": 10.0 + }, + { + "i": 3, + "text": "X", + "ocr_height": 11.2, + "ink": "#73757c", + "ground": "#f1f1f2", + "ink_px": 8, + "size": 8.0 + }, + { + "i": 4, + "text": "Invoice INV-2041.pdf", + "ocr_height": 15.6, + "ink": "#83868a", + "ground": "#f1f1f2", + "ink_px": 12, + "size": 10.0 + }, + { + "i": 173, + "text": "Site Survey Photos.pdf", + "ocr_height": 15.6, + "ink": "#84878c", + "ground": "#f1f1f2", + "ink_px": 12, + "size": 10.0 + }, + { + "i": 174, + "text": "+ Open", + "ocr_height": 15.6, + "ink": "#75797f", + "ground": "#f2f2f3", + "ink_px": 12, + "size": 10.0 + } + ], + "surfaces": [ + { + "i": 25, + "bounds": [ + 262, + 5, + 232, + 40 + ], + "fill": "#f3f3f4", + "radius": 0 + }, + { + "i": 27, + "bounds": [ + 960, + 5, + 219, + 40 + ], + "fill": "#f3f3f3", + "radius": 0 + }, + { + "i": 28, + "bounds": [ + 10, + 8, + 246, + 35 + ], + "fill": "#fdfdfd", + "radius": 0 + }, + { + "i": 29, + "bounds": [ + 501, + 8, + 231, + 37 + ], + "fill": "#f3f3f3", + "radius": 0 + }, + { + "i": 30, + "bounds": [ + 741, + 8, + 214, + 37 + ], + "fill": "#f3f3f3", + "radius": 0 + }, + { + "i": 33, + "bounds": [ + 962, + 8, + 215, + 34 + ], + "fill": "#f3f3f3", + "radius": 0 + }, + { + "i": 36, + "bounds": [ + 1185, + 10, + 76, + 30 + ], + "fill": "#f4f3f4", + "radius": 0 + }, + { + "i": 38, + "bounds": [ + 1189, + 13, + 69, + 24 + ], + "fill": "#f4f3f4", + "radius": 0 + } + ], + "dividers": [ + 0 + ] + } + }, + { + "id": "modebar", + "role": "mode bar: Read · Comment · Fill & Sign · Pages · Combine · Edit, search field, Undo, Redo, Save", + "widget": "SolidView modebar (on_render modebar_view): per mode a fixed-width column (Flat text button + 3 pt SolidView underline, accent when current); RoundedView search box with Svg search and TextInput (on_return find), IconBtn undo/redo (faint when unavailable), SolidView 1x26 divider, Primary 'Save' with Svg save", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": { + "height": 54, + "padding": [ + 15, + 19 + ], + "mode_columns": [ + 69, + 101, + 100, + 78, + 99, + 61 + ], + "underline": "3 pt, inset 16, 1 pt above the bar's line", + "search": "Fill, at most 344, right-aligned, x 35, radius 5", + "undo_redo": "34 x 34, icons 20", + "save": "128 x 33, radius 5" + }, + "colors": { + "light": { + "bar": "#ffffff", + "mode": "#55575b", + "current": "#1c6ce0", + "underline": "#116cdc", + "field": "#ffffff", + "field_line": "#dcdcde", + "placeholder": "#9c9da0", + "save": "#116cdc" + }, + "dark": { + "bar": "#232a2e", + "mode": "#b0b2b4", + "current": "#4d91e3", + "underline": "#2f75d8", + "field": "#1e2429", + "field_line": "#3c4043", + "placeholder": "#7d8184", + "save": "#2f75d8" + } + }, + "font": "theme.font_regular", + "size": 12.5, + "current_mode": "pages", + "notes": "", + "image_rect": [ + 0, + 44, + 1536, + 57 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 5, + "text": "Read", + "ocr_height": 13.4, + "ink": "#585c63", + "ground": "#f1f1f2", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 6, + "text": "Comment", + "ocr_height": 15.6, + "ink": "#6a6e74", + "ground": "#f8f8f9", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 7, + "text": "Fill & Sign", + "ocr_height": 16.0, + "ink": "#65696f", + "ground": "#f6f6f7", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 8, + "text": "Pages", + "ocr_height": 17.8, + "ink": "#3a80de", + "ground": "#f4f8fc", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 9, + "text": "Combine", + "ocr_height": 15.6, + "ink": "#6b6f75", + "ground": "#f7f7f8", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 10, + "text": "Edit", + "ocr_height": 13.4, + "ink": "#5f6268", + "ground": "#f4f4f5", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 175, + "text": "• Search in document", + "ocr_height": 14, + "ink": "#888b90", + "ground": "#f7f8f8", + "ink_px": 15, + "size": 15.0 + }, + { + "i": 202, + "text": "Save", + "ocr_height": 15.6, + "ink": "#c3def7", + "ground": "#086adb", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 2, + "bounds": [ + 8, + 44, + 1521, + 109 + ], + "fill": "#fdfdfd", + "radius": 3 + }, + { + "i": 24, + "bounds": [ + 1039, + 58, + 253, + 37 + ], + "fill": "#fdfdfd", + "radius": 0 + }, + { + "i": 32, + "bounds": [ + 1042, + 61, + 246, + 30 + ], + "fill": "#fdfdfd", + "radius": 1 + }, + { + "i": 35, + "bounds": [ + 1437, + 59, + 80, + 34 + ], + "fill": "#0468db", + "radius": 4 + } + ], + "dividers": [ + 1, + 2 + ] + } + }, + { + "id": "rail", + "role": "left rail: Pages, Outline, Comments, Search", + "widget": "SolidView rail (on_render rail_view): GestureView 69 x 77 per item holding a RoundedView (tint when selected), Svg 26 and Label", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 84, + "item": [ + 69, + 77 + ], + "first_top": 13, + "pitch": 85, + "radius": 6 + }, + "colors": { + "light": { + "rail": "#ffffff", + "selected": "#e9f2fe", + "selected_ink": "#2f6fd0", + "icon": "#5b5d61", + "label": "#76787c", + "line": "#e1e2e3" + }, + "dark": { + "rail": "#1e252a", + "selected": "#275294", + "selected_ink": "#bac7dd", + "icon": "#a9acae", + "label": "#9a9da0", + "line": "#383d41" + } + }, + "font": "theme.font_regular", + "size": 10.5, + "selected": "pages", + "notes": "", + "image_rect": [ + 0, + 101, + 88, + 877 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 14, + "text": "Pages", + "ocr_height": 13.4, + "ink": "#4882e2", + "ground": "#e1eefb", + "ink_px": 11, + "size": 9.0 + }, + { + "i": 15, + "text": "Outline", + "ocr_height": 11.2, + "ink": "#818489", + "ground": "#f6f6f7", + "ink_px": 9, + "size": 9.0 + }, + { + "i": 16, + "text": "Comments", + "ocr_height": 11.2, + "ink": "#808287", + "ground": "#f7f7f8", + "ink_px": 9, + "size": 9.0 + }, + { + "i": 17, + "text": "Search", + "ocr_height": 11.2, + "ink": "#818388", + "ground": "#f7f7f7", + "ink_px": 9, + "size": 9.0 + } + ], + "surfaces": [ + { + "i": 3, + "bounds": [ + 8, + 103, + 78, + 875 + ], + "fill": "#fdfdfd", + "radius": 0 + }, + { + "i": 34, + "bounds": [ + 19, + 117, + 55, + 68 + ], + "fill": "#e7f1fb", + "radius": 3 + } + ], + "dividers": [ + 7, + 8 + ] + } + }, + { + "id": "actions", + "role": "Pages actions: n selected, Rotate left, Rotate right, Delete, Extract, Insert from file, hint", + "widget": "SolidView 52 tall (toolrow, pages_actions): Label 'n selected' (bold, accent), Flat with Svg per action, SolidView divider, Label 'Drag pages to reorder'; Insert from file opens a strip of the library's other PDFs", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23" + ], + "geometry": { + "height": 52, + "padding": 26, + "gap": 14 + }, + "colors": { + "light": { + "bar": "#ffffff", + "count": "#1c6ce0", + "action": "#55575b", + "hint": "#76787c" + }, + "dark": { + "bar": "#232a2e", + "count": "#4d91e3", + "action": "#b0b2b4", + "hint": "#9a9da0" + } + }, + "font": "theme.font_regular", + "size": 12, + "image_rect": [ + 88, + 101, + 1441, + 54 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 11, + "text": "2 selected", + "ocr_height": 13.8, + "ink": "#337ce0", + "ground": "#f2f7fc", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 12, + "text": "Rotate left", + "ocr_height": 13.6, + "ink": "#828488", + "ground": "#f7f7f7", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 13, + "text": "Rotate right", + "ocr_height": 14.1, + "ink": "#828488", + "ground": "#f7f7f8", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 91, + "text": "Delete", + "ocr_height": 11.2, + "ink": "#7f8287", + "ground": "#f5f5f6", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 92, + "text": "Extract", + "ocr_height": 11.2, + "ink": "#73767b", + "ground": "#f4f4f5", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 138, + "text": "Insert from file", + "ocr_height": 13.4, + "ink": "#7b7d81", + "ground": "#f5f6f6", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 203, + "text": "Drag pages to reorder", + "ocr_height": 13.4, + "ink": "#87898d", + "ground": "#f7f7f8", + "ink_px": 13, + "size": 10.5 + } + ], + "surfaces": [], + "dividers": [] + } + }, + { + "id": "grid", + "role": "every page as a large thumbnail: selection, page numbers, drop marker", + "page_content": true, + "widget": "ScrollYView grid (static; grid_view): rows of GestureView tiles (on_tap toggle_select, on_pan tile_pan: cumulative offset in tiles decides the drop) holding a RoundedView frame (accent 2 pt when chosen), RectShadowView with Image (pdf.page at 48 dpi), RoundedView 22 check badge with Svg check, Label page number; a dashed SolidView marker before the drop target", + "cite": [ + "makepad widgets/src/view_ui.rs:805 (ScrollYView); a view keeps its scroll position across its own render(): view.rs:296-306", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23" + ], + "geometry": { + "tile": "190 wide at 100% (x 0.6-1.6), page aspect", + "column_gap": 30, + "row_gap": 26, + "padding": 20 + }, + "colors": { + "light": { + "frame_on": "#116cdc", + "badge": "#116cdc", + "number": "#55575b", + "marker": "#116cdc" + }, + "dark": { + "frame_on": "#2f75d8", + "badge": "#2f75d8", + "number": "#b0b2b4", + "marker": "#2f75d8" + } + }, + "font": "theme.font_regular", + "size": 12.5, + "decision": "Page contents are sample art: every tile is the engine's render (pdf.page).", + "image_rect": [ + 88, + 155, + 1441, + 823 + ], + "status": "built", + "source": { + "text": [ + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 126, + 127, + 128, + 129, + 130, + 131, + 132, + 133, + 134, + 135, + 136, + 137, + 139, + 140, + 141, + 142, + 143, + 144, + 145, + 146, + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162, + 163, + 164, + 165, + 166, + 176, + 177, + 178, + 179, + 180, + 181, + 182, + 183, + 184, + 185, + 186, + 187, + 188, + 189, + 190, + 191, + 192, + 193, + 194, + 195, + 196, + 197, + 198, + 199, + 200, + 204, + 205, + 206, + 207, + 208, + 209, + 210, + 211, + 212, + 213, + 214, + 215, + 216, + 217, + 218, + 219, + 220, + 221, + 222, + 223, + 224 + ], + "surfaces": [ + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 21, + 22, + 23, + 26, + 31, + 37 + ], + "dividers": [ + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "status", + "role": "status line: save state, storage used", + "widget": "SolidView status (on_render status_view): RoundedView 12 dot (green Saved, orange Edited/Saving), Label state, Label ' of 64 MB used' (the library's PDFs against the 64 MiB storage); on Home the label and a RoundedView bar", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "height": 44, + "padding": [ + 33, + 29 + ], + "dot": 12, + "gap": 11 + }, + "colors": { + "light": { + "status": "#ffffff", + "text": "#76787c", + "saved": "#49b039", + "edited": "#f2a20d" + }, + "dark": { + "status": "#1b2226", + "text": "#9a9da0", + "saved": "#38a540", + "edited": "#e39a1c" + } + }, + "font": "theme.font_regular", + "size": 11.5, + "state": "Saved", + "notes": "", + "image_rect": [ + 0, + 978, + 1536, + 46 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 201, + "text": "Saved", + "ocr_height": 11.2, + "ink": "#7c7f84", + "ground": "#f7f8f8", + "ink_px": 9, + "size": 9.0 + }, + { + "i": 225, + "text": "14.6 of 64 MB used", + "ocr_height": 13.4, + "ink": "#7c7e83", + "ground": "#f8f8f8", + "ink_px": 10, + "size": 10.0 + } + ], + "surfaces": [ + { + "i": 0, + "bounds": [ + 5, + 2, + 1527, + 1021 + ], + "fill": "#fbfbfc", + "radius": 0 + }, + { + "i": 1, + "bounds": [ + 88, + 155, + 1441, + 823 + ], + "fill": "#f8f8f9", + "radius": 0 + }, + { + "i": 4, + "bounds": [ + 8, + 980, + 1521, + 39 + ], + "fill": "#fdfdfd", + "radius": 0 + } + ], + "dividers": [ + 6 + ] + } + } + ], + "coverage": { + "text": 226, + "surfaces": 39, + "dividers": 55, + "unmapped": 0 + } +} diff --git a/apps/pdftools/design/map/06-combine.json b/apps/pdftools/design/map/06-combine.json new file mode 100644 index 00000000..b2439cef --- /dev/null +++ b/apps/pdftools/design/map/06-combine.json @@ -0,0 +1,1291 @@ +{ + "schema": 1, + "screen": "06-combine", + "state": "Combine three PDFs, one with pages 1-4, 9", + "image": "source/06-combine.png", + "image_sha256": "bbb5c2bf1a5288343e42b6602e231314f3dc79effb6cfe789aa14cf743baef08", + "measured": "source/06-combine.measured.json", + "measured_sha256": "cf14cd556786fc7ad0159a29b414d85e8563665ae2d1e34ae613ab59b9bcc42e", + "units": "1 image pixel = 1 layout point (the designs are a 1536 x 1024 window)", + "built_in": "apps/pdftools/bundle/main.splash (v2)", + "runtime": "OctoSense-org/makepad d653bc44 with runtime-patches.lock.json, as tools/setup.py prepares .sources/makepad", + "type_rule": "size in theme units: the ink height for runs without descenders, ink / 1.23 with them (calibrated in card-host on the runtime's own text); the build may round", + "colours": "light from this screen (or the light screens), dark from 09-dark-reading; tokens below, selected by the shared interface's ui_light", + "tokens": { + "pt_accent": { + "light": "#116cdc", + "dark": "#2f75d8", + "from": "02 s19 Save fill #116cdc; 09 s16 Save #246acf, s08 outline selection #2970d7" + }, + "pt_link": { + "light": "#1c6ce0", + "dark": "#4d91e3", + "from": "02 t00 'Read' ink #3c7ae8 (anti-aliased); 09 t00 #468bd8" + }, + "pt_tabs": { + "light": "#f4f4f5", + "dark": "#191e21", + "from": "02 s04 tab row #f4f4f5; 09 tab row sample (1350,30) #191e21" + }, + "pt_tab_on": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 active tab; 09 active tab ground #22282c" + }, + "pt_bar": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 mode bar #fefefe; 09 t07 ground #22292d" + }, + "pt_rail": { + "light": "#ffffff", + "dark": "#1e252a", + "from": "02 rail #fefefe; 09 s02 #1e252a" + }, + "pt_panel": { + "light": "#ffffff", + "dark": "#192025", + "from": "02 s02 #fefefe; 09 s01 #192025" + }, + "pt_desk": { + "light": "#eceded", + "dark": "#191f24", + "from": "02 s00 #eceded; 09 desk sample (420,500) #191f24" + }, + "pt_home": { + "light": "#f7f8f9", + "dark": "#191f24", + "from": "01 s00 #f7f8f9; dark Home not designed: 09's desk" + }, + "pt_status": { + "light": "#ffffff", + "dark": "#1b2226", + "from": "02 status #fefefe; 09 status sample (700,1000) #1b2226" + }, + "pt_pill": { + "light": "#ffffff", + "dark": "#282d32", + "from": "02 pill #fefefe; 09 s04 #282d32" + }, + "pt_card": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "04/06/08 cards #fefefe; dark: 09 bar colour (cards not in 09)" + }, + "pt_line": { + "light": "#e1e2e3", + "dark": "#383d41", + "from": "02 d00 #e1e2e3; 09 d24 #383d41" + }, + "pt_line_soft": { + "light": "#ececee", + "dark": "#2c3236", + "from": "02 d02 #ecedee; 09 d07 #303437" + }, + "pt_thumb_line": { + "light": "#d3d5d9", + "dark": "#464b50", + "from": "02 thumbnails' border (s15 #e0e1f0 on the page edge, darker at 1x); dark derived" + }, + "pt_tint": { + "light": "#e9f2fe", + "dark": "#275294", + "from": "02 s16 rail selection #e9f2fe; 09 s14 #275293" + }, + "pt_tint_line": { + "light": "#cfe0fa", + "dark": "#34619f", + "from": "04 s27 Highlight tool border #dfebfc; dark derived from pt_tint" + }, + "pt_tint_ink": { + "light": "#2f6fd0", + "dark": "#bac7dd", + "from": "02 t01 rail 'Pages' ink #5973a6 (anti-aliased blue); 09 t03 #bac7dd" + }, + "pt_select": { + "light": "#eff5fc", + "dark": "#22344d", + "from": "03 s06 result group fill #eff5fc; dark derived" + }, + "pt_field": { + "light": "#ffffff", + "dark": "#1e2429", + "from": "02 s06 search #fefefe; 09 s10 #1e2429" + }, + "pt_field_line": { + "light": "#dcdcde", + "dark": "#3c4043", + "from": "02 d01 #dcdcde; 09 d02 #3c4043" + }, + "pt_hover": { + "light": "#f1f2f3", + "dark": "#2b3237", + "from": "hover: derived" + }, + "pt_ink": { + "light": "#37393c", + "dark": "#d5d7d8", + "from": "02 t16 'Pages' title #38393b, t05 tab #414142; 09 t06 tab #d1d3d4, t09 #d7d9da" + }, + "pt_ink2": { + "light": "#55575b", + "dark": "#b0b2b4", + "from": "02 mode labels #5c5d5e-#6d6d6e; 09 mode labels #a9abad-#b3b4b4" + }, + "pt_muted": { + "light": "#76787c", + "dark": "#9a9da0", + "from": "02 t57 'Saved' #79797b, t58 #6d6d6e; 09 t59 #929698, t60 #9b9d9f" + }, + "pt_faint": { + "light": "#9c9da0", + "dark": "#7d8184", + "from": "02 t15 placeholder #9c9d9f; 09 dimmer than t25 #9fa2a5" + }, + "pt_icon": { + "light": "#5b5d61", + "dark": "#a9acae", + "from": "02 icons; 09 icons" + }, + "pt_green": { + "light": "#49b039", + "dark": "#38a540", + "from": "02 'Saved' dot sample #49b039; 09 #38a540" + }, + "pt_orange": { + "light": "#f2a20d", + "dark": "#e39a1c", + "from": "08 'Edited' dot (31,984)" + }, + "pt_red": { + "light": "#e5534f", + "dark": "#f06a66", + "from": "07 t74 'Monthly rent *' #e85652, t75 #ed4f4a" + }, + "pt_shadow": { + "light": "#00000022", + "dark": "#00000070", + "from": "page and pill shadows: soft, from 02 page edge #e9eaeb" + } + }, + "regions": [ + { + "id": "pill", + "role": "canvas controls", + "widget": "not built", + "status": "omitted", + "cite": [], + "geometry": {}, + "colors": {}, + "decision": "Combine: drop the floating page and zoom pill.", + "image_rect": [ + 505, + 865, + 560, + 75 + ], + "source": { + "text": [ + { + "i": 36, + "text": "3 / 24", + "ocr_height": 17.8, + "ink": "#58595b", + "ground": "#f7f7f7", + "ink_px": 16, + "size": 16.0 + }, + { + "i": 37, + "text": "100%", + "ocr_height": 13.4, + "ink": "#75787a", + "ground": "#f6f6f6", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 38, + "text": "+", + "ocr_height": 22.3, + "ink": "#727579", + "ground": "#fbfbfb", + "ink_px": 17, + "size": 17.0 + } + ], + "surfaces": [ + { + "i": 12, + "bounds": [ + 513, + 874, + 545, + 61 + ], + "fill": "#fcfcfc", + "radius": 16 + } + ], + "dividers": [ + 13, + 14, + 15 + ] + } + }, + { + "id": "renders", + "role": "Combine row first pages: the engine's renders", + "page_content": true, + "widget": "Image (binary_resource of a pdf.page PNG) inside its frame", + "cite": [ + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": {}, + "colors": {}, + "decision": "Page contents are sample art: every page image is the engine's render (pdf.page).", + "image_rect": [ + 503, + 247, + 82, + 336 + ], + "status": "built", + "source": { + "text": [ + 17, + 18, + 19 + ], + "surfaces": [ + 18, + 20, + 23, + 25, + 30, + 34, + 35 + ], + "dividers": [], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "tabs", + "role": "document tabs", + "widget": "SolidView tabs (on_render tabs_view): Tab = RoundedView per open PDF (Svg file-pdf 16, Label title with ellipsis, IconBtn close 13), Outline button 'Open' (Svg plus) after the last tab; Home tab only on Home", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256" + ], + "geometry": { + "height": 53, + "x": 0, + "width": "Fill", + "tab": "Fit, 48 tall from y 11; the row clips 6 so the open tab joins the mode bar", + "tab_padding": [ + 16, + 8 + ], + "gap": 2, + "open": "Outline 36 tall, 6 after the last tab" + }, + "colors": { + "light": { + "row": "#f4f4f5", + "tab_on": "#ffffff", + "border": "#e1e2e3", + "text_on": "#37393c", + "text": "#76787c" + }, + "dark": { + "row": "#191e21", + "tab_on": "#232a2e", + "border": "#383d41", + "text_on": "#d5d7d8", + "text": "#9a9da0" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 0, + 0, + 1536, + 50 + ], + "status": "built", + "source": { + "text": [ + { + "i": 0, + "text": "Q3 2026 Board Report.pdf x", + "ocr_height": 18, + "ink": "#515152", + "ground": "#f6f6f6", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 1, + "text": "Riverside Lease 2026.pdf X", + "ocr_height": 16, + "ink": "#555758", + "ground": "#eeeff1", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 2, + "text": "Field Guide to Garden Birds.pdf x", + "ocr_height": 20.2, + "ink": "#5b5c5e", + "ground": "#f0f1f3", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 9, + "text": "Invoice INV-2041.pdf x", + "ocr_height": 20.2, + "ink": "#58595c", + "ground": "#f0f1f3", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 10, + "text": "Site Survey Photos.pdf x", + "ocr_height": 20.2, + "ink": "#595a5b", + "ground": "#f0f1f3", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 11, + "text": "+ Open", + "ocr_height": 22.3, + "ink": "#555658", + "ground": "#f5f5f6", + "ink_px": 15, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 9, + "bounds": [ + 2, + 2, + 1533, + 48 + ], + "fill": "#f3f4f6", + "radius": 3 + }, + { + "i": 10, + "bounds": [ + 263, + 5, + 958, + 45 + ], + "fill": "#f2f3f5", + "radius": 3 + }, + { + "i": 13, + "bounds": [ + 1220, + 5, + 310, + 42 + ], + "fill": "#f8f8f9", + "radius": 0 + } + ], + "dividers": [ + 0 + ] + } + }, + { + "id": "modebar", + "role": "mode bar: Read · Comment · Fill & Sign · Pages · Combine · Edit, search field, Undo, Redo, Save", + "widget": "SolidView modebar (on_render modebar_view): per mode a fixed-width column (Flat text button + 3 pt SolidView underline, accent when current); RoundedView search box with Svg search and TextInput (on_return find), IconBtn undo/redo (faint when unavailable), SolidView 1x26 divider, Primary 'Save' with Svg save", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": { + "height": 54, + "padding": [ + 15, + 19 + ], + "mode_columns": [ + 69, + 101, + 100, + 78, + 99, + 61 + ], + "underline": "3 pt, inset 16, 1 pt above the bar's line", + "search": "Fill, at most 344, right-aligned, x 35, radius 5", + "undo_redo": "34 x 34, icons 20", + "save": "128 x 33, radius 5" + }, + "colors": { + "light": { + "bar": "#ffffff", + "mode": "#55575b", + "current": "#1c6ce0", + "underline": "#116cdc", + "field": "#ffffff", + "field_line": "#dcdcde", + "placeholder": "#9c9da0", + "save": "#116cdc" + }, + "dark": { + "bar": "#232a2e", + "mode": "#b0b2b4", + "current": "#4d91e3", + "underline": "#2f75d8", + "field": "#1e2429", + "field_line": "#3c4043", + "placeholder": "#7d8184", + "save": "#2f75d8" + } + }, + "font": "theme.font_regular", + "size": 12.5, + "current_mode": "combine", + "notes": "06 boxes Undo and Redo in a segmented control; the build keeps 02's plain icons", + "image_rect": [ + 0, + 50, + 1536, + 63 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 3, + "text": "Read", + "ocr_height": 15.6, + "ink": "#616569", + "ground": "#f7f7f7", + "ink_px": 14, + "size": 14.0 + }, + { + "i": 4, + "text": "Comment", + "ocr_height": 16.2, + "ink": "#62656a", + "ground": "#f6f6f6", + "ink_px": 14, + "size": 14.0 + }, + { + "i": 5, + "text": "Fill & Sign", + "ocr_height": 20.4, + "ink": "#5b5d62", + "ground": "#f4f4f5", + "ink_px": 17, + "size": 14.0 + }, + { + "i": 6, + "text": "Pages", + "ocr_height": 18.2, + "ink": "#5c5e63", + "ground": "#f3f4f4", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 7, + "text": "Combine", + "ocr_height": 16.1, + "ink": "#438ae2", + "ground": "#f2f6fb", + "ink_px": 14, + "size": 14.0 + }, + { + "i": 8, + "text": "Edit", + "ocr_height": 17.8, + "ink": "#5b5e63", + "ground": "#f4f4f4", + "ink_px": 14, + "size": 14.0 + }, + { + "i": 12, + "text": "Search in document", + "ocr_height": 13.4, + "ink": "#a2a4a8", + "ground": "#f8f8f9", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 13, + "text": "Save", + "ocr_height": 13.4, + "ink": "#cee3f9", + "ground": "#257ae5", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 15, + "bounds": [ + 931, + 63, + 311, + 38 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 17, + "bounds": [ + 934, + 67, + 305, + 31 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 29, + "bounds": [ + 1256, + 63, + 135, + 39 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 31, + "bounds": [ + 1434, + 64, + 87, + 37 + ], + "fill": "#2075e4", + "radius": 3 + }, + { + "i": 32, + "bounds": [ + 1259, + 66, + 65, + 32 + ], + "fill": "#fcfbfb", + "radius": 1 + }, + { + "i": 33, + "bounds": [ + 1326, + 66, + 62, + 32 + ], + "fill": "#fcfcfc", + "radius": 0 + } + ], + "dividers": [ + 1, + 2 + ] + } + }, + { + "id": "rail", + "role": "left rail: Pages, Outline, Comments, Search", + "widget": "SolidView rail (on_render rail_view): GestureView 69 x 77 per item holding a RoundedView (tint when selected), Svg 26 and Label", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 84, + "item": [ + 69, + 77 + ], + "first_top": 13, + "pitch": 85, + "radius": 6 + }, + "colors": { + "light": { + "rail": "#ffffff", + "selected": "#e9f2fe", + "selected_ink": "#2f6fd0", + "icon": "#5b5d61", + "label": "#76787c", + "line": "#e1e2e3" + }, + "dark": { + "rail": "#1e252a", + "selected": "#275294", + "selected_ink": "#bac7dd", + "icon": "#a9acae", + "label": "#9a9da0", + "line": "#383d41" + } + }, + "font": "theme.font_regular", + "size": 10.5, + "selected": "none", + "notes": "", + "image_rect": [ + 0, + 113, + 92, + 855 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 14, + "text": "Pages", + "ocr_height": 15.6, + "ink": "#777a7e", + "ground": "#f5f5f6", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 15, + "text": "Outline", + "ocr_height": 13.4, + "ink": "#85878b", + "ground": "#f6f7f7", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 22, + "text": "Comments", + "ocr_height": 14.1, + "ink": "#828589", + "ground": "#f6f6f7", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 25, + "text": "Search", + "ocr_height": 15.9, + "ink": "#85878b", + "ground": "#f8f8f8", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 5, + "bounds": [ + 2, + 115, + 90, + 853 + ], + "fill": "#fcfcfc", + "radius": 0 + } + ], + "dividers": [ + 18, + 19 + ] + } + }, + { + "id": "combine", + "role": "Combine card: rows with drag handle, first page, name, pages, remove; Add files; total; name; Combine", + "widget": "ScrollYView combine (static; combine_view): RoundedShadowView card (max 717), Label 'Combine files' bold 16, RoundedView list: per PDF a row 113 tall (GestureView grip with on_pan: drop in rows, RoundedView 70 x 94 with Image cover fit Smallest, Label name, RoundedView range box with TextInput 'All' + Label '(n pages)', IconBtn close), dashed 'Add files' (SolidView dashes) opening the library list, Label 'One PDF of n pages', Label 'Name' + Field, Primary 'Combine' (pdf.merge with ranges)", + "cite": [ + "makepad widgets/src/view_ui.rs:805 (ScrollYView); a view keeps its scroll position across its own render(): view.rs:296-306", + "makepad widgets/src/view_ui.rs:227 (RoundedShadowView)", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "App Flow docs/SCRIPT-API.md 'Host services: host.request'; widgets/src/splash_host.rs" + ], + "geometry": { + "card_max": 717, + "padding": [ + 24, + 34 + ], + "row": 113, + "thumb": [ + 70, + 94 + ], + "range_box": [ + 178, + 42 + ], + "add": 60, + "name_field": [ + 310, + 40 + ], + "combine": [ + 140, + 46 + ] + }, + "colors": { + "light": { + "card": "#ffffff", + "border": "#e1e2e3", + "name": "#37393c", + "range": "#55575b", + "add": "#1c6ce0", + "total": "#55575b", + "combine": "#116cdc" + }, + "dark": { + "card": "#232a2e", + "border": "#383d41", + "name": "#d5d7d8", + "range": "#b0b2b4", + "add": "#4d91e3", + "total": "#b0b2b4", + "combine": "#2f75d8" + } + }, + "font": "theme.font_regular", + "size": 13, + "notes": "the range is typed in place (the runtime places no script popover), so the box has no chevron", + "image_rect": [ + 420, + 150, + 720, + 705 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 16, + "text": "Combine files", + "ocr_height": 25, + "ink": "#3c3d3e", + "ground": "#f5f5f5", + "ink_px": 19, + "size": 19.0 + }, + { + "i": 20, + "text": "Q3 2026 Board Report.pdf", + "ocr_height": 18.1, + "ink": "#57585a", + "ground": "#f5f5f5", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 21, + "text": "All (24 pages)", + "ocr_height": 18.1, + "ink": "#7b7d80", + "ground": "#f7f7f7", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 23, + "text": "Appendix A - Figures.pdf", + "ocr_height": 18.1, + "ink": "#4f5051", + "ground": "#f5f5f5", + "ink_px": 17, + "size": 14.0 + }, + { + "i": 24, + "text": "1-4,9 (5 pages)", + "ocr_height": 19.0, + "ink": "#818386", + "ground": "#f8f8f9", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 26, + "text": "::", + "ocr_height": 20.1, + "ink": "#6c6f76", + "ground": "#f7f7f7", + "ink_px": 21, + "size": 21.0 + }, + { + "i": 27, + "text": "Cover Letter.pdf", + "ocr_height": 18.1, + "ink": "#4d4e50", + "ground": "#f5f5f5", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 28, + "text": "All (2 pages)", + "ocr_height": 20.3, + "ink": "#7b7d81", + "ground": "#f7f7f8", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 29, + "text": "X", + "ocr_height": 20.1, + "ink": "#6e7177", + "ground": "#f9f9f9", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 30, + "text": "X", + "ocr_height": 17.8, + "ink": "#6a6d73", + "ground": "#f8f8f8", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 31, + "text": "+ Add files", + "ocr_height": 22.4, + "ink": "#5693e5", + "ground": "#f8fafc", + "ink_px": 15, + "size": 15.0 + }, + { + "i": 32, + "text": "One PDF of 31 pages", + "ocr_height": 20.4, + "ink": "#606367", + "ground": "#f6f6f6", + "ink_px": 17, + "size": 14.0 + }, + { + "i": 33, + "text": "Name", + "ocr_height": 13.4, + "ink": "#6f7173", + "ground": "#f5f5f5", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 34, + "text": "Q3 2026 Board Pack", + "ocr_height": 16, + "ink": "#646669", + "ground": "#f4f4f4", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 35, + "text": "Combine", + "ocr_height": 22.3, + "ink": "#cde3f8", + "ground": "#2175e2", + "ink_px": 20, + "size": 20.0 + } + ], + "surfaces": [ + { + "i": 2, + "bounds": [ + 420, + 155, + 717, + 696 + ], + "fill": "#fcfcfc", + "radius": 7 + }, + { + "i": 3, + "bounds": [ + 444, + 239, + 669, + 414 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 6, + "bounds": [ + 448, + 474, + 662, + 113 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 7, + "bounds": [ + 448, + 358, + 662, + 113 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 8, + "bounds": [ + 448, + 243, + 662, + 113 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 11, + "bounds": [ + 448, + 589, + 662, + 60 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 14, + "bounds": [ + 501, + 725, + 310, + 40 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 16, + "bounds": [ + 504, + 728, + 304, + 33 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 19, + "bounds": [ + 876, + 278, + 178, + 43 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 21, + "bounds": [ + 875, + 510, + 179, + 42 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 22, + "bounds": [ + 875, + 394, + 179, + 42 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 24, + "bounds": [ + 970, + 779, + 140, + 46 + ], + "fill": "#1e71e3", + "radius": 3 + }, + { + "i": 26, + "bounds": [ + 879, + 513, + 172, + 36 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 27, + "bounds": [ + 879, + 397, + 172, + 36 + ], + "fill": "#fcfcfc", + "radius": 0 + }, + { + "i": 28, + "bounds": [ + 879, + 282, + 172, + 35 + ], + "fill": "#fcfcfc", + "radius": 0 + } + ], + "dividers": [ + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 20, + 21, + 22, + 23, + 24 + ] + } + }, + { + "id": "canvas", + "role": "canvas: the desk behind the pages", + "widget": "ScrollXYView canvas (on_render canvas_view): a run of up to 8 pages from the page gone to (go_page: drawn empty for one frame so the scroll clamps to the top, then filled), 'Continue with page n' after the run", + "cite": [ + "makepad widgets/src/view_ui.rs:790 (ScrollXYView); scroll clamped to content each draw: scroll_bar.rs:1264-1267", + "makepad widgets/src/view.rs:904-929 (render), a fn returning a widget may be called in on_render and inside literals (card-host probe, pdftools2-spike/scope)" + ], + "geometry": { + "width": "Fill", + "page_top": 13, + "page_gap": 13, + "bottom": 92, + "sides": 24, + "align": "centre" + }, + "colors": { + "light": { + "desk": "#eceded" + }, + "dark": { + "desk": "#191f24" + } + }, + "notes": "", + "image_rect": [ + 92, + 113, + 1444, + 855 + ], + "status": "built", + "decision": null, + "source": { + "text": [], + "surfaces": [], + "dividers": [ + 16, + 17 + ] + } + }, + { + "id": "status", + "role": "status line: save state, storage used", + "widget": "SolidView status (on_render status_view): RoundedView 12 dot (green Saved, orange Edited/Saving), Label state, Label ' of 64 MB used' (the library's PDFs against the 64 MiB storage); on Home the label and a RoundedView bar", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "height": 44, + "padding": [ + 33, + 29 + ], + "dot": 12, + "gap": 11 + }, + "colors": { + "light": { + "status": "#ffffff", + "text": "#76787c", + "saved": "#49b039", + "edited": "#f2a20d" + }, + "dark": { + "status": "#1b2226", + "text": "#9a9da0", + "saved": "#38a540", + "edited": "#e39a1c" + } + }, + "font": "theme.font_regular", + "size": 11.5, + "state": "Saved", + "notes": "", + "image_rect": [ + 0, + 968, + 1536, + 56 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 39, + "text": "• Saved", + "ocr_height": 18.3, + "ink": "#588f5a", + "ground": "#f7f8f8", + "ink_px": 15, + "size": 15.0 + }, + { + "i": 40, + "text": "14.6 of 64 MB used", + "ocr_height": 14, + "ink": "#6c6d6e", + "ground": "#f5f5f5", + "ink_px": 13, + "size": 13.0 + } + ], + "surfaces": [ + { + "i": 0, + "bounds": [ + 2, + 7, + 1533, + 963 + ], + "fill": "#f1f2f4", + "radius": 6 + }, + { + "i": 1, + "bounds": [ + 94, + 112, + 1441, + 856 + ], + "fill": "#f1f2f4", + "radius": 0 + }, + { + "i": 4, + "bounds": [ + 2, + 970, + 1533, + 53 + ], + "fill": "#fcfcfc", + "radius": 0 + } + ], + "dividers": [] + } + } + ], + "coverage": { + "text": 41, + "surfaces": 36, + "dividers": 25, + "unmapped": 0 + } +} diff --git a/apps/pdftools/design/map/07-fill-sign.json b/apps/pdftools/design/map/07-fill-sign.json new file mode 100644 index 00000000..e5a7cbd1 --- /dev/null +++ b/apps/pdftools/design/map/07-fill-sign.json @@ -0,0 +1,1558 @@ +{ + "schema": 1, + "screen": "07-fill-sign", + "state": "Fill & Sign on the lease: fields outlined, two required empty, initials placed", + "image": "source/07-fill-sign.png", + "image_sha256": "fc8314c6e42ea94a8d64df669b160e3eacc408ca4923bfe57fd1b3d4a04aa721", + "measured": "source/07-fill-sign.measured.json", + "measured_sha256": "8353ec793ec29952659243797188b35bdccef88300663f967602c2193df25aa3", + "units": "1 image pixel = 1 layout point (the designs are a 1536 x 1024 window)", + "built_in": "apps/pdftools/bundle/main.splash (v2)", + "runtime": "OctoSense-org/makepad d653bc44 with runtime-patches.lock.json, as tools/setup.py prepares .sources/makepad", + "type_rule": "size in theme units: the ink height for runs without descenders, ink / 1.23 with them (calibrated in card-host on the runtime's own text); the build may round", + "colours": "light from this screen (or the light screens), dark from 09-dark-reading; tokens below, selected by the shared interface's ui_light", + "tokens": { + "pt_accent": { + "light": "#116cdc", + "dark": "#2f75d8", + "from": "02 s19 Save fill #116cdc; 09 s16 Save #246acf, s08 outline selection #2970d7" + }, + "pt_link": { + "light": "#1c6ce0", + "dark": "#4d91e3", + "from": "02 t00 'Read' ink #3c7ae8 (anti-aliased); 09 t00 #468bd8" + }, + "pt_tabs": { + "light": "#f4f4f5", + "dark": "#191e21", + "from": "02 s04 tab row #f4f4f5; 09 tab row sample (1350,30) #191e21" + }, + "pt_tab_on": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 active tab; 09 active tab ground #22282c" + }, + "pt_bar": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 mode bar #fefefe; 09 t07 ground #22292d" + }, + "pt_rail": { + "light": "#ffffff", + "dark": "#1e252a", + "from": "02 rail #fefefe; 09 s02 #1e252a" + }, + "pt_panel": { + "light": "#ffffff", + "dark": "#192025", + "from": "02 s02 #fefefe; 09 s01 #192025" + }, + "pt_desk": { + "light": "#eceded", + "dark": "#191f24", + "from": "02 s00 #eceded; 09 desk sample (420,500) #191f24" + }, + "pt_home": { + "light": "#f7f8f9", + "dark": "#191f24", + "from": "01 s00 #f7f8f9; dark Home not designed: 09's desk" + }, + "pt_status": { + "light": "#ffffff", + "dark": "#1b2226", + "from": "02 status #fefefe; 09 status sample (700,1000) #1b2226" + }, + "pt_pill": { + "light": "#ffffff", + "dark": "#282d32", + "from": "02 pill #fefefe; 09 s04 #282d32" + }, + "pt_card": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "04/06/08 cards #fefefe; dark: 09 bar colour (cards not in 09)" + }, + "pt_line": { + "light": "#e1e2e3", + "dark": "#383d41", + "from": "02 d00 #e1e2e3; 09 d24 #383d41" + }, + "pt_line_soft": { + "light": "#ececee", + "dark": "#2c3236", + "from": "02 d02 #ecedee; 09 d07 #303437" + }, + "pt_thumb_line": { + "light": "#d3d5d9", + "dark": "#464b50", + "from": "02 thumbnails' border (s15 #e0e1f0 on the page edge, darker at 1x); dark derived" + }, + "pt_tint": { + "light": "#e9f2fe", + "dark": "#275294", + "from": "02 s16 rail selection #e9f2fe; 09 s14 #275293" + }, + "pt_tint_line": { + "light": "#cfe0fa", + "dark": "#34619f", + "from": "04 s27 Highlight tool border #dfebfc; dark derived from pt_tint" + }, + "pt_tint_ink": { + "light": "#2f6fd0", + "dark": "#bac7dd", + "from": "02 t01 rail 'Pages' ink #5973a6 (anti-aliased blue); 09 t03 #bac7dd" + }, + "pt_select": { + "light": "#eff5fc", + "dark": "#22344d", + "from": "03 s06 result group fill #eff5fc; dark derived" + }, + "pt_field": { + "light": "#ffffff", + "dark": "#1e2429", + "from": "02 s06 search #fefefe; 09 s10 #1e2429" + }, + "pt_field_line": { + "light": "#dcdcde", + "dark": "#3c4043", + "from": "02 d01 #dcdcde; 09 d02 #3c4043" + }, + "pt_hover": { + "light": "#f1f2f3", + "dark": "#2b3237", + "from": "hover: derived" + }, + "pt_ink": { + "light": "#37393c", + "dark": "#d5d7d8", + "from": "02 t16 'Pages' title #38393b, t05 tab #414142; 09 t06 tab #d1d3d4, t09 #d7d9da" + }, + "pt_ink2": { + "light": "#55575b", + "dark": "#b0b2b4", + "from": "02 mode labels #5c5d5e-#6d6d6e; 09 mode labels #a9abad-#b3b4b4" + }, + "pt_muted": { + "light": "#76787c", + "dark": "#9a9da0", + "from": "02 t57 'Saved' #79797b, t58 #6d6d6e; 09 t59 #929698, t60 #9b9d9f" + }, + "pt_faint": { + "light": "#9c9da0", + "dark": "#7d8184", + "from": "02 t15 placeholder #9c9d9f; 09 dimmer than t25 #9fa2a5" + }, + "pt_icon": { + "light": "#5b5d61", + "dark": "#a9acae", + "from": "02 icons; 09 icons" + }, + "pt_green": { + "light": "#49b039", + "dark": "#38a540", + "from": "02 'Saved' dot sample #49b039; 09 #38a540" + }, + "pt_orange": { + "light": "#f2a20d", + "dark": "#e39a1c", + "from": "08 'Edited' dot (31,984)" + }, + "pt_red": { + "light": "#e5534f", + "dark": "#f06a66", + "from": "07 t74 'Monthly rent *' #e85652, t75 #ed4f4a" + }, + "pt_shadow": { + "light": "#00000022", + "dark": "#00000070", + "from": "page and pill shadows: soft, from 02 page edge #e9eaeb" + } + }, + "regions": [ + { + "id": "pill", + "role": "canvas controls: page n / N, previous, next, zoom out, zoom %, zoom in, fit width, fit page", + "widget": "View pill (overlay, bottom centre; on_render pill_view): RoundedShadowView radius 26.5 with IconBtn chevron-left, Label 'p / N' (78 wide), IconBtn chevron-right, SolidView 1x28, IconBtn minus, Label zoom % (66 wide), IconBtn plus, SolidView, IconBtn fit-width, IconBtn fit-page", + "cite": [ + "makepad widgets/src/view_ui.rs:227 (RoundedShadowView)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'" + ], + "geometry": { + "height": 53, + "radius": 26.5, + "bottom": 12, + "padding": 14 + }, + "colors": { + "light": { + "pill": "#ffffff", + "line": "#e1e2e3", + "text": "#55575b", + "icon": "#5b5d61", + "shadow": "#00000022" + }, + "dark": { + "pill": "#282d32", + "line": "#383d41", + "text": "#b0b2b4", + "icon": "#a9acae", + "shadow": "#00000070" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 558, + 894, + 418, + 58 + ], + "status": "built", + "source": { + "text": [ + { + "i": 60, + "text": "<", + "ocr_height": 13.4, + "ink": "#484849", + "ground": "#f8f8f8", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 61, + "text": "1 / 12", + "ocr_height": 15.6, + "ink": "#535353", + "ground": "#f9f9f9", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 62, + "text": "100%", + "ocr_height": 13.4, + "ink": "#4f5050", + "ground": "#f6f6f6", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 63, + "text": "+", + "ocr_height": 20.1, + "ink": "#6e6e6f", + "ground": "#fcfcfc", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 6, + "bounds": [ + 560, + 896, + 417, + 54 + ], + "fill": "#fdfdfd", + "radius": 0 + } + ], + "dividers": [ + 25, + 26, + 27 + ] + } + }, + { + "id": "page1", + "role": "page: the engine's render", + "page_content": true, + "widget": "RectShadowView (white, shadow 7) with Image (binary_resource of pdf.page's PNG at 72 x zoom x 2 dpi), LoadingSpinner + 'Rendering page n…' until it lands; overlays placed by margin in points x zoom; GestureView over it (on_tap/on_double_tap in page points)", + "cite": [ + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/loading_spinner.rs:7 (LoadingSpinner); App Flow docs/SCRIPT-API.md 'Widgets available'", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": { + "size": "the engine's page size x zoom (100% = 1 pt per layout point)" + }, + "colors": { + "light": { + "page": "#ffffff", + "shadow": "#00000022" + }, + "dark": { + "page": "#ffffff", + "shadow": "#00000070" + } + }, + "decision": "Page contents are sample art: every page is the engine's render (pdf.page); highlights, matches and fields are overlays from the engine's rectangles.", + "image_rect": [ + 422, + 166, + 652, + 801 + ], + "status": "built", + "source": { + "text": [ + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59 + ], + "surfaces": [ + 0, + 1, + 16, + 22, + 23, + 25, + 26, + 29, + 30, + 31, + 32, + 33, + 34, + 35 + ], + "dividers": [ + 5, + 6, + 7, + 8, + 9, + 10, + 35, + 36, + 37, + 38 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "renders", + "role": "Pages panel thumbnails: the engine's renders", + "page_content": true, + "widget": "Image (binary_resource of a pdf.page PNG) inside its frame", + "cite": [ + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": {}, + "colors": {}, + "decision": "Page contents are sample art: every page image is the engine's render (pdf.page).", + "image_rect": [ + 114, + 182, + 172, + 516 + ], + "status": "built", + "source": { + "text": [ + 17, + 18 + ], + "surfaces": [ + 2, + 4, + 5 + ], + "dividers": [ + 31, + 32, + 33, + 34 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "tabs", + "role": "document tabs", + "widget": "SolidView tabs (on_render tabs_view): Tab = RoundedView per open PDF (Svg file-pdf 16, Label title with ellipsis, IconBtn close 13), Outline button 'Open' (Svg plus) after the last tab; Home tab only on Home", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256" + ], + "geometry": { + "height": 53, + "x": 0, + "width": "Fill", + "tab": "Fit, 48 tall from y 11; the row clips 6 so the open tab joins the mode bar", + "tab_padding": [ + 16, + 8 + ], + "gap": 2, + "open": "Outline 36 tall, 6 after the last tab" + }, + "colors": { + "light": { + "row": "#f4f4f5", + "tab_on": "#ffffff", + "border": "#e1e2e3", + "text_on": "#37393c", + "text": "#76787c" + }, + "dark": { + "row": "#191e21", + "tab_on": "#232a2e", + "border": "#383d41", + "text_on": "#d5d7d8", + "text": "#9a9da0" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 0, + 0, + 1536, + 48 + ], + "status": "built", + "source": { + "text": [ + { + "i": 0, + "text": "Q3 2026 Board Report.pdf", + "ocr_height": 16, + "ink": "#5d5e5e", + "ground": "#eeeeef", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 1, + "text": "Riverside Lease 2026.pdf", + "ocr_height": 16.0, + "ink": "#424242", + "ground": "#f5f5f5", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 2, + "text": "Field Guide to Garden Birds.pdf X", + "ocr_height": 16, + "ink": "#585959", + "ground": "#efefef", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 3, + "text": "Invoice INV-2041.pdf", + "ocr_height": 18, + "ink": "#606061", + "ground": "#f0f0f0", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 64, + "text": "Site Survey Photos.pdf", + "ocr_height": 16, + "ink": "#585959", + "ground": "#eeeeee", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 65, + "text": "+ Open", + "ocr_height": 16, + "ink": "#5d5d5e", + "ground": "#f0f0f1", + "ink_px": 14, + "size": 11.5 + } + ], + "surfaces": [ + { + "i": 17, + "bounds": [ + 482, + 7, + 251, + 41 + ], + "fill": "#f1f1f1", + "radius": 3 + }, + { + "i": 18, + "bounds": [ + 6, + 6, + 241, + 42 + ], + "fill": "#f1f1f1", + "radius": 3 + }, + { + "i": 19, + "bounds": [ + 963, + 7, + 230, + 41 + ], + "fill": "#f1f1f1", + "radius": 4 + }, + { + "i": 21, + "bounds": [ + 738, + 7, + 223, + 41 + ], + "fill": "#f1f1f1", + "radius": 3 + }, + { + "i": 28, + "bounds": [ + 1203, + 8, + 75, + 37 + ], + "fill": "#f2f2f2", + "radius": 4 + } + ], + "dividers": [ + 0, + 1 + ] + } + }, + { + "id": "modebar", + "role": "mode bar: Read · Comment · Fill & Sign · Pages · Combine · Edit, search field, Undo, Redo, Save", + "widget": "SolidView modebar (on_render modebar_view): per mode a fixed-width column (Flat text button + 3 pt SolidView underline, accent when current); RoundedView search box with Svg search and TextInput (on_return find), IconBtn undo/redo (faint when unavailable), SolidView 1x26 divider, Primary 'Save' with Svg save", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": { + "height": 54, + "padding": [ + 15, + 19 + ], + "mode_columns": [ + 69, + 101, + 100, + 78, + 99, + 61 + ], + "underline": "3 pt, inset 16, 1 pt above the bar's line", + "search": "Fill, at most 344, right-aligned, x 35, radius 5", + "undo_redo": "34 x 34, icons 20", + "save": "128 x 33, radius 5" + }, + "colors": { + "light": { + "bar": "#ffffff", + "mode": "#55575b", + "current": "#1c6ce0", + "underline": "#116cdc", + "field": "#ffffff", + "field_line": "#dcdcde", + "placeholder": "#9c9da0", + "save": "#116cdc" + }, + "dark": { + "bar": "#232a2e", + "mode": "#b0b2b4", + "current": "#4d91e3", + "underline": "#2f75d8", + "field": "#1e2429", + "field_line": "#3c4043", + "placeholder": "#7d8184", + "save": "#2f75d8" + } + }, + "font": "theme.font_regular", + "size": 12.5, + "current_mode": "fill", + "notes": "", + "image_rect": [ + 0, + 48, + 1536, + 60 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 4, + "text": "Read", + "ocr_height": 13.6, + "ink": "#4a4b4b", + "ground": "#f0f0f0", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 5, + "text": "Comment", + "ocr_height": 13.4, + "ink": "#575859", + "ground": "#f5f5f5", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 6, + "text": "Fill & Sign", + "ocr_height": 16.0, + "ink": "#3e86e8", + "ground": "#f1f6fc", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 7, + "text": "Pages", + "ocr_height": 15.6, + "ink": "#4a4b4b", + "ground": "#f2f2f3", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 8, + "text": "Combine", + "ocr_height": 13.4, + "ink": "#535354", + "ground": "#f3f3f4", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 9, + "text": "Edit", + "ocr_height": 15.6, + "ink": "#49494a", + "ground": "#f4f4f4", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 66, + "text": "Q Search in document", + "ocr_height": 18.0, + "ink": "#9c9c9d", + "ground": "#fafafa", + "ink_px": 16, + "size": 16.0 + }, + { + "i": 77, + "text": "5", + "ocr_height": 20.1, + "ink": "#6b6a6b", + "ground": "#fafafa", + "ink_px": 15, + "size": 15.0 + }, + { + "i": 78, + "text": "Save", + "ocr_height": 13.4, + "ink": "#cae3f9", + "ground": "#1971e6", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 15, + "bounds": [ + 996, + 58, + 281, + 40 + ], + "fill": "#fdfdfd", + "radius": 0 + }, + { + "i": 20, + "bounds": [ + 1000, + 61, + 274, + 34 + ], + "fill": "#fdfdfd", + "radius": 1 + }, + { + "i": 27, + "bounds": [ + 1437, + 60, + 82, + 36 + ], + "fill": "#156ce4", + "radius": 6 + } + ], + "dividers": [ + 2 + ] + } + }, + { + "id": "rail", + "role": "left rail: Pages, Outline, Comments, Search", + "widget": "SolidView rail (on_render rail_view): GestureView 69 x 77 per item holding a RoundedView (tint when selected), Svg 26 and Label", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 84, + "item": [ + 69, + 77 + ], + "first_top": 13, + "pitch": 85, + "radius": 6 + }, + "colors": { + "light": { + "rail": "#ffffff", + "selected": "#e9f2fe", + "selected_ink": "#2f6fd0", + "icon": "#5b5d61", + "label": "#76787c", + "line": "#e1e2e3" + }, + "dark": { + "rail": "#1e252a", + "selected": "#275294", + "selected_ink": "#bac7dd", + "icon": "#a9acae", + "label": "#9a9da0", + "line": "#383d41" + } + }, + "font": "theme.font_regular", + "size": 10.5, + "selected": "pages", + "notes": "", + "image_rect": [ + 0, + 108, + 84, + 864 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 12, + "text": "Pages", + "ocr_height": 13.4, + "ink": "#508dea", + "ground": "#dfebfc", + "ink_px": 12, + "size": 10.0 + }, + { + "i": 13, + "text": "Outline", + "ocr_height": 13.4, + "ink": "#888889", + "ground": "#f8f8f8", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 14, + "text": "Comments", + "ocr_height": 13.5, + "ink": "#959595", + "ground": "#f9f9f9", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 15, + "text": "Search", + "ocr_height": 11.2, + "ink": "#989899", + "ground": "#f8f8f8", + "ink_px": 10, + "size": 10.0 + } + ], + "surfaces": [ + { + "i": 24, + "bounds": [ + 10, + 123, + 64, + 65 + ], + "fill": "#e3edfc", + "radius": 4 + } + ], + "dividers": [ + 30 + ] + } + }, + { + "id": "panel", + "role": "left panel: Pages (thumbnails)", + "widget": "SolidView panel_narrow: head (Label 'Pages' bold 13, IconBtn close), ScrollYView thumbs (static: keeps its scroll) with per page a GestureView row: RoundedView frame (accent 2 pt when current), RoundedView page box 91 wide with Image (pdf.page at 24 dpi), Label page number", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:805 (ScrollYView); a view keeps its scroll position across its own render(): view.rs:296-306", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 204, + "head": 46, + "thumb_width": 91, + "thumb_left": 52, + "gap": 14, + "window": "40 pages around the current one" + }, + "colors": { + "light": { + "panel": "#ffffff", + "frame_on": "#116cdc", + "thumb_line": "#d3d5d9", + "number": "#76787c", + "number_on": "#116cdc" + }, + "dark": { + "panel": "#192025", + "frame_on": "#2f75d8", + "thumb_line": "#464b50", + "number": "#9a9da0", + "number_on": "#2f75d8" + } + }, + "font": "theme.font_bold", + "size": 13, + "notes": "07's panel is 239 wide; the build keeps 02's 204", + "image_rect": [ + 84, + 108, + 241, + 864 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 10, + "text": "Pages", + "ocr_height": 17.8, + "ink": "#3c3d3d", + "ground": "#f3f3f3", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 16, + "text": "X", + "ocr_height": 17.8, + "ink": "#5e5f5f", + "ground": "#f9f9f9", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 19, + "text": "2", + "ocr_height": 15.6, + "ink": "#4f4f50", + "ground": "#f6f6f6", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 20, + "text": "V", + "ocr_height": 11.2, + "ink": "#5f5f60", + "ground": "#fbfbfb", + "ink_px": 7, + "size": 7.0 + } + ], + "surfaces": [ + { + "i": 7, + "bounds": [ + 86, + 898, + 239, + 73 + ], + "fill": "#fdfdfd", + "radius": 0 + }, + { + "i": 14, + "bounds": [ + 86, + 109, + 238, + 48 + ], + "fill": "#fdfdfd", + "radius": 0 + } + ], + "dividers": [] + } + }, + { + "id": "banner", + "role": "form banner: field count, required empty, Highlight fields toggle", + "widget": "SolidView 48 tall (toolrow, form_banner): RoundedView 22 accent circle with 'i', Label summary (from pdf.fields), Label 'Highlight fields', Toggle (on_click |active|); while placing: the hint and Outline 'Cancel'", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/check_box.rs:354 (Toggle), active check_box.rs:424-426, on_click |active| check_box.rs:429" + ], + "geometry": { + "height": 48, + "padding": 22, + "gap": 12 + }, + "colors": { + "light": { + "banner": "#eff5fc", + "icon": "#116cdc", + "text": "#55575b" + }, + "dark": { + "banner": "#22344d", + "icon": "#2f75d8", + "text": "#b0b2b4" + } + }, + "font": "theme.font_regular", + "size": 12, + "image_rect": [ + 325, + 108, + 849, + 50 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 11, + "text": "This form has 6 fields. 2 required fields are empty.", + "ocr_height": 16.0, + "ink": "#525354", + "ground": "#f3f4f5", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 67, + "text": "Highlight fields", + "ocr_height": 16.0, + "ink": "#555657", + "ground": "#f1f3f4", + "ink_px": 14, + "size": 11.5 + } + ], + "surfaces": [ + { + "i": 3, + "bounds": [ + 327, + 109, + 845, + 48 + ], + "fill": "#f7fafb", + "radius": 1 + }, + { + "i": 36, + "bounds": [ + 1102, + 121, + 43, + 24 + ], + "fill": "#1a76ef", + "radius": 6 + } + ], + "dividers": [ + 3 + ] + } + }, + { + "id": "fields", + "role": "right panel: Fields, names and values, required in red; Add text, Add date, Add initials", + "widget": "SolidView right_side (360; fields_panel): head 'Fields', column heads, ScrollYView of GestureView rows (select_field) with Label name (red ' *' when required and empty) and Label value ('Empty' faint), the chosen row opens an editor (Field / checkbox choice / option chips) with Primary 'Apply' (pdf.fill); Outline 'Add text', 'Add date', 'Add initials' (initials kept in storage) arm a tap on the page (pdf.fill_sign)", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:805 (ScrollYView); a view keeps its scroll position across its own render(): view.rs:296-306", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "App Flow docs/SCRIPT-API.md 'Host services: host.request'; widgets/src/splash_host.rs" + ], + "geometry": { + "width": 360, + "row": 47, + "name_column": 162, + "buttons": "Fill x 43, gap 16, padding 20/28" + }, + "colors": { + "light": { + "panel": "#ffffff", + "name": "#55575b", + "required": "#e5534f", + "value": "#55575b", + "empty": "#9c9da0", + "button_line": "#7fa6dd" + }, + "dark": { + "panel": "#192025", + "name": "#b0b2b4", + "required": "#f06a66", + "value": "#b0b2b4", + "empty": "#7d8184", + "button_line": "#7fa6dd" + } + }, + "font": "theme.font_regular", + "size": 12.5, + "notes": "'MC' in the design is sample initials: the build shows the person's own, kept in storage, or the text icon until they type them", + "image_rect": [ + 1172, + 108, + 364, + 864 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 68, + "text": "Fields", + "ocr_height": 17.8, + "ink": "#464647", + "ground": "#f6f6f6", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 69, + "text": "Field name", + "ocr_height": 13.7, + "ink": "#828283", + "ground": "#f7f7f7", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 70, + "text": "Tenant name", + "ocr_height": 15.6, + "ink": "#525353", + "ground": "#f6f6f6", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 71, + "text": "Unit", + "ocr_height": 13.4, + "ink": "#5a5a5b", + "ground": "#f6f6f6", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 72, + "text": "Start date", + "ocr_height": 18.0, + "ink": "#575758", + "ground": "#f7f7f7", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 73, + "text": "End date", + "ocr_height": 18.1, + "ink": "#59595a", + "ground": "#f8f8f8", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 74, + "text": "Monthly rent *", + "ocr_height": 20.5, + "ink": "#e85652", + "ground": "#fdf8f8", + "ink_px": 17, + "size": 14.0 + }, + { + "i": 75, + "text": "Initials (Landlord) *", + "ocr_height": 20.4, + "ink": "#ed4f4a", + "ground": "#fdf6f6", + "ink_px": 16, + "size": 16.0 + }, + { + "i": 76, + "text": "Initials (Tenant)*", + "ocr_height": 22.2, + "ink": "#ed5450", + "ground": "#fdf8f8", + "ink_px": 16, + "size": 16.0 + }, + { + "i": 79, + "text": "Value", + "ocr_height": 13.5, + "ink": "#8e8e8f", + "ground": "#f9f9f9", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 80, + "text": "Maya Chen", + "ocr_height": 21.2, + "ink": "#656566", + "ground": "#f9f9f9", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 81, + "text": "4B", + "ocr_height": 15.6, + "ink": "#5c5d5d", + "ground": "#f7f7f7", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 82, + "text": "1 November 2026", + "ocr_height": 16, + "ink": "#606061", + "ground": "#f6f6f6", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 83, + "text": "Empty", + "ocr_height": 20.1, + "ink": "#b7b7b8", + "ground": "#fbfbfb", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 84, + "text": "Empty", + "ocr_height": 20.1, + "ink": "#b9b9ba", + "ground": "#fbfbfb", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 85, + "text": "Empty", + "ocr_height": 20.1, + "ink": "#b8b8b9", + "ground": "#fbfbfb", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 86, + "text": "Empty", + "ocr_height": 20.1, + "ink": "#b5b5b6", + "ground": "#fbfbfb", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 87, + "text": "T Add text", + "ocr_height": 20.6, + "ink": "#585859", + "ground": "#f7f7f8", + "ink_px": 18, + "size": 18.0 + }, + { + "i": 88, + "text": "Add date", + "ocr_height": 13.6, + "ink": "#5b5b5c", + "ground": "#f3f3f3", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 89, + "text": "MC Add initials", + "ocr_height": 15.6, + "ink": "#555656", + "ground": "#f6f6f6", + "ink_px": 15, + "size": 15.0 + } + ], + "surfaces": [ + { + "i": 8, + "bounds": [ + 1193, + 815, + 325, + 44 + ], + "fill": "#fdfdfd", + "radius": 0 + }, + { + "i": 9, + "bounds": [ + 1193, + 760, + 325, + 43 + ], + "fill": "#fdfdfd", + "radius": 0 + }, + { + "i": 10, + "bounds": [ + 1193, + 702, + 325, + 43 + ], + "fill": "#fdfdfd", + "radius": 0 + }, + { + "i": 11, + "bounds": [ + 1196, + 705, + 319, + 37 + ], + "fill": "#fdfdfd", + "radius": 1 + }, + { + "i": 12, + "bounds": [ + 1196, + 818, + 318, + 37 + ], + "fill": "#fdfdfd", + "radius": 1 + }, + { + "i": 13, + "bounds": [ + 1196, + 763, + 318, + 37 + ], + "fill": "#fdfdfd", + "radius": 0 + } + ], + "dividers": [ + 4, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24 + ] + } + }, + { + "id": "field_boxes", + "role": "field outlines on the page (overlay)", + "widget": "RoundedView per field rect (pdf.fields, points x zoom): blue tint and border; red when required and empty; 2 pt accent when chosen", + "cite": [ + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": {}, + "colors": { + "light": { + "field": "#2d7ff914", + "border": "#7fa6dd", + "required": "#e5534f", + "chosen": "#116cdc" + }, + "dark": { + "field": "#2d7ff922", + "border": "#7fa6dd", + "required": "#e5534f", + "chosen": "#116cdc" + } + }, + "image_rect": [ + 0, + 0, + 0, + 0 + ], + "status": "built", + "decision": null, + "source": { + "text": [], + "surfaces": [], + "dividers": [] + } + }, + { + "id": "canvas", + "role": "canvas: the desk behind the pages", + "widget": "ScrollXYView canvas (on_render canvas_view): a run of up to 8 pages from the page gone to (go_page: drawn empty for one frame so the scroll clamps to the top, then filled), 'Continue with page n' after the run", + "cite": [ + "makepad widgets/src/view_ui.rs:790 (ScrollXYView); scroll clamped to content each draw: scroll_bar.rs:1264-1267", + "makepad widgets/src/view.rs:904-929 (render), a fn returning a widget may be called in on_render and inside literals (card-host probe, pdftools2-spike/scope)" + ], + "geometry": { + "width": "Fill", + "page_top": 13, + "page_gap": 13, + "bottom": 92, + "sides": 24, + "align": "centre" + }, + "colors": { + "light": { + "desk": "#eceded" + }, + "dark": { + "desk": "#191f24" + } + }, + "notes": "", + "image_rect": [ + 325, + 157, + 849, + 815 + ], + "status": "built", + "decision": null, + "source": { + "text": [], + "surfaces": [], + "dividers": [ + 28, + 29 + ] + } + }, + { + "id": "status", + "role": "status line: save state, storage used", + "widget": "SolidView status (on_render status_view): RoundedView 12 dot (green Saved, orange Edited/Saving), Label state, Label ' of 64 MB used' (the library's PDFs against the 64 MiB storage); on Home the label and a RoundedView bar", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "height": 44, + "padding": [ + 33, + 29 + ], + "dot": 12, + "gap": 11 + }, + "colors": { + "light": { + "status": "#ffffff", + "text": "#76787c", + "saved": "#49b039", + "edited": "#f2a20d" + }, + "dark": { + "status": "#1b2226", + "text": "#9a9da0", + "saved": "#38a540", + "edited": "#e39a1c" + } + }, + "font": "theme.font_regular", + "size": 11.5, + "state": "Saved", + "notes": "", + "image_rect": [ + 0, + 972, + 1536, + 52 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 90, + "text": "• Saved", + "ocr_height": 16, + "ink": "#508c6d", + "ground": "#f8f9f9", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 91, + "text": "14.6 of 64 MB used", + "ocr_height": 16.0, + "ink": "#606061", + "ground": "#f6f6f6", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [], + "dividers": [] + } + } + ], + "coverage": { + "text": 92, + "surfaces": 37, + "dividers": 39, + "unmapped": 0 + } +} diff --git a/apps/pdftools/design/map/08-edit.json b/apps/pdftools/design/map/08-edit.json new file mode 100644 index 00000000..d89bb124 --- /dev/null +++ b/apps/pdftools/design/map/08-edit.json @@ -0,0 +1,1433 @@ +{ + "schema": 1, + "screen": "08-edit", + "state": "Edit: paragraph 3 selected, new text typed in place", + "image": "source/08-edit.png", + "image_sha256": "5ad2add5b3fbdb5541b72c9f7bb3c857e4b1ddda0a068d6e1ac7b17f609f4bb6", + "measured": "source/08-edit.measured.json", + "measured_sha256": "866697b6fd8f78ab69d6e66251a5ae53370899cd3b5cb81fe2a6d5680f6eedc3", + "units": "1 image pixel = 1 layout point (the designs are a 1536 x 1024 window)", + "built_in": "apps/pdftools/bundle/main.splash (v2)", + "runtime": "OctoSense-org/makepad d653bc44 with runtime-patches.lock.json, as tools/setup.py prepares .sources/makepad", + "type_rule": "size in theme units: the ink height for runs without descenders, ink / 1.23 with them (calibrated in card-host on the runtime's own text); the build may round", + "colours": "light from this screen (or the light screens), dark from 09-dark-reading; tokens below, selected by the shared interface's ui_light", + "tokens": { + "pt_accent": { + "light": "#116cdc", + "dark": "#2f75d8", + "from": "02 s19 Save fill #116cdc; 09 s16 Save #246acf, s08 outline selection #2970d7" + }, + "pt_link": { + "light": "#1c6ce0", + "dark": "#4d91e3", + "from": "02 t00 'Read' ink #3c7ae8 (anti-aliased); 09 t00 #468bd8" + }, + "pt_tabs": { + "light": "#f4f4f5", + "dark": "#191e21", + "from": "02 s04 tab row #f4f4f5; 09 tab row sample (1350,30) #191e21" + }, + "pt_tab_on": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 active tab; 09 active tab ground #22282c" + }, + "pt_bar": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 mode bar #fefefe; 09 t07 ground #22292d" + }, + "pt_rail": { + "light": "#ffffff", + "dark": "#1e252a", + "from": "02 rail #fefefe; 09 s02 #1e252a" + }, + "pt_panel": { + "light": "#ffffff", + "dark": "#192025", + "from": "02 s02 #fefefe; 09 s01 #192025" + }, + "pt_desk": { + "light": "#eceded", + "dark": "#191f24", + "from": "02 s00 #eceded; 09 desk sample (420,500) #191f24" + }, + "pt_home": { + "light": "#f7f8f9", + "dark": "#191f24", + "from": "01 s00 #f7f8f9; dark Home not designed: 09's desk" + }, + "pt_status": { + "light": "#ffffff", + "dark": "#1b2226", + "from": "02 status #fefefe; 09 status sample (700,1000) #1b2226" + }, + "pt_pill": { + "light": "#ffffff", + "dark": "#282d32", + "from": "02 pill #fefefe; 09 s04 #282d32" + }, + "pt_card": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "04/06/08 cards #fefefe; dark: 09 bar colour (cards not in 09)" + }, + "pt_line": { + "light": "#e1e2e3", + "dark": "#383d41", + "from": "02 d00 #e1e2e3; 09 d24 #383d41" + }, + "pt_line_soft": { + "light": "#ececee", + "dark": "#2c3236", + "from": "02 d02 #ecedee; 09 d07 #303437" + }, + "pt_thumb_line": { + "light": "#d3d5d9", + "dark": "#464b50", + "from": "02 thumbnails' border (s15 #e0e1f0 on the page edge, darker at 1x); dark derived" + }, + "pt_tint": { + "light": "#e9f2fe", + "dark": "#275294", + "from": "02 s16 rail selection #e9f2fe; 09 s14 #275293" + }, + "pt_tint_line": { + "light": "#cfe0fa", + "dark": "#34619f", + "from": "04 s27 Highlight tool border #dfebfc; dark derived from pt_tint" + }, + "pt_tint_ink": { + "light": "#2f6fd0", + "dark": "#bac7dd", + "from": "02 t01 rail 'Pages' ink #5973a6 (anti-aliased blue); 09 t03 #bac7dd" + }, + "pt_select": { + "light": "#eff5fc", + "dark": "#22344d", + "from": "03 s06 result group fill #eff5fc; dark derived" + }, + "pt_field": { + "light": "#ffffff", + "dark": "#1e2429", + "from": "02 s06 search #fefefe; 09 s10 #1e2429" + }, + "pt_field_line": { + "light": "#dcdcde", + "dark": "#3c4043", + "from": "02 d01 #dcdcde; 09 d02 #3c4043" + }, + "pt_hover": { + "light": "#f1f2f3", + "dark": "#2b3237", + "from": "hover: derived" + }, + "pt_ink": { + "light": "#37393c", + "dark": "#d5d7d8", + "from": "02 t16 'Pages' title #38393b, t05 tab #414142; 09 t06 tab #d1d3d4, t09 #d7d9da" + }, + "pt_ink2": { + "light": "#55575b", + "dark": "#b0b2b4", + "from": "02 mode labels #5c5d5e-#6d6d6e; 09 mode labels #a9abad-#b3b4b4" + }, + "pt_muted": { + "light": "#76787c", + "dark": "#9a9da0", + "from": "02 t57 'Saved' #79797b, t58 #6d6d6e; 09 t59 #929698, t60 #9b9d9f" + }, + "pt_faint": { + "light": "#9c9da0", + "dark": "#7d8184", + "from": "02 t15 placeholder #9c9d9f; 09 dimmer than t25 #9fa2a5" + }, + "pt_icon": { + "light": "#5b5d61", + "dark": "#a9acae", + "from": "02 icons; 09 icons" + }, + "pt_green": { + "light": "#49b039", + "dark": "#38a540", + "from": "02 'Saved' dot sample #49b039; 09 #38a540" + }, + "pt_orange": { + "light": "#f2a20d", + "dark": "#e39a1c", + "from": "08 'Edited' dot (31,984)" + }, + "pt_red": { + "light": "#e5534f", + "dark": "#f06a66", + "from": "07 t74 'Monthly rent *' #e85652, t75 #ed4f4a" + }, + "pt_shadow": { + "light": "#00000022", + "dark": "#00000070", + "from": "page and pill shadows: soft, from 02 page edge #e9eaeb" + } + }, + "regions": [ + { + "id": "pill", + "role": "canvas controls: page n / N, previous, next, zoom out, zoom %, zoom in, fit width, fit page", + "widget": "View pill (overlay, bottom centre; on_render pill_view): RoundedShadowView radius 26.5 with IconBtn chevron-left, Label 'p / N' (78 wide), IconBtn chevron-right, SolidView 1x28, IconBtn minus, Label zoom % (66 wide), IconBtn plus, SolidView, IconBtn fit-width, IconBtn fit-page", + "cite": [ + "makepad widgets/src/view_ui.rs:227 (RoundedShadowView)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'" + ], + "geometry": { + "height": 53, + "radius": 26.5, + "bottom": 12, + "padding": 14 + }, + "colors": { + "light": { + "pill": "#ffffff", + "line": "#e1e2e3", + "text": "#55575b", + "icon": "#5b5d61", + "shadow": "#00000022" + }, + "dark": { + "pill": "#282d32", + "line": "#383d41", + "text": "#b0b2b4", + "icon": "#a9acae", + "shadow": "#00000070" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 500, + 864, + 498, + 61 + ], + "status": "built", + "source": { + "text": [ + { + "i": 44, + "text": "<", + "ocr_height": 15.6, + "ink": "#444445", + "ground": "#fafafa", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 45, + "text": "2 / 24", + "ocr_height": 13.4, + "ink": "#5d5d5d", + "ground": "#f8f8f8", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 47, + "text": "-", + "ocr_height": 11.2, + "ink": "#363637", + "ground": "#fafafa", + "ink_px": 1, + "size": 1.0 + }, + { + "i": 48, + "text": "100%", + "ocr_height": 13.4, + "ink": "#656666", + "ground": "#f5f5f5", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 49, + "text": "+", + "ocr_height": 17.8, + "ink": "#5d5d5e", + "ground": "#fbfbfb", + "ink_px": 13, + "size": 13.0 + } + ], + "surfaces": [ + { + "i": 7, + "bounds": [ + 311, + 867, + 716, + 88 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 10, + "bounds": [ + 502, + 867, + 494, + 55 + ], + "fill": "#fefefe", + "radius": 0 + } + ], + "dividers": [ + 26 + ] + } + }, + { + "id": "page1", + "role": "page: the engine's render", + "page_content": true, + "widget": "RectShadowView (white, shadow 7) with Image (binary_resource of pdf.page's PNG at 72 x zoom x 2 dpi), LoadingSpinner + 'Rendering page n…' until it lands; overlays placed by margin in points x zoom; GestureView over it (on_tap/on_double_tap in page points)", + "cite": [ + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/loading_spinner.rs:7 (LoadingSpinner); App Flow docs/SCRIPT-API.md 'Widgets available'", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": { + "size": "the engine's page size x zoom (100% = 1 pt per layout point)" + }, + "colors": { + "light": { + "page": "#ffffff", + "shadow": "#00000022" + }, + "dark": { + "page": "#ffffff", + "shadow": "#00000070" + } + }, + "decision": "Page contents are sample art: every page is the engine's render (pdf.page); highlights, matches and fields are overlays from the engine's rectangles.", + "image_rect": [ + 307, + 131, + 724, + 728 + ], + "status": "built", + "source": { + "text": [ + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 43 + ], + "surfaces": [ + 2, + 8, + 9 + ], + "dividers": [ + 8, + 9, + 14, + 15, + 20, + 21, + 22, + 23, + 24, + 25 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "page2", + "role": "page: the engine's render", + "page_content": true, + "widget": "RectShadowView (white, shadow 7) with Image (binary_resource of pdf.page's PNG at 72 x zoom x 2 dpi), LoadingSpinner + 'Rendering page n…' until it lands; overlays placed by margin in points x zoom; GestureView over it (on_tap/on_double_tap in page points)", + "cite": [ + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/loading_spinner.rs:7 (LoadingSpinner); App Flow docs/SCRIPT-API.md 'Widgets available'", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": { + "size": "the engine's page size x zoom (100% = 1 pt per layout point)" + }, + "colors": { + "light": { + "page": "#ffffff", + "shadow": "#00000022" + }, + "dark": { + "page": "#ffffff", + "shadow": "#00000070" + } + }, + "decision": "Page contents are sample art: every page is the engine's render (pdf.page); highlights, matches and fields are overlays from the engine's rectangles.", + "image_rect": [ + 309, + 862, + 722, + 93 + ], + "status": "built", + "source": { + "text": [ + 46 + ], + "surfaces": [], + "dividers": [ + 28, + 29 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "tabs", + "role": "document tabs", + "widget": "SolidView tabs (on_render tabs_view): Tab = RoundedView per open PDF (Svg file-pdf 16, Label title with ellipsis, IconBtn close 13), Outline button 'Open' (Svg plus) after the last tab; Home tab only on Home", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256" + ], + "geometry": { + "height": 53, + "x": 0, + "width": "Fill", + "tab": "Fit, 48 tall from y 11; the row clips 6 so the open tab joins the mode bar", + "tab_padding": [ + 16, + 8 + ], + "gap": 2, + "open": "Outline 36 tall, 6 after the last tab" + }, + "colors": { + "light": { + "row": "#f4f4f5", + "tab_on": "#ffffff", + "border": "#e1e2e3", + "text_on": "#37393c", + "text": "#76787c" + }, + "dark": { + "row": "#191e21", + "tab_on": "#232a2e", + "border": "#383d41", + "text_on": "#d5d7d8", + "text": "#9a9da0" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 0, + 0, + 1536, + 50 + ], + "status": "built", + "source": { + "text": [ + { + "i": 0, + "text": "Q3 2026 Board Report.pdf", + "ocr_height": 16, + "ink": "#464747", + "ground": "#f5f5f5", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 4, + "text": "Riverside Lease202.pdf", + "ocr_height": 16, + "ink": "#646568", + "ground": "#f1f2f4", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 11, + "text": "Field Guide to Garden Birds.pdf x", + "ocr_height": 20.2, + "ink": "#68696b", + "ground": "#f3f4f5", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 12, + "text": "Invoice INV-2041.pdf", + "ocr_height": 16, + "ink": "#666769", + "ground": "#f3f3f5", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 15, + "text": "Site Survey Photos.pdf", + "ocr_height": 18, + "ink": "#68696b", + "ground": "#f3f4f5", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 17, + "text": "+ Open", + "ocr_height": 18.1, + "ink": "#58595b", + "ground": "#f4f5f6", + "ink_px": 16, + "size": 13.0 + } + ], + "surfaces": [ + { + "i": 17, + "bounds": [ + 516, + 7, + 279, + 43 + ], + "fill": "#f5f6f8", + "radius": 6 + }, + { + "i": 20, + "bounds": [ + 270, + 7, + 248, + 43 + ], + "fill": "#f5f6f8", + "radius": 0 + }, + { + "i": 21, + "bounds": [ + 1018, + 7, + 236, + 43 + ], + "fill": "#f5f6f8", + "radius": 0 + }, + { + "i": 23, + "bounds": [ + 797, + 7, + 218, + 43 + ], + "fill": "#f5f6f8", + "radius": 1 + }, + { + "i": 31, + "bounds": [ + 1268, + 10, + 84, + 33 + ], + "fill": "#f6f7f9", + "radius": 0 + } + ], + "dividers": [ + 0 + ] + } + }, + { + "id": "modebar", + "role": "mode bar: Read · Comment · Fill & Sign · Pages · Combine · Edit, search field, Undo, Redo, Save", + "widget": "SolidView modebar (on_render modebar_view): per mode a fixed-width column (Flat text button + 3 pt SolidView underline, accent when current); RoundedView search box with Svg search and TextInput (on_return find), IconBtn undo/redo (faint when unavailable), SolidView 1x26 divider, Primary 'Save' with Svg save", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": { + "height": 54, + "padding": [ + 15, + 19 + ], + "mode_columns": [ + 69, + 101, + 100, + 78, + 99, + 61 + ], + "underline": "3 pt, inset 16, 1 pt above the bar's line", + "search": "Fill, at most 344, right-aligned, x 35, radius 5", + "undo_redo": "34 x 34, icons 20", + "save": "128 x 33, radius 5" + }, + "colors": { + "light": { + "bar": "#ffffff", + "mode": "#55575b", + "current": "#1c6ce0", + "underline": "#116cdc", + "field": "#ffffff", + "field_line": "#dcdcde", + "placeholder": "#9c9da0", + "save": "#116cdc" + }, + "dark": { + "bar": "#232a2e", + "mode": "#b0b2b4", + "current": "#4d91e3", + "underline": "#2f75d8", + "field": "#1e2429", + "field_line": "#3c4043", + "placeholder": "#7d8184", + "save": "#2f75d8" + } + }, + "font": "theme.font_regular", + "size": 12.5, + "current_mode": "edit", + "notes": "", + "image_rect": [ + 0, + 50, + 1536, + 67 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 1, + "text": "Read", + "ocr_height": 15.6, + "ink": "#676768", + "ground": "#f7f7f7", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 5, + "text": "Comment", + "ocr_height": 15.8, + "ink": "#707173", + "ground": "#fafafa", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 6, + "text": "Fill & Sign", + "ocr_height": 18.1, + "ink": "#676869", + "ground": "#f7f7f7", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 7, + "text": "Pages", + "ocr_height": 17.8, + "ink": "#6c6d6e", + "ground": "#f8f8f8", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 8, + "text": "Combine", + "ocr_height": 19.4, + "ink": "#636365", + "ground": "#f8f8f8", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 13, + "text": "Edit", + "ocr_height": 15.6, + "ink": "#4582ed", + "ground": "#f5f9fd", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 14, + "text": "Q", + "ocr_height": 17.8, + "ink": "#3a3b3c", + "ground": "#f5f5f6", + "ink_px": 19, + "size": 19.0 + }, + { + "i": 16, + "text": "Search in document", + "ocr_height": 13.5, + "ink": "#909193", + "ground": "#f7f8f8", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 18, + "text": "Save", + "ocr_height": 20.1, + "ink": "#c4dcfa", + "ground": "#0a66ee", + "ink_px": 18, + "size": 18.0 + } + ], + "surfaces": [ + { + "i": 16, + "bounds": [ + 974, + 64, + 313, + 40 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 22, + "bounds": [ + 978, + 68, + 306, + 33 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 28, + "bounds": [ + 1434, + 64, + 90, + 39 + ], + "fill": "#0765ee", + "radius": 4 + } + ], + "dividers": [ + 1, + 2, + 3, + 4, + 5, + 6 + ] + } + }, + { + "id": "rail", + "role": "left rail: Pages, Outline, Comments, Search", + "widget": "SolidView rail (on_render rail_view): GestureView 69 x 77 per item holding a RoundedView (tint when selected), Svg 26 and Label", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 84, + "item": [ + 69, + 77 + ], + "first_top": 13, + "pitch": 85, + "radius": 6 + }, + "colors": { + "light": { + "rail": "#ffffff", + "selected": "#e9f2fe", + "selected_ink": "#2f6fd0", + "icon": "#5b5d61", + "label": "#76787c", + "line": "#e1e2e3" + }, + "dark": { + "rail": "#1e252a", + "selected": "#275294", + "selected_ink": "#bac7dd", + "icon": "#a9acae", + "label": "#9a9da0", + "line": "#383d41" + } + }, + "font": "theme.font_regular", + "size": 10.5, + "selected": "pages", + "notes": "Edit starts with the left panel folded, as here", + "image_rect": [ + 0, + 117, + 99, + 838 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 2, + "text": "Pages", + "ocr_height": 15.6, + "ink": "#7d8188", + "ground": "#e3ecf8", + "ink_px": 13, + "size": 10.5 + }, + { + "i": 3, + "text": "Outline", + "ocr_height": 12.4, + "ink": "#88888a", + "ground": "#f8f8f8", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 9, + "text": "Comments", + "ocr_height": 13.5, + "ink": "#858688", + "ground": "#f8f8f8", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 10, + "text": "Search", + "ocr_height": 13.4, + "ink": "#8e8e90", + "ground": "#f9f9f9", + "ink_px": 11, + "size": 11.0 + } + ], + "surfaces": [ + { + "i": 6, + "bounds": [ + 7, + 118, + 92, + 838 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 27, + "bounds": [ + 21, + 132, + 65, + 74 + ], + "fill": "#e3edfc", + "radius": 4 + } + ], + "dividers": [ + 31, + 32 + ] + } + }, + { + "id": "edit_panel", + "role": "right panel: Edit text card: paragraph, font, Apply, Cancel", + "widget": "SolidView right_side (360; edit_panel): RoundedView card: Label 'Edit text', IconBtn close, Label 'Paragraph n' (pdf.lines), Label 'Font' + RoundedView box with the engine's font and size, Primary 'Apply' (pdf.edit_text), Outline 'Cancel'", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "App Flow docs/SCRIPT-API.md 'Host services: host.request'; widgets/src/splash_host.rs" + ], + "geometry": { + "width": 360, + "card_margin": 14, + "head": 54, + "row": 52, + "buttons": "Fill x 37, gap 14" + }, + "colors": { + "light": { + "card": "#ffffff", + "border": "#e1e2e3", + "title": "#37393c", + "text": "#55575b", + "apply": "#116cdc" + }, + "dark": { + "card": "#232a2e", + "border": "#383d41", + "title": "#d5d7d8", + "text": "#b0b2b4", + "apply": "#2f75d8" + } + }, + "font": "theme.font_regular", + "size": 12.5, + "decision": "'Aligment' is a generator typo; write 'Alignment'.", + "notes": "Alignment and Colour are not built: pdf.edit_text takes only text (contract change requested). The font box shows the engine's font name and size, read-only (no chevron).", + "image_rect": [ + 1180, + 117, + 356, + 838 + ], + "status": "built", + "source": { + "text": [ + { + "i": 19, + "text": "X", + "ocr_height": 20.1, + "ink": "#555658", + "ground": "#fbfbfb", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 32, + "text": "Edit text", + "ocr_height": 16.7, + "ink": "#4d4e4f", + "ground": "#f6f6f6", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 33, + "text": "Paragraph 3", + "ocr_height": 19.7, + "ink": "#616162", + "ground": "#f8f8f9", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 34, + "text": "Font", + "ocr_height": 15.6, + "ink": "#646465", + "ground": "#f8f8f9", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 35, + "text": "Serif • 11 pt", + "ocr_height": 15.8, + "ink": "#636465", + "ground": "#f8f8f8", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 36, + "text": "Aligament", + "ocr_height": 15.6, + "ink": "#676768", + "ground": "#f6f6f6", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 37, + "text": "Colour", + "ocr_height": 15.6, + "ink": "#565758", + "ground": "#f6f6f6", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 38, + "text": "Black", + "ocr_height": 13.4, + "ink": "#5f6061", + "ground": "#f1f1f2", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 39, + "text": "Apply", + "ocr_height": 17.8, + "ink": "#b9d5f9", + "ground": "#0964ed", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 40, + "text": "V", + "ocr_height": 11.2, + "ink": "#4c4f50", + "ground": "#f7f7f7", + "ink_px": 7, + "size": 7.0 + }, + { + "i": 41, + "text": "V", + "ocr_height": 11.2, + "ink": "#646668", + "ground": "#fbfbfb", + "ink_px": 8, + "size": 8.0 + }, + { + "i": 42, + "text": "Cancel", + "ocr_height": 13.4, + "ink": "#505152", + "ground": "#f4f4f4", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 3, + "bounds": [ + 1182, + 123, + 340, + 819 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 4, + "bounds": [ + 1185, + 237, + 333, + 701 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 11, + "bounds": [ + 1185, + 127, + 333, + 54 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 12, + "bounds": [ + 1185, + 183, + 333, + 52 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 13, + "bounds": [ + 1198, + 279, + 305, + 44 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 14, + "bounds": [ + 1199, + 465, + 304, + 44 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 15, + "bounds": [ + 1199, + 373, + 301, + 44 + ], + "fill": "#fdfdfe", + "radius": 0 + }, + { + "i": 18, + "bounds": [ + 1202, + 468, + 298, + 38 + ], + "fill": "#fefefe", + "radius": 1 + }, + { + "i": 19, + "bounds": [ + 1202, + 283, + 298, + 37 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 24, + "bounds": [ + 1351, + 542, + 152, + 40 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 25, + "bounds": [ + 1200, + 543, + 139, + 39 + ], + "fill": "#0761ec", + "radius": 3 + }, + { + "i": 26, + "bounds": [ + 1355, + 545, + 144, + 34 + ], + "fill": "#fefefe", + "radius": 1 + }, + { + "i": 29, + "bounds": [ + 1270, + 376, + 81, + 37 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 30, + "bounds": [ + 1354, + 376, + 75, + 37 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 32, + "bounds": [ + 1431, + 376, + 66, + 37 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 33, + "bounds": [ + 1202, + 376, + 66, + 37 + ], + "fill": "#edf3fd", + "radius": 3 + } + ], + "dividers": [ + 7, + 10, + 11, + 12, + 13, + 16, + 17, + 18, + 19, + 27, + 33, + 34, + 35, + 36, + 37 + ] + } + }, + { + "id": "edit_box", + "role": "the paragraph being edited, on the page (overlay)", + "widget": "View at the paragraph's box (pdf.lines paragraphs, points x zoom): RoundedView white with a 1.5 pt blue border, TextInput (is_multiline) holding the new text, six RoundedView 8 handles", + "cite": [ + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": { + "outset": 8 + }, + "colors": { + "light": { + "box": "#ffffff", + "border": "#2171ef", + "text": "#111111" + }, + "dark": { + "box": "#ffffff", + "border": "#2171ef", + "text": "#111111" + } + }, + "image_rect": [ + 340, + 425, + 500, + 110 + ], + "status": "built", + "decision": null, + "source": { + "text": [], + "surfaces": [], + "dividers": [] + } + }, + { + "id": "canvas", + "role": "canvas: the desk behind the pages", + "widget": "ScrollXYView canvas (on_render canvas_view): a run of up to 8 pages from the page gone to (go_page: drawn empty for one frame so the scroll clamps to the top, then filled), 'Continue with page n' after the run", + "cite": [ + "makepad widgets/src/view_ui.rs:790 (ScrollXYView); scroll clamped to content each draw: scroll_bar.rs:1264-1267", + "makepad widgets/src/view.rs:904-929 (render), a fn returning a widget may be called in on_render and inside literals (card-host probe, pdftools2-spike/scope)" + ], + "geometry": { + "width": "Fill", + "page_top": 13, + "page_gap": 13, + "bottom": 92, + "sides": 24, + "align": "centre" + }, + "colors": { + "light": { + "desk": "#eceded" + }, + "dark": { + "desk": "#191f24" + } + }, + "notes": "", + "image_rect": [ + 99, + 117, + 1081, + 838 + ], + "status": "built", + "decision": null, + "source": { + "text": [], + "surfaces": [], + "dividers": [] + } + }, + { + "id": "status", + "role": "status line: save state, storage used", + "widget": "SolidView status (on_render status_view): RoundedView 12 dot (green Saved, orange Edited/Saving), Label state, Label ' of 64 MB used' (the library's PDFs against the 64 MiB storage); on Home the label and a RoundedView bar", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "height": 44, + "padding": [ + 33, + 29 + ], + "dot": 12, + "gap": 11 + }, + "colors": { + "light": { + "status": "#ffffff", + "text": "#76787c", + "saved": "#49b039", + "edited": "#f2a20d" + }, + "dark": { + "status": "#1b2226", + "text": "#9a9da0", + "saved": "#38a540", + "edited": "#e39a1c" + } + }, + "font": "theme.font_regular", + "size": 11.5, + "state": "Edited", + "notes": "", + "image_rect": [ + 0, + 955, + 1536, + 69 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 50, + "text": "Edited", + "ocr_height": 13.7, + "ink": "#4c4c4d", + "ground": "#f2f2f2", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 51, + "text": "14.6 of 64 MB used", + "ocr_height": 16, + "ink": "#68696a", + "ground": "#f6f7f7", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 0, + "bounds": [ + 4, + 3, + 1530, + 1010 + ], + "fill": "#fefefe", + "radius": 0 + }, + { + "i": 1, + "bounds": [ + 97, + 116, + 1433, + 839 + ], + "fill": "#fefefe", + "radius": 1 + }, + { + "i": 5, + "bounds": [ + 7, + 957, + 1523, + 52 + ], + "fill": "#fefefe", + "radius": 0 + } + ], + "dividers": [ + 30 + ] + } + } + ], + "coverage": { + "text": 52, + "surfaces": 34, + "dividers": 38, + "unmapped": 0 + } +} diff --git a/apps/pdftools/design/map/09-dark-reading.json b/apps/pdftools/design/map/09-dark-reading.json new file mode 100644 index 00000000..35ec8e14 --- /dev/null +++ b/apps/pdftools/design/map/09-dark-reading.json @@ -0,0 +1,1217 @@ +{ + "schema": 1, + "screen": "09-dark-reading", + "state": "Dark: Reading page 3, the Outline panel open", + "image": "source/09-dark-reading.png", + "image_sha256": "f61f8b32d0359fbb41909b0e58bc7d0b5314c300a1bd72eb87f3c5f6c194e54b", + "measured": "source/09-dark-reading.measured.json", + "measured_sha256": "794bdfa5519c3502f33f122e5184787d590c65254db03dcd5259b69723a42aa0", + "units": "1 image pixel = 1 layout point (the designs are a 1536 x 1024 window)", + "built_in": "apps/pdftools/bundle/main.splash (v2)", + "runtime": "OctoSense-org/makepad d653bc44 with runtime-patches.lock.json, as tools/setup.py prepares .sources/makepad", + "type_rule": "size in theme units: the ink height for runs without descenders, ink / 1.23 with them (calibrated in card-host on the runtime's own text); the build may round", + "colours": "light from this screen (or the light screens), dark from 09-dark-reading; tokens below, selected by the shared interface's ui_light", + "tokens": { + "pt_accent": { + "light": "#116cdc", + "dark": "#2f75d8", + "from": "02 s19 Save fill #116cdc; 09 s16 Save #246acf, s08 outline selection #2970d7" + }, + "pt_link": { + "light": "#1c6ce0", + "dark": "#4d91e3", + "from": "02 t00 'Read' ink #3c7ae8 (anti-aliased); 09 t00 #468bd8" + }, + "pt_tabs": { + "light": "#f4f4f5", + "dark": "#191e21", + "from": "02 s04 tab row #f4f4f5; 09 tab row sample (1350,30) #191e21" + }, + "pt_tab_on": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 active tab; 09 active tab ground #22282c" + }, + "pt_bar": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "02 mode bar #fefefe; 09 t07 ground #22292d" + }, + "pt_rail": { + "light": "#ffffff", + "dark": "#1e252a", + "from": "02 rail #fefefe; 09 s02 #1e252a" + }, + "pt_panel": { + "light": "#ffffff", + "dark": "#192025", + "from": "02 s02 #fefefe; 09 s01 #192025" + }, + "pt_desk": { + "light": "#eceded", + "dark": "#191f24", + "from": "02 s00 #eceded; 09 desk sample (420,500) #191f24" + }, + "pt_home": { + "light": "#f7f8f9", + "dark": "#191f24", + "from": "01 s00 #f7f8f9; dark Home not designed: 09's desk" + }, + "pt_status": { + "light": "#ffffff", + "dark": "#1b2226", + "from": "02 status #fefefe; 09 status sample (700,1000) #1b2226" + }, + "pt_pill": { + "light": "#ffffff", + "dark": "#282d32", + "from": "02 pill #fefefe; 09 s04 #282d32" + }, + "pt_card": { + "light": "#ffffff", + "dark": "#232a2e", + "from": "04/06/08 cards #fefefe; dark: 09 bar colour (cards not in 09)" + }, + "pt_line": { + "light": "#e1e2e3", + "dark": "#383d41", + "from": "02 d00 #e1e2e3; 09 d24 #383d41" + }, + "pt_line_soft": { + "light": "#ececee", + "dark": "#2c3236", + "from": "02 d02 #ecedee; 09 d07 #303437" + }, + "pt_thumb_line": { + "light": "#d3d5d9", + "dark": "#464b50", + "from": "02 thumbnails' border (s15 #e0e1f0 on the page edge, darker at 1x); dark derived" + }, + "pt_tint": { + "light": "#e9f2fe", + "dark": "#275294", + "from": "02 s16 rail selection #e9f2fe; 09 s14 #275293" + }, + "pt_tint_line": { + "light": "#cfe0fa", + "dark": "#34619f", + "from": "04 s27 Highlight tool border #dfebfc; dark derived from pt_tint" + }, + "pt_tint_ink": { + "light": "#2f6fd0", + "dark": "#bac7dd", + "from": "02 t01 rail 'Pages' ink #5973a6 (anti-aliased blue); 09 t03 #bac7dd" + }, + "pt_select": { + "light": "#eff5fc", + "dark": "#22344d", + "from": "03 s06 result group fill #eff5fc; dark derived" + }, + "pt_field": { + "light": "#ffffff", + "dark": "#1e2429", + "from": "02 s06 search #fefefe; 09 s10 #1e2429" + }, + "pt_field_line": { + "light": "#dcdcde", + "dark": "#3c4043", + "from": "02 d01 #dcdcde; 09 d02 #3c4043" + }, + "pt_hover": { + "light": "#f1f2f3", + "dark": "#2b3237", + "from": "hover: derived" + }, + "pt_ink": { + "light": "#37393c", + "dark": "#d5d7d8", + "from": "02 t16 'Pages' title #38393b, t05 tab #414142; 09 t06 tab #d1d3d4, t09 #d7d9da" + }, + "pt_ink2": { + "light": "#55575b", + "dark": "#b0b2b4", + "from": "02 mode labels #5c5d5e-#6d6d6e; 09 mode labels #a9abad-#b3b4b4" + }, + "pt_muted": { + "light": "#76787c", + "dark": "#9a9da0", + "from": "02 t57 'Saved' #79797b, t58 #6d6d6e; 09 t59 #929698, t60 #9b9d9f" + }, + "pt_faint": { + "light": "#9c9da0", + "dark": "#7d8184", + "from": "02 t15 placeholder #9c9d9f; 09 dimmer than t25 #9fa2a5" + }, + "pt_icon": { + "light": "#5b5d61", + "dark": "#a9acae", + "from": "02 icons; 09 icons" + }, + "pt_green": { + "light": "#49b039", + "dark": "#38a540", + "from": "02 'Saved' dot sample #49b039; 09 #38a540" + }, + "pt_orange": { + "light": "#f2a20d", + "dark": "#e39a1c", + "from": "08 'Edited' dot (31,984)" + }, + "pt_red": { + "light": "#e5534f", + "dark": "#f06a66", + "from": "07 t74 'Monthly rent *' #e85652, t75 #ed4f4a" + }, + "pt_shadow": { + "light": "#00000022", + "dark": "#00000070", + "from": "page and pill shadows: soft, from 02 page edge #e9eaeb" + } + }, + "regions": [ + { + "id": "pill", + "role": "canvas controls: page n / N, previous, next, zoom out, zoom %, zoom in, fit width, fit page", + "widget": "View pill (overlay, bottom centre; on_render pill_view): RoundedShadowView radius 26.5 with IconBtn chevron-left, Label 'p / N' (78 wide), IconBtn chevron-right, SolidView 1x28, IconBtn minus, Label zoom % (66 wide), IconBtn plus, SolidView, IconBtn fit-width, IconBtn fit-page", + "cite": [ + "makepad widgets/src/view_ui.rs:227 (RoundedShadowView)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'" + ], + "geometry": { + "height": 53, + "radius": 26.5, + "bottom": 12, + "padding": 14 + }, + "colors": { + "light": { + "pill": "#ffffff", + "line": "#e1e2e3", + "text": "#55575b", + "icon": "#5b5d61", + "shadow": "#00000022" + }, + "dark": { + "pill": "#282d32", + "line": "#383d41", + "text": "#b0b2b4", + "icon": "#a9acae", + "shadow": "#00000070" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 549, + 846, + 525, + 60 + ], + "status": "built", + "source": { + "text": [ + { + "i": 53, + "text": "3 / 24", + "ocr_height": 23, + "ink": "#cbcbcc", + "ground": "#292e33", + "ink_px": 14, + "size": 14.0 + }, + { + "i": 54, + "text": ">", + "ocr_height": 29.0, + "ink": "#c3c4c5", + "ground": "#292e33", + "ink_px": 16, + "size": 16.0 + }, + { + "i": 56, + "text": "-", + "ocr_height": 15.6, + "ink": "#c9c9c9", + "ground": "#282c31", + "ink_px": 2, + "size": 2.0 + }, + { + "i": 57, + "text": "100%", + "ocr_height": 13.4, + "ink": "#c5c6c8", + "ground": "#292e33", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 58, + "text": "+", + "ocr_height": 29.0, + "ink": "#c8c7c8", + "ground": "#272c31", + "ink_px": 17, + "size": 17.0 + } + ], + "surfaces": [ + { + "i": 4, + "bounds": [ + 551, + 848, + 522, + 57 + ], + "fill": "#282d32", + "radius": 8 + } + ], + "dividers": [ + 16, + 17, + 18 + ] + } + }, + { + "id": "page1", + "role": "page: the engine's render", + "page_content": true, + "widget": "RectShadowView (white, shadow 7) with Image (binary_resource of pdf.page's PNG at 72 x zoom x 2 dpi), LoadingSpinner + 'Rendering page n…' until it lands; overlays placed by margin in points x zoom; GestureView over it (on_tap/on_double_tap in page points)", + "cite": [ + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/loading_spinner.rs:7 (LoadingSpinner); App Flow docs/SCRIPT-API.md 'Widgets available'", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": { + "size": "the engine's page size x zoom (100% = 1 pt per layout point)" + }, + "colors": { + "light": { + "page": "#ffffff", + "shadow": "#00000022" + }, + "dark": { + "page": "#ffffff", + "shadow": "#00000070" + } + }, + "decision": "Page contents are sample art: every page is the engine's render (pdf.page); highlights, matches and fields are overlays from the engine's rectangles.", + "image_rect": [ + 502, + 121, + 800, + 787 + ], + "status": "built", + "source": { + "text": [ + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52 + ], + "surfaces": [ + 0, + 13, + 15, + 18, + 19 + ], + "dividers": [ + 4, + 5, + 6, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 19, + 31, + 32, + 33, + 34, + 35, + 36 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "page2", + "role": "page: the engine's render", + "page_content": true, + "widget": "RectShadowView (white, shadow 7) with Image (binary_resource of pdf.page's PNG at 72 x zoom x 2 dpi), LoadingSpinner + 'Rendering page n…' until it lands; overlays placed by margin in points x zoom; GestureView over it (on_tap/on_double_tap in page points)", + "cite": [ + "makepad widgets/src/view_ui.rs:150 (RectShadowView: shadow_color, shadow_radius, shadow_offset)", + "makepad widgets/src/image.rs:71 (Image), ImageFit image_cache.rs:4; binary_resource platform/src/script/res.rs:1398; used by apps/quickdeck and v1 apps/pdftools", + "makepad widgets/src/view_ui.rs:5 (View); flow: Overlay children placed by margin from the parent's origin: draw/src/turtle.rs:3990-3995", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/loading_spinner.rs:7 (LoadingSpinner); App Flow docs/SCRIPT-API.md 'Widgets available'", + "makepad widgets/src/splash_storage.rs:446-457 (no 1 MiB cap on reads, splash_storage.rs:362-368)" + ], + "geometry": { + "size": "the engine's page size x zoom (100% = 1 pt per layout point)" + }, + "colors": { + "light": { + "page": "#ffffff", + "shadow": "#00000022" + }, + "dark": { + "page": "#ffffff", + "shadow": "#00000070" + } + }, + "decision": "Page contents are sample art: every page is the engine's render (pdf.page); highlights, matches and fields are overlays from the engine's rectangles.", + "image_rect": [ + 503, + 905, + 798, + 77 + ], + "status": "built", + "source": { + "text": [ + 55 + ], + "surfaces": [ + 3 + ], + "dividers": [ + 24, + 25 + ], + "note": "sample art inside the page: not rebuilt" + } + }, + { + "id": "tabs", + "role": "document tabs", + "widget": "SolidView tabs (on_render tabs_view): Tab = RoundedView per open PDF (Svg file-pdf 16, Label title with ellipsis, IconBtn close 13), Outline button 'Open' (Svg plus) after the last tab; Home tab only on Home", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256" + ], + "geometry": { + "height": 53, + "x": 0, + "width": "Fill", + "tab": "Fit, 48 tall from y 11; the row clips 6 so the open tab joins the mode bar", + "tab_padding": [ + 16, + 8 + ], + "gap": 2, + "open": "Outline 36 tall, 6 after the last tab" + }, + "colors": { + "light": { + "row": "#f4f4f5", + "tab_on": "#ffffff", + "border": "#e1e2e3", + "text_on": "#37393c", + "text": "#76787c" + }, + "dark": { + "row": "#191e21", + "tab_on": "#232a2e", + "border": "#383d41", + "text_on": "#d5d7d8", + "text": "#9a9da0" + } + }, + "font": "theme.font_regular", + "size": 12, + "decision": null, + "notes": "", + "image_rect": [ + 0, + 0, + 1536, + 53 + ], + "status": "built", + "source": { + "text": [ + { + "i": 6, + "text": "Q3 2026 Board Report.pdf X", + "ocr_height": 17.8, + "ink": "#d1d3d4", + "ground": "#22282c", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 16, + "text": "Riverside Lease 2026.pdf", + "ocr_height": 17.8, + "ink": "#96999b", + "ground": "#1c2225", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 20, + "text": "Field Guide to Garden Birds.pdf X", + "ocr_height": 17.8, + "ink": "#939698", + "ground": "#1b2024", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 21, + "text": "Invoice INV-2041.pdf", + "ocr_height": 15.6, + "ink": "#989b9d", + "ground": "#1d2225", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 22, + "text": "X", + "ocr_height": 13.4, + "ink": "#a7a9ab", + "ground": "#1b2124", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 23, + "text": "Site Survey Photos.pdf", + "ocr_height": 17.8, + "ink": "#949799", + "ground": "#1b2124", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 24, + "text": "+ Open", + "ocr_height": 15.8, + "ink": "#3068b3", + "ground": "#191e26", + "ink_px": 15, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 6, + "bounds": [ + 560, + 10, + 295, + 43 + ], + "fill": "#1a1f23", + "radius": 0 + }, + { + "i": 9, + "bounds": [ + 296, + 10, + 262, + 42 + ], + "fill": "#1a2023", + "radius": 0 + }, + { + "i": 11, + "bounds": [ + 855, + 10, + 232, + 42 + ], + "fill": "#1a2023", + "radius": 0 + }, + { + "i": 12, + "bounds": [ + 1091, + 12, + 241, + 40 + ], + "fill": "#1a2023", + "radius": 1 + }, + { + "i": 20, + "bounds": [ + 1387, + 14, + 80, + 34 + ], + "fill": "#191f22", + "radius": 0 + } + ], + "dividers": [ + 0 + ] + } + }, + { + "id": "modebar", + "role": "mode bar: Read · Comment · Fill & Sign · Pages · Combine · Edit, search field, Undo, Redo, Save", + "widget": "SolidView modebar (on_render modebar_view): per mode a fixed-width column (Flat text button + 3 pt SolidView underline, accent when current); RoundedView search box with Svg search and TextInput (on_return find), IconBtn undo/redo (faint when unavailable), SolidView 1x26 divider, Primary 'Save' with Svg save", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/button.rs:20 (ButtonFlat), draw_icon/icon_walk button.rs:470-477", + "makepad widgets/src/text_input.rs:468 (TextInput), on_change/on_return text_input.rs:675-677, is_multiline text_input.rs:576, empty_text text_input.rs:604", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/view_ui.rs:308 (RoundedView)" + ], + "geometry": { + "height": 54, + "padding": [ + 15, + 19 + ], + "mode_columns": [ + 69, + 101, + 100, + 78, + 99, + 61 + ], + "underline": "3 pt, inset 16, 1 pt above the bar's line", + "search": "Fill, at most 344, right-aligned, x 35, radius 5", + "undo_redo": "34 x 34, icons 20", + "save": "128 x 33, radius 5" + }, + "colors": { + "light": { + "bar": "#ffffff", + "mode": "#55575b", + "current": "#1c6ce0", + "underline": "#116cdc", + "field": "#ffffff", + "field_line": "#dcdcde", + "placeholder": "#9c9da0", + "save": "#116cdc" + }, + "dark": { + "bar": "#232a2e", + "mode": "#b0b2b4", + "current": "#4d91e3", + "underline": "#2f75d8", + "field": "#1e2429", + "field_line": "#3c4043", + "placeholder": "#7d8184", + "save": "#2f75d8" + } + }, + "font": "theme.font_regular", + "size": 12.5, + "current_mode": "read", + "notes": "", + "image_rect": [ + 0, + 53, + 1536, + 64 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 0, + "text": "Read", + "ocr_height": 18.7, + "ink": "#468bd8", + "ground": "#1f2630", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 7, + "text": "Comment", + "ocr_height": 15.8, + "ink": "#a9abad", + "ground": "#22292d", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 8, + "text": "Fill & Sign", + "ocr_height": 19.0, + "ink": "#aeb0b1", + "ground": "#232a2e", + "ink_px": 17, + "size": 14.0 + }, + { + "i": 17, + "text": "Pages", + "ocr_height": 18.9, + "ink": "#b0b1b2", + "ground": "#232a2e", + "ink_px": 17, + "size": 14.0 + }, + { + "i": 18, + "text": "Combine", + "ocr_height": 15.6, + "ink": "#a8a9ab", + "ground": "#22282d", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 19, + "text": "Edit", + "ocr_height": 15.6, + "ink": "#b3b4b4", + "ground": "#242b2f", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 25, + "text": "Search in document", + "ocr_height": 16, + "ink": "#9fa2a5", + "ground": "#21272b", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 26, + "text": "Save", + "ocr_height": 15.6, + "ink": "#d4e2f5", + "ground": "#296fcf", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [ + { + "i": 7, + "bounds": [ + 956, + 65, + 307, + 41 + ], + "fill": "#1e2429", + "radius": 0 + }, + { + "i": 10, + "bounds": [ + 959, + 69, + 301, + 34 + ], + "fill": "#1e2429", + "radius": 1 + }, + { + "i": 16, + "bounds": [ + 1403, + 66, + 115, + 39 + ], + "fill": "#246acf", + "radius": 4 + }, + { + "i": 17, + "bounds": [ + 1278, + 65, + 106, + 41 + ], + "fill": "#1e2429", + "radius": 0 + }, + { + "i": 21, + "bounds": [ + 1324, + 69, + 57, + 34 + ], + "fill": "#1e2429", + "radius": 0 + }, + { + "i": 22, + "bounds": [ + 1282, + 69, + 40, + 34 + ], + "fill": "#1e2529", + "radius": 0 + } + ], + "dividers": [ + 1, + 2, + 3 + ] + } + }, + { + "id": "rail", + "role": "left rail: Pages, Outline, Comments, Search", + "widget": "SolidView rail (on_render rail_view): GestureView 69 x 77 per item holding a RoundedView (tint when selected), Svg 26 and Label", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 84, + "item": [ + 69, + 77 + ], + "first_top": 13, + "pitch": 85, + "radius": 6 + }, + "colors": { + "light": { + "rail": "#ffffff", + "selected": "#e9f2fe", + "selected_ink": "#2f6fd0", + "icon": "#5b5d61", + "label": "#76787c", + "line": "#e1e2e3" + }, + "dark": { + "rail": "#1e252a", + "selected": "#275294", + "selected_ink": "#bac7dd", + "icon": "#a9acae", + "label": "#9a9da0", + "line": "#383d41" + } + }, + "font": "theme.font_regular", + "size": 10.5, + "selected": "outline", + "notes": "", + "image_rect": [ + 0, + 117, + 104, + 863 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 1, + "text": "Pages", + "ocr_height": 15.6, + "ink": "#989c9e", + "ground": "#21272b", + "ink_px": 14, + "size": 11.5 + }, + { + "i": 2, + "text": "=", + "ocr_height": 15.6, + "ink": "#b7c6dc", + "ground": "#285494", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 3, + "text": "Outline", + "ocr_height": 14.1, + "ink": "#bac7dd", + "ground": "#285290", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 4, + "text": "Comments", + "ocr_height": 13.8, + "ink": "#a4a8aa", + "ground": "#232a2e", + "ink_px": 11, + "size": 11.0 + }, + { + "i": 5, + "text": "Search", + "ocr_height": 14.4, + "ink": "#9da0a2", + "ground": "#22282d", + "ink_px": 11, + "size": 11.0 + } + ], + "surfaces": [ + { + "i": 2, + "bounds": [ + 6, + 117, + 97, + 860 + ], + "fill": "#1e252a", + "radius": 1 + }, + { + "i": 14, + "bounds": [ + 12, + 213, + 84, + 82 + ], + "fill": "#275293", + "radius": 4 + } + ], + "dividers": [ + 26, + 27, + 28 + ] + } + }, + { + "id": "outline", + "role": "left panel: Outline tree, the current section in the accent", + "widget": "SolidView panel_wide (288): head (Label 'Outline' bold 13, IconBtn close), ScrollYView outline: per bookmark a GestureView row 42 tall (go_page) with a RoundedView (accent when the current section), IconBtn triangle (fold), Svg bookmark, Label title; children indented 36", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:805 (ScrollYView); a view keeps its scroll position across its own render(): view.rs:296-306", + "makepad widgets/src/gesture_view.rs:44 (GestureView), on_tap/on_double_tap/on_pan gesture_view.rs:182-196, cumulative pan deltas gesture_view.rs:256", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/svg.rs:10 (Svg), draw_svg.svg from http_resource: draw/src/shader/draw_svg.rs:481-518; App Flow examples/health/cards/health-02/rounds/001/layout.json.native.splash:23", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "width": 288, + "row": 42, + "indent": 36, + "radius": 4 + }, + "colors": { + "light": { + "panel": "#ffffff", + "row": "#55575b", + "current": "#116cdc", + "current_text": "#ffffff" + }, + "dark": { + "panel": "#192025", + "row": "#94979a", + "current": "#2970d7", + "current_text": "#ccdff5" + } + }, + "font": "theme.font_regular", + "size": 12, + "notes": "the tree is the engine's outline from pdf.open", + "image_rect": [ + 104, + 117, + 282, + 863 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 9, + "text": "Outline", + "ocr_height": 14.0, + "ink": "#d7d9da", + "ground": "#282e32", + "ink_px": 14, + "size": 14.0 + }, + { + "i": 10, + "text": "Summary", + "ocr_height": 18.8, + "ink": "#94979a", + "ground": "#1b2126", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 11, + "text": "Revenue", + "ocr_height": 13.4, + "ink": "#ccdff5", + "ground": "#2f75d8", + "ink_px": 13, + "size": 13.0 + }, + { + "i": 12, + "text": "Revenue by region", + "ocr_height": 18.0, + "ink": "#94989b", + "ground": "#1c2328", + "ink_px": 16, + "size": 13.0 + }, + { + "i": 13, + "text": "Operations", + "ocr_height": 16.3, + "ink": "#999c9f", + "ground": "#1e2429", + "ink_px": 15, + "size": 12.0 + }, + { + "i": 14, + "text": "Outlook", + "ocr_height": 14.3, + "ink": "#95989a", + "ground": "#1c2328", + "ink_px": 12, + "size": 12.0 + }, + { + "i": 15, + "text": "Appendix", + "ocr_height": 17.8, + "ink": "#9b9ea1", + "ground": "#1e252a", + "ink_px": 16, + "size": 13.0 + } + ], + "surfaces": [ + { + "i": 1, + "bounds": [ + 105, + 168, + 280, + 809 + ], + "fill": "#192025", + "radius": 0 + }, + { + "i": 5, + "bounds": [ + 105, + 117, + 280, + 49 + ], + "fill": "#1f252a", + "radius": 0 + }, + { + "i": 8, + "bounds": [ + 107, + 215, + 272, + 45 + ], + "fill": "#2970d7", + "radius": 7 + } + ], + "dividers": [ + 7, + 8, + 20, + 21, + 22, + 23, + 29, + 30 + ] + } + }, + { + "id": "canvas", + "role": "canvas: the desk behind the pages", + "widget": "ScrollXYView canvas (on_render canvas_view): a run of up to 8 pages from the page gone to (go_page: drawn empty for one frame so the scroll clamps to the top, then filled), 'Continue with page n' after the run", + "cite": [ + "makepad widgets/src/view_ui.rs:790 (ScrollXYView); scroll clamped to content each draw: scroll_bar.rs:1264-1267", + "makepad widgets/src/view.rs:904-929 (render), a fn returning a widget may be called in on_render and inside literals (card-host probe, pdftools2-spike/scope)" + ], + "geometry": { + "width": "Fill", + "page_top": 13, + "page_gap": 13, + "bottom": 92, + "sides": 24, + "align": "centre" + }, + "colors": { + "light": { + "desk": "#eceded" + }, + "dark": { + "desk": "#191f24" + } + }, + "notes": "", + "image_rect": [ + 386, + 117, + 1150, + 863 + ], + "status": "built", + "decision": null, + "source": { + "text": [], + "surfaces": [], + "dividers": [] + } + }, + { + "id": "status", + "role": "status line: save state, storage used", + "widget": "SolidView status (on_render status_view): RoundedView 12 dot (green Saved, orange Edited/Saving), Label state, Label ' of 64 MB used' (the library's PDFs against the 64 MiB storage); on Home the label and a RoundedView bar", + "cite": [ + "makepad widgets/src/view_ui.rs:85 (SolidView); App Flow docs/SCRIPT-API.md 'Backgrounds'", + "makepad widgets/src/view_ui.rs:308 (RoundedView)", + "makepad widgets/src/label.rs:11 (Label), max_lines label.rs:258, text_overflow draw_text.rs:1295 (Ellipsis)" + ], + "geometry": { + "height": 44, + "padding": [ + 33, + 29 + ], + "dot": 12, + "gap": 11 + }, + "colors": { + "light": { + "status": "#ffffff", + "text": "#76787c", + "saved": "#49b039", + "edited": "#f2a20d" + }, + "dark": { + "status": "#1b2226", + "text": "#9a9da0", + "saved": "#38a540", + "edited": "#e39a1c" + } + }, + "font": "theme.font_regular", + "size": 11.5, + "state": "Saved", + "notes": "", + "image_rect": [ + 0, + 980, + 1536, + 44 + ], + "status": "built", + "decision": null, + "source": { + "text": [ + { + "i": 59, + "text": "Saved", + "ocr_height": 11.2, + "ink": "#929698", + "ground": "#21282c", + "ink_px": 10, + "size": 10.0 + }, + { + "i": 60, + "text": "14.6 of 64 MB used", + "ocr_height": 13.4, + "ink": "#9b9d9f", + "ground": "#1f252a", + "ink_px": 12, + "size": 12.0 + } + ], + "surfaces": [], + "dividers": [] + } + } + ], + "coverage": { + "text": 61, + "surfaces": 23, + "dividers": 37, + "unmapped": 0 + } +} diff --git a/apps/pdftools/design/measure_desktop.py b/apps/pdftools/design/measure_desktop.py new file mode 100644 index 00000000..250666d4 --- /dev/null +++ b/apps/pdftools/design/measure_desktop.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Measure a generated desktop UX screen for pixel mapping. + +App Flow's image library measures phone screens at a fixed 406 × 776 artboard +(flows/image-lib/observe.py, measure_surfaces.py). PDF Tools v2 is a desktop +app, so this applies the same two measurements to a screen at its own size: + +- text: Apple Vision OCR through App Flow's unchanged flows/image-lib/ocr.swift, + plus each run's ink colour (the darker or more saturated of the two colour + clusters inside its box) and the background behind it; +- surfaces: closed edges at three Canny thresholds, kept when they are nearly + rectangular, with their fill (median inside), border (median on the edge) + and an estimated corner radius; plus long one-pixel dividers. + +The output is evidence for the semantic map, not a widget tree: roles, widget +choices and every text that ships are reviewed by a person (App Flow step 8). + +Usage: python measure_desktop.py --ocr /flows/image-lib/ocr.swift SCREEN.png... +Writes SCREEN.measured.json beside each screen (reused while the image's hash +and this script are unchanged). +""" +import argparse +import hashlib +import json +import subprocess +from pathlib import Path + +import cv2 +import numpy as np +from PIL import Image + + +def sha256(data): + return hashlib.sha256(data).hexdigest() + + +def hexcolor(rgb): + return "#%02x%02x%02x" % tuple(int(round(c)) for c in rgb) + + +def ocr(path, swift): + raw = subprocess.check_output(["swift", "-O", str(swift), str(path)]) + return json.loads(raw) + + +def ink_and_ground(pixels, box): + x, y, w, h = [int(round(v)) for v in box] + x0, y0 = max(0, x - 1), max(0, y - 1) + x1, y1 = min(pixels.shape[1], x + w + 1), min(pixels.shape[0], y + h + 1) + patch = pixels[y0:y1, x0:x1].reshape(-1, 3).astype(np.float32) + if len(patch) < 4: + return None, None + criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 0.5) + _, labels, centers = cv2.kmeans(patch, 2, None, criteria, 3, cv2.KMEANS_PP_CENTERS) + counts = np.bincount(labels.flatten(), minlength=2) + ground = centers[int(np.argmax(counts))] + ink = centers[int(np.argmin(counts))] + return hexcolor(ink), hexcolor(ground) + + +def corner_radius(pixels, rect, fill): + x, y, w, h = rect + fill = np.array(fill, dtype=np.float32) + limit = max(1, min(w, h) // 3) + for r in range(0, limit): + px = pixels[min(pixels.shape[0] - 1, y + r), min(pixels.shape[1] - 1, x + r)].astype(np.float32) + if np.abs(px - fill).max() < 10: + return int(round(r * 1.4142)) + return limit + + +def surfaces(pixels): + gray = cv2.cvtColor(cv2.GaussianBlur(pixels, (3, 3), 0), cv2.COLOR_RGB2GRAY) + height, width = gray.shape + found = [] + for lo, hi in [(4, 12), (10, 30), (25, 70)]: + edges = cv2.Canny(gray, lo, hi) + edges = cv2.dilate(edges, np.ones((2, 2), np.uint8)) + contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) + for contour in contours: + x, y, w, h = cv2.boundingRect(contour) + if w < 24 or h < 16 or w * h < 900 or (w > width - 4 and h > height - 4): + continue + area = cv2.contourArea(contour) + if area < 0.80 * w * h: + continue + found.append((x, y, w, h)) + kept = [] + for rect in sorted(found, key=lambda r: -r[2] * r[3]): + if all(iou(rect, other) < 0.85 for other in kept): + kept.append(rect) + out = [] + for x, y, w, h in kept: + inner = pixels[y + 4:y + h - 4, x + 4:x + w - 4].reshape(-1, 3) + if len(inner) == 0: + continue + fill = np.median(inner, axis=0) + edge = np.concatenate([pixels[y, x:x + w], pixels[y + h - 1, x:x + w], pixels[y:y + h, x], pixels[y:y + h, x + w - 1]]) + border = np.median(edge, axis=0) + out.append({"bounds": [x, y, w, h], "fill": hexcolor(fill), "border": hexcolor(border), + "radius": corner_radius(pixels, (x, y, w, h), fill)}) + return out + + +def iou(a, b): + ax, ay, aw, ah = a + bx, by, bw, bh = b + ix = max(0, min(ax + aw, bx + bw) - max(ax, bx)) + iy = max(0, min(ay + ah, by + bh) - max(ay, by)) + inter = ix * iy + return inter / float(aw * ah + bw * bh - inter or 1) + + +def dividers(pixels): + lines = [] + h, w, _ = pixels.shape + signed = pixels.astype(np.int16) + for y in range(1, h - 1): + row, above, below = signed[y], signed[y - 1], signed[y + 1] + differs = (np.abs(row - above).max(axis=1) > 6) & (np.abs(row - below).max(axis=1) > 6) + run = longest_run(differs) + if run[1] - run[0] > w * 0.18: + lines.append({"axis": "h", "at": y, "from": int(run[0]), "to": int(run[1]), + "color": hexcolor(np.median(pixels[y, run[0]:run[1]], axis=0))}) + for x in range(1, w - 1): + col, left, right = signed[:, x], signed[:, x - 1], signed[:, x + 1] + differs = (np.abs(col - left).max(axis=1) > 6) & (np.abs(col - right).max(axis=1) > 6) + run = longest_run(differs) + if run[1] - run[0] > h * 0.18: + lines.append({"axis": "v", "at": x, "from": int(run[0]), "to": int(run[1]), + "color": hexcolor(np.median(pixels[run[0]:run[1], x], axis=0))}) + return lines + + +def longest_run(mask): + best, start = (0, 0), None + for i, on in enumerate(list(mask) + [False]): + if on and start is None: + start = i + elif not on and start is not None: + if i - start > best[1] - best[0]: + best = (start, i) + start = None + return best + + +def measure(path, swift): + path = Path(path) + data = path.read_bytes() + out = path.with_suffix(".measured.json") + me = sha256(Path(__file__).read_bytes()) + if out.exists(): + old = json.loads(out.read_text()) + if old.get("image_sha256") == sha256(data) and old.get("script_sha256") == me: + return out + pixels = np.asarray(Image.open(path).convert("RGB")) + text = ocr(path, swift) + runs = [] + for o in text["observations"]: + ink, ground = ink_and_ground(pixels, o["bounds"]) + runs.append({"text": o["text"], "bounds": [round(v, 1) for v in o["bounds"]], + "confidence": round(o["confidence"], 2), "ink": ink, "ground": ground}) + result = { + "schema_version": 1, + "image": path.name, + "image_sha256": sha256(data), + "script_sha256": me, + "size": [int(pixels.shape[1]), int(pixels.shape[0])], + "ocr_engine": "Apple Vision VNRecognizeTextRequest accurate, en-US (App Flow ocr.swift)", + "text": runs, + "surfaces": surfaces(pixels), + "dividers": dividers(pixels), + "limits": ["Evidence for review: roles and widgets are assigned in the semantic map, not here."], + } + out.write_text(json.dumps(result, indent=1) + "\n") + return out + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--ocr", required=True, help="App Flow's flows/image-lib/ocr.swift") + parser.add_argument("screens", nargs="+") + args = parser.parse_args() + for screen in args.screens: + print(measure(screen, args.ocr)) + + +if __name__ == "__main__": + main() diff --git a/apps/pdftools/design/source/01-home.measured.json b/apps/pdftools/design/source/01-home.measured.json new file mode 100644 index 00000000..77716ffa --- /dev/null +++ b/apps/pdftools/design/source/01-home.measured.json @@ -0,0 +1,1069 @@ +{ + "schema_version": 1, + "image": "01-home.png", + "image_sha256": "7f52b77e4b72a1421bb54fa35b1e8d6d7df1a6da08fbc2e71089db4e1a02ad74", + "script_sha256": "ea860507e32482b955d3ac577ab3cf9fc559f59ecc916c89175293932b54e0b7", + "size": [ + 1536, + 1024 + ], + "ocr_engine": "Apple Vision VNRecognizeTextRequest accurate, en-US (App Flow ocr.swift)", + "text": [ + { + "text": "Home", + "bounds": [ + 60.3, + 22.3, + 40.2, + 13.4 + ], + "confidence": 1, + "ink": "#383a3c", + "ground": "#f4f4f4" + }, + { + "text": "Q3 2026 Board Report.pdf x", + "bounds": [ + 241.1, + 22.0, + 194.2, + 16 + ], + "confidence": 1, + "ink": "#63666b", + "ground": "#f3f4f4" + }, + { + "text": "+ Open", + "bounds": [ + 488.9, + 22.0, + 58.0, + 16 + ], + "confidence": 1, + "ink": "#404246", + "ground": "#f2f3f4" + }, + { + "text": "Recent", + "bounds": [ + 343.8, + 298.9, + 73.7, + 15.6 + ], + "confidence": 1, + "ink": "#31363e", + "ground": "#eceeef" + }, + { + "text": "NORTHWIND", + "bounds": [ + 422.0, + 370.3, + 78.1, + 11.2 + ], + "confidence": 1, + "ink": "#5184c8", + "ground": "#f2f6fb" + }, + { + "text": "COOPERATIVE", + "bounds": [ + 422.0, + 381.5, + 73.7, + 11.2 + ], + "confidence": 1, + "ink": "#92b3da", + "ground": "#f6f9fc" + }, + { + "text": "Q3 2026", + "bounds": [ + 386.1, + 410.0, + 89.6, + 23.4 + ], + "confidence": 1, + "ink": "#323941", + "ground": "#f3f3f4" + }, + { + "text": "Board Report", + "bounds": [ + 386.2, + 434.8, + 136.3, + 25.0 + ], + "confidence": 1, + "ink": "#303840", + "ground": "#f4f5f5" + }, + { + "text": "Building strong communities", + "bounds": [ + 388.5, + 468.0, + 120.6, + 12 + ], + "confidence": 1, + "ink": "#b0b7c1", + "ground": "#f8f9fa" + }, + { + "text": "through cooperative growth.", + "bounds": [ + 386.2, + 481.9, + 120.6, + 9.1 + ], + "confidence": 1, + "ink": "#b5bcc6", + "ground": "#f7f8f9" + }, + { + "text": "Q3 2026 Board Report.pdf", + "bounds": [ + 381.8, + 555.5, + 174.1, + 15.6 + ], + "confidence": 1, + "ink": "#353639", + "ground": "#f3f4f4" + }, + { + "text": "24 pages \u2022 2.8 MB", + "bounds": [ + 381.8, + 580.0, + 113.9, + 13.4 + ], + "confidence": 1, + "ink": "#797d83", + "ground": "#f6f6f7" + }, + { + "text": "Opened today", + "bounds": [ + 381.8, + 602.0, + 89.3, + 14 + ], + "confidence": 1, + "ink": "#7e8289", + "ground": "#f7f7f7" + }, + { + "text": "INVOICE", + "bounds": [ + 381.7, + 675.9, + 93.8, + 20.3 + ], + "confidence": 1, + "ink": "#245eb9", + "ground": "#f1f5fa" + }, + { + "text": "INV-2041", + "bounds": [ + 383.8, + 699.8, + 56.1, + 14.8 + ], + "confidence": 1, + "ink": "#595d62", + "ground": "#f6f6f7" + }, + { + "text": "Bill to:", + "bounds": [ + 384.0, + 722.8, + 31.3, + 11.2 + ], + "confidence": 1, + "ink": "#707277", + "ground": "#f3f4f4" + }, + { + "text": "Northwind Cooperative", + "bounds": [ + 381.8, + 736.0, + 109.4, + 11.4 + ], + "confidence": 1, + "ink": "#8b8e94", + "ground": "#f7f7f7" + }, + { + "text": "123 Harbor Way", + "bounds": [ + 381.8, + 751.8, + 69.2, + 7.2 + ], + "confidence": 1, + "ink": "#8d9096", + "ground": "#f1f2f3" + }, + { + "text": "Portland, OR 97201", + "bounds": [ + 381.8, + 760.7, + 87.1, + 11.3 + ], + "confidence": 1, + "ink": "#8e9197", + "ground": "#f6f7f7" + }, + { + "text": "Descriptior", + "bounds": [ + 388.5, + 789.8, + 46.9, + 6.7 + ], + "confidence": 1, + "ink": "#84888c", + "ground": "#f0f2f2" + }, + { + "text": "Consulting Services", + "bounds": [ + 388.5, + 807.6, + 78.1, + 11.4 + ], + "confidence": 1, + "ink": "#9d9fa2", + "ground": "#f8f9f9" + }, + { + "text": "Invoice INV-2041.pdf", + "bounds": [ + 381.8, + 843.0, + 138.4, + 16.0 + ], + "confidence": 1, + "ink": "#3c3d3f", + "ground": "#f5f5f6" + }, + { + "text": "1 page \u2022 84 KB", + "bounds": [ + 381.6, + 864.7, + 91.8, + 17.5 + ], + "confidence": 1, + "ink": "#7c8086", + "ground": "#f9f9fa" + }, + { + "text": "Opened 1 Oct", + "bounds": [ + 381.8, + 887.9, + 89.3, + 13.4 + ], + "confidence": 1, + "ink": "#757a81", + "ground": "#f7f7f8" + }, + { + "text": "Amount", + "bounds": [ + 515.7, + 787.5, + 38.0, + 11.2 + ], + "confidence": 1, + "ink": "#7d7f84", + "ground": "#f6f7f7" + }, + { + "text": "$2,400.00", + "bounds": [ + 511.3, + 807.6, + 44.7, + 11.2 + ], + "confidence": 1, + "ink": "#83868a", + "ground": "#f5f6f6" + }, + { + "text": "14.6 of 64 MB used", + "bounds": [ + 20.1, + 995.0, + 125.0, + 13.4 + ], + "confidence": 1, + "ink": "#72757b", + "ground": "#f3f4f5" + }, + { + "text": "PDF", + "bounds": [ + 747.9, + 111.5, + 38.0, + 17.8 + ], + "confidence": 1, + "ink": "#2061c5", + "ground": "#eef3f9" + }, + { + "text": "PDF Tools", + "bounds": [ + 680.6, + 154.4, + 179.2, + 34.7 + ], + "confidence": 1, + "ink": "#2d3339", + "ground": "#f4f5f6" + }, + { + "text": "Open a PDF from this device", + "bounds": [ + 649.7, + 232.0, + 216.6, + 18 + ], + "confidence": 1, + "ink": "#d4e3f5", + "ground": "#0a61cb" + }, + { + "text": "RIVERSIDE", + "bounds": [ + 674.2, + 368.1, + 120.6, + 22.3 + ], + "confidence": 1, + "ink": "#247b76", + "ground": "#f0f6f5" + }, + { + "text": "LEASE AGREEMENT", + "bounds": [ + 674.2, + 394.9, + 140.7, + 13.4 + ], + "confidence": 1, + "ink": "#498986", + "ground": "#f3f8f7" + }, + { + "text": "2026", + "bounds": [ + 674.2, + 410.5, + 38.0, + 13.4 + ], + "confidence": 1, + "ink": "#438681", + "ground": "#f1f7f6" + }, + { + "text": "This Lease Agreement is made", + "bounds": [ + 674.2, + 435.0, + 145.1, + 11.2 + ], + "confidence": 1, + "ink": "#8d9096", + "ground": "#f6f7f7" + }, + { + "text": "and entered into as of", + "bounds": [ + 674.2, + 448.0, + 107.2, + 12 + ], + "confidence": 1, + "ink": "#898c93", + "ground": "#f6f7f7" + }, + { + "text": "January 1, 2026.", + "bounds": [ + 674.2, + 461.8, + 78.1, + 11.2 + ], + "confidence": 1, + "ink": "#888c94", + "ground": "#f5f6f7" + }, + { + "text": "Riverside Lease 2026.pdf", + "bounds": [ + 674.2, + 555.5, + 160.7, + 15.6 + ], + "confidence": 1, + "ink": "#313336", + "ground": "#f3f4f4" + }, + { + "text": "12 pages \u2022 1.1 MB", + "bounds": [ + 674.2, + 580.0, + 109.4, + 13.4 + ], + "confidence": 1, + "ink": "#797d85", + "ground": "#f6f6f7" + }, + { + "text": "Opened yesterday", + "bounds": [ + 674.2, + 602.0, + 109.4, + 16.0 + ], + "confidence": 1, + "ink": "#7d8087", + "ground": "#f8f8f9" + }, + { + "text": "Site Survey Photos", + "bounds": [ + 665.3, + 673.7, + 140.7, + 15.6 + ], + "confidence": 1, + "ink": "#35373a", + "ground": "#f3f3f3" + }, + { + "text": "Project: Community Center", + "bounds": [ + 665.3, + 696.0, + 107.2, + 11.2 + ], + "confidence": 1, + "ink": "#8b8c90", + "ground": "#f7f7f7" + }, + { + "text": "Site Survey Photos.pdf", + "bounds": [ + 674.2, + 843.0, + 149.6, + 16.0 + ], + "confidence": 1, + "ink": "#2c2d2f", + "ground": "#f2f2f3" + }, + { + "text": "18 pages \u2022 9.6 MB", + "bounds": [ + 674.2, + 867.8, + 111.6, + 13.4 + ], + "confidence": 1, + "ink": "#747980", + "ground": "#f7f7f8" + }, + { + "text": "Opened 28 Sep", + "bounds": [ + 674.2, + 890.0, + 96, + 14 + ], + "confidence": 1, + "ink": "#757a81", + "ground": "#f6f6f7" + }, + { + "text": "Up to 64 MB", + "bounds": [ + 917.6, + 234.0, + 78.1, + 16.0 + ], + "confidence": 1, + "ink": "#737980", + "ground": "#f3f4f5" + }, + { + "text": "Field Guide to", + "bounds": [ + 957.8, + 368.0, + 120.6, + 20.2 + ], + "confidence": 1, + "ink": "#3d3f42", + "ground": "#f5f5f4" + }, + { + "text": "Garden Birds", + "bounds": [ + 957.8, + 390.0, + 113.9, + 20.5 + ], + "confidence": 1, + "ink": "#444749", + "ground": "#f5f4f4" + }, + { + "text": "Identify common birds", + "bounds": [ + 957.8, + 417.0, + 100.5, + 11.3 + ], + "confidence": 1, + "ink": "#757476", + "ground": "#f5f4f4" + }, + { + "text": "in your backyard", + "bounds": [ + 960.0, + 430.6, + 75.9, + 11.4 + ], + "confidence": 1, + "ink": "#7d7e80", + "ground": "#f4f4f3" + }, + { + "text": "and local parks.", + "bounds": [ + 957.8, + 444.0, + 69.2, + 11.2 + ], + "confidence": 1, + "ink": "#7e7e80", + "ground": "#f5f5f5" + }, + { + "text": "Field Guide to Garden Birds.pdf", + "bounds": [ + 964.5, + 555.5, + 196.5, + 15.6 + ], + "confidence": 1, + "ink": "#343537", + "ground": "#f3f3f4" + }, + { + "text": "7 pages \u2022 16 KB", + "bounds": [ + 964.5, + 577.8, + 100.5, + 15.6 + ], + "confidence": 1, + "ink": "#7d8288", + "ground": "#f9f9f9" + }, + { + "text": "Opened 3 Oct", + "bounds": [ + 964.4, + 599.7, + 87.2, + 16.4 + ], + "confidence": 1, + "ink": "#7f848a", + "ground": "#f9f9fa" + } + ], + "surfaces": [ + { + "bounds": [ + 0, + 7, + 1536, + 1017 + ], + "fill": "#f7f8f9", + "border": "#d2d3d7", + "radius": 0 + }, + { + "bounds": [ + 341, + 334, + 268, + 292 + ], + "fill": "#fdfdfd", + "border": "#f9f9fa", + "radius": 0 + }, + { + "bounds": [ + 922, + 334, + 267, + 292 + ], + "fill": "#fcfbfa", + "border": "#f8f9fa", + "radius": 0 + }, + { + "bounds": [ + 633, + 334, + 267, + 292 + ], + "fill": "#fdfcfc", + "border": "#f8f8f9", + "radius": 0 + }, + { + "bounds": [ + 341, + 638, + 268, + 275 + ], + "fill": "#fdfdfd", + "border": "#fafafb", + "radius": 0 + }, + { + "bounds": [ + 632, + 641, + 268, + 273 + ], + "fill": "#fcfcfc", + "border": "#f8f9fa", + "radius": 0 + }, + { + "bounds": [ + 2, + 982, + 1533, + 41 + ], + "fill": "#f7f8f8", + "border": "#f7f8f9", + "radius": 0 + }, + { + "bounds": [ + 372, + 355, + 209, + 179 + ], + "fill": "#fdfcfd", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 925, + 538, + 261, + 84 + ], + "fill": "#fdfdfd", + "border": "#fdfcfc", + "radius": 0 + }, + { + "bounds": [ + 636, + 541, + 261, + 81 + ], + "fill": "#fdfdfd", + "border": "#fbfcfc", + "radius": 1 + }, + { + "bounds": [ + 345, + 829, + 261, + 80 + ], + "fill": "#fdfdfd", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 584, + 214, + 315, + 51 + ], + "fill": "#015dca", + "border": "#f7f6f8", + "radius": 4 + }, + { + "bounds": [ + 666, + 726, + 194, + 80 + ], + "fill": "#9d8b77", + "border": "#f6f5f5", + "radius": 26 + }, + { + "bounds": [ + 203, + 11, + 266, + 37 + ], + "fill": "#f6f7f7", + "border": "#f5f6f6", + "radius": 1 + }, + { + "bounds": [ + 666, + 726, + 104, + 80 + ], + "fill": "#a3927d", + "border": "#f7f7f6", + "radius": 26 + }, + { + "bounds": [ + 15, + 11, + 186, + 39 + ], + "fill": "#fcfcfd", + "border": "#fdfdfd", + "radius": 1 + }, + { + "bounds": [ + 386, + 804, + 178, + 20 + ], + "fill": "#fcfcfc", + "border": "#fbfcfc", + "radius": 0 + }, + { + "bounds": [ + 386, + 782, + 178, + 20 + ], + "fill": "#fcfcfc", + "border": "#f7f8fa", + "radius": 0 + }, + { + "bounds": [ + 478, + 11, + 81, + 37 + ], + "fill": "#f6f7f8", + "border": "#f5f6f7", + "radius": 1 + }, + { + "bounds": [ + 745, + 84, + 46, + 54 + ], + "fill": "#f6f7f9", + "border": "#f7f8fc", + "radius": 0 + }, + { + "bounds": [ + 749, + 88, + 38, + 46 + ], + "fill": "#f6f7f9", + "border": "#f6f8fb", + "radius": 1 + } + ], + "dividers": [ + { + "axis": "h", + "at": 48, + "from": 3, + "to": 1533, + "color": "#d4d7db" + }, + { + "axis": "h", + "at": 214, + "from": 587, + "to": 895, + "color": "#3878cc" + }, + { + "axis": "h", + "at": 215, + "from": 584, + "to": 894, + "color": "#0253cc" + }, + { + "axis": "h", + "at": 262, + "from": 586, + "to": 896, + "color": "#064ebf" + }, + { + "axis": "h", + "at": 263, + "from": 588, + "to": 894, + "color": "#d3e0f2" + }, + { + "axis": "h", + "at": 979, + "from": 1, + "to": 1535, + "color": "#ecedee" + }, + { + "axis": "h", + "at": 980, + "from": 1, + "to": 1536, + "color": "#d3d5d8" + }, + { + "axis": "h", + "at": 999, + "from": 247, + "to": 1274, + "color": "#f1f1f2" + }, + { + "axis": "h", + "at": 1000, + "from": 164, + "to": 1274, + "color": "#d9d9db" + }, + { + "axis": "h", + "at": 1004, + "from": 242, + "to": 1274, + "color": "#d9dadb" + }, + { + "axis": "h", + "at": 1005, + "from": 243, + "to": 1274, + "color": "#f0f0f1" + }, + { + "axis": "v", + "at": 579, + "from": 353, + "to": 541, + "color": "#d3d4d8" + }, + { + "axis": "v", + "at": 606, + "from": 338, + "to": 623, + "color": "#cbced3" + }, + { + "axis": "v", + "at": 607, + "from": 338, + "to": 621, + "color": "#e8e9eb" + }, + { + "axis": "v", + "at": 633, + "from": 339, + "to": 621, + "color": "#edeef0" + }, + { + "axis": "v", + "at": 634, + "from": 338, + "to": 621, + "color": "#d1d3d7" + }, + { + "axis": "v", + "at": 660, + "from": 354, + "to": 540, + "color": "#e9ebed" + }, + { + "axis": "v", + "at": 661, + "from": 354, + "to": 541, + "color": "#d9dbdd" + }, + { + "axis": "v", + "at": 896, + "from": 645, + "to": 911, + "color": "#d3d5d9" + }, + { + "axis": "v", + "at": 897, + "from": 337, + "to": 623, + "color": "#c8cacf" + }, + { + "axis": "v", + "at": 898, + "from": 339, + "to": 622, + "color": "#e5e7e9" + }, + { + "axis": "v", + "at": 923, + "from": 337, + "to": 540, + "color": "#c9cbd0" + }, + { + "axis": "v", + "at": 924, + "from": 336, + "to": 540, + "color": "#f1f1f3" + }, + { + "axis": "v", + "at": 1185, + "from": 336, + "to": 540, + "color": "#f4f4f5" + }, + { + "axis": "v", + "at": 1186, + "from": 337, + "to": 623, + "color": "#c2c4ca" + }, + { + "axis": "v", + "at": 1187, + "from": 339, + "to": 621, + "color": "#ecedef" + } + ], + "limits": [ + "Evidence for review: roles and widgets are assigned in the semantic map, not here." + ] +} diff --git a/apps/pdftools/design/source/01-home.png b/apps/pdftools/design/source/01-home.png new file mode 100644 index 00000000..2ddbcafb Binary files /dev/null and b/apps/pdftools/design/source/01-home.png differ diff --git a/apps/pdftools/design/source/01-home.prompt.txt b/apps/pdftools/design/source/01-home.prompt.txt new file mode 100644 index 00000000..bbacaa4b --- /dev/null +++ b/apps/pdftools/design/source/01-home.prompt.txt @@ -0,0 +1,72 @@ +Create ONE original image: a single, complete desktop application screen for +"PDF Tools", a document app inside the OctoSense desktop. + +Requested image size: 1536 × 1024 pixels, landscape. Requested quality: high. +Show only the application's own window content, edge to edge, flat and +straight on. No operating-system menu bar, no dock, no window title bar or +traffic lights, no device frame, no perspective, no drop shadow around the +screen, no captions or annotations outside the interface. + +Visual language: an original, precise desktop document tool. The document is +the brightest thing on screen; the chrome is quiet, thin and well aligned. +Palette: a cool light grey desk behind the pages, white pages with a soft +shadow, graphite text, mid-grey secondary text, thin 1-pixel dividers in pale +grey, and ONE accent colour, a confident medium blue, used only for the +current mode, the selected item, focus and primary buttons. Yellow marks +highlighted text and search matches. Green means saved or done, red means an +error, both used sparingly. Corner radius about 8 px on panels, buttons and +fields; pages have square corners. Icons are simple outline glyphs with one +consistent stroke weight. Typography: a neutral sans-serif for the interface, +small and crisp (about 13–14 px), medium weight for panel titles; document +pages use a classic serif for body text and a sans-serif for their headings. +Do not imitate Adobe Acrobat's layout, icons, colours or branding, or any other +product's. + +Window layout, the same in every screen: +- Top row: document tabs. Each tab shows a small PDF glyph, the file name and + a close cross; the active tab is white, the others grey; a "+ Open" button + follows the last tab. +- Second row, the mode bar: the modes "Read", "Comment", "Fill & Sign", + "Pages", "Combine", "Edit" as text tabs, the current one in the accent + colour with a short underline; on the right a search field with a magnifier + and the placeholder "Search in document", then icon buttons for Undo and Redo, + and a "Save" button. +- Left: a narrow icon rail with four icons and tooltips' labels shown under + them in tiny text: "Pages", "Outline", "Comments", "Search". The selected + icon has a tinted square behind it. Next to the rail, an open panel about + 240 px wide when the screen calls for it. +- Centre: the canvas, a light grey desk with the document's pages stacked + vertically, centred, one page fully visible and the next starting below. +- Bottom centre of the canvas: a floating rounded pill with "‹", "3 / 24", + "›", a divider, "−", "100%", "+", and two small icons for "Fit width" and + "Fit page". +- Right: a panel about 300 px wide, only in screens that call for it. +- Bottom edge: a thin status line with "Saved" (with a small green dot) on the + left and "14.6 of 64 MB used" on the right. + +Use this exact fixture wherever it appears: +- Documents in the library: "Q3 2026 Board Report.pdf" (24 pages, 2.8 MB, + opened today), "Riverside Lease 2026.pdf" (12 pages, 1.1 MB, yesterday), + "Field Guide to Garden Birds.pdf" (7 pages, 16 KB, 3 Oct), "Invoice + INV-2041.pdf" (1 page, 84 KB, 1 Oct), "Site Survey Photos.pdf" (18 pages, + 9.6 MB, 28 Sep). +- The board report belongs to the fictional "Northwind Cooperative". Its + sections: Summary, Revenue, Operations, Outlook, Appendix. Page 3 is + "Revenue" with a short paragraph that includes the sentence "Revenue grew + 12% year over year, led by the Northwest region." and a simple bar chart + "Revenue by region, Q3 2026" with bars for Northwest, Coast, Valley, Metro. +- People: Maya Chen (the user), Jun Park and Ana Ruiz (reviewers). + +Render every piece of interface text and every control sharply enough to be +read and measured later. Labels, buttons, fields and panels will be rebuilt as +native components from this image; page contents are sample document art. + +This screen: the Home tab is active (the first tab shows a house glyph and +the word "Home"; a second tab "Q3 2026 Board Report.pdf" is open behind it). +No mode bar, no rail, no canvas pill on this screen. Instead, a centred +content area: the title "PDF Tools", then a wide primary button "Open a PDF +from this device" with the caption "Up to 64 MB" beside it. Below, the heading +"Recent" and a grid of five document cards in two rows, one per fixture +document, each with its first page as a thumbnail, the file name, "N pages · +size" and when it was opened. The status line shows only "14.6 of 64 MB +used" with a thin storage bar. \ No newline at end of file diff --git a/apps/pdftools/design/source/02-reading.measured.json b/apps/pdftools/design/source/02-reading.measured.json new file mode 100644 index 00000000..92d163e4 --- /dev/null +++ b/apps/pdftools/design/source/02-reading.measured.json @@ -0,0 +1,1151 @@ +{ + "schema_version": 1, + "image": "02-reading.png", + "image_sha256": "a1b56c1e0f54c0b0db9a5d1f4e465041b6d67a44efa36180f7da1d57f79c0ef2", + "script_sha256": "ea860507e32482b955d3ac577ab3cf9fc559f59ecc916c89175293932b54e0b7", + "size": [ + 1536, + 1024 + ], + "ocr_engine": "Apple Vision VNRecognizeTextRequest accurate, en-US (App Flow ocr.swift)", + "text": [ + { + "text": "Read", + "bounds": [ + 31.3, + 73.6, + 35.7, + 13.4 + ], + "confidence": 1, + "ink": "#3c7ae8", + "ground": "#f2f6fd" + }, + { + "text": "Pages", + "bounds": [ + 22.3, + 171.8, + 38.0, + 15.6 + ], + "confidence": 1, + "ink": "#5973a6", + "ground": "#e7f0fb" + }, + { + "text": "Outline", + "bounds": [ + 20.1, + 258.8, + 42.4, + 13.4 + ], + "confidence": 1, + "ink": "#6c6d6e", + "ground": "#f6f6f6" + }, + { + "text": "Comments", + "bounds": [ + 13.4, + 345.8, + 55.8, + 11.2 + ], + "confidence": 1, + "ink": "#808082", + "ground": "#f7f7f7" + }, + { + "text": "Search", + "bounds": [ + 20.1, + 428.3, + 42.4, + 11.2 + ], + "confidence": 1, + "ink": "#6c6d6f", + "ground": "#f4f4f4" + }, + { + "text": "Q3 2026 Board Report.pdf", + "bounds": [ + 58.0, + 24.5, + 180.8, + 15.6 + ], + "confidence": 1, + "ink": "#414142", + "ground": "#f4f4f4" + }, + { + "text": "Riverside Lease 2026.pdf x", + "bounds": [ + 308.1, + 24.5, + 189.8, + 15.6 + ], + "confidence": 1, + "ink": "#898a8c", + "ground": "#f3f3f3" + }, + { + "text": "Field Guide to Garden Birds.pdf x", + "bounds": [ + 551.4, + 24.5, + 218.8, + 15.6 + ], + "confidence": 1, + "ink": "#898a8b", + "ground": "#f2f2f3" + }, + { + "text": "Invoice INV-2041.pdf", + "bounds": [ + 821.6, + 24.5, + 134.0, + 15.6 + ], + "confidence": 1, + "ink": "#858688", + "ground": "#f2f2f2" + }, + { + "text": "Site Survey Photos.pdf", + "bounds": [ + 1035.9, + 24.5, + 147.3, + 15.6 + ], + "confidence": 1, + "ink": "#8a8b8d", + "ground": "#f2f3f3" + }, + { + "text": "Comment", + "bounds": [ + 100.4, + 73.4, + 69.3, + 13.7 + ], + "confidence": 1, + "ink": "#636465", + "ground": "#f7f7f7" + }, + { + "text": "Fill & Sign", + "bounds": [ + 200.9, + 73.3, + 69.3, + 16.2 + ], + "confidence": 1, + "ink": "#5c5d5e", + "ground": "#f6f6f7" + }, + { + "text": "Pages", + "bounds": [ + 301.4, + 73.6, + 44.7, + 15.6 + ], + "confidence": 1, + "ink": "#666667", + "ground": "#f8f8f8" + }, + { + "text": "Combine", + "bounds": [ + 379.4, + 73.0, + 62.8, + 14.7 + ], + "confidence": 1, + "ink": "#636464", + "ground": "#f6f6f7" + }, + { + "text": "Edit", + "bounds": [ + 477.8, + 73.6, + 29.0, + 13.4 + ], + "confidence": 1, + "ink": "#6d6d6e", + "ground": "#f7f7f7" + }, + { + "text": "Q Search in document", + "bounds": [ + 944.4, + 71.0, + 158.5, + 16.0 + ], + "confidence": 1, + "ink": "#9c9d9f", + "ground": "#fafafa" + }, + { + "text": "Pages", + "bounds": [ + 100.4, + 124.7, + 47.1, + 18.3 + ], + "confidence": 1, + "ink": "#38393b", + "ground": "#f3f3f3" + }, + { + "text": "NORTHWING", + "bounds": [ + 156.3, + 171.8, + 58.0, + 6.7 + ], + "confidence": 1, + "ink": "#6886aa", + "ground": "#f1f4f7" + }, + { + "text": "hthewtooath", + "bounds": [ + 149.6, + 198.6, + 67.0, + 4.5 + ], + "confidence": 0.3, + "ink": "#97989a", + "ground": "#f0f1f1" + }, + { + "text": "03 2025 Sourd Roport", + "bounds": [ + 149.6, + 203.0, + 71.4, + 11.2 + ], + "confidence": 1, + "ink": "#959597", + "ground": "#f7f7f7" + }, + { + "text": "Contorts", + "bounds": [ + 156.3, + 312.3, + 22.3, + 11.2 + ], + "confidence": 1, + "ink": "#8399be", + "ground": "#f8f9fb" + }, + { + "text": "Q3 2026 Board Report", + "bounds": [ + 984.6, + 147.0, + 116.1, + 14 + ], + "confidence": 1, + "ink": "#a0a0a2", + "ground": "#f8f8f9" + }, + { + "text": "+ Open", + "bounds": [ + 1444.5, + 22.0, + 58.0, + 16 + ], + "confidence": 1, + "ink": "#525254", + "ground": "#f5f5f5" + }, + { + "text": "Save", + "bounds": [ + 1446.7, + 75.9, + 33.5, + 11.2 + ], + "confidence": 1, + "ink": "#b3d5f6", + "ground": "#156edf" + }, + { + "text": "Lin.", + "bounds": [ + 147.3, + 508.7, + 73.7, + 31.2 + ], + "confidence": 0.3, + "ink": "#1064cc", + "ground": "#f5f7f9" + }, + { + "text": "3", + "bounds": [ + 234.4, + 539.9, + 17.9, + 17.8 + ], + "confidence": 1, + "ink": "#296fe7", + "ground": "#f4f8fd" + }, + { + "text": "Northwind Cooperative", + "bounds": [ + 616.2, + 147.0, + 127.3, + 14 + ], + "confidence": 1, + "ink": "#4668a6", + "ground": "#f4f6fa" + }, + { + "text": "Revenue", + "bounds": [ + 616.2, + 189.6, + 147.3, + 31.2 + ], + "confidence": 1, + "ink": "#174790", + "ground": "#f4f6f9" + }, + { + "text": "Revenue for the third quarter of 2026 was $28.7 million, up from", + "bounds": [ + 618.4, + 265.0, + 473.3, + 18.3 + ], + "confidence": 1, + "ink": "#444545", + "ground": "#f6f5f6" + }, + { + "text": "$25.6 million in Q3 2025. Revenue grew 12% year over year, led", + "bounds": [ + 616.2, + 290.0, + 466.6, + 18 + ], + "confidence": 1, + "ink": "#f8f7f3", + "ground": "#c8b24e" + }, + { + "text": "by the Northwest region.", + "bounds": [ + 616.2, + 314.6, + 183.1, + 17.8 + ], + "confidence": 1, + "ink": "#524519", + "ground": "#f6df6c" + }, + { + "text": "Strong customer demand and disciplined pricing supported", + "bounds": [ + 616.2, + 350.0, + 448.7, + 18.1 + ], + "confidence": 1, + "ink": "#4d4d4e", + "ground": "#f6f6f6" + }, + { + "text": "broad-based growth across all regions.", + "bounds": [ + 616.2, + 374.8, + 285.8, + 18.2 + ], + "confidence": 1, + "ink": "#4c4c4c", + "ground": "#f7f7f7" + }, + { + "text": "Revenue by region, Q3 2026", + "bounds": [ + 618.4, + 415.0, + 234.4, + 20.1 + ], + "confidence": 1, + "ink": "#2a4d91", + "ground": "#f3f5f9" + }, + { + "text": "Millions of USD", + "bounds": [ + 616.2, + 439.0, + 87.1, + 12 + ], + "confidence": 1, + "ink": "#a2a3a5", + "ground": "#f9f9f9" + }, + { + "text": "14", + "bounds": [ + 616.2, + 459.6, + 17.9, + 13.4 + ], + "confidence": 1, + "ink": "#7c7c7d", + "ground": "#f9f9fa" + }, + { + "text": "12.4", + "bounds": [ + 685.4, + 470.7, + 29.0, + 13.4 + ], + "confidence": 1, + "ink": "#47484a", + "ground": "#f5f5f5" + }, + { + "text": "12", + "bounds": [ + 616.2, + 488.6, + 17.9, + 13.4 + ], + "confidence": 1, + "ink": "#797a7c", + "ground": "#fafafa" + }, + { + "text": "10", + "bounds": [ + 614.0, + 515.3, + 20.1, + 15.6 + ], + "confidence": 1, + "ink": "#68696a", + "ground": "#f8f8f8" + }, + { + "text": "8", + "bounds": [ + 622.9, + 544.3, + 11.2, + 13.4 + ], + "confidence": 1, + "ink": "#6c6e6f", + "ground": "#f7f7f7" + }, + { + "text": "7.1", + "bounds": [ + 799.3, + 542.1, + 20.1, + 15.6 + ], + "confidence": 1, + "ink": "#585859", + "ground": "#f8f8f8" + }, + { + "text": "5.6", + "bounds": [ + 913.1, + 564.4, + 24.6, + 15.6 + ], + "confidence": 1, + "ink": "#4c4c4f", + "ground": "#f7f7f7" + }, + { + "text": "3.6", + "bounds": [ + 1027.0, + 593.4, + 24.6, + 13.4 + ], + "confidence": 1, + "ink": "#4f5051", + "ground": "#f7f7f7" + }, + { + "text": "Northwest", + "bounds": [ + 667.5, + 675.7, + 67.1, + 14.0 + ], + "confidence": 1, + "ink": "#4e4f51", + "ground": "#f2f2f2" + }, + { + "text": "Coast", + "bounds": [ + 792.6, + 676.0, + 38.0, + 15.6 + ], + "confidence": 1, + "ink": "#5f6163", + "ground": "#f7f7f7" + }, + { + "text": "Valley", + "bounds": [ + 904.9, + 675.3, + 42.9, + 19.6 + ], + "confidence": 1, + "ink": "#58595b", + "ground": "#f8f8f8" + }, + { + "text": "Metro", + "bounds": [ + 1020.3, + 676.0, + 38.0, + 13.4 + ], + "confidence": 1, + "ink": "#535456", + "ground": "#f2f2f3" + }, + { + "text": "3", + "bounds": [ + 1087.3, + 705.0, + 11.2, + 13.4 + ], + "confidence": 1, + "ink": "#606264", + "ground": "#f8f7f8" + }, + { + "text": "Q3 2026 Board Report", + "bounds": [ + 984.6, + 789.8, + 116.1, + 13.4 + ], + "confidence": 1, + "ink": "#a5a6a8", + "ground": "#f9f9fa" + }, + { + "text": "5", + "bounds": [ + 236.7, + 807.6, + 11.2, + 15.6 + ], + "confidence": 1, + "ink": "#757679", + "ground": "#f7f8f8" + }, + { + "text": "Northwind Cooperative", + "bounds": [ + 616.2, + 789.8, + 127.3, + 13.4 + ], + "confidence": 1, + "ink": "#4669a8", + "ground": "#f2f5f9" + }, + { + "text": "Operations", + "bounds": [ + 616.0, + 829.0, + 192.4, + 39.7 + ], + "confidence": 1, + "ink": "#18478f", + "ground": "#f7f8fb" + }, + { + "text": "Operational performance remained strong in Q3 2026. We", + "bounds": [ + 618.4, + 896.8, + 392.9, + 18.2 + ], + "confidence": 1, + "ink": "#555656", + "ground": "#f5f5f5" + }, + { + "text": "3 / 24", + "bounds": [ + 656.4, + 932.5, + 51.3, + 15.6 + ], + "confidence": 1, + "ink": "#626364", + "ground": "#f9f9f9" + }, + { + "text": "-", + "bounds": [ + 808.2, + 934.8, + 22.3, + 11.2 + ], + "confidence": 1, + "ink": "#777779", + "ground": "#fdfdfd" + }, + { + "text": "100%", + "bounds": [ + 868.5, + 932.5, + 40.2, + 13.4 + ], + "confidence": 1, + "ink": "#6a6b6c", + "ground": "#f8f8f8" + }, + { + "text": "+", + "bounds": [ + 946.6, + 930.3, + 17.9, + 20.1 + ], + "confidence": 1, + "ink": "#767778", + "ground": "#fdfdfd" + }, + { + "text": "Saved", + "bounds": [ + 55.8, + 992.8, + 40.2, + 13.4 + ], + "confidence": 1, + "ink": "#79797b", + "ground": "#f8f8f8" + }, + { + "text": "14.6 of 64 MB used", + "bounds": [ + 1390.9, + 992.8, + 116.1, + 13.4 + ], + "confidence": 1, + "ink": "#6d6d6e", + "ground": "#f6f6f6" + } + ], + "surfaces": [ + { + "bounds": [ + 288, + 107, + 1247, + 873 + ], + "fill": "#eceded", + "border": "#eeefef", + "radius": 3 + }, + { + "bounds": [ + 561, + 120, + 588, + 633 + ], + "fill": "#fefefe", + "border": "#e9eaeb", + "radius": 4 + }, + { + "bounds": [ + 85, + 109, + 204, + 869 + ], + "fill": "#fefefe", + "border": "#fdfdfe", + "radius": 0 + }, + { + "bounds": [ + 564, + 766, + 581, + 212 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1, + 1, + 1535, + 53 + ], + "fill": "#f4f4f5", + "border": "#f2f3f4", + "radius": 3 + }, + { + "bounds": [ + 137, + 430, + 100, + 132 + ], + "fill": "#fdfdfd", + "border": "#f8fafd", + "radius": 0 + }, + { + "bounds": [ + 932, + 62, + 344, + 35 + ], + "fill": "#fefefe", + "border": "#fdfdfe", + "radius": 0 + }, + { + "bounds": [ + 141, + 294, + 91, + 126 + ], + "fill": "#fefefe", + "border": "#fdfdfe", + "radius": 0 + }, + { + "bounds": [ + 141, + 152, + 91, + 125 + ], + "fill": "#fdfdfd", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 141, + 574, + 90, + 122 + ], + "fill": "#fcfcfd", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 141, + 710, + 90, + 118 + ], + "fill": "#fefdfe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 144, + 437, + 85, + 118 + ], + "fill": "#fdfdfd", + "border": "#fafbfc", + "radius": 0 + }, + { + "bounds": [ + 141, + 842, + 90, + 108 + ], + "fill": "#fdfdfd", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 935, + 66, + 337, + 28 + ], + "fill": "#fefefe", + "border": "#fcfdfd", + "radius": 0 + }, + { + "bounds": [ + 676, + 490, + 51, + 178 + ], + "fill": "#146ad9", + "border": "#4e72db", + "radius": 4 + }, + { + "bounds": [ + 142, + 156, + 86, + 99 + ], + "fill": "#fefefe", + "border": "#e0e1f0", + "radius": 3 + }, + { + "bounds": [ + 7, + 120, + 69, + 77 + ], + "fill": "#e9f2fe", + "border": "#fefdfe", + "radius": 4 + }, + { + "bounds": [ + 787, + 564, + 49, + 104 + ], + "fill": "#156ad9", + "border": "#5476de", + "radius": 1 + }, + { + "bounds": [ + 148, + 868, + 76, + 57 + ], + "fill": "#f5f5f5", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1389, + 64, + 128, + 33 + ], + "fill": "#116cdc", + "border": "#f7fafd", + "radius": 4 + }, + { + "bounds": [ + 903, + 586, + 49, + 82 + ], + "fill": "#166bda", + "border": "#4a78de", + "radius": 4 + }, + { + "bounds": [ + 1429, + 11, + 93, + 37 + ], + "fill": "#f8f8f8", + "border": "#f4f4f4", + "radius": 0 + }, + { + "bounds": [ + 1431, + 13, + 88, + 32 + ], + "fill": "#f8f8f8", + "border": "#ececec", + "radius": 0 + }, + { + "bounds": [ + 1017, + 615, + 47, + 53 + ], + "fill": "#166cd9", + "border": "#3274d9", + "radius": 0 + }, + { + "bounds": [ + 153, + 624, + 66, + 36 + ], + "fill": "#86b2d3", + "border": "#f6f6f6", + "radius": 14 + }, + { + "bounds": [ + 154, + 625, + 64, + 26 + ], + "fill": "#94beda", + "border": "#417bba", + "radius": 8 + }, + { + "bounds": [ + 151, + 603, + 69, + 18 + ], + "fill": "#f6f7f7", + "border": "#fdfdfd", + "radius": 0 + } + ], + "dividers": [ + { + "axis": "h", + "at": 52, + "from": 268, + "to": 1536, + "color": "#e1e2e3" + }, + { + "axis": "h", + "at": 94, + "from": 935, + "to": 1270, + "color": "#dcdcde" + }, + { + "axis": "h", + "at": 106, + "from": 288, + "to": 1536, + "color": "#ecedee" + }, + { + "axis": "h", + "at": 107, + "from": 288, + "to": 1536, + "color": "#dedfe0" + }, + { + "axis": "h", + "at": 237, + "from": 618, + "to": 1099, + "color": "#e6e7e8" + }, + { + "axis": "h", + "at": 238, + "from": 618, + "to": 1100, + "color": "#9c9ea4" + }, + { + "axis": "h", + "at": 667, + "from": 639, + "to": 1099, + "color": "#a4a6ad" + }, + { + "axis": "h", + "at": 749, + "from": 565, + "to": 1144, + "color": "#c1c2c3" + }, + { + "axis": "h", + "at": 750, + "from": 565, + "to": 1144, + "color": "#d0d0d2" + }, + { + "axis": "h", + "at": 879, + "from": 618, + "to": 1098, + "color": "#e2e3e5" + }, + { + "axis": "h", + "at": 880, + "from": 618, + "to": 1098, + "color": "#adafb6" + }, + { + "axis": "h", + "at": 965, + "from": 589, + "to": 1091, + "color": "#ededee" + }, + { + "axis": "h", + "at": 966, + "from": 589, + "to": 1091, + "color": "#e4e5e6" + }, + { + "axis": "h", + "at": 978, + "from": 564, + "to": 1146, + "color": "#e1e1e3" + }, + { + "axis": "v", + "at": 83, + "from": 107, + "to": 978, + "color": "#e3e3e3" + }, + { + "axis": "v", + "at": 288, + "from": 107, + "to": 978, + "color": "#ececec" + }, + { + "axis": "v", + "at": 289, + "from": 107, + "to": 978, + "color": "#dedfe0" + }, + { + "axis": "v", + "at": 642, + "from": 466, + "to": 668, + "color": "#b6b6b6" + } + ], + "limits": [ + "Evidence for review: roles and widgets are assigned in the semantic map, not here." + ] +} diff --git a/apps/pdftools/design/source/02-reading.png b/apps/pdftools/design/source/02-reading.png new file mode 100644 index 00000000..347aeb30 Binary files /dev/null and b/apps/pdftools/design/source/02-reading.png differ diff --git a/apps/pdftools/design/source/02-reading.prompt.txt b/apps/pdftools/design/source/02-reading.prompt.txt new file mode 100644 index 00000000..35db2859 --- /dev/null +++ b/apps/pdftools/design/source/02-reading.prompt.txt @@ -0,0 +1,69 @@ +Create ONE original image: a single, complete desktop application screen for +"PDF Tools", a document app inside the OctoSense desktop. + +Requested image size: 1536 × 1024 pixels, landscape. Requested quality: high. +Show only the application's own window content, edge to edge, flat and +straight on. No operating-system menu bar, no dock, no window title bar or +traffic lights, no device frame, no perspective, no drop shadow around the +screen, no captions or annotations outside the interface. + +Visual language: an original, precise desktop document tool. The document is +the brightest thing on screen; the chrome is quiet, thin and well aligned. +Palette: a cool light grey desk behind the pages, white pages with a soft +shadow, graphite text, mid-grey secondary text, thin 1-pixel dividers in pale +grey, and ONE accent colour, a confident medium blue, used only for the +current mode, the selected item, focus and primary buttons. Yellow marks +highlighted text and search matches. Green means saved or done, red means an +error, both used sparingly. Corner radius about 8 px on panels, buttons and +fields; pages have square corners. Icons are simple outline glyphs with one +consistent stroke weight. Typography: a neutral sans-serif for the interface, +small and crisp (about 13–14 px), medium weight for panel titles; document +pages use a classic serif for body text and a sans-serif for their headings. +Do not imitate Adobe Acrobat's layout, icons, colours or branding, or any other +product's. + +Window layout, the same in every screen: +- Top row: document tabs. Each tab shows a small PDF glyph, the file name and + a close cross; the active tab is white, the others grey; a "+ Open" button + follows the last tab. +- Second row, the mode bar: the modes "Read", "Comment", "Fill & Sign", + "Pages", "Combine", "Edit" as text tabs, the current one in the accent + colour with a short underline; on the right a search field with a magnifier + and the placeholder "Search in document", then icon buttons for Undo and Redo, + and a "Save" button. +- Left: a narrow icon rail with four icons and tooltips' labels shown under + them in tiny text: "Pages", "Outline", "Comments", "Search". The selected + icon has a tinted square behind it. Next to the rail, an open panel about + 240 px wide when the screen calls for it. +- Centre: the canvas, a light grey desk with the document's pages stacked + vertically, centred, one page fully visible and the next starting below. +- Bottom centre of the canvas: a floating rounded pill with "‹", "3 / 24", + "›", a divider, "−", "100%", "+", and two small icons for "Fit width" and + "Fit page". +- Right: a panel about 300 px wide, only in screens that call for it. +- Bottom edge: a thin status line with "Saved" (with a small green dot) on the + left and "14.6 of 64 MB used" on the right. + +Use this exact fixture wherever it appears: +- Documents in the library: "Q3 2026 Board Report.pdf" (24 pages, 2.8 MB, + opened today), "Riverside Lease 2026.pdf" (12 pages, 1.1 MB, yesterday), + "Field Guide to Garden Birds.pdf" (7 pages, 16 KB, 3 Oct), "Invoice + INV-2041.pdf" (1 page, 84 KB, 1 Oct), "Site Survey Photos.pdf" (18 pages, + 9.6 MB, 28 Sep). +- The board report belongs to the fictional "Northwind Cooperative". Its + sections: Summary, Revenue, Operations, Outlook, Appendix. Page 3 is + "Revenue" with a short paragraph that includes the sentence "Revenue grew + 12% year over year, led by the Northwest region." and a simple bar chart + "Revenue by region, Q3 2026" with bars for Northwest, Coast, Valley, Metro. +- People: Maya Chen (the user), Jun Park and Ana Ruiz (reviewers). + +Render every piece of interface text and every control sharply enough to be +read and measured later. Labels, buttons, fields and panels will be rebuilt as +native components from this image; page contents are sample document art. + +This screen: mode "Read". Tab "Q3 2026 Board Report.pdf" active. The rail's +"Pages" icon is selected and its panel shows the title "Pages" and a vertical +column of page thumbnails labelled 1 to 6, thumbnail 3 outlined in the accent +colour. The canvas shows page 3, "Revenue", at 100% with its paragraph and the +bar chart, and the top of page 4 below it. The canvas pill reads "3 / 24" and +"100%". No right panel. \ No newline at end of file diff --git a/apps/pdftools/design/source/03-find.measured.json b/apps/pdftools/design/source/03-find.measured.json new file mode 100644 index 00000000..62ccbc22 --- /dev/null +++ b/apps/pdftools/design/source/03-find.measured.json @@ -0,0 +1,2189 @@ +{ + "schema_version": 1, + "image": "03-find.png", + "image_sha256": "aec86c4a1cd6531b029285d31084665b45d4cdf2af465a12c795d152575f7d69", + "script_sha256": "ea860507e32482b955d3ac577ab3cf9fc559f59ecc916c89175293932b54e0b7", + "size": [ + 1536, + 1024 + ], + "ocr_engine": "Apple Vision VNRecognizeTextRequest accurate, en-US (App Flow ocr.swift)", + "text": [ + { + "text": "Read", + "bounds": [ + 26.8, + 71.4, + 35.7, + 13.4 + ], + "confidence": 1, + "ink": "#3e7ce0", + "ground": "#f0f5fc" + }, + { + "text": "Pages", + "bounds": [ + 33.5, + 158.4, + 33.5, + 13.4 + ], + "confidence": 1, + "ink": "#989ba1", + "ground": "#f7f8f8" + }, + { + "text": ":=", + "bounds": [ + 38.0, + 214.2, + 22.3, + 17.8 + ], + "confidence": 1, + "ink": "#7c8086", + "ground": "#f8f9f9" + }, + { + "text": "Outline", + "bounds": [ + 31.3, + 240.9, + 40.2, + 13.4 + ], + "confidence": 1, + "ink": "#9ea0a7", + "ground": "#f9f9f9" + }, + { + "text": "Comments", + "bounds": [ + 22.3, + 323.5, + 55.8, + 11.2 + ], + "confidence": 1, + "ink": "#a0a2a7", + "ground": "#f8f8f9" + }, + { + "text": "Search", + "bounds": [ + 31.3, + 403.8, + 40.2, + 13.4 + ], + "confidence": 1, + "ink": "#508ae0", + "ground": "#e6f0fb" + }, + { + "text": "Q3 2026 Board Report.pdf x", + "bounds": [ + 60.3, + 20.0, + 203.2, + 16 + ], + "confidence": 1, + "ink": "#5a5c60", + "ground": "#f7f8f8" + }, + { + "text": "11.", + "bounds": [ + 292.5, + 20.1, + 11.2, + 13.4 + ], + "confidence": 0.5, + "ink": "#ae6260", + "ground": "#f0dfde" + }, + { + "text": "Riverside Lease 2026.pdf", + "bounds": [ + 319.3, + 22.0, + 158.5, + 14 + ], + "confidence": 1, + "ink": "#686a6f", + "ground": "#f2f3f4" + }, + { + "text": "Comment", + "bounds": [ + 107.2, + 71.4, + 67.0, + 13.4 + ], + "confidence": 1, + "ink": "#535559", + "ground": "#f5f5f6" + }, + { + "text": "Fill & Sign", + "bounds": [ + 216.5, + 71.1, + 69.3, + 16.1 + ], + "confidence": 1, + "ink": "#515355", + "ground": "#f5f6f6" + }, + { + "text": "Pages", + "bounds": [ + 330.4, + 71.4, + 42.4, + 15.6 + ], + "confidence": 1, + "ink": "#4f5055", + "ground": "#f5f5f5" + }, + { + "text": "Combine", + "bounds": [ + 417.5, + 71.4, + 60.3, + 13.4 + ], + "confidence": 1, + "ink": "#4f5155", + "ground": "#f3f3f4" + }, + { + "text": "12 matches on 7 pages", + "bounds": [ + 113.9, + 122.7, + 140.7, + 15.6 + ], + "confidence": 1, + "ink": "#888b91", + "ground": "#f8f9f9" + }, + { + "text": "Page 1", + "bounds": [ + 113.9, + 167.0, + 44.7, + 16 + ], + "confidence": 1, + "ink": "#444649", + "ground": "#f6f6f6" + }, + { + "text": "1 match", + "bounds": [ + 314.8, + 169.6, + 46.9, + 11.4 + ], + "confidence": 1, + "ink": "#adafb5", + "ground": "#fafafa" + }, + { + "text": "... strong start to Q3. Total revenue", + "bounds": [ + 120.6, + 198.6, + 209.9, + 15.6 + ], + "confidence": 1, + "ink": "#727478", + "ground": "#f8f8f9" + }, + { + "text": "increased 12% year over year...", + "bounds": [ + 120.6, + 220.9, + 187.5, + 15.6 + ], + "confidence": 1, + "ink": "#8a8d92", + "ground": "#f8f8f9" + }, + { + "text": "Page 3", + "bounds": [ + 113.9, + 269.9, + 44.7, + 16.1 + ], + "confidence": 1, + "ink": "#484d56", + "ground": "#ecf0f6" + }, + { + "text": "Revenue grew 12% year over year,", + "bounds": [ + 116.1, + 298.9, + 207.6, + 16.1 + ], + "confidence": 1, + "ink": "#60656f", + "ground": "#ecf1f8" + }, + { + "text": "led by the Northwest region.", + "bounds": [ + 113.9, + 321.0, + 167.4, + 16.0 + ], + "confidence": 1, + "ink": "#6e747f", + "ground": "#ecf1f8" + }, + { + "text": "2 matches", + "bounds": [ + 303.6, + 269.7, + 55.9, + 13.9 + ], + "confidence": 1, + "ink": "#a4acba", + "ground": "#eef4fb" + }, + { + "text": "... continued strength in revenue", + "bounds": [ + 116.1, + 361.0, + 194.2, + 16.0 + ], + "confidence": 1, + "ink": "#585d67", + "ground": "#ebf0f7" + }, + { + "text": "across all regions...", + "bounds": [ + 116.1, + 383.7, + 116.1, + 15.6 + ], + "confidence": 1, + "ink": "#787e87", + "ground": "#eef3f9" + }, + { + "text": "Page 4", + "bounds": [ + 113.9, + 432.8, + 44.7, + 15.6 + ], + "confidence": 1, + "ink": "#515356", + "ground": "#f8f8f8" + }, + { + "text": "1 match i", + "bounds": [ + 314.8, + 435.0, + 47.0, + 11.2 + ], + "confidence": 1, + "ink": "#aaacb2", + "ground": "#f9f9fa" + }, + { + "text": "Operating expenses rose 4% while", + "bounds": [ + 116.1, + 464.0, + 203.2, + 18 + ], + "confidence": 1, + "ink": "#808288", + "ground": "#f8f8f9" + }, + { + "text": "revenue outpaced costs...", + "bounds": [ + 113.9, + 488.6, + 160.7, + 13.4 + ], + "confidence": 1, + "ink": "#707277", + "ground": "#f6f6f7" + }, + { + "text": "Page 9", + "bounds": [ + 116.1, + 537.7, + 42.4, + 15.6 + ], + "confidence": 1, + "ink": "#55565b", + "ground": "#f7f7f7" + }, + { + "text": "1 match i", + "bounds": [ + 314.8, + 539.9, + 47.0, + 11.2 + ], + "confidence": 1, + "ink": "#adb0b6", + "ground": "#fafbfb" + }, + { + "text": "...strategic investments to drive", + "bounds": [ + 116.1, + 571.0, + 187.5, + 16.0 + ], + "confidence": 1, + "ink": "#83858a", + "ground": "#f8f8f9" + }, + { + "text": "future revenue growth ...", + "bounds": [ + 116.1, + 591.0, + 149.6, + 18.0 + ], + "confidence": 1, + "ink": "#64666a", + "ground": "#f8f8f9" + }, + { + "text": "Page 15", + "bounds": [ + 113.9, + 642.5, + 51.3, + 15.6 + ], + "confidence": 1, + "ink": "#4e4f54", + "ground": "#f6f7f7" + }, + { + "text": "1 match i", + "bounds": [ + 314.8, + 644.7, + 47.0, + 11.3 + ], + "confidence": 1, + "ink": "#aaacb2", + "ground": "#f9f9fa" + }, + { + "text": "Appendix B: detailed revenue by", + "bounds": [ + 116.1, + 673.7, + 192.0, + 15.6 + ], + "confidence": 1, + "ink": "#76787c", + "ground": "#f8f8f8" + }, + { + "text": "region and product line.", + "bounds": [ + 113.9, + 696.0, + 138.4, + 16.0 + ], + "confidence": 1, + "ink": "#8c8e93", + "ground": "#f8f9f9" + }, + { + "text": "Page 16", + "bounds": [ + 116.1, + 745.0, + 49.1, + 16 + ], + "confidence": 1, + "ink": "#545559", + "ground": "#f7f7f8" + }, + { + "text": "1 match", + "bounds": [ + 317.0, + 745.0, + 44.7, + 14 + ], + "confidence": 1, + "ink": "#a9acb2", + "ground": "#f9fafa" + }, + { + "text": "Year-over-year revenue comparison", + "bounds": [ + 113.9, + 776.0, + 212.1, + 16 + ], + "confidence": 1, + "ink": "#6d6f74", + "ground": "#f7f7f8" + }, + { + "text": "shows consistent momentum ...", + "bounds": [ + 116.1, + 800.9, + 180.8, + 13.4 + ], + "confidence": 1, + "ink": "#8f9197", + "ground": "#f7f8f8" + }, + { + "text": "Page 21", + "bounds": [ + 113.9, + 847.8, + 51.3, + 15.6 + ], + "confidence": 1, + "ink": "#3a3c3f", + "ground": "#f3f4f4" + }, + { + "text": "1 match", + "bounds": [ + 314.8, + 850.0, + 46.9, + 11.2 + ], + "confidence": 1, + "ink": "#a9acb2", + "ground": "#f9fafa" + }, + { + "text": "Risks to revenue include market", + "bounds": [ + 116.1, + 879.0, + 192, + 16.0 + ], + "confidence": 1, + "ink": "#6e7075", + "ground": "#f7f7f8" + }, + { + "text": "volatility and supply constraints.", + "bounds": [ + 113.9, + 903.5, + 185.3, + 13.5 + ], + "confidence": 1, + "ink": "#8d8f94", + "ground": "#f6f7f8" + }, + { + "text": "Edit", + "bounds": [ + 522.4, + 71.4, + 29.0, + 13.4 + ], + "confidence": 1, + "ink": "#484a50", + "ground": "#f1f2f2" + }, + { + "text": "3", + "bounds": [ + 547.0, + 245.4, + 13.4, + 20.1 + ], + "confidence": 1, + "ink": "#2b448a", + "ground": "#f2f3f7" + }, + { + "text": "3", + "bounds": [ + 555.9, + 807.6, + 8.9, + 8.9 + ], + "confidence": 1, + "ink": "#56585d", + "ground": "#f7f7f7" + }, + { + "text": "4", + "bounds": [ + 544.7, + 912.5, + 17.9, + 22.3 + ], + "confidence": 1, + "ink": "#2f4585", + "ground": "#f7f7fa" + }, + { + "text": "Field Guide to Garden Birds.pdf", + "bounds": [ + 582.7, + 22.0, + 196.5, + 14 + ], + "confidence": 1, + "ink": "#686a6f", + "ground": "#f3f3f5" + }, + { + "text": "Invoice INV-2041.pdf", + "bounds": [ + 866.2, + 22.0, + 134.0, + 14 + ], + "confidence": 1, + "ink": "#6a6c71", + "ground": "#f3f4f5" + }, + { + "text": "Site Survey Photos.pdf", + "bounds": [ + 1111.8, + 22.0, + 140.7, + 14 + ], + "confidence": 1, + "ink": "#6a6b70", + "ground": "#f2f3f5" + }, + { + "text": "X", + "bounds": [ + 1288.2, + 22.3, + 13.4, + 11.2 + ], + "confidence": 1, + "ink": "#8a8d94", + "ground": "#f6f7f8" + }, + { + "text": "Q", + "bounds": [ + 986.8, + 69.2, + 20.1, + 20.1 + ], + "confidence": 1, + "ink": "#5c5e65", + "ground": "#f8f8f8" + }, + { + "text": "revenue", + "bounds": [ + 1018.0, + 73.1, + 56.0, + 12.1 + ], + "confidence": 1, + "ink": "#54555a", + "ground": "#f5f5f5" + }, + { + "text": "2 of 12", + "bounds": [ + 674.2, + 122.7, + 49.1, + 13.4 + ], + "confidence": 1, + "ink": "#595b5f", + "ground": "#f7f7f7" + }, + { + "text": "Done", + "bounds": [ + 857.3, + 122.7, + 35.7, + 13.4 + ], + "confidence": 1, + "ink": "#454649", + "ground": "#f3f3f4" + }, + { + "text": "Northwind Cooperative", + "bounds": [ + 582.7, + 191.9, + 98.2, + 13.4 + ], + "confidence": 1, + "ink": "#838488", + "ground": "#f7f7f8" + }, + { + "text": "Q3 2026 Board Report", + "bounds": [ + 942.1, + 191.9, + 96, + 13.4 + ], + "confidence": 1, + "ink": "#7e8084", + "ground": "#f7f7f7" + }, + { + "text": "Revenue", + "bounds": [ + 584.8, + 247.1, + 91.8, + 23.4 + ], + "confidence": 1, + "ink": "#2a4183", + "ground": "#f4f5f9" + }, + { + "text": "Revenue", + "bounds": [ + 587.2, + 290.0, + 58.0, + 13.4 + ], + "confidence": 1, + "ink": "#3f3e1a", + "ground": "#f3e38d" + }, + { + "text": "grew 12% year over year, led by the Northwest region.", + "bounds": [ + 643.0, + 290.0, + 312.6, + 16 + ], + "confidence": 1, + "ink": "#907343", + "ground": "#f6f1d0" + }, + { + "text": "This was driven by higher member demand, new account wins, and", + "bounds": [ + 587.2, + 316.8, + 372.8, + 15.6 + ], + "confidence": 1, + "ink": "#68696b", + "ground": "#f7f7f7" + }, + { + "text": "improved pricing. Continued strength in revenue across all regions", + "bounds": [ + 587.2, + 339.0, + 370.6, + 13.5 + ], + "confidence": 1, + "ink": "#5d5c57", + "ground": "#f2f0e4" + }, + { + "text": "reflects the cooperative's expanding market presence.", + "bounds": [ + 587.2, + 359.0, + 294.7, + 16.0 + ], + "confidence": 1, + "ink": "#656769", + "ground": "#f6f6f6" + }, + { + "text": "Revenue by region, Q3 2026", + "bounds": [ + 589.4, + 406.0, + 198.7, + 18 + ], + "confidence": 1, + "ink": "#3b3a30", + "ground": "#f4eecc" + }, + { + "text": "60", + "bounds": [ + 609.5, + 437.3, + 17.9, + 15.6 + ], + "confidence": 1, + "ink": "#626366", + "ground": "#fafafa" + }, + { + "text": "52", + "bounds": [ + 672.0, + 452.9, + 17.9, + 15.6 + ], + "confidence": 1, + "ink": "#55575b", + "ground": "#f8f8f9" + }, + { + "text": "millions)", + "bounds": [ + 589.4, + 477.4, + 13.4, + 60.2 + ], + "confidence": 1, + "ink": "#424346", + "ground": "#f2f2f2" + }, + { + "text": "Revenue (USD", + "bounds": [ + 589.4, + 535.4, + 13.4, + 93.7 + ], + "confidence": 1, + "ink": "#484a4d", + "ground": "#f3f4f4" + }, + { + "text": "40", + "bounds": [ + 611.7, + 508.7, + 15.6, + 11.2 + ], + "confidence": 1, + "ink": "#626368", + "ground": "#f6f6f7" + }, + { + "text": "30", + "bounds": [ + 614.0, + 544.3, + 13.4, + 11.2 + ], + "confidence": 1, + "ink": "#5c5e61", + "ground": "#f7f7f7" + }, + { + "text": "28", + "bounds": [ + 752.4, + 533.2, + 20.1, + 11.2 + ], + "confidence": 1, + "ink": "#525458", + "ground": "#f7f7f7" + }, + { + "text": "20", + "bounds": [ + 841.7, + 560.0, + 17.9, + 13.4 + ], + "confidence": 1, + "ink": "#525459", + "ground": "#f6f7f7" + }, + { + "text": "16", + "bounds": [ + 926.5, + 573.4, + 17.9, + 13.4 + ], + "confidence": 1, + "ink": "#4f5357", + "ground": "#f7f7f8" + }, + { + "text": "Northwest", + "bounds": [ + 654.1, + 664.8, + 58.0, + 13.4 + ], + "confidence": 1, + "ink": "#5d5e60", + "ground": "#f6f6f6" + }, + { + "text": "Coast", + "bounds": [ + 747.9, + 664.8, + 31.3, + 13.4 + ], + "confidence": 1, + "ink": "#5c5d60", + "ground": "#f5f5f5" + }, + { + "text": "Valley", + "bounds": [ + 832.7, + 664.8, + 35.7, + 13.4 + ], + "confidence": 1, + "ink": "#5c5e63", + "ground": "#f5f5f6" + }, + { + "text": "Metro", + "bounds": [ + 917.6, + 664.8, + 35.7, + 13.4 + ], + "confidence": 1, + "ink": "#505255", + "ground": "#f4f4f4" + }, + { + "text": "The Northwest region contributed 43% of total revenue, supported by", + "bounds": [ + 587.2, + 707.0, + 390.7, + 16.0 + ], + "confidence": 1, + "ink": "#676661", + "ground": "#f6f4e8" + }, + { + "text": "strong performance in commercial and agricultural segments.", + "bounds": [ + 587.2, + 729.5, + 337.1, + 15.6 + ], + "confidence": 1, + "ink": "#6b6c6f", + "ground": "#f7f7f7" + }, + { + "text": "Confidential", + "bounds": [ + 966.6, + 807.3, + 51.5, + 11.7 + ], + "confidence": 1, + "ink": "#85878b", + "ground": "#f6f6f6" + }, + { + "text": "Northwind Cooperative", + "bounds": [ + 582.7, + 874.5, + 98.2, + 11.5 + ], + "confidence": 1, + "ink": "#85868b", + "ground": "#f5f5f6" + }, + { + "text": "Q3 2026 Board Report", + "bounds": [ + 942.1, + 872.0, + 96, + 14 + ], + "confidence": 1, + "ink": "#88898d", + "ground": "#f8f8f9" + }, + { + "text": "Anaratinne", + "bounds": [ + 587.2, + 912.5, + 113.9, + 15.6 + ], + "confidence": 1, + "ink": "#2f4482", + "ground": "#f7f8fa" + }, + { + "text": "<", + "bounds": [ + 576.0, + 945.9, + 11.2, + 13.4 + ], + "confidence": 1, + "ink": "#4b4c52", + "ground": "#f8f8f9" + }, + { + "text": "3 / 24", + "bounds": [ + 620.7, + 945.9, + 42.4, + 13.4 + ], + "confidence": 1, + "ink": "#5e6065", + "ground": "#f8f8f8" + }, + { + "text": ">", + "bounds": [ + 696.6, + 945.9, + 11.2, + 15.6 + ], + "confidence": 1, + "ink": "#484a51", + "ground": "#fafafa" + }, + { + "text": "- 100%", + "bounds": [ + 754.5, + 947.8, + 73.8, + 14.1 + ], + "confidence": 1, + "ink": "#5e6064", + "ground": "#f9f9fa" + }, + { + "text": "+", + "bounds": [ + 843.9, + 945.9, + 20.1, + 17.8 + ], + "confidence": 1, + "ink": "#676970", + "ground": "#fafbfb" + }, + { + "text": "\u0413\u041e\u043bR\u0421 \u0421\u041e\u0421 \u041e\u041e\u041eI\u0428C \u0417U\u0417\u043e\u041e\u0412CCK\u043eIlK Cotoranc", + "bounds": [ + 589.4, + 977.0, + 381.8, + 5 + ], + "confidence": 0.3, + "ink": "#818285", + "ground": "#ededee" + }, + { + "text": "Document", + "bounds": [ + 1210.0, + 122.4, + 67.1, + 13.9 + ], + "confidence": 1, + "ink": "#3b3e41", + "ground": "#f2f2f2" + }, + { + "text": "File name", + "bounds": [ + 1210.0, + 158.0, + 56.0, + 14.1 + ], + "confidence": 1, + "ink": "#9b9ea4", + "ground": "#f8f8f9" + }, + { + "text": "Q3 2026 Board Report.pdf", + "bounds": [ + 1210.0, + 180.7, + 158.5, + 15.6 + ], + "confidence": 1, + "ink": "#5e5f63", + "ground": "#f6f6f7" + }, + { + "text": "Pages", + "bounds": [ + 1210.0, + 218.6, + 35.7, + 15.6 + ], + "confidence": 1, + "ink": "#94969e", + "ground": "#f9f9fa" + }, + { + "text": "24", + "bounds": [ + 1207.8, + 243.2, + 20.1, + 11.2 + ], + "confidence": 1, + "ink": "#505258", + "ground": "#f4f4f4" + }, + { + "text": "File size", + "bounds": [ + 1210.0, + 278.9, + 44.7, + 13.4 + ], + "confidence": 1, + "ink": "#9c9fa5", + "ground": "#f8f8f9" + }, + { + "text": "2.8 MB", + "bounds": [ + 1210.0, + 303.0, + 44.7, + 14 + ], + "confidence": 1, + "ink": "#63656a", + "ground": "#f7f7f8" + }, + { + "text": "Created", + "bounds": [ + 1210.0, + 341.3, + 46.9, + 11.2 + ], + "confidence": 1, + "ink": "#a1a3aa", + "ground": "#f8f8f9" + }, + { + "text": "6 Oct 2026, 09:14", + "bounds": [ + 1210.0, + 363.6, + 107.2, + 15.6 + ], + "confidence": 1, + "ink": "#585a5e", + "ground": "#f5f5f6" + }, + { + "text": "Modified", + "bounds": [ + 1210.0, + 401.5, + 51.4, + 11.3 + ], + "confidence": 1, + "ink": "#9b9da2", + "ground": "#f7f7f8" + }, + { + "text": "6 Oct 2026, 09:14", + "bounds": [ + 1210.0, + 423.9, + 107.2, + 13.4 + ], + "confidence": 1, + "ink": "#55575b", + "ground": "#f3f4f4" + }, + { + "text": "Tags", + "bounds": [ + 1210.0, + 475.2, + 33.5, + 15.6 + ], + "confidence": 1, + "ink": "#47484d", + "ground": "#f6f7f7" + }, + { + "text": "\u2022 Add tag", + "bounds": [ + 1216.7, + 506.0, + 67.0, + 16.0 + ], + "confidence": 1, + "ink": "#626468", + "ground": "#f7f7f8" + }, + { + "text": "Signatures", + "bounds": [ + 1210.0, + 553.3, + 71.4, + 15.6 + ], + "confidence": 1, + "ink": "#434448", + "ground": "#f5f5f5" + }, + { + "text": "All signatures are valid", + "bounds": [ + 1234.6, + 586.7, + 125.0, + 15.6 + ], + "confidence": 1, + "ink": "#5c5d60", + "ground": "#f6f6f6" + }, + { + "text": "Signed by: Jun Park", + "bounds": [ + 1234.6, + 613.5, + 107.2, + 15.6 + ], + "confidence": 1, + "ink": "#94959a", + "ground": "#f8f8f9" + }, + { + "text": "Signed on: 6 Oct 2026, 09:10", + "bounds": [ + 1234.6, + 635.8, + 163.0, + 15.6 + ], + "confidence": 1, + "ink": "#93959b", + "ground": "#f8f9f9" + }, + { + "text": "View signatures", + "bounds": [ + 1221.2, + 676.0, + 89.3, + 13.4 + ], + "confidence": 1, + "ink": "#787a7e", + "ground": "#f7f8f8" + }, + { + "text": "Reviewers", + "bounds": [ + 1210.0, + 725.1, + 67.0, + 13.4 + ], + "confidence": 1, + "ink": "#424348", + "ground": "#f2f2f3" + }, + { + "text": "MC", + "bounds": [ + 1214.5, + 763.0, + 24.6, + 15.6 + ], + "confidence": 1, + "ink": "#798699", + "ground": "#f1f4f8" + }, + { + "text": "Maya Chen (You)", + "bounds": [ + 1254.7, + 760.7, + 93.8, + 13.4 + ], + "confidence": 1, + "ink": "#65676b", + "ground": "#f5f5f6" + }, + { + "text": "Jun Park", + "bounds": [ + 1254.7, + 807.6, + 49.1, + 13.4 + ], + "confidence": 1, + "ink": "#6c6d70", + "ground": "#f8f8f8" + }, + { + "text": "AR", + "bounds": [ + 1219.0, + 854.4, + 17.9, + 11.2 + ], + "confidence": 1, + "ink": "#c36875", + "ground": "#fbf0f2" + }, + { + "text": "Ana Ruiz", + "bounds": [ + 1254.6, + 851.7, + 51.6, + 14.5 + ], + "confidence": 1, + "ink": "#636568", + "ground": "#f6f7f7" + }, + { + "text": "Add reviewer", + "bounds": [ + 1241.3, + 899.0, + 80.4, + 11.2 + ], + "confidence": 1, + "ink": "#787a7e", + "ground": "#f6f6f7" + }, + { + "text": "+ Open", + "bounds": [ + 1453.3, + 19.7, + 58.2, + 16.3 + ], + "confidence": 1, + "ink": "#585a5f", + "ground": "#f5f6f7" + }, + { + "text": "Save", + "bounds": [ + 1462.3, + 71.4, + 35.7, + 13.4 + ], + "confidence": 1, + "ink": "#c1daf5", + "ground": "#1569d9" + }, + { + "text": "No status", + "bounds": [ + 1406.5, + 765.1, + 55.9, + 13.6 + ], + "confidence": 1, + "ink": "#95989e", + "ground": "#f9f9f9" + }, + { + "text": "Reviewed", + "bounds": [ + 1406.5, + 809.8, + 53.6, + 13.4 + ], + "confidence": 1, + "ink": "#79ad89", + "ground": "#f5faf7" + }, + { + "text": "Changes requested v", + "bounds": [ + 1408.7, + 852.0, + 102.7, + 14 + ], + "confidence": 1, + "ink": "#dd8484", + "ground": "#fdf8f8" + }, + { + "text": "\u2022 Saved", + "bounds": [ + 20.1, + 997.0, + 53.6, + 14 + ], + "confidence": 1, + "ink": "#66977f", + "ground": "#f7f8f8" + }, + { + "text": "14.6 of 64 MB used", + "bounds": [ + 1384.2, + 997.0, + 125.0, + 14 + ], + "confidence": 1, + "ink": "#8f9197", + "ground": "#f9f9fa" + } + ], + "surfaces": [ + { + "bounds": [ + 0, + 5, + 1536, + 1019 + ], + "fill": "#fdfefe", + "border": "#eaebed", + "radius": 0 + }, + { + "bounds": [ + 99, + 101, + 1097, + 885 + ], + "fill": "#fdfdfd", + "border": "#f9f9fa", + "radius": 0 + }, + { + "bounds": [ + 388, + 154, + 808, + 832 + ], + "fill": "#fdfdfd", + "border": "#f0f1f2", + "radius": 170 + }, + { + "bounds": [ + 504, + 162, + 573, + 677 + ], + "fill": "#fefefe", + "border": "#eeeff1", + "radius": 4 + }, + { + "bounds": [ + 1195, + 104, + 340, + 880 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 101, + 155, + 287, + 829 + ], + "fill": "#fdfefe", + "border": "#fcfcfd", + "radius": 0 + }, + { + "bounds": [ + 105, + 258, + 265, + 155 + ], + "fill": "#eff5fc", + "border": "#edf3fd", + "radius": 3 + }, + { + "bounds": [ + 390, + 104, + 803, + 50 + ], + "fill": "#fafbfc", + "border": "#fafafb", + "radius": 0 + }, + { + "bounds": [ + 101, + 104, + 286, + 49 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 974, + 59, + 353, + 37 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 539, + 9, + 284, + 37 + ], + "fill": "#f8f8fa", + "border": "#f9f9fa", + "radius": 1 + }, + { + "bounds": [ + 977, + 62, + 346, + 30 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 1 + }, + { + "bounds": [ + 582, + 284, + 381, + 25 + ], + "fill": "#fbf6c6", + "border": "#fffcf9", + "radius": 4 + }, + { + "bounds": [ + 278, + 9, + 255, + 37 + ], + "fill": "#f8f8fa", + "border": "#f9f9fb", + "radius": 1 + }, + { + "bounds": [ + 1070, + 9, + 246, + 37 + ], + "fill": "#f8f8fa", + "border": "#f9f9fb", + "radius": 0 + }, + { + "bounds": [ + 825, + 9, + 238, + 37 + ], + "fill": "#f8f8fa", + "border": "#f9fafb", + "radius": 1 + }, + { + "bounds": [ + 664, + 473, + 39, + 186 + ], + "fill": "#2156a4", + "border": "#38639e", + "radius": 1 + }, + { + "bounds": [ + 9, + 359, + 84, + 69 + ], + "fill": "#e8f1fb", + "border": "#fdfefe", + "radius": 4 + }, + { + "bounds": [ + 646, + 109, + 148, + 38 + ], + "fill": "#fefefe", + "border": "#fafafb", + "radius": 0 + }, + { + "bounds": [ + 1210, + 887, + 125, + 37 + ], + "fill": "#fdfefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1400, + 798, + 118, + 38 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1400, + 753, + 118, + 38 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 743, + 553, + 40, + 106 + ], + "fill": "#2156a6", + "border": "#2f62a2", + "radius": 3 + }, + { + "bounds": [ + 1210, + 665, + 114, + 36 + ], + "fill": "#fdfdfd", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1400, + 843, + 118, + 33 + ], + "fill": "#fefdfd", + "border": "#fdfefe", + "radius": 0 + }, + { + "bounds": [ + 1213, + 890, + 119, + 31 + ], + "fill": "#fdfdfd", + "border": "#fefefe", + "radius": 1 + }, + { + "bounds": [ + 1403, + 801, + 112, + 32 + ], + "fill": "#fdfefe", + "border": "#fdfdfd", + "radius": 1 + }, + { + "bounds": [ + 1403, + 756, + 112, + 32 + ], + "fill": "#fdfefe", + "border": "#fdfdfe", + "radius": 1 + }, + { + "bounds": [ + 1213, + 668, + 107, + 30 + ], + "fill": "#fcfdfd", + "border": "#fdfdfd", + "radius": 1 + }, + { + "bounds": [ + 831, + 581, + 40, + 78 + ], + "fill": "#2056a5", + "border": "#7c98bd", + "radius": 1 + }, + { + "bounds": [ + 1403, + 846, + 114, + 27 + ], + "fill": "#fefcfd", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 650, + 114, + 101, + 29 + ], + "fill": "#fdfdfd", + "border": "#fefefe", + "radius": 1 + }, + { + "bounds": [ + 1209, + 497, + 87, + 33 + ], + "fill": "#fcfcfd", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1442, + 11, + 82, + 33 + ], + "fill": "#f8f9fa", + "border": "#f2f3f4", + "radius": 0 + }, + { + "bounds": [ + 1442, + 60, + 79, + 34 + ], + "fill": "#1167d7", + "border": "#fafcfd", + "radius": 4 + }, + { + "bounds": [ + 916, + 594, + 40, + 65 + ], + "fill": "#2157a6", + "border": "#7e98b9", + "radius": 4 + }, + { + "bounds": [ + 844, + 111, + 62, + 35 + ], + "fill": "#fdfefe", + "border": "#fafbfc", + "radius": 0 + }, + { + "bounds": [ + 1213, + 500, + 80, + 27 + ], + "fill": "#fbfbfb", + "border": "#fafbfb", + "radius": 0 + }, + { + "bounds": [ + 587, + 403, + 68, + 23 + ], + "fill": "#f9e664", + "border": "#fdfcf9", + "radius": 4 + }, + { + "bounds": [ + 795, + 111, + 43, + 36 + ], + "fill": "#fdfdfe", + "border": "#fafafb", + "radius": 0 + }, + { + "bounds": [ + 847, + 115, + 55, + 28 + ], + "fill": "#fcfdfd", + "border": "#fdfefe", + "radius": 0 + }, + { + "bounds": [ + 752, + 113, + 38, + 31 + ], + "fill": "#fefefe", + "border": "#f0f0f2", + "radius": 1 + }, + { + "bounds": [ + 848, + 706, + 56, + 21 + ], + "fill": "#f9e87d", + "border": "#fcfaf8", + "radius": 6 + }, + { + "bounds": [ + 798, + 114, + 37, + 29 + ], + "fill": "#fdfdfd", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 811, + 336, + 50, + 19 + ], + "fill": "#e6d47d", + "border": "#fdf7d8", + "radius": 6 + } + ], + "dividers": [ + { + "axis": "h", + "at": 60, + "from": 978, + "to": 1320, + "color": "#e7e8ea" + }, + { + "axis": "h", + "at": 102, + "from": 100, + "to": 1536, + "color": "#ebecee" + }, + { + "axis": "h", + "at": 153, + "from": 389, + "to": 782, + "color": "#f4f4f6" + }, + { + "axis": "h", + "at": 154, + "from": 389, + "to": 1193, + "color": "#e9e9eb" + }, + { + "axis": "h", + "at": 165, + "from": 508, + "to": 1073, + "color": "#f3f3f5" + }, + { + "axis": "h", + "at": 285, + "from": 584, + "to": 960, + "color": "#fe900f" + }, + { + "axis": "h", + "at": 286, + "from": 583, + "to": 961, + "color": "#ffb016" + }, + { + "axis": "h", + "at": 305, + "from": 644, + "to": 961, + "color": "#ffe186" + }, + { + "axis": "h", + "at": 306, + "from": 583, + "to": 961, + "color": "#fd8707" + }, + { + "axis": "h", + "at": 307, + "from": 584, + "to": 960, + "color": "#fdbc67" + }, + { + "axis": "h", + "at": 458, + "from": 1194, + "to": 1519, + "color": "#ecedef" + }, + { + "axis": "h", + "at": 537, + "from": 1195, + "to": 1518, + "color": "#efeff2" + }, + { + "axis": "h", + "at": 658, + "from": 633, + "to": 979, + "color": "#c1c4cb" + }, + { + "axis": "h", + "at": 708, + "from": 1195, + "to": 1519, + "color": "#eff0f2" + }, + { + "axis": "h", + "at": 834, + "from": 507, + "to": 1073, + "color": "#efeff0" + }, + { + "axis": "h", + "at": 835, + "from": 508, + "to": 1073, + "color": "#d3d3d5" + }, + { + "axis": "h", + "at": 845, + "from": 509, + "to": 1072, + "color": "#e7e8ea" + }, + { + "axis": "h", + "at": 846, + "from": 509, + "to": 1073, + "color": "#f6f7f8" + }, + { + "axis": "h", + "at": 926, + "from": 692, + "to": 969, + "color": "#ebebed" + }, + { + "axis": "h", + "at": 984, + "from": 1194, + "to": 1536, + "color": "#edeef0" + }, + { + "axis": "v", + "at": 1, + "from": 103, + "to": 984, + "color": "#f4f5f7" + }, + { + "axis": "v", + "at": 387, + "from": 585, + "to": 781, + "color": "#f1f2f3" + }, + { + "axis": "v", + "at": 388, + "from": 585, + "to": 781, + "color": "#eaebed" + }, + { + "axis": "v", + "at": 506, + "from": 168, + "to": 835, + "color": "#dfe0e2" + }, + { + "axis": "v", + "at": 507, + "from": 167, + "to": 834, + "color": "#f3f4f5" + }, + { + "axis": "v", + "at": 663, + "from": 472, + "to": 659, + "color": "#e1e6f1" + }, + { + "axis": "v", + "at": 701, + "from": 473, + "to": 658, + "color": "#114c9e" + }, + { + "axis": "v", + "at": 702, + "from": 472, + "to": 659, + "color": "#486ba2" + }, + { + "axis": "v", + "at": 1193, + "from": 709, + "to": 938, + "color": "#eaebee" + }, + { + "axis": "v", + "at": 1194, + "from": 709, + "to": 938, + "color": "#f2f2f4" + } + ], + "limits": [ + "Evidence for review: roles and widgets are assigned in the semantic map, not here." + ] +} diff --git a/apps/pdftools/design/source/03-find.png b/apps/pdftools/design/source/03-find.png new file mode 100644 index 00000000..f091c0e3 Binary files /dev/null and b/apps/pdftools/design/source/03-find.png differ diff --git a/apps/pdftools/design/source/03-find.prompt.txt b/apps/pdftools/design/source/03-find.prompt.txt new file mode 100644 index 00000000..7ffdcc1e --- /dev/null +++ b/apps/pdftools/design/source/03-find.prompt.txt @@ -0,0 +1,70 @@ +Create ONE original image: a single, complete desktop application screen for +"PDF Tools", a document app inside the OctoSense desktop. + +Requested image size: 1536 × 1024 pixels, landscape. Requested quality: high. +Show only the application's own window content, edge to edge, flat and +straight on. No operating-system menu bar, no dock, no window title bar or +traffic lights, no device frame, no perspective, no drop shadow around the +screen, no captions or annotations outside the interface. + +Visual language: an original, precise desktop document tool. The document is +the brightest thing on screen; the chrome is quiet, thin and well aligned. +Palette: a cool light grey desk behind the pages, white pages with a soft +shadow, graphite text, mid-grey secondary text, thin 1-pixel dividers in pale +grey, and ONE accent colour, a confident medium blue, used only for the +current mode, the selected item, focus and primary buttons. Yellow marks +highlighted text and search matches. Green means saved or done, red means an +error, both used sparingly. Corner radius about 8 px on panels, buttons and +fields; pages have square corners. Icons are simple outline glyphs with one +consistent stroke weight. Typography: a neutral sans-serif for the interface, +small and crisp (about 13–14 px), medium weight for panel titles; document +pages use a classic serif for body text and a sans-serif for their headings. +Do not imitate Adobe Acrobat's layout, icons, colours or branding, or any other +product's. + +Window layout, the same in every screen: +- Top row: document tabs. Each tab shows a small PDF glyph, the file name and + a close cross; the active tab is white, the others grey; a "+ Open" button + follows the last tab. +- Second row, the mode bar: the modes "Read", "Comment", "Fill & Sign", + "Pages", "Combine", "Edit" as text tabs, the current one in the accent + colour with a short underline; on the right a search field with a magnifier + and the placeholder "Search in document", then icon buttons for Undo and Redo, + and a "Save" button. +- Left: a narrow icon rail with four icons and tooltips' labels shown under + them in tiny text: "Pages", "Outline", "Comments", "Search". The selected + icon has a tinted square behind it. Next to the rail, an open panel about + 240 px wide when the screen calls for it. +- Centre: the canvas, a light grey desk with the document's pages stacked + vertically, centred, one page fully visible and the next starting below. +- Bottom centre of the canvas: a floating rounded pill with "‹", "3 / 24", + "›", a divider, "−", "100%", "+", and two small icons for "Fit width" and + "Fit page". +- Right: a panel about 300 px wide, only in screens that call for it. +- Bottom edge: a thin status line with "Saved" (with a small green dot) on the + left and "14.6 of 64 MB used" on the right. + +Use this exact fixture wherever it appears: +- Documents in the library: "Q3 2026 Board Report.pdf" (24 pages, 2.8 MB, + opened today), "Riverside Lease 2026.pdf" (12 pages, 1.1 MB, yesterday), + "Field Guide to Garden Birds.pdf" (7 pages, 16 KB, 3 Oct), "Invoice + INV-2041.pdf" (1 page, 84 KB, 1 Oct), "Site Survey Photos.pdf" (18 pages, + 9.6 MB, 28 Sep). +- The board report belongs to the fictional "Northwind Cooperative". Its + sections: Summary, Revenue, Operations, Outlook, Appendix. Page 3 is + "Revenue" with a short paragraph that includes the sentence "Revenue grew + 12% year over year, led by the Northwest region." and a simple bar chart + "Revenue by region, Q3 2026" with bars for Northwest, Coast, Valley, Metro. +- People: Maya Chen (the user), Jun Park and Ana Ruiz (reviewers). + +Render every piece of interface text and every control sharply enough to be +read and measured later. Labels, buttons, fields and panels will be rebuilt as +native components from this image; page contents are sample document art. + +This screen: mode "Read", with "revenue" typed in the search field. The rail's +"Search" icon is selected; its panel shows "12 matches on 7 pages" and a list +of results grouped by page ("Page 1", "Page 3", "Page 4", …), each with a +one-line snippet in which "revenue" is bold. The second result is selected. +On the canvas, page 3 shows every match highlighted in yellow and the selected +match in a stronger orange-yellow with an outline. A small bar above the canvas +reads "2 of 12" with up and down arrows and "Done". \ No newline at end of file diff --git a/apps/pdftools/design/source/04-comment.measured.json b/apps/pdftools/design/source/04-comment.measured.json new file mode 100644 index 00000000..75b70d91 --- /dev/null +++ b/apps/pdftools/design/source/04-comment.measured.json @@ -0,0 +1,1623 @@ +{ + "schema_version": 1, + "image": "04-comment.png", + "image_sha256": "4a87dcc47d9bb98ccc5f4bf04058fd407f847b2c4cc862e3fa32d7fb6337eaf3", + "script_sha256": "ea860507e32482b955d3ac577ab3cf9fc559f59ecc916c89175293932b54e0b7", + "size": [ + 1536, + 1024 + ], + "ocr_engine": "Apple Vision VNRecognizeTextRequest accurate, en-US (App Flow ocr.swift)", + "text": [ + { + "text": "Q3 2026 Board Report.pdf", + "bounds": [ + 44.7, + 20.0, + 185.3, + 16 + ], + "confidence": 1, + "ink": "#5e5e61", + "ground": "#f6f6f7" + }, + { + "text": "Riverside Lease 2026.pdf X", + "bounds": [ + 292.5, + 17.8, + 185.3, + 18.2 + ], + "confidence": 1, + "ink": "#6b6c70", + "ground": "#eeeff0" + }, + { + "text": "Field Guide to Garden Birds.pdf X", + "bounds": [ + 524.7, + 20.0, + 214.3, + 16 + ], + "confidence": 1, + "ink": "#717274", + "ground": "#eeeff1" + }, + { + "text": "Invoice INV-2041.pdf", + "bounds": [ + 785.9, + 20.0, + 129.5, + 18 + ], + "confidence": 1, + "ink": "#6a6b6e", + "ground": "#edeef0" + }, + { + "text": "Read", + "bounds": [ + 31.3, + 71.4, + 38.0, + 15.6 + ], + "confidence": 1, + "ink": "#4d4d50", + "ground": "#f3f3f4" + }, + { + "text": "Comment", + "bounds": [ + 107.0, + 70.8, + 73.9, + 16.8 + ], + "confidence": 1, + "ink": "#3974d5", + "ground": "#f5f8fc" + }, + { + "text": "Fill & Sign", + "bounds": [ + 216.3, + 70.4, + 74.1, + 19.8 + ], + "confidence": 1, + "ink": "#545557", + "ground": "#f8f8f9" + }, + { + "text": "Pages", + "bounds": [ + 323.7, + 71.4, + 44.7, + 17.8 + ], + "confidence": 1, + "ink": "#515254", + "ground": "#f6f6f6" + }, + { + "text": "Combine", + "bounds": [ + 406.3, + 71.3, + 64.8, + 15.8 + ], + "confidence": 1, + "ink": "#505154", + "ground": "#f6f6f6" + }, + { + "text": "Edit", + "bounds": [ + 504.6, + 71.4, + 33.5, + 15.6 + ], + "confidence": 1, + "ink": "#484a4d", + "ground": "#f4f4f4" + }, + { + "text": "Highlight", + "bounds": [ + 149.6, + 131.6, + 58.0, + 17.8 + ], + "confidence": 1, + "ink": "#3a6fd3", + "ground": "#e8f1fc" + }, + { + "text": "T Underline", + "bounds": [ + 258.7, + 128.2, + 87.6, + 22.4 + ], + "confidence": 1, + "ink": "#636365", + "ground": "#f9f9f9" + }, + { + "text": "S Strike out", + "bounds": [ + 399.5, + 128.7, + 91.8, + 21.4 + ], + "confidence": 1, + "ink": "#545557", + "ground": "#f8f8f8" + }, + { + "text": "Note", + "bounds": [ + 567.1, + 131.6, + 31.3, + 13.4 + ], + "confidence": 1, + "ink": "#4c4c4e", + "ground": "#f3f3f4" + }, + { + "text": "IT Text box", + "bounds": [ + 643.0, + 129.0, + 84.8, + 23 + ], + "confidence": 1, + "ink": "#646566", + "ground": "#f9f9f9" + }, + { + "text": "Pages", + "bounds": [ + 24.6, + 227.6, + 38.0, + 15.6 + ], + "confidence": 1, + "ink": "#487eda", + "ground": "#e9f2fc" + }, + { + "text": ":=", + "bounds": [ + 24.6, + 276.6, + 29.0, + 22.3 + ], + "confidence": 1, + "ink": "#686869", + "ground": "#f7f7f7" + }, + { + "text": "Outline", + "bounds": [ + 20.1, + 310.1, + 44.7, + 17.8 + ], + "confidence": 1, + "ink": "#939395", + "ground": "#fafafa" + }, + { + "text": "Comments", + "bounds": [ + 11.1, + 394.7, + 62.6, + 13.7 + ], + "confidence": 1, + "ink": "#868788", + "ground": "#f7f7f8" + }, + { + "text": "Search", + "bounds": [ + 20.1, + 479.7, + 44.7, + 13.4 + ], + "confidence": 1, + "ink": "#7e7f80", + "ground": "#f6f6f6" + }, + { + "text": "NORTHWIND COOPERATIVE", + "bounds": [ + 272.4, + 225.0, + 227.7, + 14 + ], + "confidence": 1, + "ink": "#27499c", + "ground": "#f0f3f8" + }, + { + "text": "Q3 2026 Board Report", + "bounds": [ + 270.1, + 256.6, + 305.9, + 33.5 + ], + "confidence": 1, + "ink": "#2d2f34", + "ground": "#f7f7f7" + }, + { + "text": "2. Revenue", + "bounds": [ + 267.7, + 335.5, + 150.1, + 31.7 + ], + "confidence": 1, + "ink": "#1b4faa", + "ground": "#f8f9fc" + }, + { + "text": "Total revenue for the third quarter of 2026 was $48.7 million, an increase of", + "bounds": [ + 270.1, + 386.0, + 488.9, + 20.1 + ], + "confidence": 1, + "ink": "#6a6b6c", + "ground": "#f9f9f9" + }, + { + "text": "12% compared to the same period in 2025. This growth was driven by strong", + "bounds": [ + 270.1, + 410.0, + 486.7, + 21 + ], + "confidence": 1, + "ink": "#676769", + "ground": "#f8f8f9" + }, + { + "text": "demand across all regions and improved pricing.", + "bounds": [ + 272.4, + 435.0, + 303.6, + 20.1 + ], + "confidence": 1, + "ink": "#69696a", + "ground": "#f7f7f8" + }, + { + "text": "Revenue grew 12%yearover year, led by the Northwest region.", + "bounds": [ + 274.6, + 468.0, + 408.6, + 18.3 + ], + "confidence": 1, + "ink": "#615b2e", + "ground": "#f8e475" + }, + { + "text": "Revenue by region, Q3 2026", + "bounds": [ + 272.4, + 510.9, + 241.1, + 24.5 + ], + "confidence": 1, + "ink": "#313234", + "ground": "#f6f6f6" + }, + { + "text": "(USD millions)", + "bounds": [ + 270.1, + 537.3, + 89.4, + 16.3 + ], + "confidence": 1, + "ink": "#747576", + "ground": "#f6f6f6" + }, + { + "text": "20", + "bounds": [ + 270.1, + 566.7, + 22.3, + 15.6 + ], + "confidence": 1, + "ink": "#717275", + "ground": "#f8f8f8" + }, + { + "text": "18.2", + "bounds": [ + 352.7, + 571.1, + 29.0, + 13.4 + ], + "confidence": 1, + "ink": "#49494c", + "ground": "#f1f1f2" + }, + { + "text": "15", + "bounds": [ + 272.4, + 606.8, + 17.9, + 13.4 + ], + "confidence": 1, + "ink": "#757678", + "ground": "#f7f7f7" + }, + { + "text": "12.6", + "bounds": [ + 493.4, + 611.3, + 31.3, + 13.4 + ], + "confidence": 1, + "ink": "#494a4b", + "ground": "#f2f3f3" + }, + { + "text": "9.4", + "bounds": [ + 638.5, + 640.3, + 22.3, + 13.4 + ], + "confidence": 1, + "ink": "#5f5f60", + "ground": "#f6f6f6" + }, + { + "text": "10", + "bounds": [ + 272.4, + 647.0, + 20.1, + 13.4 + ], + "confidence": 1, + "ink": "#67686a", + "ground": "#f7f7f7" + }, + { + "text": "8.5", + "bounds": [ + 772.5, + 644.7, + 22.3, + 15.6 + ], + "confidence": 1, + "ink": "#565658", + "ground": "#f5f5f5" + }, + { + "text": "Coast", + "bounds": [ + 491.2, + 747.4, + 38.0, + 13.4 + ], + "confidence": 1, + "ink": "#68696b", + "ground": "#f4f4f5" + }, + { + "text": "Valley", + "bounds": [ + 627.3, + 745.1, + 40.2, + 17.8 + ], + "confidence": 1, + "ink": "#757577", + "ground": "#f8f8f8" + }, + { + "text": "Metro", + "bounds": [ + 763.5, + 745.1, + 38.0, + 15.6 + ], + "confidence": 1, + "ink": "#737375", + "ground": "#f7f7f7" + }, + { + "text": "Site Survey Photos.pdf", + "bounds": [ + 995.7, + 20.0, + 138.4, + 18 + ], + "confidence": 1, + "ink": "#6a6b6e", + "ground": "#edeeef" + }, + { + "text": "+ Open", + "bounds": [ + 1196.5, + 19.7, + 56.0, + 16.4 + ], + "confidence": 1, + "ink": "#757577", + "ground": "#f6f7f7" + }, + { + "text": "Q Search in document", + "bounds": [ + 989.0, + 71.0, + 154.0, + 18.2 + ], + "confidence": 1, + "ink": "#a2a2a4", + "ground": "#fafafa" + }, + { + "text": "Save", + "bounds": [ + 1466.8, + 71.4, + 35.7, + 15.6 + ], + "confidence": 1, + "ink": "#d1e4f9", + "ground": "#2070e0" + }, + { + "text": "Comments 3", + "bounds": [ + 1131.9, + 129.0, + 122.8, + 18.2 + ], + "confidence": 1, + "ink": "#3c3e43", + "ground": "#f4f4f5" + }, + { + "text": "Page 3", + "bounds": [ + 1172.1, + 205.0, + 49.1, + 20.3 + ], + "confidence": 1, + "ink": "#8d8d8f", + "ground": "#f9f9f9" + }, + { + "text": "^", + "bounds": [ + 1484.7, + 205.2, + 15.6, + 13.4 + ], + "confidence": 1, + "ink": "#7d7d7f", + "ground": "#fbfbfc" + }, + { + "text": "JP", + "bounds": [ + 1147.5, + 263.3, + 22.3, + 15.6 + ], + "confidence": 1, + "ink": "#253143", + "ground": "#c6def0" + }, + { + "text": "Jun Park", + "bounds": [ + 1192.2, + 261.0, + 64.7, + 16.0 + ], + "confidence": 1, + "ink": "#3e4146", + "ground": "#f3f4f4" + }, + { + "text": "Today 10:12", + "bounds": [ + 1413.2, + 260.8, + 80.5, + 20.4 + ], + "confidence": 1, + "ink": "#9d9ea0", + "ground": "#fafafb" + }, + { + "text": "Can we add the regional breakdown", + "bounds": [ + 1194.4, + 296.7, + 243.3, + 18.3 + ], + "confidence": 1, + "ink": "#505256", + "ground": "#f5f5f6" + }, + { + "text": "table here?", + "bounds": [ + 1192.1, + 325.4, + 80.5, + 18.4 + ], + "confidence": 1, + "ink": "#585a5f", + "ground": "#f8f8f8" + }, + { + "text": "MC", + "bounds": [ + 1194.4, + 377.0, + 29.0, + 15.6 + ], + "confidence": 1, + "ink": "#4a425e", + "ground": "#e9e0f6" + }, + { + "text": "Maya Chen", + "bounds": [ + 1243.3, + 373.9, + 83.0, + 21.8 + ], + "confidence": 1, + "ink": "#43454a", + "ground": "#f7f7f8" + }, + { + "text": "Added on page 4.", + "bounds": [ + 1238.9, + 407.5, + 125.3, + 23.8 + ], + "confidence": 1, + "ink": "#494b4f", + "ground": "#f7f7f8" + }, + { + "text": "Today 10:20", + "bounds": [ + 1413.2, + 374.8, + 80.4, + 22.3 + ], + "confidence": 1, + "ink": "#959699", + "ground": "#fafafa" + }, + { + "text": "Accepted", + "bounds": [ + 1245.8, + 448.4, + 67.0, + 17.8 + ], + "confidence": 1, + "ink": "#54a973", + "ground": "#effbf4" + }, + { + "text": "Reply...", + "bounds": [ + 1149.8, + 513.1, + 51.3, + 20.1 + ], + "confidence": 1, + "ink": "#949496", + "ground": "#f9f9fa" + }, + { + "text": "Post", + "bounds": [ + 1442.2, + 522.0, + 38.0, + 17.8 + ], + "confidence": 1, + "ink": "#dae7f8", + "ground": "#2172e0" + }, + { + "text": "Page 7", + "bounds": [ + 1169.9, + 618.0, + 49.1, + 18.0 + ], + "confidence": 1, + "ink": "#8a8a8c", + "ground": "#f9f9f9" + }, + { + "text": "AR", + "bounds": [ + 1145.3, + 660.4, + 24.6, + 17.8 + ], + "confidence": 1, + "ink": "#43373f", + "ground": "#f4dadf" + }, + { + "text": "Ana Ruiz", + "bounds": [ + 1189.7, + 659.5, + 65.2, + 17.4 + ], + "confidence": 1, + "ink": "#3c3d41", + "ground": "#f2f2f3" + }, + { + "text": "Yesterday v", + "bounds": [ + 1404.1, + 659.0, + 98.7, + 20.5 + ], + "confidence": 1, + "ink": "#969798", + "ground": "#fbfbfb" + }, + { + "text": "Northwest", + "bounds": [ + 334.9, + 747.4, + 67.0, + 13.4 + ], + "confidence": 1, + "ink": "#6b6b6c", + "ground": "#f5f5f5" + }, + { + "text": "Q3 2026 Board Report", + "bounds": [ + 272.4, + 789.8, + 98.2, + 11.2 + ], + "confidence": 1, + "ink": "#8e8e90", + "ground": "#f6f6f6" + }, + { + "text": "3. Operations", + "bounds": [ + 270.1, + 881.0, + 145.2, + 27.2 + ], + "confidence": 1, + "ink": "#2352a7", + "ground": "#f5f7fb" + }, + { + "text": "<", + "bounds": [ + 553.7, + 910.2, + 11.2, + 15.6 + ], + "confidence": 1, + "ink": "#5e5e60", + "ground": "#f9f9f9" + }, + { + "text": "3 / 24", + "bounds": [ + 611.7, + 910.0, + 44.7, + 16.0 + ], + "confidence": 1, + "ink": "#595a5c", + "ground": "#f8f9f9" + }, + { + "text": "Operations remained efficient during the qu", + "bounds": [ + 272.4, + 925.8, + 265.7, + 18.2 + ], + "confidence": 1, + "ink": "#6c6c6e", + "ground": "#f7f7f8" + }, + { + "text": "prostive as auallis that and timelv ourtroach remains a nrovrtant.", + "bounds": [ + 272.4, + 948.0, + 404.1, + 16 + ], + "confidence": 1, + "ink": "#757576", + "ground": "#f6f6f7" + }, + { + "text": "3", + "bounds": [ + 924.3, + 787.5, + 8.9, + 15.6 + ], + "confidence": 1, + "ink": "#5a5c5e", + "ground": "#f8f8f8" + }, + { + "text": "-", + "bounds": [ + 750.1, + 912.5, + 24.6, + 11.2 + ], + "confidence": 1, + "ink": "#2d2d30", + "ground": "#fbfbfb" + }, + { + "text": "100%", + "bounds": [ + 783.6, + 910.2, + 49.1, + 15.6 + ], + "confidence": 1, + "ink": "#5d5f62", + "ground": "#f9f9f9" + }, + { + "text": "+", + "bounds": [ + 850.6, + 910.2, + 20.1, + 17.8 + ], + "confidence": 1, + "ink": "#313234", + "ground": "#fafafa" + }, + { + "text": "\u2022 Saved", + "bounds": [ + 19.8, + 982.8, + 71.9, + 19.9 + ], + "confidence": 1, + "ink": "#508464", + "ground": "#f9fafa" + }, + { + "text": "14.6 of 64 MB used", + "bounds": [ + 1364.1, + 983.8, + 140.7, + 13.4 + ], + "confidence": 1, + "ink": "#6c6d6f", + "ground": "#f5f5f6" + } + ], + "surfaces": [ + { + "bounds": [ + 0, + 4, + 1536, + 1020 + ], + "fill": "#fefefe", + "border": "#e8e8e8", + "radius": 10 + }, + { + "bounds": [ + 83, + 167, + 1030, + 797 + ], + "fill": "#fefefe", + "border": "#f8f8f8", + "radius": 182 + }, + { + "bounds": [ + 209, + 177, + 778, + 656 + ], + "fill": "#fefefe", + "border": "#eeeff1", + "radius": 4 + }, + { + "bounds": [ + 1113, + 170, + 422, + 792 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1124, + 185, + 400, + 399 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1127, + 238, + 393, + 343 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 2, + 956, + 1533, + 67 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 213, + 844, + 799, + 120 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 2, + 170, + 81, + 792 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 85, + 106, + 1026, + 62 + ], + "fill": "#fefefe", + "border": "#f6f6f6", + "radius": 3 + }, + { + "bounds": [ + 1124, + 598, + 400, + 104 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 528, + 892, + 484, + 53 + ], + "fill": "#fefefe", + "border": "#f8f9f9", + "radius": 0 + }, + { + "bounds": [ + 1113, + 108, + 422, + 60 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1137, + 494, + 372, + 68 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1127, + 188, + 393, + 47 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 973, + 57, + 299, + 42 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 490, + 5, + 259, + 42 + ], + "fill": "#f0f1f2", + "border": "#efeff0", + "radius": 0 + }, + { + "bounds": [ + 976, + 60, + 293, + 35 + ], + "fill": "#fefefe", + "border": "#fdfdfe", + "radius": 1 + }, + { + "bounds": [ + 255, + 5, + 235, + 42 + ], + "fill": "#f0f0f2", + "border": "#eff0f0", + "radius": 0 + }, + { + "bounds": [ + 963, + 5, + 215, + 42 + ], + "fill": "#f0f1f2", + "border": "#eff0f1", + "radius": 1 + }, + { + "bounds": [ + 752, + 5, + 211, + 42 + ], + "fill": "#f0f1f2", + "border": "#efefef", + "radius": 1 + }, + { + "bounds": [ + 345, + 594, + 47, + 143 + ], + "fill": "#1f6bdb", + "border": "#6c71d8", + "radius": 1 + }, + { + "bounds": [ + 106, + 117, + 116, + 43 + ], + "fill": "#eef6fe", + "border": "#fcfcfd", + "radius": 6 + }, + { + "bounds": [ + 9, + 179, + 67, + 74 + ], + "fill": "#edf5fd", + "border": "#fefefe", + "radius": 4 + }, + { + "bounds": [ + 2, + 108, + 81, + 60 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 1 + }, + { + "bounds": [ + 488, + 634, + 46, + 103 + ], + "fill": "#1f6bda", + "border": "#696dd4", + "radius": 6 + }, + { + "bounds": [ + 1416, + 58, + 108, + 40 + ], + "fill": "#206edf", + "border": "#f9fafd", + "radius": 6 + }, + { + "bounds": [ + 109, + 120, + 110, + 37 + ], + "fill": "#edf5fe", + "border": "#dfebfc", + "radius": 1 + }, + { + "bounds": [ + 628, + 662, + 45, + 75 + ], + "fill": "#1e6bdb", + "border": "#666bd7", + "radius": 3 + }, + { + "bounds": [ + 761, + 668, + 46, + 69 + ], + "fill": "#206cdb", + "border": "#6284da", + "radius": 4 + }, + { + "bounds": [ + 1185, + 9, + 83, + 36 + ], + "fill": "#f9f9f9", + "border": "#fbfbfb", + "radius": 1 + }, + { + "bounds": [ + 1242, + 440, + 81, + 32 + ], + "fill": "#f1fcf5", + "border": "#fcfefd", + "radius": 4 + }, + { + "bounds": [ + 1431, + 512, + 62, + 35 + ], + "fill": "#1e6edf", + "border": "#f6f9fd", + "radius": 4 + }, + { + "bounds": [ + 1245, + 444, + 75, + 25 + ], + "fill": "#eefcf4", + "border": "#effaf2", + "radius": 1 + } + ], + "dividers": [ + { + "axis": "h", + "at": 45, + "from": 962, + "to": 1276, + "color": "#e3e3e5" + }, + { + "axis": "h", + "at": 46, + "from": 256, + "to": 1276, + "color": "#f0f0f1" + }, + { + "axis": "h", + "at": 58, + "from": 980, + "to": 1264, + "color": "#dfdfe1" + }, + { + "axis": "h", + "at": 59, + "from": 980, + "to": 1264, + "color": "#ececee" + }, + { + "axis": "h", + "at": 95, + "from": 977, + "to": 1267, + "color": "#e0e1e2" + }, + { + "axis": "h", + "at": 96, + "from": 979, + "to": 1265, + "color": "#f0f0f1" + }, + { + "axis": "h", + "at": 106, + "from": 85, + "to": 1111, + "color": "#e5e6e6" + }, + { + "axis": "h", + "at": 107, + "from": 179, + "to": 1111, + "color": "#efefef" + }, + { + "axis": "h", + "at": 167, + "from": 85, + "to": 1111, + "color": "#f7f7f7" + }, + { + "axis": "h", + "at": 168, + "from": 84, + "to": 1111, + "color": "#e8e9ea" + }, + { + "axis": "h", + "at": 179, + "from": 372, + "to": 982, + "color": "#e4e4e6" + }, + { + "axis": "h", + "at": 186, + "from": 1132, + "to": 1514, + "color": "#e4e5e6" + }, + { + "axis": "h", + "at": 300, + "from": 271, + "to": 935, + "color": "#eceded" + }, + { + "axis": "h", + "at": 301, + "from": 271, + "to": 935, + "color": "#a7a7aa" + }, + { + "axis": "h", + "at": 464, + "from": 273, + "to": 686, + "color": "#fded9b" + }, + { + "axis": "h", + "at": 487, + "from": 274, + "to": 685, + "color": "#fefbe6" + }, + { + "axis": "h", + "at": 494, + "from": 1142, + "to": 1503, + "color": "#f0f0f1" + }, + { + "axis": "h", + "at": 495, + "from": 1140, + "to": 1505, + "color": "#dbdbdd" + }, + { + "axis": "h", + "at": 558, + "from": 1140, + "to": 1504, + "color": "#f0f1f2" + }, + { + "axis": "h", + "at": 559, + "from": 1141, + "to": 1504, + "color": "#dadadd" + }, + { + "axis": "h", + "at": 580, + "from": 1133, + "to": 1513, + "color": "#f3f4f4" + }, + { + "axis": "h", + "at": 581, + "from": 1133, + "to": 1513, + "color": "#eaeaeb" + }, + { + "axis": "h", + "at": 599, + "from": 1131, + "to": 1515, + "color": "#e7e7e9" + }, + { + "axis": "h", + "at": 698, + "from": 1129, + "to": 1517, + "color": "#f6f6f6" + }, + { + "axis": "h", + "at": 699, + "from": 1131, + "to": 1515, + "color": "#e7e8e8" + }, + { + "axis": "h", + "at": 735, + "from": 298, + "to": 854, + "color": "#a8a8ac" + }, + { + "axis": "h", + "at": 736, + "from": 298, + "to": 854, + "color": "#6b6b6e" + }, + { + "axis": "h", + "at": 829, + "from": 212, + "to": 982, + "color": "#c6c6c8" + }, + { + "axis": "h", + "at": 830, + "from": 211, + "to": 982, + "color": "#bbbbbe" + }, + { + "axis": "h", + "at": 831, + "from": 212, + "to": 983, + "color": "#d6d7d9" + }, + { + "axis": "h", + "at": 832, + "from": 212, + "to": 982, + "color": "#e4e4e6" + }, + { + "axis": "h", + "at": 842, + "from": 213, + "to": 982, + "color": "#e4e5e7" + }, + { + "axis": "h", + "at": 843, + "from": 213, + "to": 982, + "color": "#f6f6f7" + }, + { + "axis": "h", + "at": 891, + "from": 545, + "to": 993, + "color": "#d9dadc" + }, + { + "axis": "h", + "at": 945, + "from": 548, + "to": 983, + "color": "#dddedf" + }, + { + "axis": "h", + "at": 961, + "from": 1112, + "to": 1535, + "color": "#f6f6f6" + }, + { + "axis": "h", + "at": 962, + "from": 984, + "to": 1536, + "color": "#d4d4d5" + }, + { + "axis": "v", + "at": 209, + "from": 401, + "to": 789, + "color": "#e6e6e8" + }, + { + "axis": "v", + "at": 210, + "from": 182, + "to": 830, + "color": "#d5d6d8" + }, + { + "axis": "v", + "at": 211, + "from": 182, + "to": 829, + "color": "#c5c6c8" + }, + { + "axis": "v", + "at": 982, + "from": 181, + "to": 829, + "color": "#f0f1f1" + }, + { + "axis": "v", + "at": 983, + "from": 181, + "to": 829, + "color": "#cfcfd2" + }, + { + "axis": "v", + "at": 984, + "from": 184, + "to": 829, + "color": "#e3e4e5" + }, + { + "axis": "v", + "at": 1111, + "from": 169, + "to": 962, + "color": "#e8e8ea" + }, + { + "axis": "v", + "at": 1125, + "from": 237, + "to": 574, + "color": "#e7e8e9" + }, + { + "axis": "v", + "at": 1520, + "from": 238, + "to": 575, + "color": "#ececec" + }, + { + "axis": "v", + "at": 1521, + "from": 238, + "to": 574, + "color": "#f4f4f4" + } + ], + "limits": [ + "Evidence for review: roles and widgets are assigned in the semantic map, not here." + ] +} diff --git a/apps/pdftools/design/source/04-comment.png b/apps/pdftools/design/source/04-comment.png new file mode 100644 index 00000000..08ab16f4 Binary files /dev/null and b/apps/pdftools/design/source/04-comment.png differ diff --git a/apps/pdftools/design/source/04-comment.prompt.txt b/apps/pdftools/design/source/04-comment.prompt.txt new file mode 100644 index 00000000..60226ce4 --- /dev/null +++ b/apps/pdftools/design/source/04-comment.prompt.txt @@ -0,0 +1,73 @@ +Create ONE original image: a single, complete desktop application screen for +"PDF Tools", a document app inside the OctoSense desktop. + +Requested image size: 1536 × 1024 pixels, landscape. Requested quality: high. +Show only the application's own window content, edge to edge, flat and +straight on. No operating-system menu bar, no dock, no window title bar or +traffic lights, no device frame, no perspective, no drop shadow around the +screen, no captions or annotations outside the interface. + +Visual language: an original, precise desktop document tool. The document is +the brightest thing on screen; the chrome is quiet, thin and well aligned. +Palette: a cool light grey desk behind the pages, white pages with a soft +shadow, graphite text, mid-grey secondary text, thin 1-pixel dividers in pale +grey, and ONE accent colour, a confident medium blue, used only for the +current mode, the selected item, focus and primary buttons. Yellow marks +highlighted text and search matches. Green means saved or done, red means an +error, both used sparingly. Corner radius about 8 px on panels, buttons and +fields; pages have square corners. Icons are simple outline glyphs with one +consistent stroke weight. Typography: a neutral sans-serif for the interface, +small and crisp (about 13–14 px), medium weight for panel titles; document +pages use a classic serif for body text and a sans-serif for their headings. +Do not imitate Adobe Acrobat's layout, icons, colours or branding, or any other +product's. + +Window layout, the same in every screen: +- Top row: document tabs. Each tab shows a small PDF glyph, the file name and + a close cross; the active tab is white, the others grey; a "+ Open" button + follows the last tab. +- Second row, the mode bar: the modes "Read", "Comment", "Fill & Sign", + "Pages", "Combine", "Edit" as text tabs, the current one in the accent + colour with a short underline; on the right a search field with a magnifier + and the placeholder "Search in document", then icon buttons for Undo and Redo, + and a "Save" button. +- Left: a narrow icon rail with four icons and tooltips' labels shown under + them in tiny text: "Pages", "Outline", "Comments", "Search". The selected + icon has a tinted square behind it. Next to the rail, an open panel about + 240 px wide when the screen calls for it. +- Centre: the canvas, a light grey desk with the document's pages stacked + vertically, centred, one page fully visible and the next starting below. +- Bottom centre of the canvas: a floating rounded pill with "‹", "3 / 24", + "›", a divider, "−", "100%", "+", and two small icons for "Fit width" and + "Fit page". +- Right: a panel about 300 px wide, only in screens that call for it. +- Bottom edge: a thin status line with "Saved" (with a small green dot) on the + left and "14.6 of 64 MB used" on the right. + +Use this exact fixture wherever it appears: +- Documents in the library: "Q3 2026 Board Report.pdf" (24 pages, 2.8 MB, + opened today), "Riverside Lease 2026.pdf" (12 pages, 1.1 MB, yesterday), + "Field Guide to Garden Birds.pdf" (7 pages, 16 KB, 3 Oct), "Invoice + INV-2041.pdf" (1 page, 84 KB, 1 Oct), "Site Survey Photos.pdf" (18 pages, + 9.6 MB, 28 Sep). +- The board report belongs to the fictional "Northwind Cooperative". Its + sections: Summary, Revenue, Operations, Outlook, Appendix. Page 3 is + "Revenue" with a short paragraph that includes the sentence "Revenue grew + 12% year over year, led by the Northwest region." and a simple bar chart + "Revenue by region, Q3 2026" with bars for Northwest, Coast, Valley, Metro. +- People: Maya Chen (the user), Jun Park and Ana Ruiz (reviewers). + +Render every piece of interface text and every control sharply enough to be +read and measured later. Labels, buttons, fields and panels will be rebuilt as +native components from this image; page contents are sample document art. + +This screen: mode "Comment". Under the mode bar, a slim tool row: "Highlight" +(selected), "Underline", "Strike out", "Note", "Text box", and a row of four +colour dots. On page 3 the sentence "Revenue grew 12% year over year, led by +the Northwest region." is highlighted in yellow with a small speech-bubble +marker in the margin. The right panel is open with the title "Comments" and +"3": a thread on the highlight. Jun Park (initials avatar), "Today 10:12": +"Can we add the regional breakdown table here?" A reply from Maya Chen, +"Today 10:20": "Added on page 4." A status chip "Accepted". At the bottom of +the thread, a reply field "Reply…" and a "Post" button. Below the thread, a +second, collapsed comment by Ana Ruiz on page 7. \ No newline at end of file diff --git a/apps/pdftools/design/source/05-pages.measured.json b/apps/pdftools/design/source/05-pages.measured.json new file mode 100644 index 00000000..73b1dee9 --- /dev/null +++ b/apps/pdftools/design/source/05-pages.measured.json @@ -0,0 +1,3546 @@ +{ + "schema_version": 1, + "image": "05-pages.png", + "image_sha256": "8c20451b5e17f7cbf663e89c94971dcb90cf3ef0e91b81f473247ebea99b0e31", + "script_sha256": "ea860507e32482b955d3ac577ab3cf9fc559f59ecc916c89175293932b54e0b7", + "size": [ + 1536, + 1024 + ], + "ocr_engine": "Apple Vision VNRecognizeTextRequest accurate, en-US (App Flow ocr.swift)", + "text": [ + { + "text": "Q3 2026 Board Report.pdf", + "bounds": [ + 46.9, + 17.8, + 151.8, + 15.6 + ], + "confidence": 1, + "ink": "#58595c", + "ground": "#f6f6f6" + }, + { + "text": "Riverside Lease 2026.pdf", + "bounds": [ + 296.9, + 17.8, + 138.4, + 15.6 + ], + "confidence": 1, + "ink": "#898b8f", + "ground": "#f2f2f3" + }, + { + "text": "Field Guide to Garden Birds.pdf", + "bounds": [ + 533.6, + 20.0, + 178.6, + 13.5 + ], + "confidence": 1, + "ink": "#878a8e", + "ground": "#f1f1f2" + }, + { + "text": "X", + "bounds": [ + 712.2, + 20.1, + 13.4, + 11.2 + ], + "confidence": 1, + "ink": "#73757c", + "ground": "#f1f1f2" + }, + { + "text": "Invoice INV-2041.pdf", + "bounds": [ + 772.5, + 17.8, + 118.3, + 15.6 + ], + "confidence": 1, + "ink": "#83868a", + "ground": "#f1f1f2" + }, + { + "text": "Read", + "bounds": [ + 35.7, + 69.2, + 35.7, + 13.4 + ], + "confidence": 1, + "ink": "#585c63", + "ground": "#f1f1f2" + }, + { + "text": "Comment", + "bounds": [ + 122.6, + 68.1, + 62.9, + 15.6 + ], + "confidence": 1, + "ink": "#6a6e74", + "ground": "#f8f8f9" + }, + { + "text": "Fill & Sign", + "bounds": [ + 236.7, + 69.0, + 64.7, + 16.0 + ], + "confidence": 1, + "ink": "#65696f", + "ground": "#f6f6f7" + }, + { + "text": "Pages", + "bounds": [ + 352.7, + 69.2, + 40.2, + 17.8 + ], + "confidence": 1, + "ink": "#3a80de", + "ground": "#f4f8fc" + }, + { + "text": "Combine", + "bounds": [ + 451.0, + 69.2, + 55.8, + 15.6 + ], + "confidence": 1, + "ink": "#6b6f75", + "ground": "#f7f7f8" + }, + { + "text": "Edit", + "bounds": [ + 560.4, + 69.2, + 29.0, + 13.4 + ], + "confidence": 1, + "ink": "#5f6268", + "ground": "#f4f4f5" + }, + { + "text": "2 selected", + "bounds": [ + 113.8, + 122.5, + 67.1, + 13.8 + ], + "confidence": 1, + "ink": "#337ce0", + "ground": "#f2f7fc" + }, + { + "text": "Rotate left", + "bounds": [ + 294.7, + 122.6, + 58.1, + 13.6 + ], + "confidence": 1, + "ink": "#828488", + "ground": "#f7f7f7" + }, + { + "text": "Rotate right", + "bounds": [ + 433.0, + 124.6, + 67.1, + 14.1 + ], + "confidence": 1, + "ink": "#828488", + "ground": "#f7f7f8" + }, + { + "text": "Pages", + "bounds": [ + 31.3, + 162.9, + 29.0, + 13.4 + ], + "confidence": 1, + "ink": "#4882e2", + "ground": "#e1eefb" + }, + { + "text": "Outline", + "bounds": [ + 29.0, + 247.6, + 35.7, + 11.2 + ], + "confidence": 1, + "ink": "#818489", + "ground": "#f6f6f7" + }, + { + "text": "Comments", + "bounds": [ + 20.1, + 330.2, + 51.3, + 11.2 + ], + "confidence": 1, + "ink": "#808287", + "ground": "#f7f7f8" + }, + { + "text": "Search", + "bounds": [ + 29.0, + 408.3, + 33.5, + 11.2 + ], + "confidence": 1, + "ink": "#818388", + "ground": "#f7f7f7" + }, + { + "text": "NORTHWIND", + "bounds": [ + 136.2, + 198.6, + 80.4, + 11.2 + ], + "confidence": 1, + "ink": "#444649", + "ground": "#eeefef" + }, + { + "text": "COOPERATIVE", + "bounds": [ + 136.2, + 207.5, + 60.3, + 11.2 + ], + "confidence": 1, + "ink": "#828385", + "ground": "#f4f4f5" + }, + { + "text": "Q3 2026", + "bounds": [ + 138.4, + 236.0, + 60.3, + 16.1 + ], + "confidence": 1, + "ink": "#484b4f", + "ground": "#f3f3f4" + }, + { + "text": "BOARD REPORT", + "bounds": [ + 138.4, + 256.6, + 113.9, + 13.4 + ], + "confidence": 1, + "ink": "#46494e", + "ground": "#efeff0" + }, + { + "text": "Bulding stronger communities", + "bounds": [ + 136.2, + 278.9, + 100.5, + 13.4 + ], + "confidence": 1, + "ink": "#a1a2a5", + "ground": "#f8f8f9" + }, + { + "text": "trouch sustanable growt", + "bounds": [ + 136.2, + 294.0, + 89.3, + 5 + ], + "confidence": 1, + "ink": "#9b9c9f", + "ground": "#f1f2f2" + }, + { + "text": "Prnwet te the suam e hnwte", + "bounds": [ + 136.2, + 392.6, + 89.3, + 4.5 + ], + "confidence": 0.3, + "ink": "#a7a8aa", + "ground": "#f3f4f4" + }, + { + "text": "Oxicber 20,2006", + "bounds": [ + 136.2, + 399.0, + 51.3, + 11.5 + ], + "confidence": 1, + "ink": "#a9aaad", + "ground": "#f8f8f9" + }, + { + "text": "SUMMARY (CONT.)", + "bounds": [ + 138.4, + 486.0, + 89.3, + 11.5 + ], + "confidence": 1, + "ink": "#5d5e60", + "ground": "#f3f3f3" + }, + { + "text": "Cur hnandal posibon remains strong.", + "bounds": [ + 138.4, + 513.0, + 127.3, + 7 + ], + "confidence": 1, + "ink": "#9c9d9f", + "ground": "#f4f5f5" + }, + { + "text": "with consteyative lewerace and", + "bounds": [ + 140.7, + 524.0, + 104.9, + 5 + ], + "confidence": 1, + "ink": "#9c9d9f", + "ground": "#f2f2f3" + }, + { + "text": "healthy bquidity.", + "bounds": [ + 138.4, + 535.0, + 58.0, + 7.1 + ], + "confidence": 1, + "ink": "#a2a2a4", + "ground": "#f4f4f4" + }, + { + "text": "We thank our members. enoloymed", + "bounds": [ + 138.4, + 555.5, + 120.6, + 6.7 + ], + "confidence": 1, + "ink": "#99999c", + "ground": "#f5f6f6" + }, + { + "text": "Sod nirinkeiir lhat fontanune Inkt", + "bounds": [ + 138.4, + 568.9, + 127.3, + 4.5 + ], + "confidence": 0.3, + "ink": "#99999b", + "ground": "#eeeeef" + }, + { + "text": "and support.", + "bounds": [ + 138.4, + 577.8, + 46.9, + 11.2 + ], + "confidence": 1, + "ink": "#a4a4a8", + "ground": "#f9f9fa" + }, + { + "text": "208", + "bounds": [ + 158.5, + 613.5, + 84.8, + 37.9 + ], + "confidence": 0.3, + "ink": "#588eda", + "ground": "#f9fafd" + }, + { + "text": "NORTHATIO COOPORATME", + "bounds": [ + 134.0, + 669.0, + 64.7, + 11.4 + ], + "confidence": 1, + "ink": "#b4b4b6", + "ground": "#f6f6f6" + }, + { + "text": "03 368 BOAAO BCPORT", + "bounds": [ + 230.0, + 669.0, + 55.8, + 11.4 + ], + "confidence": 0.5, + "ink": "#b4b4b7", + "ground": "#f6f6f6" + }, + { + "text": "METRO REGION", + "bounds": [ + 140.7, + 738.0, + 71.4, + 12 + ], + "confidence": 1, + "ink": "#5b5c5e", + "ground": "#f3f3f4" + }, + { + "text": "The Metro region remains cestient", + "bounds": [ + 140.7, + 758.5, + 109.4, + 11.5 + ], + "confidence": 1, + "ink": "#aaabae", + "ground": "#f8f8f8" + }, + { + "text": "with steady performance.", + "bounds": [ + 140.7, + 771.9, + 80.4, + 7.1 + ], + "confidence": 1, + "ink": "#a3a3a6", + "ground": "#f4f4f5" + }, + { + "text": "Key metrics", + "bounds": [ + 142.9, + 789.8, + 38.0, + 11.2 + ], + "confidence": 1, + "ink": "#898b8d", + "ground": "#eeeff0" + }, + { + "text": "QJ 2026", + "bounds": [ + 232.2, + 789.8, + 26.8, + 11.2 + ], + "confidence": 1, + "ink": "#808286", + "ground": "#efeff0" + }, + { + "text": "Reverue (USO miliona)", + "bounds": [ + 142.9, + 803.0, + 71.4, + 11.3 + ], + "confidence": 1, + "ink": "#a9a9ac", + "ground": "#f5f6f6" + }, + { + "text": "153", + "bounds": [ + 241.1, + 803.1, + 17.9, + 11.2 + ], + "confidence": 1, + "ink": "#97989b", + "ground": "#f5f5f6" + }, + { + "text": "Moribers", + "bounds": [ + 142.9, + 816.5, + 29.0, + 11.2 + ], + "confidence": 1, + "ink": "#9fa0a2", + "ground": "#f5f5f5" + }, + { + "text": "8,101", + "bounds": [ + 236.7, + 816.5, + 22.3, + 11.2 + ], + "confidence": 1, + "ink": "#949497", + "ground": "#f6f7f7" + }, + { + "text": "Gronth Nun)", + "bounds": [ + 140.6, + 829.7, + 44.8, + 11.6 + ], + "confidence": 1, + "ink": "#aeaeb0", + "ground": "#f4f5f5" + }, + { + "text": "We will contieue to focus on service", + "bounds": [ + 140.7, + 852.0, + 107.2, + 11.4 + ], + "confidence": 1, + "ink": "#acacaf", + "ground": "#f9f9f9" + }, + { + "text": "outty mo coe oni etceme", + "bounds": [ + 138.4, + 865.6, + 102.7, + 4.5 + ], + "confidence": 0.3, + "ink": "#a7a7aa", + "ground": "#f1f1f1" + }, + { + "text": "MwA\u043e\u0447\u0438\u043c", + "bounds": [ + 131.7, + 901.3, + 62.5, + 4.5 + ], + "confidence": 0.3, + "ink": "#acadaf", + "ground": "#f5f6f6" + }, + { + "text": "01 30N MAG MAOH", + "bounds": [ + 227.7, + 901.0, + 51.3, + 5 + ], + "confidence": 0.3, + "ink": "#afb0b4", + "ground": "#f4f4f4" + }, + { + "text": "13", + "bounds": [ + 192.0, + 925.8, + 17.9, + 13.4 + ], + "confidence": 1, + "ink": "#67696e", + "ground": "#f2f2f3" + }, + { + "text": "SUMMARY", + "bounds": [ + 377.3, + 191.9, + 49.1, + 11.2 + ], + "confidence": 1, + "ink": "#5f6063", + "ground": "#f4f4f4" + }, + { + "text": "Northwind Cooperative delivered another", + "bounds": [ + 377.3, + 214.0, + 136.2, + 11.3 + ], + "confidence": 1, + "ink": "#98989a", + "ground": "#f8f8f8" + }, + { + "text": "strong quarter, with sclid growth across", + "bounds": [ + 375.1, + 223.0, + 134.0, + 13.5 + ], + "confidence": 1, + "ink": "#9f9fa1", + "ground": "#f9f9f9" + }, + { + "text": "all key metrics and continued progress", + "bounds": [ + 377.3, + 236.0, + 125.0, + 12 + ], + "confidence": 1, + "ink": "#9c9c9e", + "ground": "#f7f7f8" + }, + { + "text": "on our strategic priorities.", + "bounds": [ + 375.1, + 247.6, + 87.1, + 11.4 + ], + "confidence": 1, + "ink": "#9a9b9c", + "ground": "#f8f8f8" + }, + { + "text": "\u2022 Revenue increased 12% year ovse year", + "bounds": [ + 377.3, + 265.0, + 134.0, + 14 + ], + "confidence": 1, + "ink": "#a0a1a3", + "ground": "#f9fafa" + }, + { + "text": "\u2022 Operacingiocome up 9%", + "bounds": [ + 377.3, + 281.0, + 89.3, + 11.3 + ], + "confidence": 1, + "ink": "#a4a4a6", + "ground": "#f8f8f9" + }, + { + "text": "\u2022 Membership grew to 48,732", + "bounds": [ + 377.3, + 294.0, + 102.7, + 12 + ], + "confidence": 1, + "ink": "#929395", + "ground": "#f8f8f8" + }, + { + "text": "\u2022Strong balance sheet and cash position", + "bounds": [ + 375.1, + 310.0, + 138.4, + 11.3 + ], + "confidence": 1, + "ink": "#a0a1a3", + "ground": "#f8f8f8" + }, + { + "text": "ve reman lotinn on couro", + "bounds": [ + 375.1, + 334.6, + 111.6, + 4.5 + ], + "confidence": 0.3, + "ink": "#a1a2a4", + "ground": "#eeeeef" + }, + { + "text": "ethellnca. mimiwrwilln.and", + "bounds": [ + 375.1, + 345.8, + 100.5, + 4.5 + ], + "confidence": 0.3, + "ink": "#9b9c9e", + "ground": "#ececed" + }, + { + "text": "respons ble growth", + "bounds": [ + 375.1, + 356.9, + 64.7, + 7.1 + ], + "confidence": 1, + "ink": "#9c9c9e", + "ground": "#f5f5f5" + }, + { + "text": "MoTwMAntoNYETM", + "bounds": [ + 366.1, + 403.8, + 64.7, + 4.5 + ], + "confidence": 0.3, + "ink": "#afafb2", + "ground": "#f5f5f5" + }, + { + "text": "QI 300S BOMAO BERORT", + "bounds": [ + 466.6, + 401.6, + 58.0, + 11.4 + ], + "confidence": 0.3, + "ink": "#b0b0b2", + "ground": "#fafafa" + }, + { + "text": "FINANCIAL HIGHLIGHTS", + "bounds": [ + 366.1, + 486.0, + 111.6, + 11.5 + ], + "confidence": 1, + "ink": "#5d5f62", + "ground": "#f3f4f4" + }, + { + "text": "Revenue", + "bounds": [ + 363.9, + 528.7, + 33.5, + 8.9 + ], + "confidence": 1, + "ink": "#9c9c9f", + "ground": "#f8f8f8" + }, + { + "text": "Operating income", + "bounds": [ + 366.1, + 546.6, + 60.3, + 11.4 + ], + "confidence": 1, + "ink": "#a7a7aa", + "ground": "#f9f9f9" + }, + { + "text": "Net income", + "bounds": [ + 366.1, + 566.7, + 40.2, + 11.3 + ], + "confidence": 1, + "ink": "#979799", + "ground": "#f8f8f9" + }, + { + "text": "Operating margin", + "bounds": [ + 363.9, + 586.7, + 62.5, + 11.3 + ], + "confidence": 1, + "ink": "#a5a5a8", + "ground": "#f8f8f9" + }, + { + "text": "Members", + "bounds": [ + 363.9, + 604.6, + 35.7, + 11.2 + ], + "confidence": 1, + "ink": "#959597", + "ground": "#f7f8f8" + }, + { + "text": "Equity ratio", + "bounds": [ + 363.9, + 624.7, + 42.4, + 11.3 + ], + "confidence": 1, + "ink": "#a1a1a4", + "ground": "#f8f8f8" + }, + { + "text": "$84.SM", + "bounds": [ + 464.4, + 526.5, + 31.3, + 11.2 + ], + "confidence": 1, + "ink": "#8a8b8e", + "ground": "#f7f7f7" + }, + { + "text": "SIS.SM", + "bounds": [ + 464.4, + 546.6, + 31.3, + 11.2 + ], + "confidence": 1, + "ink": "#9a9b9d", + "ground": "#f9f9f9" + }, + { + "text": "$11.3M", + "bounds": [ + 464.4, + 566.7, + 31.3, + 8.9 + ], + "confidence": 1, + "ink": "#8e8f92", + "ground": "#f7f7f7" + }, + { + "text": "48,732", + "bounds": [ + 466.6, + 604.6, + 26.8, + 11.2 + ], + "confidence": 1, + "ink": "#949598", + "ground": "#f8f8f8" + }, + { + "text": "42.3%", + "bounds": [ + 468.8, + 624.7, + 26.8, + 8.9 + ], + "confidence": 1, + "ink": "#8a8a8e", + "ground": "#f6f7f7" + }, + { + "text": "0120% BONO NEROEE", + "bounds": [ + 448.7, + 669.0, + 55.8, + 11.4 + ], + "confidence": 0.3, + "ink": "#b8b8ba", + "ground": "#f7f7f7" + }, + { + "text": "BALANCE SHEET", + "bounds": [ + 361.7, + 740.7, + 80.4, + 11.3 + ], + "confidence": 1, + "ink": "#616265", + "ground": "#f4f4f4" + }, + { + "text": "As of Sep 30, 2025 (USD milions", + "bounds": [ + 361.7, + 760.7, + 102.7, + 6.7 + ], + "confidence": 1, + "ink": "#a2a2a6", + "ground": "#f5f5f5" + }, + { + "text": "Total assets (", + "bounds": [ + 361.7, + 776.0, + 38.6, + 9.3 + ], + "confidence": 1, + "ink": "#a9a9ab", + "ground": "#f8f8f9" + }, + { + "text": "Tocal Rabildies", + "bounds": [ + 361.7, + 792.0, + 46.9, + 11.2 + ], + "confidence": 1, + "ink": "#adadaf", + "ground": "#f8f8f9" + }, + { + "text": "Tocal equity", + "bounds": [ + 361.7, + 809.8, + 38.0, + 11.2 + ], + "confidence": 1, + "ink": "#a8a8ab", + "ground": "#f8f8f8" + }, + { + "text": "Equity ratio", + "bounds": [ + 361.7, + 825.0, + 38.0, + 12 + ], + "confidence": 1, + "ink": "#a8a8ab", + "ground": "#f8f8f8" + }, + { + "text": "Cosh and equivalents", + "bounds": [ + 361.7, + 841.0, + 67.0, + 11.2 + ], + "confidence": 1, + "ink": "#b0b0b4", + "ground": "#f9f9fa" + }, + { + "text": "Our bslance sheet remains strong", + "bounds": [ + 361.7, + 861.0, + 104.9, + 7 + ], + "confidence": 1, + "ink": "#a6a7aa", + "ground": "#f5f5f6" + }, + { + "text": "with ample liquidity.", + "bounds": [ + 361.7, + 867.8, + 67.0, + 11.2 + ], + "confidence": 1, + "ink": "#a7a8aa", + "ground": "#f8f8f9" + }, + { + "text": "180.3", + "bounds": [ + 468.8, + 792.0, + 24.6, + 11.2 + ], + "confidence": 1, + "ink": "#929397", + "ground": "#f9f9f9" + }, + { + "text": "132.3", + "bounds": [ + 468.8, + 807.6, + 24.6, + 11.2 + ], + "confidence": 1, + "ink": "#9b9b9f", + "ground": "#f8f8f8" + }, + { + "text": "42,3%", + "bounds": [ + 468.8, + 825.4, + 22.3, + 8.9 + ], + "confidence": 1, + "ink": "#a4a5aa", + "ground": "#f6f6f6" + }, + { + "text": "38.7", + "bounds": [ + 471.1, + 841.1, + 20.1, + 8.9 + ], + "confidence": 1, + "ink": "#a9aaae", + "ground": "#fafbfb" + }, + { + "text": "Delete", + "bounds": [ + 598.3, + 124.9, + 35.7, + 11.2 + ], + "confidence": 1, + "ink": "#7f8287", + "ground": "#f5f5f6" + }, + { + "text": "Extract", + "bounds": [ + 710.0, + 124.9, + 42.4, + 11.2 + ], + "confidence": 1, + "ink": "#73767b", + "ground": "#f4f4f5" + }, + { + "text": "REVENUE", + "bounds": [ + 605.0, + 191.9, + 46.9, + 8.9 + ], + "confidence": 1, + "ink": "#505153", + "ground": "#efeff0" + }, + { + "text": "Revenue grew 12% year over year, led by", + "bounds": [ + 602.8, + 211.9, + 149.6, + 13.4 + ], + "confidence": 1, + "ink": "#8d8757", + "ground": "#f9f4b1" + }, + { + "text": "the rortnwest fesion,", + "bounds": [ + 605.0, + 227.6, + 75.9, + 6.7 + ], + "confidence": 1, + "ink": "#9c9da0", + "ground": "#f4f5f5" + }, + { + "text": "Revenue by region, Q3 2026", + "bounds": [ + 602.8, + 243.0, + 122.8, + 11.3 + ], + "confidence": 1, + "ink": "#727376", + "ground": "#f6f6f6" + }, + { + "text": "18.7", + "bounds": [ + 689.9, + 287.8, + 17.9, + 8.9 + ], + "confidence": 1, + "ink": "#8f8f92", + "ground": "#f8f8f8" + }, + { + "text": "15.3", + "bounds": [ + 727.8, + 294.5, + 20.1, + 11.2 + ], + "confidence": 1, + "ink": "#818289", + "ground": "#f7f7f8" + }, + { + "text": "Northnest", + "bounds": [ + 602.8, + 336.9, + 38.0, + 11.2 + ], + "confidence": 1, + "ink": "#9c9da0", + "ground": "#f7f8f8" + }, + { + "text": "Coast", + "bounds": [ + 649.7, + 336.9, + 22.3, + 11.2 + ], + "confidence": 1, + "ink": "#a1a2a5", + "ground": "#fafafa" + }, + { + "text": "vaiey", + "bounds": [ + 687.6, + 341.3, + 20.1, + 4.5 + ], + "confidence": 0.3, + "ink": "#a7a8ac", + "ground": "#f4f5f5" + }, + { + "text": "browth was brosd-based, with Strong", + "bounds": [ + 602.8, + 359.0, + 129.5, + 7 + ], + "confidence": 1, + "ink": "#98989a", + "ground": "#f3f3f4" + }, + { + "text": "contoutsons trom core busiesses and", + "bounds": [ + 602.8, + 370.0, + 134.0, + 5 + ], + "confidence": 1, + "ink": "#a0a0a2", + "ground": "#f0f1f1" + }, + { + "text": "new member sdootion", + "bounds": [ + 602.8, + 381.0, + 80.4, + 5 + ], + "confidence": 1, + "ink": "#98989b", + "ground": "#f4f4f4" + }, + { + "text": "Metro", + "bounds": [ + 725.6, + 336.9, + 24.6, + 11.2 + ], + "confidence": 1, + "ink": "#a0a2a6", + "ground": "#f9f9fa" + }, + { + "text": "MMTNAATNNAINI", + "bounds": [ + 596.1, + 403.8, + 67.0, + 4.5 + ], + "confidence": 0.3, + "ink": "#b1b2b5", + "ground": "#f5f4f5" + }, + { + "text": "Q1 2005 BOMO ICPON!", + "bounds": [ + 703.3, + 403.8, + 58.0, + 6.7 + ], + "confidence": 1, + "ink": "#b0b0b3", + "ground": "#f7f7f7" + }, + { + "text": "3", + "bounds": [ + 674.2, + 435.0, + 11.2, + 13.4 + ], + "confidence": 1, + "ink": "#616369", + "ground": "#f3f3f4" + }, + { + "text": "REVENUE TRENDS", + "bounds": [ + 576.0, + 486.0, + 89.3, + 11.5 + ], + "confidence": 1, + "ink": "#5b5d60", + "ground": "#f3f3f3" + }, + { + "text": "Revenue (USO matiers)", + "bounds": [ + 576.0, + 508.7, + 75.9, + 9.3 + ], + "confidence": 1, + "ink": "#9d9ea1", + "ground": "#f7f7f8" + }, + { + "text": "75.4", + "bounds": [ + 663.1, + 526.5, + 17.9, + 11.2 + ], + "confidence": 1, + "ink": "#8d8f96", + "ground": "#f9f9fa" + }, + { + "text": "62.1", + "bounds": [ + 584.9, + 542.1, + 20.1, + 11.2 + ], + "confidence": 1, + "ink": "#8e8e93", + "ground": "#fafafa" + }, + { + "text": "q4 2025", + "bounds": [ + 580.5, + 589.0, + 29.0, + 11.2 + ], + "confidence": 1, + "ink": "#a1a1a4", + "ground": "#f6f6f6" + }, + { + "text": "012009", + "bounds": [ + 620.7, + 593.4, + 24.6, + 4.5 + ], + "confidence": 1, + "ink": "#a3a4a7", + "ground": "#f5f5f5" + }, + { + "text": "022026", + "bounds": [ + 658.6, + 593.4, + 29.0, + 4.5 + ], + "confidence": 1, + "ink": "#97989b", + "ground": "#f2f2f2" + }, + { + "text": "0 2035", + "bounds": [ + 692.1, + 593.0, + 29.0, + 5 + ], + "confidence": 0.5, + "ink": "#979799", + "ground": "#f3f4f4" + }, + { + "text": "Revenue has grown steadily over the", + "bounds": [ + 576.0, + 611.0, + 131.7, + 11.4 + ], + "confidence": 1, + "ink": "#999a9d", + "ground": "#f8f8f8" + }, + { + "text": "past four quartors, reflecting strong", + "bounds": [ + 576.0, + 622.0, + 129.5, + 12 + ], + "confidence": 1, + "ink": "#98999b", + "ground": "#f7f7f8" + }, + { + "text": "membor demano and ofta tonal", + "bounds": [ + 576.0, + 638.0, + 118.3, + 5 + ], + "confidence": 1, + "ink": "#a3a3a6", + "ground": "#f2f3f3" + }, + { + "text": "execution", + "bounds": [ + 576.0, + 649.2, + 35.7, + 4.5 + ], + "confidence": 1, + "ink": "#97979a", + "ground": "#ececed" + }, + { + "text": "CASH FLOW", + "bounds": [ + 573.7, + 740.4, + 55.9, + 11.8 + ], + "confidence": 1, + "ink": "#67696b", + "ground": "#f5f5f6" + }, + { + "text": "Operating activties", + "bounds": [ + 571.5, + 776.0, + 62.5, + 12 + ], + "confidence": 1, + "ink": "#aaaaac", + "ground": "#f9f9f9" + }, + { + "text": "14.2", + "bounds": [ + 683.2, + 776.4, + 20.1, + 11.2 + ], + "confidence": 1, + "ink": "#8f9196", + "ground": "#f8f8f9" + }, + { + "text": "Investing activities", + "bounds": [ + 571.5, + 794.0, + 60.3, + 11.4 + ], + "confidence": 1, + "ink": "#b2b2b5", + "ground": "#f8f9f9" + }, + { + "text": "Financing activities", + "bounds": [ + 571.5, + 809.8, + 62.5, + 11.2 + ], + "confidence": 1, + "ink": "#b1b1b4", + "ground": "#f8f8f9" + }, + { + "text": "(3.2)", + "bounds": [ + 683.2, + 809.8, + 20.1, + 11.2 + ], + "confidence": 1, + "ink": "#bba1a7", + "ground": "#faf9f9" + }, + { + "text": "intreste in ca", + "bounds": [ + 571.5, + 829.9, + 62.5, + 4.5 + ], + "confidence": 0.3, + "ink": "#ababaf", + "ground": "#f3f3f4" + }, + { + "text": "4,9", + "bounds": [ + 685.4, + 825.4, + 15.6, + 11.2 + ], + "confidence": 1, + "ink": "#95969b", + "ground": "#f8f8f8" + }, + { + "text": "Cath at end of period", + "bounds": [ + 571.5, + 841.0, + 69.2, + 11.2 + ], + "confidence": 1, + "ink": "#a9aaae", + "ground": "#f8f8f9" + }, + { + "text": "38.7", + "bounds": [ + 683.2, + 841.1, + 20.1, + 11.2 + ], + "confidence": 1, + "ink": "#939599", + "ground": "#fafafa" + }, + { + "text": "Soong cash generation supports our", + "bounds": [ + 571.5, + 858.9, + 118.3, + 11.2 + ], + "confidence": 1, + "ink": "#b0b0b4", + "ground": "#f8f9f9" + }, + { + "text": "investment and member commitments.", + "bounds": [ + 571.5, + 870.0, + 127.3, + 9 + ], + "confidence": 1, + "ink": "#a2a2a6", + "ground": "#f7f7f8" + }, + { + "text": "\u041c\u0418\u0422\u0423\u041b\u0417\u041e\u0421\u0425A\u0422M", + "bounds": [ + 355.0, + 901.3, + 60.3, + 4.5 + ], + "confidence": 0.3, + "ink": "#b3b3b7", + "ground": "#f7f7f7" + }, + { + "text": "14", + "bounds": [ + 413.0, + 925.8, + 17.9, + 13.4 + ], + "confidence": 1, + "ink": "#7f8287", + "ground": "#f7f7f8" + }, + { + "text": "NO\u0438\u0442 C\u043e\u0435CR\u0438\u043c", + "bounds": [ + 564.8, + 896.8, + 64.7, + 9.2 + ], + "confidence": 0.3, + "ink": "#b5b6ba", + "ground": "#f6f7f7" + }, + { + "text": "15", + "bounds": [ + 558.1, + 925.8, + 22.3, + 15.6 + ], + "confidence": 1, + "ink": "#7e8187", + "ground": "#f6f6f7" + }, + { + "text": "01300% BOMO ACAORT", + "bounds": [ + 656.4, + 896.8, + 58.0, + 9.2 + ], + "confidence": 1, + "ink": "#b3b4b7", + "ground": "#f7f7f7" + }, + { + "text": "Insert from file", + "bounds": [ + 828.3, + 122.7, + 80.4, + 13.4 + ], + "confidence": 1, + "ink": "#7b7d81", + "ground": "#f5f6f6" + }, + { + "text": "OPERATIONS", + "bounds": [ + 848.4, + 191.9, + 62.5, + 11.2 + ], + "confidence": 1, + "ink": "#5b5c5f", + "ground": "#f3f3f3" + }, + { + "text": "We continued to improve operational", + "bounds": [ + 850.6, + 214.0, + 122.8, + 11.3 + ], + "confidence": 1, + "ink": "#949598", + "ground": "#f8f8f8" + }, + { + "text": "etficiency and service reEability.", + "bounds": [ + 848.4, + 225.0, + 107.2, + 11.5 + ], + "confidence": 1, + "ink": "#9f9fa1", + "ground": "#f7f7f8" + }, + { + "text": "\u2022 Reliablity: 99.2% system uptime", + "bounds": [ + 848.4, + 243.0, + 122.8, + 11.3 + ], + "confidence": 1, + "ink": "#a3a4a6", + "ground": "#f8f9f9" + }, + { + "text": "Average responte time improved 8%", + "bounds": [ + 859.5, + 258.8, + 127.3, + 13.4 + ], + "confidence": 1, + "ink": "#a3a4a6", + "ground": "#f9f9fa" + }, + { + "text": "\u2022 Completed 12 infrastructure upgrades", + "bounds": [ + 848.4, + 274.0, + 142.9, + 14 + ], + "confidence": 1, + "ink": "#9d9d9f", + "ground": "#f9f9f9" + }, + { + "text": "\u2022 Safety incident rate declined 15%", + "bounds": [ + 848.4, + 290.0, + 129.5, + 13.4 + ], + "confidence": 1, + "ink": "#a5a5a8", + "ground": "#fafafa" + }, + { + "text": "Our teams remain comeitted to delivering", + "bounds": [ + 848.4, + 316.8, + 140.7, + 11.2 + ], + "confidence": 1, + "ink": "#a5a6a9", + "ground": "#f8f8f9" + }, + { + "text": "sale, rellable service wtule managing", + "bounds": [ + 848.4, + 332.0, + 120.6, + 7.1 + ], + "confidence": 1, + "ink": "#a0a0a3", + "ground": "#f5f5f6" + }, + { + "text": "NORTAMINO COCFERATME", + "bounds": [ + 839.4, + 401.6, + 69.2, + 11.4 + ], + "confidence": 1, + "ink": "#b2b2b6", + "ground": "#f9fafa" + }, + { + "text": "0)30% BOMIO BUGAT", + "bounds": [ + 946.6, + 399.0, + 60.3, + 14 + ], + "confidence": 0.5, + "ink": "#bdbdbf", + "ground": "#fbfbfb" + }, + { + "text": "NORTHWEST REGION", + "bounds": [ + 835.0, + 486.0, + 104.9, + 11.5 + ], + "confidence": 1, + "ink": "#5f6062", + "ground": "#f5f5f6" + }, + { + "text": "The Northwest region led growth.", + "bounds": [ + 837.2, + 508.7, + 113.9, + 11.3 + ], + "confidence": 1, + "ink": "#9a999d", + "ground": "#f7f7f8" + }, + { + "text": "driven by new members and", + "bounds": [ + 835.0, + 519.8, + 100.5, + 11.2 + ], + "confidence": 1, + "ink": "#a4a4a6", + "ground": "#f7f7f8" + }, + { + "text": "increased utace", + "bounds": [ + 835.0, + 535.0, + 58.0, + 5 + ], + "confidence": 1, + "ink": "#98989a", + "ground": "#f1f2f1" + }, + { + "text": "hcy metn", + "bounds": [ + 839.4, + 555.5, + 40.2, + 4.5 + ], + "confidence": 0.3, + "ink": "#78797b", + "ground": "#e3e4e5" + }, + { + "text": "Revenue (USD miliona)", + "bounds": [ + 841.7, + 568.9, + 73.7, + 11.2 + ], + "confidence": 1, + "ink": "#a7a6a8", + "ground": "#f8f8f8" + }, + { + "text": "Menbers", + "bounds": [ + 839.4, + 584.5, + 33.5, + 11.2 + ], + "confidence": 1, + "ink": "#aaaaac", + "ground": "#f6f6f7" + }, + { + "text": "onathiYay", + "bounds": [ + 839.4, + 604.6, + 44.7, + 4.5 + ], + "confidence": 0.3, + "ink": "#a4a4a7", + "ground": "#f2f3f3" + }, + { + "text": "Q3 2026", + "bounds": [ + 937.7, + 551.0, + 29.0, + 11.2 + ], + "confidence": 1, + "ink": "#797c82", + "ground": "#eff0f1" + }, + { + "text": "28.4", + "bounds": [ + 948.8, + 568.9, + 20.1, + 8.9 + ], + "confidence": 1, + "ink": "#8b8b8e", + "ground": "#f8f8f8" + }, + { + "text": "16,842", + "bounds": [ + 939.9, + 584.5, + 26.8, + 11.2 + ], + "confidence": 1, + "ink": "#909093", + "ground": "#f6f6f6" + }, + { + "text": "149", + "bounds": [ + 948.8, + 600.1, + 17.9, + 11.2 + ], + "confidence": 1, + "ink": "#9d9da0", + "ground": "#f6f6f6" + }, + { + "text": "Knntinuin inuwetment in in tasinichire", + "bounds": [ + 835.0, + 626.9, + 131.7, + 4.5 + ], + "confidence": 1, + "ink": "#98989b", + "ground": "#f1f1f1" + }, + { + "text": "and member procrams wh supoor", + "bounds": [ + 835.0, + 638.0, + 118.3, + 5 + ], + "confidence": 1, + "ink": "#9d9ea1", + "ground": "#f3f3f4" + }, + { + "text": "sustained growth.", + "bounds": [ + 835.0, + 647.0, + 64.7, + 11.2 + ], + "confidence": 1, + "ink": "#a6a6a9", + "ground": "#f8f8f8" + }, + { + "text": "THANO COOHRAY", + "bounds": [ + 828.3, + 673.7, + 64.7, + 4.5 + ], + "confidence": 0.3, + "ink": "#b1b2b4", + "ground": "#f3f3f4" + }, + { + "text": "10", + "bounds": [ + 897.5, + 700.5, + 17.9, + 11.2 + ], + "confidence": 1, + "ink": "#75787c", + "ground": "#f6f6f7" + }, + { + "text": "<", + "bounds": [ + 638.5, + 932.5, + 13.4, + 15.6 + ], + "confidence": 1, + "ink": "#565a62", + "ground": "#fafafa" + }, + { + "text": "3 / 24", + "bounds": [ + 676.5, + 934.8, + 40.2, + 13.4 + ], + "confidence": 1, + "ink": "#84878d", + "ground": "#fafafa" + }, + { + "text": ">", + "bounds": [ + 745.7, + 932.5, + 15.6, + 17.8 + ], + "confidence": 1, + "ink": "#545861", + "ground": "#fbfbfb" + }, + { + "text": "-", + "bounds": [ + 808.2, + 934.8, + 17.9, + 11.2 + ], + "confidence": 1, + "ink": "#70747b", + "ground": "#fbfbfb" + }, + { + "text": "100%", + "bounds": [ + 850.6, + 937.0, + 31.3, + 11.2 + ], + "confidence": 1, + "ink": "#7c7f84", + "ground": "#f5f5f6" + }, + { + "text": "+", + "bounds": [ + 897.5, + 932.5, + 20.1, + 20.1 + ], + "confidence": 1, + "ink": "#6d7177", + "ground": "#fbfbfb" + }, + { + "text": "Site Survey Photos.pdf", + "bounds": [ + 993.5, + 17.8, + 125.0, + 15.6 + ], + "confidence": 1, + "ink": "#84878c", + "ground": "#f1f1f2" + }, + { + "text": "+ Open", + "bounds": [ + 1196.7, + 17.8, + 51.3, + 15.6 + ], + "confidence": 1, + "ink": "#75797f", + "ground": "#f2f2f3" + }, + { + "text": "\u2022 Search in document", + "bounds": [ + 1049.3, + 69.0, + 136.2, + 14 + ], + "confidence": 1, + "ink": "#888b90", + "ground": "#f7f8f8" + }, + { + "text": "OUTLOOK", + "bounds": [ + 1089.5, + 191.9, + 49.1, + 11.2 + ], + "confidence": 1, + "ink": "#696b6e", + "ground": "#f7f7f7" + }, + { + "text": "We are optmistic about the remainde", + "bounds": [ + 1089.5, + 216.0, + 125.0, + 7.1 + ], + "confidence": 1, + "ink": "#939497", + "ground": "#f4f4f5" + }, + { + "text": "of 2026 and boyond.", + "bounds": [ + 1089.5, + 225.0, + 71.4, + 11.5 + ], + "confidence": 1, + "ink": "#9e9ea0", + "ground": "#f8f8f8" + }, + { + "text": "\u2022Corinued fevenue growi", + "bounds": [ + 1089.5, + 247.6, + 100.5, + 6.7 + ], + "confidence": 1, + "ink": "#8f8f92", + "ground": "#f5f5f5" + }, + { + "text": "\u2022 Investments in grid modemization", + "bounds": [ + 1089.5, + 261.0, + 127.3, + 11.2 + ], + "confidence": 1, + "ink": "#a2a2a4", + "ground": "#f8f8f9" + }, + { + "text": "\u2022 Expansion of member programs", + "bounds": [ + 1089.5, + 276.6, + 120.6, + 13.4 + ], + "confidence": 1, + "ink": "#9c9c9e", + "ground": "#f9f9f9" + }, + { + "text": "\u2022rocus on sustainability", + "bounds": [ + 1089.5, + 296.7, + 89.3, + 6.7 + ], + "confidence": 1, + "ink": "#9f9fa1", + "ground": "#f6f6f6" + }, + { + "text": "These priorities position us well to", + "bounds": [ + 1089.5, + 319.0, + 116.1, + 9 + ], + "confidence": 1, + "ink": "#a2a2a5", + "ground": "#f6f7f7" + }, + { + "text": "create long-1erm value for our", + "bounds": [ + 1089.5, + 327.9, + 102.7, + 13.4 + ], + "confidence": 1, + "ink": "#a1a1a3", + "ground": "#f7f7f8" + }, + { + "text": "members and comenunities", + "bounds": [ + 1089.5, + 341.0, + 93.8, + 9.3 + ], + "confidence": 1, + "ink": "#9a9a9c", + "ground": "#f6f6f6" + }, + { + "text": "\u043ctw\u043cno\u0441hoNent", + "bounds": [ + 1080.6, + 406.0, + 67.0, + 4.5 + ], + "confidence": 0.3, + "ink": "#aeaeb1", + "ground": "#f0f1f1" + }, + { + "text": "ot yseosi MM", + "bounds": [ + 1187.7, + 406.0, + 58.0, + 4.5 + ], + "confidence": 0.3, + "ink": "#aeaeb1", + "ground": "#f1f1f2" + }, + { + "text": "COAST REGION", + "bounds": [ + 1062.7, + 486.0, + 73.7, + 11.5 + ], + "confidence": 1, + "ink": "#5d5e61", + "ground": "#f3f4f4" + }, + { + "text": "The Coast region delivered solid", + "bounds": [ + 1062.7, + 508.7, + 109.4, + 11.3 + ], + "confidence": 1, + "ink": "#9fa0a1", + "ground": "#f8f8f8" + }, + { + "text": "results with improved efficiency.", + "bounds": [ + 1062.7, + 519.8, + 109.4, + 13.4 + ], + "confidence": 1, + "ink": "#a6a6a8", + "ground": "#f8f8f9" + }, + { + "text": "Key metrics", + "bounds": [ + 1067.2, + 546.6, + 42.4, + 11.4 + ], + "confidence": 1, + "ink": "#797a7e", + "ground": "#efeff0" + }, + { + "text": "Revenue (USO mitions)", + "bounds": [ + 1067.2, + 562.0, + 78.1, + 11.4 + ], + "confidence": 1, + "ink": "#a5a6a9", + "ground": "#f7f7f7" + }, + { + "text": "Mamhes", + "bounds": [ + 1067.2, + 582.3, + 31.3, + 4.5 + ], + "confidence": 1, + "ink": "#96979b", + "ground": "#f1f2f2" + }, + { + "text": "Growth (YoY)", + "bounds": [ + 1067.1, + 595.2, + 47.1, + 12.1 + ], + "confidence": 1, + "ink": "#9c9c9f", + "ground": "#f8f8f8" + }, + { + "text": "22.1", + "bounds": [ + 1172.1, + 562.2, + 20.1, + 11.2 + ], + "confidence": 1, + "ink": "#97999d", + "ground": "#f7f7f8" + }, + { + "text": "13,105", + "bounds": [ + 1163.2, + 580.0, + 29.0, + 8.9 + ], + "confidence": 1, + "ink": "#8e9094", + "ground": "#f7f7f7" + }, + { + "text": "9%", + "bounds": [ + 1174.3, + 595.7, + 15.6, + 11.2 + ], + "confidence": 1, + "ink": "#9fa0a4", + "ground": "#f8f8f8" + }, + { + "text": "We continue to enhance reliabiity", + "bounds": [ + 1062.7, + 626.9, + 116.1, + 7.1 + ], + "confidence": 1, + "ink": "#a8a9ac", + "ground": "#f5f5f6" + }, + { + "text": "andd member eroenence", + "bounds": [ + 1062.7, + 638.0, + 84.8, + 5 + ], + "confidence": 1, + "ink": "#a2a2a5", + "ground": "#f1f2f2" + }, + { + "text": "NOTHi\u043eCORNN1", + "bounds": [ + 1056.0, + 673.7, + 67.0, + 4.5 + ], + "confidence": 0.3, + "ink": "#b4b4b7", + "ground": "#f2f3f3" + }, + { + "text": "Saved", + "bounds": [ + 38.0, + 990.5, + 33.5, + 11.2 + ], + "confidence": 1, + "ink": "#7c7f84", + "ground": "#f7f8f8" + }, + { + "text": "Save", + "bounds": [ + 1460.1, + 69.2, + 33.5, + 15.6 + ], + "confidence": 1, + "ink": "#c3def7", + "ground": "#086adb" + }, + { + "text": "Drag pages to reorder", + "bounds": [ + 1328.4, + 124.9, + 113.9, + 13.4 + ], + "confidence": 1, + "ink": "#87898d", + "ground": "#f7f7f8" + }, + { + "text": "APPENDIX", + "bounds": [ + 1319.2, + 190.5, + 49.5, + 11.6 + ], + "confidence": 1, + "ink": "#4f5052", + "ground": "#f3f3f3" + }, + { + "text": "A. Financial Statements", + "bounds": [ + 1319.4, + 214.0, + 80.4, + 11.3 + ], + "confidence": 1, + "ink": "#949597", + "ground": "#f7f7f7" + }, + { + "text": "B. Key Metrics", + "bounds": [ + 1319.4, + 229.8, + 53.6, + 13.4 + ], + "confidence": 1, + "ink": "#98999c", + "ground": "#f8f8f9" + }, + { + "text": "C. Glossary", + "bounds": [ + 1319.4, + 252.0, + 40.2, + 7 + ], + "confidence": 1, + "ink": "#9c9ca0", + "ground": "#f6f6f6" + }, + { + "text": "D. Notes", + "bounds": [ + 1319.4, + 267.7, + 33.5, + 11.3 + ], + "confidence": 1, + "ink": "#9b9ca0", + "ground": "#f8f8f9" + }, + { + "text": "VALLEY REGION", + "bounds": [ + 1297.1, + 486.0, + 78.1, + 11.5 + ], + "confidence": 1, + "ink": "#616366", + "ground": "#f3f4f4" + }, + { + "text": "The Vailey region saw strong", + "bounds": [ + 1299.3, + 508.7, + 98.2, + 11.3 + ], + "confidence": 1, + "ink": "#9d9ea0", + "ground": "#f7f8f8" + }, + { + "text": "member growth and engagement.", + "bounds": [ + 1299.3, + 519.8, + 113.9, + 11.2 + ], + "confidence": 1, + "ink": "#a4a4a6", + "ground": "#f7f7f8" + }, + { + "text": "Key metrics", + "bounds": [ + 1301.6, + 546.6, + 42.4, + 9.4 + ], + "confidence": 1, + "ink": "#87888b", + "ground": "#f1f2f3" + }, + { + "text": "Reverue (USD milions)", + "bounds": [ + 1301.6, + 562.0, + 75.9, + 11.4 + ], + "confidence": 1, + "ink": "#a5a5a7", + "ground": "#f7f7f7" + }, + { + "text": "Membord", + "bounds": [ + 1301.6, + 582.3, + 31.3, + 4.5 + ], + "confidence": 1, + "ink": "#9a9b9f", + "ground": "#f1f1f2" + }, + { + "text": "Growth (YoY)", + "bounds": [ + 1301.5, + 595.4, + 47.0, + 11.7 + ], + "confidence": 1, + "ink": "#9f9fa2", + "ground": "#f7f7f8" + }, + { + "text": "Q32026", + "bounds": [ + 1397.6, + 544.3, + 31.3, + 11.2 + ], + "confidence": 1, + "ink": "#7f8186", + "ground": "#f0f1f2" + }, + { + "text": "18.7", + "bounds": [ + 1408.7, + 562.2, + 20.1, + 11.2 + ], + "confidence": 1, + "ink": "#9d9da0", + "ground": "#f9f9f9" + }, + { + "text": "10.68S", + "bounds": [ + 1399.8, + 582.3, + 26.8, + 4.5 + ], + "confidence": 0.3, + "ink": "#8f9094", + "ground": "#f2f3f3" + }, + { + "text": "11%", + "bounds": [ + 1406.5, + 595.7, + 20.1, + 11.2 + ], + "confidence": 1, + "ink": "#9a9b9f", + "ground": "#f9f9f9" + }, + { + "text": "Investments in technology and", + "bounds": [ + 1297.1, + 624.7, + 109.4, + 11.3 + ], + "confidence": 1, + "ink": "#a2a2a4", + "ground": "#f7f7f8" + }, + { + "text": "community programs are driving", + "bounds": [ + 1297.1, + 635.8, + 113.9, + 11.2 + ], + "confidence": 1, + "ink": "#a5a5a8", + "ground": "#f8f8f8" + }, + { + "text": "positive outcomes.", + "bounds": [ + 1297.1, + 647.0, + 67.0, + 11.2 + ], + "confidence": 1, + "ink": "#a4a5a7", + "ground": "#f8f8f8" + }, + { + "text": "\u041c\u043eTH\u043e COORATN", + "bounds": [ + 1288.2, + 673.7, + 67.0, + 4.5 + ], + "confidence": 0.3, + "ink": "#aeaeb1", + "ground": "#f1f1f2" + }, + { + "text": "12", + "bounds": [ + 1357.4, + 698.3, + 22.3, + 17.8 + ], + "confidence": 1, + "ink": "#5d6268", + "ground": "#f3f3f4" + }, + { + "text": "14.6 of 64 MB used", + "bounds": [ + 1402.0, + 992.8, + 104.9, + 13.4 + ], + "confidence": 1, + "ink": "#7c7e83", + "ground": "#f8f8f8" + } + ], + "surfaces": [ + { + "bounds": [ + 5, + 2, + 1527, + 1021 + ], + "fill": "#fbfbfc", + "border": "#f7f7f8", + "radius": 0 + }, + { + "bounds": [ + 88, + 155, + 1441, + 823 + ], + "fill": "#f8f8f9", + "border": "#f8f8f9", + "radius": 0 + }, + { + "bounds": [ + 8, + 44, + 1521, + 109 + ], + "fill": "#fdfdfd", + "border": "#fbfbfb", + "radius": 3 + }, + { + "bounds": [ + 8, + 103, + 78, + 875 + ], + "fill": "#fdfdfd", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 8, + 980, + 1521, + 39 + ], + "fill": "#fdfdfd", + "border": "#fafafa", + "radius": 0 + }, + { + "bounds": [ + 1064, + 168, + 205, + 262 + ], + "fill": "#fdfdfd", + "border": "#eeebed", + "radius": 0 + }, + { + "bounds": [ + 580, + 167, + 201, + 264 + ], + "fill": "#fdfcfd", + "border": "#f7f7f8", + "radius": 0 + }, + { + "bounds": [ + 824, + 167, + 199, + 264 + ], + "fill": "#fdfdfd", + "border": "#f7f7f8", + "radius": 0 + }, + { + "bounds": [ + 1297, + 167, + 194, + 263 + ], + "fill": "#fdfdfd", + "border": "#f7f7f6", + "radius": 0 + }, + { + "bounds": [ + 350, + 167, + 191, + 264 + ], + "fill": "#fdfdfd", + "border": "#f7f7f8", + "radius": 0 + }, + { + "bounds": [ + 1272, + 462, + 199, + 238 + ], + "fill": "#fcfcfc", + "border": "#f7f7f7", + "radius": 0 + }, + { + "bounds": [ + 1039, + 462, + 195, + 238 + ], + "fill": "#fcfcfc", + "border": "#f7f7f7", + "radius": 0 + }, + { + "bounds": [ + 811, + 462, + 192, + 238 + ], + "fill": "#fcfcfc", + "border": "#f7f7f8", + "radius": 0 + }, + { + "bounds": [ + 554, + 462, + 188, + 238 + ], + "fill": "#fdfdfd", + "border": "#f6f6f6", + "radius": 0 + }, + { + "bounds": [ + 116, + 462, + 187, + 238 + ], + "fill": "#fdfdfd", + "border": "#f7f7f8", + "radius": 0 + }, + { + "bounds": [ + 338, + 462, + 182, + 238 + ], + "fill": "#fdfdfd", + "border": "#f7f7f8", + "radius": 0 + }, + { + "bounds": [ + 116, + 724, + 181, + 202 + ], + "fill": "#fdfcfd", + "border": "#f7f7f8", + "radius": 0 + }, + { + "bounds": [ + 339, + 724, + 178, + 202 + ], + "fill": "#fdfdfd", + "border": "#f7f7f8", + "radius": 0 + }, + { + "bounds": [ + 555, + 727, + 173, + 195 + ], + "fill": "#fcfcfc", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 116, + 309, + 195, + 122 + ], + "fill": "#e3ecf1", + "border": "#f6f6f7", + "radius": 4 + }, + { + "bounds": [ + 623, + 921, + 396, + 41 + ], + "fill": "#fdfdfd", + "border": "#fefefe", + "radius": 4 + }, + { + "bounds": [ + 831, + 547, + 155, + 71 + ], + "fill": "#fafafb", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 1299, + 541, + 142, + 72 + ], + "fill": "#fafafb", + "border": "#fcfcfc", + "radius": 0 + }, + { + "bounds": [ + 1064, + 541, + 139, + 73 + ], + "fill": "#fafafa", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 1039, + 58, + 253, + 37 + ], + "fill": "#fdfdfd", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 262, + 5, + 232, + 40 + ], + "fill": "#f3f3f4", + "border": "#f3f3f3", + "radius": 0 + }, + { + "bounds": [ + 120, + 380, + 188, + 48 + ], + "fill": "#fdfdfd", + "border": "#e4e4e4", + "radius": 1 + }, + { + "bounds": [ + 960, + 5, + 219, + 40 + ], + "fill": "#f3f3f3", + "border": "#ededee", + "radius": 0 + }, + { + "bounds": [ + 10, + 8, + 246, + 35 + ], + "fill": "#fdfdfd", + "border": "#fcfcfc", + "radius": 0 + }, + { + "bounds": [ + 501, + 8, + 231, + 37 + ], + "fill": "#f3f3f3", + "border": "#f5f5f5", + "radius": 0 + }, + { + "bounds": [ + 741, + 8, + 214, + 37 + ], + "fill": "#f3f3f3", + "border": "#f4f4f4", + "radius": 0 + }, + { + "bounds": [ + 139, + 787, + 133, + 59 + ], + "fill": "#fafafa", + "border": "#fcfcfd", + "radius": 0 + }, + { + "bounds": [ + 1042, + 61, + 246, + 30 + ], + "fill": "#fdfdfd", + "border": "#fefefe", + "radius": 1 + }, + { + "bounds": [ + 962, + 8, + 215, + 34 + ], + "fill": "#f3f3f3", + "border": "#f4f4f4", + "radius": 0 + }, + { + "bounds": [ + 19, + 117, + 55, + 68 + ], + "fill": "#e7f1fb", + "border": "#fdfdfd", + "radius": 3 + }, + { + "bounds": [ + 1437, + 59, + 80, + 34 + ], + "fill": "#0468db", + "border": "#f2f8fd", + "radius": 4 + }, + { + "bounds": [ + 1185, + 10, + 76, + 30 + ], + "fill": "#f4f3f4", + "border": "#f2f2f3", + "radius": 0 + }, + { + "bounds": [ + 1302, + 560, + 136, + 16 + ], + "fill": "#f9f9f9", + "border": "#f8f8f8", + "radius": 0 + }, + { + "bounds": [ + 1189, + 13, + 69, + 24 + ], + "fill": "#f4f3f4", + "border": "#efeff0", + "radius": 0 + } + ], + "dividers": [ + { + "axis": "h", + "at": 43, + "from": 1179, + "to": 1525, + "color": "#e3e3e4" + }, + { + "axis": "h", + "at": 100, + "from": 412, + "to": 1529, + "color": "#f6f6f7" + }, + { + "axis": "h", + "at": 101, + "from": 8, + "to": 1529, + "color": "#ebeced" + }, + { + "axis": "h", + "at": 919, + "from": 627, + "to": 1011, + "color": "#d9d8d9" + }, + { + "axis": "h", + "at": 962, + "from": 680, + "to": 1013, + "color": "#d9d8d8" + }, + { + "axis": "h", + "at": 963, + "from": 680, + "to": 1011, + "color": "#e4e2e3" + }, + { + "axis": "h", + "at": 1019, + "from": 12, + "to": 1524, + "color": "#ededee" + }, + { + "axis": "v", + "at": 6, + "from": 102, + "to": 978, + "color": "#ebebec" + }, + { + "axis": "v", + "at": 7, + "from": 102, + "to": 978, + "color": "#f4f4f5" + }, + { + "axis": "v", + "at": 86, + "from": 154, + "to": 979, + "color": "#ebebec" + }, + { + "axis": "v", + "at": 118, + "from": 725, + "to": 924, + "color": "#dadada" + }, + { + "axis": "v", + "at": 119, + "from": 726, + "to": 922, + "color": "#f4f4f4" + }, + { + "axis": "v", + "at": 293, + "from": 725, + "to": 922, + "color": "#dbdbdb" + }, + { + "axis": "v", + "at": 294, + "from": 726, + "to": 923, + "color": "#e8e8e9" + }, + { + "axis": "v", + "at": 339, + "from": 464, + "to": 697, + "color": "#e6e6e6" + }, + { + "axis": "v", + "at": 340, + "from": 465, + "to": 696, + "color": "#d7d8d9" + }, + { + "axis": "v", + "at": 341, + "from": 727, + "to": 922, + "color": "#f0f0f0" + }, + { + "axis": "v", + "at": 513, + "from": 726, + "to": 922, + "color": "#f0f0f0" + }, + { + "axis": "v", + "at": 514, + "from": 725, + "to": 923, + "color": "#d9d9da" + }, + { + "axis": "v", + "at": 515, + "from": 728, + "to": 915, + "color": "#f0f0f1" + }, + { + "axis": "v", + "at": 516, + "from": 465, + "to": 696, + "color": "#dadbdb" + }, + { + "axis": "v", + "at": 517, + "from": 464, + "to": 697, + "color": "#e4e5e5" + }, + { + "axis": "v", + "at": 553, + "from": 727, + "to": 921, + "color": "#dededf" + }, + { + "axis": "v", + "at": 554, + "from": 727, + "to": 921, + "color": "#ededee" + }, + { + "axis": "v", + "at": 555, + "from": 466, + "to": 698, + "color": "#dadada" + }, + { + "axis": "v", + "at": 556, + "from": 466, + "to": 696, + "color": "#e9e9e9" + }, + { + "axis": "v", + "at": 738, + "from": 465, + "to": 672, + "color": "#f5f5f5" + }, + { + "axis": "v", + "at": 739, + "from": 464, + "to": 696, + "color": "#d2d1d2" + }, + { + "axis": "v", + "at": 740, + "from": 465, + "to": 697, + "color": "#e9e9ea" + }, + { + "axis": "v", + "at": 812, + "from": 464, + "to": 697, + "color": "#e7e7e8" + }, + { + "axis": "v", + "at": 813, + "from": 465, + "to": 698, + "color": "#d9dadb" + }, + { + "axis": "v", + "at": 825, + "from": 169, + "to": 429, + "color": "#dadada" + }, + { + "axis": "v", + "at": 826, + "from": 170, + "to": 427, + "color": "#eceded" + }, + { + "axis": "v", + "at": 1019, + "from": 170, + "to": 427, + "color": "#d8d8d9" + }, + { + "axis": "v", + "at": 1020, + "from": 170, + "to": 428, + "color": "#e8e8e8" + }, + { + "axis": "v", + "at": 1064, + "from": 172, + "to": 428, + "color": "#bac9e6" + }, + { + "axis": "v", + "at": 1065, + "from": 170, + "to": 429, + "color": "#0858d4" + }, + { + "axis": "v", + "at": 1066, + "from": 169, + "to": 429, + "color": "#ddebfa" + }, + { + "axis": "v", + "at": 1230, + "from": 465, + "to": 696, + "color": "#f3f3f3" + }, + { + "axis": "v", + "at": 1231, + "from": 464, + "to": 696, + "color": "#d6d6d7" + }, + { + "axis": "v", + "at": 1232, + "from": 466, + "to": 697, + "color": "#eeeeee" + }, + { + "axis": "v", + "at": 1265, + "from": 170, + "to": 428, + "color": "#1976dc" + }, + { + "axis": "v", + "at": 1266, + "from": 171, + "to": 426, + "color": "#619fe3" + }, + { + "axis": "v", + "at": 1273, + "from": 464, + "to": 696, + "color": "#dadadb" + }, + { + "axis": "v", + "at": 1274, + "from": 465, + "to": 696, + "color": "#eaeaea" + }, + { + "axis": "v", + "at": 1297, + "from": 172, + "to": 428, + "color": "#b8c5e0" + }, + { + "axis": "v", + "at": 1298, + "from": 169, + "to": 428, + "color": "#0954e5" + }, + { + "axis": "v", + "at": 1299, + "from": 169, + "to": 428, + "color": "#d4e7fd" + }, + { + "axis": "v", + "at": 1467, + "from": 465, + "to": 696, + "color": "#e9e9e9" + }, + { + "axis": "v", + "at": 1468, + "from": 464, + "to": 696, + "color": "#d9d9da" + }, + { + "axis": "v", + "at": 1469, + "from": 466, + "to": 697, + "color": "#f0f0f1" + }, + { + "axis": "v", + "at": 1487, + "from": 181, + "to": 418, + "color": "#f0f5fc" + }, + { + "axis": "v", + "at": 1488, + "from": 170, + "to": 427, + "color": "#1568eb" + }, + { + "axis": "v", + "at": 1489, + "from": 171, + "to": 427, + "color": "#a8c0e4" + }, + { + "axis": "v", + "at": 1529, + "from": 154, + "to": 978, + "color": "#e9e9ea" + } + ], + "limits": [ + "Evidence for review: roles and widgets are assigned in the semantic map, not here." + ] +} diff --git a/apps/pdftools/design/source/05-pages.png b/apps/pdftools/design/source/05-pages.png new file mode 100644 index 00000000..d6093fd7 Binary files /dev/null and b/apps/pdftools/design/source/05-pages.png differ diff --git a/apps/pdftools/design/source/05-pages.prompt.txt b/apps/pdftools/design/source/05-pages.prompt.txt new file mode 100644 index 00000000..6d4d87dd --- /dev/null +++ b/apps/pdftools/design/source/05-pages.prompt.txt @@ -0,0 +1,70 @@ +Create ONE original image: a single, complete desktop application screen for +"PDF Tools", a document app inside the OctoSense desktop. + +Requested image size: 1536 × 1024 pixels, landscape. Requested quality: high. +Show only the application's own window content, edge to edge, flat and +straight on. No operating-system menu bar, no dock, no window title bar or +traffic lights, no device frame, no perspective, no drop shadow around the +screen, no captions or annotations outside the interface. + +Visual language: an original, precise desktop document tool. The document is +the brightest thing on screen; the chrome is quiet, thin and well aligned. +Palette: a cool light grey desk behind the pages, white pages with a soft +shadow, graphite text, mid-grey secondary text, thin 1-pixel dividers in pale +grey, and ONE accent colour, a confident medium blue, used only for the +current mode, the selected item, focus and primary buttons. Yellow marks +highlighted text and search matches. Green means saved or done, red means an +error, both used sparingly. Corner radius about 8 px on panels, buttons and +fields; pages have square corners. Icons are simple outline glyphs with one +consistent stroke weight. Typography: a neutral sans-serif for the interface, +small and crisp (about 13–14 px), medium weight for panel titles; document +pages use a classic serif for body text and a sans-serif for their headings. +Do not imitate Adobe Acrobat's layout, icons, colours or branding, or any other +product's. + +Window layout, the same in every screen: +- Top row: document tabs. Each tab shows a small PDF glyph, the file name and + a close cross; the active tab is white, the others grey; a "+ Open" button + follows the last tab. +- Second row, the mode bar: the modes "Read", "Comment", "Fill & Sign", + "Pages", "Combine", "Edit" as text tabs, the current one in the accent + colour with a short underline; on the right a search field with a magnifier + and the placeholder "Search in document", then icon buttons for Undo and Redo, + and a "Save" button. +- Left: a narrow icon rail with four icons and tooltips' labels shown under + them in tiny text: "Pages", "Outline", "Comments", "Search". The selected + icon has a tinted square behind it. Next to the rail, an open panel about + 240 px wide when the screen calls for it. +- Centre: the canvas, a light grey desk with the document's pages stacked + vertically, centred, one page fully visible and the next starting below. +- Bottom centre of the canvas: a floating rounded pill with "‹", "3 / 24", + "›", a divider, "−", "100%", "+", and two small icons for "Fit width" and + "Fit page". +- Right: a panel about 300 px wide, only in screens that call for it. +- Bottom edge: a thin status line with "Saved" (with a small green dot) on the + left and "14.6 of 64 MB used" on the right. + +Use this exact fixture wherever it appears: +- Documents in the library: "Q3 2026 Board Report.pdf" (24 pages, 2.8 MB, + opened today), "Riverside Lease 2026.pdf" (12 pages, 1.1 MB, yesterday), + "Field Guide to Garden Birds.pdf" (7 pages, 16 KB, 3 Oct), "Invoice + INV-2041.pdf" (1 page, 84 KB, 1 Oct), "Site Survey Photos.pdf" (18 pages, + 9.6 MB, 28 Sep). +- The board report belongs to the fictional "Northwind Cooperative". Its + sections: Summary, Revenue, Operations, Outlook, Appendix. Page 3 is + "Revenue" with a short paragraph that includes the sentence "Revenue grew + 12% year over year, led by the Northwest region." and a simple bar chart + "Revenue by region, Q3 2026" with bars for Northwest, Coast, Valley, Metro. +- People: Maya Chen (the user), Jun Park and Ana Ruiz (reviewers). + +Render every piece of interface text and every control sharply enough to be +read and measured later. Labels, buttons, fields and panels will be rebuilt as +native components from this image; page contents are sample document art. + +This screen: mode "Pages". No left panel (the rail's "Pages" icon is still +selected). The canvas becomes a grid of large page thumbnails, five per row, +pages 1 to 15 visible, each with its number below. Pages 5 and 6 are selected +with an accent outline and a check badge. A toolbar row under the mode bar: +"2 selected", then "Rotate left", "Rotate right", "Delete", "Extract", +"Insert from file", and on the right "Drag pages to reorder". A thin +insertion marker shows between pages 9 and 10 where a page is being dragged. \ No newline at end of file diff --git a/apps/pdftools/design/source/06-combine.measured.json b/apps/pdftools/design/source/06-combine.measured.json new file mode 100644 index 00000000..f3a5dbd5 --- /dev/null +++ b/apps/pdftools/design/source/06-combine.measured.json @@ -0,0 +1,1083 @@ +{ + "schema_version": 1, + "image": "06-combine.png", + "image_sha256": "bbb5c2bf1a5288343e42b6602e231314f3dc79effb6cfe789aa14cf743baef08", + "script_sha256": "ea860507e32482b955d3ac577ab3cf9fc559f59ecc916c89175293932b54e0b7", + "size": [ + 1536, + 1024 + ], + "ocr_engine": "Apple Vision VNRecognizeTextRequest accurate, en-US (App Flow ocr.swift)", + "text": [ + { + "text": "Q3 2026 Board Report.pdf x", + "bounds": [ + 51.3, + 20.0, + 198.7, + 18 + ], + "confidence": 1, + "ink": "#515152", + "ground": "#f6f6f6" + }, + { + "text": "Riverside Lease 2026.pdf X", + "bounds": [ + 308.1, + 20.0, + 183.1, + 16 + ], + "confidence": 1, + "ink": "#555758", + "ground": "#eeeff1" + }, + { + "text": "Field Guide to Garden Birds.pdf x", + "bounds": [ + 547.0, + 17.8, + 218.8, + 20.2 + ], + "confidence": 1, + "ink": "#5b5c5e", + "ground": "#f0f1f3" + }, + { + "text": "Read", + "bounds": [ + 35.7, + 73.6, + 51.3, + 15.6 + ], + "confidence": 1, + "ink": "#616569", + "ground": "#f7f7f7" + }, + { + "text": "Comment", + "bounds": [ + 104.9, + 73.3, + 91.6, + 16.2 + ], + "confidence": 1, + "ink": "#62656a", + "ground": "#f6f6f6" + }, + { + "text": "Fill & Sign", + "bounds": [ + 238.9, + 73.6, + 78.1, + 20.4 + ], + "confidence": 1, + "ink": "#5b5d62", + "ground": "#f4f4f5" + }, + { + "text": "Pages", + "bounds": [ + 361.6, + 73.4, + 47.0, + 18.2 + ], + "confidence": 1, + "ink": "#5c5e63", + "ground": "#f3f4f4" + }, + { + "text": "Combine", + "bounds": [ + 455.4, + 73.4, + 71.6, + 16.1 + ], + "confidence": 1, + "ink": "#438ae2", + "ground": "#f2f6fb" + }, + { + "text": "Edit", + "bounds": [ + 569.3, + 73.6, + 35.7, + 17.8 + ], + "confidence": 1, + "ink": "#5b5e63", + "ground": "#f4f4f4" + }, + { + "text": "Invoice INV-2041.pdf x", + "bounds": [ + 821.6, + 17.8, + 158.5, + 20.2 + ], + "confidence": 1, + "ink": "#58595c", + "ground": "#f0f1f3" + }, + { + "text": "Site Survey Photos.pdf x", + "bounds": [ + 1035.9, + 17.8, + 171.9, + 20.2 + ], + "confidence": 1, + "ink": "#595a5b", + "ground": "#f0f1f3" + }, + { + "text": "+ Open", + "bounds": [ + 1230.1, + 17.8, + 62.5, + 22.3 + ], + "confidence": 1, + "ink": "#555658", + "ground": "#f5f5f6" + }, + { + "text": "Search in document", + "bounds": [ + 971.2, + 75.9, + 127.3, + 13.4 + ], + "confidence": 1, + "ink": "#a2a4a8", + "ground": "#f8f8f9" + }, + { + "text": "Save", + "bounds": [ + 1473.5, + 75.9, + 33.5, + 13.4 + ], + "confidence": 1, + "ink": "#cee3f9", + "ground": "#257ae5" + }, + { + "text": "Pages", + "bounds": [ + 26.8, + 176.2, + 40.2, + 15.6 + ], + "confidence": 1, + "ink": "#777a7e", + "ground": "#f5f5f6" + }, + { + "text": "Outline", + "bounds": [ + 24.6, + 263.3, + 46.9, + 13.4 + ], + "confidence": 1, + "ink": "#85878b", + "ground": "#f6f7f7" + }, + { + "text": "Combine files", + "bounds": [ + 459.9, + 196.0, + 151.8, + 25 + ], + "confidence": 1, + "ink": "#3c3d3e", + "ground": "#f5f5f5" + }, + { + "text": "NORTUMNO", + "bounds": [ + 511.3, + 258.8, + 42.4, + 15.6 + ], + "confidence": 0.3, + "ink": "#9a9a9c", + "ground": "#f6f6f6" + }, + { + "text": "Q3 2026", + "bounds": [ + 511.3, + 283.0, + 42.4, + 14 + ], + "confidence": 1, + "ink": "#737477", + "ground": "#f6f6f6" + }, + { + "text": "Board Report", + "bounds": [ + 511.3, + 294.0, + 58.0, + 12 + ], + "confidence": 1, + "ink": "#808082", + "ground": "#f7f7f7" + }, + { + "text": "Q3 2026 Board Report.pdf", + "bounds": [ + 596.1, + 292.0, + 187.5, + 18.1 + ], + "confidence": 1, + "ink": "#57585a", + "ground": "#f5f5f5" + }, + { + "text": "All (24 pages)", + "bounds": [ + 888.5, + 292.1, + 87.1, + 18.1 + ], + "confidence": 1, + "ink": "#7b7d80", + "ground": "#f7f7f7" + }, + { + "text": "Comments", + "bounds": [ + 13.3, + 349.9, + 67.1, + 14.1 + ], + "confidence": 1, + "ink": "#828589", + "ground": "#f6f6f7" + }, + { + "text": "Appendix A - Figures.pdf", + "bounds": [ + 596.1, + 408.0, + 178.6, + 18.1 + ], + "confidence": 1, + "ink": "#4f5051", + "ground": "#f5f5f5" + }, + { + "text": "1-4,9 (5 pages)", + "bounds": [ + 886.2, + 407.7, + 105.1, + 19.0 + ], + "confidence": 1, + "ink": "#818386", + "ground": "#f8f8f9" + }, + { + "text": "Search", + "bounds": [ + 24.5, + 437.1, + 44.8, + 15.9 + ], + "confidence": 1, + "ink": "#85878b", + "ground": "#f8f8f8" + }, + { + "text": "::", + "bounds": [ + 459.9, + 519.8, + 17.9, + 20.1 + ], + "confidence": 1, + "ink": "#6c6f76", + "ground": "#f7f7f7" + }, + { + "text": "Cover Letter.pdf", + "bounds": [ + 596.1, + 524.0, + 118.3, + 18.1 + ], + "confidence": 1, + "ink": "#4d4e50", + "ground": "#f5f5f5" + }, + { + "text": "All (2 pages)", + "bounds": [ + 886.3, + 524.0, + 82.6, + 20.3 + ], + "confidence": 1, + "ink": "#7b7d81", + "ground": "#f7f7f8" + }, + { + "text": "X", + "bounds": [ + 1071.6, + 406.0, + 17.9, + 20.1 + ], + "confidence": 1, + "ink": "#6e7177", + "ground": "#f9f9f9" + }, + { + "text": "X", + "bounds": [ + 1071.6, + 522.0, + 17.9, + 17.8 + ], + "confidence": 1, + "ink": "#6a6d73", + "ground": "#f8f8f8" + }, + { + "text": "+ Add files", + "bounds": [ + 718.9, + 609.0, + 93.8, + 22.4 + ], + "confidence": 1, + "ink": "#5693e5", + "ground": "#f8fafc" + }, + { + "text": "One PDF of 31 pages", + "bounds": [ + 444.3, + 689.0, + 160.7, + 20.4 + ], + "confidence": 1, + "ink": "#606367", + "ground": "#f6f6f6" + }, + { + "text": "Name", + "bounds": [ + 444.3, + 736.2, + 40.2, + 13.4 + ], + "confidence": 1, + "ink": "#6f7173", + "ground": "#f5f5f5" + }, + { + "text": "Q3 2026 Board Pack", + "bounds": [ + 513.5, + 736.0, + 138.4, + 16 + ], + "confidence": 1, + "ink": "#646669", + "ground": "#f4f4f4" + }, + { + "text": "Combine", + "bounds": [ + 986.8, + 792.0, + 96, + 22.3 + ], + "confidence": 1, + "ink": "#cde3f8", + "ground": "#2175e2" + }, + { + "text": "3 / 24", + "bounds": [ + 593.9, + 894.6, + 60.3, + 17.8 + ], + "confidence": 1, + "ink": "#58595b", + "ground": "#f7f7f7" + }, + { + "text": "100%", + "bounds": [ + 806.0, + 896.8, + 42.4, + 13.4 + ], + "confidence": 1, + "ink": "#75787a", + "ground": "#f6f6f6" + }, + { + "text": "+", + "bounds": [ + 870.7, + 894.6, + 20.1, + 22.3 + ], + "confidence": 1, + "ink": "#727579", + "ground": "#fbfbfb" + }, + { + "text": "\u2022 Saved", + "bounds": [ + 22.3, + 985.9, + 69.3, + 18.3 + ], + "confidence": 1, + "ink": "#588f5a", + "ground": "#f7f8f8" + }, + { + "text": "14.6 of 64 MB used", + "bounds": [ + 1366.3, + 988.0, + 131.7, + 14 + ], + "confidence": 1, + "ink": "#6c6d6e", + "ground": "#f5f5f5" + } + ], + "surfaces": [ + { + "bounds": [ + 2, + 7, + 1533, + 963 + ], + "fill": "#f1f2f4", + "border": "#f5f6f7", + "radius": 6 + }, + { + "bounds": [ + 94, + 112, + 1441, + 856 + ], + "fill": "#f1f2f4", + "border": "#f1f2f4", + "radius": 0 + }, + { + "bounds": [ + 420, + 155, + 717, + 696 + ], + "fill": "#fcfcfc", + "border": "#ebebed", + "radius": 7 + }, + { + "bounds": [ + 444, + 239, + 669, + 414 + ], + "fill": "#fcfcfc", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 2, + 970, + 1533, + 53 + ], + "fill": "#fcfcfc", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 2, + 115, + 90, + 853 + ], + "fill": "#fcfcfc", + "border": "#fbfbfb", + "radius": 0 + }, + { + "bounds": [ + 448, + 474, + 662, + 113 + ], + "fill": "#fcfcfc", + "border": "#fcfcfc", + "radius": 0 + }, + { + "bounds": [ + 448, + 358, + 662, + 113 + ], + "fill": "#fcfcfc", + "border": "#fdfcfc", + "radius": 0 + }, + { + "bounds": [ + 448, + 243, + 662, + 113 + ], + "fill": "#fcfcfc", + "border": "#fafafa", + "radius": 0 + }, + { + "bounds": [ + 2, + 2, + 1533, + 48 + ], + "fill": "#f3f4f6", + "border": "#fcfbfb", + "radius": 3 + }, + { + "bounds": [ + 263, + 5, + 958, + 45 + ], + "fill": "#f2f3f5", + "border": "#f3f3f4", + "radius": 3 + }, + { + "bounds": [ + 448, + 589, + 662, + 60 + ], + "fill": "#fcfcfc", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 513, + 874, + 545, + 61 + ], + "fill": "#fcfcfc", + "border": "#eaeaec", + "radius": 16 + }, + { + "bounds": [ + 1220, + 5, + 310, + 42 + ], + "fill": "#f8f8f9", + "border": "#f8f8f9", + "radius": 0 + }, + { + "bounds": [ + 501, + 725, + 310, + 40 + ], + "fill": "#fcfcfc", + "border": "#fbfbfb", + "radius": 0 + }, + { + "bounds": [ + 931, + 63, + 311, + 38 + ], + "fill": "#fcfcfc", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 504, + 728, + 304, + 33 + ], + "fill": "#fcfcfc", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 934, + 67, + 305, + 31 + ], + "fill": "#fcfcfc", + "border": "#faf9fa", + "radius": 0 + }, + { + "bounds": [ + 504, + 364, + 84, + 101 + ], + "fill": "#fbfafa", + "border": "#fdfcfc", + "radius": 0 + }, + { + "bounds": [ + 876, + 278, + 178, + 43 + ], + "fill": "#fcfcfc", + "border": "#fcfcfc", + "radius": 0 + }, + { + "bounds": [ + 504, + 249, + 78, + 98 + ], + "fill": "#fafafa", + "border": "#fcfcfc", + "radius": 0 + }, + { + "bounds": [ + 875, + 510, + 179, + 42 + ], + "fill": "#fcfcfc", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 875, + 394, + 179, + 42 + ], + "fill": "#fcfcfc", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 504, + 480, + 73, + 101 + ], + "fill": "#fbfbfb", + "border": "#fcfcfc", + "radius": 0 + }, + { + "bounds": [ + 970, + 779, + 140, + 46 + ], + "fill": "#1e71e3", + "border": "#fafafc", + "radius": 3 + }, + { + "bounds": [ + 508, + 368, + 66, + 94 + ], + "fill": "#f9f9f9", + "border": "#f6f6f6", + "radius": 0 + }, + { + "bounds": [ + 879, + 513, + 172, + 36 + ], + "fill": "#fcfcfc", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 879, + 397, + 172, + 36 + ], + "fill": "#fcfcfc", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 879, + 282, + 172, + 35 + ], + "fill": "#fcfcfc", + "border": "#fcfcfc", + "radius": 0 + }, + { + "bounds": [ + 1256, + 63, + 135, + 39 + ], + "fill": "#fcfcfc", + "border": "#fbfbfb", + "radius": 0 + }, + { + "bounds": [ + 508, + 252, + 67, + 78 + ], + "fill": "#fbfbfb", + "border": "#dfe0e2", + "radius": 0 + }, + { + "bounds": [ + 1434, + 64, + 87, + 37 + ], + "fill": "#2075e4", + "border": "#f7f8fc", + "radius": 3 + }, + { + "bounds": [ + 1259, + 66, + 65, + 32 + ], + "fill": "#fcfbfb", + "border": "#fdfdfd", + "radius": 1 + }, + { + "bounds": [ + 1326, + 66, + 62, + 32 + ], + "fill": "#fcfcfc", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 513, + 504, + 53, + 36 + ], + "fill": "#ebebeb", + "border": "#fbfbfb", + "radius": 7 + }, + { + "bounds": [ + 514, + 514, + 51, + 24 + ], + "fill": "#e9e8ea", + "border": "#f9f9fa", + "radius": 3 + } + ], + "dividers": [ + { + "axis": "h", + "at": 5, + "from": 1226, + "to": 1525, + "color": "#eeeeef" + }, + { + "axis": "h", + "at": 98, + "from": 934, + "to": 1238, + "color": "#dadcdf" + }, + { + "axis": "h", + "at": 113, + "from": 93, + "to": 1536, + "color": "#dbdde0" + }, + { + "axis": "h", + "at": 157, + "from": 427, + "to": 1130, + "color": "#dadbde" + }, + { + "axis": "h", + "at": 355, + "from": 447, + "to": 1110, + "color": "#f2f3f4" + }, + { + "axis": "h", + "at": 356, + "from": 447, + "to": 1110, + "color": "#e7e8eb" + }, + { + "axis": "h", + "at": 587, + "from": 446, + "to": 1110, + "color": "#e9e9ea" + }, + { + "axis": "h", + "at": 667, + "from": 446, + "to": 1111, + "color": "#e8e9eb" + }, + { + "axis": "h", + "at": 668, + "from": 446, + "to": 1111, + "color": "#f4f5f6" + }, + { + "axis": "h", + "at": 725, + "from": 506, + "to": 805, + "color": "#f5f5f5" + }, + { + "axis": "h", + "at": 726, + "from": 505, + "to": 807, + "color": "#e0e0e3" + }, + { + "axis": "h", + "at": 846, + "from": 427, + "to": 1131, + "color": "#e7e8e9" + }, + { + "axis": "h", + "at": 847, + "from": 427, + "to": 1130, + "color": "#d5d5d7" + }, + { + "axis": "h", + "at": 876, + "from": 651, + "to": 1034, + "color": "#dfdfe1" + }, + { + "axis": "h", + "at": 930, + "from": 539, + "to": 1031, + "color": "#e3e4e6" + }, + { + "axis": "h", + "at": 931, + "from": 539, + "to": 1031, + "color": "#d8d9dc" + }, + { + "axis": "h", + "at": 968, + "from": 0, + "to": 1536, + "color": "#dcdde0" + }, + { + "axis": "h", + "at": 969, + "from": 1, + "to": 1535, + "color": "#f5f6f6" + }, + { + "axis": "v", + "at": 91, + "from": 114, + "to": 772, + "color": "#e8e9ea" + }, + { + "axis": "v", + "at": 92, + "from": 114, + "to": 772, + "color": "#e1e1e4" + }, + { + "axis": "v", + "at": 422, + "from": 162, + "to": 844, + "color": "#d5d6d8" + }, + { + "axis": "v", + "at": 423, + "from": 162, + "to": 845, + "color": "#eeeff0" + }, + { + "axis": "v", + "at": 1110, + "from": 243, + "to": 647, + "color": "#e0e2e5" + }, + { + "axis": "v", + "at": 1133, + "from": 164, + "to": 841, + "color": "#e1e2e4" + }, + { + "axis": "v", + "at": 1134, + "from": 164, + "to": 841, + "color": "#dadadc" + } + ], + "limits": [ + "Evidence for review: roles and widgets are assigned in the semantic map, not here." + ] +} diff --git a/apps/pdftools/design/source/06-combine.png b/apps/pdftools/design/source/06-combine.png new file mode 100644 index 00000000..24085b65 Binary files /dev/null and b/apps/pdftools/design/source/06-combine.png differ diff --git a/apps/pdftools/design/source/06-combine.prompt.txt b/apps/pdftools/design/source/06-combine.prompt.txt new file mode 100644 index 00000000..b44a4e5c --- /dev/null +++ b/apps/pdftools/design/source/06-combine.prompt.txt @@ -0,0 +1,71 @@ +Create ONE original image: a single, complete desktop application screen for +"PDF Tools", a document app inside the OctoSense desktop. + +Requested image size: 1536 × 1024 pixels, landscape. Requested quality: high. +Show only the application's own window content, edge to edge, flat and +straight on. No operating-system menu bar, no dock, no window title bar or +traffic lights, no device frame, no perspective, no drop shadow around the +screen, no captions or annotations outside the interface. + +Visual language: an original, precise desktop document tool. The document is +the brightest thing on screen; the chrome is quiet, thin and well aligned. +Palette: a cool light grey desk behind the pages, white pages with a soft +shadow, graphite text, mid-grey secondary text, thin 1-pixel dividers in pale +grey, and ONE accent colour, a confident medium blue, used only for the +current mode, the selected item, focus and primary buttons. Yellow marks +highlighted text and search matches. Green means saved or done, red means an +error, both used sparingly. Corner radius about 8 px on panels, buttons and +fields; pages have square corners. Icons are simple outline glyphs with one +consistent stroke weight. Typography: a neutral sans-serif for the interface, +small and crisp (about 13–14 px), medium weight for panel titles; document +pages use a classic serif for body text and a sans-serif for their headings. +Do not imitate Adobe Acrobat's layout, icons, colours or branding, or any other +product's. + +Window layout, the same in every screen: +- Top row: document tabs. Each tab shows a small PDF glyph, the file name and + a close cross; the active tab is white, the others grey; a "+ Open" button + follows the last tab. +- Second row, the mode bar: the modes "Read", "Comment", "Fill & Sign", + "Pages", "Combine", "Edit" as text tabs, the current one in the accent + colour with a short underline; on the right a search field with a magnifier + and the placeholder "Search in document", then icon buttons for Undo and Redo, + and a "Save" button. +- Left: a narrow icon rail with four icons and tooltips' labels shown under + them in tiny text: "Pages", "Outline", "Comments", "Search". The selected + icon has a tinted square behind it. Next to the rail, an open panel about + 240 px wide when the screen calls for it. +- Centre: the canvas, a light grey desk with the document's pages stacked + vertically, centred, one page fully visible and the next starting below. +- Bottom centre of the canvas: a floating rounded pill with "‹", "3 / 24", + "›", a divider, "−", "100%", "+", and two small icons for "Fit width" and + "Fit page". +- Right: a panel about 300 px wide, only in screens that call for it. +- Bottom edge: a thin status line with "Saved" (with a small green dot) on the + left and "14.6 of 64 MB used" on the right. + +Use this exact fixture wherever it appears: +- Documents in the library: "Q3 2026 Board Report.pdf" (24 pages, 2.8 MB, + opened today), "Riverside Lease 2026.pdf" (12 pages, 1.1 MB, yesterday), + "Field Guide to Garden Birds.pdf" (7 pages, 16 KB, 3 Oct), "Invoice + INV-2041.pdf" (1 page, 84 KB, 1 Oct), "Site Survey Photos.pdf" (18 pages, + 9.6 MB, 28 Sep). +- The board report belongs to the fictional "Northwind Cooperative". Its + sections: Summary, Revenue, Operations, Outlook, Appendix. Page 3 is + "Revenue" with a short paragraph that includes the sentence "Revenue grew + 12% year over year, led by the Northwest region." and a simple bar chart + "Revenue by region, Q3 2026" with bars for Northwest, Coast, Valley, Metro. +- People: Maya Chen (the user), Jun Park and Ana Ruiz (reviewers). + +Render every piece of interface text and every control sharply enough to be +read and measured later. Labels, buttons, fields and panels will be rebuilt as +native components from this image; page contents are sample document art. + +This screen: mode "Combine". The canvas shows a centred card titled "Combine +files" with three rows in order, each with a drag handle, the first page as a +small thumbnail, the file name, a page-range field and a remove cross: +"Q3 2026 Board Report.pdf" with range "All (24 pages)", "Appendix A - +Figures.pdf" with range "1–4, 9 (5 pages)", "Cover Letter.pdf" with range +"All (2 pages)". Below the rows: "+ Add files". A summary line "One PDF of 31 +pages", a name field "Q3 2026 Board Pack", and a primary button "Combine". No +left panel. \ No newline at end of file diff --git a/apps/pdftools/design/source/07-fill-sign.measured.json b/apps/pdftools/design/source/07-fill-sign.measured.json new file mode 100644 index 00000000..aed93aa4 --- /dev/null +++ b/apps/pdftools/design/source/07-fill-sign.measured.json @@ -0,0 +1,1804 @@ +{ + "schema_version": 1, + "image": "07-fill-sign.png", + "image_sha256": "fc8314c6e42ea94a8d64df669b160e3eacc408ca4923bfe57fd1b3d4a04aa721", + "script_sha256": "ea860507e32482b955d3ac577ab3cf9fc559f59ecc916c89175293932b54e0b7", + "size": [ + 1536, + 1024 + ], + "ocr_engine": "Apple Vision VNRecognizeTextRequest accurate, en-US (App Flow ocr.swift)", + "text": [ + { + "text": "Q3 2026 Board Report.pdf", + "bounds": [ + 46.9, + 20.0, + 151.8, + 16 + ], + "confidence": 1, + "ink": "#5d5e5e", + "ground": "#eeeeef" + }, + { + "text": "Riverside Lease 2026.pdf", + "bounds": [ + 288.0, + 22.0, + 163.0, + 16.0 + ], + "confidence": 1, + "ink": "#424242", + "ground": "#f5f5f5" + }, + { + "text": "Field Guide to Garden Birds.pdf X", + "bounds": [ + 524.7, + 20.0, + 198.7, + 16 + ], + "confidence": 1, + "ink": "#585959", + "ground": "#efefef" + }, + { + "text": "Invoice INV-2041.pdf", + "bounds": [ + 779.2, + 20.0, + 127.3, + 18 + ], + "confidence": 1, + "ink": "#606061", + "ground": "#f0f0f0" + }, + { + "text": "Read", + "bounds": [ + 31.2, + 71.3, + 35.8, + 13.6 + ], + "confidence": 1, + "ink": "#4a4b4b", + "ground": "#f0f0f0" + }, + { + "text": "Comment", + "bounds": [ + 113.9, + 71.4, + 69.2, + 13.4 + ], + "confidence": 1, + "ink": "#575859", + "ground": "#f5f5f5" + }, + { + "text": "Fill & Sign", + "bounds": [ + 223.3, + 71.0, + 71.4, + 16.0 + ], + "confidence": 1, + "ink": "#3e86e8", + "ground": "#f1f6fc" + }, + { + "text": "Pages", + "bounds": [ + 334.9, + 71.4, + 42.4, + 15.6 + ], + "confidence": 1, + "ink": "#4a4b4b", + "ground": "#f2f2f3" + }, + { + "text": "Combine", + "bounds": [ + 417.5, + 71.4, + 62.5, + 13.4 + ], + "confidence": 1, + "ink": "#535354", + "ground": "#f3f3f4" + }, + { + "text": "Edit", + "bounds": [ + 518.0, + 71.4, + 33.5, + 15.6 + ], + "confidence": 1, + "ink": "#49494a", + "ground": "#f4f4f4" + }, + { + "text": "Pages", + "bounds": [ + 100.5, + 127.2, + 44.7, + 17.8 + ], + "confidence": 1, + "ink": "#3c3d3d", + "ground": "#f3f3f3" + }, + { + "text": "This form has 6 fields. 2 required fields are empty.", + "bounds": [ + 386.2, + 127.0, + 292.5, + 16.0 + ], + "confidence": 1, + "ink": "#525354", + "ground": "#f3f4f5" + }, + { + "text": "Pages", + "bounds": [ + 24.6, + 167.3, + 33.5, + 13.4 + ], + "confidence": 1, + "ink": "#508dea", + "ground": "#dfebfc" + }, + { + "text": "Outline", + "bounds": [ + 22.3, + 249.9, + 40.2, + 13.4 + ], + "confidence": 1, + "ink": "#888889", + "ground": "#f8f8f8" + }, + { + "text": "Comments", + "bounds": [ + 13.4, + 334.6, + 58.1, + 13.5 + ], + "confidence": 1, + "ink": "#959595", + "ground": "#f9f9f9" + }, + { + "text": "Search", + "bounds": [ + 22.3, + 419.4, + 40.2, + 11.2 + ], + "confidence": 1, + "ink": "#989899", + "ground": "#f8f8f8" + }, + { + "text": "X", + "bounds": [ + 283.5, + 127.2, + 17.9, + 17.8 + ], + "confidence": 1, + "ink": "#5e5f5f", + "ground": "#f9f9f9" + }, + { + "text": "RIVERSIDE RESIDENCES", + "bounds": [ + 167.4, + 203.0, + 75.9, + 11.2 + ], + "confidence": 1, + "ink": "#a0a0a1", + "ground": "#f4f3f4" + }, + { + "text": "1", + "bounds": [ + 189.8, + 428.3, + 11.2, + 13.4 + ], + "confidence": 1, + "ink": "#242526", + "ground": "#f3f3f3" + }, + { + "text": "2", + "bounds": [ + 189.8, + 700.5, + 11.2, + 15.6 + ], + "confidence": 1, + "ink": "#4f4f50", + "ground": "#f6f6f6" + }, + { + "text": "V", + "bounds": [ + 192.0, + 908.0, + 13.4, + 11.2 + ], + "confidence": 1, + "ink": "#5f5f60", + "ground": "#fbfbfb" + }, + { + "text": "RIVERSIDE RESIDENCES", + "bounds": [ + 636.3, + 211.9, + 308.1, + 22.3 + ], + "confidence": 1, + "ink": "#434343", + "ground": "#f4f4f4" + }, + { + "text": "RESIDENTIAL LEASE AGREEMENT", + "bounds": [ + 663.1, + 245.0, + 252.3, + 14 + ], + "confidence": 1, + "ink": "#4d4d4d", + "ground": "#f5f5f5" + }, + { + "text": "This Residential Lease Agreement (\"Agreement\") is made between Riverside", + "bounds": [ + 495.6, + 287.8, + 446.5, + 18.2 + ], + "confidence": 1, + "ink": "#787879", + "ground": "#f8f8f8" + }, + { + "text": "Residences LLC (\"Landlord\") and the Tenant named below.", + "bounds": [ + 495.6, + 307.9, + 346.0, + 15.6 + ], + "confidence": 1, + "ink": "#808081", + "ground": "#f8f8f8" + }, + { + "text": "1. PARTIES", + "bounds": [ + 495.6, + 341.0, + 73.8, + 16.4 + ], + "confidence": 1, + "ink": "#525252", + "ground": "#f7f7f8" + }, + { + "text": "Tenant name:", + "bounds": [ + 515.7, + 368.0, + 75.9, + 13.5 + ], + "confidence": 1, + "ink": "#828283", + "ground": "#f6f6f6" + }, + { + "text": "Landlord:", + "bounds": [ + 515.7, + 394.9, + 55.8, + 11.2 + ], + "confidence": 1, + "ink": "#797a7a", + "ground": "#f4f4f5" + }, + { + "text": "Maya Chen", + "bounds": [ + 600.5, + 367.9, + 60.4, + 13.7 + ], + "confidence": 1, + "ink": "#606367", + "ground": "#eceff4" + }, + { + "text": "Riverside Residences LLC", + "bounds": [ + 602.8, + 392.6, + 147.3, + 13.4 + ], + "confidence": 1, + "ink": "#7b7b7c", + "ground": "#f6f6f6" + }, + { + "text": "2. PREMISES", + "bounds": [ + 495.5, + 420.7, + 89.6, + 17.4 + ], + "confidence": 1, + "ink": "#565657", + "ground": "#f7f7f7" + }, + { + "text": "Address:", + "bounds": [ + 518.0, + 448.4, + 49.1, + 11.2 + ], + "confidence": 1, + "ink": "#7c7d7e", + "ground": "#f5f5f5" + }, + { + "text": "123 Riverfront Drive, Portland, OR 97201", + "bounds": [ + 582.7, + 446.0, + 230.0, + 16 + ], + "confidence": 1, + "ink": "#777878", + "ground": "#f7f7f7" + }, + { + "text": "Unit:", + "bounds": [ + 518.0, + 473.0, + 31.3, + 13.4 + ], + "confidence": 1, + "ink": "#808081", + "ground": "#f8f8f8" + }, + { + "text": "4B", + "bounds": [ + 578.2, + 473.0, + 20.1, + 13.4 + ], + "confidence": 1, + "ink": "#4d4f51", + "ground": "#eceff4" + }, + { + "text": "3. TERM", + "bounds": [ + 495.5, + 505.7, + 60.6, + 17.0 + ], + "confidence": 1, + "ink": "#4e4e4f", + "ground": "#f7f7f7" + }, + { + "text": "Start date:", + "bounds": [ + 517.8, + 530.4, + 56.1, + 14.6 + ], + "confidence": 1, + "ink": "#808081", + "ground": "#f8f8f8" + }, + { + "text": "1 November 2026", + "bounds": [ + 584.9, + 531.0, + 96, + 13.4 + ], + "confidence": 1, + "ink": "#646568", + "ground": "#f0f2f5" + }, + { + "text": "End date:", + "bounds": [ + 723.3, + 531.0, + 49.1, + 13.4 + ], + "confidence": 1, + "ink": "#838384", + "ground": "#f7f7f7" + }, + { + "text": "(month-to-month thereafter)", + "bounds": [ + 870.7, + 531.0, + 142.9, + 16.0 + ], + "confidence": 1, + "ink": "#888889", + "ground": "#f8f8f8" + }, + { + "text": "4. RENT", + "bounds": [ + 495.6, + 564.0, + 60.3, + 18.3 + ], + "confidence": 1, + "ink": "#4c4d4d", + "ground": "#f6f6f7" + }, + { + "text": "Monthly rent: $", + "bounds": [ + 518.0, + 591.0, + 89.3, + 14 + ], + "confidence": 1, + "ink": "#7f7f80", + "ground": "#f7f7f7" + }, + { + "text": "Rent is due on the first day of each month.", + "bounds": [ + 518.0, + 613.5, + 225.5, + 13.5 + ], + "confidence": 1, + "ink": "#8b8b8c", + "ground": "#f8f8f8" + }, + { + "text": "5. SIGNATURES", + "bounds": [ + 497.9, + 649.0, + 104.9, + 14 + ], + "confidence": 1, + "ink": "#454646", + "ground": "#f4f4f4" + }, + { + "text": "By signing below, theparties agree to the terms of this Agreement.", + "bounds": [ + 518.0, + 669.0, + 337.1, + 16.0 + ], + "confidence": 1, + "ink": "#838384", + "ground": "#f9f9f9" + }, + { + "text": "Landlord", + "bounds": [ + 495.6, + 700.5, + 53.6, + 13.5 + ], + "confidence": 1, + "ink": "#616262", + "ground": "#f5f5f6" + }, + { + "text": "Signature:", + "bounds": [ + 497.9, + 725.1, + 51.3, + 13.4 + ], + "confidence": 1, + "ink": "#868687", + "ground": "#f7f7f7" + }, + { + "text": "Name:", + "bounds": [ + 497.9, + 749.6, + 35.7, + 13.4 + ], + "confidence": 1, + "ink": "#808081", + "ground": "#f7f7f8" + }, + { + "text": "Date:", + "bounds": [ + 497.9, + 774.1, + 29.0, + 13.4 + ], + "confidence": 1, + "ink": "#878789", + "ground": "#f7f7f7" + }, + { + "text": "Riverside Residences LLC", + "bounds": [ + 558.1, + 749.6, + 129.5, + 13.4 + ], + "confidence": 1, + "ink": "#7f7f80", + "ground": "#f6f6f7" + }, + { + "text": "Tenant", + "bounds": [ + 783.6, + 700.5, + 42.4, + 11.2 + ], + "confidence": 1, + "ink": "#666667", + "ground": "#f6f6f6" + }, + { + "text": "Signature:", + "bounds": [ + 785.9, + 722.8, + 51.3, + 15.6 + ], + "confidence": 1, + "ink": "#929293", + "ground": "#f7f7f7" + }, + { + "text": "Name:", + "bounds": [ + 785.9, + 749.6, + 35.7, + 13.4 + ], + "confidence": 1, + "ink": "#939394", + "ground": "#f8f8f9" + }, + { + "text": "Date:", + "bounds": [ + 785.9, + 774.1, + 31.3, + 11.2 + ], + "confidence": 1, + "ink": "#8f8f90", + "ground": "#f9f9f9" + }, + { + "text": "Maya Chen", + "bounds": [ + 843.9, + 749.6, + 62.5, + 13.4 + ], + "confidence": 1, + "ink": "#858586", + "ground": "#f7f7f7" + }, + { + "text": "Initials:", + "bounds": [ + 497.9, + 805.4, + 42.4, + 13.4 + ], + "confidence": 1, + "ink": "#8d8c8d", + "ground": "#f9f9f9" + }, + { + "text": "Initials:", + "bounds": [ + 788.1, + 805.4, + 40.2, + 11.2 + ], + "confidence": 1, + "ink": "#8c8c8c", + "ground": "#f5f5f5" + }, + { + "text": "MC", + "bounds": [ + 917.6, + 832.1, + 35.7, + 20.1 + ], + "confidence": 1, + "ink": "#2a2a2a", + "ground": "#f5f5f6" + }, + { + "text": "Page 1 of 4", + "bounds": [ + 475.5, + 863.3, + 55.9, + 13.6 + ], + "confidence": 1, + "ink": "#7f7f80", + "ground": "#f9f9f9" + }, + { + "text": "Riverside Residences LLC", + "bounds": [ + 759.1, + 863.0, + 120.6, + 14 + ], + "confidence": 1, + "ink": "#7f7f80", + "ground": "#f5f5f6" + }, + { + "text": "<", + "bounds": [ + 584.9, + 914.7, + 11.2, + 13.4 + ], + "confidence": 1, + "ink": "#484849", + "ground": "#f8f8f8" + }, + { + "text": "1 / 12", + "bounds": [ + 618.4, + 914.7, + 40.2, + 15.6 + ], + "confidence": 1, + "ink": "#535353", + "ground": "#f9f9f9" + }, + { + "text": "100%", + "bounds": [ + 783.6, + 914.7, + 38.0, + 13.4 + ], + "confidence": 1, + "ink": "#4f5050", + "ground": "#f6f6f6" + }, + { + "text": "+", + "bounds": [ + 841.7, + 912.5, + 17.9, + 20.1 + ], + "confidence": 1, + "ink": "#6e6e6f", + "ground": "#fcfcfc" + }, + { + "text": "Site Survey Photos.pdf", + "bounds": [ + 1004.7, + 20.0, + 129.5, + 16 + ], + "confidence": 1, + "ink": "#585959", + "ground": "#eeeeee" + }, + { + "text": "+ Open", + "bounds": [ + 1212.3, + 20.0, + 55.8, + 16 + ], + "confidence": 1, + "ink": "#5d5d5e", + "ground": "#f0f0f1" + }, + { + "text": "Q Search in document", + "bounds": [ + 1009.1, + 69.0, + 147.3, + 18.0 + ], + "confidence": 1, + "ink": "#9c9c9d", + "ground": "#fafafa" + }, + { + "text": "Highlight fields", + "bounds": [ + 998.0, + 127.0, + 93.8, + 16.0 + ], + "confidence": 1, + "ink": "#555657", + "ground": "#f1f3f4" + }, + { + "text": "Fields", + "bounds": [ + 1187.7, + 127.2, + 44.7, + 17.8 + ], + "confidence": 1, + "ink": "#464647", + "ground": "#f6f6f6" + }, + { + "text": "Field name", + "bounds": [ + 1187.7, + 178.3, + 64.8, + 13.7 + ], + "confidence": 1, + "ink": "#828283", + "ground": "#f7f7f7" + }, + { + "text": "Tenant name", + "bounds": [ + 1190.0, + 227.6, + 82.6, + 15.6 + ], + "confidence": 1, + "ink": "#525353", + "ground": "#f6f6f6" + }, + { + "text": "Unit", + "bounds": [ + 1192.2, + 274.4, + 29.0, + 13.4 + ], + "confidence": 1, + "ink": "#5a5a5b", + "ground": "#f6f6f6" + }, + { + "text": "Start date", + "bounds": [ + 1189.9, + 319.0, + 67.0, + 18.0 + ], + "confidence": 1, + "ink": "#575758", + "ground": "#f7f7f7" + }, + { + "text": "End date", + "bounds": [ + 1190.0, + 365.9, + 60.3, + 18.1 + ], + "confidence": 1, + "ink": "#59595a", + "ground": "#f8f8f8" + }, + { + "text": "Monthly rent *", + "bounds": [ + 1192.1, + 410.3, + 91.6, + 20.5 + ], + "confidence": 1, + "ink": "#e85652", + "ground": "#fdf8f8" + }, + { + "text": "Initials (Landlord) *", + "bounds": [ + 1187.7, + 457.0, + 125.0, + 20.4 + ], + "confidence": 1, + "ink": "#ed4f4a", + "ground": "#fdf6f6" + }, + { + "text": "Initials (Tenant)*", + "bounds": [ + 1187.5, + 505.3, + 112.0, + 22.2 + ], + "confidence": 1, + "ink": "#ed5450", + "ground": "#fdf8f8" + }, + { + "text": "5", + "bounds": [ + 1330.6, + 69.2, + 15.6, + 20.1 + ], + "confidence": 1, + "ink": "#6b6a6b", + "ground": "#fafafa" + }, + { + "text": "Save", + "bounds": [ + 1460.1, + 71.4, + 33.5, + 13.4 + ], + "confidence": 1, + "ink": "#cae3f9", + "ground": "#1971e6" + }, + { + "text": "Value", + "bounds": [ + 1352.9, + 178.4, + 33.5, + 13.5 + ], + "confidence": 1, + "ink": "#8e8e8f", + "ground": "#f9f9f9" + }, + { + "text": "Maya Chen", + "bounds": [ + 1352.8, + 227.0, + 74.0, + 21.2 + ], + "confidence": 1, + "ink": "#656566", + "ground": "#f9f9f9" + }, + { + "text": "4B", + "bounds": [ + 1352.9, + 274.4, + 22.3, + 15.6 + ], + "confidence": 1, + "ink": "#5c5d5d", + "ground": "#f7f7f7" + }, + { + "text": "1 November 2026", + "bounds": [ + 1355.2, + 319.0, + 109.4, + 16 + ], + "confidence": 1, + "ink": "#606061", + "ground": "#f6f6f6" + }, + { + "text": "Empty", + "bounds": [ + 1352.9, + 365.9, + 42.4, + 20.1 + ], + "confidence": 1, + "ink": "#b7b7b8", + "ground": "#fbfbfb" + }, + { + "text": "Empty", + "bounds": [ + 1352.9, + 412.7, + 42.4, + 20.1 + ], + "confidence": 1, + "ink": "#b9b9ba", + "ground": "#fbfbfb" + }, + { + "text": "Empty", + "bounds": [ + 1352.9, + 461.8, + 42.4, + 20.1 + ], + "confidence": 1, + "ink": "#b8b8b9", + "ground": "#fbfbfb" + }, + { + "text": "Empty", + "bounds": [ + 1352.9, + 508.7, + 42.4, + 20.1 + ], + "confidence": 1, + "ink": "#b5b5b6", + "ground": "#fbfbfb" + }, + { + "text": "T Add text", + "bounds": [ + 1205.5, + 713.7, + 85.0, + 20.6 + ], + "confidence": 1, + "ink": "#585859", + "ground": "#f7f7f8" + }, + { + "text": "Add date", + "bounds": [ + 1239.0, + 774.0, + 55.9, + 13.6 + ], + "confidence": 1, + "ink": "#5b5b5c", + "ground": "#f3f3f3" + }, + { + "text": "MC Add initials", + "bounds": [ + 1203.3, + 827.7, + 111.6, + 15.6 + ], + "confidence": 1, + "ink": "#555656", + "ground": "#f6f6f6" + }, + { + "text": "\u2022 Saved", + "bounds": [ + 22.3, + 986.0, + 62.5, + 16 + ], + "confidence": 1, + "ink": "#508c6d", + "ground": "#f8f9f9" + }, + { + "text": "14.6 of 64 MB used", + "bounds": [ + 1395.3, + 986.0, + 116.1, + 16.0 + ], + "confidence": 1, + "ink": "#606061", + "ground": "#f6f6f6" + } + ], + "surfaces": [ + { + "bounds": [ + 324, + 157, + 850, + 816 + ], + "fill": "#fdfdfd", + "border": "#ececec", + "radius": 144 + }, + { + "bounds": [ + 422, + 166, + 652, + 801 + ], + "fill": "#fdfdfd", + "border": "#ededed", + "radius": 7 + }, + { + "bounds": [ + 86, + 159, + 239, + 737 + ], + "fill": "#fdfdfd", + "border": "#fcfcfc", + "radius": 0 + }, + { + "bounds": [ + 327, + 109, + 845, + 48 + ], + "fill": "#f7fafb", + "border": "#f8fbfc", + "radius": 1 + }, + { + "bounds": [ + 117, + 184, + 165, + 238 + ], + "fill": "#fdfdfd", + "border": "#fcfdfe", + "radius": 0 + }, + { + "bounds": [ + 118, + 475, + 156, + 221 + ], + "fill": "#fdfdfd", + "border": "#fcfcfc", + "radius": 0 + }, + { + "bounds": [ + 560, + 896, + 417, + 54 + ], + "fill": "#fdfdfd", + "border": "#f6f6f6", + "radius": 0 + }, + { + "bounds": [ + 86, + 898, + 239, + 73 + ], + "fill": "#fdfdfd", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1193, + 815, + 325, + 44 + ], + "fill": "#fdfdfd", + "border": "#fbfcfd", + "radius": 0 + }, + { + "bounds": [ + 1193, + 760, + 325, + 43 + ], + "fill": "#fdfdfd", + "border": "#fbfbfc", + "radius": 0 + }, + { + "bounds": [ + 1193, + 702, + 325, + 43 + ], + "fill": "#fdfdfd", + "border": "#fcfdfe", + "radius": 0 + }, + { + "bounds": [ + 1196, + 705, + 319, + 37 + ], + "fill": "#fdfdfd", + "border": "#fcfcfe", + "radius": 1 + }, + { + "bounds": [ + 1196, + 818, + 318, + 37 + ], + "fill": "#fdfdfd", + "border": "#fdfdfe", + "radius": 1 + }, + { + "bounds": [ + 1196, + 763, + 318, + 37 + ], + "fill": "#fdfdfd", + "border": "#fbfcfd", + "radius": 0 + }, + { + "bounds": [ + 86, + 109, + 238, + 48 + ], + "fill": "#fdfdfd", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 996, + 58, + 281, + 40 + ], + "fill": "#fdfdfd", + "border": "#fefefd", + "radius": 0 + }, + { + "bounds": [ + 595, + 359, + 391, + 27 + ], + "fill": "#f4f7fc", + "border": "#fdfdfe", + "radius": 0 + }, + { + "bounds": [ + 482, + 7, + 251, + 41 + ], + "fill": "#f1f1f1", + "border": "#fcfcfc", + "radius": 3 + }, + { + "bounds": [ + 6, + 6, + 241, + 42 + ], + "fill": "#f1f1f1", + "border": "#f5f5f6", + "radius": 3 + }, + { + "bounds": [ + 963, + 7, + 230, + 41 + ], + "fill": "#f1f1f1", + "border": "#f8f9f9", + "radius": 4 + }, + { + "bounds": [ + 1000, + 61, + 274, + 34 + ], + "fill": "#fdfdfd", + "border": "#fdfdfd", + "radius": 1 + }, + { + "bounds": [ + 738, + 7, + 223, + 41 + ], + "fill": "#f1f1f1", + "border": "#fbfbfc", + "radius": 3 + }, + { + "bounds": [ + 598, + 363, + 384, + 20 + ], + "fill": "#f4f7fc", + "border": "#eff3f9", + "radius": 0 + }, + { + "bounds": [ + 599, + 582, + 194, + 26 + ], + "fill": "#fef7f6", + "border": "#fefdfd", + "radius": 0 + }, + { + "bounds": [ + 10, + 123, + 64, + 65 + ], + "fill": "#e3edfc", + "border": "#fdfdfd", + "radius": 4 + }, + { + "bounds": [ + 578, + 524, + 131, + 26 + ], + "fill": "#f4f7fb", + "border": "#fbfcfd", + "radius": 6 + }, + { + "bounds": [ + 615, + 586, + 175, + 19 + ], + "fill": "#fef7f6", + "border": "#fdf4f3", + "radius": 0 + }, + { + "bounds": [ + 1437, + 60, + 82, + 36 + ], + "fill": "#156ce4", + "border": "#fcfcfe", + "radius": 6 + }, + { + "bounds": [ + 1203, + 8, + 75, + 37 + ], + "fill": "#f2f2f2", + "border": "#eeeeef", + "radius": 4 + }, + { + "bounds": [ + 572, + 466, + 95, + 27 + ], + "fill": "#f4f7fc", + "border": "#fbfcfd", + "radius": 6 + }, + { + "bounds": [ + 582, + 528, + 124, + 19 + ], + "fill": "#f2f5f9", + "border": "#eaeefa", + "radius": 0 + }, + { + "bounds": [ + 777, + 524, + 85, + 26 + ], + "fill": "#f5f7fc", + "border": "#fcfdfd", + "radius": 0 + }, + { + "bounds": [ + 576, + 469, + 87, + 21 + ], + "fill": "#f4f7fc", + "border": "#f2f7fc", + "radius": 1 + }, + { + "bounds": [ + 780, + 528, + 79, + 19 + ], + "fill": "#f5f7fc", + "border": "#f2f5fa", + "radius": 1 + }, + { + "bounds": [ + 918, + 826, + 41, + 31 + ], + "fill": "#f9f9fa", + "border": "#e2e9f8", + "radius": 0 + }, + { + "bounds": [ + 555, + 797, + 41, + 29 + ], + "fill": "#f4f6fb", + "border": "#fafbfd", + "radius": 0 + }, + { + "bounds": [ + 1102, + 121, + 43, + 24 + ], + "fill": "#1a76ef", + "border": "#f5f9fb", + "radius": 6 + } + ], + "dividers": [ + { + "axis": "h", + "at": 46, + "from": 731, + "to": 1536, + "color": "#dadadb" + }, + { + "axis": "h", + "at": 47, + "from": 1193, + "to": 1536, + "color": "#f3f3f4" + }, + { + "axis": "h", + "at": 107, + "from": 328, + "to": 1175, + "color": "#dedfe1" + }, + { + "axis": "h", + "at": 157, + "from": 325, + "to": 1171, + "color": "#e0e1e2" + }, + { + "axis": "h", + "at": 158, + "from": 1172, + "to": 1536, + "color": "#e0e1e2" + }, + { + "axis": "h", + "at": 360, + "from": 597, + "to": 982, + "color": "#b8cfeb" + }, + { + "axis": "h", + "at": 361, + "from": 596, + "to": 983, + "color": "#98b7e1" + }, + { + "axis": "h", + "at": 362, + "from": 647, + "to": 984, + "color": "#fbfdfe" + }, + { + "axis": "h", + "at": 381, + "from": 659, + "to": 981, + "color": "#fdfefe" + }, + { + "axis": "h", + "at": 382, + "from": 618, + "to": 981, + "color": "#c7d8ef" + }, + { + "axis": "h", + "at": 383, + "from": 618, + "to": 981, + "color": "#a0bae1" + }, + { + "axis": "h", + "at": 672, + "from": 1181, + "to": 1528, + "color": "#e2e2e3" + }, + { + "axis": "h", + "at": 703, + "from": 1196, + "to": 1514, + "color": "#678fcd" + }, + { + "axis": "h", + "at": 704, + "from": 1195, + "to": 1515, + "color": "#c5d4eb" + }, + { + "axis": "h", + "at": 742, + "from": 1195, + "to": 1514, + "color": "#5382c1" + }, + { + "axis": "h", + "at": 743, + "from": 1197, + "to": 1512, + "color": "#eef2f9" + }, + { + "axis": "h", + "at": 760, + "from": 1200, + "to": 1510, + "color": "#f5f6fb" + }, + { + "axis": "h", + "at": 761, + "from": 1195, + "to": 1514, + "color": "#477cc4" + }, + { + "axis": "h", + "at": 762, + "from": 1194, + "to": 1515, + "color": "#f0f4fa" + }, + { + "axis": "h", + "at": 799, + "from": 1195, + "to": 1515, + "color": "#d9e2ef" + }, + { + "axis": "h", + "at": 800, + "from": 1196, + "to": 1514, + "color": "#6a91c9" + }, + { + "axis": "h", + "at": 816, + "from": 1196, + "to": 1514, + "color": "#668fce" + }, + { + "axis": "h", + "at": 817, + "from": 1195, + "to": 1515, + "color": "#c0d1ec" + }, + { + "axis": "h", + "at": 855, + "from": 1195, + "to": 1515, + "color": "#81a3d7" + }, + { + "axis": "h", + "at": 856, + "from": 1196, + "to": 1513, + "color": "#9fb9e1" + }, + { + "axis": "h", + "at": 897, + "from": 569, + "to": 967, + "color": "#dededf" + }, + { + "axis": "h", + "at": 945, + "from": 565, + "to": 970, + "color": "#f3f2f2" + }, + { + "axis": "h", + "at": 946, + "from": 567, + "to": 969, + "color": "#cecfcf" + }, + { + "axis": "h", + "at": 971, + "from": 0, + "to": 1536, + "color": "#e0e0e1" + }, + { + "axis": "h", + "at": 972, + "from": 0, + "to": 1536, + "color": "#f3f4f3" + }, + { + "axis": "v", + "at": 84, + "from": 158, + "to": 972, + "color": "#e3e3e3" + }, + { + "axis": "v", + "at": 119, + "from": 477, + "to": 694, + "color": "#cfcecf" + }, + { + "axis": "v", + "at": 272, + "from": 185, + "to": 418, + "color": "#e6edfb" + }, + { + "axis": "v", + "at": 273, + "from": 186, + "to": 420, + "color": "#0d68e8" + }, + { + "axis": "v", + "at": 274, + "from": 187, + "to": 419, + "color": "#348cf1" + }, + { + "axis": "v", + "at": 424, + "from": 175, + "to": 961, + "color": "#e1e1e2" + }, + { + "axis": "v", + "at": 425, + "from": 171, + "to": 963, + "color": "#f1f2f2" + }, + { + "axis": "v", + "at": 1070, + "from": 171, + "to": 963, + "color": "#eff0f0" + }, + { + "axis": "v", + "at": 1071, + "from": 173, + "to": 962, + "color": "#dfdfe1" + } + ], + "limits": [ + "Evidence for review: roles and widgets are assigned in the semantic map, not here." + ] +} diff --git a/apps/pdftools/design/source/07-fill-sign.png b/apps/pdftools/design/source/07-fill-sign.png new file mode 100644 index 00000000..21613a5c Binary files /dev/null and b/apps/pdftools/design/source/07-fill-sign.png differ diff --git a/apps/pdftools/design/source/07-fill-sign.prompt.txt b/apps/pdftools/design/source/07-fill-sign.prompt.txt new file mode 100644 index 00000000..dcc4360a --- /dev/null +++ b/apps/pdftools/design/source/07-fill-sign.prompt.txt @@ -0,0 +1,72 @@ +Create ONE original image: a single, complete desktop application screen for +"PDF Tools", a document app inside the OctoSense desktop. + +Requested image size: 1536 × 1024 pixels, landscape. Requested quality: high. +Show only the application's own window content, edge to edge, flat and +straight on. No operating-system menu bar, no dock, no window title bar or +traffic lights, no device frame, no perspective, no drop shadow around the +screen, no captions or annotations outside the interface. + +Visual language: an original, precise desktop document tool. The document is +the brightest thing on screen; the chrome is quiet, thin and well aligned. +Palette: a cool light grey desk behind the pages, white pages with a soft +shadow, graphite text, mid-grey secondary text, thin 1-pixel dividers in pale +grey, and ONE accent colour, a confident medium blue, used only for the +current mode, the selected item, focus and primary buttons. Yellow marks +highlighted text and search matches. Green means saved or done, red means an +error, both used sparingly. Corner radius about 8 px on panels, buttons and +fields; pages have square corners. Icons are simple outline glyphs with one +consistent stroke weight. Typography: a neutral sans-serif for the interface, +small and crisp (about 13–14 px), medium weight for panel titles; document +pages use a classic serif for body text and a sans-serif for their headings. +Do not imitate Adobe Acrobat's layout, icons, colours or branding, or any other +product's. + +Window layout, the same in every screen: +- Top row: document tabs. Each tab shows a small PDF glyph, the file name and + a close cross; the active tab is white, the others grey; a "+ Open" button + follows the last tab. +- Second row, the mode bar: the modes "Read", "Comment", "Fill & Sign", + "Pages", "Combine", "Edit" as text tabs, the current one in the accent + colour with a short underline; on the right a search field with a magnifier + and the placeholder "Search in document", then icon buttons for Undo and Redo, + and a "Save" button. +- Left: a narrow icon rail with four icons and tooltips' labels shown under + them in tiny text: "Pages", "Outline", "Comments", "Search". The selected + icon has a tinted square behind it. Next to the rail, an open panel about + 240 px wide when the screen calls for it. +- Centre: the canvas, a light grey desk with the document's pages stacked + vertically, centred, one page fully visible and the next starting below. +- Bottom centre of the canvas: a floating rounded pill with "‹", "3 / 24", + "›", a divider, "−", "100%", "+", and two small icons for "Fit width" and + "Fit page". +- Right: a panel about 300 px wide, only in screens that call for it. +- Bottom edge: a thin status line with "Saved" (with a small green dot) on the + left and "14.6 of 64 MB used" on the right. + +Use this exact fixture wherever it appears: +- Documents in the library: "Q3 2026 Board Report.pdf" (24 pages, 2.8 MB, + opened today), "Riverside Lease 2026.pdf" (12 pages, 1.1 MB, yesterday), + "Field Guide to Garden Birds.pdf" (7 pages, 16 KB, 3 Oct), "Invoice + INV-2041.pdf" (1 page, 84 KB, 1 Oct), "Site Survey Photos.pdf" (18 pages, + 9.6 MB, 28 Sep). +- The board report belongs to the fictional "Northwind Cooperative". Its + sections: Summary, Revenue, Operations, Outlook, Appendix. Page 3 is + "Revenue" with a short paragraph that includes the sentence "Revenue grew + 12% year over year, led by the Northwest region." and a simple bar chart + "Revenue by region, Q3 2026" with bars for Northwest, Coast, Valley, Metro. +- People: Maya Chen (the user), Jun Park and Ana Ruiz (reviewers). + +Render every piece of interface text and every control sharply enough to be +read and measured later. Labels, buttons, fields and panels will be rebuilt as +native components from this image; page contents are sample document art. + +This screen: mode "Fill & Sign". Tab "Riverside Lease 2026.pdf" active, page +1 of 12 on the canvas: a residential lease form whose fields are outlined in +pale blue. Filled fields: "Tenant name: Maya Chen", "Unit: 4B", "Start date: +1 November 2026". Empty fields: "Monthly rent" (marked required) and two +"Initials" boxes. A banner above the canvas: "This form has 6 fields. 2 +required fields are empty." with a "Highlight fields" toggle. The right panel +"Fields" lists every field with its value or "Empty"; required ones have a red +asterisk. Below the list: "Add text", "Add date", "Add initials" buttons; an +initials box "MC" is being placed on the page. \ No newline at end of file diff --git a/apps/pdftools/design/source/08-edit.measured.json b/apps/pdftools/design/source/08-edit.measured.json new file mode 100644 index 00000000..eaf1bf58 --- /dev/null +++ b/apps/pdftools/design/source/08-edit.measured.json @@ -0,0 +1,1284 @@ +{ + "schema_version": 1, + "image": "08-edit.png", + "image_sha256": "5ad2add5b3fbdb5541b72c9f7bb3c857e4b1ddda0a068d6e1ac7b17f609f4bb6", + "script_sha256": "ea860507e32482b955d3ac577ab3cf9fc559f59ecc916c89175293932b54e0b7", + "size": [ + 1536, + 1024 + ], + "ocr_engine": "Apple Vision VNRecognizeTextRequest accurate, en-US (App Flow ocr.swift)", + "text": [ + { + "text": "Q3 2026 Board Report.pdf", + "bounds": [ + 53.6, + 20.0, + 169.7, + 16 + ], + "confidence": 1, + "ink": "#464747", + "ground": "#f5f5f5" + }, + { + "text": "Read", + "bounds": [ + 33.5, + 75.9, + 38.0, + 15.6 + ], + "confidence": 1, + "ink": "#676768", + "ground": "#f7f7f7" + }, + { + "text": "Pages", + "bounds": [ + 35.7, + 180.7, + 38.0, + 15.6 + ], + "confidence": 1, + "ink": "#7d8188", + "ground": "#e3ecf8" + }, + { + "text": "Outline", + "bounds": [ + 33.3, + 264.8, + 45.0, + 12.4 + ], + "confidence": 1, + "ink": "#88888a", + "ground": "#f8f8f8" + }, + { + "text": "Riverside Lease202.pdf", + "bounds": [ + 312.6, + 20.0, + 163.0, + 16 + ], + "confidence": 0.5, + "ink": "#646568", + "ground": "#f1f2f4" + }, + { + "text": "Comment", + "bounds": [ + 109.4, + 75.8, + 80.4, + 15.8 + ], + "confidence": 1, + "ink": "#707173", + "ground": "#fafafa" + }, + { + "text": "Fill & Sign", + "bounds": [ + 234.4, + 75.9, + 71.4, + 18.1 + ], + "confidence": 1, + "ink": "#676869", + "ground": "#f7f7f7" + }, + { + "text": "Pages", + "bounds": [ + 350.5, + 75.9, + 46.9, + 17.8 + ], + "confidence": 1, + "ink": "#6c6d6e", + "ground": "#f8f8f8" + }, + { + "text": "Combine", + "bounds": [ + 439.6, + 75.1, + 67.4, + 19.4 + ], + "confidence": 1, + "ink": "#636365", + "ground": "#f8f8f8" + }, + { + "text": "Comments", + "bounds": [ + 22.3, + 350.2, + 62.5, + 13.5 + ], + "confidence": 1, + "ink": "#858688", + "ground": "#f8f8f8" + }, + { + "text": "Search", + "bounds": [ + 33.5, + 435.0, + 42.4, + 13.4 + ], + "confidence": 1, + "ink": "#8e8e90", + "ground": "#f9f9f9" + }, + { + "text": "Field Guide to Garden Birds.pdf x", + "bounds": [ + 560.4, + 17.8, + 223.3, + 20.2 + ], + "confidence": 1, + "ink": "#68696b", + "ground": "#f3f4f5" + }, + { + "text": "Invoice INV-2041.pdf", + "bounds": [ + 837.2, + 20.0, + 136.2, + 16 + ], + "confidence": 1, + "ink": "#666769", + "ground": "#f3f3f5" + }, + { + "text": "Edit", + "bounds": [ + 549.2, + 75.9, + 33.5, + 15.6 + ], + "confidence": 1, + "ink": "#4582ed", + "ground": "#f5f9fd" + }, + { + "text": "Q", + "bounds": [ + 986.8, + 75.9, + 20.1, + 17.8 + ], + "confidence": 1, + "ink": "#3a3b3c", + "ground": "#f5f5f6" + }, + { + "text": "Site Survey Photos.pdf", + "bounds": [ + 1058.2, + 20.0, + 147.3, + 18 + ], + "confidence": 1, + "ink": "#68696b", + "ground": "#f3f4f5" + }, + { + "text": "Search in document", + "bounds": [ + 1024.7, + 78.0, + 131.7, + 13.5 + ], + "confidence": 1, + "ink": "#909193", + "ground": "#f7f8f8" + }, + { + "text": "+ Open", + "bounds": [ + 1277.0, + 20.0, + 60.4, + 18.1 + ], + "confidence": 1, + "ink": "#58595b", + "ground": "#f4f5f6" + }, + { + "text": "Save", + "bounds": [ + 1444.5, + 75.9, + 62.5, + 20.1 + ], + "confidence": 1, + "ink": "#c4dcfa", + "ground": "#0a66ee" + }, + { + "text": "X", + "bounds": [ + 1478.0, + 145.0, + 20.1, + 20.1 + ], + "confidence": 1, + "ink": "#555658", + "ground": "#fbfbfb" + }, + { + "text": "NORTHWIND COOPERATIVE", + "bounds": [ + 357.2, + 171.8, + 225.5, + 13.4 + ], + "confidence": 1, + "ink": "#5a5a5b", + "ground": "#f6f6f6" + }, + { + "text": "Summary", + "bounds": [ + 355.8, + 228.7, + 149.9, + 37.8 + ], + "confidence": 1, + "ink": "#373839", + "ground": "#f9f9f9" + }, + { + "text": "Northwind Cooperative delivered a solid performance in Q3 2026, with", + "bounds": [ + 357.2, + 321.0, + 580.5, + 25 + ], + "confidence": 1, + "ink": "#4a4a4b", + "ground": "#f8f8f8" + }, + { + "text": "strong revenue growth, disciplined cost management and continued", + "bounds": [ + 357.2, + 356.9, + 533.6, + 20.1 + ], + "confidence": 1, + "ink": "#424343", + "ground": "#f5f5f5" + }, + { + "text": "member value.", + "bounds": [ + 359.3, + 389.7, + 125.2, + 21.4 + ], + "confidence": 1, + "ink": "#474848", + "ground": "#f7f7f7" + }, + { + "text": "The board met on 21 September 2026 to review the", + "bounds": [ + 359.4, + 457.0, + 435.3, + 23 + ], + "confidence": 1, + "ink": "#464647", + "ground": "#f7f7f7" + }, + { + "text": "quarter's results.|", + "bounds": [ + 357.1, + 483.4, + 149.8, + 28.2 + ], + "confidence": 1, + "ink": "#3f3f3f", + "ground": "#f8f8f8" + }, + { + "text": "We advanced key operational initiatives across our regions and", + "bounds": [ + 359.4, + 551.0, + 493.4, + 22.4 + ], + "confidence": 1, + "ink": "#4b4b4c", + "ground": "#f8f8f8" + }, + { + "text": "reinforced our focus on member service, product quality and", + "bounds": [ + 357.2, + 584.5, + 480.0, + 22.5 + ], + "confidence": 1, + "ink": "#4b4b4c", + "ground": "#f7f7f7" + }, + { + "text": "sustainable practices.", + "bounds": [ + 357.2, + 620.0, + 174.1, + 25 + ], + "confidence": 1, + "ink": "#49494a", + "ground": "#f8f8f8" + }, + { + "text": "This report summarises financial results, operations highlights and", + "bounds": [ + 359.4, + 676.0, + 515.7, + 22.3 + ], + "confidence": 1, + "ink": "#4c4d4d", + "ground": "#f7f7f8" + }, + { + "text": "our outlook for the remainder of the year.", + "bounds": [ + 359.3, + 707.5, + 317.4, + 24.9 + ], + "confidence": 1, + "ink": "#4b4b4c", + "ground": "#f9f9f9" + }, + { + "text": "Edit text", + "bounds": [ + 1198.8, + 146.7, + 62.8, + 16.7 + ], + "confidence": 1, + "ink": "#4d4e4f", + "ground": "#f6f6f6" + }, + { + "text": "Paragraph 3", + "bounds": [ + 1198.7, + 199.9, + 80.8, + 19.7 + ], + "confidence": 1, + "ink": "#616162", + "ground": "#f8f8f9" + }, + { + "text": "Font", + "bounds": [ + 1198.9, + 254.3, + 33.5, + 15.6 + ], + "confidence": 1, + "ink": "#646465", + "ground": "#f8f8f9" + }, + { + "text": "Serif \u2022 11 pt", + "bounds": [ + 1210.0, + 294.4, + 89.3, + 15.8 + ], + "confidence": 1, + "ink": "#636465", + "ground": "#f8f8f8" + }, + { + "text": "Aligament", + "bounds": [ + 1198.9, + 348.0, + 64.7, + 15.6 + ], + "confidence": 1, + "ink": "#676768", + "ground": "#f6f6f6" + }, + { + "text": "Colour", + "bounds": [ + 1198.9, + 441.7, + 46.9, + 15.6 + ], + "confidence": 1, + "ink": "#565758", + "ground": "#f6f6f6" + }, + { + "text": "Black", + "bounds": [ + 1248.0, + 479.7, + 35.7, + 13.4 + ], + "confidence": 1, + "ink": "#5f6061", + "ground": "#f1f1f2" + }, + { + "text": "Apply", + "bounds": [ + 1243.5, + 555.5, + 44.7, + 17.8 + ], + "confidence": 1, + "ink": "#b9d5f9", + "ground": "#0964ed" + }, + { + "text": "V", + "bounds": [ + 1471.3, + 294.5, + 15.6, + 11.2 + ], + "confidence": 1, + "ink": "#4c4f50", + "ground": "#f7f7f7" + }, + { + "text": "V", + "bounds": [ + 1471.3, + 481.9, + 15.6, + 11.2 + ], + "confidence": 1, + "ink": "#646668", + "ground": "#fbfbfb" + }, + { + "text": "Cancel", + "bounds": [ + 1404.3, + 555.5, + 44.7, + 13.4 + ], + "confidence": 1, + "ink": "#505152", + "ground": "#f4f4f4" + }, + { + "text": "Q3 2026 Board Report", + "bounds": [ + 359.4, + 809.8, + 113.9, + 13.4 + ], + "confidence": 1, + "ink": "#939394", + "ground": "#f7f7f7" + }, + { + "text": "<", + "bounds": [ + 533.6, + 883.5, + 11.2, + 15.6 + ], + "confidence": 1, + "ink": "#444445", + "ground": "#fafafa" + }, + { + "text": "2 / 24", + "bounds": [ + 580.5, + 885.7, + 44.7, + 13.4 + ], + "confidence": 1, + "ink": "#5d5d5d", + "ground": "#f8f8f8" + }, + { + "text": "NORTHWIND COOPERATIVE", + "bounds": [ + 357.2, + 919.0, + 225.5, + 14 + ], + "confidence": 1, + "ink": "#585859", + "ground": "#f6f6f6" + }, + { + "text": "-", + "bounds": [ + 718.9, + 885.7, + 20.1, + 11.2 + ], + "confidence": 1, + "ink": "#363637", + "ground": "#fafafa" + }, + { + "text": "100%", + "bounds": [ + 772.5, + 885.7, + 38.0, + 13.4 + ], + "confidence": 1, + "ink": "#656666", + "ground": "#f5f5f5" + }, + { + "text": "+", + "bounds": [ + 843.9, + 883.5, + 17.9, + 17.8 + ], + "confidence": 1, + "ink": "#5d5d5e", + "ground": "#fbfbfb" + }, + { + "text": "Edited", + "bounds": [ + 49.1, + 977.0, + 42.5, + 13.7 + ], + "confidence": 1, + "ink": "#4c4c4d", + "ground": "#f2f2f2" + }, + { + "text": "14.6 of 64 MB used", + "bounds": [ + 1370.8, + 977.0, + 127.3, + 16 + ], + "confidence": 1, + "ink": "#68696a", + "ground": "#f6f7f7" + } + ], + "surfaces": [ + { + "bounds": [ + 4, + 3, + 1530, + 1010 + ], + "fill": "#fefefe", + "border": "#f6f7f9", + "radius": 0 + }, + { + "bounds": [ + 97, + 116, + 1433, + 839 + ], + "fill": "#fefefe", + "border": "#f8f9fb", + "radius": 1 + }, + { + "bounds": [ + 307, + 131, + 724, + 728 + ], + "fill": "#fefefe", + "border": "#f1f2f4", + "radius": 4 + }, + { + "bounds": [ + 1182, + 123, + 340, + 819 + ], + "fill": "#fefefe", + "border": "#f7f8fa", + "radius": 0 + }, + { + "bounds": [ + 1185, + 237, + 333, + 701 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 7, + 957, + 1523, + 52 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 7, + 118, + 92, + 838 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 311, + 867, + 716, + 88 + ], + "fill": "#fefefe", + "border": "#fefdfd", + "radius": 0 + }, + { + "bounds": [ + 348, + 431, + 488, + 98 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 355, + 443, + 474, + 79 + ], + "fill": "#fefefe", + "border": "#f8f9fc", + "radius": 0 + }, + { + "bounds": [ + 502, + 867, + 494, + 55 + ], + "fill": "#fefefe", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 1185, + 127, + 333, + 54 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1185, + 183, + 333, + 52 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1198, + 279, + 305, + 44 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1199, + 465, + 304, + 44 + ], + "fill": "#fefefe", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 1199, + 373, + 301, + 44 + ], + "fill": "#fdfdfe", + "border": "#fdfefe", + "radius": 0 + }, + { + "bounds": [ + 974, + 64, + 313, + 40 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 516, + 7, + 279, + 43 + ], + "fill": "#f5f6f8", + "border": "#f7f8fa", + "radius": 6 + }, + { + "bounds": [ + 1202, + 468, + 298, + 38 + ], + "fill": "#fefefe", + "border": "#fcfcfc", + "radius": 1 + }, + { + "bounds": [ + 1202, + 283, + 298, + 37 + ], + "fill": "#fefefe", + "border": "#fdfdfd", + "radius": 0 + }, + { + "bounds": [ + 270, + 7, + 248, + 43 + ], + "fill": "#f5f6f8", + "border": "#f7f8fa", + "radius": 0 + }, + { + "bounds": [ + 1018, + 7, + 236, + 43 + ], + "fill": "#f5f6f8", + "border": "#f7f8fa", + "radius": 0 + }, + { + "bounds": [ + 978, + 68, + 306, + 33 + ], + "fill": "#fefefe", + "border": "#f4f5f5", + "radius": 0 + }, + { + "bounds": [ + 797, + 7, + 218, + 43 + ], + "fill": "#f5f6f8", + "border": "#f7f8f9", + "radius": 1 + }, + { + "bounds": [ + 1351, + 542, + 152, + 40 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1200, + 543, + 139, + 39 + ], + "fill": "#0761ec", + "border": "#f8f9fd", + "radius": 3 + }, + { + "bounds": [ + 1355, + 545, + 144, + 34 + ], + "fill": "#fefefe", + "border": "#fdfdfd", + "radius": 1 + }, + { + "bounds": [ + 21, + 132, + 65, + 74 + ], + "fill": "#e3edfc", + "border": "#fefefe", + "radius": 4 + }, + { + "bounds": [ + 1434, + 64, + 90, + 39 + ], + "fill": "#0765ee", + "border": "#f9fbfe", + "radius": 4 + }, + { + "bounds": [ + 1270, + 376, + 81, + 37 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1354, + 376, + 75, + 37 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1268, + 10, + 84, + 33 + ], + "fill": "#f6f7f9", + "border": "#f7f8fa", + "radius": 0 + }, + { + "bounds": [ + 1431, + 376, + 66, + 37 + ], + "fill": "#fefefe", + "border": "#fefefe", + "radius": 0 + }, + { + "bounds": [ + 1202, + 376, + 66, + 37 + ], + "fill": "#edf3fd", + "border": "#f5f9fe", + "radius": 3 + } + ], + "dividers": [ + { + "axis": "h", + "at": 48, + "from": 1251, + "to": 1529, + "color": "#e7e8ea" + }, + { + "axis": "h", + "at": 65, + "from": 979, + "to": 1281, + "color": "#ececed" + }, + { + "axis": "h", + "at": 66, + "from": 977, + "to": 1283, + "color": "#dedfe1" + }, + { + "axis": "h", + "at": 100, + "from": 980, + "to": 1282, + "color": "#f1f2f3" + }, + { + "axis": "h", + "at": 101, + "from": 980, + "to": 1282, + "color": "#e3e5e7" + }, + { + "axis": "h", + "at": 116, + "from": 531, + "to": 1255, + "color": "#e8e9ec" + }, + { + "axis": "h", + "at": 117, + "from": 531, + "to": 1255, + "color": "#eff1f3" + }, + { + "axis": "h", + "at": 125, + "from": 1190, + "to": 1513, + "color": "#e8e9ec" + }, + { + "axis": "h", + "at": 132, + "from": 309, + "to": 1025, + "color": "#dee0e3" + }, + { + "axis": "h", + "at": 133, + "from": 310, + "to": 1025, + "color": "#e8e9ec" + }, + { + "axis": "h", + "at": 180, + "from": 1185, + "to": 1518, + "color": "#f3f4f5" + }, + { + "axis": "h", + "at": 181, + "from": 1185, + "to": 1518, + "color": "#eaecef" + }, + { + "axis": "h", + "at": 234, + "from": 1184, + "to": 1518, + "color": "#f3f4f5" + }, + { + "axis": "h", + "at": 235, + "from": 1184, + "to": 1518, + "color": "#e8e9ec" + }, + { + "axis": "h", + "at": 279, + "from": 359, + "to": 972, + "color": "#515252" + }, + { + "axis": "h", + "at": 280, + "from": 359, + "to": 972, + "color": "#c8c9ca" + }, + { + "axis": "h", + "at": 320, + "from": 1202, + "to": 1497, + "color": "#e7e8ea" + }, + { + "axis": "h", + "at": 466, + "from": 1204, + "to": 1497, + "color": "#e6e7e9" + }, + { + "axis": "h", + "at": 505, + "from": 1213, + "to": 1498, + "color": "#f5f6f7" + }, + { + "axis": "h", + "at": 506, + "from": 1203, + "to": 1498, + "color": "#e8eaec" + }, + { + "axis": "h", + "at": 521, + "from": 358, + "to": 825, + "color": "#96bdf2" + }, + { + "axis": "h", + "at": 522, + "from": 358, + "to": 825, + "color": "#2171ef" + }, + { + "axis": "h", + "at": 795, + "from": 359, + "to": 972, + "color": "#575858" + }, + { + "axis": "h", + "at": 796, + "from": 359, + "to": 972, + "color": "#aaabac" + }, + { + "axis": "h", + "at": 854, + "from": 312, + "to": 1015, + "color": "#f6f7f8" + }, + { + "axis": "h", + "at": 855, + "from": 309, + "to": 1027, + "color": "#dedfe2" + }, + { + "axis": "h", + "at": 919, + "from": 521, + "to": 975, + "color": "#dfe0e3" + }, + { + "axis": "h", + "at": 938, + "from": 1190, + "to": 1514, + "color": "#e4e6e8" + }, + { + "axis": "h", + "at": 955, + "from": 311, + "to": 1027, + "color": "#eeeff0" + }, + { + "axis": "h", + "at": 956, + "from": 311, + "to": 1028, + "color": "#e5e7ea" + }, + { + "axis": "h", + "at": 1009, + "from": 9, + "to": 1529, + "color": "#e7e9eb" + }, + { + "axis": "v", + "at": 5, + "from": 118, + "to": 955, + "color": "#e9ebed" + }, + { + "axis": "v", + "at": 6, + "from": 118, + "to": 955, + "color": "#f6f6f7" + }, + { + "axis": "v", + "at": 1183, + "from": 236, + "to": 931, + "color": "#e5e6e8" + }, + { + "axis": "v", + "at": 1184, + "from": 236, + "to": 931, + "color": "#f6f6f7" + }, + { + "axis": "v", + "at": 1518, + "from": 236, + "to": 649, + "color": "#e8e9eb" + }, + { + "axis": "v", + "at": 1530, + "from": 117, + "to": 957, + "color": "#e8e9eb" + }, + { + "axis": "v", + "at": 1531, + "from": 116, + "to": 957, + "color": "#f0f0f3" + } + ], + "limits": [ + "Evidence for review: roles and widgets are assigned in the semantic map, not here." + ] +} diff --git a/apps/pdftools/design/source/08-edit.png b/apps/pdftools/design/source/08-edit.png new file mode 100644 index 00000000..b1744395 Binary files /dev/null and b/apps/pdftools/design/source/08-edit.png differ diff --git a/apps/pdftools/design/source/08-edit.prompt.txt b/apps/pdftools/design/source/08-edit.prompt.txt new file mode 100644 index 00000000..a2e6e45d --- /dev/null +++ b/apps/pdftools/design/source/08-edit.prompt.txt @@ -0,0 +1,70 @@ +Create ONE original image: a single, complete desktop application screen for +"PDF Tools", a document app inside the OctoSense desktop. + +Requested image size: 1536 × 1024 pixels, landscape. Requested quality: high. +Show only the application's own window content, edge to edge, flat and +straight on. No operating-system menu bar, no dock, no window title bar or +traffic lights, no device frame, no perspective, no drop shadow around the +screen, no captions or annotations outside the interface. + +Visual language: an original, precise desktop document tool. The document is +the brightest thing on screen; the chrome is quiet, thin and well aligned. +Palette: a cool light grey desk behind the pages, white pages with a soft +shadow, graphite text, mid-grey secondary text, thin 1-pixel dividers in pale +grey, and ONE accent colour, a confident medium blue, used only for the +current mode, the selected item, focus and primary buttons. Yellow marks +highlighted text and search matches. Green means saved or done, red means an +error, both used sparingly. Corner radius about 8 px on panels, buttons and +fields; pages have square corners. Icons are simple outline glyphs with one +consistent stroke weight. Typography: a neutral sans-serif for the interface, +small and crisp (about 13–14 px), medium weight for panel titles; document +pages use a classic serif for body text and a sans-serif for their headings. +Do not imitate Adobe Acrobat's layout, icons, colours or branding, or any other +product's. + +Window layout, the same in every screen: +- Top row: document tabs. Each tab shows a small PDF glyph, the file name and + a close cross; the active tab is white, the others grey; a "+ Open" button + follows the last tab. +- Second row, the mode bar: the modes "Read", "Comment", "Fill & Sign", + "Pages", "Combine", "Edit" as text tabs, the current one in the accent + colour with a short underline; on the right a search field with a magnifier + and the placeholder "Search in document", then icon buttons for Undo and Redo, + and a "Save" button. +- Left: a narrow icon rail with four icons and tooltips' labels shown under + them in tiny text: "Pages", "Outline", "Comments", "Search". The selected + icon has a tinted square behind it. Next to the rail, an open panel about + 240 px wide when the screen calls for it. +- Centre: the canvas, a light grey desk with the document's pages stacked + vertically, centred, one page fully visible and the next starting below. +- Bottom centre of the canvas: a floating rounded pill with "‹", "3 / 24", + "›", a divider, "−", "100%", "+", and two small icons for "Fit width" and + "Fit page". +- Right: a panel about 300 px wide, only in screens that call for it. +- Bottom edge: a thin status line with "Saved" (with a small green dot) on the + left and "14.6 of 64 MB used" on the right. + +Use this exact fixture wherever it appears: +- Documents in the library: "Q3 2026 Board Report.pdf" (24 pages, 2.8 MB, + opened today), "Riverside Lease 2026.pdf" (12 pages, 1.1 MB, yesterday), + "Field Guide to Garden Birds.pdf" (7 pages, 16 KB, 3 Oct), "Invoice + INV-2041.pdf" (1 page, 84 KB, 1 Oct), "Site Survey Photos.pdf" (18 pages, + 9.6 MB, 28 Sep). +- The board report belongs to the fictional "Northwind Cooperative". Its + sections: Summary, Revenue, Operations, Outlook, Appendix. Page 3 is + "Revenue" with a short paragraph that includes the sentence "Revenue grew + 12% year over year, led by the Northwest region." and a simple bar chart + "Revenue by region, Q3 2026" with bars for Northwest, Coast, Valley, Metro. +- People: Maya Chen (the user), Jun Park and Ana Ruiz (reviewers). + +Render every piece of interface text and every control sharply enough to be +read and measured later. Labels, buttons, fields and panels will be rebuilt as +native components from this image; page contents are sample document art. + +This screen: mode "Edit". Page 2 of the board report on the canvas, a page of +serif text. One paragraph is selected for editing with a blue outline and a +text cursor; it now reads "The board met on 21 September 2026 to review the +quarter's results." with the date just changed. The right panel "Edit text" +shows "Paragraph 3", font "Serif · 11 pt", alignment buttons (left selected), +colour "Black", and two buttons "Apply" (primary) and "Cancel". The status line +shows "Edited" with an amber dot instead of "Saved". \ No newline at end of file diff --git a/apps/pdftools/design/source/09-dark-reading.measured.json b/apps/pdftools/design/source/09-dark-reading.measured.json new file mode 100644 index 00000000..b571f35d --- /dev/null +++ b/apps/pdftools/design/source/09-dark-reading.measured.json @@ -0,0 +1,1264 @@ +{ + "schema_version": 1, + "image": "09-dark-reading.png", + "image_sha256": "f61f8b32d0359fbb41909b0e58bc7d0b5314c300a1bd72eb87f3c5f6c194e54b", + "script_sha256": "ea860507e32482b955d3ac577ab3cf9fc559f59ecc916c89175293932b54e0b7", + "size": [ + 1536, + 1024 + ], + "ocr_engine": "Apple Vision VNRecognizeTextRequest accurate, en-US (App Flow ocr.swift)", + "text": [ + { + "text": "Read", + "bounds": [ + 28.9, + 77.7, + 45.0, + 18.7 + ], + "confidence": 1, + "ink": "#468bd8", + "ground": "#1f2630" + }, + { + "text": "Pages", + "bounds": [ + 33.5, + 178.5, + 40.2, + 15.6 + ], + "confidence": 1, + "ink": "#989c9e", + "ground": "#21272b" + }, + { + "text": "=", + "bounds": [ + 44.7, + 243.2, + 20.1, + 15.6 + ], + "confidence": 1, + "ink": "#b7c6dc", + "ground": "#285494" + }, + { + "text": "Outline", + "bounds": [ + 31.2, + 267.4, + 47.1, + 14.1 + ], + "confidence": 1, + "ink": "#bac7dd", + "ground": "#285290" + }, + { + "text": "Comments", + "bounds": [ + 20.1, + 356.7, + 69.3, + 13.8 + ], + "confidence": 1, + "ink": "#a4a8aa", + "ground": "#232a2e" + }, + { + "text": "Search", + "bounds": [ + 31.1, + 443.4, + 45.0, + 14.4 + ], + "confidence": 1, + "ink": "#9da0a2", + "ground": "#22282d" + }, + { + "text": "Q3 2026 Board Report.pdf X", + "bounds": [ + 62.5, + 24.5, + 216.6, + 17.8 + ], + "confidence": 1, + "ink": "#d1d3d4", + "ground": "#22282c" + }, + { + "text": "Comment", + "bounds": [ + 107.1, + 78.0, + 75.9, + 15.8 + ], + "confidence": 1, + "ink": "#a9abad", + "ground": "#22292d" + }, + { + "text": "Fill & Sign", + "bounds": [ + 214.2, + 77.5, + 78.4, + 19.0 + ], + "confidence": 1, + "ink": "#aeb0b1", + "ground": "#232a2e" + }, + { + "text": "Outline", + "bounds": [ + 120.5, + 135.8, + 58.2, + 14.0 + ], + "confidence": 1, + "ink": "#d7d9da", + "ground": "#282e32" + }, + { + "text": "Summary", + "bounds": [ + 156.2, + 186.9, + 69.5, + 18.8 + ], + "confidence": 1, + "ink": "#94979a", + "ground": "#1b2126" + }, + { + "text": "Revenue", + "bounds": [ + 156.3, + 229.8, + 64.7, + 13.4 + ], + "confidence": 1, + "ink": "#ccdff5", + "ground": "#2f75d8" + }, + { + "text": "Revenue by region", + "bounds": [ + 194.2, + 272.0, + 131.7, + 18.0 + ], + "confidence": 1, + "ink": "#94989b", + "ground": "#1c2328" + }, + { + "text": "Operations", + "bounds": [ + 158.4, + 314.2, + 80.5, + 16.3 + ], + "confidence": 1, + "ink": "#999c9f", + "ground": "#1e2429" + }, + { + "text": "Outlook", + "bounds": [ + 158.4, + 356.5, + 60.5, + 14.3 + ], + "confidence": 1, + "ink": "#95989a", + "ground": "#1c2328" + }, + { + "text": "Appendix", + "bounds": [ + 158.5, + 399.3, + 71.4, + 17.8 + ], + "confidence": 1, + "ink": "#9b9ea1", + "ground": "#1e252a" + }, + { + "text": "Riverside Lease 2026.pdf", + "bounds": [ + 339.3, + 24.5, + 169.7, + 17.8 + ], + "confidence": 1, + "ink": "#96999b", + "ground": "#1c2225" + }, + { + "text": "Pages", + "bounds": [ + 325.8, + 77.6, + 49.5, + 18.9 + ], + "confidence": 1, + "ink": "#b0b1b2", + "ground": "#232a2e" + }, + { + "text": "Combine", + "bounds": [ + 406.3, + 78.1, + 71.4, + 15.6 + ], + "confidence": 1, + "ink": "#a8a9ab", + "ground": "#22282d" + }, + { + "text": "Edit", + "bounds": [ + 506.8, + 78.1, + 35.7, + 15.6 + ], + "confidence": 1, + "ink": "#b3b4b4", + "ground": "#242b2f" + }, + { + "text": "Field Guide to Garden Birds.pdf X", + "bounds": [ + 602.8, + 24.5, + 236.7, + 17.8 + ], + "confidence": 1, + "ink": "#939698", + "ground": "#1b2024" + }, + { + "text": "Invoice INV-2041.pdf", + "bounds": [ + 897.5, + 26.8, + 142.9, + 15.6 + ], + "confidence": 1, + "ink": "#989b9d", + "ground": "#1d2225" + }, + { + "text": "X", + "bounds": [ + 1056.0, + 24.5, + 13.4, + 13.4 + ], + "confidence": 1, + "ink": "#a7a9ab", + "ground": "#1b2124" + }, + { + "text": "Site Survey Photos.pdf", + "bounds": [ + 1131.9, + 24.5, + 156.3, + 17.8 + ], + "confidence": 1, + "ink": "#949799", + "ground": "#1b2124" + }, + { + "text": "+ Open", + "bounds": [ + 1397.6, + 24.5, + 60.3, + 15.8 + ], + "confidence": 1, + "ink": "#3068b3", + "ground": "#191e26" + }, + { + "text": "Search in document", + "bounds": [ + 998.0, + 78.0, + 142.9, + 16 + ], + "confidence": 1, + "ink": "#9fa2a5", + "ground": "#21272b" + }, + { + "text": "Save", + "bounds": [ + 1455.6, + 78.1, + 35.7, + 15.6 + ], + "confidence": 1, + "ink": "#d4e2f5", + "ground": "#296fcf" + }, + { + "text": "NORTHWIND", + "bounds": [ + 618.4, + 185.2, + 151.8, + 20.1 + ], + "confidence": 1, + "ink": "#23448c", + "ground": "#f4f5f8" + }, + { + "text": "COOPERATIVE", + "bounds": [ + 620.7, + 207.5, + 151.8, + 11.2 + ], + "confidence": 1, + "ink": "#5e95dd", + "ground": "#f8f9fc" + }, + { + "text": "Q3 2026 BOARD REPORT", + "bounds": [ + 1058.2, + 189.6, + 196.5, + 15.6 + ], + "confidence": 1, + "ink": "#868687", + "ground": "#f6f6f6" + }, + { + "text": "2. REVENUE", + "bounds": [ + 551.1, + 285.7, + 194.9, + 37.6 + ], + "confidence": 1, + "ink": "#1a3f85", + "ground": "#f6f7f9" + }, + { + "text": "Total revenue for Q3 2026 was $45.8 million, compared with $40.9 million in", + "bounds": [ + 553.7, + 339.0, + 560.4, + 20.2 + ], + "confidence": 1, + "ink": "#505052", + "ground": "#f5f5f5" + }, + { + "text": "Q32025. Revenue grew 12% year over year, led by the Northwest region.", + "bounds": [ + 553.7, + 368.0, + 518.0, + 20.2 + ], + "confidence": 1, + "ink": "#5e5a30", + "ground": "#f7eb81" + }, + { + "text": "Strong", + "bounds": [ + 1076.1, + 370.3, + 49.1, + 17.8 + ], + "confidence": 1, + "ink": "#575859", + "ground": "#f5f5f5" + }, + { + "text": "demand across all markets and effective pricing discipline contributed to", + "bounds": [ + 553.7, + 397.0, + 535.8, + 20.2 + ], + "confidence": 1, + "ink": "#555557", + "ground": "#f6f6f6" + }, + { + "text": "the increase.", + "bounds": [ + 553.6, + 425.9, + 96.1, + 18.3 + ], + "confidence": 1, + "ink": "#525354", + "ground": "#f5f5f6" + }, + { + "text": "Revenue by region, Q3 2026", + "bounds": [ + 555.9, + 481.9, + 241.1, + 20.1 + ], + "confidence": 1, + "ink": "#334c8f", + "ground": "#f5f6f9" + }, + { + "text": "(USD millions)", + "bounds": [ + 551.4, + 506.0, + 104.9, + 18.3 + ], + "confidence": 1, + "ink": "#575859", + "ground": "#f4f4f5" + }, + { + "text": "20", + "bounds": [ + 551.4, + 535.4, + 22.3, + 17.8 + ], + "confidence": 1, + "ink": "#454647", + "ground": "#f4f4f4" + }, + { + "text": "18.7", + "bounds": [ + 640.7, + 537.7, + 38.0, + 17.8 + ], + "confidence": 1, + "ink": "#545455", + "ground": "#f7f7f7" + }, + { + "text": "15", + "bounds": [ + 551.4, + 575.6, + 22.3, + 17.8 + ], + "confidence": 1, + "ink": "#3d3e40", + "ground": "#f5f5f5" + }, + { + "text": "11.2", + "bounds": [ + 770.2, + 595.7, + 38.0, + 17.8 + ], + "confidence": 1, + "ink": "#515253", + "ground": "#f8f8f8" + }, + { + "text": "10", + "bounds": [ + 551.4, + 611.3, + 26.8, + 24.5 + ], + "confidence": 1, + "ink": "#4a4b4d", + "ground": "#f9f9f9" + }, + { + "text": "8.9", + "bounds": [ + 904.2, + 613.5, + 29.0, + 20.1 + ], + "confidence": 1, + "ink": "#5e5e5f", + "ground": "#f7f8f8" + }, + { + "text": "7.0", + "bounds": [ + 1033.7, + 631.4, + 26.8, + 17.8 + ], + "confidence": 1, + "ink": "#575757", + "ground": "#f6f6f6" + }, + { + "text": "5", + "bounds": [ + 560.4, + 658.1, + 15.6, + 17.8 + ], + "confidence": 1, + "ink": "#505052", + "ground": "#f8f8f8" + }, + { + "text": "0", + "bounds": [ + 562.6, + 700.5, + 11.2, + 13.4 + ], + "confidence": 1, + "ink": "#505052", + "ground": "#f5f5f5" + }, + { + "text": "Northwest", + "bounds": [ + 622.9, + 718.3, + 75.9, + 18.0 + ], + "confidence": 1, + "ink": "#535455", + "ground": "#f6f5f6" + }, + { + "text": "Coast", + "bounds": [ + 770.2, + 718.4, + 42.4, + 15.6 + ], + "confidence": 1, + "ink": "#575759", + "ground": "#f5f5f5" + }, + { + "text": "Valley", + "bounds": [ + 895.3, + 718.4, + 44.7, + 20.1 + ], + "confidence": 1, + "ink": "#535556", + "ground": "#f6f6f6" + }, + { + "text": "Metro", + "bounds": [ + 1026.9, + 720.5, + 42.5, + 13.6 + ], + "confidence": 1, + "ink": "#5b5b5d", + "ground": "#f5f5f5" + }, + { + "text": "The Northwest region accounted for 41% of total revenue, up from 38% in", + "bounds": [ + 553.7, + 769.7, + 549.2, + 22.3 + ], + "confidence": 1, + "ink": "#575859", + "ground": "#f7f7f7" + }, + { + "text": "Q3 2025.", + "bounds": [ + 553.7, + 798.7, + 67.0, + 22.3 + ], + "confidence": 1, + "ink": "#505152", + "ground": "#f6f6f7" + }, + { + "text": "3 / 24", + "bounds": [ + 629.6, + 863.0, + 60.3, + 23 + ], + "confidence": 1, + "ink": "#cbcbcc", + "ground": "#292e33" + }, + { + "text": ">", + "bounds": [ + 725.6, + 861.1, + 22.3, + 29.0 + ], + "confidence": 1, + "ink": "#c3c4c5", + "ground": "#292e33" + }, + { + "text": "3. OPERATIONS", + "bounds": [ + 553.7, + 948.0, + 247.8, + 36 + ], + "confidence": 1, + "ink": "#1b3567", + "ground": "#f3f5f8" + }, + { + "text": "-", + "bounds": [ + 785.9, + 867.8, + 33.5, + 15.6 + ], + "confidence": 1, + "ink": "#c9c9c9", + "ground": "#282c31" + }, + { + "text": "100%", + "bounds": [ + 828.3, + 867.8, + 55.8, + 13.4 + ], + "confidence": 1, + "ink": "#c5c6c8", + "ground": "#292e33" + }, + { + "text": "+", + "bounds": [ + 902.0, + 861.1, + 33.5, + 29.0 + ], + "confidence": 1, + "ink": "#c8c7c8", + "ground": "#272c31" + }, + { + "text": "Saved", + "bounds": [ + 46.9, + 995.0, + 35.7, + 11.2 + ], + "confidence": 1, + "ink": "#929698", + "ground": "#21282c" + }, + { + "text": "14.6 of 64 MB used", + "bounds": [ + 1379.7, + 992.8, + 131.7, + 13.4 + ], + "confidence": 1, + "ink": "#9b9d9f", + "ground": "#1f252a" + } + ], + "surfaces": [ + { + "bounds": [ + 502, + 121, + 800, + 785 + ], + "fill": "#fcfcfc", + "border": "#1a2026", + "radius": 3 + }, + { + "bounds": [ + 105, + 168, + 280, + 809 + ], + "fill": "#192025", + "border": "#191f24", + "radius": 0 + }, + { + "bounds": [ + 6, + 117, + 97, + 860 + ], + "fill": "#1e252a", + "border": "#1e252b", + "radius": 1 + }, + { + "bounds": [ + 503, + 910, + 798, + 72 + ], + "fill": "#fcfcfc", + "border": "#45484c", + "radius": 3 + }, + { + "bounds": [ + 551, + 848, + 522, + 57 + ], + "fill": "#282d32", + "border": "#1f242a", + "radius": 8 + }, + { + "bounds": [ + 105, + 117, + 280, + 49 + ], + "fill": "#1f252a", + "border": "#1f252a", + "radius": 0 + }, + { + "bounds": [ + 560, + 10, + 295, + 43 + ], + "fill": "#1a1f23", + "border": "#282d30", + "radius": 0 + }, + { + "bounds": [ + 956, + 65, + 307, + 41 + ], + "fill": "#1e2429", + "border": "#1d2428", + "radius": 0 + }, + { + "bounds": [ + 107, + 215, + 272, + 45 + ], + "fill": "#2970d7", + "border": "#1b2228", + "radius": 7 + }, + { + "bounds": [ + 296, + 10, + 262, + 42 + ], + "fill": "#1a2023", + "border": "#1a1f22", + "radius": 0 + }, + { + "bounds": [ + 959, + 69, + 301, + 34 + ], + "fill": "#1e2429", + "border": "#22282b", + "radius": 1 + }, + { + "bounds": [ + 855, + 10, + 232, + 42 + ], + "fill": "#1a2023", + "border": "#181d20", + "radius": 0 + }, + { + "bounds": [ + 1091, + 12, + 241, + 40 + ], + "fill": "#1a2023", + "border": "#191f22", + "radius": 1 + }, + { + "bounds": [ + 634, + 559, + 54, + 149 + ], + "fill": "#276dd2", + "border": "#226ed2", + "radius": 1 + }, + { + "bounds": [ + 12, + 213, + 84, + 82 + ], + "fill": "#275293", + "border": "#20282e", + "radius": 4 + }, + { + "bounds": [ + 764, + 617, + 54, + 91 + ], + "fill": "#276cd2", + "border": "#3076d4", + "radius": 1 + }, + { + "bounds": [ + 1403, + 66, + 115, + 39 + ], + "fill": "#246acf", + "border": "#1e2529", + "radius": 4 + }, + { + "bounds": [ + 1278, + 65, + 106, + 41 + ], + "fill": "#1e2429", + "border": "#1d2428", + "radius": 0 + }, + { + "bounds": [ + 893, + 635, + 54, + 73 + ], + "fill": "#286dd2", + "border": "#3678d3", + "radius": 1 + }, + { + "bounds": [ + 1022, + 652, + 56, + 59 + ], + "fill": "#276dd2", + "border": "#9eb9d6", + "radius": 1 + }, + { + "bounds": [ + 1387, + 14, + 80, + 34 + ], + "fill": "#191f22", + "border": "#191f23", + "radius": 0 + }, + { + "bounds": [ + 1324, + 69, + 57, + 34 + ], + "fill": "#1e2429", + "border": "#1e2529", + "radius": 0 + }, + { + "bounds": [ + 1282, + 69, + 40, + 34 + ], + "fill": "#1e2529", + "border": "#1f252a", + "radius": 0 + } + ], + "dividers": [ + { + "axis": "h", + "at": 10, + "from": 564, + "to": 850, + "color": "#292e31" + }, + { + "axis": "h", + "at": 66, + "from": 962, + "to": 1256, + "color": "#2e3335" + }, + { + "axis": "h", + "at": 67, + "from": 960, + "to": 1258, + "color": "#3c4043" + }, + { + "axis": "h", + "at": 103, + "from": 961, + "to": 1256, + "color": "#3e4245" + }, + { + "axis": "h", + "at": 120, + "from": 502, + "to": 1301, + "color": "#0a1116" + }, + { + "axis": "h", + "at": 121, + "from": 502, + "to": 1301, + "color": "#414246" + }, + { + "axis": "h", + "at": 122, + "from": 502, + "to": 1299, + "color": "#e0e0e0" + }, + { + "axis": "h", + "at": 166, + "from": 104, + "to": 385, + "color": "#303437" + }, + { + "axis": "h", + "at": 167, + "from": 104, + "to": 385, + "color": "#22282d" + }, + { + "axis": "h", + "at": 243, + "from": 551, + "to": 1254, + "color": "#cacbcc" + }, + { + "axis": "h", + "at": 244, + "from": 551, + "to": 1254, + "color": "#b0b1b3" + }, + { + "axis": "h", + "at": 365, + "from": 624, + "to": 1072, + "color": "#fbf7d2" + }, + { + "axis": "h", + "at": 388, + "from": 624, + "to": 1072, + "color": "#fcf8c7" + }, + { + "axis": "h", + "at": 708, + "from": 582, + "to": 1119, + "color": "#9da0a0" + }, + { + "axis": "h", + "at": 709, + "from": 582, + "to": 1119, + "color": "#13181b" + }, + { + "axis": "h", + "at": 710, + "from": 582, + "to": 1119, + "color": "#d9d9d9" + }, + { + "axis": "h", + "at": 847, + "from": 562, + "to": 1062, + "color": "#585c5f" + }, + { + "axis": "h", + "at": 848, + "from": 559, + "to": 1064, + "color": "#21262b" + }, + { + "axis": "h", + "at": 908, + "from": 502, + "to": 1300, + "color": "#080d13" + }, + { + "axis": "h", + "at": 909, + "from": 502, + "to": 1300, + "color": "#a0a2a4" + }, + { + "axis": "h", + "at": 977, + "from": 9, + "to": 383, + "color": "#3b4043" + }, + { + "axis": "h", + "at": 978, + "from": 11, + "to": 380, + "color": "#262b30" + }, + { + "axis": "h", + "at": 979, + "from": 11, + "to": 380, + "color": "#1a1f24" + }, + { + "axis": "h", + "at": 980, + "from": 9, + "to": 497, + "color": "#24292d" + }, + { + "axis": "h", + "at": 981, + "from": 0, + "to": 1536, + "color": "#383d41" + }, + { + "axis": "h", + "at": 982, + "from": 762, + "to": 1297, + "color": "#141b20" + }, + { + "axis": "v", + "at": 4, + "from": 118, + "to": 974, + "color": "#373b3d" + }, + { + "axis": "v", + "at": 102, + "from": 169, + "to": 977, + "color": "#303639" + }, + { + "axis": "v", + "at": 103, + "from": 169, + "to": 977, + "color": "#393f41" + }, + { + "axis": "v", + "at": 385, + "from": 118, + "to": 973, + "color": "#383d41" + }, + { + "axis": "v", + "at": 386, + "from": 121, + "to": 972, + "color": "#23282c" + }, + { + "axis": "v", + "at": 501, + "from": 121, + "to": 892, + "color": "#05090c" + }, + { + "axis": "v", + "at": 502, + "from": 121, + "to": 891, + "color": "#616267" + }, + { + "axis": "v", + "at": 503, + "from": 121, + "to": 890, + "color": "#e9e9ec" + }, + { + "axis": "v", + "at": 1299, + "from": 123, + "to": 890, + "color": "#dcdcdd" + }, + { + "axis": "v", + "at": 1300, + "from": 121, + "to": 892, + "color": "#3e4146" + }, + { + "axis": "v", + "at": 1301, + "from": 121, + "to": 891, + "color": "#070910" + } + ], + "limits": [ + "Evidence for review: roles and widgets are assigned in the semantic map, not here." + ] +} diff --git a/apps/pdftools/design/source/09-dark-reading.png b/apps/pdftools/design/source/09-dark-reading.png new file mode 100644 index 00000000..ce511f6c Binary files /dev/null and b/apps/pdftools/design/source/09-dark-reading.png differ diff --git a/apps/pdftools/design/source/09-dark-reading.prompt.txt b/apps/pdftools/design/source/09-dark-reading.prompt.txt new file mode 100644 index 00000000..9c51a35c --- /dev/null +++ b/apps/pdftools/design/source/09-dark-reading.prompt.txt @@ -0,0 +1,69 @@ +Create ONE original image: a single, complete desktop application screen for +"PDF Tools", a document app inside the OctoSense desktop. + +Requested image size: 1536 × 1024 pixels, landscape. Requested quality: high. +Show only the application's own window content, edge to edge, flat and +straight on. No operating-system menu bar, no dock, no window title bar or +traffic lights, no device frame, no perspective, no drop shadow around the +screen, no captions or annotations outside the interface. + +Visual language: an original, precise desktop document tool. The document is +the brightest thing on screen; the chrome is quiet, thin and well aligned. +Palette: a cool light grey desk behind the pages, white pages with a soft +shadow, graphite text, mid-grey secondary text, thin 1-pixel dividers in pale +grey, and ONE accent colour, a confident medium blue, used only for the +current mode, the selected item, focus and primary buttons. Yellow marks +highlighted text and search matches. Green means saved or done, red means an +error, both used sparingly. Corner radius about 8 px on panels, buttons and +fields; pages have square corners. Icons are simple outline glyphs with one +consistent stroke weight. Typography: a neutral sans-serif for the interface, +small and crisp (about 13–14 px), medium weight for panel titles; document +pages use a classic serif for body text and a sans-serif for their headings. +Do not imitate Adobe Acrobat's layout, icons, colours or branding, or any other +product's. + +Window layout, the same in every screen: +- Top row: document tabs. Each tab shows a small PDF glyph, the file name and + a close cross; the active tab is white, the others grey; a "+ Open" button + follows the last tab. +- Second row, the mode bar: the modes "Read", "Comment", "Fill & Sign", + "Pages", "Combine", "Edit" as text tabs, the current one in the accent + colour with a short underline; on the right a search field with a magnifier + and the placeholder "Search in document", then icon buttons for Undo and Redo, + and a "Save" button. +- Left: a narrow icon rail with four icons and tooltips' labels shown under + them in tiny text: "Pages", "Outline", "Comments", "Search". The selected + icon has a tinted square behind it. Next to the rail, an open panel about + 240 px wide when the screen calls for it. +- Centre: the canvas, a light grey desk with the document's pages stacked + vertically, centred, one page fully visible and the next starting below. +- Bottom centre of the canvas: a floating rounded pill with "‹", "3 / 24", + "›", a divider, "−", "100%", "+", and two small icons for "Fit width" and + "Fit page". +- Right: a panel about 300 px wide, only in screens that call for it. +- Bottom edge: a thin status line with "Saved" (with a small green dot) on the + left and "14.6 of 64 MB used" on the right. + +Use this exact fixture wherever it appears: +- Documents in the library: "Q3 2026 Board Report.pdf" (24 pages, 2.8 MB, + opened today), "Riverside Lease 2026.pdf" (12 pages, 1.1 MB, yesterday), + "Field Guide to Garden Birds.pdf" (7 pages, 16 KB, 3 Oct), "Invoice + INV-2041.pdf" (1 page, 84 KB, 1 Oct), "Site Survey Photos.pdf" (18 pages, + 9.6 MB, 28 Sep). +- The board report belongs to the fictional "Northwind Cooperative". Its + sections: Summary, Revenue, Operations, Outlook, Appendix. Page 3 is + "Revenue" with a short paragraph that includes the sentence "Revenue grew + 12% year over year, led by the Northwest region." and a simple bar chart + "Revenue by region, Q3 2026" with bars for Northwest, Coast, Valley, Metro. +- People: Maya Chen (the user), Jun Park and Ana Ruiz (reviewers). + +Render every piece of interface text and every control sharply enough to be +read and measured later. Labels, buttons, fields and panels will be rebuilt as +native components from this image; page contents are sample document art. + +This screen: the same as "Reading" (mode "Read", page 3 of the board report at +100%), in the dark appearance: near-black graphite chrome, a dark desk, the +accent blue lighter for contrast, text light grey. The page itself stays white +with its content. The rail's "Outline" icon is selected; its panel shows the +title "Outline" and a bookmark tree: "Summary", "Revenue" (selected, with +"Revenue by region" nested under it), "Operations", "Outlook", "Appendix". \ No newline at end of file diff --git a/apps/pdftools/design/source/generation.json b/apps/pdftools/design/source/generation.json new file mode 100644 index 00000000..c46724ea --- /dev/null +++ b/apps/pdftools/design/source/generation.json @@ -0,0 +1,229 @@ +{ + "provider": "OpenAI Image API", + "model": "gpt-image-2", + "requested_size": [ + 1536, + 1024 + ], + "quality": "high", + "generated_at": "2026-10-10T21:28:11+00:00", + "authorized_by": "the maintainer, 10 Oct 2026 (\"All 9 now\")", + "screens": [ + { + "name": "01-home", + "image": "01-home.png", + "prompt": "01-home.prompt.txt", + "image_sha256": "7f52b77e4b72a1421bb54fa35b1e8d6d7df1a6da08fbc2e71089db4e1a02ad74", + "prompt_sha256": "5b0e5328f3e1dfe6f8bf9dd3f4f2ec4b08272262e006238bda2fb27f6d4341aa", + "bytes": 1598337, + "seconds": 66.1, + "revised_prompt": null, + "usage": { + "input_tokens": 1136, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 1136 + }, + "output_tokens": 5488, + "output_tokens_details": { + "image_tokens": 5488, + "text_tokens": 0 + }, + "total_tokens": 6624 + }, + "created": 1791667526 + }, + { + "name": "02-reading", + "image": "02-reading.png", + "prompt": "02-reading.prompt.txt", + "image_sha256": "a1b56c1e0f54c0b0db9a5d1f4e465041b6d67a44efa36180f7da1d57f79c0ef2", + "prompt_sha256": "38876686c79314a81213d34dac30459bf20d5284f5d686261226a9833f4fb1af", + "bytes": 1237678, + "seconds": 71.0, + "revised_prompt": null, + "usage": { + "input_tokens": 1084, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 1084 + }, + "output_tokens": 5488, + "output_tokens_details": { + "image_tokens": 5488, + "text_tokens": 0 + }, + "total_tokens": 6572 + }, + "created": 1791667531 + }, + { + "name": "03-find", + "image": "03-find.png", + "prompt": "03-find.prompt.txt", + "image_sha256": "aec86c4a1cd6531b029285d31084665b45d4cdf2af465a12c795d152575f7d69", + "prompt_sha256": "93241fad610433aeefc5ee4e3a5fd658db1f91b918d11ed0d9bfe67b1b7d43d4", + "bytes": 1353004, + "seconds": 80.2, + "revised_prompt": null, + "usage": { + "input_tokens": 1105, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 1105 + }, + "output_tokens": 5488, + "output_tokens_details": { + "image_tokens": 5488, + "text_tokens": 0 + }, + "total_tokens": 6593 + }, + "created": 1791667540 + }, + { + "name": "04-comment", + "image": "04-comment.png", + "prompt": "04-comment.prompt.txt", + "image_sha256": "4a87dcc47d9bb98ccc5f4bf04058fd407f847b2c4cc862e3fa32d7fb6337eaf3", + "prompt_sha256": "b030cb52125352aeb8b000342f4dd629deead9ce497af15c54651700adb57e06", + "bytes": 1208661, + "seconds": 78.1, + "revised_prompt": null, + "usage": { + "input_tokens": 1162, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 1162 + }, + "output_tokens": 5488, + "output_tokens_details": { + "image_tokens": 5488, + "text_tokens": 0 + }, + "total_tokens": 6650 + }, + "created": 1791667602 + }, + { + "name": "05-pages", + "image": "05-pages.png", + "prompt": "05-pages.prompt.txt", + "image_sha256": "8c20451b5e17f7cbf663e89c94971dcb90cf3ef0e91b81f473247ebea99b0e31", + "prompt_sha256": "2b2b1c9655c9bd8d8c4b1c47e369f3cad56a18931e7fed89a9aa8933856d5c04", + "bytes": 1401704, + "seconds": 87.9, + "revised_prompt": null, + "usage": { + "input_tokens": 1099, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 1099 + }, + "output_tokens": 5488, + "output_tokens_details": { + "image_tokens": 5488, + "text_tokens": 0 + }, + "total_tokens": 6587 + }, + "created": 1791667619 + }, + { + "name": "06-combine", + "image": "06-combine.png", + "prompt": "06-combine.prompt.txt", + "image_sha256": "bbb5c2bf1a5288343e42b6602e231314f3dc79effb6cfe789aa14cf743baef08", + "prompt_sha256": "c9d0a6ebec2a237a783f9114c39d0be960c7148dd9254e2e97826f82fe20609b", + "bytes": 1175890, + "seconds": 67.1, + "revised_prompt": null, + "usage": { + "input_tokens": 1118, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 1118 + }, + "output_tokens": 5488, + "output_tokens_details": { + "image_tokens": 5488, + "text_tokens": 0 + }, + "total_tokens": 6606 + }, + "created": 1791667607 + }, + { + "name": "07-fill-sign", + "image": "07-fill-sign.png", + "prompt": "07-fill-sign.prompt.txt", + "image_sha256": "fc8314c6e42ea94a8d64df669b160e3eacc408ca4923bfe57fd1b3d4a04aa721", + "prompt_sha256": "e0aa2860cb831bb80767794d9714eefc95f4013036b5657012dd92552e81ddb0", + "bytes": 1199656, + "seconds": 75.7, + "revised_prompt": null, + "usage": { + "input_tokens": 1149, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 1149 + }, + "output_tokens": 5488, + "output_tokens_details": { + "image_tokens": 5488, + "text_tokens": 0 + }, + "total_tokens": 6637 + }, + "created": 1791667679 + }, + { + "name": "08-edit", + "image": "08-edit.png", + "prompt": "08-edit.prompt.txt", + "image_sha256": "5ad2add5b3fbdb5541b72c9f7bb3c857e4b1ddda0a068d6e1ac7b17f609f4bb6", + "prompt_sha256": "adbe7636678bc5bcf2ef33acf4f9e5b78eb1753994eb1ad54f16dc677ee23c39", + "bytes": 1173319, + "seconds": 71.6, + "revised_prompt": null, + "usage": { + "input_tokens": 1099, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 1099 + }, + "output_tokens": 5488, + "output_tokens_details": { + "image_tokens": 5488, + "text_tokens": 0 + }, + "total_tokens": 6587 + }, + "created": 1791667677 + }, + { + "name": "09-dark-reading", + "image": "09-dark-reading.png", + "prompt": "09-dark-reading.prompt.txt", + "image_sha256": "f61f8b32d0359fbb41909b0e58bc7d0b5314c300a1bd72eb87f3c5f6c194e54b", + "prompt_sha256": "5b1f501a1f840486474389d906f0a9679f80749e989356cef766c80dcc278056", + "bytes": 1333609, + "seconds": 72.2, + "revised_prompt": null, + "usage": { + "input_tokens": 1083, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 1083 + }, + "output_tokens": 5488, + "output_tokens_details": { + "image_tokens": 5488, + "text_tokens": 0 + }, + "total_tokens": 6571 + }, + "created": 1791667689 + } + ] +} \ No newline at end of file diff --git a/apps/pdftools/design/source/prompts.md b/apps/pdftools/design/source/prompts.md new file mode 100644 index 00000000..35f2e755 --- /dev/null +++ b/apps/pdftools/design/source/prompts.md @@ -0,0 +1,199 @@ +# PDF Tools v2: image prompts + +App Flow step 2 (exact text to submit) for step 3 (image generation, a +**HUMAN** checkpoint for a paid generator). Built from `../BRIEF.md`. + +Adaptation from App Flow's atlas route: these are desktop screens, so each +screen is its own image at the generator's largest landscape size, so that its +text stays sharp enough to measure. App Flow's atlas puts 8–12 phone screens in +one image instead. Consistency comes from the identical shared block in every +prompt; screen 1 is generated first and approved as the style reference +before the rest. + +Each submitted prompt is the shared block followed by one screen block. The +exact text sent for screen N is saved as `NN-.prompt.txt` beside its +untouched output `NN-.png`. The generator, model and requested size go +in `generation.json`. + +## Shared block (identical in every prompt) + +```text +Create ONE original image: a single, complete desktop application screen for +"PDF Tools", a document app inside the OctoSense desktop. + +Requested image size: 1536 × 1024 pixels, landscape. Requested quality: high. +Show only the application's own window content, edge to edge, flat and +straight on. No operating-system menu bar, no dock, no window title bar or +traffic lights, no device frame, no perspective, no drop shadow around the +screen, no captions or annotations outside the interface. + +Visual language: an original, precise desktop document tool. The document is +the brightest thing on screen; the chrome is quiet, thin and well aligned. +Palette: a cool light grey desk behind the pages, white pages with a soft +shadow, graphite text, mid-grey secondary text, thin 1-pixel dividers in pale +grey, and ONE accent colour, a confident medium blue, used only for the +current mode, the selected item, focus and primary buttons. Yellow marks +highlighted text and search matches. Green means saved or done, red means an +error, both used sparingly. Corner radius about 8 px on panels, buttons and +fields; pages have square corners. Icons are simple outline glyphs with one +consistent stroke weight. Typography: a neutral sans-serif for the interface, +small and crisp (about 13–14 px), medium weight for panel titles; document +pages use a classic serif for body text and a sans-serif for their headings. +Do not imitate Adobe Acrobat's layout, icons, colours or branding, or any other +product's. + +Window layout, the same in every screen: +- Top row: document tabs. Each tab shows a small PDF glyph, the file name and + a close cross; the active tab is white, the others grey; a "+ Open" button + follows the last tab. +- Second row, the mode bar: the modes "Read", "Comment", "Fill & Sign", + "Pages", "Combine", "Edit" as text tabs, the current one in the accent + colour with a short underline; on the right a search field with a magnifier + and the placeholder "Search in document", then icon buttons for Undo and Redo, + and a "Save" button. +- Left: a narrow icon rail with four icons and tooltips' labels shown under + them in tiny text: "Pages", "Outline", "Comments", "Search". The selected + icon has a tinted square behind it. Next to the rail, an open panel about + 240 px wide when the screen calls for it. +- Centre: the canvas, a light grey desk with the document's pages stacked + vertically, centred, one page fully visible and the next starting below. +- Bottom centre of the canvas: a floating rounded pill with "‹", "3 / 24", + "›", a divider, "−", "100%", "+", and two small icons for "Fit width" and + "Fit page". +- Right: a panel about 300 px wide, only in screens that call for it. +- Bottom edge: a thin status line with "Saved" (with a small green dot) on the + left and "14.6 of 64 MB used" on the right. + +Use this exact fixture wherever it appears: +- Documents in the library: "Q3 2026 Board Report.pdf" (24 pages, 2.8 MB, + opened today), "Riverside Lease 2026.pdf" (12 pages, 1.1 MB, yesterday), + "Field Guide to Garden Birds.pdf" (7 pages, 16 KB, 3 Oct), "Invoice + INV-2041.pdf" (1 page, 84 KB, 1 Oct), "Site Survey Photos.pdf" (18 pages, + 9.6 MB, 28 Sep). +- The board report belongs to the fictional "Northwind Cooperative". Its + sections: Summary, Revenue, Operations, Outlook, Appendix. Page 3 is + "Revenue" with a short paragraph that includes the sentence "Revenue grew + 12% year over year, led by the Northwest region." and a simple bar chart + "Revenue by region, Q3 2026" with bars for Northwest, Coast, Valley, Metro. +- People: Maya Chen (the user), Jun Park and Ana Ruiz (reviewers). + +Render every piece of interface text and every control sharply enough to be +read and measured later. Labels, buttons, fields and panels will be rebuilt as +native components from this image; page contents are sample document art. +``` + +## Screen blocks + +### 01 Home (`01-home`) + +```text +This screen: the Home tab is active (the first tab shows a house glyph and +the word "Home"; a second tab "Q3 2026 Board Report.pdf" is open behind it). +No mode bar, no rail, no canvas pill on this screen. Instead, a centred +content area: the title "PDF Tools", then a wide primary button "Open a PDF +from this device" with the caption "Up to 64 MB" beside it. Below, the heading +"Recent" and a grid of five document cards in two rows, one per fixture +document, each with its first page as a thumbnail, the file name, "N pages · +size" and when it was opened. The status line shows only "14.6 of 64 MB +used" with a thin storage bar. +``` + +### 02 Reading (`02-reading`) + +```text +This screen: mode "Read". Tab "Q3 2026 Board Report.pdf" active. The rail's +"Pages" icon is selected and its panel shows the title "Pages" and a vertical +column of page thumbnails labelled 1 to 6, thumbnail 3 outlined in the accent +colour. The canvas shows page 3, "Revenue", at 100% with its paragraph and the +bar chart, and the top of page 4 below it. The canvas pill reads "3 / 24" and +"100%". No right panel. +``` + +### 03 Find (`03-find`) + +```text +This screen: mode "Read", with "revenue" typed in the search field. The rail's +"Search" icon is selected; its panel shows "12 matches on 7 pages" and a list +of results grouped by page ("Page 1", "Page 3", "Page 4", …), each with a +one-line snippet in which "revenue" is bold. The second result is selected. +On the canvas, page 3 shows every match highlighted in yellow and the selected +match in a stronger orange-yellow with an outline. A small bar above the canvas +reads "2 of 12" with up and down arrows and "Done". +``` + +### 04 Comment (`04-comment`) + +```text +This screen: mode "Comment". Under the mode bar, a slim tool row: "Highlight" +(selected), "Underline", "Strike out", "Note", "Text box", and a row of four +colour dots. On page 3 the sentence "Revenue grew 12% year over year, led by +the Northwest region." is highlighted in yellow with a small speech-bubble +marker in the margin. The right panel is open with the title "Comments" and +"3": a thread on the highlight. Jun Park (initials avatar), "Today 10:12": +"Can we add the regional breakdown table here?" A reply from Maya Chen, +"Today 10:20": "Added on page 4." A status chip "Accepted". At the bottom of +the thread, a reply field "Reply…" and a "Post" button. Below the thread, a +second, collapsed comment by Ana Ruiz on page 7. +``` + +### 05 Pages (`05-pages`) + +```text +This screen: mode "Pages". No left panel (the rail's "Pages" icon is still +selected). The canvas becomes a grid of large page thumbnails, five per row, +pages 1 to 15 visible, each with its number below. Pages 5 and 6 are selected +with an accent outline and a check badge. A toolbar row under the mode bar: +"2 selected", then "Rotate left", "Rotate right", "Delete", "Extract", +"Insert from file", and on the right "Drag pages to reorder". A thin +insertion marker shows between pages 9 and 10 where a page is being dragged. +``` + +### 06 Combine (`06-combine`) + +```text +This screen: mode "Combine". The canvas shows a centred card titled "Combine +files" with three rows in order, each with a drag handle, the first page as a +small thumbnail, the file name, a page-range field and a remove cross: +"Q3 2026 Board Report.pdf" with range "All (24 pages)", "Appendix A - +Figures.pdf" with range "1–4, 9 (5 pages)", "Cover Letter.pdf" with range +"All (2 pages)". Below the rows: "+ Add files". A summary line "One PDF of 31 +pages", a name field "Q3 2026 Board Pack", and a primary button "Combine". No +left panel. +``` + +### 07 Fill & Sign (`07-fill-sign`) + +```text +This screen: mode "Fill & Sign". Tab "Riverside Lease 2026.pdf" active, page +1 of 12 on the canvas: a residential lease form whose fields are outlined in +pale blue. Filled fields: "Tenant name: Maya Chen", "Unit: 4B", "Start date: +1 November 2026". Empty fields: "Monthly rent" (marked required) and two +"Initials" boxes. A banner above the canvas: "This form has 6 fields. 2 +required fields are empty." with a "Highlight fields" toggle. The right panel +"Fields" lists every field with its value or "Empty"; required ones have a red +asterisk. Below the list: "Add text", "Add date", "Add initials" buttons; an +initials box "MC" is being placed on the page. +``` + +### 08 Edit (`08-edit`) + +```text +This screen: mode "Edit". Page 2 of the board report on the canvas, a page of +serif text. One paragraph is selected for editing with a blue outline and a +text cursor; it now reads "The board met on 21 September 2026 to review the +quarter's results." with the date just changed. The right panel "Edit text" +shows "Paragraph 3", font "Serif · 11 pt", alignment buttons (left selected), +colour "Black", and two buttons "Apply" (primary) and "Cancel". The status line +shows "Edited" with an amber dot instead of "Saved". +``` + +### 09 Dark reading (`09-dark-reading`) + +```text +This screen: the same as "Reading" (mode "Read", page 3 of the board report at +100%), in the dark appearance: near-black graphite chrome, a dark desk, the +accent blue lighter for contrast, text light grey. The page itself stays white +with its content. The rail's "Outline" icon is selected; its panel shows the +title "Outline" and a bookmark tree: "Summary", "Revenue" (selected, with +"Revenue by region" nested under it), "Operations", "Outlook", "Appendix". +``` diff --git a/apps/pdftools/dev-fixture/engine.splash b/apps/pdftools/dev-fixture/engine.splash new file mode 100644 index 00000000..fc247735 --- /dev/null +++ b/apps/pdftools/dev-fixture/engine.splash @@ -0,0 +1,325 @@ +// PDF Tools' dev fixture: a stand-in for the shell's pdf engine, for UI tests +// in App Hub's card-host, which serves no host services. It never ships. +// apps/pdftools/tests/ui.py puts it in place of engine() in a scratch copy of +// the bundle, after make_fixture.py has filled the app's storage: dev/pages/ +// holds each sample page drawn at 2 pixels per point, and dev/fixture.json +// what this answers. It keeps SERVICE.md's shapes, so the app runs its one +// code path; the pages it serves never change (a rotation, an edit or a +// filled field shows in the answers, not in the pictures). +let fx = nil +fn fx_load(){ + if fx == nil { + let f = try { fs.read("dev/fixture.json").parse_json() } catch { nil } + if f == nil { f = {docs: {}} } + fx = {f: f open: {} next: 1 delay: optional(f, "delay", 0.12) fail: optional(f, "fail", "") ids: 100} + } + fx +} +// Every answer is worked out on a later turn, in a handler of its own. +fn engine(method, args, done){ + start_timeout(0.12, || { + let x = fx_load() + let s = optional(x.open, "" + optional(args, "doc", ""), nil) + if method == "find" && s != nil { + fx_find(x, s, args, {i: 0 matches: []}, done) + return + } + let answer = fx_answer(x, method, args) + start_timeout(0.01, || done(answer)) + }) +} +// Find, one page a turn: each page's lines are a file to read and search. +fn fx_find(x, s, args, run, done){ + if run.i >= s.order.len() { + done(fx_ok({total: run.matches.len() matches: run.matches})) + return + } + let re = regex(fx_escape(("" + args.query).trim()), "i") + let e = s.order[run.i] + for l in fx_lines(x.f.docs[e.name].id, e.p).lines { + let m = re.exec(l.text) + if m != nil && run.matches.len() < optional(args, "limit", 200) { + let b = l.box + let w = b[2] * m.value.len() / max(1, l.text.len()) + let at = b[0] + b[2] * m.index / max(1, l.text.len()) + run.matches.push({page: run.i + 1 rects: [[at, b[1], w, b[3]]] snippet: l.text.replace(m.value, "[[" + m.value + "]]")}) + } + } + run.i = run.i + 1 + start_timeout(0.005, || fx_find(x, s, args, run, done)) +} +fn fx_lines(id, page){ + let path = "dev/lines/" + id + "/" + page + ".json" + if !fs.exists(path) { return {lines: [] paragraphs: []} } + let v = try { fs.read(path).parse_json() } catch { nil } + if v == nil { return {lines: [] paragraphs: []} } + v +} +fn fx_ok(data){ {is_ok: true data: data error: nil} } +fn fx_err(e){ {is_ok: false data: nil error: e} } +fn fx_copy(v){ v.to_json().parse_json() } +fn fx_name(path){ + let parts = ("" + path).split("/") + parts[parts.len() - 1] +} +fn fx_count(o){ + let n = 0 + for k v in o { n = n + 1 } + n +} +fn fx_snapshot(s){ {order: fx_copy(s.order) comments: fx_copy(s.comments) fields: fx_copy(s.fields)} } +fn fx_change(s){ + s.history.push(fx_snapshot(s)) + s.future = [] + s.edited = true +} +fn fx_sizes(x, s){ + let out = [] + for e in s.order { + let d = x.f.docs[e.name] + let size = d.sizes[e.p - 1] + if e.rot % 180 != 0 { out.push([size[1], size[0]]) } else { out.push([size[0], size[1]]) } + } + out +} +fn fx_open_doc(x, name){ + let d = optional(x.f.docs, name, nil) + if d == nil { return nil } + let order = [] + let map = optional(d, "map", nil) + if map != nil { for e in map { order.push({name: e.name p: e.p rot: 0}) } } else { for i in d.sizes.len() { order.push({name: name p: i + 1 rot: 0}) } } + {name: name order: order comments: fx_copy(d.comments) fields: fx_copy(d.fields) edited: false history: [] future: []} +} +fn fx_answer(x, method, args){ + if x.fail == method || x.fail == "all" { return fx_err("pdf." + method + ": the dev fixture refuses this call") } + if method == "open" { + let name = fx_name(args.path) + if has(optional(x.f, "damaged", []), name) { return fx_err("damaged: the file's cross-reference table is broken (dev fixture)") } + if has(optional(x.f, "protected", []), name) { return fx_err("protected: this PDF needs a password to open") } + if fx_count(x.open) >= 8 { return fx_err("too_many_open: 8 documents are open") } + let s = fx_open_doc(x, name) + if s == nil { return fx_err("not_found: " + args.path) } + let h = "d" + x.next + x.next = x.next + 1 + x.open[h] = s + let d = x.f.docs[name] + return fx_ok({doc: h path: args.path pages: s.order.len() sizes: fx_sizes(x, s) title: d.title outline: d.outline fields: s.fields.len() comments: s.comments.len() can_edit: true}) + } + if method == "merge" { + let order = [] + for item in args.paths { + let path = pick(item.is_string(), item, optional(item, "path", "")) + let d = optional(x.f.docs, fx_name(path), nil) + if d == nil { return fx_err("not_found: " + path) } + let pages = range_pages(pick(item.is_string(), "", optional(item, "pages", "")), d.sizes.len()) + if pages == nil { return fx_err("invalid: the pages of " + path) } + for p in pages { order.push({name: fx_name(path) p: p rot: 0}) } + } + return fx_new_file(x, args.out, order) + } + if method == "info" && optional(args, "doc", nil) == nil { + let d = optional(x.f.docs, fx_name(args.path), nil) + if d == nil { return fx_err("not_found: " + args.path) } + let pages = [] + for s in d.sizes { pages.push({width: s[0] height: s[1]}) } + return fx_ok({file: args.path pages: pages file_size: d.file_size title: d.title}) + } + let s = optional(x.open, "" + optional(args, "doc", ""), nil) + if s == nil { return fx_err("unknown_doc: no open document " + optional(args, "doc", "")) } + let d = x.f.docs[s.name] + if method == "close" { + if s.edited && !optional(args, "discard", false) { return fx_err("unsaved: the document has unsaved changes") } + let kept = {} + for k v in x.open { if k != args.doc { kept[k] = v } } + x.open = kept + return fx_ok({closed: true}) + } + if method == "state" { return fx_ok({edited: s.edited can_undo: s.history.len() > 0 can_redo: s.future.len() > 0 pages: s.order.len()}) } + if method == "info" { + let pages = [] + for z in fx_sizes(x, s) { pages.push({width: z[0] height: z[1]}) } + return fx_ok({file: s.name pages: pages file_size: d.file_size title: d.title}) + } + if method == "page" { + let e = optional(s.order, args.page - 1, nil) + if e == nil { return fx_err("invalid: no page " + args.page) } + let src = x.f.docs[e.name] + return fx_ok({path: "dev/pages/" + src.id + "/" + e.p + ".png" width: 0 height: 0 dpi: optional(args, "dpi", 96)}) + } + if method == "lines" { + let e = optional(s.order, args.page - 1, nil) + if e == nil { return fx_err("invalid: no page " + args.page) } + return fx_ok(fx_lines(x.f.docs[e.name].id, e.p)) + } + if method == "find" { + let q = ("" + args.query).trim() + let re = regex(fx_escape(q), "i") + let matches = [] + for i e in s.order { + let page = fx_lines(x.f.docs[e.name].id, e.p) + for l in page.lines { + let m = re.exec(l.text) + if m != nil && matches.len() < optional(args, "limit", 200) { + let b = l.box + let w = b[2] * m.value.len() / max(1, l.text.len()) + let at = b[0] + b[2] * m.index / max(1, l.text.len()) + matches.push({page: i + 1 rects: [[at, b[1], w, b[3]]] snippet: l.text.replace(m.value, "[[" + m.value + "]]")}) + } + } + } + return fx_ok({total: matches.len() matches: matches}) + } + if method == "text" { return fx_ok({pages: []}) } + if method == "comments" { return fx_ok({comments: s.comments}) } + if method == "comment" { + if args.op == "add" { + fx_change(s) + x.ids = x.ids + 1 + let id = "c" + x.ids + s.comments.push({id: id page: args.page type: args.type author: optional(args, "author", "PdfCraft") text: optional(args, "text", "") date: fx_now() + color: fx_upper(optional(args, "color", "#FFEF00")) status: "none" rects: optional(args, "rects", [[optional(args, "at", [0, 0])[0], optional(args, "at", [0, 0])[1], 18, 18]]) replies: []}) + return fx_ok({id: id}) + } + let c = nil + for k in s.comments { if k.id == args.id { c = k } } + if c == nil { return fx_err("invalid: no comment " + args.id) } + fx_change(s) + let c2 = nil + for k in s.comments { if k.id == args.id { c2 = k } } + if args.op == "edit" { c2.text = optional(args, "text", c2.text) return fx_ok({id: c2.id}) } + if args.op == "reply" { + x.ids = x.ids + 1 + c2.replies.push({id: "r" + x.ids author: optional(args, "author", "PdfCraft") text: args.text date: fx_now()}) + return fx_ok({id: "r" + x.ids}) + } + if args.op == "status" { c2.status = args.status return fx_ok({id: c2.id}) } + if args.op == "delete" { + s.comments.retain(|k| k.id != args.id) + return fx_ok({deleted: true}) + } + return fx_err("invalid: op " + args.op) + } + if method == "fields" { return fx_ok({fields: s.fields}) } + if method == "fill" { + fx_change(s) + let n = 0 + for f in s.fields { + let v = optional(args.values, f.name, nil) + if v != nil { + f.value = v + n = n + 1 + } + } + return fx_ok({filled: n}) + } + if method == "fill_sign" { + fx_change(s) + return fx_ok({added: true}) + } + if method == "pages" { return fx_pages(x, s, args) } + if method == "edit_text" { + fx_change(s) + return fx_ok({edited: true}) + } + if method == "undo" || method == "redo" { + let from = pick(method == "undo", s.history, s.future) + if from.len() == 0 { return fx_ok({edited: s.edited can_undo: s.history.len() > 0 can_redo: s.future.len() > 0}) } + let snap = from.pop() + if method == "undo" { s.future.push(fx_snapshot(s)) } else { s.history.push(fx_snapshot(s)) } + s.order = snap.order + s.comments = snap.comments + s.fields = snap.fields + s.edited = true + return fx_ok({edited: s.edited can_undo: s.history.len() > 0 can_redo: s.future.len() > 0}) + } + if method == "save" { + s.edited = false + return fx_ok({path: "accounts/device/library/" + s.name bytes: d.file_size}) + } + if method == "export" { return fx_ok({paths: []}) } + fx_err("pdf." + method + " is not a method of the pdf service") +} +let fx_specials = ["\\", ".", "*", "+", "?", "(", ")", "[", "]", "{", "}", "|", "^", "$"] +fn fx_escape(text){ + let out = text + for c in fx_specials { out = out.replace(c, "\\" + c) } + out +} +// The service's comment dates: local time, no zone or seconds. +fn fx_now(){ + let t = local_time(time_now()) + "" + t.year + "-" + two(t.month) + "-" + two(t.day) + "T" + two(t.hour) + ":" + two(t.minute) +} +// The engine answers colours as #RRGGBB in capitals. +fn fx_upper(c){ + let out = "" + c + for pair in [["a", "A"], ["b", "B"], ["c", "C"], ["d", "D"], ["e", "E"], ["f", "F"]] { out = out.replace(regex(pair[0], "g"), pair[1]) } + out +} +// A new file in the library made of pages of others (a merge, an extract): +// a stand-in file, and a fixture document whose pages are theirs. +fn fx_new_file(x, out, order){ + let name = fx_name(out) + if fs.exists(out) { return fx_err("invalid: " + out + " already exists") } + fs.write(out, "PDF Tools dev fixture: a stand-in PDF\n") + // Its pages are drawn by the documents they came from. + let first = x.f.docs[order[0].name] + let sizes = [] + for e in order { sizes.push(x.f.docs[e.name].sizes[e.p - 1]) } + x.f.docs[name] = {id: first.id title: stem(name) file_size: 400000 sizes: sizes outline: [] comments: [] fields: [] map: order} + return fx_ok({out: out bytes: 400000}) +} +fn fx_pages(x, s, args){ + let pages = optional(args, "pages", []) + if args.op == "extract" { + let order = [] + for p in pages { order.push(s.order[p - 1]) } + let r = fx_new_file(x, args.out, order) + if !r.is_ok { return r } + return fx_ok({path: args.out}) + } + if args.op == "delete" && pages.len() >= s.order.len() { return fx_err("invalid: deleting every page is refused") } + fx_change(s) + if args.op == "rotate" { + for p in pages { s.order[p - 1].rot = s.order[p - 1].rot + args.angle + 360 } + } + if args.op == "delete" { + let kept = [] + for i e in s.order { if !has(pages, i + 1) { kept.push(e) } } + s.order = kept + } + if args.op == "duplicate" { + let out = [] + for i e in s.order { + out.push(e) + if has(pages, i + 1) { out.push(fx_copy(e)) } + } + s.order = out + } + if args.op == "move" { + let moving = [] + let rest = [] + for i e in s.order { if has(pages, i + 1) { moving.push(e) } else { rest.push(e) } } + let to = clamp(args.to, 1, rest.len() + 1) + let out = [] + for i e in rest { + if i + 1 == to { for m in moving { out.push(m) } } + out.push(e) + } + if to > rest.len() { for m in moving { out.push(m) } } + s.order = out + } + if args.op == "insert_file" { + let other = fx_open_doc(x, fx_name(args.path)) + if other == nil { return fx_err("not_found: " + args.path) } + let out = [] + let at = clamp(args.at, 1, s.order.len() + 1) + for i e in s.order { + if i + 1 == at { for o in other.order { out.push(o) } } + out.push(e) + } + if at > s.order.len() { for o in other.order { out.push(o) } } + s.order = out + } + fx_ok({pages: s.order.len()}) +} diff --git a/apps/pdftools/dev-fixture/make_fixture.py b/apps/pdftools/dev-fixture/make_fixture.py new file mode 100644 index 00000000..fc0c482d --- /dev/null +++ b/apps/pdftools/dev-fixture/make_fixture.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 +"""PDF Tools' dev fixture: sample documents for UI tests in App Hub's card-host. + +It never ships. `apps/pdftools/tests/ui.py` runs it to fill a card-host app +storage folder (PDF Tools' jail) with: + +- accounts/device/library/*.pdf: small stand-in PDFs, so Home lists them; +- dev/pages//.png: each page drawn here with PIL at 2 pixels per PDF + point, the size the app asks for at 100%; +- dev/fixture.json: what the stand-in engine (engine.splash) answers for + each document: pages and sizes, outline, form fields and comments; +- dev/lines//.json: each page's text lines and paragraphs with their + boxes in points. + +The page text is the approved designs' sample text (design/source/), so a +grab can be put beside its design. Usage: make_fixture.py +""" +import json +import sys +import time +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +S = 2 # pixels per point +LETTER = (612, 792) +FONTS = { + "serif": ["/System/Library/Fonts/Supplemental/Georgia.ttf", "/System/Library/Fonts/Supplemental/Times New Roman.ttf"], + "serif-bold": ["/System/Library/Fonts/Supplemental/Georgia Bold.ttf"], + "sans": ["/System/Library/Fonts/Supplemental/Arial.ttf"], + "sans-bold": ["/System/Library/Fonts/Supplemental/Arial Bold.ttf"], +} +NAMES = {"serif": "Georgia", "serif-bold": "Georgia-Bold", "sans": "Arial", "sans-bold": "Arial-Bold"} +BLUE = (27, 71, 144) +INK = (40, 40, 42) +GREY = (120, 122, 126) +_cache = {} + + +def font(kind, size): + key = (kind, size) + if key not in _cache: + found = None + for path in FONTS[kind]: + if Path(path).exists(): + found = ImageFont.truetype(path, int(round(size * S))) + break + _cache[key] = found or ImageFont.load_default() + return _cache[key] + + +class Page: + def __init__(self, size=LETTER): + self.size = size + self.img = Image.new("RGB", (size[0] * S, size[1] * S), "white") + self.draw = ImageDraw.Draw(self.img) + self.lines = [] + self.paragraphs = [] + + def text(self, x, y, text, kind="serif", size=11, color=INK, record=True): + f = font(kind, size) + self.draw.text((x * S, y * S), text, font=f, fill=color) + w = f.getlength(text) / S + if record: + self.lines.append({"n": len(self.lines) + 1, "text": text, "box": [round(x, 1), round(y, 1), round(w, 1), round(size * 1.25, 1)], + "font": NAMES[kind], "size": size}) + return w + + def para(self, x, y, width, text, kind="serif", size=11, leading=1.55, color=INK): + f = font(kind, size) + words, line, rows = text.split(), "", [] + for word in words: + trial = (line + " " + word).strip() + if f.getlength(trial) / S > width and line: + rows.append(line) + line = word + else: + line = trial + if line: + rows.append(line) + first = len(self.lines) + 1 + top = y + for row in rows: + self.text(x, y, row, kind, size, color) + y += size * leading + self.paragraphs.append({"n": len(self.paragraphs) + 1, "text": " ".join(rows), "box": [x, round(top, 1), width, round(y - top, 1)], + "lines": list(range(first, len(self.lines) + 1)), "font": NAMES[kind], "size": size}) + return y + + def rule(self, x, y, w, color=(170, 172, 178)): + self.draw.line([(x * S, y * S), (x * S + w * S, y * S)], fill=color, width=S) + + def box(self, x, y, w, h, fill=None, outline=None): + self.draw.rectangle([x * S, y * S, (x + w) * S, (y + h) * S], fill=fill, outline=outline, width=S) + + def mark(self, line, color=(250, 226, 89)): + """Paint a highlight under a recorded line, as a saved annotation looks.""" + x, y, w, h = line["box"] + layer = Image.new("RGBA", self.img.size, (0, 0, 0, 0)) + ImageDraw.Draw(layer).rectangle([x * S - 2, y * S, (x + w) * S + 2, (y + h) * S], fill=color + (150,)) + self.img = Image.alpha_composite(self.img.convert("RGBA"), layer).convert("RGB") + self.draw = ImageDraw.Draw(self.img) + self.draw.text((x * S, y * S), line["text"], font=font("serif", line["size"]), fill=INK) + + def answer(self): + return {"lines": self.lines, "paragraphs": self.paragraphs} + + +def report_header(p, n): + p.text(54, 44, "Northwind Cooperative", "sans", 8.5, BLUE) + p.text(450, 44, "Q3 2026 Board Report", "sans", 8.5, GREY) + p.text(552, 752, str(n), "sans", 9, GREY) + + +def chart(p, x, y, values, labels, top): + w, h = 300, 150 + p.rule(x + 20, y + h, w) + for k in range(5): + gy = y + h - k * h / 4 + p.text(x, gy - 6, str(int(top * k / 4)), "sans", 7, GREY, record=False) + step = w / len(values) + for i, v in enumerate(values): + bh = h * v / top + bx = x + 30 + i * step + p.box(bx, y + h - bh, step * 0.5, bh, fill=(22, 106, 217)) + p.text(bx + 2, y + h - bh - 13, str(v), "sans", 8, INK, record=False) + p.text(bx - 4, y + h + 6, labels[i], "sans", 8, INK, record=False) + + +SECTIONS = [ + ("Summary", "Northwind Cooperative delivered a solid performance in Q3 2026, with strong revenue growth, disciplined cost management and continued member value."), + ("Revenue", "Revenue for the third quarter of 2026 was $28.7 million, up from $25.6 million in Q3 2025. Revenue grew 12% year over year, led by the Northwest region."), + ("Operations", "Operational performance remained strong in Q3 2026. We continued to improve operational efficiency and service reliability."), + ("Outlook", "We are optimistic about the remainder of 2026 and beyond, with continued revenue growth and investments in grid modernization."), + ("Appendix", "A. Financial Statements. B. Key Metrics. C. Glossary. D. Notes."), +] + + +def board_report(): + pages = [] + # 1: cover + p = Page() + p.text(54, 90, "NORTHWIND", "sans-bold", 20, BLUE) + p.text(54, 116, "COOPERATIVE", "sans", 9, BLUE) + p.text(54, 200, "Q3 2026", "sans-bold", 30, INK) + p.text(54, 238, "Board Report", "sans-bold", 30, INK) + p.text(54, 290, "Building strong communities through cooperative growth.", "sans", 11, GREY) + p.box(0, 520, 612, 272, fill=(84, 132, 196)) + p.text(54, 470, "Prepared for the Board of Directors, October 20, 2026", "sans", 9, GREY) + pages.append(p) + # 2: contents / summary + p = Page() + report_header(p, 2) + p.text(54, 80, "Summary", "sans-bold", 26, INK) + p.rule(54, 120, 504) + y = p.para(54, 140, 470, SECTIONS[0][1], size=12) + y = p.para(54, y + 14, 470, "The board met on 21 September 2026 to review the quarter's results.", size=12) + y = p.para(54, y + 14, 470, "We advanced key operational initiatives across our regions and reinforced our focus on member service, product quality and sustainable practices.", size=12) + p.para(54, y + 14, 470, "This report summarises financial results, operations highlights and our outlook for the remainder of the year.", size=12) + pages.append(p) + # 3: revenue with the chart and a saved highlight + p = Page() + report_header(p, 3) + p.text(54, 78, "Revenue", "sans-bold", 26, BLUE) + p.rule(54, 118, 504) + y = p.para(54, 136, 480, SECTIONS[1][1], size=12) + highlighted = p.lines[p.paragraphs[0]["lines"][1] - 1] + p.mark(highlighted) + y = p.para(54, y + 10, 480, "Strong customer demand and disciplined pricing supported broad-based growth across all regions.", size=12) + p.text(54, y + 18, "Revenue by region, Q3 2026", "sans-bold", 12, BLUE) + p.text(54, y + 36, "Millions of USD", "sans", 8, GREY, record=False) + chart(p, 54, y + 56, [12.4, 7.1, 5.6, 3.6], ["Northwest", "Coast", "Valley", "Metro"], 14) + p.para(54, y + 250, 480, "The Northwest region contributed 43% of total revenue, supported by strong performance in commercial and agricultural segments.", size=12) + pages.append(p) + # 4..24: the other sections, then numbered notes + for n in range(4, 25): + p = Page() + report_header(p, n) + title, body = SECTIONS[(n - 2) % len(SECTIONS)] if n <= 6 else ("Notes, part " + str(n - 6), "This page continues the notes to the report. Revenue and costs are shown for the quarter, with comparisons to the same period last year." if n in (9, 15, 16, 21) else "This page continues the notes to the report, with the figures the board asked for at its last meeting.") + p.text(54, 78, title, "sans-bold", 24, BLUE) + p.rule(54, 116, 504) + y = p.para(54, 136, 480, body, size=12) + for k in range(4): + y = p.para(54, y + 12, 480, "Members, staff and partners worked together through the quarter. The cooperative kept its commitments while preparing for the year ahead.", size=12) + pages.append(p) + outline = [{"title": "Summary", "page": 2, "children": []}, + {"title": "Revenue", "page": 3, "children": [{"title": "Revenue by region", "page": 3, "children": []}]}, + {"title": "Operations", "page": 4, "children": []}, + {"title": "Outlook", "page": 5, "children": []}, + {"title": "Appendix", "page": 6, "children": []}] + third = pages[2] + rev = third.lines[third.paragraphs[0]["lines"][1] - 1]["box"] + comments = [{"id": "c1", "page": 3, "type": "highlight", "author": "Jun Park", "text": "Can we add the regional breakdown table here?", "date": "2026-10-10T10:12", + "color": "#FFD84D", "status": "accepted", "rects": [rev], + "replies": [{"id": "r1", "author": "Maya Chen", "text": "Added on page 4.", "date": "2026-10-10T10:20"}]}, + {"id": "c2", "page": 7, "type": "note", "author": "Ana Ruiz", "text": "Please check these figures against the audited accounts.", "date": "2026-10-09T15:30", + "color": "#2D7FF9", "status": "none", "rects": [[480, 140, 18, 18]], "replies": []}] + return pages, outline, comments, [] + + +def lease(): + pages = [] + p = Page() + p.text(200, 60, "RIVERSIDE RESIDENCES", "sans-bold", 18, INK) + p.text(230, 86, "RESIDENTIAL LEASE AGREEMENT", "sans", 9, INK) + y = p.para(54, 120, 500, "This Residential Lease Agreement (\"Agreement\") is made between Riverside Residences LLC (\"Landlord\") and the Tenant named below.", size=10.5) + rows = [("1. PARTIES", [("Tenant name:", "Tenant name"), ("Landlord:", None)]), ("2. PREMISES", [("Address:", None), ("Unit:", "Unit")]), + ("3. TERM", [("Start date:", "Start date"), ("End date:", "End date")]), ("4. RENT", [("Monthly rent: $", "Monthly rent")])] + fields = [] + y += 14 + for head, items in rows: + p.text(54, y, head, "sans-bold", 10) + y += 24 + for label, field in items: + p.text(70, y, label, "serif", 10) + if field: + rect = [160, y - 3, 210 if field in ("Tenant name", "Monthly rent") else 110, 18] + fields.append({"name": field, "type": "text", "value": {"Tenant name": "Maya Chen", "Unit": "4B", "Start date": "1 November 2026"}.get(field, ""), + "required": field == "Monthly rent", "read_only": False, "page": 1, "rect": rect, "options": []}) + else: + p.text(160, y, {"Landlord:": "Riverside Residences LLC", "Address:": "123 Riverfront Drive, Portland, OR 97201"}[label], "serif", 10) + y += 26 + y += 6 + p.text(54, y, "5. SIGNATURES", "sans-bold", 10) + y = p.para(70, y + 22, 470, "By signing below, the parties agree to the terms of this Agreement.", size=10) + p.text(70, y + 10, "Landlord", "serif-bold", 10) + p.text(330, y + 10, "Tenant", "serif-bold", 10) + p.text(70, y + 70, "Initials:", "serif", 10) + p.text(330, y + 70, "Initials:", "serif", 10) + fields.append({"name": "Initials (Landlord)", "type": "text", "value": "", "required": True, "read_only": False, "page": 1, "rect": [120, y + 66, 32, 20], "options": []}) + fields.append({"name": "Initials (Tenant)", "type": "text", "value": "", "required": True, "read_only": False, "page": 1, "rect": [380, y + 66, 32, 20], "options": []}) + p.text(54, 752, "Page 1 of 2", "serif", 9, GREY) + pages.append(p) + p = Page() + p.para(54, 80, 500, "6. RULES. The Tenant agrees to follow the building rules attached to this Agreement.", size=10.5) + p.text(54, 752, "Page 2 of 2", "serif", 9, GREY) + pages.append(p) + return pages, [], [], fields + + +def simple(title, count, lead): + pages = [] + for n in range(1, count + 1): + p = Page() + p.text(54, 70, title if n == 1 else title + ", page " + str(n), "serif-bold", 22 if n == 1 else 14, INK) + y = p.para(54, 120, 480, lead, size=11) + p.para(54, y + 12, 480, "This sample page stands in for the engine's render in PDF Tools' card-host tests.", size=11) + pages.append(p) + return pages, [], [], [] + + +DOCS = [ + ("Q3 2026 Board Report", "board", board_report, 2936013), + ("Riverside Lease 2026", "lease", lease, 1153434), + ("Field Guide to Garden Birds", "birds", lambda: simple("Field Guide to Garden Birds", 7, "Identify common birds in your backyard and local parks."), 16384), + ("Invoice INV-2041", "invoice", lambda: simple("INVOICE", 1, "INV-2041. Bill to: Northwind Cooperative, 123 Harbor Way, Portland, OR 97201. Consulting Services $2,400.00"), 86016), + ("Site Survey Photos", "survey", lambda: simple("Site Survey Photos", 4, "Project: Community Center. Date: September 25, 2026."), 10066330), + ("Appendix A - Figures", "appendix", lambda: simple("Appendix A - Figures", 9, "Figures for the Q3 2026 Board Report."), 412000), + ("Cover Letter", "letter", lambda: simple("Cover Letter", 2, "Dear members of the board, please find the quarter's report enclosed."), 52000), +] + + +def main(): + jail = Path(sys.argv[1]) + library = jail / "accounts/device/library" + library.mkdir(parents=True, exist_ok=True) + out = {"docs": {}, "damaged": ["Damaged scan.pdf"], "protected": ["Payroll 2026.pdf"], "you": "Maya Chen"} + for title, ident, make, size in DOCS: + pages, outline, comments, fields = make() + folder = jail / "dev/pages" / ident + folder.mkdir(parents=True, exist_ok=True) + for n, p in enumerate(pages, 1): + p.img.save(folder / f"{n}.png", optimize=True) + small = pages[0].img.resize((pages[0].img.width // 8, pages[0].img.height // 8)) + small.save(library / f"{title}.pdf") + # Each page's lines in a small file of its own: the stand-in reads one + # when asked, inside one handler's 64 ms. + lines = jail / "dev/lines" / ident + lines.mkdir(parents=True, exist_ok=True) + for n, p in enumerate(pages, 1): + (lines / f"{n}.json").write_text(json.dumps(p.answer())) + out["docs"][f"{title}.pdf"] = { + "id": ident, "title": title, "file_size": size, + "sizes": [list(p.size) for p in pages], "outline": outline, "comments": comments, "fields": fields, + } + for name in out["damaged"] + out["protected"]: + Image.new("RGB", (60, 80), "white").save(library / name) + (jail / "dev/fixture.json").write_text(json.dumps(out)) + # The library index as the design shows Home: when each was last opened. + now = time.time() + def at(month, day): + return time.mktime((2026, month, day, 10, 0, 0, 0, 0, -1)) + opened = {"Q3 2026 Board Report": now - 3600, "Riverside Lease 2026": now - 86400, + "Field Guide to Garden Birds": at(10, 3), "Invoice INV-2041": at(10, 1), "Site Survey Photos": at(9, 28)} + docs = {f"{title}.pdf": {"opened": stamp} for title, stamp in opened.items()} + (library.parent / "library.json").write_text(json.dumps({"version": 1, "docs": docs})) + print(f"{len(out['docs'])} documents in {jail}") + + +if __name__ == "__main__": + main() diff --git a/apps/pdftools/tests/evidence/shell-20261009/README.md b/apps/pdftools/tests/evidence/shell-20261009/README.md deleted file mode 100644 index 66d6a228..00000000 --- a/apps/pdftools/tests/evidence/shell-20261009/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# PDF Tools in the desktop shell — 2026-10-09 - -English | [简体中文](README.zh-CN.md) - -[ui.py](../../ui.py) ran PDF Tools end to end in a hidden OctoSense desktop -shell built from this branch (`cargo build --locked -p octosense`, debug, on -macOS, Apple silicon), with the real `pdf` engine working in the app's own -storage. Each run had a fresh `OCTOSENSE_HOME` with the sample PDFs that -octosense-pdf-service's `pdftools_fixture` example writes; the shell started -from a Terminal.app tab with `MAKEPAD_HIDE_WINDOWS=1`, -`MAKEPAD_REMOTE=127.0.0.1:8915` and `MAKEPAD_WM_TEST_APP=pdftools`, under the -machine's shell-run lock, and ended with `/quit`. [receipt.json](receipt.json) -holds the binary, bundle and source digests, the commands and the checks. - -Each frame is the app's window cut from an original `/g` grab of the shell -and halved with macOS `sips`; the `missing/` frames are original `/g` grabs -of App Hub's `card-host`. Every frame was inspected. - -- [light/](light): the library with the engine's first pages, "Open a PDF - from this device" refused because a hidden window is never in front, the - page grid, the page view and Next, the text and find, the info list (top - and end), split every 2 pages and "where I choose", a split at page 2, a - damaged file refused from a merge, merge (choose, order, done, the merged - file), removing a PDF (the question, then the library), and the damaged - file. -- [dark/](dark): the same journey after the shell's own switch to its dark - style, splitting every 2 pages instead (which replaces a part the light run - made). The app keeps two columns of pages there: the switch re-runs the - app's script and the shell does not call `on_app_resize` again until the - window's size changes. -- [restart/](restart): the same home after a restart. What merge and split - wrote is still there, and `Imported PDF.pdf`, placed in the library between - the runs as `files.import` leaves a file, was read, drawn and opened - (pages, text, a page). -- [full/](full): storage filled to 8 KB short of its 64 MiB: the engine - refuses to write the first pages and a merge, and the app says storage is - full; then a PDF is removed. -- [empty/](empty): no PDFs, in light and dark: the empty library and its - Open button, then the refusal. -- [missing/](missing): App Hub's `card-host`, which serves no host - services: no engine (`no service answers "pdf" on this device`) and no - files service. - -**Not verified**: the host's file dialog and what follows a real import (a -hidden window cannot open it: the files service refuses first), the import's -other refusals (storage full, too large, busy), window sizes other than the -shell's default, Linux, Windows, phones. diff --git a/apps/pdftools/tests/evidence/shell-20261009/README.zh-CN.md b/apps/pdftools/tests/evidence/shell-20261009/README.zh-CN.md deleted file mode 100644 index 0a8387f5..00000000 --- a/apps/pdftools/tests/evidence/shell-20261009/README.zh-CN.md +++ /dev/null @@ -1,36 +0,0 @@ -# PDF Tools 在桌面 Shell 中 — 2026-10-09 - -[English](README.md) | 简体中文 - -[ui.py](../../ui.py) 在由本分支构建的隐藏 OctoSense 桌面 Shell 中端到端运行了 -PDF Tools(`cargo build --locked -p octosense`,debug 构建,macOS,Apple -silicon),真实的 `pdf` 引擎在应用自己的存储中工作。每次运行都使用一个新的 -`OCTOSENSE_HOME`,其中放有 octosense-pdf-service 的 `pdftools_fixture` 示例写出 -的示例 PDF;Shell 在本机的 Shell 运行锁之下,从 Terminal.app 标签页以 -`MAKEPAD_HIDE_WINDOWS=1`、`MAKEPAD_REMOTE=127.0.0.1:8915` 和 -`MAKEPAD_WM_TEST_APP=pdftools` 启动,并以 `/quit` 结束。 -[receipt.json](receipt.json) 记录了二进制、应用包和源文件的摘要、命令以及各项检查。 - -每一帧都是从 Shell 的原始 `/g` 截图中裁出的应用窗口,再用 macOS 的 `sips` 缩小 -一半;`missing/` 中的帧是 App Hub `card-host` 的原始 `/g` 截图。每一帧都已检查。 - -- [light/](light):带引擎生成首页图的资料库;因为隐藏窗口从不在前台, - “Open a PDF from this device”被拒绝;页面网格、单页视图和 Next、文字和查找、 - 信息列表(顶部和末尾)、每 2 页拆分和“Where I choose”、在第 2 页处拆分、 - 损坏的文件不能加入合并、合并(选择、排序、完成、合并后的文件)、移除一个 PDF - (确认,然后回到资料库),以及损坏的文件。 -- [dark/](dark):通过 Shell 自己的深色样式切换后的同一流程,改为每 2 页拆分 - (会替换浅色运行生成的一个文件)。这里页面网格保持两列:切换会重新运行应用的 - 脚本,而在窗口尺寸改变之前 Shell 不会再次调用 `on_app_resize`。 -- [restart/](restart):重启后同一个 home。合并和拆分写入的文件仍在;在两次运行 - 之间按 `files.import` 留下文件的方式放入资料库的 `Imported PDF.pdf` 被读取、 - 绘制并打开(页面、文字、单页)。 -- [full/](full):存储被填到距 64 MiB 上限只差 8 KB:引擎拒绝写入首页图和一次 - 合并,应用说明存储已满;然后移除一个 PDF。 -- [empty/](empty):没有 PDF,浅色和深色:空资料库及其 Open 按钮,然后是拒绝。 -- [missing/](missing):App Hub 的 `card-host`,它不提供任何宿主服务:没有引擎 - (`no service answers "pdf" on this device`),也没有文件服务。 - -**未验证**:宿主的文件对话框以及一次真实导入之后的流程(隐藏窗口无法打开它: -文件服务会先拒绝)、导入的其他拒绝情况(存储已满、文件太大、正在忙)、Shell -默认尺寸以外的窗口尺寸、Linux、Windows 和手机。 diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/01-library.png b/apps/pdftools/tests/evidence/shell-20261009/dark/01-library.png deleted file mode 100644 index 1b5bc8f1..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/01-library.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/02-open-from-device.png b/apps/pdftools/tests/evidence/shell-20261009/dark/02-open-from-device.png deleted file mode 100644 index 3325dc64..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/02-open-from-device.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/03-pages.png b/apps/pdftools/tests/evidence/shell-20261009/dark/03-pages.png deleted file mode 100644 index b5021aad..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/03-pages.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/04-page.png b/apps/pdftools/tests/evidence/shell-20261009/dark/04-page.png deleted file mode 100644 index cc6e2322..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/04-page.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/05-page-next.png b/apps/pdftools/tests/evidence/shell-20261009/dark/05-page-next.png deleted file mode 100644 index c9f340ae..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/05-page-next.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/06-text.png b/apps/pdftools/tests/evidence/shell-20261009/dark/06-text.png deleted file mode 100644 index 22473dd4..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/06-text.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/07-text-find.png b/apps/pdftools/tests/evidence/shell-20261009/dark/07-text-find.png deleted file mode 100644 index cdff55bc..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/07-text-find.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/08-info.png b/apps/pdftools/tests/evidence/shell-20261009/dark/08-info.png deleted file mode 100644 index 387a8f57..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/08-info.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/08b-info-end.png b/apps/pdftools/tests/evidence/shell-20261009/dark/08b-info-end.png deleted file mode 100644 index f8e6ca03..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/08b-info-end.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/09-split-every.png b/apps/pdftools/tests/evidence/shell-20261009/dark/09-split-every.png deleted file mode 100644 index cd6aa0c4..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/09-split-every.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/10-split-where.png b/apps/pdftools/tests/evidence/shell-20261009/dark/10-split-where.png deleted file mode 100644 index 24e58475..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/10-split-where.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/11-split-done.png b/apps/pdftools/tests/evidence/shell-20261009/dark/11-split-done.png deleted file mode 100644 index 0f2bed4c..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/11-split-done.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/12-merge-choose.png b/apps/pdftools/tests/evidence/shell-20261009/dark/12-merge-choose.png deleted file mode 100644 index 5f3985c4..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/12-merge-choose.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/13-merge-order.png b/apps/pdftools/tests/evidence/shell-20261009/dark/13-merge-order.png deleted file mode 100644 index f1649307..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/13-merge-order.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/14-merge-done.png b/apps/pdftools/tests/evidence/shell-20261009/dark/14-merge-done.png deleted file mode 100644 index 8c0d0956..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/14-merge-done.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/15-merged-pages.png b/apps/pdftools/tests/evidence/shell-20261009/dark/15-merged-pages.png deleted file mode 100644 index aca04707..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/15-merged-pages.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/16-remove-ask.png b/apps/pdftools/tests/evidence/shell-20261009/dark/16-remove-ask.png deleted file mode 100644 index 3abff3e4..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/16-remove-ask.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/17-removed.png b/apps/pdftools/tests/evidence/shell-20261009/dark/17-removed.png deleted file mode 100644 index f8893236..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/17-removed.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/dark/18-damaged.png b/apps/pdftools/tests/evidence/shell-20261009/dark/18-damaged.png deleted file mode 100644 index 7e5c9352..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/dark/18-damaged.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/empty/dark/e01-empty.png b/apps/pdftools/tests/evidence/shell-20261009/empty/dark/e01-empty.png deleted file mode 100644 index 863e946d..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/empty/dark/e01-empty.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/empty/dark/e02-empty-open.png b/apps/pdftools/tests/evidence/shell-20261009/empty/dark/e02-empty-open.png deleted file mode 100644 index af1fb57b..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/empty/dark/e02-empty-open.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/empty/light/e01-empty.png b/apps/pdftools/tests/evidence/shell-20261009/empty/light/e01-empty.png deleted file mode 100644 index a110ae95..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/empty/light/e01-empty.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/empty/light/e02-empty-open.png b/apps/pdftools/tests/evidence/shell-20261009/empty/light/e02-empty-open.png deleted file mode 100644 index db22bf65..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/empty/light/e02-empty-open.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/full/f01-library-full.png b/apps/pdftools/tests/evidence/shell-20261009/full/f01-library-full.png deleted file mode 100644 index 10686076..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/full/f01-library-full.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/full/f02-merge-full.png b/apps/pdftools/tests/evidence/shell-20261009/full/f02-merge-full.png deleted file mode 100644 index 8221ffad..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/full/f02-merge-full.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/full/f03-removed.png b/apps/pdftools/tests/evidence/shell-20261009/full/f03-removed.png deleted file mode 100644 index ce91eac5..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/full/f03-removed.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/01-library.png b/apps/pdftools/tests/evidence/shell-20261009/light/01-library.png deleted file mode 100644 index 2a7b3065..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/01-library.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/02-open-from-device.png b/apps/pdftools/tests/evidence/shell-20261009/light/02-open-from-device.png deleted file mode 100644 index ce0019a6..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/02-open-from-device.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/03-pages.png b/apps/pdftools/tests/evidence/shell-20261009/light/03-pages.png deleted file mode 100644 index 81d38568..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/03-pages.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/04-page.png b/apps/pdftools/tests/evidence/shell-20261009/light/04-page.png deleted file mode 100644 index 7a2ba025..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/04-page.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/05-page-next.png b/apps/pdftools/tests/evidence/shell-20261009/light/05-page-next.png deleted file mode 100644 index 001b384f..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/05-page-next.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/06-text.png b/apps/pdftools/tests/evidence/shell-20261009/light/06-text.png deleted file mode 100644 index dd2c0170..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/06-text.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/07-text-find.png b/apps/pdftools/tests/evidence/shell-20261009/light/07-text-find.png deleted file mode 100644 index 753c9715..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/07-text-find.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/08-info.png b/apps/pdftools/tests/evidence/shell-20261009/light/08-info.png deleted file mode 100644 index 0ab9ab56..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/08-info.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/08b-info-end.png b/apps/pdftools/tests/evidence/shell-20261009/light/08b-info-end.png deleted file mode 100644 index 7c9eb631..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/08b-info-end.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/09-split-every.png b/apps/pdftools/tests/evidence/shell-20261009/light/09-split-every.png deleted file mode 100644 index 27dae0b4..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/09-split-every.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/10-split-where.png b/apps/pdftools/tests/evidence/shell-20261009/light/10-split-where.png deleted file mode 100644 index 3ff09811..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/10-split-where.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/11-split-done.png b/apps/pdftools/tests/evidence/shell-20261009/light/11-split-done.png deleted file mode 100644 index 53ce4b05..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/11-split-done.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/12-merge-choose.png b/apps/pdftools/tests/evidence/shell-20261009/light/12-merge-choose.png deleted file mode 100644 index 4a8e76e2..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/12-merge-choose.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/13-merge-order.png b/apps/pdftools/tests/evidence/shell-20261009/light/13-merge-order.png deleted file mode 100644 index 7cb29176..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/13-merge-order.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/14-merge-done.png b/apps/pdftools/tests/evidence/shell-20261009/light/14-merge-done.png deleted file mode 100644 index 7896f24d..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/14-merge-done.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/15-merged-pages.png b/apps/pdftools/tests/evidence/shell-20261009/light/15-merged-pages.png deleted file mode 100644 index 82a4e946..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/15-merged-pages.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/16-remove-ask.png b/apps/pdftools/tests/evidence/shell-20261009/light/16-remove-ask.png deleted file mode 100644 index c6868032..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/16-remove-ask.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/17-removed.png b/apps/pdftools/tests/evidence/shell-20261009/light/17-removed.png deleted file mode 100644 index a1ae5af9..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/17-removed.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/light/18-damaged.png b/apps/pdftools/tests/evidence/shell-20261009/light/18-damaged.png deleted file mode 100644 index 56c2fe7a..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/light/18-damaged.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/missing/m01-missing-library.png b/apps/pdftools/tests/evidence/shell-20261009/missing/m01-missing-library.png deleted file mode 100644 index f49c2854..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/missing/m01-missing-library.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/missing/m02-missing-document.png b/apps/pdftools/tests/evidence/shell-20261009/missing/m02-missing-document.png deleted file mode 100644 index eed706c0..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/missing/m02-missing-document.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/missing/m03-missing-merge.png b/apps/pdftools/tests/evidence/shell-20261009/missing/m03-missing-merge.png deleted file mode 100644 index a5da68e4..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/missing/m03-missing-merge.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/receipt.json b/apps/pdftools/tests/evidence/shell-20261009/receipt.json deleted file mode 100644 index 64b88fe5..00000000 --- a/apps/pdftools/tests/evidence/shell-20261009/receipt.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "result": "pass", - "date": "2026-10-09", - "scope": "PDF Tools end to end in a hidden OctoSense desktop shell built from this branch (debug, macOS, Apple silicon) with the real pdf engine in the app's own storage, plus App Hub's card-host without host services. Every engine call in the shell runs was real.", - "live_engine": true, - "build_base": "4b704d6b (main with #419, #433 and #432); the branch was then rebased onto c6fbae6f, a docs-only delta that the shell binary does not contain", - "bundle_blake3": "06790fdf253a3692a3d0bf57c0ab71997d788f8aaaf25df7b53b561439f1d602", - "shell": { - "branch_base": "c6fbae6fdd68de7d3ee4e676c73022d805d3a70b", - "binary_sha256": "09dea35ce48a6cf5da98cfc9058016c5f3435531d5a6d46d3f4240135d6869a5", - "binary": "target/debug/octosense (cargo build --locked -p octosense)", - "app_hub_revision": "28745c0f6fd71986820e9835183a3045f61baefc", - "runtime": "the root's .sources: makepad 32d6415f with runtime-patches tree 80242e50, Octoscript-Makepad 33dea2f1, Octoscript 2e37d9e6", - "launch": "a Terminal.app tab (osascript) under the machine's shell-run lock: OCTOSENSE_HOME= OCTOS_APP_CORE_DIR=/octos-home/.octos MAKEPAD_HOME=/makepad MAKEPAD_HIDE_WINDOWS=1 MAKEPAD_REMOTE=127.0.0.1:8915 MAKEPAD_WM_TEST_APP=pdftools; ended with /quit", - "admitted": "card: os.pdftools running under 3 capability(ies), 1 host(s), 67108864 bytes of storage, 4000000000 instructions, 134217728 bytes of heap" - }, - "card_host": { - "binary_sha256": "0a4de5a77090ec269781484e126efe49675dbf22754cb73d3ce822fbc408f1b1", - "app_hub_revision": "28745c0f6fd71986820e9835183a3045f61baefc", - "admitted": "card-host: os.pdftools 0.1.0 admitted — capabilities {\"files\", \"pdf\", \"storage\"}, hosts {}, storage 67108864 bytes, agent none" - }, - "source_sha256": { - "apps/pdftools/bundle/main.splash": "1d41943b8d46305c3a26bc21015754c977dd673902f2da391e0e2c143530e073", - "apps/pdftools/bundle/manifest.json": "5dbab29c74d43592d2d4187010e1e2da6fb114b013c37e30d00e3e1ea6ba8344", - "apps/pdftools/bundle/icon.svg": "653adc67f464a4472a907d511dc5d7de646db1a39d29002b9e48d673a858a7bc", - "apps/pdftools/tests/ui.py": "231b59e7a1a534f87cf133e38557ca96964ec15a435dbca5a8103427872231cf", - "apps/pdf/host-service/examples/pdftools_fixture.rs": "e8b3ad6ced135bb1f4bb4fa93df3ddb76bd928eb2a8f30d1ad0af015b9742305" - }, - "commands": [ - "cargo build --locked -p octosense", - "python3 apps/pdftools/tests/ui.py --shell target/debug/octosense --lock /octosense-shell.lock --work /homes --output /run", - "python3 apps/pdftools/tests/ui.py --shell target/debug/octosense --lock /octosense-shell.lock --work /homes --output /run2 --only full --only empty", - "python3 apps/pdftools/tests/ui.py --card-host /target/release/card-host --only missing --work /homes --output /card-host", - "frames: sips -c --cropOffset then sips --resampleWidth (the app's window from each grab's sidecar area)" - ], - "runs": { - "shell": "the samples, light then dark through the shell's style menu (OctoSense · Dark)", - "restart": "the same home again, with Imported PDF.pdf (pdftools_fixture --import-sample) placed in the library between the runs", - "full": "a fresh home whose storage was filled to 8 KB short of 64 MiB", - "empty": "a fresh home with no PDFs, light then dark", - "missing": "card-host, the samples, MAKEPAD_WIDGET_STYLE=macos; no host services" - }, - "checks": [ - "The library lists the samples with the engine's first pages and '4 pages · 9 KB'; 'PDFs up to 64 MB' from files.status", - "Open a PDF from this device: files.import is refused with 'foreground_required: Return to this app before choosing or sharing a file' (a hidden window never has focus) and the app shows it in a card", - "Field guide opens on its page grid (five columns from on_app_resize); a page opens at 1400 px; Next goes to page 3", - "Text shows 'PAGE 1'; finding 'wren' reports '2 pages contain “wren”.'", - "Info shows 'A4, 210 × 297 mm', 'Not protected' and 'Helvetica, Helvetica-Bold, Times-Italic, Times-Roman'", - "Split every 2 previews 'Makes 4 files'; Where I choose marks 'File 2 starts here'; light splits at page 2 (2 files, Pages 2–7); dark splits every 2 pages (4 files, Page 7) and first says 'Replaces 1 file of the same name in your library.'", - "A damaged file can't join a merge ('“Damaged scan” can't be opened, so it can't be merged.')", - "Merge Quarterly report and Board minutes, Board minutes moved up, into 'Merged' (6 pages · 13 KB); dark merges Apartment lease and Board minutes into 'Merged 2'; both open", - "Remove asks once more and removes the PDF ('Removed “Field guide-part1” from PDF Tools.')", - "Damaged scan reads 'Couldn't open this PDF' with the engine's own message", - "After a restart Merged and Merged 2 are still there; Imported PDF is read (3 pages), drawn, opened, and its text and page show", - "With 8 KB left, the library says 'PDF Tools' storage is full' and a merge fails with 'There isn't room for it in PDF Tools' storage…'; removing a PDF leaves 4 PDFs", - "The empty library offers Open a PDF from this device, in light and dark, and shows the refusal", - "In card-host: 'This device has no PDF engine' with 'no service answers \"pdf\" on this device', and Open a PDF from this device 'Not available here'", - "No [E], on_render, callback or script-budget error lines in any shell or card-host log" - ], - "not_verified": [ - "the host's file dialog and what follows a real import (imported() in the script): a hidden window cannot open the dialog", - "the import's other refusals (storage full, too large, too many files, busy)", - "window sizes other than the shell's default; a light/dark switch re-runs the script, so the grid keeps two columns until the window's size changes", - "Linux, Windows, phones" - ], - "grabs": [ - "dark/01-library.png", - "dark/02-open-from-device.png", - "dark/03-pages.png", - "dark/04-page.png", - "dark/05-page-next.png", - "dark/06-text.png", - "dark/07-text-find.png", - "dark/08-info.png", - "dark/08b-info-end.png", - "dark/09-split-every.png", - "dark/10-split-where.png", - "dark/11-split-done.png", - "dark/12-merge-choose.png", - "dark/13-merge-order.png", - "dark/14-merge-done.png", - "dark/15-merged-pages.png", - "dark/16-remove-ask.png", - "dark/17-removed.png", - "dark/18-damaged.png", - "empty/dark/e01-empty.png", - "empty/dark/e02-empty-open.png", - "empty/light/e01-empty.png", - "empty/light/e02-empty-open.png", - "full/f01-library-full.png", - "full/f02-merge-full.png", - "full/f03-removed.png", - "light/01-library.png", - "light/02-open-from-device.png", - "light/03-pages.png", - "light/04-page.png", - "light/05-page-next.png", - "light/06-text.png", - "light/07-text-find.png", - "light/08-info.png", - "light/08b-info-end.png", - "light/09-split-every.png", - "light/10-split-where.png", - "light/11-split-done.png", - "light/12-merge-choose.png", - "light/13-merge-order.png", - "light/14-merge-done.png", - "light/15-merged-pages.png", - "light/16-remove-ask.png", - "light/17-removed.png", - "light/18-damaged.png", - "missing/m01-missing-library.png", - "missing/m02-missing-document.png", - "missing/m03-missing-merge.png", - "restart/r01-library.png", - "restart/r02-imported-pages.png", - "restart/r03-imported-text.png", - "restart/r04-imported-page.png" - ] -} diff --git a/apps/pdftools/tests/evidence/shell-20261009/restart/r01-library.png b/apps/pdftools/tests/evidence/shell-20261009/restart/r01-library.png deleted file mode 100644 index ce62924a..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/restart/r01-library.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/restart/r02-imported-pages.png b/apps/pdftools/tests/evidence/shell-20261009/restart/r02-imported-pages.png deleted file mode 100644 index c9ac0492..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/restart/r02-imported-pages.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/restart/r03-imported-text.png b/apps/pdftools/tests/evidence/shell-20261009/restart/r03-imported-text.png deleted file mode 100644 index 96d6d5db..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/restart/r03-imported-text.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/shell-20261009/restart/r04-imported-page.png b/apps/pdftools/tests/evidence/shell-20261009/restart/r04-imported-page.png deleted file mode 100644 index 95f60ebd..00000000 Binary files a/apps/pdftools/tests/evidence/shell-20261009/restart/r04-imported-page.png and /dev/null differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/README.md b/apps/pdftools/tests/evidence/v2-20261010/README.md new file mode 100644 index 00000000..8724d282 --- /dev/null +++ b/apps/pdftools/tests/evidence/v2-20261010/README.md @@ -0,0 +1,58 @@ +# PDF Tools v2 runs — 2026-10-10 + +English | [简体中文](README.zh-CN.md) + +[ui.py](../../ui.py) ran PDF Tools v2 end to end on this branch (macOS, +Apple silicon). [receipt.json](receipt.json) holds the binaries', bundle's +and sources' digests, the commands, what each run checked and what was not +verified. The shell runs and `missing` ran at the runtime's 64 ms script +budget; `fixture` ran with `--budget-ms 1000`, because its stand-in engine +answers in script. + +**The hidden desktop shell, with the real `pdf` engine** (`cargo build +--locked -p octosense --no-default-features --features app-hub,craft-engines`, +debug): each run had a fresh `OCTOSENSE_HOME` with the sample PDFs that +octosense-pdf-service's `pdftools_fixture` example writes; the shell started +from a Terminal.app tab with `MAKEPAD_HIDE_WINDOWS=1`, +`MAKEPAD_REMOTE=127.0.0.1:8915` and `MAKEPAD_WM_TEST_APP=pdftools`, under the +machine's shell-run lock, one instance at a time, and ended with `/quit`. +Every engine call was real. Each frame is the app's window cut from an +original `/g` grab, one pixel per point: the shell gave PDF Tools its default +window, 990 x 603 points. + +- [light/](light): Home with the engine's first pages; "Open a PDF from this + device" refused (a hidden window is never in front); the report open + (page 1, then page 2 from its thumbnail); find ("9 matches on 4 pages", + the first match highlighted on its page); a highlight on the Summary's + first line in Comment mode, then its reply; Pages with page 2 turned right, + then Undo; Combine with Board minutes' page 1 ("One PDF of 5 pages"), then + the combined PDF open; initials placed by Fill & Sign on the lease (it has + no form fields); Edit with clause 1 in the editor and " (amended)" typed, + then applied; Save; the damaged file. +- [dark/](dark): the same journey after the shell's own switch to its dark + style, on what the light pass left (the same tabs, the name and initials + it kept, its edits). +- [restart/](restart): the same home after a restart: the library and what + the runs wrote, `Imported PDF.pdf` (placed in the library between the runs + as `files.import` leaves a file) opened, and the report back at the page + it was left at. +- [full/](full): storage filled to 8 KB short of its 64 MiB: Home says + storage is full; removing a PDF asks once more, then removes it. +- [empty/](empty): no PDFs, light and dark: the empty library and its Open + button, then the refusal. +- [compare/](compare): shell frames beside their designs, the same height + (the designs are 1536 x 1024; the shell's window is 990 x 603, so the + layout is the narrow one README.md describes). + +**App Hub's `card-host`**, which serves no host services: + +- [missing/](missing): the shipped bundle: no engine, no files service. +- [fixture/](fixture): a scratch copy of the bundle with the stand-in engine + (`dev-fixture/engine.splash`) and the designs' sample documents + (`dev-fixture/make_fixture.py`), at the designs' 1536 x 1024: every + designed screen beside its design (design left, app right), light, then + dark (`d`-prefixed; only 09 has a dark design), and the damaged, + protected and unsaved states. Sheets are halved and reduced to 256 + colours. + +Every frame was inspected. diff --git a/apps/pdftools/tests/evidence/v2-20261010/README.zh-CN.md b/apps/pdftools/tests/evidence/v2-20261010/README.zh-CN.md new file mode 100644 index 00000000..8a28dd7a --- /dev/null +++ b/apps/pdftools/tests/evidence/v2-20261010/README.zh-CN.md @@ -0,0 +1,45 @@ +# PDF Tools 第 2 版的运行记录 — 2026-10-10 + +[English](README.md) | 简体中文 + +[ui.py](../../ui.py) 在这个分支上端到端地运行了 PDF Tools 第 2 版(macOS,Apple +芯片)。[receipt.json](receipt.json) 记录了二进制文件、应用包和源文件的摘要、所用命令、 +每次运行检查的内容以及未验证的内容。Shell 中的运行和 `missing` 使用运行时 64 ms 的脚本 +预算;`fixture` 使用 `--budget-ms 1000`,因为它的替身引擎在脚本中作答。 + +**隐藏的桌面 Shell,使用真实的 `pdf` 引擎**(`cargo build --locked -p octosense +--no-default-features --features app-hub,craft-engines`,debug 构建):每次运行都使用 +一个新的 `OCTOSENSE_HOME`,其中放有 octosense-pdf-service 的 `pdftools_fixture` 示例写入 +的示例 PDF;Shell 从 Terminal.app 的标签页以 `MAKEPAD_HIDE_WINDOWS=1`、 +`MAKEPAD_REMOTE=127.0.0.1:8915` 和 `MAKEPAD_WM_TEST_APP=pdftools` 启动,持有本机的 +Shell 运行锁,同一时间只运行一个实例,并以 `/quit` 结束。每一次引擎调用都是真实的。每一帧 +都是从原始 `/g` 截图中裁出的应用窗口,每点一个像素:Shell 给 PDF Tools 的是它的默认窗口, +990 x 603 点。 + +- [light/](light):主页及引擎渲染的首页;“Open a PDF from this device”被拒绝(隐藏窗口 + 永远不在最前面);打开报告(第 1 页,再通过缩略图到第 2 页);查找(“9 matches on 4 + pages”,第一处匹配在页面上高亮);在 Comment 模式下高亮 Summary 的第一行,然后回复; + 在 Pages 中把第 2 页向右旋转,然后撤销;Combine 加入 Board minutes 的第 1 页(“One PDF + of 5 pages”),随后合并出的 PDF 打开;在租约上用 Fill & Sign 放置姓名缩写(它没有表单 + 字段);Edit 中把第 1 条放进编辑框并输入“ (amended)”,然后应用;保存;损坏的文件。 +- [dark/](dark):通过 Shell 自己的样式切换到深色后的同一流程,在浅色一轮留下的状态上进行 + (同样的标签、它保存的名字和姓名缩写、它的编辑)。 +- [restart/](restart):重启后的同一个主目录:资料库和各次运行写入的内容、在两次运行之间 + 按 `files.import` 留下文件的方式放入资料库的 `Imported PDF.pdf` 被打开,以及报告回到 + 离开时所在的页面。 +- [full/](full):存储被填到距 64 MiB 只差 8 KB:主页提示存储已满;移除一个 PDF 时会再 + 确认一次,然后将其移除。 +- [empty/](empty):没有 PDF,浅色和深色:空资料库及其 Open 按钮,然后是拒绝提示。 +- [compare/](compare):Shell 截图与其设计图等高并排(设计图为 1536 x 1024;Shell 的窗口 + 为 990 x 603,因此布局是 README.md 中描述的窄窗口布局)。 + +**App Hub 的 `card-host`**,它不提供任何宿主服务: + +- [missing/](missing):发布的应用包:没有引擎,也没有文件服务。 +- [fixture/](fixture):应用包的一个临时副本,使用替身引擎(`dev-fixture/engine.splash`) + 和取自设计图的示例文档(`dev-fixture/make_fixture.py`),按设计图的 1536 x 1024 显示: + 每个设计过的界面都与其设计图并排(左为设计图,右为应用),先浅色,再深色(文件名以 `d` + 开头;只有 09 有深色设计图),另有损坏、受保护和未保存三种状态。并排图缩小一半,并减为 + 256 色。 + +每一帧都经过检查。 diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/01-home.png b/apps/pdftools/tests/evidence/v2-20261010/compare/01-home.png new file mode 100644 index 00000000..9c33dd5b Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/01-home.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/02-reading.png b/apps/pdftools/tests/evidence/v2-20261010/compare/02-reading.png new file mode 100644 index 00000000..35321251 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/02-reading.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/03-find.png b/apps/pdftools/tests/evidence/v2-20261010/compare/03-find.png new file mode 100644 index 00000000..47f1d5ae Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/03-find.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/04-comment.png b/apps/pdftools/tests/evidence/v2-20261010/compare/04-comment.png new file mode 100644 index 00000000..7d566e6f Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/04-comment.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/05-pages-rotated.png b/apps/pdftools/tests/evidence/v2-20261010/compare/05-pages-rotated.png new file mode 100644 index 00000000..63c4928c Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/05-pages-rotated.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/06-combine.png b/apps/pdftools/tests/evidence/v2-20261010/compare/06-combine.png new file mode 100644 index 00000000..4f224cb2 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/06-combine.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/07-fill-sign.png b/apps/pdftools/tests/evidence/v2-20261010/compare/07-fill-sign.png new file mode 100644 index 00000000..aaa299ba Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/07-fill-sign.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/08-edit.png b/apps/pdftools/tests/evidence/v2-20261010/compare/08-edit.png new file mode 100644 index 00000000..2f0c43bb Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/08-edit.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/d01-home.png b/apps/pdftools/tests/evidence/v2-20261010/compare/d01-home.png new file mode 100644 index 00000000..c0e51675 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/d01-home.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/d02-reading.png b/apps/pdftools/tests/evidence/v2-20261010/compare/d02-reading.png new file mode 100644 index 00000000..dcf09287 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/d02-reading.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/d04-comment.png b/apps/pdftools/tests/evidence/v2-20261010/compare/d04-comment.png new file mode 100644 index 00000000..7036bfd1 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/d04-comment.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/d07-fill-sign.png b/apps/pdftools/tests/evidence/v2-20261010/compare/d07-fill-sign.png new file mode 100644 index 00000000..aefdc6b1 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/d07-fill-sign.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/compare/d08-edit.png b/apps/pdftools/tests/evidence/v2-20261010/compare/d08-edit.png new file mode 100644 index 00000000..961ca4be Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/compare/d08-edit.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/01-home.png b/apps/pdftools/tests/evidence/v2-20261010/dark/01-home.png new file mode 100644 index 00000000..3f763f6d Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/01-home.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/02-reading.png b/apps/pdftools/tests/evidence/v2-20261010/dark/02-reading.png new file mode 100644 index 00000000..dbe5323b Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/02-reading.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/02b-page-2.png b/apps/pdftools/tests/evidence/v2-20261010/dark/02b-page-2.png new file mode 100644 index 00000000..ffb26ce4 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/02b-page-2.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/03-find.png b/apps/pdftools/tests/evidence/v2-20261010/dark/03-find.png new file mode 100644 index 00000000..3679da54 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/03-find.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/04-comment.png b/apps/pdftools/tests/evidence/v2-20261010/dark/04-comment.png new file mode 100644 index 00000000..a28f6622 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/04-comment.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/04b-replied.png b/apps/pdftools/tests/evidence/v2-20261010/dark/04b-replied.png new file mode 100644 index 00000000..bc010586 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/04b-replied.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/05-pages-rotated.png b/apps/pdftools/tests/evidence/v2-20261010/dark/05-pages-rotated.png new file mode 100644 index 00000000..4ac34430 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/05-pages-rotated.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/05b-undone.png b/apps/pdftools/tests/evidence/v2-20261010/dark/05b-undone.png new file mode 100644 index 00000000..684d5c51 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/05b-undone.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/06-combine.png b/apps/pdftools/tests/evidence/v2-20261010/dark/06-combine.png new file mode 100644 index 00000000..c09a1ec1 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/06-combine.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/06a-combine-end.png b/apps/pdftools/tests/evidence/v2-20261010/dark/06a-combine-end.png new file mode 100644 index 00000000..bb6b65d3 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/06a-combine-end.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/06b-combined.png b/apps/pdftools/tests/evidence/v2-20261010/dark/06b-combined.png new file mode 100644 index 00000000..56e8b40d Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/06b-combined.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/07-fill-sign.png b/apps/pdftools/tests/evidence/v2-20261010/dark/07-fill-sign.png new file mode 100644 index 00000000..81d86b34 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/07-fill-sign.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/08-edit.png b/apps/pdftools/tests/evidence/v2-20261010/dark/08-edit.png new file mode 100644 index 00000000..9e5f5dce Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/08-edit.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/08b-edited.png b/apps/pdftools/tests/evidence/v2-20261010/dark/08b-edited.png new file mode 100644 index 00000000..24247285 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/08b-edited.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/09-saved.png b/apps/pdftools/tests/evidence/v2-20261010/dark/09-saved.png new file mode 100644 index 00000000..f3950b16 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/09-saved.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/dark/10-damaged.png b/apps/pdftools/tests/evidence/v2-20261010/dark/10-damaged.png new file mode 100644 index 00000000..6cafc8a5 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/dark/10-damaged.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/empty/de01-empty.png b/apps/pdftools/tests/evidence/v2-20261010/empty/de01-empty.png new file mode 100644 index 00000000..0d7a3ca3 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/empty/de01-empty.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/empty/de02-empty-open.png b/apps/pdftools/tests/evidence/v2-20261010/empty/de02-empty-open.png new file mode 100644 index 00000000..54adbc73 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/empty/de02-empty-open.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/empty/e01-empty.png b/apps/pdftools/tests/evidence/v2-20261010/empty/e01-empty.png new file mode 100644 index 00000000..9915df64 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/empty/e01-empty.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/empty/e02-empty-open.png b/apps/pdftools/tests/evidence/v2-20261010/empty/e02-empty-open.png new file mode 100644 index 00000000..dcc7c9aa Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/empty/e02-empty-open.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/01-home.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/01-home.png new file mode 100644 index 00000000..50b04a80 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/01-home.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/02-reading.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/02-reading.png new file mode 100644 index 00000000..8afc2b12 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/02-reading.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/03-find.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/03-find.png new file mode 100644 index 00000000..e04888c8 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/03-find.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/04-comment.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/04-comment.png new file mode 100644 index 00000000..909bd71e Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/04-comment.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/05-pages.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/05-pages.png new file mode 100644 index 00000000..d37b5356 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/05-pages.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/06-combine.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/06-combine.png new file mode 100644 index 00000000..11d52e92 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/06-combine.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/07-fill-sign.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/07-fill-sign.png new file mode 100644 index 00000000..ac5511bc Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/07-fill-sign.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/08-edit.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/08-edit.png new file mode 100644 index 00000000..86864021 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/08-edit.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/09-outline.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/09-outline.png new file mode 100644 index 00000000..2a2866ee Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/09-outline.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/d01-home.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/d01-home.png new file mode 100644 index 00000000..785ff45d Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/d01-home.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/d02-reading.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/d02-reading.png new file mode 100644 index 00000000..62d9cb8c Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/d02-reading.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/d03-find.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/d03-find.png new file mode 100644 index 00000000..fd2df8c5 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/d03-find.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/d04-comment.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/d04-comment.png new file mode 100644 index 00000000..52b83ebd Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/d04-comment.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/d05-pages.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/d05-pages.png new file mode 100644 index 00000000..aac86fcc Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/d05-pages.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/d06-combine.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/d06-combine.png new file mode 100644 index 00000000..e3135ef6 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/d06-combine.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/d07-fill-sign.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/d07-fill-sign.png new file mode 100644 index 00000000..d96749e6 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/d07-fill-sign.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/d08-edit.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/d08-edit.png new file mode 100644 index 00000000..fe3fc8f2 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/d08-edit.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/d09-outline.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/d09-outline.png new file mode 100644 index 00000000..99ec74d9 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/d09-outline.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/ds1-damaged.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/ds1-damaged.png new file mode 100644 index 00000000..2db2010f Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/ds1-damaged.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/ds2-protected.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/ds2-protected.png new file mode 100644 index 00000000..9f80031e Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/ds2-protected.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/ds3-unsaved.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/ds3-unsaved.png new file mode 100644 index 00000000..a8126376 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/ds3-unsaved.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/s1-damaged.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/s1-damaged.png new file mode 100644 index 00000000..d1dcc7da Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/s1-damaged.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/s2-protected.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/s2-protected.png new file mode 100644 index 00000000..550a2874 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/s2-protected.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/fixture/s3-unsaved.png b/apps/pdftools/tests/evidence/v2-20261010/fixture/s3-unsaved.png new file mode 100644 index 00000000..5f1485a5 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/fixture/s3-unsaved.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/full/f01-home-full.png b/apps/pdftools/tests/evidence/v2-20261010/full/f01-home-full.png new file mode 100644 index 00000000..f8b86d6b Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/full/f01-home-full.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/full/f02-remove-asks.png b/apps/pdftools/tests/evidence/v2-20261010/full/f02-remove-asks.png new file mode 100644 index 00000000..a8e3c0e2 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/full/f02-remove-asks.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/full/f03-removed.png b/apps/pdftools/tests/evidence/v2-20261010/full/f03-removed.png new file mode 100644 index 00000000..e6bd2609 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/full/f03-removed.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/01-home.png b/apps/pdftools/tests/evidence/v2-20261010/light/01-home.png new file mode 100644 index 00000000..1199ce2c Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/01-home.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/01b-open-from-device.png b/apps/pdftools/tests/evidence/v2-20261010/light/01b-open-from-device.png new file mode 100644 index 00000000..a976596d Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/01b-open-from-device.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/02-reading.png b/apps/pdftools/tests/evidence/v2-20261010/light/02-reading.png new file mode 100644 index 00000000..31064b7a Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/02-reading.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/02b-page-2.png b/apps/pdftools/tests/evidence/v2-20261010/light/02b-page-2.png new file mode 100644 index 00000000..aa48eff2 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/02b-page-2.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/03-find.png b/apps/pdftools/tests/evidence/v2-20261010/light/03-find.png new file mode 100644 index 00000000..b74947a4 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/03-find.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/04-comment.png b/apps/pdftools/tests/evidence/v2-20261010/light/04-comment.png new file mode 100644 index 00000000..b3a29f4a Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/04-comment.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/04b-replied.png b/apps/pdftools/tests/evidence/v2-20261010/light/04b-replied.png new file mode 100644 index 00000000..d8bdb2d8 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/04b-replied.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/05-pages-rotated.png b/apps/pdftools/tests/evidence/v2-20261010/light/05-pages-rotated.png new file mode 100644 index 00000000..afca5738 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/05-pages-rotated.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/05b-undone.png b/apps/pdftools/tests/evidence/v2-20261010/light/05b-undone.png new file mode 100644 index 00000000..1d97ee8d Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/05b-undone.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/06-combine.png b/apps/pdftools/tests/evidence/v2-20261010/light/06-combine.png new file mode 100644 index 00000000..29e15acb Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/06-combine.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/06a-combine-end.png b/apps/pdftools/tests/evidence/v2-20261010/light/06a-combine-end.png new file mode 100644 index 00000000..616e117c Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/06a-combine-end.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/06b-combined.png b/apps/pdftools/tests/evidence/v2-20261010/light/06b-combined.png new file mode 100644 index 00000000..4bc3207a Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/06b-combined.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/07-fill-sign.png b/apps/pdftools/tests/evidence/v2-20261010/light/07-fill-sign.png new file mode 100644 index 00000000..d8bbbf9d Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/07-fill-sign.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/08-edit.png b/apps/pdftools/tests/evidence/v2-20261010/light/08-edit.png new file mode 100644 index 00000000..829b4820 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/08-edit.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/08b-edited.png b/apps/pdftools/tests/evidence/v2-20261010/light/08b-edited.png new file mode 100644 index 00000000..fe86470a Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/08b-edited.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/09-saved.png b/apps/pdftools/tests/evidence/v2-20261010/light/09-saved.png new file mode 100644 index 00000000..c31d2d0e Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/09-saved.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/light/10-damaged.png b/apps/pdftools/tests/evidence/v2-20261010/light/10-damaged.png new file mode 100644 index 00000000..f45a4575 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/light/10-damaged.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/missing/m01-missing-home.png b/apps/pdftools/tests/evidence/v2-20261010/missing/m01-missing-home.png new file mode 100644 index 00000000..b045fe3c Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/missing/m01-missing-home.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/missing/m02-missing-document.png b/apps/pdftools/tests/evidence/v2-20261010/missing/m02-missing-document.png new file mode 100644 index 00000000..c1e0e5d2 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/missing/m02-missing-document.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/receipt.json b/apps/pdftools/tests/evidence/v2-20261010/receipt.json new file mode 100644 index 00000000..67305470 --- /dev/null +++ b/apps/pdftools/tests/evidence/v2-20261010/receipt.json @@ -0,0 +1,163 @@ +{ + "result": "pass", + "date": "2026-10-10", + "scope": "PDF Tools v2 end to end: in a hidden OctoSense desktop shell built from this branch (debug, macOS, Apple silicon) with the real pdf engine in the app's own storage, and in App Hub's card-host without host services (the shipped bundle, and a scratch copy with the dev fixture at the designs' size). Every engine call in the shell runs was real.", + "live_engine": true, + "base": "3bd15bad161f44d6110da4d3374ecfb17c615fef (main with #472, the pdf service v2); the sources below are this pull request's", + "bundle_blake3": "a8d0803251468c82b04bcacd11f5c4312a2b95e06a6a72f703272b7bfb1212a7", + "shell": { + "binary": "target/debug/octosense (cargo build --locked -p octosense --no-default-features --features app-hub,craft-engines)", + "binary_sha256": "fc4b8cb67a5a507e2171e3da170d3c38af896d3a87ae0996997a360a2a66be6a", + "app_hub_revision": "80a64b4557aaba71f7c826c5dc69721c21c0f0fc", + "runtime": "the root's .sources: makepad d653bc44 with runtime-patches tree 3f4fd959 (35 stacked patches), Octoscript-Makepad 6cf2d90a, Octoscript 2e37d9e6", + "launch": "a Terminal.app tab (osascript) under the machine's shell-run lock, one OctoSense at a time: OCTOSENSE_HOME= OCTOS_APP_CORE_DIR=/octos-home/.octos MAKEPAD_HOME=/makepad MAKEPAD_HIDE_WINDOWS=1 MAKEPAD_REMOTE=127.0.0.1:8915 MAKEPAD_WM_TEST_APP=pdftools; ended with /quit", + "script_budget": "the runtime's 64 ms (load average 10 to 30)", + "window": "the shell's default app window: 990 x 603 points in a 1400 x 898 window (2x)" + }, + "card_host": { + "binary_sha256": "760bb822435d578d69b89c39d7f25a8f8dafc1851a1911687efc0f483df895ec", + "app_hub_revision": "8347a489141c0e24cab564883db7c4592935a8b2 (an ancestor of the shell's pin; the pin adds only the services' isolate-closed hook, catalog data and docs)", + "size": "1536 x 1024 (--size), MAKEPAD_WIDGET_STYLE=macos and macos-dark", + "script_budget": "missing: the runtime's 64 ms; fixture: MAKEPAD_SPLASH_BUDGET_MS=1000 (script_budgets)" + }, + "source_sha256": { + "apps/pdftools/bundle/main.splash": "d6db2eca74df93236a11f55d38b824ad71deddd56eb307631c3a512a22780d8a", + "apps/pdftools/bundle/manifest.json": "60430b8ce6b85e64431e35846ace062c13c16fe7abf3699c43d65ad451b181fd", + "apps/pdftools/tests/ui.py": "3039646310fe807dbec7a824193d80be04f8373c83182493b9cecdf4e8878bd5", + "apps/pdftools/dev-fixture/engine.splash": "33c7727d1cdf72bc473f4da1eec7157366e156e891ac6da2eed51a127fd762d8", + "apps/pdftools/dev-fixture/make_fixture.py": "92541102e942d7af8ff45fff33a8885a1a8ef6c47efa76d812e89b5218d53614", + "apps/pdf/host-service/examples/pdftools_fixture.rs": "f44ad5a202da98b4a07468e9dccdff59f7e2b333edec6e879245a3b7491307fc", + "apps/pdftools/design/SERVICE.md": "1354e62ef1bb96f3fc6a1c335e3757146661949b6c3ff4b85ad0b68fa8984ca3" + }, + "commands": [ + "python3 tools/setup.py", + "cargo test --locked -p octosense-shell --lib pdftools_model", + "cargo build --locked -p octosense --no-default-features --features app-hub,craft-engines", + "python3 apps/pdftools/tests/ui.py --shell target/debug/octosense --lock /octosense-shell.lock --work --output /shell-run", + "python3 apps/pdftools/tests/ui.py --card-host /target/release/card-host --only missing --work --output /card-host-run", + "python3 apps/pdftools/tests/ui.py --card-host /target/release/card-host --only fixture --budget-ms 1000 --work --output /fixture-run", + "the shell runs again with --budget-ms 1000 while other builds loaded the machine (passed; frames not kept)", + "hub stamp ; hub check --system-app", + "frames: the app's window cut from each shell grab by its sidecar's area (one pixel per point); card-host grabs halved; side-by-side sheets halved (card-host) or at the shell's height, reduced to 256 colours (PIL)" + ], + "runs": { + "shell": "the samples in the shell's default window, light, then dark through the shell's style menu (OctoSense · Dark), the dark pass on what the light pass left", + "restart": "the same home again, with Imported PDF.pdf (pdftools_fixture --import-sample) placed in the library between the runs", + "full": "a fresh home whose storage was filled to 8 KB short of 64 MiB", + "empty": "a fresh home with no PDFs, light then dark", + "missing": "card-host, the shipped bundle, the samples, MAKEPAD_WIDGET_STYLE=macos; no host services", + "fixture": "card-host, a scratch copy of the bundle with dev-fixture/engine.splash in place of engine(), make_fixture.py's documents, 1536 x 1024, macos then macos-dark" + }, + "checks": [ + "Home lists the samples with the engine's first pages ('2 pages · 6 KB', 'Not opened yet'), the damaged one as 'Couldn't open this PDF', and '37 KB of 64 MB used'", + "Open a PDF from this device: files.import is refused with 'foreground_required: Return to this app before choosing or sharing a file' (a hidden window never has focus), and the app shows it in a card", + "The report opens at 100% in the 990-point window with its thumbnails; page 2 from its thumbnail reads '2 / 4'", + "Find 'revenue': '9 matches on 4 pages', results by page with the match in bold, '1 of 9', the match highlighted on page 1; the page refits to 95% while the results panel is open", + "Comment: 'Comment as' is asked once and 'Maya Chen' kept; the pill's next goes to page 2; a tap on the Summary's first line adds a highlight from pdf.lines' box (the count goes 0 to 1, and 1 to 2 in the dark pass); the thread shows MC, Maya Chen; a reply on an empty highlight becomes its text; the right panel is 320 points and the page fits at 89%", + "Pages: page 2 chosen and turned right (its tile turns landscape), then Undo turns it back", + "Combine: Board minutes added with the range '1' ('(1 page)', 'One PDF of 5 pages'); Combine writes 'Quarterly report combined.pdf' ('… combined 2' in the dark pass) and opens it ('Combined into “Quarterly report combined”, 5 pages.')", + "Fill & Sign: the lease has no form fields; Add initials places 'MC' beside clause 3 (kept for the dark pass); Edited", + "Edit: clause 1 in the editor (Paragraph 4, 'Times-Roman · 11 pt'), ' (amended)' typed and applied; the page re-renders with the edit; Save says Saved (a plain save works from the hidden shell)", + "Damaged scan reads 'Couldn't open this PDF' with the engine's own message", + "The shell's style switch to dark re-runs the script and keeps the tabs, the name, the initials and the edits (#440)", + "After a restart: Imported PDF opens (3 pages); the report reopens at page 2; the saved lease's cover shows its initials", + "With 8 KB left: 'PDF Tools' storage is full' on Home (no cover can be written); removing a PDF asks 'Remove it from PDF Tools' storage?', then 'Removed “Apartment lease.pdf” from PDF Tools.'", + "No PDFs: 'No PDFs here yet' with Open a PDF from this device, light and dark, then the refusal", + "card-host without services: 'This device has no PDF engine' and 'Not available here'", + "card-host with the dev fixture: the nine designed screens, light and dark, beside their designs; the damaged, protected and unsaved states", + "No [E], on_render, callback or script-budget error lines in any shell or card-host log" + ], + "not_verified": [ + "the host's file dialog and what follows a real import: a hidden window cannot open it (the files service refuses first)", + "form fields with the real engine: no sample PDF has a form (pdf.fields, pdf.fill and the field overlays ran only on the dev fixture)", + "with the real engine, only highlight and reply/edit of the comment operations, only rotate and Undo of the page operations, and no Redo, outline (the samples have none), password-protected PDF, eight-open cap or close-with-unsaved-changes dialog; the dev fixture covers their screens", + "window sizes other than the shell's default 990 x 603 and card-host's 1536 x 1024", + "behaviour on a heavily loaded machine: in earlier rounds (before library.json moved off the tap path), at load averages of 60 to 150, the tap that opens a PDF overran the 64 ms budget twice and was lost; the runs kept here ran at load 10 to 30, and the same runs passed with MAKEPAD_SPLASH_BUDGET_MS=1000 at load 45 to 117", + "Linux, Windows, phones" + ], + "script_budgets": "shell runs and missing: the runtime's 64 ms (load average 10 to 30). fixture: MAKEPAD_SPLASH_BUDGET_MS=1000, because its stand-in engine answers in script (it reads and parses the fixture's JSON): at 64 ms its journey passed, but one stand-in answer overran in the dark pass. The real engine answers natively.", + "grabs": [ + "light/01-home.png", + "light/01b-open-from-device.png", + "light/02-reading.png", + "light/02b-page-2.png", + "light/03-find.png", + "light/04-comment.png", + "light/04b-replied.png", + "light/05-pages-rotated.png", + "light/05b-undone.png", + "light/06-combine.png", + "light/06a-combine-end.png", + "light/06b-combined.png", + "light/07-fill-sign.png", + "light/08-edit.png", + "light/08b-edited.png", + "light/09-saved.png", + "light/10-damaged.png", + "dark/01-home.png", + "dark/02-reading.png", + "dark/02b-page-2.png", + "dark/03-find.png", + "dark/04-comment.png", + "dark/04b-replied.png", + "dark/05-pages-rotated.png", + "dark/05b-undone.png", + "dark/06-combine.png", + "dark/06a-combine-end.png", + "dark/06b-combined.png", + "dark/07-fill-sign.png", + "dark/08-edit.png", + "dark/08b-edited.png", + "dark/09-saved.png", + "dark/10-damaged.png", + "restart/r01-home.png", + "restart/r02-imported.png", + "restart/r03-place-kept.png", + "full/f01-home-full.png", + "full/f02-remove-asks.png", + "full/f03-removed.png", + "empty/de01-empty.png", + "empty/de02-empty-open.png", + "empty/e01-empty.png", + "empty/e02-empty-open.png", + "missing/m01-missing-home.png", + "missing/m02-missing-document.png", + "fixture/01-home.png", + "fixture/02-reading.png", + "fixture/03-find.png", + "fixture/04-comment.png", + "fixture/05-pages.png", + "fixture/06-combine.png", + "fixture/07-fill-sign.png", + "fixture/08-edit.png", + "fixture/09-outline.png", + "fixture/s1-damaged.png", + "fixture/s2-protected.png", + "fixture/s3-unsaved.png", + "fixture/d01-home.png", + "fixture/d02-reading.png", + "fixture/d03-find.png", + "fixture/d04-comment.png", + "fixture/d05-pages.png", + "fixture/d06-combine.png", + "fixture/d07-fill-sign.png", + "fixture/d08-edit.png", + "fixture/d09-outline.png", + "fixture/ds1-damaged.png", + "fixture/ds2-protected.png", + "fixture/ds3-unsaved.png", + "compare/01-home.png", + "compare/02-reading.png", + "compare/03-find.png", + "compare/04-comment.png", + "compare/05-pages-rotated.png", + "compare/06-combine.png", + "compare/07-fill-sign.png", + "compare/08-edit.png", + "compare/d02-reading.png", + "compare/d01-home.png", + "compare/d04-comment.png", + "compare/d07-fill-sign.png", + "compare/d08-edit.png" + ] +} diff --git a/apps/pdftools/tests/evidence/v2-20261010/restart/r01-home.png b/apps/pdftools/tests/evidence/v2-20261010/restart/r01-home.png new file mode 100644 index 00000000..7ec791b6 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/restart/r01-home.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/restart/r02-imported.png b/apps/pdftools/tests/evidence/v2-20261010/restart/r02-imported.png new file mode 100644 index 00000000..4673fbe8 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/restart/r02-imported.png differ diff --git a/apps/pdftools/tests/evidence/v2-20261010/restart/r03-place-kept.png b/apps/pdftools/tests/evidence/v2-20261010/restart/r03-place-kept.png new file mode 100644 index 00000000..bf260bf0 Binary files /dev/null and b/apps/pdftools/tests/evidence/v2-20261010/restart/r03-place-kept.png differ diff --git a/apps/pdftools/tests/ui.py b/apps/pdftools/tests/ui.py index 375fb17b..378661ca 100644 --- a/apps/pdftools/tests/ui.py +++ b/apps/pdftools/tests/ui.py @@ -1,30 +1,43 @@ #!/usr/bin/env python3 -"""PDF Tools end to end: the real pdf engine, in a hidden OctoSense desktop shell. - -The shell is the one this checkout builds (`cargo build --locked -p octosense`). -For each run the driver makes a fresh OctoSense home, writes sample PDFs into -PDF Tools' storage there (octosense-pdf-service's `pdftools_fixture` example), -starts the shell hidden from a Terminal.app tab (never as a child of this -process), opens the app with MAKEPAD_WM_TEST_APP and walks it over Makepad's -remote bridge, saving original /g grabs. Every engine call is real. - - shell the samples in light, then dark through the shell's own style menu - restart the same home again, with a PDF placed in the library between the - runs as files.import leaves one (`Imported PDF.pdf`): the path an - opened file takes, without the host's dialog +"""PDF Tools v2 end to end, in a hidden OctoSense desktop shell and in App Hub's card-host. + +Shell runs use the shell this checkout builds, with the real pdf engine +(`cargo build --locked -p octosense --no-default-features --features +app-hub,craft-engines`). For each run the driver makes a fresh OctoSense home, +writes sample PDFs into PDF Tools' storage there (octosense-pdf-service's +`pdftools_fixture` example), starts the shell hidden from a Terminal.app tab +(never as a child of this process), opens the app with MAKEPAD_WM_TEST_APP and +walks it over Makepad's remote bridge, saving original /g grabs. Every engine +call is real. + + shell every mode on the samples in the shell's own window, light, then + dark through the shell's own style menu (the dark pass works on + what the light pass left): Home, "Open a PDF from this device" + refused, reading and thumbnails, find, a highlight and its reply, + a rotation and its undo, combine with a page range, initials + placed by Fill & Sign, a text edit, save, a damaged file + restart the same home again: the library, the titles, the page a PDF was + left at, and a PDF placed as files.import leaves one + (`Imported PDF.pdf`) full storage with no room left: the engine's refusal, then a removal - empty no PDFs: the empty library and "Open a PDF from this device". A + empty no PDFs: the empty library and "Open a PDF from this device" (a hidden window never has focus, so the files service refuses the - dialog before it opens, and the app shows that refusal - missing App Hub's card-host, which serves no host services: no engine and - no files service + dialog and the app shows that refusal) + +card-host runs serve no host services: + + missing the shipped bundle: no engine and no files service + fixture a scratch copy of the bundle with dev-fixture/engine.splash in + place of engine(), and make_fixture.py's sample documents (the + approved designs' sample text): every designed screen at the + designs' 1536 x 1024, light and dark, each grab put beside its + design in compare/, and the damaged, protected and unsaved states Shell runs hold a lock folder, so one hidden shell runs on this machine at a time, and refuse to start while another OctoSense runs. - cargo build --locked -p octosense python3 apps/pdftools/tests/ui.py --shell target/debug/octosense --lock --output target/pdftools-ui - python3 apps/pdftools/tests/ui.py --card-host /target/release/card-host --only missing --output target/pdftools-ui + python3 apps/pdftools/tests/ui.py --card-host /target/release/card-host --output target/pdftools-ui """ import argparse import json @@ -35,6 +48,7 @@ import shutil import socket import subprocess +import sys import tempfile import time from urllib.error import HTTPError, URLError @@ -42,12 +56,14 @@ from urllib.request import urlopen ROOT = Path(__file__).resolve().parents[3] -BUNDLE = ROOT / "apps/pdftools/bundle" +APP_DIR = ROOT / "apps/pdftools" +BUNDLE = APP_DIR / "bundle" +FIXTURE = APP_DIR / "dev-fixture" +DESIGNS = APP_DIR / "design/source" APP = "os.pdftools" LIBRARY = "accounts/device/library" # The storage PDF Tools gets: App Hub's system ceiling (no storage.max_bytes). QUOTA = 64 * 1024 * 1024 -ICON_UP = "" def fixture(*args): @@ -74,6 +90,10 @@ class Driver: """The remote bridge, and the app inside whatever host answers it.""" scale = 1.0 + # MAKEPAD_SPLASH_BUDGET_MS for the hosts these runs start, or None for the + # runtime's 64 ms: the budget is wall-clock time, so a loaded machine can + # overrun it in any handler (--budget-ms). + budget_ms = None def __init__(self, port): self.port = port @@ -121,21 +141,24 @@ def ready(self, timeout=90.0): time.sleep(0.5) def area(self, timeout=60.0): - """The app's surface: its window in a shell, the whole window in card-host.""" + """The app's surface: its window in a shell, its Splash view in card-host.""" deadline = time.monotonic() + timeout while True: try: snap = self.remote("snap")["s"] - break + views = [w["r"] for w in snap if w["ty"] == "MpModuleView"] + if views: + return max(views, key=lambda r: r[2] * r[3]) + # card-host: the app's Splash view (older hosts reported a Window). + roots = [w["r"] for w in snap if w["ty"] in ("Splash", "Window")] + if roots: + return max(roots, key=lambda r: r[2] * r[3]) except (OSError, URLError, AssertionError): - # A busy UI thread (an engine call, a first load) answers late. - if time.monotonic() > deadline: - raise - time.sleep(0.5) - views = [w["r"] for w in snap if w["ty"] == "MpModuleView"] - if views: - return max(views, key=lambda r: r[2] * r[3]) - return next(w["r"] for w in snap if w["ty"] == "Window") + pass + # A busy UI thread (an engine call, a first load) answers late. + if time.monotonic() > deadline: + raise AssertionError("the app's surface never appeared in /snap") + time.sleep(0.5) def widgets(self): return [w for w in self.remote("snap")["s"] if w["ty"] != "Splash" and w["r"][2] > 0 and w["r"][3] > 0] @@ -163,13 +186,23 @@ def gone(self, text, timeout=30.0): time.sleep(0.2) raise AssertionError(f"{text!r} is still on screen") + def pictures(self): + """The app's own pictures (the engine's renders), not the shell's + wallpaper or dock: inside the app's surface and narrower than it.""" + ax, ay, aw, ah = self.area() + return [w for w in self.widgets() if w["ty"] == "Image" and w["r"][2] < aw - 1 + and ax - 1 <= w["r"][0] <= ax + aw and ay - 1 <= w["r"][1] <= ay + ah] + def images(self, count, timeout=60.0): """Wait for at least `count` pictures: the engine's page renders.""" deadline = time.monotonic() + timeout while time.monotonic() < deadline: - if sum(1 for w in self.widgets() if w["ty"] == "Image") >= count: - time.sleep(0.4) - return + try: + if len(self.pictures()) >= count: + time.sleep(0.4) + return + except (OSError, ValueError, AssertionError): + pass time.sleep(0.3) raise AssertionError(f"fewer than {count} page pictures after {timeout} s") @@ -195,11 +228,8 @@ def reveal(self, text, kind="Label", tries=12, exact=True): not drawn at all, then back up.""" ax, ay, aw, ah = self.area() top = ay + 170 - # The screen's bottom bar, when it has one: its buttons sit in the - # last 90 points of the app. - bar = [w["r"][1] for w in self.widgets() - if w["ty"] in ("Button", "ButtonFlat") and w["r"][1] > ay + ah - 90] - floor = min(bar) - 4 if bar else ay + ah - 4 + # PDF Tools' status line takes the last 44 points of the app. + floor = ay + ah - 48 step, last = 240, None for attempt in range(tries): try: @@ -211,8 +241,6 @@ def reveal(self, text, kind="Label", tries=12, exact=True): last = None continue x, y, w, h = found["r"] - if found["ty"] in ("Button", "ButtonFlat") and y > ay + ah - 90: - return # the bottom bar itself # A row the pane cuts off is reported only as tall as what shows. if top <= y and y + h <= floor and h >= 12: return @@ -277,6 +305,8 @@ def __init__(self, binary, home, port, log): "MAKEPAD_REMOTE": f"127.0.0.1:{port}", "MAKEPAD_WM_TEST_APP": "pdftools", } + if Driver.budget_ms: + env["MAKEPAD_SPLASH_BUDGET_MS"] = str(Driver.budget_ms) command = "cd {} && env {} {} > {} 2>&1; exit".format( shlex.quote(str(ROOT)), " ".join(f"{k}={shlex.quote(str(v))}" for k, v in env.items()), shlex.quote(str(binary)), shlex.quote(str(log))) @@ -340,13 +370,15 @@ def quit(self): class CardHost(Driver): - def __init__(self, binary, data, port, style, log): + def __init__(self, binary, bundle, data, port, style, log, size="1536x1024"): super().__init__(port) env = os.environ.copy() env.update(MAKEPAD_HIDE_WINDOWS="1", MAKEPAD_REMOTE=f"127.0.0.1:{port}", MAKEPAD_WIDGET_STYLE=style) + if Driver.budget_ms: + env["MAKEPAD_SPLASH_BUDGET_MS"] = str(Driver.budget_ms) self.log = log self.handle = log.open("w") - self.process = subprocess.Popen([str(binary), "--bundle", str(BUNDLE), "--system", "--app-data", str(data)], + self.process = subprocess.Popen([str(binary), "--bundle", str(bundle), "--system", "--app-data", str(data), "--size", size], cwd=ROOT, env=env, stdout=self.handle, stderr=self.handle) self.ready() @@ -365,204 +397,461 @@ def quit(self): self.handle.close() -# ---------------------------------------------------------------- journeys +# ---------------------------------------------------------------- helpers + +def top_page(app): + """The first page image on the canvas: [x, y, w, h] in points (what + shows of it: a scroll view reports only its visible part).""" + imgs = [w for w in app.pictures() if w["r"][2] > 300] + assert imgs, "no page on the canvas" + return min(imgs, key=lambda w: w["r"][1])["r"] + + +def zoom(app): + """The pill's zoom, as a scale: '90%' is 0.9.""" + for w in app.widgets(): + found = re.fullmatch(r"(\d+)%", w.get("t") or "") + if found: + return int(found[1]) / 100 + raise AssertionError("no zoom on the pill") + + +def click_page(app, px, py, page_width=None): + """Click at a point of the top page, given in the page's points. The run + of pages starts at the top of the canvas, so its top left shows.""" + x, y, w, h = top_page(app) + k = zoom(app) + app.click(x + px * k, y + py * k) + + +def pill_next(app): + """The pill's next page: the first icon button right of 'N / M'.""" + label = next(w for w in app.widgets() if re.fullmatch(r"\d+ / \d+", w.get("t") or "")) + row, (x, y, w, h) = icon_buttons_near(app, label["t"], "Label") + bx, by, bw, bh = [r for r in row if r[0] >= x + w - 1][0] + app.click(bx + bw / 2, by + bh / 2) + + +def range_fields(app): + """Combine's page-range fields, top to bottom (not the search field).""" + ax, ay, aw, ah = app.area() + return sorted((w for w in app.widgets() if w["ty"] == "TextInput" and w["r"][2] < 200 and w["r"][1] > ay + 110), + key=lambda w: w["r"][1]) + -def journey(app, out, dark): - """Every screen on the samples, with the real engine.""" +def press_combine(app): + """Combine's own button, at the end of its card (not the mode's tab).""" + app.scroll(600) + buttons = [w for w in app.widgets() if w["ty"] in ("Button", "ButtonFlat") and w.get("t") == "Combine"] + x, y, w, h = max(buttons, key=lambda b: b["r"][1])["r"] + app.click(x + w / 2, y + h / 2) + + +def icon_buttons_near(app, text, kind="Button"): + """The text-less (icon) buttons on the same row as a labelled control, left to right.""" + x, y, w, h = app.find(text, kind)["r"] + row = [b["r"] for b in app.widgets() if b["ty"] in ("Button", "ButtonFlat") and not b.get("t") + and abs((b["r"][1] + b["r"][3] / 2) - (y + h / 2)) < 12] + return sorted(row, key=lambda r: r[0]), (x, y, w, h) + + +def click_icon_left_of(app, text, nth=1, kind="Button"): + """Click the nth icon button left of a labelled control (1 = the nearest).""" + row, (x, y, w, h) = icon_buttons_near(app, text, kind) + left = [r for r in row if r[0] + r[2] <= x + 1] + bx, by, bw, bh = left[-nth] + app.click(bx + bw / 2, by + bh / 2) + + +def tap_tile(app, number, rise=70): + """Pages mode: tap a page tile above its number.""" + app.tap_above(str(number), rise, below=150) + + +# The fixture run's storage, for the boxes make_fixture.py recorded. +FIXTURE_JAIL = None + + +def fixture_paragraph(ident, page, n): + """The centre of paragraph n of a fixture page, in the page's points.""" + lines = json.loads((FIXTURE_JAIL / "dev/lines" / ident / f"{page}.json").read_text()) + x, y, w, h = lines["paragraphs"][n - 1]["box"] + return x + w / 2, y + h / 2 + + +def name_once(app): + """Comment as: the name comments carry, asked once and kept.""" + try: + app.find("Your name", "TextInput", timeout=3) + except AssertionError: + return + app.tap("Your name", "TextInput") + app.type("Maya Chen") + app.tap("Save", "Button", below=150) + app.gone("Comment as") + + +# ---------------------------------------------------------------- fixture journey (card-host) + +def fixture_journey(app, out, dark): + """The approved designs' states on the dev fixture, one grab per screen.""" p = "d" if dark else "" - app.see("4 pages · 9 KB") - app.scroll(-3000) + app.see("Q3 2026 Board Report.pdf") + app.images(5) + # 01-home shows Home with the report open in a tab. + app.press("Q3 2026 Board Report.pdf") app.images(3) - app.see("PDFs up to 64 MB") - app.grab(out, f"{p}01-library") - app.tap("Open a PDF from this device", "Label") - app.see("Couldn't open a PDF from this device") - app.see("Bring PDF Tools to the front, then choose the file again.") - app.see("foreground_required", exact=False) - app.grab(out, f"{p}02-open-from-device") - app.tap("OK") - app.gone("Couldn't open a PDF from this device") - - app.press("Field guide") - app.see("7 pages · 16 KB") - app.see("Split into files", kind=None) + app.tap("Open", "Button") + app.scroll(-3000) app.images(5) - app.grab(out, f"{p}03-pages") - app.tap_above("2", 120, below=300) - app.see("Page 2 of 7") + app.grab(out, f"{p}01-home") + + # The other screens show five PDFs open: open them in the designs' + # order, then come back to the first. + for name in ("Riverside Lease 2026.pdf", "Field Guide to Garden Birds.pdf", "Invoice INV-2041.pdf", "Site Survey Photos.pdf"): + app.press(name) + app.images(1) + app.tap("Open", "Button") + app.tap("Q3 2026 Board Report.pdf", "Label") + app.images(3) + app.tap("3", "Label", below=150) + app.see("3 / 24") + app.images(4) + app.grab(out, f"{p}02-reading") + + app.tap("Search in document", "TextInput") + app.type("revenue") + app.key("Return") + app.see("matches on 7 pages", exact=False) + click_icon_left_of(app, "Done") # the down arrow: the second match + app.see("2 of 11") + app.images(2) + app.grab(out, f"{p}03-find") + app.tap("Done") + + app.tap("Comment", "Button") + name_once(app) + app.tap("Outline", "Label") + app.tap("Revenue", "Label") + app.tap("Outline", "Label") + app.see("3 / 24") app.images(1) - app.grab(out, f"{p}04-page") - app.tap("Next") - app.see("Page 3 of 7") - app.grab(out, f"{p}05-page-next") - app.tap("‹ Pages") - - app.tap("Text") - app.see("PAGE 1") - app.grab(out, f"{p}06-text") - app.tap("Find in the text", "TextInput") - app.type("wren") - app.see("2 pages contain “wren”.") - app.grab(out, f"{p}07-text-find") - app.tap("Info") - app.see("A4, 210 × 297 mm") - app.grab(out, f"{p}08-info") - app.see("Helvetica, Helvetica-Bold, Times-Italic, Times-Roman") - app.see("Not protected") - app.grab(out, f"{p}08b-info-end") - - app.tap("Pages") - app.tap("Split into files") - app.see("Makes 4 files") - if dark: - # The light run left Field guide-part2: this split replaces it. - app.see("Replaces 1 file of the same name in your library.") - app.grab(out, f"{p}09-split-every") - app.scroll(-3000) - app.tap("Where I choose") - app.see("Tap a page to split there", kind=None) - app.tap_above("2", 120, below=300) - app.see("File 2 starts here") - app.see("Split into 2 files", kind=None) - app.grab(out, f"{p}10-split-where") + app.grab(out, f"{p}04-comment") + + app.tap("Pages", "Button") + app.images(12) + tap_tile(app, 5) + tap_tile(app, 6) + app.see("2 selected") + app.grab(out, f"{p}05-pages") + tap_tile(app, 5) + tap_tile(app, 6) + + app.tap("Combine", "Button") + app.tap("Add files", "Label") + app.tap("Appendix A - Figures.pdf", "Label") + app.tap("Add files", "Label") + app.tap("Cover Letter.pdf", "Label") + ranges = sorted((w for w in app.widgets() if w["ty"] == "TextInput" and w["r"][2] < 200), key=lambda w: w["r"][1]) + x, y, w, h = ranges[1]["r"] + app.click(x + w / 2, y + h / 2) + app.type("1-4, 9") + app.see("(5 pages)") + app.grab(out, f"{p}06-combine") + + app.tap("Edit", "Button") + app.tap("Outline", "Label") + app.tap("Summary", "Label") + app.tap("Outline", "Label") + app.see("2 / 24") + app.images(1) + click_page(app, *fixture_paragraph("board", 2, 2), 612) # "The board met on 21 September 2026…" + app.see("Paragraph 2") + app.grab(out, f"{p}08-edit") + app.tap("Cancel", "Button") + + app.tap("Open", "Button") + app.press("Riverside Lease 2026.pdf") + app.tap("Fill & Sign", "Button") + app.see("This form has 7 fields", exact=False) + app.images(1) + app.grab(out, f"{p}07-fill-sign") + + app.tap("Q3 2026 Board Report.pdf", "Label") + app.tap("Read", "Button") + app.tap("Outline", "Label") + app.tap("Revenue", "Label") + app.see("3 / 24") + app.images(1) + app.grab(out, f"{p}09-outline") + + +def fixture_states(app, out, dark): + """The states the brief lists, on the fixture: damaged, protected, unsaved.""" + p = "d" if dark else "" + app.tap("Open", "Button") + app.press("Damaged scan.pdf") + app.see("Couldn't open this PDF") + app.grab(out, f"{p}s1-damaged") + app.tap("Close this tab", "Button") + app.tap("Open", "Button") + app.press("Payroll 2026.pdf") + app.see("This PDF is protected with a password") + app.grab(out, f"{p}s2-protected") + app.tap("Close this tab", "Button") + # a change, then closing its tab asks first + app.tap("Riverside Lease 2026.pdf", "Label") + app.tap("Monthly rent *", "Label") + app.tap("Type the value", "TextInput") + app.type("1850") + app.tap("Apply", "Button") + app.see("Edited") + tab = app.find("Riverside Lease 2026.pdf •", "Label")["r"] + app.click(tab[0] + tab[2] + 22, tab[1] + tab[3] / 2) # its close cross + app.see("Save the changes to “Riverside Lease 2026.pdf”?") + app.grab(out, f"{p}s3-unsaved") + app.tap("Cancel", "Button") + + +# ---------------------------------------------------------------- shell journey (the real engine) + +def shell_journey(app, out, dark): + """Every mode on the samples, with the real pdf engine, in the shell's + own window size. The dark pass runs on what the light pass left: the + same tabs, the comment name and initials it kept, its edits.""" + p = "d" if dark else "" if dark: - app.scroll(-3000) - app.tap("Every few pages") - app.tap("Split into 4 files") - app.see("Split into 4 files") - app.see("Page 7") - else: - app.tap("Split into 2 files") - app.see("Split into 2 files") - app.see("Pages 2–7") + app.tap("Open", "Button") # Home: the light pass ended on a tab + app.see("Quarterly report.pdf") + app.images(3) app.scroll(-3000) - app.grab(out, f"{p}11-split-done") + app.grab(out, f"{p}01-home") + if not dark: + app.tap("Open a PDF from this device", "Button") + app.find("Couldn't open a PDF from this device", "Label") + app.find("Bring PDF Tools to the front, then choose the file again.", "Label") + app.grab(out, "01b-open-from-device") + app.tap("OK", "Button") + app.gone("Couldn't open a PDF from this device") + + app.press("Quarterly report.pdf") + app.images(3) + app.find(" / 4", "Label", exact=False, timeout=60) + app.tap("1", "Label", below=150) + app.find("1 / 4", "Label") + app.images(3) + app.grab(out, f"{p}02-reading") + app.tap("2", "Label", below=150) + app.find("2 / 4", "Label") + app.images(2) + app.grab(out, f"{p}02b-page-2") + + app.tap("Search in document", "TextInput") + app.type("revenue") + app.key("Return") + app.find("9 matches on 4 pages", "Label", timeout=60) + app.images(1) + app.grab(out, f"{p}03-find") app.tap("Done") - app.see("Field guide-part2") - first, second, name = ("Apartment lease", "Board minutes", "Merged 2") if dark else ("Quarterly report", "Board minutes", "Merged") - app.tap("Merge PDFs") - if not dark: - app.press("Damaged scan") - app.see("“Damaged scan” can't be opened, so it can't be merged.") - app.gone("“Damaged scan” can't be opened, so it can't be merged.", timeout=20) - app.press(first) - app.press(second) - app.see("Next: order 2 files", kind=None) - app.grab(out, f"{p}12-merge-choose") - app.tap("Next: order 2 files") - app.see(f"One PDF of {'4' if dark else '6'} pages, saved in your library.") - app.grab(out, f"{p}13-merge-order") - app.scroll(-3000) - board = app.find("Board minutes", "Label")["r"] - up = [w for w in app.widgets() if w.get("t") == ICON_UP and abs(w["r"][1] - board[1]) < 30] - assert up, "no move-up button on the Board minutes row" - x, y, w, h = up[0]["r"] + app.tap("Comment", "Button") + name_once(app) + app.find("1 / 4", "Label") # find went to the first match + pill_next(app) + app.find("2 / 4", "Label") + app.images(1) + before = comment_count(app) + click_page(app, 120, 128) # the Summary's first line + wait_until(lambda: comment_count(app) == before + 1, "the highlight never joined the comments") + app.find("Maya Chen", "Label") + app.grab(out, f"{p}04-comment") + app.tap("Reply...", "TextInput") + app.type("Checked against the ledger.") + app.tap("Post", "Button") + app.find("Checked against the ledger.", "Label") + app.grab(out, f"{p}04b-replied") + + app.tap("Pages", "Button") + app.images(3) + upright = app.find("2", "Label", below=150)["r"][1] + try: + app.find("1 selected", "Label", timeout=2) # the dark pass: still chosen from the light one + except AssertionError: + tap_tile(app, 2) + app.find("1 selected", "Label") + app.tap("Rotate right", "Button") + # A page turned on its side draws a wide, low tile: its number rises. + wait_until(lambda: app.find("2", "Label", below=150)["r"][1] < upright - 20, "page 2 never turned") + app.images(3) + app.grab(out, f"{p}05-pages-rotated") + click_icon_left_of(app, "Save", nth=2) # Undo: two icon buttons left of Save + wait_until(lambda: abs(app.find("2", "Label", below=150)["r"][1] - upright) < 2, "Undo never turned page 2 back") + app.grab(out, f"{p}05b-undone") + + app.tap("Combine", "Button") + app.tap("Add files", "Label") + app.tap("Board minutes.pdf", "Label") + x, y, w, h = range_fields(app)[1]["r"] app.click(x + w / 2, y + h / 2) - app.tap("Merge 2 PDFs") - app.see("Merged into one PDF") - app.see(name) + app.type("1") + app.find("(1 page)", "Label") + app.grab(out, f"{p}06-combine") + app.scroll(600) # the card's end: the total, the name, Combine + app.find("One PDF of 5 pages", "Label") + app.grab(out, f"{p}06a-combine-end") + press_combine(app) + app.find("Combined into", "Label", exact=False, timeout=60) app.images(1) - app.grab(out, f"{p}14-merge-done") - app.tap("Open") - app.see(name) - app.see("Split into files", kind=None) - app.images(4) - app.grab(out, f"{p}15-merged-pages") - app.tap("‹ Library") - - part = "Field guide-part1" - app.press(part) - app.tap("Remove") - app.see("Remove this PDF from PDF Tools?") - app.see("Keep it", kind=None) - app.grab(out, f"{p}16-remove-ask") - app.tap("Remove") - app.see(f"Removed “{part}” from PDF Tools.") - app.gone(part) - app.grab(out, f"{p}17-removed") - # The note goes by itself after 8 s; a tap could land on the row under it. - app.gone(f"Removed “{part}” from PDF Tools.", timeout=20) - - app.press("Damaged scan") - app.see("Couldn't open this PDF") - app.grab(out, f"{p}18-damaged") - app.tap("‹ Library") + app.grab(out, f"{p}06b-combined") + + app.tap("Open", "Button") + app.press("Apartment lease.pdf") + app.images(1) + app.tap("Fill & Sign", "Button") + app.find("This PDF has no form fields", "Label", exact=False) + app.find("Saved", "Label") + app.tap("Add initials", "Label") + try: + app.find("Initials", "TextInput", timeout=3) # empty: the light pass + app.tap("Initials", "TextInput") + app.type("MC") + except AssertionError: + app.find("MC", "TextInput") # kept from the light pass + click_page(app, 360, 330) # beside clause 3 + app.find("Edited", "Label") + app.images(1) + app.grab(out, f"{p}07-fill-sign") + + app.tap("Edit", "Button") + app.find("Click a paragraph on the page to edit its text.", "Label") + click_page(app, 120, 168) # clause 1 + app.find("Paragraph", "Label", exact=False) + editor = [w for w in app.widgets() if w["ty"] == "TextInput" and (w.get("t") or "").startswith("This agreement")][0]["r"] + app.click(editor[0] + editor[2] - 6, editor[1] + editor[3] - 7) # after its last word + app.type(" (amended)") + app.grab(out, f"{p}08-edit") + app.tap("Apply", "Button") + app.find("Click a paragraph on the page to edit its text.", "Label") + app.images(1) + app.grab(out, f"{p}08b-edited") + + app.tap("Save", "Button") + app.find("Saved", "Label") + app.grab(out, f"{p}09-saved") + + app.tap("Open", "Button") + app.press("Damaged scan.pdf") + app.find("Couldn't open this PDF", "Label") + app.grab(out, f"{p}10-damaged") + app.tap("Close this tab", "Button") + + +def wait_until(check, failure, timeout=30.0): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + try: + if check(): + return + except (AssertionError, OSError, ValueError): + pass + time.sleep(0.4) + raise AssertionError(failure) + + +def comment_count(app): + """The count beside the comment panel's heading.""" + ax, ay, aw, ah = app.area() + head = [w for w in app.widgets() if w["ty"] == "Label" and w.get("t") == "Comments" and w["r"][0] > ax + aw / 2] + assert head, "no comment panel" + hx, hy, hw, hh = head[0]["r"] + for w in app.widgets(): + if w["ty"] == "Label" and (w.get("t") or "").isdigit() and w["r"][0] > hx and abs(w["r"][1] - hy) < 10: + return int(w["t"]) + raise AssertionError("no comment count") def restart(app, out): """The same storage after a restart, plus a PDF placed as an import leaves one.""" - app.see("Merged") - app.see("Merged 2") - app.see("Imported PDF") - # Read by the engine on this launch: the index never knew it. - app.see("3 pages · ", exact=False) - app.images(4) - app.grab(out, "r01-library") - app.press("Imported PDF") - app.see("Split into files", kind=None) + app.see("Imported PDF.pdf") + app.see("Opened today", exact=False) app.images(3) - app.grab(out, "r02-imported-pages") - app.tap("Text") - app.see("Garden plan", exact=False) - app.grab(out, "r03-imported-text") - app.tap("Pages") - app.tap_above("1", 120, below=300) - app.see("Page 1 of 3") + app.scroll(-3000) + app.grab(out, "r01-home") + app.press("Imported PDF.pdf") app.images(1) - app.grab(out, "r04-imported-page") - app.tap("‹ Pages") - app.tap("‹ Library") + app.find("1 / 3", "Label", timeout=60) + app.grab(out, "r02-imported") + app.tap("Open", "Button") + app.press("Quarterly report.pdf") + app.find("2 / 4", "Label", timeout=60) # where the shell run left it + app.images(1) + app.grab(out, "r03-place-kept") def full(app, out): """No room left: the engine refuses to write, and the app says so.""" - app.see("PDF Tools' storage is full") - app.grab(out, "f01-library-full") - app.tap("Merge PDFs") - app.press("Board minutes") - app.press("Apartment lease") - app.tap("Next: order 2 files") - app.tap("Merge 2 PDFs") - app.see("Couldn't merge: There isn't room for it in PDF Tools' storage. Remove a PDF you no longer need, then try again.") - app.grab(out, "f02-merge-full") - app.tap("Cancel") - app.press("Apartment lease") - app.tap("Remove") - app.tap("Remove") - # The library tries the first pages again at once, on the UI thread - # (#399), so the 8 s note may be gone before the bridge answers. - app.see("4 PDFs") - app.gone("Apartment lease") + app.find("PDF Tools' storage is full", "Label", timeout=90) + app.grab(out, "f01-home-full") + app.scroll(400) + time.sleep(1) + trash = sorted((w for w in app.widgets() if w["ty"] in ("Button", "ButtonFlat") and not w.get("t") and w["r"][2] < 30), + key=lambda w: (w["r"][1], w["r"][0])) + assert trash, "no remove button on the cards" + x, y, w, h = trash[0]["r"] + app.click(x + w / 2, y + h / 2) + app.find("Remove it from PDF Tools' storage?", "Label") + app.grab(out, "f02-remove-asks") + app.tap("Remove", "Button") + app.find("from PDF Tools.", "Label", exact=False, timeout=20) app.grab(out, "f03-removed") def empty(app, out, dark=False): p = "d" if dark else "" - app.see("No PDFs here yet") - app.see("PDFs up to 64 MB") + app.find("No PDFs here yet", "Label", timeout=60) app.grab(out, f"{p}e01-empty") - app.tap("Open a PDF from this device") - app.see("Bring PDF Tools to the front, then choose the file again.") + app.tap("Open a PDF from this device", "Button") + app.find("Bring PDF Tools to the front, then choose the file again.", "Label") app.grab(out, f"{p}e02-empty-open") - app.tap("OK") + app.tap("OK", "Button") def missing(app, out): app.see("This device has no PDF engine") app.see("Not available here") - app.grab(out, "m01-missing-library") - app.press("Field guide") + app.grab(out, "m01-missing-home") + app.press("Field guide.pdf") app.see("This device has no PDF engine") app.grab(out, "m02-missing-document") - app.tap("‹ Library") - app.tap("Merge PDFs") - app.see("This device has no PDF engine", exact=True) - app.grab(out, "m03-missing-merge") + app.tap("Close this tab", "Button") # ---------------------------------------------------------------- runs +def compare(out, names): + """Each grab beside its design, the same height: compare/NN-name.png.""" + from PIL import Image + folder = out / "compare" + folder.mkdir(exist_ok=True) + for design, grab in names: + a = Image.open(DESIGNS / f"{design}.png").convert("RGB") + b = Image.open(out / f"{grab}.png").convert("RGB") + b = b.resize((int(b.width * a.height / b.height), a.height)) + sheet = Image.new("RGB", (a.width + b.width + 12, a.height), "#ff00ff") + sheet.paste(a, (0, 0)) + sheet.paste(b, (a.width + 12, 0)) + sheet.save(folder / f"{grab}.png") + + +def keep_failure(app, out, name): + """On a failed step: the screen and its widgets, for whoever reads the run.""" + try: + app.grab(out, f"failed-{name}") + (out / f"failed-{name}.snap.json").write_text(json.dumps(app.remote("snap"), indent=1)) + except Exception as error: # the failure itself matters more + print("could not keep the failure:", error) + + def check_log(log): errors = [line for line in log.read_text(errors="replace").splitlines() if "[E]" in line and ("splash" in line.lower() or "script" in line.lower() or "pdftools" in line.lower()) @@ -622,6 +911,9 @@ def session(name, home, steps): # Where things are, for whoever reads the grabs: the window's widgets. (out / "snap.json").write_text(json.dumps(app.remote("snap"), indent=1)) steps(app, out) + except BaseException: + keep_failure(app, out, name) + raise finally: app.quit() check_log(log) @@ -632,9 +924,9 @@ def session(name, home, steps): fixture(home / "apps") def light_then_dark(app, out): - journey(app, out, dark=False) + shell_journey(app, out, dark=False) app.style("octosense-dark", "octosense dark") - journey(app, out, dark=True) + shell_journey(app, out, dark=True) session("shell", home, light_then_dark) if "restart" in runs: fixture("--import-sample", home / "apps" / APP / LIBRARY / "Imported PDF.pdf") @@ -654,38 +946,85 @@ def both(app, out): session("empty", home, both) +def dev_bundle(out): + """A scratch copy of the bundle with the stand-in engine in place of engine().""" + if out.exists(): + shutil.rmtree(out) + shutil.copytree(BUNDLE, out) + main = out / "main.splash" + text = main.read_text() + start = text.index("fn engine(method, args, done){") + end = text.index("\n}\n", start) + 3 + if 'host.request("pdf." + method' not in text[start:end] or text.count("fn engine(method, args, done){") != 1: + raise SystemExit("engine() in main.splash is not what this script replaces: update dev_bundle()") + main.write_text(text[:start] + (FIXTURE / "engine.splash").read_text().rstrip() + "\n" + text[end:]) + return out + + +DESIGNED = [("01-home", "01-home"), ("02-reading", "02-reading"), ("03-find", "03-find"), ("04-comment", "04-comment"), + ("05-pages", "05-pages"), ("06-combine", "06-combine"), ("07-fill-sign", "07-fill-sign"), ("08-edit", "08-edit"), + ("09-dark-reading", "09-outline")] + + def card_host_runs(args, runs): binary = args.card_host.resolve() assert binary.is_file(), f"build App Hub's card-host first: {binary}" assert port_free(args.port), f"port {args.port} is taken" - if "missing" in runs: - with tempfile.TemporaryDirectory(prefix="pdftools-card-host-", dir=args.work) as scratch: - data = Path(scratch) + with tempfile.TemporaryDirectory(prefix="pdftools-card-host-", dir=args.work) as scratch: + scratch = Path(scratch) + if "missing" in runs: + data = scratch / "missing" fixture(data) out = args.output / "missing" out.mkdir(parents=True, exist_ok=True) - app = CardHost(binary, data, args.port, "macos", out / "card-host.log") + app = CardHost(binary, BUNDLE, data, args.port, "macos", out / "card-host.log") try: missing(app, out) + except BaseException: + keep_failure(app, out, "missing") + raise finally: app.quit() check_log(out / "card-host.log") + if "fixture" in runs: + bundle = dev_bundle(scratch / "bundle") + for dark in (False, True): + data = scratch / ("fixture-dark" if dark else "fixture") + subprocess.run([sys.executable, str(FIXTURE / "make_fixture.py"), str(data / APP)], check=True) + global FIXTURE_JAIL + FIXTURE_JAIL = data / APP + out = args.output / "fixture" + out.mkdir(parents=True, exist_ok=True) + log = out / ("card-host-dark.log" if dark else "card-host.log") + app = CardHost(binary, bundle, data, args.port, "macos-dark" if dark else "macos", log) + try: + fixture_journey(app, out, dark) + fixture_states(app, out, dark) + except BaseException: + keep_failure(app, out, "dark" if dark else "light") + raise + finally: + app.quit() + check_log(log) + compare(out, [(design, ("d" if dark else "") + grab) for design, grab in DESIGNED]) def main(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("--shell", type=Path, help="the desktop shell binary (target/debug/octosense)") - parser.add_argument("--card-host", type=Path, help="App Hub's card-host, for the run without host services") + parser.add_argument("--card-host", type=Path, help="App Hub's card-host, for the runs without host services") parser.add_argument("--lock", type=Path, default=Path(tempfile.gettempdir()) / "octosense-shell.lock", help="the folder that marks a hidden shell run on this machine") parser.add_argument("--port", type=int, default=0, help="the remote bridge's port (8915 for a shell, 8911 for card-host)") parser.add_argument("--output", type=Path, default=ROOT / "target/pdftools-ui") parser.add_argument("--work", type=Path, help="where the runs' OctoSense homes go (default: the system's temporary folder)") - parser.add_argument("--only", choices=["shell", "restart", "full", "empty", "missing"], action="append") - parser.add_argument("--grab-scale", type=float, default=1.0, help="the grabs' scale (the bridge's /g scale)") + parser.add_argument("--only", choices=["shell", "restart", "full", "empty", "missing", "fixture"], action="append") + parser.add_argument("--budget-ms", type=int, help="MAKEPAD_SPLASH_BUDGET_MS for the hosts (default: the runtime's 64 ms); for a loaded machine") + parser.add_argument("--grab-scale", type=float, default=0.5, help="the grabs' scale (the bridge's /g scale): 0.5 is one pixel per point on a 2x screen") args = parser.parse_args() Driver.scale = args.grab_scale - card_host_only = ("missing",) + Driver.budget_ms = args.budget_ms + card_host_only = ("missing", "fixture") runs = args.only or (["shell", "restart", "full", "empty"] if args.shell else []) + (list(card_host_only) if args.card_host else []) assert runs, "give --shell, --card-host or both" if any(run not in card_host_only for run in runs): @@ -693,7 +1032,7 @@ def main(): args.port = args.port or 8915 shell_runs(args, runs) if any(run in card_host_only for run in runs): - assert args.card_host, "the missing run needs --card-host" + assert args.card_host, "the missing and fixture runs need --card-host" args.port = 8911 if args.port in (0, 8915) else args.port card_host_runs(args, runs) print("all journeys passed; grabs in", args.output) diff --git a/crates/shell/src/pdftools_model_tests.rs b/crates/shell/src/pdftools_model_tests.rs index be7fdf32..11ad89e7 100644 --- a/crates/shell/src/pdftools_model_tests.rs +++ b/crates/shell/src/pdftools_model_tests.rs @@ -111,3 +111,44 @@ fn the_index_keeps_no_title_for_a_pdf_that_shows_its_name() { assert_eq!(doc["title"], json!("Quarterly report")); assert_eq!(doc["pages"], json!(null)); } + +#[test] +fn a_page_range_names_the_pages_it_takes() { + let pages = |text: &str, total: u32| pdftools_model(&format!("range_pages(\"{text}\", {total}).to_json()")); + assert_eq!(pages("", 3), json!([1, 2, 3]), "empty is every page"); + assert_eq!(pages("All", 2), json!([1, 2])); + assert_eq!(pages("1-4, 9", 9), json!([1, 2, 3, 4, 9]), "Combine's example (06-combine)"); + assert_eq!(pages("1–3", 9), json!([1, 2, 3]), "a typed dash"); + assert_eq!(pages("2,", 9), json!([2]), "an empty part is left out"); + for bad in ["x", "12", "4-2", "0", "1-2-3", "2.5"] { + assert_eq!(pages(bad, 9), json!(null), "{bad} names no page this PDF has"); + } +} + +#[test] +fn a_find_snippet_marks_its_match_in_bold() { + assert_eq!( + pdftools_model("snippet_html(\"… strong [[revenue]] & more\").to_json()"), + json!("… strong revenue <up> & more") + ); +} + +#[test] +fn an_avatar_shows_the_first_letters_of_the_first_and_last_names() { + let initials = |name: &str| pdftools_model(&format!("initials_of(\"{name}\").to_json()")); + assert_eq!(initials("Jun Park"), json!("JP")); + assert_eq!(initials("Ana Ruiz"), json!("AR")); + assert_eq!(initials("Ana María Ruiz"), json!("AR")); + assert_eq!(initials(" Maya "), json!("M")); + assert_eq!(initials(""), json!("?")); +} + +#[test] +fn a_comment_date_is_read_as_the_service_writes_it() { + // SERVICE.md: local time without a zone or seconds. + assert_eq!( + pdftools_model("comment_time(\"2026-10-10T22:21\").to_json()"), + json!({"year": 2026, "month": 10, "day": 10, "hour": 22, "minute": 21}) + ); + assert_eq!(pdftools_model("comment_time(\"later\").to_json()"), json!(null)); +}