Skip to content

Commit 33b047b

Browse files
committed
Refine profile page: unify small text styles with new font-microcaps, replace custom separators with DottedList, and update layout gaps for improved spacing logic and theme consistency.
1 parent 69872de commit 33b047b

5 files changed

Lines changed: 133 additions & 75 deletions

File tree

web/components/profile-about.tsx

Lines changed: 85 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -216,30 +216,79 @@ export function ProfileConnectionGoals(props: {profile: Profile}) {
216216

217217
const {seekingText, relationshipText, romanticStyles} = goals
218218

219-
const status = [relationshipText, ...(romanticStyles ?? [])].filter(Boolean).join(' · ')
219+
const status = [relationshipText, ...(romanticStyles ?? [])].filter(Boolean).join(DOT_SEPARATOR)
220220

221221
return (
222222
<div
223223
className="border-canvas-300 mt-2 flex max-w-2xl flex-wrap gap-x-16 gap-y-6 border-t pt-6"
224224
data-testid="profile-connection-goals"
225225
>
226-
<HeroFact label={t('profile.looking_for', 'Looking for')}>{seekingText}</HeroFact>
227-
{status && <HeroFact label={t('profile.status', 'Status')}>{status}</HeroFact>}
226+
{/* Split back apart at render: `seekingText` is still consumed as one plain string by
227+
`SeekingAndRelationship`, so the builders keep returning strings. */}
228+
<HeroFact label={t('profile.looking_for', 'Looking for')}>
229+
<DottedList items={seekingText.split(DOT_SEPARATOR)} />
230+
</HeroFact>
231+
{status && (
232+
<HeroFact label={t('profile.status', 'Status')}>
233+
<DottedList items={status.split(DOT_SEPARATOR)} />
234+
</HeroFact>
235+
)}
228236
</div>
229237
)
230238
}
231239

240+
export const DOT_SEPARATOR = ' · '
241+
242+
/**
243+
* A run of fragments joined by middots, with the middots stepped back from the words.
244+
*
245+
* The separator is punctuation, not content, so it should not sit at the same weight as the text it
246+
* divides — the eyebrow over the name has always dimmed its own slashes, and these now match.
247+
*
248+
* Carries no colour or size of its own: every caller has a different register (a hero fact, a tag
249+
* run, a qualifier under a rail value) and the run should inherit whichever it lands in.
250+
*/
251+
function DottedList(props: {items: string[]}) {
252+
const {items} = props
253+
return (
254+
<>
255+
{items.map((item, i) => (
256+
<React.Fragment key={i}>
257+
{/* The spaces around the middot are load-bearing, not decoration: they are the only
258+
soft-wrap opportunities in the run. Spacing the separator with padding instead leaves the
259+
whole list one unbreakable inline chain, which overflows any container narrower than the
260+
full run — the Interests block pushed the grid wide enough to scroll the page sideways. */}
261+
{i > 0 && (
262+
<>
263+
{' '}
264+
<span aria-hidden className="text-ink-500/50 select-none">
265+
·
266+
</span>{' '}
267+
</>
268+
)}
269+
{item}
270+
</React.Fragment>
271+
))}
272+
</>
273+
)
274+
}
275+
276+
/**
277+
* Label over value, for the two facts that close the hero.
278+
*
279+
* Both lines stepped down from where they were. The label was `ink-400`, the one neutral grey in a
280+
* warm ramp, at 10px — 2.15:1 in light and 3.0:1 in dark, under AA either way for text that small.
281+
* The value was `ink-900`, which in dark (#F7F4EF) is within a percent of the name's white, so a
282+
* third-tier fact was rendering at the brightest value on the page. Only the name keeps that now.
283+
*/
232284
function HeroFact(props: {label: string; children: ReactNode}) {
233285
const {label, children} = props
234286
return (
235287
<div className="min-w-0">
236-
<div
237-
className="text-ink-400 font-dm-sans uppercase"
238-
style={{fontSize: '10px', letterSpacing: '0.18em', marginBottom: '7px'}}
239-
>
288+
<div className="text-ink-500 font-microcaps" style={{marginBottom: '7px'}}>
240289
{label}
241290
</div>
242-
<div className="text-ink-900" style={{fontSize: '15.5px'}}>
291+
<div className="text-ink-700 font-dm-sans" style={{fontSize: '15.5px'}}>
243292
{children}
244293
</div>
245294
</div>
@@ -361,7 +410,7 @@ export function getCompactSeekingText(profile: Profile, t: any) {
361410
? t('profile.age_max_compact', 'under {max}', {max})
362411
: t('profile.age_range_compact', '{min}-{max}', {min, max})
363412

364-
return [connection, genderText, ageText].filter(Boolean).join(' · ')
413+
return [connection, genderText, ageText].filter(Boolean).join(DOT_SEPARATOR)
365414
}
366415

367416
function capitalizeFirst(s: string) {
@@ -610,16 +659,19 @@ function Cannabis(props: {profile: Profile}) {
610659
const prefText = formatPartnerPreferences(profile.cannabis_pref, t)
611660

612661
return (
613-
<AboutRow title={t('profile.cannabis', 'Cannabis')} text={parts}>
614-
{showIntentions && (
615-
<div className="flex flex-wrap gap-2 mt-2">
616-
{profile.cannabis_intention!.map((i) => (
617-
<Chip key={i}>
618-
{t(`profile.substance_intention.${i}`, INVERTED_SUBSTANCE_INTENTION_CHOICES[i])}
619-
</Chip>
620-
))}
621-
</div>
622-
)}
662+
<AboutRow
663+
title={t('profile.cannabis', 'Cannabis')}
664+
text={parts}
665+
details={
666+
showIntentions && (
667+
<DottedList
668+
items={profile.cannabis_intention!.map((i) =>
669+
t(`profile.substance_intention.${i}`, INVERTED_SUBSTANCE_INTENTION_CHOICES[i]),
670+
)}
671+
/>
672+
)
673+
}
674+
>
623675
{prefText && (
624676
<div className={'text-ink-500 mt-2'} style={{fontSize: '12.5px'}}>
625677
{prefText}
@@ -645,16 +697,19 @@ function Psychedelics(props: {profile: Profile}) {
645697
const prefText = formatPartnerPreferences(profile.psychedelics_pref, t)
646698

647699
return (
648-
<AboutRow title={t('profile.psychedelics', 'Psychedelics')} text={parts}>
649-
{showIntentions && (
650-
<div className="flex flex-wrap gap-2 mt-2">
651-
{profile.psychedelics_intention!.map((i) => (
652-
<Chip key={i}>
653-
{t(`profile.substance_intention.${i}`, INVERTED_SUBSTANCE_INTENTION_CHOICES[i])}
654-
</Chip>
655-
))}
656-
</div>
657-
)}
700+
<AboutRow
701+
title={t('profile.psychedelics', 'Psychedelics')}
702+
text={parts}
703+
details={
704+
showIntentions && (
705+
<DottedList
706+
items={profile.psychedelics_intention!.map((i) =>
707+
t(`profile.substance_intention.${i}`, INVERTED_SUBSTANCE_INTENTION_CHOICES[i]),
708+
)}
709+
/>
710+
)
711+
}
712+
>
658713
{prefText && (
659714
<div className={'text-ink-500 mt-2'} style={{fontSize: '12.5px'}}>
660715
{prefText}
@@ -834,16 +889,7 @@ function TagList(props: {items: string[]}) {
834889
const {items} = props
835890
return (
836891
<div className="text-primary-900" style={{fontSize: '15px', lineHeight: '1.65'}}>
837-
{items.map((item, i) => (
838-
<React.Fragment key={item}>
839-
{i > 0 && (
840-
<span aria-hidden className="text-ink-400 select-none">
841-
{' · '}
842-
</span>
843-
)}
844-
{item}
845-
</React.Fragment>
846-
))}
892+
<DottedList items={items} />
847893
</div>
848894
)
849895
}

web/components/profile/profile-hero.tsx

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,17 @@ export default function ProfileHero(props: {
121121
</Col>
122122

123123
{/* The tagline is the one thing on this page written to be read as a voice, so it gets the
124-
serif italic at display size instead of a quoted aside pinned behind a rule. */}
124+
serif italic at display size instead of a quoted aside pinned behind a rule.
125+
The colour has to be per-theme, because the primary ramp inverts and no single step is an
126+
accent on both grounds. Dark gets `primary-700` (#DCAB71), warm gold that steps down from
127+
the name's white — as `primary-900` it was #F3E4CE, within a few percent of the name, so
128+
the amber did no work. Light gets no accent at all: any amber dark enough to clear AA on
129+
the beige canvas is a mid-brown (700 is #855022), which reads as muddy rather than warm.
130+
There the italic serif is already doing the differentiating, and `ink-700` just steps it
131+
back from the black of the name.
132+
The negative indent hangs the opening quote into the margin so the first word lines up
133+
with the name's first letter; without it the glyph pushes the text right by ~0.35em and
134+
the tagline looks accidentally indented. */}
125135
{profile.headline && (
126136
<div
127137
className="font-heading text-primary-900 max-w-[38ch] italic sm:max-w-[52ch]"
@@ -130,29 +140,29 @@ export default function ProfileHero(props: {
130140
fontSize: 'clamp(1.15rem, 1.5vw, 1.5rem)',
131141
lineHeight: '1.5',
132142
fontWeight: 400,
143+
textIndent: '-0.38em',
133144
}}
134145
>
135146
{profile.headline}
136147
</div>
137148
)}
138149

139-
{/* Outlined rather than filled, squared rather than pill: five filled pills read as five
140-
buttons. These are labels, and the eye should land on them last, after the tagline.
141-
Same tracking as the eyebrow they echo, and a border kept under the eyebrow's weight so
142-
a row of them does not out-shout the line that opens the block. */}
150+
{/* Unboxed: the rail already established that an outline means "you can click this" (see the
151+
`Chip` docstring in profile-about), and nothing here is clickable. The border was also
152+
carrying almost no weight — `canvas-300` sits at 1.30:1 on the light canvas and 1.75:1 on
153+
the dark one, so the boxes read as ghosts rather than as objects, and no single value
154+
fixes both: the ink ramp inverts, so any border strong enough to register in light is a
155+
cage in dark.
156+
Separation comes from the gap instead, opened to 24px because without a box the only
157+
thing dividing two tags is space, and 8px against the 0.16em tracking inside them read as
158+
one continuous run.
159+
Colour is off the brand ramp on purpose: as `primary-800` these were near-cream in dark
160+
mode, so the thing meant to be read last was as bright as the tagline, and amber stopped
161+
meaning anything by being used twice. */}
143162
{profile.keywords && profile.keywords.length > 0 && (
144-
<Row className="max-w-3xl flex-wrap gap-2" data-testid="profile-keywords">
163+
<Row className="max-w-3xl flex-wrap gap-x-6 gap-y-2.5" data-testid="profile-keywords">
145164
{profile.keywords.map(capitalizePure).map((tag, i) => (
146-
<span
147-
key={i}
148-
className="border-canvas-300/70 text-primary-800 font-dm-sans rounded-[3px] border uppercase"
149-
style={{
150-
padding: '6px 11px',
151-
fontSize: '11px',
152-
fontWeight: '400',
153-
letterSpacing: '0.16em',
154-
}}
155-
>
165+
<span key={i} className="text-ink-700 font-microcaps">
156166
{tag.trim()}
157167
</span>
158168
))}

web/components/profile/profile-info.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import {getStars} from 'web/lib/supabase/stars'
3939

4040
import {BackButton} from '../back-button'
4141

42+
const signupMessage = 'Sign up to connect with them for free'
43+
4244
export function ProfileInfo(props: {
4345
profile: Profile
4446
user: User
@@ -209,12 +211,7 @@ export function ProfileInfo(props: {
209211
<div className="from-canvas-50 absolute bottom-0 h-12 w-full bg-gradient-to-t to-transparent" />
210212
</Col>
211213
<Row className="gap-2">
212-
<SignUpButton
213-
text={t(
214-
'profile.info.signup_to_see',
215-
'Sign up to see their full profile or connect with them',
216-
)}
217-
/>
214+
<SignUpButton text={t('profile.info.signup_to_see', signupMessage)} />
218215
</Row>
219216
</Col>
220217
)}
@@ -567,14 +564,7 @@ function ProfileContent(props: {
567564
)}
568565
</Col>
569566
)}
570-
{!currentUser && (
571-
<SignUpButton
572-
text={t(
573-
'profile.info.signup_to_see',
574-
'Sign up to see their full profile or connect with them',
575-
)}
576-
/>
577-
)}
567+
{!currentUser && <SignUpButton text={t('profile.info.signup_to_see', signupMessage)} />}
578568
</Col>
579569
</div>
580570
{/*<LikesDisplay*/}

web/components/profile/profile-primary-info.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,19 @@ export default function ProfilePrimaryInfo(props: {
4141

4242
return (
4343
<div
44-
className={clsx(
45-
'text-ink-500 flex flex-wrap items-center',
46-
eyebrow && 'font-dm-sans uppercase',
47-
)}
44+
className={clsx('text-ink-500 flex flex-wrap items-center', eyebrow && 'font-microcaps')}
4845
data-testid="profile-gender-location-height-inches"
49-
style={eyebrow ? {fontSize: '11px', letterSpacing: '0.16em'} : {fontSize: '15px'}}
46+
style={eyebrow ? undefined : {fontSize: '15px'}}
5047
>
5148
{parts.map((part, i) => (
5249
<React.Fragment key={i}>
50+
{/* `ink-500/50` rather than `ink-400`: the latter is the one neutral grey in a warm ramp
51+
(#A0A0A0 light, #646464 dark) and read as a different, colder family than the text it
52+
separates. Half-strength ink-500 stays warm and still steps back. */}
5353
{i > 0 && (
5454
<span
5555
aria-hidden
56-
className={clsx('text-ink-400 select-none', eyebrow ? 'px-2.5' : 'px-3.5')}
56+
className={clsx('text-ink-500/50 select-none', eyebrow ? 'px-2.5' : 'px-3.5')}
5757
>
5858
/
5959
</span>

web/styles/globals.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,18 @@ ol > li::marker {
650650
font-size: 14px;
651651
}
652652

653+
/* The small tracked caps that label things on the profile — the eyebrow over the name, the keyword
654+
chips, the "Looking for" / "Status" labels. These had drifted to three near-identical variants
655+
(11px/0.16em, 11px/0.16em, 10px/0.18em); a 1px and 0.02em difference is not enough to read as a
656+
distinct level, only enough to look misaligned. One style, and colour carries the hierarchy. */
657+
.font-microcaps {
658+
font-family: 'DM Sans', sans-serif;
659+
font-size: 11px;
660+
font-weight: 500;
661+
letter-spacing: 0.16em;
662+
text-transform: uppercase;
663+
}
664+
653665
input {
654666
transition: background-color 0.2s ease-in-out;
655667
}

0 commit comments

Comments
 (0)