Commits
Once the publish flow takes over (signed in + flow open), the top "Publish…"
button is redundant with the stepper's own publish control. Stop rendering
the actions bar in that state so the action isn't shown twice.
The publish flow's confirm step had an unstyled publication <select> and an
unstyled publish button, plus a "Keep editing" cancel. Remove the cancel
button (onCancel stays for the inline create-publication step), and style the
picker + primary action to match the editor's .publish__select / .publish__button
(token border, sun fill), with the working-status text muted.
When signed in, the editor showed two identical identity pills: WriteStudio's
own AccountPill in the actions bar and the app bar's app-bar__identity (which
already provides the profile link, the Publications nav, and Sign out). Remove
WriteStudio's pill — and the now-unused AccountPill component, its test, and
its CSS — leaving the actions bar with just the Publish button.
Replace the bare "→" after "Create one on Bluesky" with the project's
standard external-link glyph (the same icon as the author page's Bluesky
link), keep target="_blank" with a hardened rel, and add an aria-label so
the open-in-new-tab behaviour is announced to assistive tech.
The SignInPanel's inner elements were unstyled (default input + grey
buttons), and the panel/menu cards used rgba()/Canvas colours that wash
out on the dark canvas. Mirror the editor's LoginForm (login.css) — display
title, muted lede, framed input, sun primary button, outline cancel, accent
signup link — and reframe the cards + account-pill menu on the paper-raised
surface with token borders/shadow so they read in light and dark.
The Publish button and account pill floated at the viewport edge and the
title hugged the far left, because `.write-corner` had no column constraint
and wrapping the title in `.write-header` overrode its shared-column rules.
Drop the wrapper (title stays a direct child, reusing .studio__title) and
put the pill + Publish into one `.write-actions` bar constrained to
var(--studio-measure), so everything lines up with the app bar and editor.
Also remove the standalone signed-out "Sign in" button: signing in is now
offered only on the way to publishing (the publish-framed SignInPanel), so
onSignIn always stamps the resume intent. Drop the now-dead `restoredRef`
and a redundant displayName ternary flagged in review.
When a writing-first draft is restored, mergeAssets swapped each body image
token back to its data: URL. If the localStorage skeleton survived but the
IndexedDB bytes were evicted, the token had no entry in the assets map and the
old fallback kept the literal token (e.g. "a0") as the image url -- a broken
src that, not being a data: URL, also got published verbatim by uploadHeldAssets.
Drop the dangling token instead (matching the cover path's graceful null
fallback), while leaving external image URLs untouched since they are not tokens.
The previous version animated the icon pseudo-elements' width 0 -> 0.85em on
hover, which pushed any text after the mention sideways (only unnoticed because
the test mention ended its paragraph). Reserve the icon width + margin permanently
(invisible at rest via opacity 0) and animate only opacity + transform on hover —
neither affects layout, so following text stays put. Same technique the author
handle link uses. Color stays var(--sun)/currentColor, i.e. the per-publication
theme accent. Verified in-browser: trailing text holds its x-position between rest
and hover while both icons fade in.
Match the profile-page author handle link more fully: on hover/focus the mention
brightens to --sun and a Bluesky logo (left) and external-arrow (right) slide in.
They're drawn as currentColor mask pseudo-elements (::before/::after) because the
mention arrives as sanitised HTML and can't carry nested <svg> children; the icons
inherit the link colour and collapse to zero width at rest so the mention reads as
plain inline text. The sanitiser already gives the anchor target="_blank" + safe rel,
so mentions open in a new tab. Verified in the browser (hover reveals both icons in
--sun).
Mention anchors render with class="skypress-mention" (the sanitizer keeps the
class, strips data-did). The generic article-link rule painted them solid --sun;
give them the quieter author__handle-link treatment instead: inherit the prose
color, no underline, brighten to --sun on hover/focus. The class selector
out-specifies the generic :global(a) rule. Verified in the browser against the
live reader page.
Two issues surfaced by an in-browser smoke test (publish from @jeherve.com
mentioning @jeremy.herve.bzh):
1. The @ autocomplete never ran ours — every keystroke hit WordPress's built-in
user completer (GET /wp/v2/users → 404). Both completers claim the '@' trigger
and the block editor resolves a trigger to the FIRST matching completer, so
appending ours left it shadowed. Replace every '@'-triggered completer with ours
instead of appending (extracted as replaceAtMentionCompleter).
2. Selecting an option inserted nothing: getOptionCompletion returned
{ action: 'replace', value }, but 'replace' swaps the whole block (the slash-
completer contract) and dropped the inline anchor. Return the anchor element
directly so it inserts at the caret in place of the typed @query — the core
link-completer pattern.
Also: the live grapheme counter showed 'Bluesky post: N/300' at all times; only
warn when actually over the limit (hidden otherwise).
Verified end to end in the browser: autocomplete resolves jeremy.herve.bzh from
public.api.bsky.app, inserts the class+href+data-did anchor, the confirm dialog
discloses the notify target, and the published post carries the #mention facet
(correct DID + byte offsets) plus the document's flat mentions interop list.
The 300-grapheme backstop lived inside buildBskyPost, which publish() calls
at step 2 — after step 1 has already created the document record. If it fired,
the PDS kept a published-but-postless document (and a retry minted a second
orphan). Hoist the check into a shared assertBskyPostWithinLimit() and run it
in publish() before any record is written; buildBskyPost keeps calling it as a
final backstop. Adds a regression test asserting an over-limit publish rejects
with zero records written.
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 publish flow's confirm step had an unstyled publication <select> and an
unstyled publish button, plus a "Keep editing" cancel. Remove the cancel
button (onCancel stays for the inline create-publication step), and style the
picker + primary action to match the editor's .publish__select / .publish__button
(token border, sun fill), with the working-status text muted.
When signed in, the editor showed two identical identity pills: WriteStudio's
own AccountPill in the actions bar and the app bar's app-bar__identity (which
already provides the profile link, the Publications nav, and Sign out). Remove
WriteStudio's pill — and the now-unused AccountPill component, its test, and
its CSS — leaving the actions bar with just the Publish button.
The SignInPanel's inner elements were unstyled (default input + grey
buttons), and the panel/menu cards used rgba()/Canvas colours that wash
out on the dark canvas. Mirror the editor's LoginForm (login.css) — display
title, muted lede, framed input, sun primary button, outline cancel, accent
signup link — and reframe the cards + account-pill menu on the paper-raised
surface with token borders/shadow so they read in light and dark.
The Publish button and account pill floated at the viewport edge and the
title hugged the far left, because `.write-corner` had no column constraint
and wrapping the title in `.write-header` overrode its shared-column rules.
Drop the wrapper (title stays a direct child, reusing .studio__title) and
put the pill + Publish into one `.write-actions` bar constrained to
var(--studio-measure), so everything lines up with the app bar and editor.
Also remove the standalone signed-out "Sign in" button: signing in is now
offered only on the way to publishing (the publish-framed SignInPanel), so
onSignIn always stamps the resume intent. Drop the now-dead `restoredRef`
and a redundant displayName ternary flagged in review.
When a writing-first draft is restored, mergeAssets swapped each body image
token back to its data: URL. If the localStorage skeleton survived but the
IndexedDB bytes were evicted, the token had no entry in the assets map and the
old fallback kept the literal token (e.g. "a0") as the image url -- a broken
src that, not being a data: URL, also got published verbatim by uploadHeldAssets.
Drop the dangling token instead (matching the cover path's graceful null
fallback), while leaving external image URLs untouched since they are not tokens.
The previous version animated the icon pseudo-elements' width 0 -> 0.85em on
hover, which pushed any text after the mention sideways (only unnoticed because
the test mention ended its paragraph). Reserve the icon width + margin permanently
(invisible at rest via opacity 0) and animate only opacity + transform on hover —
neither affects layout, so following text stays put. Same technique the author
handle link uses. Color stays var(--sun)/currentColor, i.e. the per-publication
theme accent. Verified in-browser: trailing text holds its x-position between rest
and hover while both icons fade in.
Match the profile-page author handle link more fully: on hover/focus the mention
brightens to --sun and a Bluesky logo (left) and external-arrow (right) slide in.
They're drawn as currentColor mask pseudo-elements (::before/::after) because the
mention arrives as sanitised HTML and can't carry nested <svg> children; the icons
inherit the link colour and collapse to zero width at rest so the mention reads as
plain inline text. The sanitiser already gives the anchor target="_blank" + safe rel,
so mentions open in a new tab. Verified in the browser (hover reveals both icons in
--sun).
Mention anchors render with class="skypress-mention" (the sanitizer keeps the
class, strips data-did). The generic article-link rule painted them solid --sun;
give them the quieter author__handle-link treatment instead: inherit the prose
color, no underline, brighten to --sun on hover/focus. The class selector
out-specifies the generic :global(a) rule. Verified in the browser against the
live reader page.
Two issues surfaced by an in-browser smoke test (publish from @jeherve.com
mentioning @jeremy.herve.bzh):
1. The @ autocomplete never ran ours — every keystroke hit WordPress's built-in
user completer (GET /wp/v2/users → 404). Both completers claim the '@' trigger
and the block editor resolves a trigger to the FIRST matching completer, so
appending ours left it shadowed. Replace every '@'-triggered completer with ours
instead of appending (extracted as replaceAtMentionCompleter).
2. Selecting an option inserted nothing: getOptionCompletion returned
{ action: 'replace', value }, but 'replace' swaps the whole block (the slash-
completer contract) and dropped the inline anchor. Return the anchor element
directly so it inserts at the caret in place of the typed @query — the core
link-completer pattern.
Also: the live grapheme counter showed 'Bluesky post: N/300' at all times; only
warn when actually over the limit (hidden otherwise).
Verified end to end in the browser: autocomplete resolves jeremy.herve.bzh from
public.api.bsky.app, inserts the class+href+data-did anchor, the confirm dialog
discloses the notify target, and the published post carries the #mention facet
(correct DID + byte offsets) plus the document's flat mentions interop list.
The 300-grapheme backstop lived inside buildBskyPost, which publish() calls
at step 2 — after step 1 has already created the document record. If it fired,
the PDS kept a published-but-postless document (and a retry minted a second
orphan). Hoist the check into a shared assertBskyPostWithinLimit() and run it
in publish() before any record is written; buildBskyPost keeps calling it as a
final backstop. Adds a regression test asserting an over-limit publish rejects
with zero records written.
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.