Skip to content

feat(ui): add ui.document.dirty for unsaved-changes indicators (SD-2667)#3040

Merged
caio-pizzol merged 3 commits intomainfrom
caio/sd-2667-ui-document-dirty
May 1, 2026
Merged

feat(ui): add ui.document.dirty for unsaved-changes indicators (SD-2667)#3040
caio-pizzol merged 3 commits intomainfrom
caio/sd-2667-ui-document-dirty

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

`DocumentSlice` gains a typed `dirty: boolean` field. Apps wiring a Save / Export button or a "you have unsaved changes" indicator drop their host-listening boilerplate.

```tsx
const { ready, mode, dirty } = useSuperDocDocument();
return Save;
```

Semantics chosen:

  • `dirty` flips to `true` on any editor transaction where `tr.docChanged` is true. Selection-only transactions leave it alone.
  • Cleared by a successful `ui.document.export(...)` or `ui.document.replaceFile(...)`.
  • Editor swap (replaceFile, document switch) resets to `false` so the new document opens clean.
  • Rejected export leaves `dirty` alone so the consumer can retry without losing the unsaved indicator.
  • Undo-to-clean is intentionally out of scope β€” apps that want Word/GDocs "no unsaved changes" semantics layer their own edit-count diff on top.

Implementation notes:

  • Internal flag scoped to the controller closure; `documentMemo` includes `dirty` in its identity check so a flag flip busts the memo and re-fires `ui.document.subscribe` listeners (without re-firing on typing-only events that didn't move the flag).
  • The `onTransaction` handler runs alongside the existing `scheduleNotify` wiring; it short-circuits when `dirty` is already true so a 50-keystroke burst is one mutation, not fifty.

Verified: 164/164 super-editor UI tests; superdoc + @superdoc-dev/react builds clean; BYO-UI demo builds clean. 7 new tests cover: starts false, flips on docChanged, stays false on selection-only, subscribers re-fire on flip, export clears, rejected export preserves, replaceFile clears.

Once merged, the BYO-UI docs PR (#3034) drops the `useSuperDocHost()` transaction-listener snippet from `document-control.mdx` in favor of `useSuperDocDocument().dirty`.

DocumentSlice gains a dirty: boolean field driven by editor
transactions. Flips to true on any transaction with tr.docChanged;
clears on a successful ui.document.export(...) or ui.document.replaceFile().
Selection-only transactions don't move it.

- Internal flag tracked alongside documentMemo so a flag flip busts
  the memo without re-allocating on typing-only events.
- Editor swap (replaceFile, document switch) resets dirty to false in
  attachEditorListeners so the new document opens clean.
- Rejected export() leaves dirty alone; the consumer can retry.
- Undo-to-clean is intentionally out of scope β€” apps that need
  Word/GDocs 'no unsaved changes' semantics layer their own edit-count
  diff on top.
- Hook EMPTY_DOCUMENT fallback updated.
- 7 new tests covering: starts false; flips on docChanged; stays false
  on selection-only; subscribers re-fire on flip; export clears;
  rejected export preserves; replaceFile clears.
@caio-pizzol caio-pizzol requested a review from a team as a code owner April 30, 2026 23:18
@linear
Copy link
Copy Markdown

linear Bot commented Apr 30, 2026

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e260a5e75

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with πŸ‘.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/super-editor/src/ui/create-super-doc-ui.ts Outdated
@codecov-commenter
Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.

πŸ“’ Thoughts on this report? Let us know!

Wires the new ui.document.dirty flag into the demo's Export button so
the docs page has a concrete reference for the pattern. The button
shows a small amber dot and a tooltip suffix when dirty is true, then
clears once export() resolves.
attachEditorListeners() runs on both editorCreate (new document) and
on activeSurfaceChange (body ↔ header / footer / footnote routing
within the same document). Resetting dirty in that helper meant a
user could edit the body, click into a header, and lose the
unsaved-changes signal even though nothing was saved.

Move the reset out of attachEditorListeners and into a dedicated
editorCreate handler. ui.document.replaceFile() still clears dirty
explicitly (defensive). New test covers the editorCreate path.

Caught by chatgpt-codex on PR #3040 review.
caio-pizzol added a commit that referenced this pull request May 1, 2026
…, #3040

Three workarounds drop now that the controller surface covers them:

- comments.mdx: post a reply via ui.comments.reply(parentId, { text })
  instead of useSuperDocHost() + editor.doc.comments.create.
- custom-commands.mdx: drop the structural cast on superdoc.activeEditor;
  use the typed editor argument that execute({ payload, superdoc, editor })
  now provides. Pattern B's chain-command cast narrowed and called out.
- document-control.mdx: drop the transaction-listener snippet; use
  useSuperDocDocument().dirty directly. Includes the export-button
  example pattern. Trade-offs trimmed accordingly.

API reference updated: useSuperDocDocument now returns dirty;
ui.commands.register() execute receives editor; ui.comments.reply
listed in the comments handle block.
@caio-pizzol caio-pizzol merged commit 3ab9cc1 into main May 1, 2026
71 checks passed
@caio-pizzol caio-pizzol deleted the caio/sd-2667-ui-document-dirty branch May 1, 2026 10:03
caio-pizzol added a commit that referenced this pull request May 1, 2026
…, #3040

Three workarounds drop now that the controller surface covers them:

- comments.mdx: post a reply via ui.comments.reply(parentId, { text })
  instead of useSuperDocHost() + editor.doc.comments.create.
- custom-commands.mdx: drop the structural cast on superdoc.activeEditor;
  use the typed editor argument that execute({ payload, superdoc, editor })
  now provides. Pattern B's chain-command cast narrowed and called out.
- document-control.mdx: drop the transaction-listener snippet; use
  useSuperDocDocument().dirty directly. Includes the export-button
  example pattern. Trade-offs trimmed accordingly.

API reference updated: useSuperDocDocument now returns dirty;
ui.commands.register() execute receives editor; ui.comments.reply
listed in the comments handle block.
@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 1, 2026

πŸŽ‰ This PR is included in @superdoc-dev/mcp v0.3.0-next.26

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 1, 2026

πŸŽ‰ This PR is included in vscode-ext v2.3.0-next.71

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 1, 2026

πŸŽ‰ This PR is included in @superdoc-dev/react v1.2.0-next.69

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 1, 2026

πŸŽ‰ This PR is included in superdoc v1.30.0-next.28

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 1, 2026

πŸŽ‰ This PR is included in superdoc-cli v0.8.0-next.44

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 1, 2026

πŸŽ‰ This PR is included in superdoc-sdk v1.8.0-next.30

caio-pizzol added a commit that referenced this pull request May 1, 2026
…667) (#3043)

The JSDoc on SuperDocUI.document still claimed unsaved-changes was
intentionally not on the slice. That landed in #3040 as
DocumentSlice.dirty. The stale JSDoc misled at least one external
reviewer into flagging the docs as documenting a non-existent field;
update the JSDoc so the source is accurate.
caio-pizzol added a commit that referenced this pull request May 1, 2026
…odules (SD-2669) (#3034)

* docs(byo-ui): add Build Your Own UI section nav and stub pages (SD-2669)

Adds a new top-level "Build Your Own UI" group between Modules and
Solutions in docs.json, plus 10 stub pages mapping one-to-one to
controller domains: overview, react-setup, toolbar-and-commands,
custom-commands, comments, review, selection-and-viewport,
document-control, reference-demo, api-reference.

The stub pages include frontmatter, a one-line value statement, and
a Note callout pointing at SD-2669 for status. Stubs land first so
cross-doc callouts on the existing Modules pages can link to stable
URLs while the content is being written.

Overview is filled in to anchor the layer model (Document API /
superdoc/ui / built-in modules) and link to every page in the section.

* docs(byo-ui): align overview and stubs with brand voice (SD-2669)

Rewrites the overview to lead with one sentence, the layer model
table, and a 15-line code snippet showing useSuperDocCommand bound
to a button. Adds icons to the CardGroup so the section index reads
at a glance.

Sweeps em dashes from every stub. Brand voice rules ban them in
favor of hyphens, periods, or split sentences.

Adds superdoc/ui and superdoc/ui/react to the docs import allowlist
so future pages in this section pass the import validator.

Mintlify validate passes.

* docs(byo-ui): write 8 BYO-UI section pages (SD-2669)

Fills in:
- React setup: provider, onReady, hooks table, common pitfalls
- Toolbar and commands: per-button binding, payloads, built-in id table
- Custom commands: register, getState, execute, override, namespacing
- Comments: list, add from selection, capture-based composer, threads
- Selection and viewport: selection slice, capture, scrollIntoView, getRect
- Document control: setMode (Edit/Suggest), export, replaceFile
- Reference demo: what it covers, how to read it, what it deliberately doesn't do
- API reference: handwritten skeleton mirroring page order

review.mdx and api-reference.mdx will be revisited after the
ui.review β†’ ui.trackChanges rename lands as a separate PR.

All pages follow brand voice rules: code-first, short sentences,
no em dashes, public APIs only. Trade-offs called out per page.

Mintlify validate passes; import validator green.

* docs(byo-ui): rename Review page to Track changes, drop merged-feed surface

Mirrors the controller-side rename in #3029 / superdoc 1.30.1:
'ui.review' is gone and 'ui.trackChanges' takes its place.

- Rename review.mdx β†’ track-changes.mdx and rewrite for the
  tracked-changes-only surface (items, total, activeId).
- Document the merged comments + changes feed as a consumer-side
  composition pattern with useSuperDocComments + useSuperDocTrackChanges.
- Update docs.json nav, overview card grid, react-setup hook table,
  api-reference handle + types, and reference-demo coverage table.

* docs: cross-link built-in modules to Build Your Own UI

Adds <Tip> callouts on the four pages a consumer hits before they
realize there's a custom-UI path: modules/comments, modules/track-changes,
modules/toolbar/headless, and core/react/overview. Each one routes them
to the relevant Build Your Own UI page so they don't fight the built-in
module before discovering useSuperDocComments / useSuperDocTrackChanges
/ useSuperDocCommand.

* docs(byo-ui): address review feedback on canonical pages

- Custom commands: rewrite execute example to use the public
  ui.selection.getSnapshot().selectionTarget shape (was passing the
  wrong target type to editor.doc.insert). Lead with a pure-additive
  pattern; mark the activeEditor reach as a known escape hatch.
- Custom commands: split override into two patterns. Pattern A
  (sibling + dispatch both) is the default. Pattern B (true override)
  now actually toggles the mark instead of silently discarding it.
- Comments: de-emphasize the reply pattern. Group-by-parent shape is
  public; posting a reply is filed as a follow-up and points to the
  reference demo as a temporary workaround.
- Track changes: move the merged-feed pattern out of the canonical
  page and into reference-demo as 'one app-level composition'. Keeps
  the controller surface tight.
- Drop SSO-gated linear.app URLs from public-facing docs; keep ticket
  IDs as plain text.

* docs(byo-ui): replace ticket references with copy-safe workarounds

Public docs shouldn't promise specific follow-up tickets readers can't
see. Each former 'tracked under SD-XXXX' note now describes either how
the surface actually works today or a workaround that ships.

- comments: post replies via useSuperDocHost() + editor.doc.comments.create
  (full snippet, not just a pointer to the demo).
- custom-commands: drop the 'when SD-2844 lands' framing; the scoped
  structural cast is the pattern.
- selection-and-viewport / track-changes: explain that non-body entities
  snap because story activation mounts the surface synchronously, and
  show how to layer your own smooth scroll if needed.
- reference-demo: show window.getSelection() rect lookup as the path for
  floating menus today.
- document-control: full transaction-listener snippet for a dirty
  indicator.

* docs(byo-ui): use the typed controller APIs that landed in #3035, #3039, #3040

Three workarounds drop now that the controller surface covers them:

- comments.mdx: post a reply via ui.comments.reply(parentId, { text })
  instead of useSuperDocHost() + editor.doc.comments.create.
- custom-commands.mdx: drop the structural cast on superdoc.activeEditor;
  use the typed editor argument that execute({ payload, superdoc, editor })
  now provides. Pattern B's chain-command cast narrowed and called out.
- document-control.mdx: drop the transaction-listener snippet; use
  useSuperDocDocument().dirty directly. Includes the export-button
  example pattern. Trade-offs trimmed accordingly.

API reference updated: useSuperDocDocument now returns dirty;
ui.commands.register() execute receives editor; ui.comments.reply
listed in the comments handle block.

* docs(byo-ui): address codex review on PR #3034

- api-reference.mdx: ui.toolbar.execute(id, payload?) was signature
  syntax, not valid TS. Split into two valid call forms with prose.
- core/react/overview.mdx: drop the <Tip> block per AGENTS.md rule
  ('Don't add Tips, Warnings, or deep explanations in overview
  pages'). Cross-link kept as plain inline paragraph.

* docs(byo-ui): drop em dashes per brand style

* docs(byo-ui): correct toolbar ids and comment scrollTo scope

Two factual fixes flagged in PR #3034 review:

- toolbar-and-commands.mdx: the built-in command id table listed
  several ids that aren't on PublicToolbarItemId. strike β†’
  strikethrough, color β†’ text-color, highlight β†’ highlight-color,
  and the entire 'Block' row (heading-1/-2/-3, paragraph) is dropped
  because no block-level commands exist on this surface today.
  Replaced the table with the actual id set grouped by domain.
- comments.mdx: the trade-off note claimed ui.comments.scrollTo is
  story-aware. CommentAddress is body-scoped in the contract β€” only
  TrackedChangeAddress carries a story field β€” so the call doesn't
  navigate to header/footer/note comments. Replaced with an honest
  description and pointer to ui.viewport.scrollIntoView for non-body
  cases.

* refactor: rename build-your-own-ui to bring-your-own-ui (SD-2669)

The BYO acronym is conventional for 'Bring Your Own X' (BYOK, BYOC,
BYOD). It also reads more accurately to what consumers actually do:
they bring their existing React app + design system and slot
SuperDoc behind it, rather than building a UI from scratch.

- Section title in docs.json: 'Build Your Own UI' becomes 'Bring
  Your Own UI'.
- URL slugs: /build-your-own-ui/* becomes /bring-your-own-ui/*.
- apps/docs/build-your-own-ui/ renamed to apps/docs/bring-your-own-ui/
  (10 mdx pages, all internal links updated).
- demos/build-your-own-ui/ renamed to demos/bring-your-own-ui/. Demo's
  package.json name, README heading, the docs reference-demo links,
  the playwright port-map key, and the ci-demos.yml matrix entry all
  follow.
- Cross-link callouts on /modules/comments, /modules/track-changes,
  /modules/toolbar/headless, /core/react/overview reworded.

* docs: integrate Bring Your Own UI into the broader docs (SD-2669)

Fixes stale correctness issues and lays out the layer model so
existing docs route customers to the right path.

Stage 1 β€” correctness fixes on BYO pages
- bring-your-own-ui/reference-demo: drop the 'demo reaches through
  useSuperDocHost()' prose; replies and custom commands now use the
  typed surface after #3035 and #3039 shipped.
- bring-your-own-ui/custom-commands: Pattern B (override) reframed
  as an advanced escape hatch with a Warning, since override truly
  replaces and consumers shouldn't trust the built-in to still run.
- modules/track-changes: drop 'merged comments + changes feed' from
  the BYO callout; the merged-feed pattern moved to reference-demo
  as an app-level composition.

Stage 2 β€” toolbar IA cleanup
- modules/toolbar/overview: replace the two-path table with a
  three-path decision: Built-in / Bring Your Own UI / Headless
  toolbar. Recommendation: new React apps reach for BYO first;
  headless stays supported for non-React and existing integrations.
- modules/toolbar/headless: promote the BYO redirect from a Tip to
  a 'Using React?' section with the supported-vs-recommended framing.
- modules/toolbar/examples: drop the React + shadcn and React + MUI
  examples; redirect React readers to BYO. Vue / Svelte / vanilla
  examples stay.

Stage 3 β€” onboarding links
- getting-started/frameworks/react: add BYO card to Next steps.
- getting-started/quickstart: add BYO link inline under the React
  scenario plus a card in 'What's next'.

Stage 4 β€” authoring guidance + LLM context
- AGENTS.md: new 'Layer model' section with explicit recommendation
  order (Document API > superdoc/ui > Modules > Core). API naming
  marks superdoc.activeEditor.commands.* as legacy/compat. Source
  file table extended with the doc-api contract and superdoc/ui.
- llms.txt: add Bring Your Own UI link plus a Layer Model section
  so agents recommend the right surface.

Stage 5 β€” disambiguate track-changes pages without URL renames
- modules/track-changes: title 'Track changes module',
  sidebarTitle 'Track changes module'.
- bring-your-own-ui/track-changes: title 'Custom track changes UI',
  sidebarTitle stays 'Track changes' for nav consistency.

* docs: route live-state events to BYO UI; expand llms-full layer model

- core/superdoc/events.mdx: leading Note tells React consumers to
  prefer superdoc/ui subscriptions over manual superdoc.on(...) loops
  for live UI state. Lifecycle / integration / analytics events stay
  the documented use case.
- llms-full.txt: new Layer Model section after Architecture so agents
  recommend Document API for mutations, superdoc/ui for custom React
  UI, and modules for built-in UI. Lists the four common
  anti-patterns to steer clear of in customer code.

* docs: route legacy paths to typed surfaces (SD-2669)

- modules/comments: lead the 'API methods' section with a Note that
  steers new code to editor.doc.comments.* (mutations) or ui.comments.*
  (React UI). The activeEditor.commands.* docs stay for backwards
  compatibility but no longer present as the recommended path.
- core/supereditor/overview: was framing SuperEditor as 'Custom UI
  implementation' and claimed Document API was 'coming soon'. Both
  outdated. Rewrote 'When to use' as a layer-picker that points
  custom React UI at Bring Your Own UI, document mutations at the
  Document API, and SuperEditor at the genuine low-level cases
  (engine internals, custom extensions, server-side headless).

* docs: add BYO UI pointers in core methods + doc-api migration guide

- core/superdoc/methods: lead the Comments-methods section and the
  activeEditor property docs with a Note steering new code to
  ui.comments.* / editor.doc.* / ui.commands.*. addCommentsList
  / removeCommentsList stay supported for built-in sidebar users;
  activeEditor.commands stays for backwards compat.
- guides/migration/document-api: append a 'Driving custom React UI'
  section. Migrators dropping editor.commands/state/view from doc
  mutations also benefit from migrating the UI side to typed hooks
  in the same pass. Points at the BYO UI overview and the demo.

* docs: route remaining built-in callouts to typed surfaces

- getting-started/import-export: post-init insertContent example
  steers new code to editor.doc.insert; chain command stays for
  apps already wired to activeEditor.commands.*
- modules/toolbar/built-in: top-of-page Tip routes React readers
  to Bring Your Own UI first; headless stays as the non-React
  alternative.

* docs: add four Mermaid diagrams to anchor BYO UI conceptually

- bring-your-own-ui/overview: layer-model diagram showing how Your UI
  β†’ superdoc/ui/react β†’ superdoc/ui β†’ Document API β†’ engine, with
  built-in modules as a sibling path also routed through Document API.
- bring-your-own-ui/react-setup: provider-tree diagram showing one
  provider, one controller, many components subscribing per slice.
- bring-your-own-ui/comments: capture-sequence diagram for the textarea-
  composer flow (select, click Comment, capture, focus moves, submit
  posts via createFromCapture).
- getting-started/quickstart: pick-your-surface decision flow at the
  top of the page so visitors route to AI agents / backend / built-in
  editor / Bring Your Own UI without reading the section headings.

* docs(byo-ui): drop the three BYO Mermaid diagrams

Felt too much for the BYO surface. Removing the layer-model,
provider-tree, and capture-sequence diagrams. Visual story for
BYO will land via media assets and embedded demos in a follow-up,
not diagrams. Quickstart 'pick your surface' diagram stays β€” it's
site-wide orientation, not BYO-specific.

* docs(byo-ui): add embedded BYO UI demo on overview page

New snippet at apps/docs/snippets/components/byo-ui-demo.jsx renders
a compact 'workspace slice' driven by superdoc/ui:

- Custom toolbar (B / I / Comment / Edit-Suggest / Export) wired to
  ui.commands observables, ui.document.setMode, ui.document.export.
- Activity sidebar shows live ui.comments and ui.trackChanges with
  Resolve / Accept / Reject actions. Empty state with hints when no
  comments or changes exist.
- Selection-capture composer for new comments via
  ui.selection.capture() + ui.comments.createFromCapture().
- Try-it checklist above the workspace.

Loading: lazy behind a 'Launch interactive demo' button so the 5 MB
SuperDoc UMD only loads on user gesture. Editor comes from the
existing UMD pattern (window.SuperDoc); controller comes from a
dynamic import() of superdoc/ui's ESM entry. The embed bridges
controller observables into local React state instead of using
superdoc/ui/react β€” that bundle imports React as a bare ESM
specifier, which is fragile in Mintlify's runtime. Caption text
points React app users at the typed hooks.

Pinned to superdoc@1.30.1 (deterministic; floats forward only when
we update the snippet). v1 ships overview only; per-domain variants
(comments, track-changes, custom-command) come in follow-ups.

* docs(byo-ui): revert embed from overview, hits Mintlify runtime issues

Two failures on the deployed preview:

1. MDX runtime can't resolve capital-letter JSX references inside a
   snippet ('Expected component Workspace to be defined'). The helper
   functions inside byo-ui-demo.jsx need to be inlined into one big
   component, no nested <Workspace />, <ToolButton />, etc.

2. The SuperDoc UMD throws 'Identifier fs has already been declared'
   on injection, suggesting it's being loaded twice. Likely a baseUrl
   mismatch with the existing SuperDocEditor snippet on extension
   pages β€” different URL strings dodge the dedup querySelector.

Per the agreed fallback plan, don't block #3034 on the embed. The
snippet file stays at apps/docs/snippets/components/byo-ui-demo.jsx
for the follow-up PR where we can debug Mintlify's runtime quirks
without delaying the docs launch.

* docs(byo-ui): tighten sidebar β€” group toolbar, drop reference-demo, drop embed snippet

Sidebar IA cleanup. Customer-facing surface goes from 10 entries to 8
(one of which expands to 2 sub-pages):

- Group 'Toolbar and commands' + 'Custom commands' under a 'Toolbar
  and commands' parent. URLs unchanged; sidebarTitle on each page set
  to 'Built-in' / 'Custom' so the sub-entries don't collide with the
  group label. Page titles ('Toolbar and commands', 'Custom commands')
  stay verbose at the top of each page.
- Drop the 'Reference demo' page. Most of it duplicated content that
  belongs in the demo's own README. The merged-feed pattern, the
  'deliberately doesn't do' list, and the three takeaways for your own
  UI all moved to demos/bring-your-own-ui/README.md. Overview gets a
  one-line callout pointing at the demo on GitHub.
- 'Document control' page sidebarTitle becomes 'Document' β€” matches
  the one-word pattern of Comments / Track changes (each named after
  its ui.* slice).
- Drop apps/docs/snippets/components/byo-ui-demo.jsx. The embed work
  is captured in SD-2871 and isn't blocking the docs launch.

Final sidebar:
  Overview / React setup / Toolbar and commands (Built-in, Custom) /
  Comments / Track changes / Selection and viewport / Document /
  API reference

* docs(quickstart): hide mermaid pan/zoom controls on the surface picker

actions={false} disables the Mintlify-injected zoom/pan widget on
the 'Pick your surface' diagram. The diagram is small and one-glance
readable; the controls add visual noise without buying anything.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants