Skip to content

Commit 32bf1a9

Browse files
committed
feat(design): workbench light + dark, shared color picker, per-color random
Splits the single Workbench /design card into two — 'Workbench · Light' (warm cream card, links to the quote composer) and 'Workbench · Dark' (warm near-black card, links to the pattern generator). Both carry the same terracotta accent and TerminalIcon tile so they read as one design language in two moods. Switches apps/pattern-generator to the dark Workbench palette — #0F0F10 ground, #17171A surfaces, cream ink, same terracotta accent. The scoped Workbench CSS picks up the new palette via root variables with no rewrite of the controls chrome. Replaces the inline WbColorRow in pattern-generator with the shared CustomColorPicker component (variant='brutalist'), giving the full HSV picker with eyedropper, saturation/value square, and hue slider. Adds a per-color randomize button above each swatch — a micro icon button that sits on the label row, hovers to accent tint, and fills the slot with a freshly rolled random hex. Also swaps the WbColorPicker in the quote generator ControlPanel for the same shared CustomColorPicker (minimal wrapper keeping the Workbench label styling above) so both surfaces use the same picker.
1 parent c6e0d13 commit 32bf1a9

3 files changed

Lines changed: 171 additions & 93 deletions

File tree

src/app/QuoteGenerator/components/ControlPanel.jsx

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useRef } from 'react';
1+
import React from 'react';
22
import {
33
TextTIcon,
44
PaletteIcon,
@@ -8,6 +8,7 @@ import {
88
QuotesIcon,
99
CaretDownIcon,
1010
} from '@phosphor-icons/react';
11+
import CustomColorPicker from '../../../components/CustomColorPicker';
1112

1213
// ─── Workbench-native widgets ───────────────────────────────────────────
1314
// Reads CSS variables set by the host page (QuoteGeneratorPage) so the
@@ -250,38 +251,12 @@ const WbDropdown = ({ label, options, value, onChange, icon: Icon }) => (
250251
</div>
251252
);
252253

253-
const WbColorPicker = ({ label, value, onChange }) => {
254-
const ref = useRef(null);
255-
return (
256-
<div className="flex items-center gap-3">
257-
<div
258-
className="wb-swatch"
259-
style={{ background: value }}
260-
onClick={() => ref.current?.click()}
261-
role="button"
262-
aria-label={`Pick ${label}`}
263-
>
264-
<input
265-
ref={ref}
266-
type="color"
267-
value={value}
268-
onChange={(e) => onChange(e.target.value)}
269-
style={{ opacity: 0, width: 0, height: 0, pointerEvents: 'none' }}
270-
/>
271-
</div>
272-
<div className="flex-1 min-w-0">
273-
<div className="wb-label mb-1">{label}</div>
274-
<input
275-
type="text"
276-
value={(value || '').toUpperCase()}
277-
onChange={(e) => onChange(e.target.value)}
278-
className="wb-input wb-hex"
279-
style={{ padding: '6px 10px', fontSize: 13 }}
280-
/>
281-
</div>
282-
</div>
283-
);
284-
};
254+
const WbColorPicker = ({ label, value, onChange }) => (
255+
<div>
256+
{label && <div className="wb-label mb-2">{label}</div>}
257+
<CustomColorPicker value={value} onChange={onChange} variant="brutalist" />
258+
</div>
259+
);
285260

286261
// ─── THEME PRESETS ──────────────────────────────────────────────────────
287262
const FONT_OPTIONS = [

src/pages/DesignSelectionPage.jsx

Lines changed: 111 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,111 @@ const DesignSelectionPage = () => {
592592
</motion.div>
593593
</Link>
594594

595-
{/* ── WORKBENCH ── */}
595+
{/* ── WORKBENCH · LIGHT ── */}
596+
<Link to="/apps/quote-generator" className="group block relative">
597+
<motion.div
598+
whileHover={{ y: -10 }}
599+
className="h-full p-12 flex flex-col justify-between overflow-hidden transition-all duration-500 relative"
600+
style={{
601+
background: '#FAF7F0',
602+
borderRadius: 14,
603+
boxShadow:
604+
'inset 0 0 0 1px rgba(25,23,22,0.08), 0 30px 60px -30px rgba(25,23,22,0.18)',
605+
}}
606+
>
607+
<div
608+
className="absolute inset-0 pointer-events-none"
609+
style={{
610+
background:
611+
'radial-gradient(60% 40% at 50% 0%, rgba(196,100,58,0.08), transparent 65%)',
612+
}}
613+
/>
614+
<div
615+
className="absolute inset-0 pointer-events-none"
616+
style={{
617+
backgroundImage:
618+
'radial-gradient(circle at 30% 30%, rgba(25,23,22,0.025) 1px, transparent 1px)',
619+
backgroundSize: '3px 3px',
620+
opacity: 0.6,
621+
}}
622+
/>
623+
624+
<div className="space-y-8 relative z-10">
625+
<div
626+
className="w-14 h-14 flex items-center justify-center"
627+
style={{
628+
background: 'rgba(196,100,58,0.12)',
629+
color: '#C4643A',
630+
borderRadius: 10,
631+
border: '1px solid rgba(196,100,58,0.3)',
632+
}}
633+
>
634+
<TerminalIcon size={26} weight="regular" />
635+
</div>
636+
<div className="space-y-4">
637+
<div
638+
className="text-[10px] flex items-center gap-2"
639+
style={{
640+
color: '#6B6A65',
641+
fontFamily: "'JetBrains Mono', monospace",
642+
letterSpacing: '0.08em',
643+
}}
644+
>
645+
<span
646+
className="h-1.5 w-1.5 rounded-full"
647+
style={{ background: '#C4643A' }}
648+
/>
649+
<span>light · v2026.1</span>
650+
</div>
651+
<h3
652+
className="leading-none tracking-[-0.01em]"
653+
style={{
654+
fontFamily: "'Fraunces', serif",
655+
fontVariationSettings: "'opsz' 72, 'SOFT' 0, 'WONK' 0",
656+
color: '#1A1918',
657+
fontSize: 52,
658+
fontWeight: 400,
659+
}}
660+
>
661+
Workbench
662+
</h3>
663+
<p
664+
className="text-sm leading-relaxed"
665+
style={{
666+
fontFamily: "'Instrument Sans', sans-serif",
667+
color: '#6B6A65',
668+
}}
669+
>
670+
Calm 2026 light dev-tool aesthetic — warm cream canvas,
671+
muted terracotta accent, Fraunces and Instrument Sans.
672+
Applied to the quote composer.
673+
</p>
674+
</div>
675+
</div>
676+
<div
677+
className="mt-12 pt-8 flex justify-between items-center relative z-10"
678+
style={{ borderTop: '1px solid rgba(25,23,22,0.08)' }}
679+
>
680+
<span
681+
className="text-[10px] flex items-center gap-2"
682+
style={{
683+
color: '#C4643A',
684+
fontFamily: "'JetBrains Mono', monospace",
685+
letterSpacing: '0.1em',
686+
}}
687+
>
688+
open composer
689+
</span>
690+
<ArrowRightIcon
691+
className="group-hover:translate-x-2 transition-all"
692+
size={22}
693+
style={{ color: '#C4643A' }}
694+
/>
695+
</div>
696+
</motion.div>
697+
</Link>
698+
699+
{/* ── WORKBENCH · DARK ── */}
596700
<Link to="/apps/pattern-generator" className="group block relative">
597701
<motion.div
598702
whileHover={{ y: -10 }}
@@ -604,15 +708,13 @@ const DesignSelectionPage = () => {
604708
'inset 0 0 0 1px rgba(239,236,228,0.08), 0 30px 60px -30px rgba(0,0,0,0.6)',
605709
}}
606710
>
607-
{/* warm top vignette */}
608711
<div
609712
className="absolute inset-0 pointer-events-none"
610713
style={{
611714
background:
612715
'radial-gradient(60% 40% at 50% 0%, rgba(196,100,58,0.08), transparent 65%)',
613716
}}
614717
/>
615-
{/* subtle film grain */}
616718
<div
617719
className="absolute inset-0 pointer-events-none"
618720
style={{
@@ -648,15 +750,15 @@ const DesignSelectionPage = () => {
648750
className="h-1.5 w-1.5 rounded-full"
649751
style={{ background: '#C4643A' }}
650752
/>
651-
<span>composer · v2026.1</span>
753+
<span>dark · v2026.1</span>
652754
</div>
653755
<h3
654756
className="leading-none tracking-[-0.01em]"
655757
style={{
656758
fontFamily: "'Fraunces', serif",
657759
fontVariationSettings: "'opsz' 72, 'SOFT' 0, 'WONK' 0",
658760
color: '#EFECE4',
659-
fontSize: 56,
761+
fontSize: 52,
660762
fontWeight: 400,
661763
}}
662764
>
@@ -669,9 +771,9 @@ const DesignSelectionPage = () => {
669771
color: '#A8A49B',
670772
}}
671773
>
672-
Calm 2026 dev-tool aesthetic — Claude Code, Codex. Warm
673-
near-black canvas, a single muted terracotta accent,
674-
Fraunces and Instrument Sans. No noise.
774+
Same Workbench vocabulary after hours — warm near-black
775+
canvas, cream ink, the same terracotta accent. Applied
776+
to the pattern generator.
675777
</p>
676778
</div>
677779
</div>
@@ -687,7 +789,7 @@ const DesignSelectionPage = () => {
687789
letterSpacing: '0.1em',
688790
}}
689791
>
690-
open workbench
792+
open pattern lab
691793
</span>
692794
<ArrowRightIcon
693795
className="group-hover:translate-x-2 transition-all"

src/pages/apps/PatternGeneratorPage.jsx

Lines changed: 52 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,30 @@ import {
1414
} from '@phosphor-icons/react';
1515
import Seo from '../../components/Seo';
1616
import { useToast } from '../../hooks/useToast';
17+
import CustomColorPicker from '../../components/CustomColorPicker';
1718

18-
// ─── WORKBENCH :: calm modern dev-tool aesthetic (light) ───────────────
19+
// ─── WORKBENCH :: calm modern dev-tool aesthetic (dark) ────────────────
1920
const W = {
20-
bg: '#FAF7F0',
21-
bgSoft: '#F4EFE4',
22-
surface: '#FFFFFF',
23-
surface2: '#F6F1E4',
24-
hair: 'rgba(25,23,22,0.08)',
25-
hairHi: 'rgba(25,23,22,0.16)',
26-
ink: '#1A1918',
27-
inkSoft: '#6B6A65',
28-
inkDim: '#A8A49B',
21+
bg: '#0F0F10',
22+
bgSoft: '#141415',
23+
surface: '#17171A',
24+
surface2: '#1E1E22',
25+
hair: 'rgba(239,236,228,0.08)',
26+
hairHi: 'rgba(239,236,228,0.16)',
27+
ink: '#EFECE4',
28+
inkSoft: '#A8A49B',
29+
inkDim: '#6E6B64',
2930
accent: '#C4643A',
30-
accentSoft: 'rgba(196,100,58,0.10)',
31-
accentRing: 'rgba(196,100,58,0.22)',
31+
accentSoft: 'rgba(196,100,58,0.14)',
32+
accentRing: 'rgba(196,100,58,0.28)',
3233
};
3334

35+
const randomHex = () =>
36+
'#' +
37+
Math.floor(Math.random() * 0xFFFFFF)
38+
.toString(16)
39+
.padStart(6, '0');
40+
3441
// ─── Workbench widgets (local copies, drive off CSS vars) ───────────────
3542
const WB_CSS = `
3643
.wb-label {
@@ -148,6 +155,20 @@ const WB_CSS = `
148155
background: #A85430;
149156
border-color: #A85430;
150157
}
158+
.wb-micro-btn {
159+
display: inline-flex; align-items: center; justify-content: center;
160+
width: 22px; height: 22px;
161+
border-radius: 6px;
162+
background: transparent;
163+
border: none;
164+
color: var(--wb-ink-soft);
165+
cursor: pointer;
166+
transition: background .15s ease, color .15s ease;
167+
}
168+
.wb-micro-btn:hover {
169+
background: var(--wb-accent-soft);
170+
color: var(--wb-accent);
171+
}
151172
.wb-kbd {
152173
display: inline-flex; align-items: center; gap: 4px;
153174
padding: 2px 6px; border-radius: 5px;
@@ -245,38 +266,6 @@ const WbDropdown = ({ label, options, value, onChange, icon: Icon }) => (
245266
</div>
246267
);
247268

248-
const WbColorRow = ({ label, value, onChange }) => {
249-
const ref = useRef(null);
250-
return (
251-
<div className="flex items-center gap-3">
252-
<div
253-
className="wb-swatch"
254-
style={{ background: value }}
255-
onClick={() => ref.current?.click()}
256-
role="button"
257-
aria-label={`Pick ${label}`}
258-
>
259-
<input
260-
ref={ref}
261-
type="color"
262-
value={value}
263-
onChange={(e) => onChange(e.target.value)}
264-
style={{ opacity: 0, width: 0, height: 0, pointerEvents: 'none' }}
265-
/>
266-
</div>
267-
<div className="flex-1 min-w-0">
268-
<div className="wb-label mb-1">{label}</div>
269-
<input
270-
type="text"
271-
value={(value || '').toUpperCase()}
272-
onChange={(e) => onChange(e.target.value)}
273-
className="wb-input"
274-
/>
275-
</div>
276-
</div>
277-
);
278-
};
279-
280269
// ─── data ─────────────────────────────────────────────────────────────
281270
const PREDEFINED_PALETTES = [
282271
['#264653', '#2a9d8f', '#e9c46a', '#f4a261', '#e76f51'],
@@ -775,14 +764,26 @@ const PatternGeneratorPage = () => {
775764
</button>
776765
</div>
777766

778-
<div className="space-y-3">
767+
<div className="space-y-4">
779768
{palette.map((color, i) => (
780-
<WbColorRow
781-
key={i}
782-
label={`Color ${i + 1}`}
783-
value={color}
784-
onChange={(newColor) => updateColor(i, newColor)}
785-
/>
769+
<div key={i} className="space-y-1.5">
770+
<div className="flex items-center justify-between">
771+
<span className="wb-label">Color {i + 1}</span>
772+
<button
773+
onClick={() => updateColor(i, randomHex())}
774+
className="wb-micro-btn"
775+
title="Randomize this color"
776+
aria-label="Randomize this color"
777+
>
778+
<ShuffleIcon size={11} />
779+
</button>
780+
</div>
781+
<CustomColorPicker
782+
value={color}
783+
onChange={(newColor) => updateColor(i, newColor)}
784+
variant="brutalist"
785+
/>
786+
</div>
786787
))}
787788
</div>
788789
</div>

0 commit comments

Comments
 (0)