chore: Tailwind CSS v3 → v4 migration (#99)

* chore: run Tailwind v4 upgrade tool (Phase 1)

- Upgraded tailwindcss v3 → v4.2.1, postcss plugin to @tailwindcss/postcss
- Deleted tailwind.config.js, migrated theme to CSS @theme block in index.css
- Replaced @tailwind directives with @import 'tailwindcss'
- Added @custom-variant dark, @utility blocks for custom utilities
- Updated class names across 128 files (shadow-sm → shadow-xs, etc.)
- Removed autoprefixer (built into v4)
- Added migration plan doc

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: switch from @tailwindcss/postcss to @tailwindcss/vite (Phase 2)

- Replaced @tailwindcss/postcss with @tailwindcss/vite plugin
- Deleted postcss.config.js (no longer needed)
- Tailwind now runs as a native Vite plugin for faster HMR

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: convert to OKLCH colors, move keyframes into @theme (Phase 3-4)

- Replaced all HSL color indirection with direct OKLCH values in @theme
- Moved all keyframes inside @theme block (v4 pattern)
- Eliminated hsl(var(--x)) double-indirection across 17 component files
- Replaced hsl() inline styles with var(--color-*) theme references
- Cleaned up redundant rdp-* utility blocks
- Fixed @custom-variant dark syntax to use :where()
- Added sidebar/glass/shadow vars as OKLCH in :root

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #99.
This commit is contained in:
chihlasm
2026-03-07 22:10:44 -05:00
committed by GitHub
parent 732ccba966
commit d365c38b61
137 changed files with 1922 additions and 1709 deletions

View File

@@ -39,7 +39,7 @@ export function AIFixReviewModal({ fixes, onApply, onApplyAll, onClose }: AIFixR
const allHandled = pendingFixes.length === 0
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-xs p-4">
<div className="relative flex h-[80vh] w-full max-w-2xl flex-col bg-card border border-border rounded-2xl shadow-lg">
{/* Header */}
<div className="flex items-center justify-between border-b border-border px-6 py-4">
@@ -127,7 +127,7 @@ export function AIFixReviewModal({ fixes, onApply, onApplyAll, onClose }: AIFixR
<div className="mt-3 flex gap-2">
<button
onClick={() => handleApply(fix)}
className="flex items-center gap-1 rounded-md bg-gradient-brand px-3 py-1.5 text-xs font-medium text-white shadow-sm shadow-primary/20 hover:opacity-90"
className="flex items-center gap-1 rounded-md bg-gradient-brand px-3 py-1.5 text-xs font-medium text-white shadow-xs shadow-primary/20 hover:opacity-90"
>
<Check className="h-3 w-3" />
Apply

View File

@@ -139,14 +139,14 @@ function FlowCanvasInner({ selectedNodeId, onNodeSelect, onSelectAnswerType, onN
proOptions={{ hideAttribution: true }}
className="dark bg-accent/30"
>
<Background variant={BackgroundVariant.Dots} gap={20} size={1.5} color="hsl(var(--muted-foreground) / 0.25)" />
<Controls showInteractive={false} className="!bg-card !border-border !shadow-lg" />
<Background variant={BackgroundVariant.Dots} gap={20} size={1.5} color="oklch(0.63 0.02 260 / 0.25)" />
<Controls showInteractive={false} className="bg-card! border-border! shadow-lg!" />
{minimapVisible && (
<MiniMap
pannable
zoomable
nodeColor={minimapNodeColor}
className="!bg-card !border-border"
className="bg-card! border-border!"
nodeStrokeWidth={2}
/>
)}

View File

@@ -16,7 +16,7 @@ function FlowCanvasAnswerNodeComponent({ data, selected }: NodeProps) {
return (
<>
<Handle type="target" position={Position.Top} className="!bg-border !w-2 !h-2 !border-0" />
<Handle type="target" position={Position.Top} className="bg-border! w-2! h-2! border-0!" />
<div
className={cn(
@@ -61,7 +61,7 @@ function FlowCanvasAnswerNodeComponent({ data, selected }: NodeProps) {
)}
</div>
<Handle type="source" position={Position.Bottom} className="!bg-border !w-2 !h-2 !border-0" />
<Handle type="source" position={Position.Bottom} className="bg-border! w-2! h-2! border-0!" />
</>
)
}

View File

@@ -62,15 +62,15 @@ function FlowCanvasNodeComponent({ data, selected }: NodeProps) {
return (
<>
{/* Target handle at top */}
<Handle type="target" position={Position.Top} className="!bg-border !w-2 !h-2 !border-0" />
<Handle type="target" position={Position.Top} className="bg-border! w-2! h-2! border-0!" />
<div
onContextMenu={(e) => onContextMenu?.(e, node.id)}
className={cn(
'w-[280px] rounded-xl border border-border bg-card shadow-sm cursor-pointer transition-all',
'w-[280px] rounded-xl border border-border bg-card shadow-xs cursor-pointer transition-all',
config.borderClass,
selected && 'ring-1 ring-primary shadow-md',
isGhost && 'border-dashed !border-primary/40 opacity-60'
isGhost && 'border-dashed border-primary/40! opacity-60'
)}
>
{/* Header */}
@@ -175,7 +175,7 @@ function FlowCanvasNodeComponent({ data, selected }: NodeProps) {
</div>
{/* Source handle at bottom */}
<Handle type="source" position={Position.Bottom} className="!bg-border !w-2 !h-2 !border-0" />
<Handle type="source" position={Position.Bottom} className="bg-border! w-2! h-2! border-0!" />
</>
)
}

View File

@@ -28,7 +28,7 @@ export function MetadataSidePanel({ isOpen, onClose }: MetadataSidePanelProps) {
<>
{/* Backdrop — click to close */}
<div
className="fixed inset-0 z-40 bg-background/40 backdrop-blur-sm"
className="fixed inset-0 z-40 bg-background/40 backdrop-blur-xs"
onClick={onClose}
aria-hidden="true"
/>

View File

@@ -62,7 +62,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
className={cn(
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
'bg-card text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
titleError ? 'border-red-400' : 'border-border'
)}
/>
@@ -107,7 +107,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
className={cn(
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
'bg-background text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
)}
/>
)}
@@ -134,7 +134,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
className={cn(
'block w-full rounded-md border border-border px-3 py-2 font-mono text-sm',
'bg-background text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
)}
/>
)}
@@ -154,7 +154,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
className={cn(
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
'bg-background text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
)}
/>
</div>
@@ -195,7 +195,7 @@ export function NodeFormAction({ node, onUpdate }: NodeFormActionProps) {
className={cn(
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
'bg-card text-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
>
<option value="">Link to existing node...</option>

View File

@@ -104,7 +104,7 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
className={cn(
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
'bg-card text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
questionError ? 'border-red-400' : 'border-border'
)}
/>
@@ -126,7 +126,7 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
className={cn(
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
'bg-background text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
)}
/>
</div>
@@ -187,7 +187,7 @@ export function NodeFormDecision({ node, onUpdate }: NodeFormDecisionProps) {
className={cn(
'block w-full rounded-md border px-3 py-2 text-sm',
'bg-background text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary',
optionLabelError ? 'border-red-400' : 'border-border'
)}
/>

View File

@@ -59,7 +59,7 @@ export function NodeFormResolution({ node, onUpdate }: NodeFormResolutionProps)
className={cn(
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
'bg-card text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
titleError ? 'border-red-400' : 'border-border'
)}
/>
@@ -103,7 +103,7 @@ Document what was done and the outcome.
className={cn(
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
'bg-background text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
)}
/>
)}
@@ -134,7 +134,7 @@ Document what was done and the outcome.
className={cn(
'block w-full rounded-md border border-border px-3 py-2 text-sm',
'bg-background text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary'
)}
/>
</div>

View File

@@ -184,7 +184,7 @@ function NodeListItem({
'group flex items-center gap-1 rounded-md px-2 py-1.5 text-sm transition-colors cursor-pointer',
isRootNode
? isSelected
? 'bg-blue-500/20 ring-2 ring-blue-500 shadow-sm'
? 'bg-blue-500/20 ring-2 ring-blue-500 shadow-xs'
: 'bg-blue-500/10 border border-blue-500/30 hover:bg-blue-500/15'
: isSelected
? 'bg-primary/10 ring-1 ring-primary'
@@ -581,7 +581,7 @@ export function NodeList() {
{/* Add Node Type Selector */}
{addingToParent && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-xs">
<div className="w-full max-w-xs rounded-lg border border-border bg-card p-4 shadow-lg">
<h3 className="mb-3 text-sm font-semibold">Select Node Type</h3>
<div className="space-y-2">

View File

@@ -167,7 +167,7 @@ export function NodePicker({
className={cn(
'flex-1 rounded-md border border-border px-2 py-1 text-sm',
'bg-card text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
/>
</div>
@@ -201,7 +201,7 @@ export function NodePicker({
className={cn(
'block w-full rounded-md border px-3 py-2 text-sm',
'bg-card text-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
error ? 'border-red-400' : 'border-border'
)}
>

View File

@@ -64,7 +64,7 @@ interface AddNodePickerProps {
function AddNodePicker({ onSelect, onCancel }: AddNodePickerProps) {
return (
<div className="flex items-center gap-2 rounded-xl border border-dashed border-primary/40 bg-card px-3 py-2 shadow-sm">
<div className="flex items-center gap-2 rounded-xl border border-dashed border-primary/40 bg-card px-3 py-2 shadow-xs">
<span className="text-xs text-muted-foreground shrink-0">Add:</span>
<button
@@ -691,7 +691,7 @@ export function TreeCanvas() {
style={{
// Subtle dot grid background
backgroundImage:
'radial-gradient(circle, hsl(var(--border)) 1px, transparent 1px)',
'radial-gradient(circle, var(--color-border) 1px, transparent 1px)',
backgroundSize: '24px 24px',
}}
>

View File

@@ -168,7 +168,7 @@ export function TreeCanvasNode({
return (
<div
className={cn(
'relative rounded-xl border border-border bg-card shadow-sm transition-all duration-150',
'relative rounded-xl border border-border bg-card shadow-xs transition-all duration-150',
config.borderClass,
isExpanded && 'ring-1 ring-primary shadow-md',
isSelected && !isExpanded && 'ring-1 ring-primary/50',

View File

@@ -73,7 +73,7 @@ export function TreeMetadataForm() {
className={cn(
'mt-1 block w-full rounded-md border px-3 py-2 text-sm',
'bg-card text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20',
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20',
nameError ? 'border-red-400' : 'border-border'
)}
/>
@@ -94,7 +94,7 @@ export function TreeMetadataForm() {
className={cn(
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
'bg-card text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
/>
</div>
@@ -112,7 +112,7 @@ export function TreeMetadataForm() {
className={cn(
'mt-1 block w-full rounded-md border border-border px-3 py-2 text-sm',
'bg-card text-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
>
<option value="">No category</option>
@@ -134,7 +134,7 @@ export function TreeMetadataForm() {
className={cn(
'block min-w-0 flex-1 rounded-md border border-border px-3 py-2 text-sm',
'bg-card text-foreground placeholder:text-muted-foreground',
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20'
'focus:border-primary focus:outline-hidden focus:ring-1 focus:ring-primary/20'
)}
autoFocus
/>

View File

@@ -76,7 +76,7 @@ export function ValidationSummary({ errors, onSelectNode, onFixWithAI, isFixing
'flex items-center gap-1.5 rounded-md px-3 py-1 text-xs font-medium transition-colors',
isFixing
? 'bg-primary/10 text-primary cursor-wait'
: 'bg-gradient-brand text-white shadow-sm shadow-primary/20 hover:opacity-90'
: 'bg-gradient-brand text-white shadow-xs shadow-primary/20 hover:opacity-90'
)}
>
{isFixing ? (
@@ -109,7 +109,7 @@ export function ValidationSummary({ errors, onSelectNode, onFixWithAI, isFixing
: 'cursor-default'
)}
>
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-red-400" />
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-400" />
<div className="flex-1">
<p className="text-red-400">{error.message}</p>
{error.nodeId && (
@@ -133,7 +133,7 @@ export function ValidationSummary({ errors, onSelectNode, onFixWithAI, isFixing
: 'cursor-default'
)}
>
<AlertTriangle className="mt-0.5 h-4 w-4 flex-shrink-0 text-yellow-400" />
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-yellow-400" />
<div className="flex-1">
<p className="text-yellow-400">{warning.message}</p>
{warning.nodeId && (

View File

@@ -155,10 +155,10 @@ export function useTreeLayout(): UseTreeLayoutResult {
target: child.id,
type: 'smoothstep',
label: edgeLabel,
labelStyle: { fill: 'hsl(var(--muted-foreground))', fontSize: 11 },
labelBgStyle: { fill: 'hsl(var(--card))', fillOpacity: 0.9 },
labelStyle: { fill: 'var(--color-muted-foreground)', fontSize: 11 },
labelBgStyle: { fill: 'var(--color-card)', fillOpacity: 0.9 },
labelBgPadding: [4, 2] as [number, number],
style: { stroke: 'hsl(var(--border))' },
style: { stroke: 'var(--color-border)' },
})
walk(child, node.id)
@@ -183,17 +183,17 @@ export function useTreeLayout(): UseTreeLayoutResult {
type: 'smoothstep',
animated: true,
label: ref.label ? truncateLabel(ref.label) : undefined,
labelStyle: { fill: 'hsl(var(--primary))', fontSize: 10, fontWeight: 500 },
labelBgStyle: { fill: 'hsl(var(--card))', fillOpacity: 0.95 },
labelStyle: { fill: 'var(--color-primary)', fontSize: 10, fontWeight: 500 },
labelBgStyle: { fill: 'var(--color-card)', fillOpacity: 0.95 },
labelBgPadding: [4, 2] as [number, number],
style: {
stroke: 'hsl(var(--primary))',
stroke: 'var(--color-primary)',
strokeWidth: 2,
strokeDasharray: '6 3',
},
markerEnd: {
type: 'arrowclosed' as const,
color: 'hsl(var(--primary))',
color: 'var(--color-primary)',
width: 16,
height: 16,
},