diff --git a/src/sidepanel/App.tsx b/src/sidepanel/App.tsx index bac2b0c..ce26bca 100644 --- a/src/sidepanel/App.tsx +++ b/src/sidepanel/App.tsx @@ -91,14 +91,33 @@ export function App() { // Active Space ambient tint — applied to 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 diff --git a/src/sidepanel/SpaceTint.stories.tsx b/src/sidepanel/SpaceTint.stories.tsx new file mode 100644 index 0000000..624b475 --- /dev/null +++ b/src/sidepanel/SpaceTint.stories.tsx @@ -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 and the color name into +// data-space-tint on . 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 ( +
+

+ Space tint preview +

+

+ Active color: {color}. --space-tint is + written onto <body>; the Storybook canvas body + renders the same way the side panel body does. +

+
+ + {color} +
+
+ ) +} + +const meta: Meta = { + 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 + +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: () => ( +
+ {COLORS.map((c) => ( +
+ {c} + +
+ ))} +
+ ), +} diff --git a/src/sidepanel/globalStyles.tsx b/src/sidepanel/globalStyles.tsx index 902fe69..ac88743 100644 --- a/src/sidepanel/globalStyles.tsx +++ b/src/sidepanel/globalStyles.tsx @@ -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 ; 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; } diff --git a/src/sidepanel/theme.ts b/src/sidepanel/theme.ts index 349cd8a..b6b4d98 100644 --- a/src/sidepanel/theme.ts +++ b/src/sidepanel/theme.ts @@ -53,24 +53,23 @@ export const COLOR_HEX: Record = { 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 = { - 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 = {