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
27 changes: 23 additions & 4 deletions src/sidepanel/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,33 @@ export function App() {
// Active Space ambient tint — applied to <body> via the --space-tint
// CSS variable so the gradient persists across live-create/edit
// sub-views and outlives the side-panel React tree.
const activeColor =
store && windowId !== undefined
? store.spaces[store.activeSpaceByWindow[windowId] ?? '']?.color
: undefined
//
// We mirror the same activeId-or-spaces[0] fallback the render uses
// (see `active` below). Otherwise a freshly created Space — which
// doesn't call switchTo and so leaves activeSpaceByWindow untouched
// — would render its content but skip the tint until the user
// clicks its pill.
const activeColor = (() => {
if (!store || windowId === undefined) return undefined
const windowSpaces = Object.values(store.spaces)
.filter((s) => s.windowId === windowId)
.sort((a, b) => a.order - b.order)
const activeId = store.activeSpaceByWindow[windowId]
const active = windowSpaces.find((s) => s.id === activeId) ?? windowSpaces[0]
return active?.color
})()
useEffect(() => {
const tint = activeColor ? COLOR_GRADIENT[activeColor] : ''
if (tint) document.body.style.setProperty('--space-tint', tint)
else document.body.style.removeProperty('--space-tint')

// The CSS rule in globalStyles keyed off html[data-space-tint] flips
// the palette to light-mode values when a colored tint is active, so
// dark text reads against the bright pastel body. grey is excluded
// by the selector; the no-tint state removes the attribute entirely.
const root = document.documentElement
if (activeColor) root.setAttribute('data-space-tint', activeColor)
else root.removeAttribute('data-space-tint')
}, [activeColor])

const { openMenu, setOpenMenu } = menu
Expand Down
140 changes: 140 additions & 0 deletions src/sidepanel/SpaceTint.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { useEffect } from 'react'
import type { SpaceColor } from '../shared/types'
import { COLOR_GRADIENT, COLOR_HEX, COLORS } from './theme'

// Mirrors what App.tsx does in the real side panel: writes the active
// Space's gradient into --space-tint on <body> and the color name into
// data-space-tint on <html>. globalStyles.tsx then paints the gradient
// and (for non-grey colors) flips the palette to light-mode values so
// dark text reads against the pastel body.
function applyTint(color: SpaceColor | 'none') {
const root = document.documentElement
if (color === 'none') {
document.body.style.removeProperty('--space-tint')
root.removeAttribute('data-space-tint')
} else {
document.body.style.setProperty('--space-tint', COLOR_GRADIENT[color])
root.setAttribute('data-space-tint', color)
}
}

function Single({ color }: { color: SpaceColor }) {
useEffect(() => {
applyTint(color)
return () => applyTint('none')
}, [color])
return (
<div style={{ padding: 16, minHeight: 'calc(100vh - 24px)' }}>
<h2 style={{ margin: 0, color: 'var(--fg)', fontSize: 14 }}>
Space tint preview
</h2>
<p style={{ marginTop: 8, color: 'var(--muted)', fontSize: 12 }}>
Active color: <code>{color}</code>. <code>--space-tint</code> is
written onto <code>&lt;body&gt;</code>; the Storybook canvas body
renders the same way the side panel body does.
</p>
<div
style={{
marginTop: 16,
display: 'inline-flex',
alignItems: 'center',
gap: 8,
padding: '6px 10px',
borderRadius: 999,
background: 'var(--bg-soft)',
color: 'var(--fg)',
fontSize: 12,
border: '1px solid var(--border)',
}}
>
<span
style={{
display: 'inline-block',
width: 10,
height: 10,
borderRadius: '50%',
background: COLOR_HEX[color],
}}
/>
{color}
</div>
</div>
)
}

const meta: Meta<typeof Single> = {
title: 'Theme/SpaceTint',
component: Single,
// The default `panel` background paints the canvas body white and
// hides the tint — disable so the tint is the only body paint.
parameters: {
layout: 'fullscreen',
backgrounds: { disable: true },
},
argTypes: {
color: { control: { type: 'select' }, options: COLORS },
},
}
export default meta

type Story = StoryObj<typeof Single>

export const Pink: Story = { args: { color: 'pink' } }
export const Blue: Story = { args: { color: 'blue' } }
export const Red: Story = { args: { color: 'red' } }
export const Green: Story = { args: { color: 'green' } }
export const Yellow: Story = { args: { color: 'yellow' } }
export const Cyan: Story = { args: { color: 'cyan' } }
export const Purple: Story = { args: { color: 'purple' } }
export const Orange: Story = { args: { color: 'orange' } }
export const Grey: Story = { args: { color: 'grey' } }

// Side-by-side swatches — useful for comparing relative tint strength
// without flipping stories. Each swatch paints the gradient locally
// (not on body) so all 9 colors are visible at once.
export const AllColors: StoryObj = {
parameters: {
layout: 'fullscreen',
backgrounds: { disable: true },
},
render: () => (
<div
style={{
padding: 12,
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 10,
}}
>
{COLORS.map((c) => (
<div
key={c}
style={{
padding: 14,
minHeight: 120,
borderRadius: 8,
backgroundColor: 'var(--bg)',
backgroundImage: COLOR_GRADIENT[c],
border: '1px solid var(--border)',
color: 'var(--fg)',
fontSize: 12,
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'space-between',
}}
>
<span>{c}</span>
<span
style={{
width: 12,
height: 12,
borderRadius: '50%',
background: COLOR_HEX[c],
}}
/>
</div>
))}
</div>
),
}
24 changes: 24 additions & 0 deletions src/sidepanel/globalStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,30 @@ const styles = css`
}
}

/* When the active Space has a non-grey tint, the body is painted with
an opaque saturated gradient (theme.ts COLOR_GRADIENT). This block
gives surfaces an Arc-style frosted look: white text, translucent
white buttons/pills that let the body color show through, and a
near-black opaque --bg for menus/popovers so they stay readable
when they float over the saturated body. grey (and the no-tint
state) keeps the user's prefers-color-scheme. App.tsx writes
data-space-tint on <html>; the attribute selector outranks the
:root @media block on specificity. */
html[data-space-tint]:not([data-space-tint='grey']) {
--fg: #ffffff;
--muted: rgba(255, 255, 255, 0.78);
--subtle: rgba(255, 255, 255, 0.58);
--border: rgba(255, 255, 255, 0.20);
--bg: #15171c;
--bg-soft: rgba(255, 255, 255, 0.12);
--bg-hover: rgba(255, 255, 255, 0.22);
--bg-active: rgba(255, 255, 255, 0.32);
--accent: #ffffff;
--accent-soft: rgba(255, 255, 255, 0.20);
--danger: #ffd2cc;
--shadow: 0 8px 24px rgba(0, 0, 0, 0.32), 0 1px 3px rgba(0, 0, 0, 0.22);
}

*, *::before, *::after {
box-sizing: border-box;
}
Expand Down
33 changes: 16 additions & 17 deletions src/sidepanel/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,23 @@ export const COLOR_HEX: Record<SpaceColor, string> = {
orange: '#fa7b17',
}

// Arc-style ambient gradient applied to the side panel root for the
// active Space. The translucent stops let the underlying --bg show
// through, so the same gradient stays readable in both light and dark
// modes without re-keying per scheme.
// Arc-style ambient body fill for the active Space. The non-grey
// entries are saturated, opaque gradients (lighter top-left → darker
// bottom-right) so the whole panel reads as that color, the way Arc's
// sidebar does. White text on these passes contrast at all hues.
// grey stays translucent — the palette isn't swapped for it, so it
// rides on the user's prefers-color-scheme rather than forcing a
// colored fill.
export const COLOR_GRADIENT: Record<SpaceColor, string> = {
grey: 'linear-gradient(155deg, rgba(154, 160, 166, 0.32) 0%, rgba(154, 160, 166, 0) 60%)',
blue: 'linear-gradient(155deg, rgba(106, 183, 255, 0.40) 0%, rgba(26, 115, 232, 0.10) 70%)',
red: 'linear-gradient(155deg, rgba(255, 138, 128, 0.40) 0%, rgba(217, 48, 37, 0.10) 70%)',
yellow:
'linear-gradient(155deg, rgba(255, 213, 79, 0.45) 0%, rgba(249, 171, 0, 0.10) 70%)',
green:
'linear-gradient(155deg, rgba(102, 187, 106, 0.40) 0%, rgba(24, 128, 56, 0.10) 70%)',
pink: 'linear-gradient(155deg, rgba(255, 128, 171, 0.40) 0%, rgba(208, 24, 132, 0.10) 70%)',
purple:
'linear-gradient(155deg, rgba(179, 136, 255, 0.40) 0%, rgba(147, 52, 230, 0.10) 70%)',
cyan: 'linear-gradient(155deg, rgba(77, 208, 225, 0.40) 0%, rgba(0, 123, 131, 0.10) 70%)',
orange:
'linear-gradient(155deg, rgba(255, 183, 77, 0.45) 0%, rgba(250, 123, 23, 0.10) 70%)',
grey: 'linear-gradient(155deg, rgba(154, 160, 166, 0.45) 0%, rgba(154, 160, 166, 0.22) 100%)',
blue: 'linear-gradient(155deg, #6886c1 0%, #4f6aa3 100%)',
red: 'linear-gradient(155deg, #c07676 0%, #9d5454 100%)',
yellow: 'linear-gradient(155deg, #c09150 0%, #9b6f28 100%)',
green: 'linear-gradient(155deg, #6ca072 0%, #4d7d54 100%)',
pink: 'linear-gradient(155deg, #c789a8 0%, #a36684 100%)',
purple: 'linear-gradient(155deg, #9078bf 0%, #6f57a0 100%)',
cyan: 'linear-gradient(155deg, #5e8c95 0%, #406f78 100%)',
orange: 'linear-gradient(155deg, #c08562 0%, #9a663a 100%)',
}

export const FONT_SCALE: Record<UIFontSize, number> = {
Expand Down