Skip to content

Brand/monoline icon - #7

Merged
mindaugaskasp merged 2 commits into
mainfrom
brand/monoline-icon
Jul 28, 2026
Merged

Brand/monoline icon#7
mindaugaskasp merged 2 commits into
mainfrom
brand/monoline-icon

Conversation

@mindaugaskasp

Copy link
Copy Markdown
Owner

No description provided.

mindaugaskasp and others added 2 commits July 28, 2026 21:03
Replace the mascot icon with the Monoline "Shades-D": a warm cream letter D
wearing spectacles on a charcoal tile, one amber seam as the diff gutter.
Single 1024 master (logo.svg -> icon.png) drives macOS .icns, Windows .ico
and the Linux icon set.

Installer branding matched to the mark:
- macOS DMG: warm-light backdrop (dark backdrop hides Finder's black captions),
  amber drag-arrow, faint D watermark — dmg-background.svg + rendered PNGs.
- Windows NSIS: welcome/finish sidebar (164x314) and inner header (150x57)
  BMPs, wired in electron-builder.yml; installer/uninstaller icons default to
  the app icon.
- Linux deb/AppImage have no installer UI; they inherit icon.png.

New `npm run render:brand` (scripts/render-brand-assets.mjs) rasterizes the
SVG sources to icon.png + the two BMPs through the dev-dependency Electron,
with an inline 24-bit BMP encoder (no new dependency).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mindaugaskasp
mindaugaskasp merged commit 3a0fcfe into main Jul 28, 2026
2 checks passed
@mindaugaskasp
mindaugaskasp deleted the brand/monoline-icon branch July 28, 2026 20:10
@mindaugaskasp
mindaugaskasp restored the brand/monoline-icon branch July 30, 2026 21:33
mindaugaskasp added a commit that referenced this pull request Jul 31, 2026
* feat(tools): dev tools pack — JWT, epoch, URL, HTML entities, sort/dedupe

Five pure text→text tools added to the TEXT_TOOLS registry (utils/devTools.js),
so each is a validate/format pair that plugs into the existing tool dialog with
no new component:

- JWT Decode — header + payload as one JSON object (signature never verified)
- Epoch / Date — Unix s/ms ↔ ISO 8601 UTC
- URL Encode / Decode — toggles on percent-encoding, survives malformed input
- HTML Entities Encode / Decode — named, decimal and hex entities
- Sort & Dedupe Lines

Wired into MENU_ACTIONS + both menus (app + MenuBar); they also appear in the
sidebar Tools list automatically (derived from TEXT_TOOLS keys). Hash/checksum
and a regex tester are intentionally out of scope here — they need async / a
second input, which the single-input sync dialog can't host.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(diff): apply a unified .patch to a base file

Adds applyUnifiedDiff(base, patch) — a pure, exact-match unified-diff applier
that rejects (never silently misapplies) hunks whose context doesn't match, and
returns the rejected indices. Property-tested by round-tripping every patch
toUnifiedDiff produces back to the changed side (add/delete/replace, EOF-newline
and multi-hunk cases).

UX is option A: "Apply Patch…" picks a base file and a .patch, then opens
base ↔ patched in the normal diff view so the change is shown, not just written.
Wired as diffStore.applyPatch() + the 'apply-patch' action, in the Edit menu
(next to Copy Diff as Patch), the MenuBar, and a pinned sidebar Tools button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ui): command palette (⌘/Ctrl+Shift+P)

A fuzzy palette over every menu command, built by flattening the SAME
buildMenus() the menu bar uses — no duplicated action list. Submenu labels fold
into their leaves ("JSON · Format / Validate") so each command reads on its own;
matching reuses rank() and the keyboard driver is useQuickLookKeys with its
list-only defaults, so all event logic stays in tested composables. Backdrop
dismiss goes through useBackdropClose.

flattenCommands is pure and unit-tested; the overlay styles through design
tokens only. Opens from View → Command Palette… and the MenuBar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(diff): export a comparison as self-contained HTML

File → Export Diff as HTML… writes the current comparison (files or paste mode)
as a single self-contained document — inline CSS, no external assets, so it
opens anywhere and can't phone home — for pasting into a ticket or PR.

diffToHtml is pure and unit-tested: it reuses the new diffLines() (factored out
of toUnifiedDiff so both share one aligned line list) and escapes every piece of
user text and both names, so content can never inject markup. Main only writes
the document the renderer built (diff:exportHtml → save dialog). PDF/image can
follow later by printing this same HTML.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(snippets): import from VS Code snippets JSON or any text file

File → Import Snippets… reads a chosen file and adds snippets to the encrypted
library. A VS Code .code-snippets JSON becomes one snippet per entry; any other
file is imported verbatim as a single snippet named after it.

parseSnippetImport is a pure, size-capped, shape-validated untrusted-input
surface (CLAUDE.md #6) — it only produces drafts; the snippet store re-encrypts
each through the existing add()/vault path before anything is stored, so an
import can never write undecryptable or unvalidated data. The diff store
orchestrates and reports the count via the shared notice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(quicklook): inline convert tools in the global launcher

Fold a keyboard-navigable convert section into Quick Look: summon the launcher
(its existing rebindable global shortcut — no new binding, so no VS Code
collision), type "base64", and convert a string inline — encode/decode, JWT,
URL, HTML entities, epoch, JSON, UUID — without ever raising the app.

The transforms are the same pure helpers the in-app tools use
(utils/quickLookCommands: base64 + TEXT_TOOLS), so the two never diverge and the
work stays offline. Commands rank next to snippets/diffs (kind:'command').

Navigation mirrors the snippet flow so it reads the same on every row:
- → (or Enter) on a tool opens its convert panel; the input auto-focuses.
- ← at the start of the input, or Escape, backs out to the list — and focus
  returns to the search box so arrow-key navigation resumes.
- Tab cycles input ↔ output (read-only, selectable); the focused field shows
  the same accent edge the snippet preview uses.
The preview panel is now kind-aware so a command selection never reads tags/labels
meant for snippets.

Pure runConvert + the new onExpand key path are unit-tested; the full
summon→→→convert→copy→back flow (auto-focus, ←/Tab/Escape, nav resume) is covered
by e2e/quick-look-convert.spec.mjs (DOM-driven — the transparent hide-on-blur
window can't be screenshotted reliably).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* update quick look

* feat(tools): rich Epoch / Date panel + shared control kit

Turns the Epoch tool from a single text box into a purpose-built panel,
hosted in BOTH the Tools dialog and the Quick Look launcher:

- utils/epoch.js — pure nowEpoch / epochRows / pickToEpoch / relativeTime
  (Vue-free, unit-tested)
- SegmentedControl.vue — reusable one-of-N kit primitive (v-model)
- ToolEpoch.vue — live "Now" readout, seconds/millis + UTC/Local toggles,
  date-picker → epoch and epoch → ISO/Relative/Local rows with per-row copy
- TEXT_TOOLS gains a `panel` kind; TextToolDialog renders the panel instead
  of Monaco, the launcher's convert panel renders it compact
- clock + calendar icons

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: refresh CLAUDE.md

- Drop stale references: DEVELOPMENT_PLAN.md (absent) and the README "mermaid
  diagram" (the README carries no mermaid, only Mermaid as an app feature).
- Add a rule to sweep in-progress reasoning comments once a feature settles,
  before committing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(quicklook): collapsible Tools section

The convert tools now sit under a single "Tools" row below the snippets seam —
Enter or → expands a nicely-formatted, indented tool list; Enter/←/Esc collapses
it — so browsing snippets stays compact. A tool-matching search auto-expands and
lands on the first match, and keyboard nav stays one flat index-based list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(quicklook): default shortcut is Cmd/Ctrl+Shift+Space on every platform

macOS defaulted to Shift+Space, which misfires constantly — Shift is held while
typing capitals, so a stray Space popped the launcher. Unify to the two-modifier
Cmd/Ctrl+Shift+Space (already the Windows/Linux default), which can't fire by
accident and is Spotlight-adjacent. Still rebindable in Settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tools): panel dialog (Epoch) doesn't close on backdrop click

Clicking outside the native date picker to dismiss it also closed the whole
Epoch dialog. A panel tool now keeps close-on-backdrop off, so an outside click
dismisses only the picker; Escape and the Close button still close the dialog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(quicklook): keyboard ↓/↑ keeps the active preview line in view at the bottom

The line-copy feature stepped the active line with scrollIntoView('nearest'),
which under-scrolls near the end — smooth-scroll lag makes 'nearest' read a
mid-animation position as already visible, so ↓ stalls once the max height is
reached (mouse wheel still worked). Scroll the container by the exact overflow
via scrollTop instead (instant, scroll-behavior removed), so it never stalls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tools): rich UUID panel — generate v1/v4/v5/v6/v7, inspect, convert, verify

- utils/uuid.js: pure generators — v1/v6 (time), v4 (random), v7 (time-ordered),
  v5 (deterministic SHA-1); formatUuid (canonical / 32-hex / urn / braced + case);
  uuidInfo (version/variant); toCanonicalUuid (any form → canonical, both-way
  convert). v5 hashes with Web Crypto — a public digest, no key material — so it
  runs inline in the launcher too. Tests include the documented DNS/python.org v5
  vector.
- ToolUuid.vue: version selector, v5 namespace presets (DNS/URL/OID/X.500) +
  custom + name, a v5 verify/match field (a v5 hash can't be reversed to a name,
  so a candidate is checked instead), case/form controls, and a live version
  badge; hosted in the Tools dialog and the Quick Look launcher.
- Flatten the Tools → UUID menu to a direct item (its "Convert" leaf was stale).
- hash icon; e2e for generate, reverse-convert, and verify.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tools): rich URL panel — encode/decode + editable query-param table

- utils/url.js: pure encodeUrl/decodeUrl (component or full-URI), parseQuery
  (decoded key/value pairs, fragment kept out), buildQuery (re-encode, drop
  empty rows).
- ToolUrl.vue: explicit Encode/Decode + Component/Full-URI toggles, and an
  editable query-param table that rebuilds the URL live (add/remove rows, per-
  output copy); hosted in the Tools dialog and the Quick Look launcher.
- headIcon map (adds the link icon) for the launcher panel head.
- Tests: url util (7) + e2e (encode/decode · component/full, live param edit,
  clipboard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tools): rich JWT panel — decode header/payload + humanized claims

- utils/jwt.js: pure parseJwt (base64url header/payload) + jwtClaims (alg,
  humanized iat/nbf/exp with relative + absolute time, expired/not-yet-valid
  flags), reusing relativeTime from utils/epoch. Decode only — the signature is
  never verified.
- ToolJwt.vue: alg badge + expired/not-expired pill, a time-claim summary,
  pretty header/payload JSON with copy, and a "signature isn't verified" note;
  hosted in the Tools dialog and the Quick Look launcher (shield-check icon).
- Tests: jwt util (4, incl. the jwt.io example) + e2e (decode + copy, malformed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tools): rich JSON panel — Pretty/Minify/Sort, JSONPath filter, syntax tree

Replaces the plain JSON format/validate dialog with a panel tool:
- Pretty / Minify / Sort-keys via SegmentedControl (deep key sort).
- JSONPath-subset filter (child, index, wildcard, recursive descent,
  bracket notation) with a live match count; hand-rolled, no deps.
- Syntax-colored, collapsible JsonTree (recursive, self-imported) using
  the semantic status tokens so highlighting stays legible in all 14
  themes; deep nodes auto-collapse.
- Wired into textTools/TextToolDialog and the Quick Look-up launcher
  (braces headIcon), plus the flattened Tools → JSON menu (both surfaces),
  Cmd/Ctrl+Shift+J.

Pure shaping + path logic in utils/json.js with thorough tests over messy
real-world payloads (JSON-in-string, XML/HTML strings, unicode, big ints)
that never re-parse string values. E2E covers format/filter/minify/copy
and invalid-input reporting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tools): rich Lines panel — clean up, sort, and build lists

Replaces the plain "Sort & Dedupe Lines" dialog (now just "Lines") with a
panel tool:
- Clean up: trim, drop blanks, dedupe (keep first).
- Sort: Original / A→Z / Z→A / Natural (numeric-aware).
- List builder: literal (non-regex) per-line find/replace, wrap each line
  with a prefix/suffix, and join with a separator + optional one-per-line —
  e.g. a paste of UUIDs → a SQL IN-clause list.
- Live in→out·dupes count and one-click copy (via the main process).

Wired into textTools/TextToolDialog and the Quick Look-up launcher (list
headIcon), plus the renamed Tools → Lines menu (both surfaces). Pure
pipeline in utils/lines.js with thorough tests (ordering, natural sort,
literal find/replace, the SQL-list headline case) that supersede the old
sortDedupeLines helper. E2E covers the SQL-list build + copy and natural
sort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tools): Lines — explode input by a delimiter (Split by)

Adds a "Split input by" field to the Lines panel: besides newlines, the
input is also split on a literal delimiter, so a single delimited line
(e.g. `aaad, adad, ddd, 444`) explodes into separate lines before the
pipeline — pair it with wrap + join to turn a CSV row into a quoted list.
Blank leaves it splitting on lines as before. The existing per-line
find/replace is unchanged. Util tests + e2e cover the explode-to-list case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tools): rich Base64 panel — toggle+live, URL-safe, wrap, byte counts

Replaces the standalone Base64 dialog with a panel tool, consistent with
the other rich tools:
- Explicit Encode/Decode segmented toggle with live output.
- URL-safe alphabet (-_ , padding stripped) and optional 76-column MIME
  wrap on encode; decode is tolerant (accepts standard or URL-safe input,
  strips whitespace, re-pads).
- Live byte + character counts and one-click copy (via the main process).

Now a TEXT_TOOLS `panel` entry rendered in both the Tools dialog and the
Quick Look launcher, replacing the launcher's two Base64 Encode/Decode
text-converts with one panel. Retires Base64Dialog.vue + showBase64Dialog;
the sidebar chip now derives from TEXT_TOOLS. Flattened Tools → Base64 menu
(both surfaces), Cmd/Ctrl+Shift+B.

utils/base64 gains encode options, a tolerant decode, and byteLength, with
tests (URL-safe, wrap, whitespace-tolerant decode, byte counts). The
launcher's generic convert-flow e2e now rides HTML Entities (the remaining
text-convert); a new base64 e2e covers encode/decode/URL-safe/copy/error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* build: declare allowScripts for the six build-toolchain packages

npm 11 gates install scripts behind an `allowScripts` allow-list; without
one, every install warns about six unreviewed packages. All six are build
toolchain that must run their script to work — electron and esbuild fetch
their platform binaries, fsevents is the macOS watcher, electron-winstaller
picks the 7z arch, vue-demi shims the Vue major — and none ship in the app,
so they are approved by `npm install-scripts approve`.

package.json only: the lockfile is untouched (a lock rewrite goes through
`make install` with the container's npm, per CLAUDE.md). Documents the gate
in CLAUDE.md so a new dependency's install script gets read before it is
approved, rather than the gate being disabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tools): one searchable tools UI, with your recents on the shelf

The sidebar shelf and the launcher's Tools list both showed twelve
identical entries — a wall of uppercase pills, and rows that all wore the
same wrench under a blanket "CONVERT" that was wrong for most of them (JWT
decodes, UUID generates, JSON formats). Both now read from one registry.

- utils/tools.js: the single registry — each tool declares its icon, a
  proper-case name, an accurate one-word kind, and the menu action that
  opens it, so the surfaces cannot drift apart. Includes Encrypt, which the
  old shelf inconsistently omitted.
- Sidebar shelf (ToolsShelf.vue): up to five recent tools as icon chips
  plus one "All tools" control — two rows instead of four, with the tools
  you actually use one click away.
- The full list is the command palette in a tools scope (no fourth list to
  maintain): Recent above All tools, each row with its own icon and kind.
  Section labels ride as a pseudo-element on the opening row so keyboard
  indices stay 1:1 with results.
- Launcher rows take their icon and action word from the same registry.
- Recents persist in settings (max 5, most-recent-first) and are recorded
  in handleMenuAction — one choke point, so the menu, a shortcut, the shelf
  and the palette all count.
- icons.js gains the three icons the registry needed: binary, tag, replace.

Tests: utils/tools.js unit-tested including a cross-check that every
declared icon exists in the icon map; settingsStore covers dedupe/cap/junk
input; e2e covers the empty shelf, opening and searching the palette,
recents appearing in both surfaces, and a chip opening its tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* build: pin every allowScripts entry to an exact version

Four of the six install-script approvals were version-pinned; `electron`
and `vue-demi` fell back to "approved by name (all versions)", which would
let a future version inherit the approval without review.

npm can only pin what it can locate, and both packages were missing a
`resolved` URL — and an `integrity` hash — in the lockfile. Dropping the two
entries and re-resolving them through the container's npm (`make install`'s
path, so the lock stays Linux-complete) adds both fields: four lines, no
version drift, and two packages that now carry integrity hashes they
previously lacked.

The approval itself reads npm's HIDDEN lockfile
(node_modules/.package-lock.json), not the committed one, so that had to be
refreshed too before `npm install-scripts approve` would pin. All six
entries are now exact; noted in CLAUDE.md so the next person doesn't repeat
the detour. Verified with `npm ci` inside the container.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: pin Node 22 for local dev and record the known install warnings

`npm install` on Node 20 warns EBADENGINE three times: for the app itself
and for @electron/rebuild / node-abi, all of which require >=22.12. Docker
(node:22-bookworm-slim) and CI (node-version: 22) already run 22 — only a
local machine could drift, and nothing made the requirement discoverable
outside the `engines` field.

Adds .nvmrc so `nvm use` picks the right major, states it in the README
build steps, and notes in CLAUDE.md that all four places (.nvmrc, engines,
Dockerfile, CI) move together. Widening `engines` is explicitly ruled out:
the toolchain requirement is real, not cosmetic.

Also records that the three deprecation warnings (inflight, rimraf@2,
glob@7) are transitive dev-only dependencies of electron-builder — already
at its latest, zero audit findings, nothing we import — so they are
upstream's to fix and must not be papered over with `overrides` inside a
build tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tools)!: drop the SQL and HTML Entities tools

Removes both tools end to end — TEXT_TOOLS entries, registry rows, menu
items (including SQL's Cmd/Ctrl+Shift+Q), store actions, the shortcut
listing, and their tests. The HTML-entity helpers go with them; SQL keeps
utils/sqlFormat.js, which the snippet editor still uses to format SQL
snippets. SQL and HTML remain fully supported as LANGUAGES for diffing,
snippet highlighting and detection — only the tools are gone.

HTML Entities was the last text→text tool, so the launcher's convert
machinery had no reachable user: runConvert, the convertInput/convertResult
plumbing, and QuickLookConvert's input/output textareas are all deleted
rather than left as unreachable code. Every launcher tool now renders a
rich panel, and the component is just the frame around one.

That exposed a real gap the old textarea had hidden: entering a launcher
tool left focus on the body, so Escape never reached the panel's handler
and there was no keyboard way back to the list. The panel now lands the
caret in its first field — you can type immediately, and Escape works with
no click first.

Also refreshes the README, which still described the pre-panel tool lineup
and "nine themes" (there are fourteen), and removes the dead
DEVELOPMENT_PLAN.md links from the README and docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tools): rich XML panel — Pretty/Minify, XPath filter, element tree

The last plain format/validate dialog becomes a panel like the rest:
- Pretty / Minify toggle, live as you type.
- An XPath-subset filter (/a/b, //descendant, [n] 1-based as XPath means it,
  @attr and *) with a live match count. Hand-rolled, no new dependency.
  Attribute matches are values rather than elements, so they list on their
  own instead of being forced into the tree.
- A collapsible element tree that reads attributes on the tag, the way the
  source does; deep nodes auto-collapse.
- Parse failures show inline with the line and column the validator already
  knew, instead of a bare status line.

Parsing goes through DOMParser in 'text/xml': it builds a tree without
executing anything and does not resolve external entities, and the document
is never inserted into the page — only read into plain objects the template
renders as text (CLAUDE.md #7).

Fixes a colour bug this work exposed in BOTH trees: --warning-text is the
ink for text ON a warning fill, so it is near-black on all six dark themes.
JSON numbers and booleans have been invisible there since that tree shipped,
and XML attribute names inherited it. Both now use --favorite, the palette's
legible amber on every ground. check:themes does not cover this pairing, so
it was caught by eye.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(tools)!: fold Find & Replace into the Lines tool

Lines already did a literal per-line find/replace, so the standalone Replace
tool was a second, weaker copy of the same idea. Lines now runs the real
thing through the shared replaceText(): Text / Word / Regex modes, an
Ignore-case toggle, `$1` capture groups in regex mode, a count of what was
replaced, and a compile error surfaced inline instead of input passing
through untouched.

Replace runs per line, which is what makes `^`/`$` anchors useful here and
keeps it composable with split/sort/dedupe/wrap. The one thing it cannot
express is a pattern meant to span newlines — noted in the util.

The standalone dialog is gone (utils/findReplace.js stays; Lines is its
caller now). Cmd/Ctrl+Shift+R was freed by that removal and now opens Lines,
so the old muscle memory still lands where replace lives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(quick-look): restore ← out of a tool panel, and scroll the results list

Two keyboard bugs in the launcher:

← no longer left a tool panel. That is a regression from the panel-only
rework: the ArrowLeft handler lived on the input/output textareas, so
deleting them took the only way back to the list with it (Escape and the
back button still worked, but ← is the mirror of the → that entered). The
guard is now useCaretBackOut — a composable with its own unit tests, because
this is exactly the kind of event logic that regresses invisibly when a
component is refactored. It backs out only when the caret has nowhere left
to go: inside a field with a caret past 0, a live selection, or a modifier
held, the key still belongs to the field.

Arrowing down never scrolled the results list. useQuickLook had no
scrollIntoView at all — the command palette has one, the launcher was simply
missing it, so the selection walked off the bottom and out of sight. It got
noticeable once the Tools section made the list long. The snippet preview
was never affected; usePreviewLines scrolls that by exact overflow.

Both covered by e2e: ← held by a mid-field caret and honoured at its start,
and the list scrolling once the selection passes the fold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(tools): delete the dead text-buffer path and sweep the comments

Every TEXT_TOOLS entry became a panel, so TextToolDialog's Monaco branch had
no reachable user — and neither did the fields feeding it. Removing it takes
out a whole layer:

- TextToolDialog is now the dialog frame plus a panel: no Monaco buffer, no
  status line, no Format/Copy/Add-to-Snippets actions, no file drop.
- utils/textTools.js is gone. Once validate/format/language/validLabel/
  requiresValid/actionLabel went, each entry was {title, panel} with panel
  always equal to its key — the registry already carries the name, so the
  dialog reads that and switches on the id. One list, not two. The Epoch
  dialog is titled "Epoch" now, matching the shelf and palette.
- utils/devTools.js is gone entirely: the panels use utils/jwt, epoch and url,
  so nothing referenced it. uuid.js loses convertUuid/validateUuid and the
  three helpers only they used; the TextTool typedef goes too.

Comment sweep across the tools work, per the standing rule: the multi-line
headers narrating what each panel does are gone (the template and imports
already say it), leaving only the terse "why" lines — the DOMParser safety
note, the per-line replace limitation, the self-import needed for recursive
components, the --favorite token gotcha, and the pseudo-element section
labels that keep keyboard indices 1:1 with rows.

CLAUDE.md's "new tool" instruction pointed at the deleted file; it now
describes the registry + panel + case, and records that no validate path
remains to fall back on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(tools): keyboard-routine e2e, and focus the panel's first field

Audited all eight tools across all fourteen themes with an instrumented
pass rather than by eye — measuring text contrast against each element's own
resolved background, horizontal overflow, and vertical centring of controls
sharing a row. Nothing was off: zero findings.

Worth recording how that number was reached, because the first run reported
51 problems and every one was a lie. It sampled colours 30ms after switching
theme, mid-`transition: color 0.12s`, so it read values partway between the
old and new palette — `.seg-opt.on` "failing" at 1.08:1 when its
--accent/--text-on-accent pairing is ~7.6:1. Disabling transitions before
measuring cleared all 51. The instrument was then validated against the bug
that did ship: re-applying --warning-text to .xt-key reads 1.07:1 versus
6.85:1 healthy, well under the 2.2 threshold, so a real regression of that
class would be caught.

Driving the routine on the keyboard alone did surface one wart: a tool
dialog opened with nothing focused, so the first Tab landed on Close and you
could not paste without reaching for the mouse. The panel's first field is
now focused on open, matching what the launcher panel already did.

The new spec covers what click-driven tests walk past: every tool opened by
typing in the palette and closed with Escape, the caret landing in the first
field, Tab never escaping the dialog, and the launcher's full arrow chain
(list → Tools → expand → panel → ← → navigation resumes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tools): palette duplicates, section spacing, panel scrolling, tag copy

Four things the screenshots caught:

The palette listed recent tools twice — once under Recent, then again in
"All tools" — so URL, Encrypt and Patch each appeared in the same view. The
second section now holds only what Recent does not, and is labelled "Other
tools" to stay honest; with no recents it is still one "All tools" list.

The section label had a single pixel of clearance and read as if it were
touching the row above it. Its row now reserves a label's height plus a gap.

A tall panel could not be scrolled from the keyboard in the launcher. The
window is short, the Lines panel overflows it, and since the caret starts in
the first textarea every ArrowDown was swallowed by the field — the result at
the bottom was mouse-only. usePanelScroll takes the key once the field has no
caret left to give (a textarea keeps it while it can still move; an input,
button or checkbox never can), and handles PageUp/PageDown. A composable with
unit tests, like the ← guard beside it.

"Default" was the placeholder for a snippet or diff carrying no tags, in a
span already classed `untagged`. It reads as a tag named Default; it now says
Untagged.

Also drops the .qc-field/.qc-in/.qc-out/.qc-err rules the panel-only refactor
orphaned, and covers the launcher scroll in the keyboard spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(quick-look): put Tools at the top of the list

The Tools section sat after every snippet and saved diff, so on a large
library reaching a tool meant scrolling to the bottom of the list. It now
leads the list. Collapsed — which is its default — that costs exactly one
row, and → expands it in place without moving anything else.

The seam that separated the two groups moves with it: it now sits under the
tools block, above the first snippet. A tool-matching query still opens the
section and lands the selection on the first tool, now at index 1.

Also retires the last "convert" wording in the footer hint: → on a tool row
reads "open tool", since these tools format and generate as well as convert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(quick-look): main window could surface without keyboard focus

Dismissing the launcher on macOS makes the main window non-focusable for
300ms so the OS hands focus back to whatever you were in, rather than raising
DiffBro. That is a timed state nothing else knew about, so any code that
focused the main window inside that window silently did nothing — the window
came up, but not as the key window, and you had to click it.

Three paths could land in it: choosing a launcher result (openInMain),
relaunching the app (second-instance), and re-activating from the dock. All
three now clear the state first through allowMainFocus(), which also cancels
the pending timer instead of racing it, so repeated summon/dismiss cycles
cannot leave overlapping timers fighting over focusability. The timer is
guarded against a destroyed window on quit.

Verified on Linux only: this whole branch is darwin-gated, so the Docker
e2e cannot reach it — the launcher and smoke suites confirm nothing else
regressed, and the focus behaviour itself needs a manual check on macOS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ui): format tiles open a filtered picker; tooltips; scoped preview

- The empty-state format tiles are buttons now: each opens the file picker
  filtered to that format, into the first free side. The renderer names a
  format and main resolves it to dialog filters (src/main/fileFilters.js), so
  it can never supply a filter of its own. Every dialog keeps "All files",
  since everything non-.xlsx still diffs as text. The filter lookup uses
  hasOwn — a test for 'constructor' caught it resolving up the prototype
  chain and handing the dialog a function.

- Mini icon buttons show a hover tooltip. The tool-panel copy buttons had
  only aria-label, which names them for a screen reader but shows nothing on
  hover; the sidebar glyphs had tooltips that were whole sentences.

- The snippet preview opens from the title only. Hovering anywhere on the row
  popped the card while you were reaching for its buttons.

- The palette's first section label had less room than its own height, so
  "Recent" clipped against the search bar.

Fixes a regression from the tooltip pass in the same change: an icon-only
button takes its accessible name from `title`, so shortening those labels
renamed the buttons — six e2e specs that locate them by name failed, and the
screen-reader name degraded to "New". Short text stays in `title`; the full
name moves to `aria-label`.

Each fix is covered in e2e/ui-affordances.spec.mjs so none of them can come
back quietly: the tiles are buttons, icon buttons keep both labels, the copy
button has a title, the preview ignores the row, and the section label has
clearance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ui): real tooltips, saved-diff hints, symmetric shelf padding

Icon buttons had `title`, which the OS draws — the app cannot see, style or
time it, and in this window it often never appeared. AppTooltip renders one
bubble in the page instead, opted into with data-tip and teleported to body.

That teleport matters: left inside the app tree the bubble had a box and
passed a visibility check while never being painted, because a parent
stacking context capped it. The test now asserts it escaped the tree, since
toBeVisible() alone would have gone on passing.

Converted the icon buttons in the tool panels, the snippet rows, the sidebar
section headers and the saved-diff rows. Where `title` had been the only
label, aria-label now carries the full name so the accessible name is
unchanged — and two e2e locators that reached for `getByTitle` move to the
role/name they should have used.

Also makes the tools shelf padding symmetric: 6px above and 10px below read
as a misaligned band once the chips wrapped to a second row.

Every fix here was written test-first, red before green, per the workflow now
at the top of CLAUDE.md. The four regression tests from the previous commit
were retro-validated the same way — each was re-run against a reverted fix and
confirmed to fail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(quick-look): a tool panel keeps its input when you leave it

Backing out of a tool destroyed the panel, so an accidental Escape threw away
whatever you had typed. The panel is now mounted once and hidden with v-show
rather than removed, and each tool keeps its own state.

KeepAlive was the obvious route and does not work here: Vue's compiler cannot
generate a KeepAlive whose only child is a v-if with no v-else, and it crashes
the build. Mounting once and toggling visibility avoids that entirely.

A hidden panel is not remounted, so it no longer gets focus from onMounted — a
`visible` prop tells it when it is shown again and it re-takes the caret.

Extracting the result rows into QuickLookResults keeps QuickLook.vue inside the
120-line template cap that this change pushed it past; the scroll-into-view
watcher moves with the rows, so the child owns that behaviour outright.

Both behaviours were written test-first and seen failing: the input came back
empty ("" instead of "keep me") and the mode reset to Encode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(paste): copied files compare as files, not as their names

Copying files in a file manager puts a file list on the clipboard AND a
plain-text flavour holding just their names. Paste-to-compare read only the
text, so two copied CSVs pasted "mock_products.csv\nstatement.csv" into the
editor instead of opening them.

Paste now asks for files first: two become the two sides, one fills the free
side, and a clipboard holding only text falls through to the existing paste
flow untouched.

The flavours differ per platform — text/uri-list on X11, an XML plist under
NSFilenamesPboardType on macOS, with public.file-url and FileNameW as
single-file fallbacks. Parsing is a pure module so all four are unit-tested,
including the cases that must NOT yield a file: a copied https:// link, a
file:// URL pointing at another host, and a relative plist entry.

Paths off the clipboard are untrusted, so they take the same route as a
dialog pick — resolved, refused under userData, stat'd as a real file, and
run through allow() before anything reads them (rule 6).

Written test-first: the e2e failed with the slots still reading
"left file… ⇄ right file…", and the plain-text case passed throughout, which
is what proves the existing behaviour survived.

Only the X11 path is exercised end to end — the Docker e2e is Linux, so the
macOS and Windows flavours rest on their unit tests and need a manual check
on those platforms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(paste): copied files load without the paste-text prompt first

The file check ran after the confirm, so Cmd+V on copied files still opened
"Paste detected" and only loaded them once you answered it — a prompt about
entering paste mode, which is not what you asked for. It now runs before:
copied files load straight away, and the prompt is left to pasted TEXT, where
switching into paste mode is a real decision.

My own e2e hid this. It clicked the confirm dialog if it happened to be
there, so the flow passed while still showing a prompt it should never have
shown. It now asserts the dialog stays hidden, which is what failed before
this fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(paste): read the macOS pasteboard flavour that actually holds the files

Copied files still did nothing on macOS. Three separate reasons, all found by
dumping a real pasteboard rather than guessing again:

- availableFormats() never mentions NSFilenamesPboardType, so gating reads on
  that list meant never reading the files at all.
- It DOES list text/uri-list — and hands back zero bytes for it. Returning at
  the first format merely present threw the files away.
- NSFilenamesPboardType is a BINARY plist, not the XML one the parser expected,
  and it is the only flavour carrying every copied file. The Apple URL flavour
  holds just the first.

Sources are now tried by content, in order, and the first that actually yields
paths wins. pathsFromBinaryPlist walks the plist's string objects — ASCII and
UTF-16BE, inline or trailing-int lengths — which is far less than decoding the
format but enough for a list of paths.

Tested against bytes captured from a real macOS pasteboard, including the two
traps above, and verified end to end on macOS: two files copied, Cmd+V, no
paste prompt, both slots loaded. The Linux uri-list path is unchanged and
still covered by its own e2e.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(paste): handle the Windows clipboard flavours for copied files

Explorer copies files as CF_HDROP — a 20-byte DROPFILES header whose first
field is the offset to a double-null-terminated path list, and whose last says
whether those paths are UTF-16. Only the single-path FileNameW fallback was
handled, so a multi-file copy had nothing to read. Adds pathsFromHdrop, and
tries text/uri-list through read() as well as readBuffer(), since newer
Chromium exposes CF_HDROP under that name and Electron serves some flavours
through only one of the two.

Writing the tests turned up a bug that would have broken Windows on its own:
file:///C:/x parses to "/C:/x", and that leading slash makes the path
unopenable. The drive letter now leads.

NOT verified on a real Windows machine — I have no Windows here. The parsers
are unit-tested against accurate CF_HDROP buffers (wide, ANSI, UNC, unicode,
truncated), but whether Electron hands us those bytes under the name
"CF_HDROP" is untested: it is a predefined Windows format rather than a
registered one, and readBuffer is marked experimental. FileNameW is a
registered name, so single-file paste is the likelier of the two to work.
Sources are tried in order and fall through, so an unreadable flavour costs
nothing but a miss.

macOS re-verified end to end after the change; the Linux e2e still passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(snippets): URL snippet type, opened in the browser with one click

A snippet whose language is "url" is a saved link: the row offers an Open
button that hands it to the OS browser.

Opening is fenced the way the claude.ai links already were — validated in the
MAIN process and confirmed by the user before anything reaches the OS. The
fence is the SCHEME: only http(s) passes, because openExternal will otherwise
open a local file, run a script handler, or launch another application from a
crafted string. That check is a pure policy function with its own tests
(javascript:, file:, data:, vbscript:, smb:, ms-msdt: and friends all refused,
along with junk and absurd lengths).

URL snippets never travel. _bundle leaves them out of an export and
restoreBundle drops them from an import, so a link can only ever be one typed
on this machine — a shared bundle cannot carry one for someone else to click.
Both halves are tested, and both failed before the filters existed.

Also on the saved-diff rows, which the same pass touched: delete uses the
trash icon the rest of the app uses, the "kept" label is gone (it said nothing
a blank does not), and the actions only appear on hover — the resting row now
gives its width to the diff name, matching the snippet rows. The sharing e2e
hovers first, as a user must.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: correct the openExternal rule to name both call sites

The rule added with the URL snippet said openExternal was reachable "only
through links.js". The audit it was meant to support found a third call site
it had missed: menu.js opens the issue tracker. That one is safe for a
different reason — the URL is hardcoded in main and the renderer cannot
influence it — but a rule that names the wrong set is worse than none, since
the next reader trusts it instead of grepping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant