Skip to content

Add a Sticky Note widget with in-place rich text - #15

Open
jondkinney wants to merge 89 commits into
kemalandic:mainfrom
jondkinney:pr7-sticky-note
Open

Add a Sticky Note widget with in-place rich text#15
jondkinney wants to merge 89 commits into
kemalandic:mainfrom
jondkinney:pr7-sticky-note

Conversation

@jondkinney

Copy link
Copy Markdown

Stack 7 of 7 — builds on #14

Incremental diff (just this PR's changes): jondkinney/edgecontrol@pr6-reminders-widget...pr7-sticky-note

What this does

Adds a Sticky Note widget edited in place on the dashboard — a real rich-text surface, not a plain-text box, and with no edit/view mode toggle: you just type.

Markdown-lite typing. Headings, bullets, numbered lists and [ ] checkboxes form as you type, with the conversions deferred so they never fight the caret. Ordered lists renumber themselves; list text aligns in one column with a tight marker gutter; Enter on an empty marker ends the list.

Checkboxes are custom-drawn, sized above the body text while keeping the caret body-sized next to them, and toggle by click/tap or ⌘Return — preserving the line's indent level.

Links: paste a URL over a selection to link it; Enter inside a link opens it.

Appearance is configurable per note — font family and size, color, and opacity — with defaults chosen to read well on a dim kiosk display (18pt mono, soft white, 0.5 opacity).

An Edit menu is added to the app so the standard editing shortcuts work at all — the kiosk previously had no menu to host them.

Verification

** TEST SUCCEEDED ** — 90 tests, 0 failures. The editor was exercised by hand across list/checkbox/link transitions, since it is NSTextView behavior that unit tests do not usefully cover.

Note on scope

This is the largest PR in the stack (28 commits) and is almost entirely confined to StickyNoteWidget.swift. It is last in the stack precisely because it is the most self-contained — dropping it costs you nothing else.


Targets main per GitHub's base-branch rule; until #9#14 merge this diff also contains their commits.

jondkinney and others added 30 commits August 16, 2026 19:47
Xcode 16.4's compiler rejects several spots that earlier
toolchains accepted under the pinned Swift 6 language mode:

- PluginWidgetEntry declares Sendable while storing an NSImage;
  mark the image nonisolated(unsafe), since entries are built and
  consumed on the main actor.
- The analog clock's inline tick-mark arithmetic exceeds the type
  checker's expression time limit; extract a helper with explicit
  CGFloat conversions.
- The edit-mode toggle mutates main-actor state from a @sendable
  zone action; hop to the main actor explicitly.
- The plugin notify path sends non-Sendable UNNotificationSettings
  across isolation; extract just the authorization status via the
  completion-handler API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 41a6d72ed5d0e22531abe12f9f9492938ae33e26)
The test target did not compile, so the suite had never run. Two
distinct isolation errors: tests calling the main-actor-isolated
CICDSettingsView.quotaText from nonisolated test methods, and
LayoutEngineTests building main-actor state inside setUpWithError /
tearDownWithError, whose XCTest signatures are nonisolated even on a
@mainactor test case.

Annotate the two quota tests @mainactor, and move the layout fixtures
to the async set-up hooks, which do inherit the class's isolation.
The super calls go with them: awaiting the non-Sendable superclass
across the actor hop is itself an error, and XCTest already invokes
the empty base implementations.

All 87 tests now build and pass.
One commit can fan out to several workflows, and some runs share
a constant display title ("pages build and deployment"), so rows
showing only repository and title render as indistinguishable
duplicates. Append the workflow name, dimmed like the host label,
so such rows tell apart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit e7f7a7b6afb6d01500d47d4344f86be4c86ee27f)
Repeat runs of one workflow (re-runs, successive Pages deploys
with their constant title) rendered as indistinguishable rows.
The widget reads better as a status board than an activity log:
show each repository workflow once with its most recent run's
result, add a compact age label to every row, and make the
header badge count the workflows actually listed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 209fb62b24a5af417b71e643fdb7e9b12488ba7d)
Hiding a repository required typing its exact owner/name into
the Hidden list. The settings pane now also offers a menu of the
repositories the widget is showing right now; picking one adds
it to the hidden set, which the service already persists and
excludes from polling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit adb1fe21b145afc7bb01b31bd284c8c9dad0a9f6)
Disk I/O, Storage, Network Stats, WiFi Info, Audio and Day
Progress all render single-row content in their compact layouts
yet declared 2-row minimums, wasting half their footprint on a
6-row strip display. Lower the minimums to height 1; give
Network Stats a side-by-side variant at that height (its stacked
rows could overflow the cell), keep Day Progress's ring out of
1-row placements, and clip widget content to its cell so a
compact layout can never paint over neighbors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 1bf2297022cb1f8da96d9698a5fa4649b2d29364)
An audit of every size-adaptive widget, aimed at fitting more on
a 6-row strip without premature degradation:

- Radial gauges draw their type label inside the ring instead of
  a caption row below, so the ring fills the cell and a 2x2
  placement works; temp widgets keep the gauge at 2x2 rather
  than degrading to icon+number.
- Top Processes keeps its MEM column at every height (it was
  keyed to height though columns are a width concern) and fills
  available rows from measured height instead of a fixed 4/8,
  with the process service cap raised from 5 to 12.
- Storage keeps its ring layout down to 2 rows when wide, sizes
  the ring to the cell instead of a 140px cap, and centers
  vertically; Disk I/O and Day Progress center instead of
  floating above dead space.
- Per-core temp and CPU cores pick column count from height as
  well as width, so tall placements fill and short ones avoid
  needless scrolling.
- Weather's compact layout was unreachable (threshold below the
  minimum size); it now serves 4-row placements. World Clocks
  distributes cards across the available height.
- Bluetooth's device list scrolls instead of clipping; WiFi and
  Network Stats keep their info chips in compact when width
  allows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit ab8b8ccb7e09489f050d5c6f1cb6943dfdfe9d1b)
Small sizes were shedding the text that says what a widget is:
gauges dropped their subtitle ("M4 MAX", "96.3 / 128 GB") below
150px, Disk I/O lost its header in compact and 1-row layouts,
and Storage fell back to an anonymous bar at 3x2 although its
ring layout scales to the cell. Gauge subtitles now render at
every size, Disk I/O keeps its name down to 1 row when width
allows, Storage keeps the ring at every multi-row size, and the
Network 1-row layout gains DOWN/UP labels matching Disk I/O.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 61caf4945c6c6734f587e820934cecc50f98cefe)
Network and Disk I/O both show a pair of labeled rates, yet at
identical box sizes one stacked top-aligned rows over dead space
while the other centered side-by-side groups. Extract
RatePairView and render both widgets through it, so widgets of
the same shape look the same at every size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit f96ec803d4ae7c0ca280abd144c6f139da439922)
The Unit and Sort By pickers rendered blank and would not open:
their schema entries declared no options, and nothing consumed
their values. Unit duplicated the global Units setting, so the
dead entries are removed. Sort By is now real (CPU or memory)
and Top Processes gains a Rows choice — auto fills the widget
height as before, a number pins the count and scrolls past it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit a925501e118714a354a3b57e7ceef1102f2a2daa)
Network now keeps its name at the same sizes and spot Disk I/O
does (full header to 2 rows, caption at 1 row). The Day Bar
clock's day-of-week strip was 9-11pt beside a double-height time
row; raise it to 12-15pt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 0501f5c712a855dbca29ff870a70f50334fca5c8)
A full-height 6-row placement fits 16 rows of 43px, but both the
service's published list and the widget's auto clamp stopped at
12, stranding a four-row strip at the bottom. Raise both, and
offer 16 in the Rows picker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 67d97a471e6d4a37f095817c7a0a1a8a3554b3b7)
The bar layout skipped the leading spacer Disk I/O has, pinning
the rate pair under the caption instead of centering it in the
card. Same spacers now, so the two widgets align row for row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 7b0eeada2af4dae10640c206347389a6927613f6)
The time row rendered at a fixed multiple of the theme font, so
it never matched the day strip's width. It is now one
concatenated Text (colored colons, dimmed seconds and AM/PM
scale as a single unit) at a deliberately oversized base size
that minimumScaleFactor shrinks to exactly fill the container —
the SwiftUI way to grow type to fit. Day Bar spacing opens up a
step to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 5b8f184e24deb106393a21c75d32292ea90ab219)
Day cells were equal-width columns with the highlight capsule
filling the whole cell, so the strip's visible edges depended on
which day was highlighted — inset the clock to match the labels
and a Saturday capsule would overhang it. Chips now hug their
labels and justify across the row: the first and last chip sit
flush against the container edges the clock also fills, so the
two rows share edges on every day of the week.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit d1b16e620fdf4195850f631f678dee993bd1d20b)
Replace the edge-justified distribution with a centered cluster
at fixed chip spacing, sitting over the width-filling clock with
the existing row spacing between them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 2a223d70e6859fa9d6edf2f74cf4de3eed944199)
Restore the justified day strip (the width was right) and drop
the time row's greedy height, which was pinning the days to the
top edge. The strip-plus-clock group now hugs its content and
the container centers it vertically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 7b112a03820350a489c0cac58509a3651a107d9b)
Values like "3.5 KB/s" and "216.5 KB/s" measured differently, so
minimumScaleFactor re-picked the scale on every tick and the
type visibly pulsed with live data. Pad every value to the
formatters' widest possible output (11 figures) with figure
spaces: constant measured width, constant rendered size, sized
for the worst realistic reading instead of the current one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 94be637b1d46bdfdfbd938633297537d3a26fa62)
CPU and MEM headers are live sort buttons (touch and mouse): tap
to sort by that column, tap again to flip direction, with the
active column showing its arrow. The configured Sort By remains
the default the widget opens with.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 60b43b0504bbfc1da2dc941581796ee76aaf76dd)
The caption was gated on width >= 3, so a 1x2 placement showed
an unlabeled pair of numbers. One small-caps line fits a
2-column cell; show it at every width.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 3ff6ea5d3316145b3922a6476f96c0b183580090)
RatePairView gains a vertical mode — groups stacked instead of
side by side — used when either widget is one column wide, under
a slim caption title (the full header can't fit ~120px). Minimum
sizes open to 1x1 accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit a6f1f75e1a4d420b4f6fd07b2659ce8f9d094f7a)
Rearranging on a full page meant shrinking or removing widgets
just to open a gap, and adding from the catalog silently failed
with nowhere to place. Edit mode now treats overlap as a staging
state: drags, resizes and catalog adds land even on occupied
cells (a full page parks the new widget at the origin and opens
an edit session), with orange borders and drop previews marking
staged collisions.

The no-overlap invariant moves to the session boundary: store
writes pause during editing, edit mode refuses to end while
overlaps remain (jumping to the first offending page), and the
quit-time flush declines an overlapping document — so an
overlapping layout can never be persisted. The tradeoff is that
a crash mid-edit reverts to the pre-session layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit ef8ee1e4840f14baaf7866bae2e5381b5c9d60e2)
The model has carried a persisted page order and a tested
movePage all along; no UI ever called it. Add Move Up/Down to
the page row's context menu. movePage now normalizes by the
stored order first (it assumed array position matched, which an
imported document can break) and keeps the currently visible
page on screen by following its id across the reorder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 414586669912612e5e014a0a327cf9a1056f7023)
The page transition hardcoded its edges, so swiping back from
page 3 to 2 played the same forward animation as 2 to 3. Track
direction in a willSet on currentPageIndex — every navigation
path (touch swipe, drag, page dots, page manager) assigns it, so
one hook covers them all — and pick the transition edges from
it. The flag is deliberately not published: it must hold steady
through the body pass the index change triggers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 53ec7fb6fd56aded2090ec707e3cd2850921198f)
- Export Layout encoded the on-disk document, which now lags the
  screen for a whole edit session; export the live document.
- Entering an edit session flushes the clean pre-session state,
  so a write debounced moments earlier can't be swallowed by the
  session's write suppression.
- Plugins were promised at most 5 processes; keep that contract
  though the widget now shows up to 12.
- The page manager's steppers can stage collisions during an
  edit session with no grid in sight; flag overlapped rows and
  say what to do in words.
- TouchScrollView clamps its offset when content shrinks, so a
  collapsed list can't stay pinned past its own end.
- Page transitions: SwiftUI animates a removed view with the
  transition recorded at its last committed render, so the old
  willSet-based direction flag left the outgoing page sliding
  the wrong way on every direction change. All navigation now
  routes through navigate(to:), which commits the direction in
  its own body pass before moving the index.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit c79e9540b19350b0dcb53636eb05eaa899a69be9)
With overlaps staged, the top widget's controls eclipsed the one
beneath — there was no way to reach the covered widget's resize
handle. Tapping (or starting a drag on) a widget now selects it:
it renders above staged overlaps with an accent border, so the
selected widget's handles are always the reachable ones.
Selection clears when edit mode ends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 4aefbeea7a541a5ee4a298c81ac2ad9b586be5d7)
Page changes animated by view transitions resolved the outgoing
page's edge from its previous render, so backward swipes nudged
forward before sliding back — and the discrete swipe pipeline
(classify on release, hop through two observers) made every
change feel detached and late.

Pages now live in one wide HStack offset by the current index
plus live finger travel: the touch service publishes horizontal
drag distance continuously, the shell tracks it 1:1 (with
one-third-rate rubber-banding past the ends) and settles in a
single spring transaction on release — page turn or spring-back.
Direction is pure geometry, so the transition machinery and the
double-navigation path through AppModel are gone. Only the
current page and its neighbors render real content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 058adba513c3e55d374dc215ac8740fe5a11f065)
Widgets without their own tap behavior become launchers: tapping
Top Processes opens Activity Monitor, Storage opens DaisyDisk
(falling back to Finder when it isn't installed), the clock
opens Calendar, and every widget's settings gain an "Opens on
Tap" field to pick any app or clear it. Widget-internal zones
are smaller and win hit-tests, so interactive widgets keep their
behavior; CI/CD is excluded since its rows already open runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 264c4fde39056fd6b5262515e457e58b8b6aed4b)
The tap-to-launch modifier sat after .position, which wraps the
card in a page-sized container; its contentShape made that whole
invisible wrapper clickable, so the topmost widget intercepted
every click on the page — including other widgets' buttons.
Attach it to the sized card before offset/position, so the tap
area (and the registered touch zone) is the card itself.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit ef9ea11223909e80281e0404799a90cfca3bef89)
body(size:config:) had no way to know which placement it renders
for, so no widget could persist its own state. The page and
instance ids now ride along in the config under underscore keys,
letting a widget hand edits back to the layout engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 4ab7ae7453cfac15edf3c3e9e270c3e271f3e921)
jondkinney and others added 30 commits August 16, 2026 20:05
New Due Time setting (HH:mm) says when a due-today reminder
fires if nothing sooner was given. The compose row gains a small
tab-to field for a remind-in offset — ":30" is thirty minutes,
":03" three, "2" two hours, "2:15" two-fifteen — which overrides
the default. Timed reminders get a real alarm so they notify,
and same-day rows show their time instead of the date. Tapping
the widget's empty area opens the Reminders app by default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit e89df8cebb3ac3aeb74a831c2e671ce8c59af0f9)
The Reminders widget's due time becomes a proper hour/minute
picker instead of a free-text HH:mm field; the value still
persists as an "HH:mm" string so existing configs round-trip.
Schema entries gain an optional help caption rendered under
the control, and the two due-today settings use it to explain
what they actually do: Due Today stamps new reminders with
today's date, and the due time is when they come due and
notify unless a typed "in…" offset overrides it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 4fd451056d9689ea476372a588954174306ff7cc)
Free-form text on a tinted card, typed directly into the widget
and persisted in its placement config via the injected identity,
so it saves with the layout and round-trips through export and
import. The settings field edits the same note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit e6634b294a79521d6aa246e9a7dd6898c77bb628)
A nine-color palette picker and a 0-1 tint opacity slider in the
widget's settings; the color also drives the note's accent. The
default stays the theme's subtle yellow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit f0ff70605fc7019c5c65019580eb357dca51bd43)
Slack-style linking without full markdown: notes store links as
inline [title](url) and render them as tappable titles. In the
editor (macOS 15 selection APIs), pasting a URL over selected
text turns the selection into the link title, and pasting a bare
URL prompts for a name — long URLs never sprawl across the note.
The note now has display and edit modes: rendered text with
clickable links, click (or the pencil) to edit, Done or focus
loss to finish.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit e7ea955d89bc1ae7fab5ee51d627a60d745fd7a4)
onPasteCommand never fires on a focused TextEditor — the
underlying text view consumes Cmd+V itself, so URLs pasted raw.
A local keyDown monitor now sees the keystroke first, takes over
only when the pasteboard holds a URL, and passes every other
paste through to the text view's native handling (which also
retires the hand-rolled plain-paste fallback).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit d7cc92b340f6a39e5dc191bac5092dd96b3365dc)
Cmd+A/C/V/X/Z are menu key equivalents on macOS; with no Edit
menu they reached no text field anywhere in the app — the sticky
note, the reminders field, every settings input. Standard Edit
items dispatched down the responder chain fix all of them, and
plain-text paste in the sticky editor starts working (the URL
monitor intercepts before menu dispatch, so link pastes are
unaffected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit d42c737e95488f5b5318f7aa860446903c82c24b)
The display/edit split is gone: the note is an always-editable
NSTextView where links render inline as styled clickable titles.
Pasting a URL over selected text linkifies the selection in
place; a bare-URL paste prompts for a title. Storage is
unchanged — links serialize to [title](url) — so existing notes,
export/import, and the settings field all keep working. Saves
are debounced 800ms: the previous per-keystroke document write
re-rendered the whole dashboard per character, which was the
typing sluggishness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 12b53177e6168ecb2c5b9300930787205601ea94)
The note becomes a converting editor: "- " turns into a bullet,
"- [ ] " into a clickable glyph checkbox, "# "/"## "/"### " into
sized headings that end at the line break, and a "---" line into
a dim horizontal rule — conversions happen at the keystroke and
the note is rich text from then on, never markdown. Lists
continue on return. A new Format menu wires Bold, Italic,
Underline (standard machinery, which no menu ever exposed, so
none of them worked anywhere) and a custom Strikethrough
(Cmd+Shift+X). Font family and size become widget settings,
reapplied to existing text preserving traits and heading scale.
Storage graduates to RTF so all formatting persists, with the
plain-text mirror kept in "note" for the settings field and
pre-RTF notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 73fd7cbe329d73338471a1ada7a48d4242ad1410)
Typing "- " converts to a bullet before "[ ]" can be finished,
so the checkbox shorthand was unreachable. The checkbox cases
now chain off the fresh bullet: typing "- [ ]" naturally passes
through the bullet stage and completes as a checkbox on the
next space.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 1e62bf11a0311d3b85ddd8431f6c96030bae41ba)
Checkbox glyphs toggle on click, so hovering them should read as
a control: apply the hover-only .cursor text attribute (the same
mechanism link hover uses) to every checkbox after edits and
loads. It never serializes into the stored RTF.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit bc1241335b9f7912166efd7b5bae2b9c1be79965)
Converting as early as "[" made a pre-checked todo untypeable:
"- [ " became an empty checkbox before the x could follow. The
conversion now waits for the closing bracket, so "- [ ]",
"- []", "- [x]" and "- [ x]" all type through naturally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit c6e3329692331973ca2b3773254e3a18165ac68a)
The dash converted on its trailing space, creating a confusing
interstitial bullet and stealing the bracket that starts a
checkbox. It now converts on the first following character, with
"[" excluded, so bullets appear exactly when intent is clear and
checkbox typing never detours. And pressing return on a bare
bullet or checkbox line deletes the marker — the standard way
out of a list — instead of continuing it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 2d02960179ce8362ea4550ac4ac2baabf3f07e33)
Return with the cursor strictly inside a link's text opens the
link instead of breaking the line — at the link's edges it still
inserts a newline so writing continues normally. Checkbox glyphs
render at twice the body size: they're tap targets, not
characters, and get restyled by the same pass that gives them
the pointer cursor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 02babcec6674a836e0b57a6e72685fe5bc2e16da)
The caret spans its line, so the 2x glyph inflated the insertion
cursor with it. Render the glyph at 1.5x and clamp the line to
body height via paragraph style (the box's ink fits), with a
small baseline settle — big tap target, normal caret.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 6b4084f95080ccd134b8b01b6880243b94f960a6)
Caret height follows line height and line height follows the
tallest glyph, so the enlarged checkbox either grew the caret
(2x attempt) or squashed the whole list line (line-clamp
attempt). Body-size glyphs with just the pointer cursor are the
stable point; the styling pass now also strips the oversized
font, paragraph clamp and baseline offset from notes saved
during the experiments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 484e667664deb9727f02d70da7cfa3368343a602)
The glyph checkboxes are replaced in the view by drawn image
attachments: a rounded stroke square, filled with the note's
accent color plus a contrast-aware checkmark when done, sized
against the body font and centered on the cap height — precise
control over footprint, so the caret and line stay normal.
Storage still carries the ☐/☑ characters (serialization maps
attachments back to glyphs), keeping RTF, the plain mirror and
older notes compatible; any glyph that appears via load or
paste becomes a drawn box on the spot. Clicking toggles the
attachment; continuation and list-exit understand attachment
lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 6baf1cbe18f471153713332ce36b12f71f63cbc7)
Emptying the note left the typing attributes carrying whatever
font was deleted — with no neighboring character to inherit
from, a deleted heading made the empty note type headings
forever, with no way out. An empty note now resets the pen to
body, and Format > Body Text (Cmd+0) strips heading size,
bold/italic, underline and strikethrough from the selection or
current line while keeping links.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 70a61cfb9b2c5fca06f374f5fac1c951abac600f)
Paragraph rhythm via the normalization pass: body, bullet and
checkbox lines share a base spacing (0.22em after each), while
heading paragraphs get more, especially above (0.5em before,
0.3em after). Detected by font size, so loaded and pasted
content is spaced too; Body Text resets a line to body rhythm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit ded2ca95795467b04eb00bd71fc4aa62f63d3f94)
Typing "-" or "*" right after a fresh checkbox switches the item
to a bullet — changing your mind shouldn't need deleting. "*"
now starts bullets exactly like "-". Format gains Bigger and
Smaller (Cmd+=/Cmd+-) which adjust the note's persistent Font
Size setting, reflowing all text through the existing
family/size machinery so headings keep their proportions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 035b57533d8a10165236e83e495398925a8d274d)
On the size-change pass the stale-content check compared against
a binding SwiftUI hadn't accepted the mid-update write into, so
the freshly scaled text was reloaded from the old RTF while the
checkbox images regenerated at the new size — boxes grew, text
didn't. The reload now skips the font-change pass, the scaled
content pushes on the next runloop tick, and checkbox images
redraw explicitly with the reflow. Cmd+0 becomes Default Size
(snapping the note back to the settings default of 13); Body
Text moves to Cmd+Shift+0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit f6709d3ca974882a29ad97468a60dbdbe2f837ba)
List lines become marker + tab + text with a paragraph tab stop
and hanging indent: bullets, checkboxes and numerals of any
width share one text column, and wrapped lines align under the
text. Typing "1. " (any number) starts an ordered item; return
continues with the next number, and the usual empty-marker exit
applies. Older notes' no-break-space markers migrate to the tab
column automatically during normalization.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit bb182afa158b707c4e47502b5c3881955bf86197)
Typing another list trigger right after an existing marker now
swaps the item's type in place: "1. " on a bullet makes it
numbered, "[ ]"/"[x]" (closing bracket then space) makes any
item a checkbox, "-"/"*" makes it a bullet, and "# " turns it
into a heading. Tab and Shift-Tab indent and unindent the
caret's line (or all selected lines) from anywhere within them,
up to six levels; the level lives in the paragraph geometry so
it survives saves and re-normalization, and Enter continues a
list at the same depth.

Alignment: the text column tightens from 2.1em to 1.8em, and
the narrow bullet is inset to sit centered under the checkbox
column instead of hugging the left edge. All paragraph styles
now come from one geometry builder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 619435658fcbb1a91f0dd30c9ce65917347e7b06)
A renumbering pass runs with every normalization: numbered
items in a contiguous list block follow the previous number at
their indent level, so pressing Enter mid-list, deleting an
item, or converting one to a bullet renumbers the rest.
Bullets and checkboxes between numbers don't break the count;
a blank or plain-text line ends the block and numbering resets.
The block's first number is kept as typed, so lists may start
anywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit c22a46f286523699ecb4eab2d853f65a10c21279)
didChangeText notified observers first and normalized after, so
when renumbering rewrote list numbers the SwiftUI binding had
already captured the pre-renumber text. The next render saw a
mismatch, reloaded the view from that stale RTF, and threw the
caret to the end of the note — pressing Enter mid-list lost
your place. Normalizing before super.didChangeText() means every
observer sees the final text and the reload never triggers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit 57987c4b59ee98deb9372a4b3af43821bd751f14)
"10." was wider than the 1.8em list column, so its tab
overshot to the next tab interval, indenting the text a full
extra column. The column is now sized to fit a two-digit
number and its dot in the note's actual font.

Markers also right-align to a shared edge just before the
text column: number dots line up regardless of digit count,
the checkbox's right side sits on that edge, and the bullet
is centered over the checkbox — every marker stays close to
its text instead of hugging the left edge of a wide column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit bc432c26c84ba7bdb627408a9c25754c1a718fdf)
New sticky notes default to 18pt mono on a half-opacity yellow
tint (existing notes keep their saved settings). Cmd+0 snaps
to the new 18pt default. Marker Felt and Noteworthy set their
letters so tight on the small panel that they blur together, so
those families get 0.08em of tracking, stamped during
normalization so loaded and pasted text is covered too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit e249f97142a53e32adb1b6871841021a668c7579)
The note's text color becomes a config picker (soft white,
white, gray, black, and the accent palette) instead of the
theme's fixed white. The default is soft white — a touch
darker than pure white, easier on the eyes against the tinted
card. Changing it recolors existing text live: every non-link
run takes the new color, dimmed runs like horizontal rules
keep their alpha, links keep their accent, and checkbox
strokes redraw to match. Like the font path, the reload guard
skips the recolor pass so the caret and content survive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit eae50385ec62105f5227e430696d15e89c8c2a4c)
Format > Toggle Checked flips the checkbox of every checkbox
line the caret or selection touches, using the same write path
as clicking the box; non-checkbox lines are left alone.
Cmd+Return follows the "complete the item" convention from
Obsidian and Todoist, and unlike overloading Cmd+X it can't
collide with cut muscle-memory next to a caret.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit adf575b86c8f52dc5fe3843998c40092420986f4)
Toggling replaced the checkbox attachment with a bare one
carrying no paragraph style, so normalization read the line as
indent level 0 and outdented it — Cmd+Return on an indented
todo walked it back to the margin. The replacement now carries
over the character's current paragraph style; both the
keyboard and click/tap paths go through it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BcS3YNvZzHypLREnxBU5sV
(cherry picked from commit cbe4023819de076d42090f0650ff073dd85958f5)
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