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 = {