diff --git a/docs/userstyles.md b/docs/userstyles.md index c01c6cf22..cc08bcc0a 100644 --- a/docs/userstyles.md +++ b/docs/userstyles.md @@ -1,64 +1,151 @@ -# Userstyle selectors +# Browser userstyles -Witchsky exposes stable `wsky-` CSS classes on the web app for browser -userstyles. These names are an additive compatibility API: generated -React Native Web classes such as `css-*` and `r-*` are implementation details -and should not be targeted. +The Witchsky web app exposes relatively stable `wsky-` CSS classes for browser +userstyle and userscript use. These names are an additive compatibility API. +Generated React Native Web classes such as `css-*` and `r-*`, exact inline styles, +DOM position, and `data-testid` values are implementation details and probably +shouldn't be targeted. -## Stable classes +## Selector reference + +Every selector below is part of the stable userstyle API. + +### App layout and navigation | Selector | Element | | --- | --- | | `.wsky-main` | Main application content | -| `.wsky-screen` | Any screen root | -| `.wsky-screen--home` | Home screen | -| `.wsky-screen--profile` | Profile screen | -| `.wsky-screen--post-thread` | Post thread screen | +| `.wsky-center` | Shared centered-column container | +| `.wsky-center-borders` | Desktop vertical border rails around the center column | +| `.wsky-header` | Shared screen header | +| `.wsky-screen` | Root of any screen | | `.wsky-nav` | Any primary navigation container | | `.wsky-nav--desktop` | Desktop left navigation | | `.wsky-nav--mobile` | Mobile web bottom navigation | +| `.wsky-nav--compact` | Desktop navigation in compact rail mode | | `.wsky-nav__item` | Primary navigation item | +| `.wsky-nav__compose` | Desktop compose action wrapper | | `.wsky-sidebar` | Any secondary sidebar | | `.wsky-sidebar--right` | Desktop right sidebar | | `.wsky-tabs` | Tab list | -| `.wsky-feed-page` | Feed page container | +| `.wsky-fab` | Shared floating action button root | +| `.wsky-fab--compose` | Floating compose button | +| `.wsky-fab--load-latest` | Floating load/scroll-to-latest button | + +### Screens and major regions + +All named screen roots also have `.wsky-screen`. + +| Selector | Element | +| --- | --- | +| `.wsky-screen--home` | Home screen | +| `.wsky-screen--profile` | Profile screen | +| `.wsky-screen--post-thread` | Post thread screen | +| `.wsky-screen--search` | Search screen | +| `.wsky-screen--notifications` | Notifications screen | +| `.wsky-screen--settings` | Settings screen | +| `.wsky-screen--bookmarks` | Saved Posts screen | +| `.wsky-screen--saved-feeds` | Saved Feeds editor screen | +| `.wsky-screen--feeds` | Feeds discovery screen | +| `.wsky-screen--lists` | Lists screen | +| `.wsky-screen--messages` | Chat list screen | +| `.wsky-profile` | Profile content | +| `.wsky-profile__header` | Profile header | +| `.wsky-search` | Search content | +| `.wsky-notifications` | Notifications content | +| `.wsky-settings` | Settings content | +| `.wsky-bookmarks` | Saved Posts list | +| `.wsky-saved-feeds` | Saved Feeds editor content | +| `.wsky-saved-feeds__item` | Saved feed row | +| `.wsky-saved-feeds__item--pinned` | Pinned saved-feed row | +| `.wsky-saved-feeds__item--unpinned` | Unpinned saved-feed row | +| `.wsky-feeds` | Feeds discovery content | +| `.wsky-lists` | Lists content | +| `.wsky-messages` | Chat list content | +| `.wsky-home__topbar` | Desktop/tablet home logo and feeds-shortcut row | +| `.wsky-home__tabs-region` | Desktop/tablet sticky home tabs region | +| `.wsky-also-liked` | Also Liked section in a post thread | +| `.wsky-also-liked__header` | Also Liked expand/collapse header | + +### Feeds, cards, and empty states + +| Selector | Element | +| --- | --- | +| `.wsky-feed-page` | One feed page inside a pager | | `.wsky-feed` | Feed list | | `.wsky-feed-item` | Feed item | -| `.wsky-post` | Post feed item | +| `.wsky-feed-card` | Feed or list source card | +| `.wsky-empty-state` | Shared empty-state container | + +### Posts + +The post hooks are shared by home and custom feeds, search results, hashtag and +topic results, Saved Posts, Also Liked cards, and non-focused posts in thread +views. + +| Selector | Element | +| --- | --- | +| `.wsky-post` | Post item in a feed, result list, saved list, or thread | +| `.wsky-post__surface` | Visible post surface; use for card background, padding, border, and radius | +| `.wsky-post__hover` | Post hover/pressed overlay | | `.wsky-post__meta` | Post author and timestamp area | | `.wsky-post__content` | Post content area | | `.wsky-post__text` | Post text container | -| `.wsky-post__embed` | Post embed container | +| `.wsky-post__embed` | Entire post embed, including media, link, quote, poll, or record cards | +| `.wsky-post__media` | Image, gallery, or video surface inside an embed | +| `.wsky-post__gallery-scroll` | Horizontal scrolling container inside an expanded gallery | | `.wsky-post__actions` | Reply, repost, like, and share controls | -| `.wsky-profile` | Profile content | -| `.wsky-profile__header` | Profile header | + +### Composer, dialogs, and menus + +| Selector | Element | +| --- | --- | | `.wsky-composer` | Post composer | | `.wsky-composer__topbar` | Composer top bar | +| `.wsky-dialog` | Shared dialog content layer | +| `.wsky-dialog__backdrop` | Shared dialog backdrop | +| `.wsky-menu` | Shared dropdown menu surface | +| `.wsky-menu__item` | Shared dropdown menu item | + +## State attributes + +Classes describe what an element is. `data-wsky-*` attributes expose stable +state and subtype values. + +| Attribute | Element | Values | +| --- | --- | --- | +| `data-wsky-nav-item` | `.wsky-nav__item` | `home`, `search`, `notifications`, `chat`, `feeds`, `lists`, `saved`, `profile`, or `settings` | +| `data-wsky-active` | `.wsky-nav__item` | `true` or `false` | +| `data-wsky-embed` | `.wsky-post` | `true` or `false` | +| `data-wsky-reply` | `.wsky-post` | `true` or `false` | +| `data-wsky-repost` | `.wsky-post` | `true` or `false` | +| `data-feed-context` | Feed posts when supplied by the feed | Opaque feed context | -Some elements also expose state as `data-wsky-*` attributes. For example: +For example: ```css -/* Hide embeds in reposts. */ +/* Hide embeds only on reposts. */ .wsky-post[data-wsky-repost='true'] .wsky-post__embed { display: none !important; } -/* Make post text a little larger. */ -.wsky-post__text { - font-size: 17px !important; +/* Emphasize the active desktop navigation item. */ +.wsky-nav--desktop + .wsky-nav__item[data-wsky-active='true'] { + background: color-mix(in srgb, AccentColor 15%, transparent) !important; } ``` -Navigation items expose `data-wsky-nav-item` (`home`, `search`, `chat`, -`notifications`, or `profile`) and `data-wsky-active`. Posts expose -`data-wsky-embed`, `data-wsky-reply`, `data-wsky-repost`, and the existing -`data-feed-context`. +## Notes -React Native Web may apply inline styles, so userstyles will sometimes need -`!important`. Existing `data-testid` values remain available, but they are test -hooks rather than part of this compatibility API. +- React Native Web often emits inline styles, so userstyles will sometimes need +`!important`. +- Existing `data-testid` values remain available for tests, but are not part of + this compatibility API. +- Prefer semantic hooks over element names or DOM relationships. React Native + Web may add or remove wrapper elements between releases. +- Removing or changing a published selector or attribute is a breaking change + for userstyle authors. Adding selectors is backward compatible. -When adding a new selector, use `userStyle()` from `#/lib/userstyles`, keep the -name semantic rather than visual, and update this list. Removing or changing a -published selector should be treated as a breaking change for userstyle -authors. +When adding a hook in Witchsky, use `userStyle()` from `#/lib/userstyles`, keep +the name semantic rather than visual, and update this guide. diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index fb2143f70..3e67e32a9 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -22,6 +22,7 @@ import {useLingui} from '@lingui/react' import {DismissableLayer, FocusGuards, FocusScope} from 'radix-ui/internal' import {RemoveScrollBar} from 'react-remove-scroll-bar' +import {userStyle} from '#/lib/userstyles' import {logger} from '#/logger' import {useA11y} from '#/state/a11y' import {useDialogStateControlContext} from '#/state/dialogs' @@ -217,6 +218,7 @@ export function Inner({ style, ])}> + { + const element = node as unknown as HTMLElement | null + element?.classList.add('wsky-menu__item') + }} className="radix-dropdown-item" accessibilityHint="" accessibilityLabel={label} @@ -331,7 +337,11 @@ export function Submenu({children, label, trigger, style}: SubmenuProps) { { + triggerRef.current = node + const element = node as unknown as HTMLElement | null + element?.classList.add('wsky-menu__item') + }} className="radix-dropdown-item" accessibilityHint="" accessibilityLabel={label} @@ -407,6 +417,7 @@ export function Submenu({children, label, trigger, style}: SubmenuProps) { className="dropdown-menu-transform-origin dropdown-menu-constrain-size"> + + + + + @@ -165,6 +168,7 @@ function BookmarksInner() { return ( openComposer({logContext: 'Fab'})} icon={} accessibilityRole="button" diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index 949ebf12b..cee6f0995 100644 --- a/src/screens/Messages/ChatList.tsx +++ b/src/screens/Messages/ChatList.tsx @@ -18,6 +18,7 @@ import { type NavigationProp, } from '#/lib/routes/types' import {cleanError} from '#/lib/strings/errors' +import {userStyle} from '#/lib/userstyles' import {logger} from '#/logger' import {listenSoftReset} from '#/state/events' import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const' @@ -250,7 +251,9 @@ export function MessagesScreenInner({navigation, route}: Props) { } return ( - +
{hasSection && ( - + ( - + {richText?.text ? ( - + - + {children} ) diff --git a/src/screens/PostThread/components/ThreadItemReaderSegment.tsx b/src/screens/PostThread/components/ThreadItemReaderSegment.tsx index 99042876b..017a6704b 100644 --- a/src/screens/PostThread/components/ThreadItemReaderSegment.tsx +++ b/src/screens/PostThread/components/ThreadItemReaderSegment.tsx @@ -7,6 +7,7 @@ import { } from '@atproto/api' import {Trans} from '@lingui/react/macro' +import {userStyle} from '#/lib/userstyles' import { POST_TOMBSTONE, type Shadow, @@ -120,10 +121,17 @@ const ThreadItemReaderSegmentInner = memo( [record], ) return ( - + - + {seam.expanded && ( <> @@ -134,7 +142,7 @@ const ThreadItemReaderSegmentInner = memo( )} {richText?.text ? ( - + {/* Intentionally not line-limited: reader view is for uninterrupted reading */} {post.embed && ( - + - + {richText?.text ? ( - + {post.embed && ( - + - + {children} ) diff --git a/src/screens/ProfileList/index.tsx b/src/screens/ProfileList/index.tsx index 77af470cd..7000da660 100644 --- a/src/screens/ProfileList/index.tsx +++ b/src/screens/ProfileList/index.tsx @@ -231,6 +231,7 @@ function ProfileListScreenLoaded({ openComposer({logContext: 'Fab'})} icon={} accessibilityRole="button" @@ -274,6 +275,7 @@ function ProfileListScreenLoaded({ )} openComposer({logContext: 'Fab'})} icon={} accessibilityRole="button" diff --git a/src/screens/SavedFeeds.tsx b/src/screens/SavedFeeds.tsx index db574318f..1e76f8365 100644 --- a/src/screens/SavedFeeds.tsx +++ b/src/screens/SavedFeeds.tsx @@ -16,6 +16,7 @@ import { type CommonNavigatorParams, type NavigationProp, } from '#/lib/routes/types' +import {userStyle} from '#/lib/userstyles' import {logger} from '#/logger' import {useA11y} from '#/state/a11y' import { @@ -104,7 +105,7 @@ function SavedFeedsInner({ } return ( - + @@ -126,7 +127,10 @@ function SavedFeedsInner({ - + {noSavedFeedsOfAnyType && ( + @@ -317,7 +321,7 @@ function SavedFeedsA11y({ - + {noSavedFeedsOfAnyType && ( + {feed.type === 'timeline' ? ( ) : ( @@ -533,7 +542,13 @@ function UnpinnedFeedItem({ } return ( - + {feed.type === 'timeline' ? ( ) : ( diff --git a/src/screens/Search/Shell.tsx b/src/screens/Search/Shell.tsx index b9eac11f5..0a1531423 100644 --- a/src/screens/Search/Shell.tsx +++ b/src/screens/Search/Shell.tsx @@ -27,6 +27,7 @@ import {HITSLOP_10, HITSLOP_20} from '#/lib/constants' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {MagnifyingGlassIcon} from '#/lib/icons' import {type NavigationProp, type SearchParams} from '#/lib/routes/types' +import {userStyle} from '#/lib/userstyles' import {listenSoftReset} from '#/state/events' import { unstableCacheProfileView, @@ -517,7 +518,9 @@ export function SearchScreenShell({ const showHeader = !gtMobile || navButton !== 'menu' return ( - + { @@ -643,7 +646,7 @@ export function SearchScreenShell({ - + + @@ -190,7 +191,10 @@ export function SettingsScreen({}: Props) { - + diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index fea19cb88..07d090aa1 100644 --- a/src/view/com/feeds/FeedPage.tsx +++ b/src/view/com/feeds/FeedPage.tsx @@ -175,6 +175,7 @@ export function FeedPage({ {hasSession && ( } accessibilityRole="button" diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index ee6239255..dd9bd22a7 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -10,6 +10,7 @@ import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' import {sanitizeHandle} from '#/lib/strings/handles' +import {userStyle} from '#/lib/userstyles' import { type FeedSourceInfo, hydrateFeedGenerator, @@ -197,6 +198,7 @@ export function FeedSourceCardLoaded({ params: {name: feed.creatorDid, rkey: new AtUri(feed.uri).rkey}, }} style={[ + userStyle('wsky-feed-card'), a.flex_1, a.p_lg, a.gap_md, @@ -211,6 +213,7 @@ export function FeedSourceCardLoaded({ return ( + style={[ + userStyle('wsky-home__topbar'), + a.flex_row, + a.align_center, + gutters, + a.pt_md, + t.atoms.bg, + ]}> { headerHeight.set(e.nativeEvent.layout.height) }}> diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index 1027efa0f..b2910f7e9 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -14,6 +14,7 @@ import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' +import {userStyle} from '#/lib/userstyles' import { POST_TOMBSTONE, type Shadow, @@ -171,7 +172,13 @@ function PostInner({ { setHover(false) }}> - + {showReplyLine && ( {richText.text ? ( - + {post.embed ? ( + style={[ + userStyle('wsky-post__embed'), + maybeApplyGalleryOffsetStyles('embed', { + post, + modui: moderation.ui('contentList'), + additionalCauses: [], + }), + ]}> { setHover(false) }}> - + + { testID?: string icon: JSX.Element style?: StyleProp + userStyleVariant?: 'compose' } -export function FABInner({testID, icon, onPress, style, ...props}: FABProps) { +export function FABInner({ + testID, + icon, + onPress, + style, + userStyleVariant, + ...props +}: FABProps) { const insets = useSafeAreaInsets() const {gtMobile} = useBreakpoints() const t = useTheme() @@ -47,6 +56,10 @@ export function FABInner({testID, icon, onPress, style, ...props}: FABProps) { return ( - + + @@ -543,6 +544,7 @@ export function FeedsScreen(_props: Props) { {hasSession && ( } accessibilityRole="button" diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx index 54f3568fc..90b3cdb06 100644 --- a/src/view/screens/Lists.tsx +++ b/src/view/screens/Lists.tsx @@ -11,6 +11,7 @@ import { type NativeStackScreenProps, type NavigationProp, } from '#/lib/routes/types' +import {userStyle} from '#/lib/userstyles' import {MyLists} from '#/view/com/lists/MyLists' import {atoms as a} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -52,7 +53,7 @@ export function ListsScreen({}: Props) { ) return ( - + @@ -73,7 +74,7 @@ export function ListsScreen({}: Props) { - + + @@ -157,11 +160,14 @@ export function NotificationsScreen({}: Props) { )} initialPage={initialActiveTab}> {sections.map((section, i) => ( - {section.component} + + {section.component} + ))} openComposer({logContext: 'Fab'})} icon={} accessibilityRole="button" diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 6456b1dd7..faeeee5dc 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -609,6 +609,7 @@ function ProfileScreenLoaded({ {hasSession && ( } accessibilityRole="button" diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index 2c5ebb176..2965ab5cd 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -651,7 +651,11 @@ function ComposeBtn({minimal}: {minimal: boolean}) { openComposer({mention: await getProfileHandle(), logContext: 'Fab'}) return ( - +