Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions apps/web/app/space/[id]/(space)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}
/>
<SpaceHeaderContentGate
serverHasSidebar={hasSidebar}
Expand Down Expand Up @@ -164,7 +165,7 @@ export default async function Layout(props0: LayoutProps) {
}
/>
{isProfile && <PersonalSpaceHeadline spaceId={spaceId} personEntityId={props.id} />}
<EntityPageInlineDescription entityId={props.id} spaceId={spaceId} />
<EntityPageInlineDescription entityId={props.id} spaceId={spaceId} hideWhenReading={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
Expand All @@ -190,7 +191,9 @@ export default async function Layout(props0: LayoutProps) {
/>
</div>

<div className="mt-6 flex flex-col gap-6">
{/* 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. */}
<div className={isProfile ? 'mt-10 flex flex-col gap-6' : 'mt-6 flex flex-col gap-6'}>
<AddDataPanel spaceId={spaceId} />

{typeIds.includes(SystemIds.PERSON_TYPE) ? (
Expand Down
8 changes: 3 additions & 5 deletions apps/web/core/claims/browse/claim-page-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
},
{
Expand All @@ -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,
},
];
Expand Down
10 changes: 3 additions & 7 deletions apps/web/core/debates/browse/debate-feed-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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')}
>
<DebaterVideo
participant={slot1Participant}
Expand Down
8 changes: 2 additions & 6 deletions apps/web/core/wallet/wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createGeoWalletConfig, createMockConfig } from '@geogenesis/auth/wallet

import * as React from 'react';

import { Button } from '~/design-system/button';
import { Button, PILL_BUTTON_CLASS_NAME } from '~/design-system/button';

import { trackPrivyAuth } from '../analytics';
import { Environment } from '../environment';
Expand Down Expand Up @@ -73,11 +73,7 @@ function PrivyConnectButton() {
};

return (
<Button
variant="primary"
className="h-7 shrink-0 !gap-0 !rounded-full !border-transparent !bg-[#151515] !px-2.5 !py-0 !text-[16px] !leading-[13px] font-normal tracking-[-0.35px] whitespace-nowrap !text-white !shadow-none hover:!bg-[#151515] focus-visible:!border-text focus-visible:!shadow-inner-text"
onClick={onLogin}
>
<Button variant="primary" className={PILL_BUTTON_CLASS_NAME} onClick={onLogin}>
Log in
</Button>
);
Expand Down
22 changes: 21 additions & 1 deletion apps/web/design-system/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
* `<Button>` inside a `<Link>` nests two interactive elements.
*/
export const buttonClassNames = (className = '') =>
cva(
`relative inline-flex items-center justify-center rounded border font-medium tracking-[-0.17px] shadow-light transition duration-200 ease-in-out focus:outline-hidden ${className}`,
{
Expand Down
34 changes: 33 additions & 1 deletion apps/web/design-system/clamped-text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ type ClampedTextProps = {
* description and its skills.
*/
label?: string;
/**
* Where More and Less sit. `inline` (the default) puts More over the end of the last line, which
* costs every line a gutter; `below` gives the text its full width and puts the toggle on a line
* of its own under it.
*/
togglePlacement?: 'inline' | 'below';
};

// No type of its own: the toggle takes the variant of the text it belongs to, so
Expand Down Expand Up @@ -115,6 +121,7 @@ export function ClampedText({
variant = 'body',
textClassName = '',
label,
togglePlacement = 'inline',
}: ClampedTextProps) {
assertSupportedMaxLines(maxLines);

Expand Down Expand Up @@ -148,9 +155,34 @@ export function ClampedText({

const showToggle = isOverflowing;
const clamp = !expanded;
const reserveToggle = showToggle && clamp;
const isBelow = togglePlacement === 'below';
const reserveToggle = showToggle && clamp && !isBelow;
const typeClassName = textStyles[variant];

if (isBelow) {
return (
<div ref={wrapperRef} className="box-border w-full min-w-0">
<Tag
ref={textRef as React.Ref<never>}
Comment on lines +162 to +166
className={cx(typeClassName, textClassName, clamp && LINE_CLAMP_CLASS[maxLines])}
>
{text}
</Tag>
{showToggle && (
<button
type="button"
onClick={() => setExpanded(value => !value)}
aria-expanded={expanded}
aria-label={label ? `Show ${expanded ? 'less' : 'more'} ${label}` : undefined}
className={cx(typeClassName, TOGGLE_CLASS, 'mt-1 block')}
>
{expanded ? 'Less' : 'More'}
</button>
)}
</div>
);
}

return (
<div ref={wrapperRef} className={cx('relative box-border w-full min-w-0', reserveToggle && TOGGLE_GUTTER_CLASS)}>
<Tag
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,17 @@ const COVER_PLACEHOLDER_HEIGHT = 120;
const AVATAR_OVERFLOW = 40;
const TRANSITION = { duration: 0.15, ease: 'easeInOut' as const };

// maxWidth is always ENTITY_PAGE_COVER_MAX_WIDTH so the wrapper never animates horizontally.
// When there's no cover the extra width is invisible (height is 0 or 40).
function computeLayout(hasCover: boolean, hasCoverImage: boolean, hasAvatar: boolean) {
type CoverSize = { maxWidth: number; height: number };

const DEFAULT_COVER_SIZE: CoverSize = { maxWidth: ENTITY_PAGE_COVER_MAX_WIDTH, height: COVER_IMAGE_HEIGHT };

// maxWidth is fixed for a page (ENTITY_PAGE_COVER_MAX_WIDTH unless the caller passes its own) so
// the wrapper never animates horizontally. When there's no cover the extra width is invisible
// (height is 0 or 40).
function computeLayout(hasCover: boolean, hasCoverImage: boolean, hasAvatar: boolean, coverSize: CoverSize) {
return {
height: hasCover
? hasCoverImage
? COVER_IMAGE_HEIGHT
: COVER_PLACEHOLDER_HEIGHT
: hasAvatar
? AVATAR_OVERFLOW
: 0,
maxWidth: ENTITY_PAGE_COVER_MAX_WIDTH,
height: hasCover ? (hasCoverImage ? coverSize.height : COVER_PLACEHOLDER_HEIGHT) : hasAvatar ? AVATAR_OVERFLOW : 0,
maxWidth: coverSize.maxWidth,
marginBottom: hasCover ? (hasAvatar ? 80 : 32) : hasAvatar ? 64 : 0,
marginTop: hasCover ? -24 : 0,
};
Expand All @@ -54,6 +53,7 @@ function computeLayout(hasCover: boolean, hasCoverImage: boolean, hasAvatar: boo
export const EditableCoverAvatarHeader = ({
avatarUrl,
contentMaxWidth = ENTITY_PAGE_CONTENT_MAX_WIDTH,
coverSize = DEFAULT_COVER_SIZE,
coverUrl,
compact = false,
withAvatar = false,
Expand All @@ -66,6 +66,8 @@ export const EditableCoverAvatarHeader = ({
* — a profile — passes the wider with-sidebar width instead.
*/
contentMaxWidth?: number;
/** How wide the cover can grow and how tall its image is. A profile passes its own. */
coverSize?: CoverSize;
/** Responsive inline padding inside the aligned content column. */
contentInsetClassName?: string;
coverUrl: string | null;
Expand Down Expand Up @@ -143,8 +145,8 @@ export const EditableCoverAvatarHeader = ({
);
}

const layout = computeLayout(hasCover, hasCoverImage, hasAvatar);
const coverHeight = hasCoverImage ? COVER_IMAGE_HEIGHT : COVER_PLACEHOLDER_HEIGHT;
const layout = computeLayout(hasCover, hasCoverImage, hasAvatar, coverSize);
const coverHeight = hasCoverImage ? coverSize.height : COVER_PLACEHOLDER_HEIGHT;
const mobileCoverHeightClass = hasCoverImage ? MOBILE_COVER_IMAGE_HEIGHT_CLASS : '';
const mobileCoverAvatarMarginClass = hasCoverImage && hasAvatar ? MOBILE_COVER_AVATAR_MARGIN_CLASS : '';

Expand Down
4 changes: 4 additions & 0 deletions apps/web/partials/entity-page/entity-page-cover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type EntityPageCoverProps = {
withAvatar?: boolean;
/** Width of the text column below, so the avatar lines up with the name. */
contentMaxWidth?: number;
/** How wide the cover can grow and how tall it is — see `EditableCoverAvatarHeader`. */
coverSize?: { maxWidth: number; height: number };
/** Responsive inline padding inside that column, shared with the content below. */
contentInsetClassName?: string;
};
Expand Down Expand Up @@ -75,6 +77,7 @@ export const EntityPageCover = ({
compact = false,
withAvatar = false,
contentMaxWidth,
coverSize,
contentInsetClassName,
}: EntityPageCoverProps) => {
const { id, spaceId } = useEntityStoreInstance();
Expand All @@ -89,6 +92,7 @@ export const EntityPageCover = ({
compact={compact}
withAvatar={withAvatar}
contentMaxWidth={contentMaxWidth}
coverSize={coverSize}
contentInsetClassName={contentInsetClassName}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -80,7 +83,7 @@ export function EntityPageInlineDescription({
);
}

if (!description) {
if (!description || hideWhenReading) {
return null;
}

Expand Down
2 changes: 2 additions & 0 deletions apps/web/partials/entity-page/entity-page-layout.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
32 changes: 29 additions & 3 deletions apps/web/partials/entity-page/sticky-side-rail.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as React from 'react';

import cx from 'classnames';

/**
* Sticky right-hand rail shared by Explore and space overview panels.
*
Expand All @@ -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 (
<aside className="sticky top-11 ml-8 flex h-[calc(100dvh-2.75rem)] w-[min(var(--width-side-rail),32%)] min-w-[var(--width-side-rail-min)] shrink-0 flex-col self-start lg:hidden">
<aside
className={cx(
'sticky top-11 ml-8 flex h-[calc(100dvh-2.75rem)] w-[min(var(--width-side-rail),32%)] min-w-[var(--width-side-rail-min)] shrink-0 flex-col self-start lg:hidden',
divider &&
"border-l border-grey-02 before:absolute before:-top-5 before:-left-px before:h-5 before:border-l before:border-grey-02 before:content-['']"
)}
>
<div className="no-scrollbar min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain">
<div className="flex flex-col pt-5 pb-6">{children}</div>
<div className={cx('flex flex-col pb-6', !flushTop && 'pt-5', divider && 'pl-5')}>{children}</div>
</div>
</aside>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/partials/profile/organization-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { FallbackImage } from '~/design-system/fallback-image';
* gradient beam, which is a different picture for every organisation and reads
* as a logo somebody chose rather than as the absence of one.
*/
export function OrganizationImage({ url, size }: { url: string | null | undefined; size: 16 | 20 | 36 }) {
const box = size === 36 ? 'h-9 w-9 rounded' : size === 20 ? 'h-5 w-5 rounded-sm' : 'h-4 w-4 rounded-sm';
export function OrganizationImage({ url, size }: { url: string | null | undefined; size: 16 | 20 | 50 }) {
const box = size === 50 ? 'h-[50px] w-[50px] rounded' : size === 20 ? 'h-5 w-5 rounded-sm' : 'h-4 w-4 rounded-sm';

return (
<span className={`relative shrink-0 overflow-hidden bg-grey-01 ${box}`}>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/partials/profile/personal-space-profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ function ProfileActivity({ spaceId, personEntityId }: { spaceId: string; personE
isCountUnavailable: isFactsError,
isError: debates.isError,
href: `/space/${spaceId}/debates`,
seeAllLabel: 'See all debates',
seeAllLabel: 'View all debates',
},
{
key: 'claims',
Expand All @@ -176,7 +176,7 @@ function ProfileActivity({ spaceId, personEntityId }: { spaceId: string; personE
isCountUnavailable: isFactsError && responses.isError,
isError: positions.isError,
href: `/space/${spaceId}/positions`,
seeAllLabel: 'See all claims',
seeAllLabel: 'View all claims',
},
];

Expand Down
Loading
Loading