Skip to content

Commit 3c25fea

Browse files
authored
Added Shade semantic tokens (#27109)
ref. https://linear.app/ghost/issue/DES-1329/semantic-tokens - Made semantic tokens the only visual contract for shared UI in Shade, so components are more consistent, more themeable, and easier for AI agents to use correctly.
1 parent a43d080 commit 3c25fea

40 files changed

+371
-124
lines changed

apps/shade/src/components/features/color-picker/color-picker.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export type ColorPickerProps = Omit<HTMLAttributes<HTMLDivElement>, 'onChange'>
6262

6363
export const ColorPickerRoot = ({
6464
value,
65-
defaultValue = '#000000',
65+
defaultValue = 'rgb(0 0 0)',
6666
onChange,
6767
className,
6868
...props
@@ -245,10 +245,10 @@ export const ColorPickerHue = ({
245245
onValueChange={([value]) => setHue(value)}
246246
{...props}
247247
>
248-
<Slider.Track className="relative my-0.5 h-3 w-full grow rounded-full bg-[linear-gradient(90deg,#FF0000,#FFFF00,#00FF00,#00FFFF,#0000FF,#FF00FF,#FF0000)]">
248+
<Slider.Track className="relative my-0.5 h-3 w-full grow rounded-full bg-[linear-gradient(90deg,red,yellow,lime,aqua,blue,magenta,red)]">
249249
<Slider.Range className="absolute h-full" />
250250
</Slider.Track>
251-
<Slider.Thumb className="block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" />
251+
<Slider.Thumb className="block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:ring-1 focus-visible:ring-focus-ring focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" />
252252
</Slider.Root>
253253
);
254254
};
@@ -274,7 +274,7 @@ export const ColorPickerAlpha = ({
274274
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-transparent to-black/50 dark:to-white/50" />
275275
<Slider.Range className="absolute h-full rounded-full bg-transparent" />
276276
</Slider.Track>
277-
<Slider.Thumb className="block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" />
277+
<Slider.Thumb className="block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:ring-1 focus-visible:ring-focus-ring focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" />
278278
</Slider.Root>
279279
);
280280
};

apps/shade/src/components/features/post-share-modal/post-share-modal.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const PostShareModal: React.FC<PostShareModalProps> = (
6464
</div>
6565
<DialogHeader className='relative -mt-5'>
6666
<DialogTitle className='text-3xl leading-[1.15em] font-bold'>
67-
<span className='text-green-500'>{primaryTitle}</span><br />
67+
<span className='text-state-success'>{primaryTitle}</span><br />
6868
<span>{secondaryTitle}</span>
6969
</DialogTitle>
7070
{description &&
@@ -104,16 +104,16 @@ const PostShareModal: React.FC<PostShareModalProps> = (
104104
:
105105
<>
106106
<div className='flex items-center gap-2'>
107-
<a aria-label='Share on X' className='flex h-[34px] w-14 items-center justify-center rounded-xs bg-muted px-3 hover:bg-muted-foreground/20 [&_svg]:h-4' href={`https://twitter.com/intent/tweet?text=${encodedPostTitle}%0A${encodedPostURL}`} rel="noopener noreferrer" target='_blank'>
107+
<a aria-label='Share on X' className='flex h-(--control-height) w-14 items-center justify-center rounded-xs bg-muted px-3 hover:bg-muted-foreground/20 [&_svg]:h-4' href={`https://twitter.com/intent/tweet?text=${encodedPostTitle}%0A${encodedPostURL}`} rel="noopener noreferrer" target='_blank'>
108108
<svg aria-hidden="true" viewBox="0 0 24 24"><path className="social-x_svg__x" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></svg>
109109
</a>
110-
<a aria-label='Share on Threads' className='flex h-[34px] w-14 items-center justify-center rounded-xs bg-muted px-3 hover:bg-muted-foreground/20 [&_svg]:h-4' href={`https://threads.net/intent/post?text=${encodedPostURLTitle}`} rel="noopener noreferrer" target='_blank'>
110+
<a aria-label='Share on Threads' className='flex h-(--control-height) w-14 items-center justify-center rounded-xs bg-muted px-3 hover:bg-muted-foreground/20 [&_svg]:h-4' href={`https://threads.net/intent/post?text=${encodedPostURLTitle}`} rel="noopener noreferrer" target='_blank'>
111111
<svg fill="none" viewBox="0 0 18 18"><g clipPath="url(#social-threads_svg__clip0_351_18008)"><path d="M13.033 8.38a5.924 5.924 0 00-.223-.102c-.13-2.418-1.452-3.802-3.67-3.816h-.03c-1.327 0-2.43.566-3.11 1.597l1.22.837c.507-.77 1.304-.934 1.89-.934h.02c.73.004 1.282.217 1.639.63.26.302.433.72.519 1.245a9.334 9.334 0 00-2.097-.101c-2.109.121-3.465 1.351-3.374 3.06.047.868.478 1.614 1.216 2.1.624.413 1.428.614 2.263.568 1.103-.06 1.969-.48 2.572-1.25.459-.585.749-1.342.877-2.296.526.317.915.735 1.13 1.236.366.854.387 2.255-.756 3.398-1.003 1.002-2.207 1.435-4.028 1.448-2.02-.015-3.547-.663-4.54-1.925-.93-1.182-1.41-2.89-1.428-5.075.018-2.185.498-3.893 1.428-5.075.993-1.262 2.52-1.91 4.54-1.925 2.034.015 3.588.666 4.62 1.934.505.622.886 1.405 1.137 2.317l1.43-.382c-.305-1.122-.784-2.09-1.436-2.892C13.52 1.35 11.587.517 9.096.5h-.01C6.6.517 4.689 1.354 3.404 2.986 2.262 4.44 1.672 6.46 1.652 8.994v.012c.02 2.534.61 4.555 1.752 6.008C4.69 16.646 6.6 17.483 9.086 17.5h.01c2.21-.015 3.768-.594 5.051-1.876 1.68-1.678 1.629-3.78 1.075-5.07-.397-.927-1.154-1.678-2.189-2.175zm-3.816 3.587c-.924.052-1.884-.363-1.932-1.252-.035-.659.47-1.394 1.99-1.482a8.9 8.9 0 01.512-.014c.552 0 1.068.053 1.538.156-.175 2.187-1.203 2.542-2.108 2.592z" fill="#000"></path></g><defs><clipPath id="social-threads_svg__clip0_351_18008"><path d="M0 0h17v17H0z" fill="#fff" transform="translate(.5 .5)"></path></clipPath></defs></svg>
112112
</a>
113-
<a aria-label='Share on Facebook' className='flex h-[34px] w-14 items-center justify-center rounded-xs bg-muted px-3 hover:bg-muted-foreground/20 [&_svg]:h-4' href={`https://www.facebook.com/sharer/sharer.php?u=${encodedPostURL}`} rel="noopener noreferrer" target='_blank'>
113+
<a aria-label='Share on Facebook' className='flex h-(--control-height) w-14 items-center justify-center rounded-xs bg-muted px-3 hover:bg-muted-foreground/20 [&_svg]:h-4' href={`https://www.facebook.com/sharer/sharer.php?u=${encodedPostURL}`} rel="noopener noreferrer" target='_blank'>
114114
<svg fill="none" viewBox="0 0 40 40"><title>social-facebook</title><path className="social-facebook_svg__fb" d="M20 40.004c11.046 0 20-8.955 20-20 0-11.046-8.954-20-20-20s-20 8.954-20 20c0 11.045 8.954 20 20 20z" fill="#1977f3"></path><path d="M27.785 25.785l.886-5.782h-5.546V16.25c0-1.58.773-3.125 3.26-3.125h2.522V8.204s-2.29-.39-4.477-.39c-4.568 0-7.555 2.767-7.555 7.781v4.408h-5.08v5.782h5.08v13.976a20.08 20.08 0 003.125.242c1.063 0 2.107-.085 3.125-.242V25.785h4.66z" fill="#fff"></path></svg>
115115
</a>
116-
<a aria-label='Share on LinkedIn' className='flex h-[34px] w-14 items-center justify-center rounded-xs bg-muted px-3 hover:bg-muted-foreground/20 [&_svg]:h-4' href={`https://www.linkedin.com/shareArticle?mini=true&title=${encodedPostTitle}&url=${encodedPostURL}`} rel="noopener noreferrer" target='_blank'>
116+
<a aria-label='Share on LinkedIn' className='flex h-(--control-height) w-14 items-center justify-center rounded-xs bg-muted px-3 hover:bg-muted-foreground/20 [&_svg]:h-4' href={`https://www.linkedin.com/shareArticle?mini=true&title=${encodedPostTitle}&url=${encodedPostURL}`} rel="noopener noreferrer" target='_blank'>
117117
<svg fill="none" viewBox="0 0 16 16"><g clipPath="url(#social-linkedin_svg__clip0_537_833)"><path className="social-linkedin_svg__linkedin" clipRule="evenodd" d="M1.778 16h12.444c.982 0 1.778-.796 1.778-1.778V1.778C16 .796 15.204 0 14.222 0H1.778C.796 0 0 .796 0 1.778v12.444C0 15.204.796 16 1.778 16z" fill="#007ebb" fillRule="evenodd"></path><path clipRule="evenodd" d="M13.778 13.778h-2.374V9.734c0-1.109-.421-1.729-1.299-1.729-.955 0-1.453.645-1.453 1.729v4.044H6.363V6.074h2.289v1.038s.688-1.273 2.322-1.273c1.634 0 2.804.997 2.804 3.061v4.878zM3.634 5.065c-.78 0-1.411-.636-1.411-1.421s.631-1.422 1.41-1.422c.78 0 1.411.637 1.411 1.422 0 .785-.631 1.421-1.41 1.421zm-1.182 8.713h2.386V6.074H2.452v7.704z" fill="#fff" fillRule="evenodd"></path></g><defs><clipPath id="social-linkedin_svg__clip0_537_833"><path d="M0 0h16v16H0z" fill="#fff"></path></clipPath></defs></svg>
118118
</a>
119119
</div>

apps/shade/src/components/layout/list-header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type ListHeaderProps = PropsWithChildrenAndClassName & {
1616
function ListHeaderLeft({className, children}: PropsWithChildrenAndClassName) {
1717
return (
1818
<div
19-
className={cn('flex min-w-0 flex-col gap-1 h-full min-h-[34px] justify-center', className)}
19+
className={cn('flex min-w-0 flex-col gap-1 h-full min-h-(--control-height) justify-center', className)}
2020
data-list-header='list-header-left'
2121
>
2222
{children}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type {Meta, StoryObj} from '@storybook/react-vite';
2+
import {Button} from '@/components/ui/button';
3+
import {H1} from './heading';
4+
import {ViewHeader, ViewHeaderActions} from './view-header';
5+
6+
const meta = {
7+
title: 'Layout / View Header',
8+
component: ViewHeader,
9+
tags: ['autodocs'],
10+
parameters: {
11+
layout: 'fullscreen'
12+
}
13+
} satisfies Meta<typeof ViewHeader>;
14+
15+
export default meta;
16+
type Story = StoryObj<typeof meta>;
17+
18+
export const Default: Story = {
19+
render: () => (
20+
<ViewHeader>
21+
<div>
22+
<H1>Members</H1>
23+
<p className='mt-2 text-sm text-text-secondary'>Manage your audience and member settings.</p>
24+
</div>
25+
<ViewHeaderActions>
26+
<Button variant="outline">Export</Button>
27+
<Button>Add member</Button>
28+
</ViewHeaderActions>
29+
</ViewHeader>
30+
)
31+
};
32+
33+
export const TitleOnly: Story = {
34+
render: () => (
35+
<ViewHeader>
36+
<H1>Newsletter</H1>
37+
<div />
38+
</ViewHeader>
39+
)
40+
};
41+
42+
export const StickyPreview: Story = {
43+
render: () => (
44+
<div className="h-[520px] overflow-y-auto border border-border-default bg-surface-page px-8">
45+
<ViewHeader className="bg-surface-page/90">
46+
<div>
47+
<H1>Analytics</H1>
48+
<p className='mt-2 text-sm text-text-secondary'>Scroll this panel to verify sticky behavior.</p>
49+
</div>
50+
<ViewHeaderActions>
51+
<Button variant="outline">Filter</Button>
52+
<Button variant="secondary">Compare</Button>
53+
<Button>Export report</Button>
54+
</ViewHeaderActions>
55+
</ViewHeader>
56+
<div className="space-y-3 py-8">
57+
{Array.from({length: 18}, (_, index) => (
58+
<div key={index} className="rounded-md border border-border-default bg-surface-panel p-4 text-sm text-text-secondary">
59+
Placeholder content row {index + 1}
60+
</div>
61+
))}
62+
</div>
63+
</div>
64+
)
65+
};

apps/shade/src/components/layout/view-header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ const ViewHeader:React.FC<ViewHeaderProps> = ({className, children}) => {
2020
const [headerComponent, actionsComponent] = React.Children.toArray(children);
2121

2222
return (
23-
<header className='sticky top-0 z-50 -mx-8 bg-white/70 backdrop-blur-md dark:bg-black'>
24-
<div className={cn('relative flex min-h-[102px] items-center justify-between gap-5 p-8 before:absolute before:inset-x-8 before:bottom-0 before:block before:border-b before:border-gray-200 before:content-[""] before:dark:border-gray-950', className)}>
23+
<header className='sticky top-0 z-50 -mx-8 bg-surface-page/70 backdrop-blur-md'>
24+
<div className={cn('relative flex min-h-[102px] items-center justify-between gap-5 p-8 before:absolute before:inset-x-8 before:bottom-0 before:block before:border-b before:border-border-default before:content-[""]', className)}>
2525
{headerComponent}
2626
{actionsComponent}
2727
</div>

apps/shade/src/components/ui/badge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {cva, type VariantProps} from 'class-variance-authority';
44
import {cn} from '@/lib/utils';
55

66
const badgeVariants = cva(
7-
'inline-flex items-center rounded-xs border px-1.5 text-xs font-semibold transition-colors focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden',
7+
'inline-flex items-center rounded-xs border px-1.5 text-xs font-semibold transition-colors focus:ring-2 focus:ring-focus-ring focus:ring-offset-2 focus:outline-hidden',
88
{
99
variants: {
1010
variant: {

apps/shade/src/components/ui/banner.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ const bannerVariants = cva(
99
{
1010
variants: {
1111
variant: {
12-
default: 'border border-border bg-background shadow-sm hover:shadow-md dark:border-gray-900 dark:bg-gray-925',
12+
default: 'border border-border-default bg-surface-panel shadow-sm hover:shadow-md',
1313
gradient: [
14-
'cursor-pointer border border-gray-100 bg-white dark:border-gray-950 dark:bg-black',
14+
'cursor-pointer border border-border-subtle bg-surface-elevated',
1515
'shadow-[-7px_-6px_42px_8px_rgb(75_225_226_/_28%),7px_6px_42px_8px_rgb(202_103_255_/_32%)]',
1616
'dark:shadow-[-7px_-6px_42px_8px_rgb(75_225_226_/_36%),7px_6px_42px_8px_rgb(202_103_255_/_38%)]',
1717
'hover:shadow-[-7px_-4px_42px_10px_rgb(75_225_226_/_38%),7px_8px_42px_10px_rgb(202_103_255_/_42%)]',
1818
'dark:hover:shadow-[-7px_-4px_42px_10px_rgb(75_225_226_/_50%),7px_8px_42px_10px_rgb(202_103_255_/_52%)]',
1919
'hover:translate-y-[-2px] hover:scale-[1.01]'
2020
],
21-
info: 'border border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/30',
22-
success: 'border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/30',
23-
warning: 'border border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950/30',
24-
destructive: 'bg-white shadow-sm dark:bg-gray-950'
21+
info: 'border border-state-info/40 bg-state-info/10',
22+
success: 'border border-state-success/40 bg-state-success/10',
23+
warning: 'border border-state-warning/40 bg-state-warning/10',
24+
destructive: 'bg-surface-panel shadow-sm'
2525
},
2626
size: {
2727
sm: 'p-2 text-sm',
@@ -87,7 +87,7 @@ const Banner = React.forwardRef<HTMLDivElement, BannerProps>(
8787
{dismissible && (
8888
<Button
8989
aria-label="Dismiss notification"
90-
className="absolute top-1 right-1 size-8 text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
90+
className="absolute top-1 right-1 size-8 text-text-secondary hover:text-text-primary"
9191
size="icon"
9292
variant="ghost"
9393
onClick={handleDismiss}

apps/shade/src/components/ui/breadcrumb.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const BreadcrumbList = React.forwardRef<
1919
<ol
2020
ref={ref}
2121
className={cn(
22-
'flex flex-wrap items-center gap-1.5 break-words text-sm font-medium text-muted-foreground sm:gap-2.5 h-[34px]',
22+
'flex flex-wrap items-center gap-1.5 break-words text-sm font-medium text-muted-foreground sm:gap-2.5 h-(--control-height)',
2323
className
2424
)}
2525
{...props}

apps/shade/src/components/ui/button.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ import {ChevronDown} from 'lucide-react';
66
import {cn} from '@/lib/utils';
77

88
const buttonVariants = cva(
9-
'inline-flex items-center justify-center gap-2 rounded-md text-sm whitespace-nowrap transition-colors focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.5px]',
9+
'inline-flex items-center justify-center gap-2 rounded-md text-sm whitespace-nowrap transition-colors focus-visible:ring-1 focus-visible:ring-focus-ring focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.5px]',
1010
{
1111
variants: {
1212
variant: {
1313
default: 'bg-primary font-medium text-primary-foreground hover:bg-primary/90',
1414
destructive: 'bg-destructive font-medium text-destructive-foreground hover:bg-destructive/90',
1515
outline: 'border border-input bg-background font-medium hover:bg-accent hover:text-accent-foreground',
16-
secondary: 'bg-secondary font-medium text-secondary-foreground hover:bg-secondary/80 dark:bg-gray-925/70 dark:hover:bg-gray-900',
16+
secondary: 'bg-secondary font-medium text-secondary-foreground hover:bg-secondary/80',
1717
ghost: 'font-medium hover:bg-accent hover:text-accent-foreground',
1818
link: 'font-medium text-primary underline-offset-4 hover:underline',
1919
dropdown: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground'
2020
},
2121
size: {
22-
default: 'h-[34px] px-3 py-2',
22+
default: 'h-(--control-height) px-3 py-2',
2323
sm: 'h-7 rounded-md px-3 text-xs [&_svg]:size-3',
2424
lg: 'h-11 rounded-md px-8 text-md font-semibold',
2525
icon: 'size-9'

apps/shade/src/components/ui/card.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,9 @@ interface KpiCardValueProps {
185185
const KpiCardHeaderValue: React.FC<KpiCardValueProps> = ({value, diffDirection, diffValue, diffTooltip}) => {
186186
const diffContainerClassName = cn(
187187
'flex items-center gap-1 text-xs h-[22px] px-1.5 rounded-xs group/diff cursor-default',
188-
diffDirection === 'up' && `text-green-600 bg-green/10 ${diffTooltip && 'hover:bg-green/20'}`,
189-
diffDirection === 'down' && `text-red-600 bg-red/10 ${diffTooltip && 'hover:bg-red/20'}`,
190-
diffDirection === 'same' && 'text-gray-700 bg-muted'
188+
diffDirection === 'up' && `text-state-success bg-state-success/10 ${diffTooltip && 'hover:bg-state-success/20'}`,
189+
diffDirection === 'down' && `text-state-danger bg-state-danger/10 ${diffTooltip && 'hover:bg-state-danger/20'}`,
190+
diffDirection === 'same' && 'text-text-secondary bg-muted'
191191
);
192192
return (
193193
<div className='relative flex flex-col items-start gap-2 lg:flex-row lg:gap-3'>

0 commit comments

Comments
 (0)