Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
193d06b
Add semantic success color for applets
tonyfresher Aug 4, 2026
1ab31ee
Move panel header below content with ring styling
tonyfresher Aug 4, 2026
765f132
Add shared texture utilities for applets and views
tonyfresher Aug 4, 2026
66c4206
Clarify object outlines and separators in design guidance
tonyfresher Aug 4, 2026
a1fdfba
Clarify texture placement rules in workspace design guidance
tonyfresher Aug 4, 2026
dbe3196
Constrain customize panel width
tonyfresher Aug 4, 2026
84dbd5a
Simplify workspace surface and texture guidance
tonyfresher Aug 4, 2026
e5b09bd
Clarify workspace design guidance for surfaces, textures, and stateful.
tonyfresher Aug 4, 2026
3eedaf6
Refine workspace design guidance and contract tests
tonyfresher Aug 4, 2026
031d33d
Add workspace radius themes and update Tailwind
tonyfresher Aug 5, 2026
78aa597
Add Tangerine theme and constrain bottom panel width
tonyfresher Aug 5, 2026
02d4b4e
Use OKLCH colors for theme derivation
tonyfresher Aug 5, 2026
1c9e11e
Ensure applet workflows finish by focusing the final workspace tab
tonyfresher Aug 5, 2026
9adc8b9
Fix archive action spacing in chat selector
tonyfresher Aug 5, 2026
bd19b2f
Simplify texture noise rendering with blended backgrounds
tonyfresher Aug 5, 2026
ad731dd
Simplify font theme customization options
tonyfresher Aug 5, 2026
94ed8a1
Increase texture noise frequency
tonyfresher Aug 5, 2026
b907abe
Prevent text selection in workspace tabs
tonyfresher Aug 5, 2026
f5d886e
Add tooltip and filled stop icon to composer
tonyfresher Aug 5, 2026
0e9ce68
Rename font link IDs from 'mei' to 'moi'
tonyfresher Aug 5, 2026
5b38c6b
Apply widget-local workspace themes
tonyfresher Aug 5, 2026
bee07b6
Fix theme color-mix syntax and update paper color
tonyfresher Aug 5, 2026
8e69b68
Update theme color mixing values
tonyfresher Aug 5, 2026
05fe686
Add accent foreground and border theme colors
tonyfresher Aug 5, 2026
f5a8be7
Use solid background for scratchpad
tonyfresher Aug 5, 2026
eda9111
Rename default font theme to sans
tonyfresher Aug 5, 2026
4a0650e
Update workspace font and radius theme presets
tonyfresher Aug 5, 2026
b489b55
Use primary styling for chat messages
tonyfresher Aug 5, 2026
7cf876e
Read widget theme from the workspace layout context
claude Aug 5, 2026
45bf4fd
Merge branch 'main' into tinkering/applet-success-color-guidance
claude Aug 6, 2026
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
1 change: 1 addition & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Use semantic color tokens by intent:
- `primary` for the single most important action in a local region.
- `muted` for quiet structure or disabled fills and `muted-foreground` for secondary content.
- `accent` for hover, active, selection, and subtle highlights.
- `success` for positive outcomes, healthy or active states, and presence or progress indicators.
- `destructive` for destructive actions, invalid states, and errors.
- `border`, `input`, and `ring` for structure, controls, and focus.

Expand Down
19 changes: 12 additions & 7 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion client/components/shared/BottomPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export function BottomPanel({ className, title, children, onClose, ref }: Bottom
<motion.div
ref={ref}
className={cn(
'absolute inset-x-4 bottom-4 no-scrollbar flex max-h-full flex-col gap-4 overflow-y-auto rounded-2xl bg-card p-(--page-pad) pb-0 text-card-foreground shadow-md',
'absolute inset-x-4 bottom-4 no-scrollbar flex max-h-full flex-col gap-4 overflow-y-auto rounded-2xl bg-card text-card-foreground shadow-md',
'mx-auto max-w-[calc(var(--chat-max-container)+var(--page-pad)*2)] p-(--page-pad) pb-0',
className
)}
variants={{
Expand Down
2 changes: 1 addition & 1 deletion client/components/shared/PanelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ReactNode } from 'react'

export function PanelHeader({ children }: { children?: ReactNode }) {
return (
<header className="@container flex h-11 shrink-0 items-center gap-2.5 border-b border-border/75 pr-3 pl-4">
<header className="@container relative flex h-11 shrink-0 items-center gap-2.5 pr-3 pl-4 ring-1 ring-border">
{children}
</header>
)
Expand Down
22 changes: 18 additions & 4 deletions client/features/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { type RefObject, useRef, useState } from 'react'

import { IconFile, IconLoader2, IconPaperclip, IconPlayerStop, IconX } from '@tabler/icons-react'
import {
IconFile,
IconLoader2,
IconPaperclip,
IconPlayerStopFilled,
IconX
} from '@tabler/icons-react'

import {
canSubmitComposerAction,
Expand All @@ -10,6 +16,7 @@ import {
ComposerTextarea
} from '@/client/components/shared/Composer'
import { Button } from '@/client/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/client/components/ui/tooltip'
import { cn } from '@/client/lib/cn'
import { useWorkspaceId } from '@/client/features/workspace/WorkspaceContext'
import { uploadFiles } from '@/client/features/chat/uploads'
Expand Down Expand Up @@ -176,9 +183,16 @@ export function ChatComposer({
</Button>
<ModelPicker />
{processing ? (
<Button type="button" size="icon" onClick={onStop} aria-label="Stop agent">
<IconPlayerStop stroke={1.5} />
</Button>
<Tooltip>
<TooltipTrigger
render={
<Button type="button" size="icon" onClick={onStop} aria-label="Stop agent">
<IconPlayerStopFilled />
</Button>
}
/>
<TooltipContent>Stop answering</TooltipContent>
</Tooltip>
) : (
<ComposerSubmitButton
label="Send message"
Expand Down
2 changes: 1 addition & 1 deletion client/features/chat/ChatSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function ChatSessionItem({
'min-w-0 truncate',
(running || pending) && '-mr-4',
confirmingArchive
? '-4'
? '-mr-14'
: canArchive &&
'group-focus-within/chat:-mr-4 group-hover/chat:-mr-4 [@media(hover:none)]:-mr-4'
)}
Expand Down
2 changes: 1 addition & 1 deletion client/features/chat/TurnView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const TurnView = memo(function TurnView({ turn, processing = false }: Tur
<FilePart key={i} mediaType={p.mediaType} url={p.url} filename={p.filename} />
))}
{text && (
<p className="max-w-full min-w-0 rounded-lg bg-accent px-3 py-2 text-sm leading-normal wrap-anywhere whitespace-pre-wrap text-accent-foreground">
<p className="max-w-full min-w-0 rounded-lg bg-primary px-3 py-2 text-sm leading-normal wrap-anywhere whitespace-pre-wrap text-primary-foreground">
{text}
</p>
)}
Expand Down
4 changes: 1 addition & 3 deletions client/features/scratchpad/Scratchpad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ function makeAssetStore(workspaceId: string): TLAssetStore {
// from the panel, leaving just the zoom menu. Kept on defaults: NavigationPanel
// (zoom), ContextMenu, KeyboardShortcutsDialog.
function ScratchBackground() {
return (
<div className="absolute inset-0 bg-[color-mix(in_oklch,var(--muted),var(--background)_50%)]" />
)
return <div className="absolute inset-0 bg-background" />
}

function UniformGrid({ x, y, z, size }: TLGridProps) {
Expand Down
23 changes: 23 additions & 0 deletions client/features/widgets/WidgetFrame.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'

import { describe, expect, test } from 'bun:test'

import { DEFAULT_PRIMARY_COLOR } from '@/lib/themes'

import { WidgetFrame } from './WidgetFrame'

describe('WidgetFrame', () => {
test('applies the swapped widget theme without forcing dark mode', () => {
const html = renderToStaticMarkup(createElement(WidgetFrame))
const chromeClasses = html.match(/data-widget-chrome="true"[^>]*class="([^"]+)"/)?.[1]

expect(chromeClasses).toBeDefined()
expect(chromeClasses?.split(' ')).toContain('text-foreground')
expect(chromeClasses?.split(' ')).not.toContain('dark')
expect(html).toContain(`--background:${DEFAULT_PRIMARY_COLOR}`)
expect(html).toContain('--primary:color-mix(var(--background) 3%, oklch(1 0 0) 97%)')
expect(html).toContain('--accent-foreground:var(--foreground)')
expect(html).toContain('--border:color-mix(var(--foreground) 15%, transparent)')
})
})
8 changes: 6 additions & 2 deletions client/features/widgets/WidgetFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { IconMinus, IconPlus } from '@tabler/icons-react'
import { cn } from '@/client/lib/cn'

import { Button } from '@/client/components/ui/button'
import { useWorkspaceThemeSetting } from '@/client/features/workspace/WorkspaceLayoutContext'
import { getWorkspaceThemeStyle } from '@/client/runtime/workspace-theme'

type WidgetFrameProps = {
editing?: boolean
Expand All @@ -16,6 +18,8 @@ type WidgetFrameProps = {
}

export function WidgetFrame({ editing, hidden, onRemove, children }: WidgetFrameProps) {
const theme = useWorkspaceThemeSetting()

return (
<motion.div
variants={{
Expand All @@ -35,8 +39,9 @@ export function WidgetFrame({ editing, hidden, onRemove, children }: WidgetFrame
// Stable hook for widget snapshots: the capture clone overrides this
// element's chrome (radius/shadow/stroke) so thumbnails come out square.
data-widget-chrome
style={getWorkspaceThemeStyle(theme, 'widget')}
className={cn(
'dark absolute inset-0 overflow-hidden rounded-2xl [corner-shape:superellipse(1.2)]',
'absolute inset-0 overflow-hidden rounded-2xl text-foreground [corner-shape:superellipse(1.2)]',
// Outer drop shadow on the wrapper itself.
'shadow-[0_1px_2px_-1px_rgba(0,0,0,0.08),0_2px_4px_0_rgba(0,0,0,0.03)]',
// 1px inset stroke painted on a pseudo so it lands ON TOP of the
Expand All @@ -45,7 +50,6 @@ export function WidgetFrame({ editing, hidden, onRemove, children }: WidgetFrame
'after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit]',
"after:content-[''] after:[corner-shape:inherit]",
'after:shadow-[inset_0_0_0_1px_rgba(0,0,0,0.05)]',
'text-foreground',
editing && 'pointer-events-none'
)}
>
Expand Down
64 changes: 40 additions & 24 deletions client/features/workspace/CustomizePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ import { useWorkspaceLayoutCtx } from '@/client/features/workspace/WorkspaceLayo
import { BottomPanel } from '@/client/components/shared/BottomPanel'
import { cn } from '@/client/lib/cn'
import { getWorkspaceThemeStyle, usePreloadWorkspaceFonts } from '@/client/runtime/workspace-theme'
import { COLOR_THEMES, type ColorThemeConfig, FONT_THEMES } from '@/lib/themes'
import type { ColorTheme, FontTheme } from '@/lib/types'
import {
COLOR_THEMES,
type ColorThemeConfig,
FONT_THEMES,
RADIUS_THEMES,
type RadiusThemeConfig,
resolveWorkspaceTheme
} from '@/lib/themes'
import type { ColorTheme, FontTheme, RadiusTheme, WorkspaceTheme } from '@/lib/types'

const FONT_OPTIONS = Object.entries(FONT_THEMES) as [FontTheme, (typeof FONT_THEMES)[FontTheme]][]

const COLOR_OPTIONS = Object.entries(COLOR_THEMES) as [ColorTheme, ColorThemeConfig][]

function presetMatches(preset: ColorThemeConfig, primary?: string): boolean {
return (preset.primary ?? undefined) === primary
}
const RADIUS_OPTIONS = Object.entries(RADIUS_THEMES) as [RadiusTheme, RadiusThemeConfig][]

type CustomizeOptionGroupProps = {
children: ReactNode
Expand All @@ -26,7 +31,7 @@ function CustomizeOptionGroup({ children, label }: CustomizeOptionGroupProps) {
return (
<div className="flex flex-col gap-2">
<p className="text-xs font-medium">{label}</p>
<div role="group" aria-label={label} className="grid grid-cols-3 gap-2">
<div role="group" aria-label={label} className="grid grid-cols-4 gap-2">
{children}
</div>
</div>
Expand Down Expand Up @@ -55,7 +60,7 @@ function CustomizeOption({ active, children, className, onSelect, style }: Custo
)}
style={style}
>
<div className="flex-1">{children}</div>
<div className="flex flex-1 flex-col justify-center">{children}</div>
{active && <IconCircleCheckFilled size={20} stroke={1.5} aria-hidden="true" />}
</button>
)
Expand All @@ -69,11 +74,10 @@ type CustomizePanelProps = {
export function CustomizePanel({ onClose, ref }: CustomizePanelProps) {
usePreloadWorkspaceFonts()
const { layout, setLayout } = useWorkspaceLayoutCtx()
const currentFont = layout.theme?.font ?? 'default'
const currentPrimary = layout.theme?.primary
const theme = resolveWorkspaceTheme(layout.theme)

function setTheme(font: FontTheme, primary: string | undefined) {
setLayout({ theme: { font, ...(primary ? { primary } : {}) } })
function setTheme(update: Partial<WorkspaceTheme>) {
setLayout({ theme: { ...theme, ...update } })
}

return (
Expand All @@ -83,14 +87,11 @@ export function CustomizePanel({ onClose, ref }: CustomizePanelProps) {
{FONT_OPTIONS.map(([key, config]) => (
<CustomizeOption
key={key}
active={key === currentFont}
onSelect={() => setTheme(key, currentPrimary)}
active={key === theme.font}
onSelect={() => setTheme({ font: key })}
style={{ fontFamily: config.sans }}
>
<div className="flex flex-col">
<span className="text-sm font-medium">{config.label}</span>
<span className="text-xs text-muted-foreground">{config.feel}</span>
</div>
<span className="text-sm font-medium">{config.label}</span>
</CustomizeOption>
))}
</CustomizeOptionGroup>
Expand All @@ -99,14 +100,10 @@ export function CustomizePanel({ onClose, ref }: CustomizePanelProps) {
{COLOR_OPTIONS.map(([key, preset]) => (
<CustomizeOption
key={key}
active={presetMatches(preset, currentPrimary)}
active={key === theme.color}
className={preset.primary && 'bg-background text-foreground'}
onSelect={() => setTheme(currentFont, preset.primary)}
style={
preset.primary
? getWorkspaceThemeStyle({ font: currentFont, primary: preset.primary })
: undefined
}
onSelect={() => setTheme({ color: key })}
style={getWorkspaceThemeStyle({ ...theme, color: key })}
>
<div className="flex gap-2">
<span
Expand All @@ -124,6 +121,25 @@ export function CustomizePanel({ onClose, ref }: CustomizePanelProps) {
</CustomizeOption>
))}
</CustomizeOptionGroup>

<CustomizeOptionGroup label="Radius">
{RADIUS_OPTIONS.map(([key, preset]) => (
<CustomizeOption
key={key}
active={key === theme.radius}
onSelect={() => setTheme({ radius: key })}
style={getWorkspaceThemeStyle({ ...theme, radius: key })}
>
<div className="flex items-center gap-2">
<span
className="size-5 shrink-0 rounded-sm bg-card texture-checker-[10px] shadow-xs"
aria-hidden="true"
/>
<span className="text-sm font-medium">{preset.label}</span>
</div>
</CustomizeOption>
))}
</CustomizeOptionGroup>
</div>
</BottomPanel>
)
Expand Down
8 changes: 8 additions & 0 deletions client/features/workspace/WorkspaceLayoutContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export function useWorkspaceLayoutCtx(): WorkspaceLayoutContextValue {
return ctx
}

// The theme alone, for chrome that only needs to paint itself (widget frames,
// previews). Unlike `useWorkspaceLayoutCtx` this tolerates being read outside a
// provider and returns `undefined` there, which callers already treat as "use
// the default theme" — so the same component still renders in isolation.
export function useWorkspaceThemeSetting(): WorkspaceLayout['theme'] {
return useContext(WorkspaceLayoutContext)?.layout.theme
}

// Strip the server-only metadata so what we PUT back (and expose as `layout`)
// is just the persisted `WorkspaceLayout`.
function stripMeta(data: WorkspaceLayoutResponse): WorkspaceLayout {
Expand Down
Loading