feat(docx-editor): blue page with a pilcrow, matching pdf-editor's badge

The mark now shares pdf-editor's silhouette, corner geometry and weight, in
blue against its red, so the two editors read as a pair in the dock. It
carries a pilcrow rather than a format label: at 16px "DOC" is a smudge
while the paragraph mark is still a glyph, and it says word processor rather
than file extension.

Drawn as paths, not <text>. An icon that needs a particular font installed
to make sense is an icon that eventually renders wrong somewhere.

Still nothing of Microsoft's: their blue sheet carries a white W, and the
page-with-a-folded-corner is the universal document glyph rather than
anyone's property.

Also adds docs/DESIGN-one-extension-catalogue.md, which writes down the
larger point this kept bumping into — that bundled and community extensions
are two systems for one kind of thing, with two trust rules and two lists in
Settings, and that "ships by default" should be a line of configuration
rather than a separate distribution path. Proposed only; the awkward part is
keeping a fresh offline install usable, which the note answers by treating
the shipped copies as a pre-seeded cache that still verifies like everything
else.
This commit is contained in:
Local Dev 2026-09-22 00:06:50 +02:00
parent 676424db87
commit 18eee9dbc7
8 changed files with 109 additions and 37 deletions

View file

@ -4,7 +4,7 @@
"version": "0.1.1",
"description": "Open, edit and save Word documents (.docx) in a full Theseus tab, or export them as PDF. Ribbon-style formatting, tables, lists, images and links; headers, footers, footnotes, page setup and the document's own styles are carried through a save untouched.",
"author": "Silent Mode",
"icon": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2032%2032%22%20role%3D%22img%22%20aria-label%3D%22Word%20editor%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22dx-page%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%220%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23ffffff%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23e9edf4%22%2F%3E%3C%2FlinearGradient%3E%3C%2Fdefs%3E%3Cpath%20d%3D%22M6.5%202.5h12l7%207v20a1%201%200%200%201-1%201h-18a1%201%200%200%201-1-1v-26a1%201%200%200%201%201-1z%22%20fill%3D%22url(%23dx-page)%22%20stroke%3D%22%232a3242%22%20stroke-width%3D%221.5%22%20stroke-linejoin%3D%22round%22%2F%3E%3Cpath%20d%3D%22M18.5%202.5v7h7%22%20fill%3D%22%23cfd8e6%22%20stroke%3D%22%232a3242%22%20stroke-width%3D%221.5%22%20stroke-linejoin%3D%22round%22%2F%3E%3Crect%20x%3D%229.5%22%20y%3D%2213%22%20width%3D%229%22%20height%3D%222.2%22%20rx%3D%221.1%22%20fill%3D%22%230ac18e%22%2F%3E%3Crect%20x%3D%229.5%22%20y%3D%2217.6%22%20width%3D%2213%22%20height%3D%221.6%22%20rx%3D%22.8%22%20fill%3D%22%2397a3b6%22%2F%3E%3Crect%20x%3D%229.5%22%20y%3D%2221%22%20width%3D%2213%22%20height%3D%221.6%22%20rx%3D%22.8%22%20fill%3D%22%2397a3b6%22%2F%3E%3Crect%20x%3D%229.5%22%20y%3D%2224.4%22%20width%3D%228.5%22%20height%3D%221.6%22%20rx%3D%22.8%22%20fill%3D%22%2397a3b6%22%2F%3E%3Cg%20transform%3D%22translate(20.4%2019.2)%22%3E%3Ccircle%20cx%3D%224.6%22%20cy%3D%224.6%22%20r%3D%225.4%22%20fill%3D%22%230ac18e%22%2F%3E%3Cpath%20d%3D%22M6.6%201.9H3.9a1.85%201.85%200%200%200%200%203.7h.8M5.3%201.9v5.6M6.6%201.9v5.6%22%20fill%3D%22none%22%20stroke%3D%22%230a0f16%22%20stroke-width%3D%221.15%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E",
"icon": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20d%3D%22M6%202h8l6%206v12a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2V4a2%202%200%200%201%202-2z%22%20fill%3D%22%232563eb%22%2F%3E%3Cpath%20d%3D%22M14%202v6h6z%22%20fill%3D%22%231e40af%22%2F%3E%3Cg%20fill%3D%22%23fff%22%3E%3Cpath%20d%3D%22M12.3%208.6a3.1%203.1%200%200%200%200%206.2z%22%2F%3E%3Crect%20x%3D%2212.05%22%20y%3D%228.6%22%20width%3D%221.7%22%20height%3D%2210.8%22%20rx%3D%22.2%22%2F%3E%3Crect%20x%3D%2214.85%22%20y%3D%228.6%22%20width%3D%221.7%22%20height%3D%2210.8%22%20rx%3D%22.2%22%2F%3E%3Crect%20x%3D%2212.05%22%20y%3D%228.6%22%20width%3D%224.5%22%20height%3D%221.7%22%20rx%3D%22.2%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E",
"main": "index.js",
"capabilities": [
"sidebar-panel",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 695 B

After

Width:  |  Height:  |  Size: 398 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 680 B

View file

@ -1,42 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="Word editor">
<title>Word editor</title>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<!--
Silent Mode's own mark for the Word editor.
Silent Mode's mark for the Word editor.
It is deliberately NOT Microsoft's Word icon. That icon — and the blue
sheet with a white "W" that goes with it — is Microsoft's trademark, and
borrowing it to make our extension look official is exactly the kind of
thing a browser that talks about sovereignty should not do.
Deliberately not Microsoft's Word icon. That mark, and the blue sheet
with a white "W" it belongs to, are Microsoft's trademarks; dressing our
extension up as theirs would be both a legal problem and a dishonest one.
What it does instead is say "text document" in the only vocabulary that
survives 16 pixels: a portrait page with a turned corner, a coloured
heading rule, a few lines of body text, and a pilcrow — the paragraph
mark every word processor has used since the trade was done on paper.
The accent is Silent Mode's green, not Office blue.
It shares pdf-editor's page silhouette, corner geometry and weight so the
two editors read as a set in the dock, and carries a pilcrow — the
paragraph mark every word processor has used since the trade was done on
paper — rather than a format label. The glyph is drawn as paths, not
<text>: an icon that needs a particular font installed to make sense is
an icon that eventually renders wrong somewhere.
-->
<defs>
<linearGradient id="dx-page" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff"/>
<stop offset="1" stop-color="#e9edf4"/>
</linearGradient>
</defs>
<!-- the sheet, with the top-right corner turned down -->
<path d="M6.5 2.5h12l7 7v20a1 1 0 0 1-1 1h-18a1 1 0 0 1-1-1v-26a1 1 0 0 1 1-1z"
fill="url(#dx-page)" stroke="#2a3242" stroke-width="1.5" stroke-linejoin="round"/>
<path d="M18.5 2.5v7h7" fill="#cfd8e6" stroke="#2a3242" stroke-width="1.5" stroke-linejoin="round"/>
<!-- heading rule, then body lines -->
<rect x="9.5" y="13" width="9" height="2.2" rx="1.1" fill="#0ac18e"/>
<rect x="9.5" y="17.6" width="13" height="1.6" rx=".8" fill="#97a3b6"/>
<rect x="9.5" y="21" width="13" height="1.6" rx=".8" fill="#97a3b6"/>
<rect x="9.5" y="24.4" width="8.5" height="1.6" rx=".8" fill="#97a3b6"/>
<!-- pilcrow, bottom-right, overlapping the page edge so it reads as a mark
rather than as more body text -->
<g transform="translate(20.4 19.2)">
<circle cx="4.6" cy="4.6" r="5.4" fill="#0ac18e"/>
<path d="M6.6 1.9H3.9a1.85 1.85 0 0 0 0 3.7h.8M5.3 1.9v5.6M6.6 1.9v5.6"
fill="none" stroke="#0a0f16" stroke-width="1.15" stroke-linecap="round"/>
<path d="M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z" fill="#2563eb"/>
<path d="M14 2v6h6z" fill="#1e40af"/>
<g fill="#fff">
<!-- bowl: a half disc hanging off the left stem -->
<path d="M12.3 8.6a3.1 3.1 0 0 0 0 6.2z"/>
<!-- the two stems -->
<rect x="12.05" y="8.6" width="1.7" height="10.8" rx=".2"/>
<rect x="14.85" y="8.6" width="1.7" height="10.8" rx=".2"/>
<!-- top bar tying the bowl and stems together -->
<rect x="12.05" y="8.6" width="4.5" height="1.7" rx=".2"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,87 @@
# One catalogue, one set of rules
Status: proposed, 2026-09-22. Not implemented.
Theseus currently runs two extension systems that happen to produce the same
kind of thing.
| | Bundled add-ons | Community extensions |
|----------------|-------------------------------------|---------------------------------------|
| How it arrives | compiled into the build, seeded into the profile on first run | installed from the gateway catalogue |
| Who signs | the Silent Mode operator's Ed25519 key, pubkeys compiled into the app | the owner of a BNS name, checked against the chain |
| Update channel | `theseus.x/extensions/<id>/updates.json` | `theseus.x/extensions/community/<id>/updates.json` |
| In Settings | a section headed "Built into Theseus" | a separate section headed "Community" |
| How to publish | `scripts/sign-addon-update.mjs` + `sia-upload` | the publish page, or `PUT /api/ext/…` |
Two catalogues in the UI, two trust rules, two publishing paths — for a
difference that is really only "who wrote it" and "is it there on day one".
## What this should be instead
**One catalogue.** Every extension is listed in the same place, whoever wrote
it. Silent Mode publishes under a Silent Mode name the same way anyone else
publishes under theirs. Being first-party is a fact about the publisher, not
a separate distribution system, and the UI should say so with a line of text
rather than a second list.
**One rule for trust.** An extension is installable because a signature
checks out against the current owner of the publisher's name. That is
already what `verifyPublisherEntry` does; the operator Ed25519 key becomes
either one more accepted signer or nothing at all. `pickChannelEntry`
already accepts both, so this is mostly deletion.
**Defaults are configuration.** Which extensions a fresh Theseus starts with
is a list of ids in the app's config, not a folder that gets copied. Changing
the default set should be editing that list, not moving directories around
and rebuilding.
## The one thing that needs care
If defaults are fetched from the catalogue on first run, a fresh install with
no network — or with the gateway down — has no editor, no wallet and no
screenshot tool. That is a real regression from today, where the build
carries them.
So keep shipping the code, but stop treating it as a different kind of thing:
- The build continues to include the default extensions' folders. They are a
**pre-seeded cache**, not a separate class of add-on.
- On first run they are installed from that cache, then verified and updated
against the catalogue exactly like anything the user installed by hand.
- If a seeded copy fails verification, it is quarantined rather than run —
a cache is only a cache.
That keeps offline first-run working while leaving one set of rules
everywhere else.
## Sketch of the work
- **Config**: `defaultExtensions: [{ id, publisher }]` in the app's settings,
read at first run and on profile migration. Ids the user has removed are
remembered and not re-seeded — uninstalling a default must stick.
- **`main.js`**: seeding installs *through* the normal install path instead
of copying a folder and calling it built-in. One code path, one place where
a signature is checked.
- **`settings.html`**: one list. "Installed by default" becomes a badge on a
row, next to the publisher's name. The separate Community section and its
Install button fold into the single catalogue view.
- **`addon-updater.js`**: drop the distinction between the two channel
shapes; an extension has a publisher and an updates URL.
- **Gateway**: the catalogue gains first-party entries. Nothing changes
structurally — `/api/ext` already stores whatever a name owner signs.
- **Publishing**: `scripts/sign-addon-update.mjs` and the operator key stop
being the first-party route. Silent Mode's own extensions go through
`PUT /api/ext/<our-name>/<id>/<version>` like everyone else's, which means
the release process needs that name's wallet rather than a key file.
## Open questions
- **Which name publishes Silent Mode's extensions?** Whoever goes first owns
each id permanently. Worth deciding once, deliberately.
- **Pinning.** A default extension is installed without anyone choosing it,
so a compromised or coerced catalogue entry reaches every fresh profile.
Pinning the default set to a version and a publisher in the app's config
bounds that.
- **Does the operator key survive at all?** Keeping it as an accepted signer
for the default set is a hedge against the name-owner key being lost;
keeping it forever means the two-rules problem never fully goes away.