A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Drop the redundant account pill from /write; the app bar carries identity

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.

+11 -212
-47
src/components/AccountPill.test.tsx
··· 1 - import { describe, it, expect, vi } from 'vitest'; 2 - import { act, createElement } from 'react'; 3 - import { createRoot } from 'react-dom/client'; 4 - 5 - ( globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; 6 - import AccountPill from './AccountPill'; 7 - 8 - function mount( props: Record< string, unknown > ) { 9 - const container = document.createElement( 'div' ); 10 - document.body.appendChild( container ); 11 - act( () => createRoot( container ).render( createElement( AccountPill, props as never ) ) ); 12 - return container; 13 - } 14 - 15 - // The pill's menu is collapsed until the trigger is clicked; open it before asserting items. 16 - const openMenu = ( container: HTMLElement ) => { 17 - const trigger = container.querySelector( '.account-pill__trigger' ) as HTMLButtonElement; 18 - act( () => trigger.dispatchEvent( new MouseEvent( 'click', { bubbles: true } ) ) ); 19 - }; 20 - 21 - describe( 'AccountPill', () => { 22 - it( 'shows the handle and a Manage-publications + Profile + Sign out menu', () => { 23 - const c = mount( { 24 - displayName: 'Alice', handle: 'alice.test', avatar: null, onSignOut: vi.fn(), 25 - } ); 26 - expect( c.textContent ).toContain( 'alice.test' ); 27 - openMenu( c ); 28 - expect( c.querySelector( 'a[href="/dashboard"]' ) ).not.toBe( null ); 29 - expect( c.querySelector( 'a[href="/@alice.test"]' ) ).not.toBe( null ); 30 - expect( c.textContent?.toLowerCase() ).toContain( 'sign out' ); 31 - } ); 32 - 33 - it( 'invokes onSignOut when Sign out is clicked', () => { 34 - const onSignOut = vi.fn(); 35 - const c = mount( { displayName: 'Alice', handle: 'alice.test', avatar: null, onSignOut } ); 36 - openMenu( c ); 37 - const btn = [ ...c.querySelectorAll( 'button' ) ].find( ( b ) => /sign out/i.test( b.textContent ?? '' ) )!; 38 - act( () => btn.dispatchEvent( new MouseEvent( 'click', { bubbles: true } ) ) ); 39 - expect( onSignOut ).toHaveBeenCalledTimes( 1 ); 40 - } ); 41 - 42 - it( 'omits the Profile link when no handle is known', () => { 43 - const c = mount( { displayName: 'did:plc:me', handle: null, avatar: null, onSignOut: vi.fn() } ); 44 - openMenu( c ); 45 - expect( c.querySelector( 'a[href^="/@"]' ) ).toBe( null ); 46 - } ); 47 - } );
-86
src/components/AccountPill.tsx
··· 1 - import { useEffect, useRef, useState } from 'react'; 2 - import { authorPath } from '../lib/auth/profile'; 3 - 4 - interface Props { 5 - displayName: string; 6 - handle: string | null; 7 - avatar: string | null; 8 - onSignOut: () => void; 9 - } 10 - 11 - /** 12 - * Signed-in account pill for the writing-first page: avatar + handle, opening a menu with 13 - * Manage publications (→ existing dashboard), Profile (public author page, omitted when no 14 - * handle), and Sign out. Management lives on the dashboard by design — the editor stays the 15 - * focus here (design 2026-06-17, Q5). 16 - */ 17 - export default function AccountPill( { displayName, handle, avatar, onSignOut }: Props ) { 18 - const [ open, setOpen ] = useState( false ); 19 - const [ avatarOk, setAvatarOk ] = useState( true ); 20 - const rootRef = useRef< HTMLDivElement >( null ); 21 - 22 - useEffect( () => { 23 - if ( ! open ) { 24 - return; 25 - } 26 - function onDown( event: MouseEvent ) { 27 - if ( rootRef.current && ! rootRef.current.contains( event.target as Node ) ) { 28 - setOpen( false ); 29 - } 30 - } 31 - document.addEventListener( 'mousedown', onDown ); 32 - return () => document.removeEventListener( 'mousedown', onDown ); 33 - }, [ open ] ); 34 - 35 - const profileHref = authorPath( handle ); 36 - 37 - return ( 38 - <div className="account-pill" ref={ rootRef }> 39 - <button 40 - type="button" 41 - className="account-pill__trigger" 42 - aria-haspopup="menu" 43 - aria-expanded={ open } 44 - onClick={ () => setOpen( ( v ) => ! v ) } 45 - > 46 - { avatar && avatarOk ? ( 47 - <img 48 - className="account-pill__avatar" 49 - src={ avatar } 50 - alt="" 51 - width={ 28 } 52 - height={ 28 } 53 - onError={ () => setAvatarOk( false ) } 54 - /> 55 - ) : ( 56 - <span className="account-pill__avatar account-pill__avatar--fallback" aria-hidden="true"> 57 - { displayName.charAt( 0 ).toUpperCase() } 58 - </span> 59 - ) } 60 - <span className="account-pill__handle"> 61 - { handle ? `@${ handle }` : displayName } 62 - </span> 63 - </button> 64 - { open && ( 65 - <div className="account-pill__menu" role="menu"> 66 - <a className="account-pill__item" role="menuitem" href="/dashboard"> 67 - Manage publications 68 - </a> 69 - { profileHref && ( 70 - <a className="account-pill__item" role="menuitem" href={ profileHref }> 71 - Profile 72 - </a> 73 - ) } 74 - <button 75 - type="button" 76 - className="account-pill__item account-pill__item--button" 77 - role="menuitem" 78 - onClick={ onSignOut } 79 - > 80 - Sign out 81 - </button> 82 - </div> 83 - ) } 84 - </div> 85 - ); 86 - }
+4 -2
src/components/WriteStudio.test.tsx
··· 62 62 expect( container.textContent?.toLowerCase() ).not.toContain( 'sign in' ); 63 63 } ); 64 64 65 - it( 'signed in: renders the editor, the account pill, and the Publish action', async () => { 65 + it( 'signed in: renders the editor and the Publish action, and does NOT duplicate the identity pill (the app bar carries it)', async () => { 66 66 auth.value = { 67 67 status: 'signed-in', agent: {}, did: 'did:plc:me', handle: 'me.test', 68 68 displayName: 'Me', avatar: null, pdsUrl: 'https://pds', signIn: vi.fn(), signOut: vi.fn(), ··· 72 72 root.render( createElement( WriteStudio ) ); 73 73 } ); 74 74 expect( container.querySelector( '[data-testid="sky-editor"]' ) ).not.toBe( null ); 75 - expect( container.querySelector( '.account-pill' ) ).not.toBe( null ); 76 75 expect( container.querySelector( '.write-publish' ) ).not.toBe( null ); 76 + // The app bar (mocked out here) is the single source of the signed-in identity — 77 + // WriteStudio must not render its own duplicate pill. 78 + expect( container.querySelector( '.account-pill' ) ).toBe( null ); 77 79 } ); 78 80 79 81 it( 'resume: a publish intent on a signed-in load opens the publish flow', async () => {
+4 -15
src/components/WriteStudio.tsx
··· 8 8 import PublishedPill from './PublishedPill'; 9 9 import CoverImagePicker from './CoverImagePicker'; 10 10 import SignInPanel from './SignInPanel'; 11 - import AccountPill from './AccountPill'; 12 11 import WritePublishFlow from './WritePublishFlow'; 13 12 import { createDeferredMediaUpload } from '../lib/write/deferred-media'; 14 13 import { createDraftStore, type WriteDraft } from '../lib/write/draft-store'; 15 14 import { listPublications, type Publication } from '../lib/publish/publications'; 16 15 import { normalizeBlocks } from '../lib/publish/records'; 17 16 import { validateCoverFile, type CoverUpload } from '../lib/media/cover'; 18 - import { displayNameFor } from '../lib/auth/profile'; 19 17 import type { BlockNode } from '../lib/blocks/render'; 20 18 import type { BlobRefJson } from '../lib/media/blob'; 21 19 ··· 35 33 } 36 34 37 35 function WriteSurface() { 38 - const { status, agent, did, handle, displayName, avatar, pdsUrl, error, signIn, signOut } = 39 - useAuth(); 36 + const { status, agent, did, handle, pdsUrl, error, signIn } = useAuth(); 40 37 41 38 const draftStore = useMemo( () => createDraftStore(), [] ); 42 39 const mediaUpload = useMemo( () => createDeferredMediaUpload(), [] ); ··· 155 152 <> 156 153 <AppBar current="editor" /> 157 154 158 - { /* Right-aligned actions in the shared content column: the account pill (when 159 - signed in) and the always-present Publish button. The editor — not auth — 160 - stays the focus, so there is no standalone "Sign in" affordance. */ } 155 + { /* The Publish action, right-aligned in the shared content column. The signed-in 156 + identity + sign-out live in the app bar above, so there is no pill here — and 157 + no standalone "Sign in" affordance (signing in happens on the way to publish). */ } 161 158 <div className="write-actions"> 162 - { signedIn && ( 163 - <AccountPill 164 - displayName={ displayNameFor( { did: did!, handle, displayName, avatar } ) } 165 - handle={ handle } 166 - avatar={ avatar } 167 - onSignOut={ () => void signOut() } 168 - /> 169 - ) } 170 159 <button 171 160 type="button" 172 161 className="write-publish"
+3 -62
src/styles/write-chrome.css
··· 1 1 /* src/styles/write-chrome.css 2 - * Chrome unique to the writing-first page (/write): the top actions bar (account pill + 3 - * Publish), the sign-in panel, and the publish stepper. The editor body itself reuses 2 + * Chrome unique to the writing-first page (/write): the top actions bar (the Publish button), 3 + * the sign-in panel, and the publish stepper. The signed-in identity + sign-out live in the 4 + * shared app bar (app-bar.css), not here. The editor body itself reuses 4 5 * editor-chrome.css (.studio__title / .studio__lede / .studio__cover*), so the title/lede 5 6 * stay direct children of the column — never wrap them, or they lose their shared alignment. 6 7 */ ··· 33 34 .write-publish:disabled { 34 35 opacity: 0.5; 35 36 cursor: not-allowed; 36 - } 37 - 38 - .account-pill { 39 - position: relative; 40 - } 41 - 42 - .account-pill__trigger { 43 - display: inline-flex; 44 - align-items: center; 45 - gap: 0.5rem; 46 - background: transparent; 47 - border: 0; 48 - cursor: pointer; 49 - font: inherit; 50 - } 51 - 52 - .account-pill__avatar { 53 - border-radius: 999px; 54 - } 55 - 56 - .account-pill__avatar--fallback { 57 - display: inline-grid; 58 - place-items: center; 59 - width: 28px; 60 - height: 28px; 61 - border-radius: 999px; 62 - background: var(--sun-tint); 63 - color: var(--sun); 64 - font-weight: 700; 65 - font-size: 0.85rem; 66 - } 67 - 68 - .account-pill__menu { 69 - position: absolute; 70 - right: 0; 71 - margin-top: 0.4rem; 72 - min-width: 12rem; 73 - display: flex; 74 - flex-direction: column; 75 - background: var(--paper-raised); 76 - border: 1px solid var(--line-strong); 77 - border-radius: 0.5rem; 78 - box-shadow: var(--shadow); 79 - overflow: hidden; 80 - z-index: 5; 81 - } 82 - 83 - .account-pill__item { 84 - padding: 0.6rem 0.9rem; 85 - text-align: left; 86 - background: transparent; 87 - border: 0; 88 - font: inherit; 89 - cursor: pointer; 90 - color: inherit; 91 - text-decoration: none; 92 - } 93 - 94 - .account-pill__item:hover { 95 - background: var(--panel); 96 37 } 97 38 98 39 /* Sign-in panel + publish stepper: framed cards in the centred column. Use the paper-raised