fix(website): restore condensed marketplace list card (#23161)
## Summary The condensed marketplace list-card design (sans-serif partner name, description excerpt, a single scope line, and a "View profile →" text CTA) was the intended glowup card, shipped by #22471/#22402. During the #23016 resync, the `PartnerCard.tsx` merge conflict was resolved in favor of main's rich card, so the condensed design never actually landed on `main`. This PR restores the condensed design, ported onto main's current data layer and helpers — none of #23016's profile/case-study/matching work is touched. ## What changed - `PartnerCard.tsx` — rewritten to render the condensed layout (sans-serif `PartnerName`, `CardIntro` description excerpt, `CardFoot` with a single `ScopeLine` and a mono `CardCta` "View profile →" text link, no chip rows, no money row, no LinkedIn icon). Reuses main's data layer and helpers directly: `richTextExcerpt`, `resolvePartnerScopeCards`, `PartnerAvatar`, `titleCaseFallback`, and the shared `CardFrame` shell (same entrance-animation/hover idiom already used by `MarketplaceMatchCard`). No new helper files were needed — everything the condensed design requires already exists on `main`. - `PartnerChipRow.tsx` — deleted (zero remaining importers after the port; grep confirmed only self-reference). - `PartnerMoneyRow.tsx` — deleted (zero remaining importers after the port; grep confirmed only self-reference). Kept untouched: `fetch-live-marketplace-partners.ts`, `marketplace-partner.ts`, `get-marketplace-partners.ts`, `marketplace-partners-source.ts`, all `PartnerProfile*`/case-study/matching files, `MarketplaceGrid.tsx` (already has the `repeat(N, minmax(0, 1fr))` grid columns on main — no change needed), and the shared label helpers `PARTNER_SCOPE_LABELS`/`SERVED_GEO_LABELS`/`SPOKEN_LANGUAGE_LABELS` (still used by `FilterBar.tsx`/`PartnerReachFacts.tsx`). Website-only change — no partners-app version bump, no `.po` catalog changes. ## Test plan - [x] `npx nx typecheck twenty-website` — clean - [x] `npx oxlint -c .oxlintrc.json .` (twenty-website) — 0 warnings, 0 errors - [x] `npx oxfmt --check .` (twenty-website) — clean - [x] `npx jest partners-marketplace` — 12 suites / 54 tests passing - [x] Verified locally against the running dev server (`/partners/list`): 16 partner cards render with the condensed layout (sans-serif name, excerpt, single scope line, "View profile →"), no chip rows/money rows/LinkedIn icons, real partner data renders correctly (e.g. 01GROWTH, Inc) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23161?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { IconBrandLinkedin } from '@tabler/icons-react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { LocalizedLink } from '@/platform/i18n/LocalizedLink';
|
||||
import {
|
||||
color,
|
||||
FONT_WEIGHT,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
@@ -14,30 +14,24 @@ import {
|
||||
semanticColor,
|
||||
spacing,
|
||||
} from '@/tokens';
|
||||
import { Button, ExternalLink } from '@/ui';
|
||||
|
||||
import { isSafeHttpUrl } from './is-safe-http-url';
|
||||
import { CardFrame, type PartnerCardIndexStyle } from './MarketplaceCardFrame';
|
||||
import { type MarketplacePartner } from './marketplace-partner';
|
||||
import { PartnerAvatar } from './PartnerAvatar';
|
||||
import { PartnerChipRow } from './PartnerChipRow';
|
||||
import { PartnerMoneyRow } from './PartnerMoneyRow';
|
||||
import { PARTNER_SCOPE_LABELS } from './partner-scope-labels';
|
||||
import { resolvePartnerScopeCards } from './resolve-partner-scope-cards';
|
||||
import { richTextExcerpt } from './rich-text-excerpt';
|
||||
import { SERVED_GEO_LABELS } from './served-geo-labels';
|
||||
import { SPOKEN_LANGUAGE_LABELS } from './spoken-language-labels';
|
||||
import { titleCaseFallback } from './title-case-fallback';
|
||||
|
||||
const CardArticle = styled(CardFrame)`
|
||||
gap: ${spacing(5)};
|
||||
padding: ${spacing(6)};
|
||||
gap: ${spacing(3.5)};
|
||||
padding: ${spacing(5.5)} ${spacing(5.5)} ${spacing(4.5)};
|
||||
will-change: transform;
|
||||
`;
|
||||
|
||||
const CardHeader = styled.div`
|
||||
const CardTop = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${spacing(4)};
|
||||
gap: ${spacing(3.25)};
|
||||
`;
|
||||
|
||||
const HeaderText = styled.div`
|
||||
@@ -46,30 +40,22 @@ const HeaderText = styled.div`
|
||||
min-width: 0;
|
||||
|
||||
& > * + * {
|
||||
margin-top: ${spacing(1)};
|
||||
margin-top: ${spacing(0.75)};
|
||||
}
|
||||
`;
|
||||
|
||||
const NameRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${spacing(2)};
|
||||
`;
|
||||
|
||||
const PartnerName = styled.h3`
|
||||
color: ${semanticColor.ink};
|
||||
font-family: ${fontFamily('serif')};
|
||||
font-size: ${fontSize(6)};
|
||||
font-weight: ${FONT_WEIGHT.light};
|
||||
letter-spacing: -0.02em;
|
||||
line-height: ${fontSize(7)};
|
||||
font-family: ${fontFamily('sans')};
|
||||
font-size: ${fontSize(4.25)};
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.2;
|
||||
`;
|
||||
|
||||
// The card's whole-surface link uses the "stretched link" pattern: an inline
|
||||
// <a> on the name with an absolutely-positioned ::after that covers the
|
||||
// entire <article>. Other anchors inside the card (LinkedIn icon, View
|
||||
// profile) sit above this overlay via z-index, so each stays an independent
|
||||
// click target without illegal nested <a> elements.
|
||||
// entire <article>.
|
||||
const NameLink = styled(LocalizedLink)`
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
@@ -88,69 +74,62 @@ const NameLink = styled(LocalizedLink)`
|
||||
}
|
||||
`;
|
||||
|
||||
const LinkedinIconLink = styled(ExternalLink)`
|
||||
align-items: center;
|
||||
color: ${semanticColor.inkMuted};
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
transition: color 0.2s ease;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
color: ${semanticColor.ink};
|
||||
}
|
||||
`;
|
||||
|
||||
const LocationEyebrow = styled.span`
|
||||
color: ${semanticColor.inkMuted};
|
||||
font-family: ${fontFamily('mono')};
|
||||
font-size: ${fontSize(3)};
|
||||
font-weight: ${FONT_WEIGHT.medium};
|
||||
letter-spacing: 0.08em;
|
||||
line-height: ${fontSize(4)};
|
||||
font-size: ${fontSize(2.625)};
|
||||
letter-spacing: 0.1em;
|
||||
line-height: 1.2;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const Introduction = styled.p`
|
||||
const CardIntro = styled.p`
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
color: ${semanticColor.inkMuted};
|
||||
display: -webkit-box;
|
||||
font-family: ${fontFamily('sans')};
|
||||
font-size: ${fontSize(4)};
|
||||
line-height: ${fontSize(5.5)};
|
||||
font-size: ${fontSize(3.625)};
|
||||
line-height: 1.5;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
`;
|
||||
|
||||
const CardFoot = styled.div`
|
||||
align-items: center;
|
||||
border-top: 1px solid ${semanticColor.line};
|
||||
display: flex;
|
||||
gap: ${spacing(3.5)};
|
||||
justify-content: space-between;
|
||||
margin-top: ${spacing(0.5)};
|
||||
padding-top: ${spacing(3.5)};
|
||||
`;
|
||||
|
||||
const ScopeLine = styled.p`
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
color: ${semanticColor.inkMuted};
|
||||
display: -webkit-box;
|
||||
flex: 1;
|
||||
font-family: ${fontFamily('sans')};
|
||||
font-size: ${fontSize(3.5)};
|
||||
font-weight: ${FONT_WEIGHT.medium};
|
||||
line-height: 1.4;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Divider = styled.hr`
|
||||
background-color: ${semanticColor.line};
|
||||
border: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ChipRows = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& > * + * {
|
||||
margin-top: ${spacing(3)};
|
||||
}
|
||||
`;
|
||||
|
||||
// The price row pins to the card bottom (margin-top: auto), so the button —
|
||||
// its plain trailing sibling — sits right under it with the same gap on every
|
||||
// card, regardless of how many lines the chip rows took. The flexible space
|
||||
// lands above the price (between the chips and the price), not between the
|
||||
// price and the button.
|
||||
const MoneyRowPin = styled.div`
|
||||
margin-top: auto;
|
||||
`;
|
||||
|
||||
const CtaWrapper = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
const CardCta = styled.span`
|
||||
align-items: center;
|
||||
color: ${color('blue')};
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
font-family: ${fontFamily('mono')};
|
||||
font-size: ${fontSize(2.75)};
|
||||
gap: ${spacing(1.25)};
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
type PartnerCardProps = {
|
||||
@@ -162,7 +141,7 @@ export function PartnerCard({ partner, index }: PartnerCardProps) {
|
||||
const { i18n } = useLingui();
|
||||
const headingId = `partner-card-heading-${partner.slug}`;
|
||||
const style: PartnerCardIndexStyle = { '--partner-card-index': index };
|
||||
// Unprefixed; LocalizedLink (the name link) and the Button add the locale.
|
||||
// Unprefixed; LocalizedLink (the name link) adds the locale.
|
||||
const profileHref = `/partners/profile/${partner.slug}`;
|
||||
|
||||
const locationLine = [
|
||||
@@ -172,77 +151,37 @@ export function PartnerCard({ partner, index }: PartnerCardProps) {
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
const linkedinHref =
|
||||
partner.links.linkedin !== null && isSafeHttpUrl(partner.links.linkedin)
|
||||
? partner.links.linkedin
|
||||
: null;
|
||||
const scopeCards = resolvePartnerScopeCards(partner.partnerScope);
|
||||
const scopeLine = scopeCards.map((scope) => i18n._(scope.label)).join(' · ');
|
||||
|
||||
const descriptionExcerpt = richTextExcerpt(partner.description);
|
||||
|
||||
return (
|
||||
<CardArticle aria-labelledby={headingId} style={style}>
|
||||
<CardHeader>
|
||||
<CardTop>
|
||||
<PartnerAvatar
|
||||
name={partner.name}
|
||||
slug={partner.slug}
|
||||
profilePictureUrl={partner.profilePictureUrl}
|
||||
/>
|
||||
<HeaderText>
|
||||
<NameRow>
|
||||
<PartnerName id={headingId}>
|
||||
<NameLink href={profileHref}>{partner.name}</NameLink>
|
||||
</PartnerName>
|
||||
{linkedinHref !== null && (
|
||||
<LinkedinIconLink
|
||||
href={linkedinHref}
|
||||
aria-label={i18n._(msg`View ${partner.name} on LinkedIn`)}
|
||||
>
|
||||
<IconBrandLinkedin size={16} aria-hidden="true" />
|
||||
</LinkedinIconLink>
|
||||
)}
|
||||
</NameRow>
|
||||
<PartnerName id={headingId}>
|
||||
<NameLink href={profileHref}>{partner.name}</NameLink>
|
||||
</PartnerName>
|
||||
{locationLine.length > 0 && (
|
||||
<LocationEyebrow>{locationLine}</LocationEyebrow>
|
||||
)}
|
||||
</HeaderText>
|
||||
</CardHeader>
|
||||
</CardTop>
|
||||
|
||||
<Introduction>{descriptionExcerpt}</Introduction>
|
||||
<CardIntro>{descriptionExcerpt}</CardIntro>
|
||||
|
||||
<Divider aria-hidden="true" />
|
||||
|
||||
<ChipRows>
|
||||
<PartnerChipRow
|
||||
label={msg`Regions`}
|
||||
values={partner.region}
|
||||
valueLabels={SERVED_GEO_LABELS}
|
||||
/>
|
||||
<PartnerChipRow
|
||||
label={msg`Languages`}
|
||||
values={partner.languagesSpoken}
|
||||
valueLabels={SPOKEN_LANGUAGE_LABELS}
|
||||
/>
|
||||
<PartnerChipRow
|
||||
label={msg`Categories`}
|
||||
values={partner.partnerScope}
|
||||
valueLabels={PARTNER_SCOPE_LABELS}
|
||||
/>
|
||||
</ChipRows>
|
||||
|
||||
<MoneyRowPin>
|
||||
<PartnerMoneyRow
|
||||
hourlyRateUsd={partner.hourlyRateUsd}
|
||||
projectBudgetMinUsd={partner.projectBudgetMinUsd}
|
||||
/>
|
||||
</MoneyRowPin>
|
||||
|
||||
<CtaWrapper>
|
||||
<Button
|
||||
href={profileHref}
|
||||
label={i18n._(msg`View profile`)}
|
||||
variant="filled"
|
||||
/>
|
||||
</CtaWrapper>
|
||||
<CardFoot>
|
||||
{scopeLine.length > 0 && (
|
||||
<ScopeLine title={scopeLine}>{scopeLine}</ScopeLine>
|
||||
)}
|
||||
<CardCta>{i18n._(msg`View profile`)} →</CardCta>
|
||||
</CardFoot>
|
||||
</CardArticle>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import {
|
||||
fontFamily,
|
||||
FONT_WEIGHT,
|
||||
fontSize,
|
||||
mediaUp,
|
||||
semanticColor,
|
||||
spacing,
|
||||
} from '@/tokens';
|
||||
|
||||
import { partnerChipClassName } from './partner-chip-style';
|
||||
import { titleCaseFallback } from './title-case-fallback';
|
||||
|
||||
const Row = styled.dl`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${spacing(2)};
|
||||
|
||||
${mediaUp('md')} {
|
||||
// Align the label to the first chip's baseline rather than the row's
|
||||
// center: when chips wrap to multiple lines in a narrow card, centering
|
||||
// floats the label to the middle and visually lifts the first chip into
|
||||
// the row above. Baseline keeps the label pinned beside the first chip.
|
||||
align-items: baseline;
|
||||
flex-direction: row;
|
||||
gap: ${spacing(4)};
|
||||
}
|
||||
`;
|
||||
|
||||
const RowLabel = styled.dt`
|
||||
color: ${semanticColor.inkMuted};
|
||||
font-family: ${fontFamily('mono')};
|
||||
font-size: ${fontSize(3)};
|
||||
font-weight: ${FONT_WEIGHT.medium};
|
||||
letter-spacing: 0.08em;
|
||||
line-height: ${fontSize(4)};
|
||||
text-transform: uppercase;
|
||||
|
||||
${mediaUp('md')} {
|
||||
flex-shrink: 0;
|
||||
width: 80px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChipList = styled.ul`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${spacing(2)};
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
type PartnerChipRowProps<TValue extends string> = {
|
||||
label: MessageDescriptor;
|
||||
values: readonly TValue[];
|
||||
valueLabels: Record<TValue, MessageDescriptor>;
|
||||
};
|
||||
|
||||
export function PartnerChipRow<TValue extends string>({
|
||||
label,
|
||||
values,
|
||||
valueLabels,
|
||||
}: PartnerChipRowProps<TValue>) {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<RowLabel>{i18n._(label)}</RowLabel>
|
||||
<dd>
|
||||
<ChipList>
|
||||
{values.map((value) => {
|
||||
const descriptor = valueLabels[value];
|
||||
const text = descriptor
|
||||
? i18n._(descriptor)
|
||||
: titleCaseFallback(value);
|
||||
return (
|
||||
<li key={value} className={partnerChipClassName}>
|
||||
{text}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ChipList>
|
||||
</dd>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { IconBriefcase, IconCurrencyDollar } from '@tabler/icons-react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import {
|
||||
color,
|
||||
fontFamily,
|
||||
FONT_WEIGHT,
|
||||
fontSize,
|
||||
semanticColor,
|
||||
spacing,
|
||||
} from '@/tokens';
|
||||
|
||||
import { formatUsdCompact } from './format-usd-compact';
|
||||
|
||||
const Row = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${spacing(2)};
|
||||
`;
|
||||
|
||||
const Pill = styled.span`
|
||||
align-items: center;
|
||||
background-color: ${color('white')};
|
||||
border: 1px solid ${semanticColor.line};
|
||||
border-radius: 999px;
|
||||
color: ${color('black-80')};
|
||||
display: inline-flex;
|
||||
font-family: ${fontFamily('sans')};
|
||||
font-size: ${fontSize(3)};
|
||||
font-weight: ${FONT_WEIGHT.medium};
|
||||
gap: ${spacing(1.5)};
|
||||
line-height: 1;
|
||||
padding: ${spacing(1.5)} ${spacing(3)};
|
||||
`;
|
||||
|
||||
type PartnerMoneyRowProps = {
|
||||
hourlyRateUsd: number | null;
|
||||
projectBudgetMinUsd: number | null;
|
||||
};
|
||||
|
||||
export function PartnerMoneyRow({
|
||||
hourlyRateUsd,
|
||||
projectBudgetMinUsd,
|
||||
}: PartnerMoneyRowProps) {
|
||||
const { i18n } = useLingui();
|
||||
const hourly = formatUsdCompact(hourlyRateUsd);
|
||||
const minBudget = formatUsdCompact(projectBudgetMinUsd);
|
||||
|
||||
if (!hourly && !minBudget) return null;
|
||||
|
||||
return (
|
||||
<Row aria-label={i18n._(msg`Pricing`)}>
|
||||
{hourly && (
|
||||
<Pill>
|
||||
<IconCurrencyDollar size={14} aria-hidden="true" />
|
||||
{i18n._(msg`${hourly}/hr`)}
|
||||
</Pill>
|
||||
)}
|
||||
{minBudget && (
|
||||
<Pill>
|
||||
<IconBriefcase size={14} aria-hidden="true" />
|
||||
{i18n._(msg`from ${minBudget}`)}
|
||||
</Pill>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { formatUsdCompact } from './format-usd-compact';
|
||||
|
||||
describe('formatUsdCompact', () => {
|
||||
it('returns null when the amount is null', () => {
|
||||
expect(formatUsdCompact(null)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when the amount is NaN', () => {
|
||||
expect(formatUsdCompact(Number.NaN)).toBeNull();
|
||||
});
|
||||
|
||||
it('renders sub-thousand amounts in full', () => {
|
||||
expect(formatUsdCompact(140)).toBe('$140');
|
||||
});
|
||||
|
||||
it('renders zero', () => {
|
||||
expect(formatUsdCompact(0)).toBe('$0');
|
||||
});
|
||||
|
||||
it('compacts thousands', () => {
|
||||
expect(formatUsdCompact(8000)).toBe('$8K');
|
||||
});
|
||||
|
||||
it('compacts thousands to one decimal', () => {
|
||||
expect(formatUsdCompact(8500)).toBe('$8.5K');
|
||||
});
|
||||
|
||||
it('compacts larger budgets', () => {
|
||||
expect(formatUsdCompact(25000)).toBe('$25K');
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
const COMPACT_FORMATTER = new Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
|
||||
// Compact USD for the marketplace pricing pills ($140/hr, from $8K). A null
|
||||
// amount (the partner left the rate unset) returns null so the caller can omit
|
||||
// the pill entirely rather than render "$0".
|
||||
export const formatUsdCompact = (usd: number | null): string | null => {
|
||||
if (usd === null || Number.isNaN(usd)) return null;
|
||||
return COMPACT_FORMATTER.format(usd);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
import { css } from '@linaria/core';
|
||||
|
||||
import {
|
||||
color,
|
||||
fontFamily,
|
||||
FONT_WEIGHT,
|
||||
fontSize,
|
||||
radius,
|
||||
semanticColor,
|
||||
spacing,
|
||||
} from '@/tokens';
|
||||
|
||||
// The static label chip on partner cards: a bordered, uppercase mono tag for
|
||||
// regions, languages, and categories.
|
||||
export const partnerChipClassName = css`
|
||||
border: 1px solid ${semanticColor.line};
|
||||
border-radius: ${radius(4)};
|
||||
color: ${color('black-80')};
|
||||
font-family: ${fontFamily('mono')};
|
||||
font-size: ${fontSize(3)};
|
||||
font-weight: ${FONT_WEIGHT.medium};
|
||||
letter-spacing: 0.04em;
|
||||
line-height: ${fontSize(4)};
|
||||
padding: ${spacing(1)} ${spacing(2.5)};
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
Reference in New Issue
Block a user