Commits
Set iso.toolbar.inspector to true so the Isolated Block Editor renders a
settings (gear) button in the toolbar that opens the block inspector as a
popover. This makes block settings such as an image block's alt text
reachable. Other iso defaults (moreMenu, footer, blocks, sidebar) are
left unchanged.
The page documented the community standard.site lexicons field-by-field,
in two hand-authored tables, as if they were SkyPress's own. They're not
(Decision 0005), and spelling out someone else's schema invites drift and
overstates what SkyPress defines. Drop that interop section and its
tables; keep only an honest list of the records publishing writes, with a
pointer out to standard.site.
The one format SkyPress does own, blog.skypress.content.gutenberg, was
described only by its schema, which is abstract about what blocks look
like. Add a worked example built from a real published record (the Potato
buns recipe) showing the repeating name / attributes / innerBlocks shape
and the $type discriminator, so the structure is concrete.
Add a short self-hosting section pointing to the source repo, since the
records living in the reader's own account is the whole point and the
project is meant to be run anywhere.
# Conflicts:
# src/lib/reader/highlight.test.ts
# src/lib/reader/highlight.ts
Rendering the modal inside the eyebrow <p> nested a block-level backdrop in a
paragraph (invalid) and, worse, inherited the eyebrow's text-transform: uppercase,
corrupting the displayed JSON casing. Portal to <body> fixes both and lets the
fixed overlay escape any ancestor overflow/transform. Also pin the value-only
(no uri/cid envelope) decision in the page source test.
The lexicon page printed the blog.skypress.content.gutenberg schema as
flat monospace, while the reader's code blocks already get coloured. The
existing highlighter only exposed highlightCodeBlocks, which auto-detects
the language on rendered wp-block-code HTML — wrong fit for a schema
string we already know is JSON.
Add highlightSource(source, language) to the same module (keeping
highlight.js imported in one place) to tokenise a known-language string
directly, falling back to plain entity-escaped text if the grammar
throws. The page highlights its schema as JSON in frontmatter, so it
stays server-side with no client JS, like the reader.
The lexicon code block sits on a light --paper-raised surface, not the
reader's dark <pre>, so its token palette is built fresh: brand tokens
that flip with the theme for keys/keywords, plus two string/number hues
overridden under prefers-color-scheme: dark. Verified legible in both
light and dark themes.
highlightOne() decoded entities before stripping <br> tags, which
conflated genuine line-separator breaks (stored unescaped) with a
literal <br> typed in a code sample (stored escaped as <br>).
A code block containing an HTML/Markdown <br> example had it silently
turned into a newline. Strip the breaks on the still-escaped source so
only real break tags are affected; add a regression test.
The default social card still showed the old mark. Rebuild it from the
new public/skypress-logo-banner.png so shared links carry the new brand.
The banner is 1200x300 (4:1) — too short for an OpenGraph card, where
platforms expect ~1.91:1 and would letterbox or crop it. Composite it
centered onto a 1200x630 cream canvas (#faf6ef, the --paper token, which
matches the banner background exactly) to produce a seamless, correctly
proportioned og-default.png. Base.astro already serves it at 1200x630, so
no code change is needed.
Swap the hand-drawn type-block mark for the new winged-sun logo across
the favicon and every in-app spot (Logo.astro, AppBar, LoadingScene).
The mark now lives once in src/lib/brand/skypress-mark.ts as inline SVG
whose paths use fill="currentColor", so it follows the surrounding color
(the --sun token, which lightens in dark mode) instead of being locked to
one orange. A single source avoids duplicating the path data across the
Astro/React island boundary. The favicon keeps the static-orange
public/skypress-logo.svg, since a favicon can't inherit currentColor.
This drops the landing-only sun->moon easter-egg: the new logo is a sun
with no moon variant and only one asset was provided. Its night/dusk swap
styles in index.astro and the three tests that locked it are removed.
Commit 96634b6 reformatted Footer.astro to single-quoted attributes (and
dropped the duplicate publication link). The footer's content is intact, but
these source-level regexes hard-coded `href="..."` double quotes and so
broke on the reformat. Accept either quote style so the assertions track the
markup's meaning, not its formatting.
A `*/` inside the comment above `.studio__published` (in `--sun*/--ink`)
closed the comment early, so the trailing prose plus the selector collapsed
into one invalid rule that every CSS parser discards. The publish notice
rendered as an unstyled full-width paragraph instead of the centred sunrise
pill. Reword the comment to drop the literal `*/`.
Add a PostCSS-based regression test that asserts the rule survives parsing.
The .reader__author inline-flex box is sized by the 22px avatar, but
inline boxes default to vertical-align: baseline. That baseline-anchored
the tall author box against the shorter meta text (separators, date,
reading time), leaving the avatar visually misaligned. Aligning the
author box to vertical-align: middle puts the avatar, name, and trailing
meta text on a common centerline.
Two ADRs claimed 0017 from parallel branches both merged to trunk:
0017-render-article-module.md (the render-article module) collided with
0017-known-provider-detection.md (the provider-logos work, merged first).
Provider-logos keeps 0017; the render-article decision moves to 0018.
Rename the file and repoint its five referrers (0007, 0011, 0016,
AGENTS.md rule 6b, and the render-article.ts docblock). Each decision
number now maps to exactly one file.
# Conflicts:
# src/components/Studio.tsx
The PublishPanel submit/publish handlers are async, which arms React's
act-tracking after the click's act() scope closes. A subsequent bare
root.unmount() then flushes that pending state update outside act() and
logs a "not wrapped in act" warning. Pre-flushing before unmount doesn't
help — the unmount's own flushSync has to run inside act().
Wrap root.unmount() in act() in both clickUpdate and clickPublish. This
also clears two equivalent warnings that already existed via clickUpdate
before the publish-success-pill work added clickPublish.
The presence-gate test helper called root.unmount() outside act(), which
made React log "An update to Root inside a test was not wrapped in act(...)"
once per test. Wrapping the unmount in act() silences the warnings and keeps
the test output clean. No assertion or behavior change.
Foreign `site.standard.publication` records (written by Leaflet, pckt,
Offprint, …) now display the originating service's logo next to their
hostname — on the dashboard "From other apps" list and the public
profile "Elsewhere" section, which also gains the hostname it didn't
show before.
The hostname alone can't identify the service, because the paid tiers
serve from a custom domain. So detection is two-step: an app-specific
`$type` discriminator embedded in the record first (pckt writes
`theme.$type === "blog.pckt.theme"`, which survives a custom domain),
then a dot-boundary hostname-suffix fallback (`*.leaflet.pub`,
`*.pckt.blog`, `*.offprint.app`). Leaflet records carry no such
discriminator and Offprint is unsampled, so those two on a custom
domain stay logo-less, exactly as before — see Decision 0017.
Detection and the monochrome glyph data live in one framework-agnostic
module (`lib/publish/providers.ts`), since Astro can't server-render a
React component and the read path takes no client island: the React
dashboard and the Astro profile each render the shared data through a
tiny `ProviderLogo` of their own. Leaflet's saved asset was a raster
PNG, so its glyph is a substitute vector feather (Lucide, ISC).
The signed-in action bar already brings existing reactions to the post via
the Like/Repost/Quote/Reply buttons, so the standalone thread link below the
public-actions note was redundant. The signed-out 'View this post on Bluesky'
affordance is left in place.
The action bar rendered whenever a document carried a bskyPostRef, which
only proves the reference exists — not that the post it points at is still
live. A deleted post left Like/Repost/Quote/Reply buttons and a dead thread
link that couldn't work.
Add a client-side presence gate to the PostActions island: signed-in readers
learn the post is gone from the authenticated getPosts (a null result), and
signed-out readers from a new unauthenticated fetchPostExists() against the
public AppView. The gate is optimistic (shows by default, hides only on a
definitive 'gone') so a transient error never hides a live post. When gone,
the whole block renders nothing — buttons, note, and thread link.
The "+ New publication" button was a prominent, sun-filled primary in the
list header in every state. But once a writer has a publication, their wanted
action is to write (already the top-bar CTA), not to create another.
Make the button state-dependent: keep it prominent in the empty state, where
creating a publication is the point, and render it as a quiet secondary
control below the list once at least one publication exists.
The page documented the community standard.site lexicons field-by-field,
in two hand-authored tables, as if they were SkyPress's own. They're not
(Decision 0005), and spelling out someone else's schema invites drift and
overstates what SkyPress defines. Drop that interop section and its
tables; keep only an honest list of the records publishing writes, with a
pointer out to standard.site.
The one format SkyPress does own, blog.skypress.content.gutenberg, was
described only by its schema, which is abstract about what blocks look
like. Add a worked example built from a real published record (the Potato
buns recipe) showing the repeating name / attributes / innerBlocks shape
and the $type discriminator, so the structure is concrete.
Add a short self-hosting section pointing to the source repo, since the
records living in the reader's own account is the whole point and the
project is meant to be run anywhere.
Rendering the modal inside the eyebrow <p> nested a block-level backdrop in a
paragraph (invalid) and, worse, inherited the eyebrow's text-transform: uppercase,
corrupting the displayed JSON casing. Portal to <body> fixes both and lets the
fixed overlay escape any ancestor overflow/transform. Also pin the value-only
(no uri/cid envelope) decision in the page source test.
The lexicon page printed the blog.skypress.content.gutenberg schema as
flat monospace, while the reader's code blocks already get coloured. The
existing highlighter only exposed highlightCodeBlocks, which auto-detects
the language on rendered wp-block-code HTML — wrong fit for a schema
string we already know is JSON.
Add highlightSource(source, language) to the same module (keeping
highlight.js imported in one place) to tokenise a known-language string
directly, falling back to plain entity-escaped text if the grammar
throws. The page highlights its schema as JSON in frontmatter, so it
stays server-side with no client JS, like the reader.
The lexicon code block sits on a light --paper-raised surface, not the
reader's dark <pre>, so its token palette is built fresh: brand tokens
that flip with the theme for keys/keywords, plus two string/number hues
overridden under prefers-color-scheme: dark. Verified legible in both
light and dark themes.
highlightOne() decoded entities before stripping <br> tags, which
conflated genuine line-separator breaks (stored unescaped) with a
literal <br> typed in a code sample (stored escaped as <br>).
A code block containing an HTML/Markdown <br> example had it silently
turned into a newline. Strip the breaks on the still-escaped source so
only real break tags are affected; add a regression test.
The default social card still showed the old mark. Rebuild it from the
new public/skypress-logo-banner.png so shared links carry the new brand.
The banner is 1200x300 (4:1) — too short for an OpenGraph card, where
platforms expect ~1.91:1 and would letterbox or crop it. Composite it
centered onto a 1200x630 cream canvas (#faf6ef, the --paper token, which
matches the banner background exactly) to produce a seamless, correctly
proportioned og-default.png. Base.astro already serves it at 1200x630, so
no code change is needed.
Swap the hand-drawn type-block mark for the new winged-sun logo across
the favicon and every in-app spot (Logo.astro, AppBar, LoadingScene).
The mark now lives once in src/lib/brand/skypress-mark.ts as inline SVG
whose paths use fill="currentColor", so it follows the surrounding color
(the --sun token, which lightens in dark mode) instead of being locked to
one orange. A single source avoids duplicating the path data across the
Astro/React island boundary. The favicon keeps the static-orange
public/skypress-logo.svg, since a favicon can't inherit currentColor.
This drops the landing-only sun->moon easter-egg: the new logo is a sun
with no moon variant and only one asset was provided. Its night/dusk swap
styles in index.astro and the three tests that locked it are removed.
Commit 96634b6 reformatted Footer.astro to single-quoted attributes (and
dropped the duplicate publication link). The footer's content is intact, but
these source-level regexes hard-coded `href="..."` double quotes and so
broke on the reformat. Accept either quote style so the assertions track the
markup's meaning, not its formatting.
A `*/` inside the comment above `.studio__published` (in `--sun*/--ink`)
closed the comment early, so the trailing prose plus the selector collapsed
into one invalid rule that every CSS parser discards. The publish notice
rendered as an unstyled full-width paragraph instead of the centred sunrise
pill. Reword the comment to drop the literal `*/`.
Add a PostCSS-based regression test that asserts the rule survives parsing.
The .reader__author inline-flex box is sized by the 22px avatar, but
inline boxes default to vertical-align: baseline. That baseline-anchored
the tall author box against the shorter meta text (separators, date,
reading time), leaving the avatar visually misaligned. Aligning the
author box to vertical-align: middle puts the avatar, name, and trailing
meta text on a common centerline.
Two ADRs claimed 0017 from parallel branches both merged to trunk:
0017-render-article-module.md (the render-article module) collided with
0017-known-provider-detection.md (the provider-logos work, merged first).
Provider-logos keeps 0017; the render-article decision moves to 0018.
Rename the file and repoint its five referrers (0007, 0011, 0016,
AGENTS.md rule 6b, and the render-article.ts docblock). Each decision
number now maps to exactly one file.
The PublishPanel submit/publish handlers are async, which arms React's
act-tracking after the click's act() scope closes. A subsequent bare
root.unmount() then flushes that pending state update outside act() and
logs a "not wrapped in act" warning. Pre-flushing before unmount doesn't
help — the unmount's own flushSync has to run inside act().
Wrap root.unmount() in act() in both clickUpdate and clickPublish. This
also clears two equivalent warnings that already existed via clickUpdate
before the publish-success-pill work added clickPublish.
Foreign `site.standard.publication` records (written by Leaflet, pckt,
Offprint, …) now display the originating service's logo next to their
hostname — on the dashboard "From other apps" list and the public
profile "Elsewhere" section, which also gains the hostname it didn't
show before.
The hostname alone can't identify the service, because the paid tiers
serve from a custom domain. So detection is two-step: an app-specific
`$type` discriminator embedded in the record first (pckt writes
`theme.$type === "blog.pckt.theme"`, which survives a custom domain),
then a dot-boundary hostname-suffix fallback (`*.leaflet.pub`,
`*.pckt.blog`, `*.offprint.app`). Leaflet records carry no such
discriminator and Offprint is unsampled, so those two on a custom
domain stay logo-less, exactly as before — see Decision 0017.
Detection and the monochrome glyph data live in one framework-agnostic
module (`lib/publish/providers.ts`), since Astro can't server-render a
React component and the read path takes no client island: the React
dashboard and the Astro profile each render the shared data through a
tiny `ProviderLogo` of their own. Leaflet's saved asset was a raster
PNG, so its glyph is a substitute vector feather (Lucide, ISC).
The action bar rendered whenever a document carried a bskyPostRef, which
only proves the reference exists — not that the post it points at is still
live. A deleted post left Like/Repost/Quote/Reply buttons and a dead thread
link that couldn't work.
Add a client-side presence gate to the PostActions island: signed-in readers
learn the post is gone from the authenticated getPosts (a null result), and
signed-out readers from a new unauthenticated fetchPostExists() against the
public AppView. The gate is optimistic (shows by default, hides only on a
definitive 'gone') so a transient error never hides a live post. When gone,
the whole block renders nothing — buttons, note, and thread link.
The "+ New publication" button was a prominent, sun-filled primary in the
list header in every state. But once a writer has a publication, their wanted
action is to write (already the top-bar CTA), not to create another.
Make the button state-dependent: keep it prominent in the empty state, where
creating a publication is the point, and render it as a quiet secondary
control below the list once at least one publication exists.