Add a Sticky Note widget with in-place rich text - #15
Open
jondkinney wants to merge 89 commits into
Open
Conversation
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)
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 isNSTextViewbehavior 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
mainper GitHub's base-branch rule; until #9–#14 merge this diff also contains their commits.