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
4 changes: 4 additions & 0 deletions dev/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DirectEdit } from '../src/direct-edit'
import { Avatar, Button, Badge } from './components/ui'
import { Button as BaseUIButton } from '@base-ui/react/button'
import { CanvasPlayground } from './canvas-playground'
import { TokenShowcase } from './token-showcase'

const gray = {
50: 'var(--color-gray-50, #f9fafb)',
Expand Down Expand Up @@ -186,6 +187,9 @@ export default function App() {
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>

{/* Design Tokens — uses Tailwind @theme + shadcn token utility classes */}
<TokenShowcase />

{/* Layout & Flex */}
<div style={card}>
<div style={sectionLabel}>Layout & Flex</div>
Expand Down
2 changes: 2 additions & 0 deletions dev/main.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import './theme.css'

// Install React DevTools hook before React initializes
import '../src/preload'

Expand Down
46 changes: 46 additions & 0 deletions dev/theme.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* Dev harness design tokens — Tailwind v4 theme + utilities only (NO preflight,
so the existing inline-styled dev app is not reset). */
@layer theme, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);

/* Direct Tailwind @theme tokens — these emit --color-* onto :root */
@theme {
--color-brand: oklch(0.55 0.2 264);
--color-brand-foreground: oklch(0.98 0 0);
}

/* shadcn-style tokens (canonical modern shadcn v4 shape) */
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--border: oklch(0.269 0 0);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-destructive: var(--destructive);
--color-border: var(--border);
}
28 changes: 28 additions & 0 deletions dev/token-showcase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export function TokenShowcase() {
return (
<section style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', opacity: 0.6 }}>
DESIGN TOKENS (Tailwind + shadcn)
</div>
<div className="bg-background text-foreground border border-border rounded-lg p-6">
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-start' }}>
<span className="bg-brand text-brand-foreground text-sm font-medium rounded-md px-3 py-1.5">
Tailwind @theme: bg-brand
</span>
<button className="bg-primary text-primary-foreground text-sm font-semibold rounded-md px-4 py-2">
shadcn: bg-primary
</button>
<button className="bg-secondary text-secondary-foreground text-sm font-medium rounded-md px-4 py-2">
bg-secondary
</button>
<p className="text-muted-foreground text-lg font-semibold">
text-muted-foreground · text-lg · font-semibold
</p>
<span className="bg-destructive text-white text-xs font-medium rounded px-2 py-1">
bg-destructive
</span>
</div>
</div>
</section>
)
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "made-refine",
"version": "0.3.0",
"version": "0.3.1",
"packageManager": "bun@latest",
"description": "Visual CSS editor for React",
"main": "./dist/index.js",
Expand Down
17 changes: 16 additions & 1 deletion src/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
getSelectionColors,
parseColorValue,
getElementDisplayName,
invalidateColorTokenIndex,
} from './utils'
import { InteractionOverlay } from './panel/interaction-overlay'
import { SelectedCommentComposer } from './panel/selected-comment-composer'
Expand Down Expand Up @@ -272,6 +273,12 @@ export function DirectEditPanelInner({
)
const { scrollRef, activeSection } = useSectionNav(sectionRefs)

// Re-read the host app's design tokens when the selected element changes, so a
// stale module-level color-token index never sticks (e.g. after a theme toggle).
React.useEffect(() => {
invalidateColorTokenIndex()
}, [elementInfo])

return (
<TooltipProvider delayDuration={200}>
<div
Expand Down Expand Up @@ -352,6 +359,7 @@ export function DirectEditPanelInner({
backgroundColor={computedColor.backgroundColor}
onSetCSS={onSetCSS}
pendingStyles={pendingStyles}
classList={elementInfo.classList}
/>
</div>
)}
Expand All @@ -368,6 +376,7 @@ export function DirectEditPanelInner({
onOutlineColorChange={(value) => onUpdateColor('outlineColor', value)}
onSetCSS={onSetCSS}
pendingStyles={pendingStyles}
classList={elementInfo.classList}
/>
</div>

Expand All @@ -382,7 +391,11 @@ export function DirectEditPanelInner({
{elementInfo.isTextElement && computedTypography && (
<div ref={textSectionRef}>
<CollapsibleSection title="Text">
<TypographyInputs typography={computedTypography} onUpdate={onUpdateTypography} />
<TypographyInputs
typography={computedTypography}
onUpdate={onUpdateTypography}
classList={elementInfo.classList}
/>
</CollapsibleSection>
</div>
)}
Expand Down Expand Up @@ -415,6 +428,8 @@ export function DirectEditPanelInner({
hasTextContent={elementInfo.isTextElement}
showBorderColor={computedColor.borderColor.alpha > 0}
showOutlineColor={computedColor.outlineColor.alpha > 0}
classList={elementInfo.classList}
pendingStyles={pendingStyles}
/>
</CollapsibleSection>
</div>
Expand Down
20 changes: 18 additions & 2 deletions src/panel/border-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ColorPickerGroup } from '../ui/color-picker'
import { NumberInput, Tip, CollapsibleSection } from './shared'
import { ColorInput } from './fill-section'
import { Button } from '../ui/button'
import { getColorTokenIndex, resolveColorToken, getTokenAliasChain, tokenFromCssValue } from '../utils/design-tokens'
import {
ChevronDown,
Square,
Expand Down Expand Up @@ -85,9 +86,11 @@ interface BorderInputsProps {
onPositionChange: (position: BorderPosition) => void
outlineStyle?: BorderStyle
outlineWidth?: number
classList?: string[]
pendingStyles?: Record<string, string>
}

export function BorderInputs({ border, borderColor, outlineColor, onChange, onBatchChange, onBorderColorChange, onOutlineColorChange, onSetCSS, borderPosition, borderStyleControlPreference, onPositionChange, outlineStyle, outlineWidth }: BorderInputsProps) {
export function BorderInputs({ border, borderColor, outlineColor, onChange, onBatchChange, onBorderColorChange, onOutlineColorChange, onSetCSS, borderPosition, borderStyleControlPreference, onPositionChange, outlineStyle, outlineWidth, classList, pendingStyles }: BorderInputsProps) {
const [selectedSide, setSelectedSide] = React.useState<BorderSideOption>('All')

const isOutline = borderPosition === 'outline'
Expand Down Expand Up @@ -189,6 +192,14 @@ export function BorderInputs({ border, borderColor, outlineColor, onChange, onBa

const activeColor = isOutline ? outlineColor : borderColor
const activeColorChange = isOutline ? onOutlineColorChange : onBorderColorChange
const activeProperty = isOutline ? 'outline-color' : 'border-color'
const activeToken =
activeColor?.token ??
tokenFromCssValue(pendingStyles?.[activeProperty]) ??
(classList && activeColor
? resolveColorToken(activeProperty, classList, activeColor, getColorTokenIndex())
: null)
const activeAliasChain = activeToken ? getTokenAliasChain(activeToken) : undefined
const currentStyleLabel = BORDER_STYLE_OPTIONS.find((o) => o.value === currentStyle)?.label ?? currentStyle

return (
Expand Down Expand Up @@ -299,6 +310,8 @@ export function BorderInputs({ border, borderColor, outlineColor, onChange, onBa
id={isOutline ? 'outline-color' : 'border-color'}
value={activeColor}
onChange={activeColorChange}
token={activeToken}
aliasChain={activeAliasChain}
/>
</div>
{borderStyleControlPreference === 'icon' && <div className="size-7 shrink-0" />}
Expand All @@ -320,9 +333,10 @@ interface BorderSectionProps {
onOutlineColorChange?: (value: ColorValue) => void
onSetCSS?: (properties: Record<string, string>) => void
pendingStyles?: Record<string, string>
classList?: string[]
}

export function BorderSection({ border, borderColor, outlineColor, borderStyleControlPreference, onChange, onBatchChange, onBorderColorChange, onOutlineColorChange, onSetCSS, pendingStyles }: BorderSectionProps) {
export function BorderSection({ border, borderColor, outlineColor, borderStyleControlPreference, onChange, onBatchChange, onBorderColorChange, onOutlineColorChange, onSetCSS, pendingStyles, classList }: BorderSectionProps) {
// Auto-detect initial position from pending styles
const hasOutlinePending = Boolean(
pendingStyles?.['outline-style'] || pendingStyles?.['outline-width']
Expand Down Expand Up @@ -460,6 +474,8 @@ export function BorderSection({ border, borderColor, outlineColor, borderStyleCo
onPositionChange={handlePositionChange}
outlineStyle={outlineStyleValue}
outlineWidth={outlineWidthValue}
classList={classList}
pendingStyles={pendingStyles}
/>
</ColorPickerGroup>
) : null}
Expand Down
109 changes: 108 additions & 1 deletion src/panel/fill-section.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as React from 'react'
import { act, cleanup, render, fireEvent } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { BackgroundFillSection } from './fill-section'
import { BackgroundFillSection, ColorInput, FillSection } from './fill-section'
import { invalidateColorTokenIndex } from '../utils/design-tokens'
import { formatColorValue } from '../ui/color-utils'
import type { ColorValue } from '../types'

vi.mock('../ui/tooltip', () => ({
Expand Down Expand Up @@ -134,3 +136,108 @@ describe('BackgroundFillSection', () => {
expect(container.querySelectorAll('input[type="text"]').length).toBe(2)
})
})

describe('ColorInput token awareness', () => {
const visibleColor: ColorValue = { hex: 'FF0000', alpha: 100, raw: '#FF0000' }

it('with no token renders the hex input (today behavior)', () => {
const { container } = render(<ColorInput value={visibleColor} onChange={vi.fn()} />)
expect(container.querySelector('input[type="text"]')).not.toBeNull()
})

it('with a token renders the chip label and not the inline hex input', () => {
const { container } = render(
<ColorInput value={visibleColor} onChange={vi.fn()} token="--color-primary" />,
)
expect(container.textContent).toContain('--color-primary')
// The hex/alpha inputs move into the (closed) popover, so they are not in the row.
expect(container.querySelector('input')).toBeNull()
})
})

describe('FillSection token resolution', () => {
let injectedStyle: HTMLStyleElement | null = null

afterEach(() => {
injectedStyle?.remove()
injectedStyle = null
invalidateColorTokenIndex()
})

function injectThemeStyle(css: string) {
const el = document.createElement('style')
el.textContent = css
document.head.appendChild(el)
injectedStyle = el
invalidateColorTokenIndex()
}

it('renders a token chip when the text color is bound to a theme variable', () => {
injectThemeStyle(':root{--color-foreground:#FAFAFA}')
const foreground: ColorValue = { hex: 'FAFAFA', alpha: 100, raw: '#FAFAFA' }
const { container } = render(
<FillSection
textColor={foreground}
onTextChange={vi.fn()}
hasTextContent
classList={['text-foreground']}
/>,
)
expect(container.textContent).toContain('--color-foreground')
})

it('falls back to the plain hex input when no token matches', () => {
const orphan: ColorValue = { hex: '123456', alpha: 100, raw: '#123456' }
const { container } = render(
<FillSection
textColor={orphan}
onTextChange={vi.fn()}
hasTextContent
classList={[]}
/>,
)
expect(container.querySelector('input[type="text"]')).not.toBeNull()
expect(container.textContent).not.toContain('--color-')
})

it('renders the chip for a value whose .token is set (picker binding)', () => {
const bound: ColorValue = {
hex: '3B82F6',
alpha: 100,
raw: 'var(--color-primary)',
token: '--color-primary',
}
const { container } = render(
<FillSection textColor={bound} onTextChange={vi.fn()} hasTextContent classList={[]} />,
)
expect(container.textContent).toContain('--color-primary')
})

it('recovers the chip from a var() in pendingStyles', () => {
const literal: ColorValue = { hex: '3B82F6', alpha: 100, raw: '#3B82F6' }
const { container } = render(
<FillSection
textColor={literal}
onTextChange={vi.fn()}
hasTextContent
classList={[]}
pendingStyles={{ color: 'var(--color-primary)' }}
/>,
)
expect(container.textContent).toContain('--color-primary')
})
})

describe('formatColorValue token binding', () => {
it('returns var(--token) when token is set', () => {
expect(
formatColorValue({ hex: '3B82F6', alpha: 100, raw: '', token: '--color-primary' }),
).toBe('var(--color-primary)')
})

it('wraps a translucent bound token in color-mix', () => {
expect(
formatColorValue({ hex: '3B82F6', alpha: 50, raw: '', token: '--color-primary' }),
).toBe('color-mix(in srgb, var(--color-primary) 50%, transparent)')
})
})
Loading
Loading