diff --git a/apps/web/app/space/[id]/(space)/layout.tsx b/apps/web/app/space/[id]/(space)/layout.tsx index 4f1254f831..cc8223bf61 100644 --- a/apps/web/app/space/[id]/(space)/layout.tsx +++ b/apps/web/app/space/[id]/(space)/layout.tsx @@ -23,7 +23,7 @@ import { Spacer } from '~/design-system/spacer'; import { EditableSpaceHeading } from '~/partials/entity-page/editable-space-header'; import { EntityPageCover } from '~/partials/entity-page/entity-page-cover'; import { EntityPageInlineDescription } from '~/partials/entity-page/entity-page-inline-description'; -import { ENTITY_PAGE_WITH_SIDEBAR_MAX_WIDTH } from '~/partials/entity-page/entity-page-layout'; +import { ENTITY_PAGE_WITH_SIDEBAR_MAX_WIDTH, PROFILE_COVER_SIZE } from '~/partials/entity-page/entity-page-layout'; import { PersonalProfileBioStarterMerge } from '~/partials/entity-page/personal-profile-bio-starter-merge'; import { PersonalProfileSuggestedCard } from '~/partials/entity-page/personal-profile-suggested-card'; import { PersonalProfileSuggestedTaskSync } from '~/partials/entity-page/personal-profile-suggested-task-sync'; @@ -132,6 +132,7 @@ export default async function Layout(props0: LayoutProps) { avatarUrl={props.avatarUrl} coverUrl={props.coverUrl} contentMaxWidth={isProfile ? ENTITY_PAGE_WITH_SIDEBAR_MAX_WIDTH : undefined} + coverSize={isProfile ? PROFILE_COVER_SIZE : undefined} /> {isProfile && } - + {/* * A profile renders none of this row. Types move to the rail's * About section, the vote pair into the action row beside Edit @@ -190,7 +191,9 @@ export default async function Layout(props0: LayoutProps) { /> -
+ {/* 40px above the tab bar on a profile, 24px elsewhere. The description's + own 20px bottom margin collapses into this one, so this is the gap. */} +
{typeIds.includes(SystemIds.PERSON_TYPE) ? ( diff --git a/apps/web/core/claims/browse/claim-page-view.tsx b/apps/web/core/claims/browse/claim-page-view.tsx index cc70698fc5..8d73469d58 100644 --- a/apps/web/core/claims/browse/claim-page-view.tsx +++ b/apps/web/core/claims/browse/claim-page-view.tsx @@ -155,9 +155,7 @@ export function ClaimPageView({ }; // Matches profile record tabs: unknown/error stays reachable, while a settled zero disappears. const hasDebates = hasRecordToShow( - record.debatesLoading || record.debatesError || record.debatesCountUnavailable - ? undefined - : record.debatesTotal + record.debatesLoading || record.debatesError || record.debatesCountUnavailable ? undefined : record.debatesTotal ); const hasClaims = hasRecordToShow( record.claimsLoading || record.claimsError || record.claimsCountUnavailable ? undefined : record.claimsTotal @@ -347,7 +345,7 @@ function ClaimTabPanel({ isError: record.debatesError, isCountUnavailable: record.debatesCountUnavailable, href: hrefs.debates, - seeAllLabel: 'See all debates', + seeAllLabel: 'View all debates', onSeeAll: onSelectSystemTab ? () => onSelectSystemTab('debates') : undefined, }, { @@ -359,7 +357,7 @@ function ClaimTabPanel({ isError: record.claimsError, isCountUnavailable: record.claimsCountUnavailable, href: hrefs.claims, - seeAllLabel: 'See all claims', + seeAllLabel: 'View all claims', onSeeAll: onSelectSystemTab ? () => onSelectSystemTab('claims') : undefined, }, ]; diff --git a/apps/web/core/debates/browse/debate-feed-player.tsx b/apps/web/core/debates/browse/debate-feed-player.tsx index 143aa60370..f5034b82c7 100644 --- a/apps/web/core/debates/browse/debate-feed-player.tsx +++ b/apps/web/core/debates/browse/debate-feed-player.tsx @@ -36,12 +36,7 @@ type DebateFeedPlayerProps = { preload?: boolean; }; -export function DebateFeedPlayer({ - debate, - active, - preload = false, - reducedOverlays = false, -}: DebateFeedPlayerProps) { +export function DebateFeedPlayer({ debate, active, preload = false, reducedOverlays = false }: DebateFeedPlayerProps) { // Loading is deliberately wider than playing. `useDebatePlayback`'s flag gates only the URL // fetch and the transcript query — playback is driven by `active` in the effect below — so a // preloading card fetches without autoplaying off-screen. @@ -324,7 +319,8 @@ export function DebateFeedPlayer({ data-debate-autoplay-blocked={autoplayBlocked ? 'true' : 'false'} // No gap and one radius on the outside: the two tiles are a single surface in the Figma // frame, which is what lets the subtitle straddle the seam rather than sit inside one tile. - className="group relative flex flex-col overflow-hidden rounded-xl" + // 12px in the compact gallery (a profile's or claim's Activity), 16px in the feeds. + className={cx('group relative flex flex-col overflow-hidden', reducedOverlays ? 'rounded-lg' : 'rounded-xl')} > + ); diff --git a/apps/web/design-system/button.tsx b/apps/web/design-system/button.tsx index 3b029da6cb..7fc3459495 100644 --- a/apps/web/design-system/button.tsx +++ b/apps/web/design-system/button.tsx @@ -16,7 +16,27 @@ type ButtonProps = React.ComponentPropsWithoutRef<'button'> & { small?: boolean; }; -const buttonClassNames = (className = '') => +/** The pill's shape and type, shared by both of its colourings below. */ +const PILL_BUTTON_SHAPE = + 'h-7 shrink-0 !gap-0 !rounded-full !px-2.5 !py-0 !text-[16px] !leading-[13px] font-normal tracking-[-0.35px] whitespace-nowrap !shadow-none'; + +/** + * The black pill: the navbar's Log in button. + * Layered over the `primary` variant, whose colours it overrides. + */ +export const PILL_BUTTON_CLASS_NAME = `${PILL_BUTTON_SHAPE} !border-transparent !bg-[#151515] !text-white hover:!bg-[#151515] focus-visible:!border-text focus-visible:!shadow-inner-text`; + +/** + * The same pill in the `secondary` variant's colours — white, a grey outline that + * darkens on hover. Layered over `secondary`, which supplies those colours. + */ +export const PILL_BUTTON_SECONDARY_CLASS_NAME = PILL_BUTTON_SHAPE; + +/** + * The button's classes on their own, for a link that should look like one. A + * ` + )} +
+ ); + } + return (
{ const { id, spaceId } = useEntityStoreInstance(); @@ -89,6 +92,7 @@ export const EntityPageCover = ({ compact={compact} withAvatar={withAvatar} contentMaxWidth={contentMaxWidth} + coverSize={coverSize} contentInsetClassName={contentInsetClassName} /> ); diff --git a/apps/web/partials/entity-page/entity-page-inline-description.tsx b/apps/web/partials/entity-page/entity-page-inline-description.tsx index 4068219cbe..abd5604ef4 100644 --- a/apps/web/partials/entity-page/entity-page-inline-description.tsx +++ b/apps/web/partials/entity-page/entity-page-inline-description.tsx @@ -24,10 +24,13 @@ export function EntityPageInlineDescription({ entityId, spaceId, fallbackDescription, + hideWhenReading = false, }: { entityId: string; spaceId: string; fallbackDescription?: string | null; + /** Shown only while editing. A profile reads its description in the rail's About section instead. */ + hideWhenReading?: boolean; }) { const isEditing = useUserIsEditing(spaceId); const { storage } = useMutate(); @@ -80,7 +83,7 @@ export function EntityPageInlineDescription({ ); } - if (!description) { + if (!description || hideWhenReading) { return null; } diff --git a/apps/web/partials/entity-page/entity-page-layout.ts b/apps/web/partials/entity-page/entity-page-layout.ts index 7897c2cc3f..ef23e34283 100644 --- a/apps/web/partials/entity-page/entity-page-layout.ts +++ b/apps/web/partials/entity-page/entity-page-layout.ts @@ -1,6 +1,8 @@ import type * as React from 'react'; export const ENTITY_PAGE_COVER_MAX_WIDTH = 1192; +/** A personal space profile's cover: 1200 × 300, a 4:1 banner. */ +export const PROFILE_COVER_SIZE = { maxWidth: 1200, height: 300 }; export const ENTITY_PAGE_CONTENT_MAX_WIDTH = 900; export const ENTITY_PAGE_WITH_SIDEBAR_MAX_WIDTH = 1142; diff --git a/apps/web/partials/entity-page/sticky-side-rail.tsx b/apps/web/partials/entity-page/sticky-side-rail.tsx index 5f8d449a13..bb66fbde68 100644 --- a/apps/web/partials/entity-page/sticky-side-rail.tsx +++ b/apps/web/partials/entity-page/sticky-side-rail.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; +import cx from 'classnames'; + /** * Sticky right-hand rail shared by Explore and space overview panels. * @@ -16,12 +18,36 @@ import * as React from 'react'; * `min()` applies that cap; `min-w` floors it, because a rail thinner than that stops being * readable. It is `shrink-0` so flexbox never takes it below that floor — past the floor the * rail is dropped outright by `lg:hidden` rather than shaved further. + * + * `flushTop` drops the top padding so the rail's first section lines up with the top of the + * column beside it. Without the padding, a stuck rail sits directly under the navbar. + * + * `divider` draws a vertical rule down the rail's left edge, in the tab bar's grey (`grey-02`), 20px + * from its content, running the rail's full height — which is the viewport's, less the navbar — so + * it reaches the bottom of the screen. The `before:` segment carries it up another 20px, across the + * `Spacer` a profile puts between its tab bar and this row, so the rule starts at the tab bar itself. + * The 20px is padding inside the scroller rather than on the rail, so a row whose hover background + * bleeds left of the content (the profile's Spaces list) is not clipped by `overflow-x-hidden`. */ -export function StickySideRail({ children }: { children: React.ReactNode }) { +export function StickySideRail({ + children, + flushTop = false, + divider = false, +}: { + children: React.ReactNode; + flushTop?: boolean; + divider?: boolean; +}) { return ( -