Skip to content

Commit fbdc8b0

Browse files
authored
Merge pull request #131 from peelar/fix/green-gradient
2 parents 9aff93b + d5e5c61 commit fbdc8b0

42 files changed

Lines changed: 1774 additions & 3544 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/dev-only-playground.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"dopeshot-app": patch
3+
---
4+
5+
Hide the playground route outside development builds
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"dopeshot-app": minor
3+
---
4+
5+
Complete OKLCH-based gradient generation pipeline
6+
7+
New gradient pipeline with 6 visually distinct gradient types (each uses different color harmonies):
8+
9+
1. **Mesh Gradient** - Organic multi-color blobs using all harmonies
10+
2. **Split-Complementary Linear** - 150° hue rotation for sophisticated contrast
11+
3. **Triadic Radial** - 120° hue rotation, radial gradient type
12+
4. **Multi-Stop Diagonal** - Three colors (base + triadic + complementary)
13+
5. **Analogous Cool Linear** - -30° hue rotation (cool tones)
14+
6. **Warm Analogous Linear** - +30° hue rotation (warm tones)
15+
16+
Key improvements:
17+
18+
- **Adaptive lightness**: Dark screenshots → darker gradients (12-40%), light screenshots → lighter gradients (50-90%)
19+
- Harmonious palette expansion using color theory (analogous, complementary, triadic, monochromatic)
20+
- Replaced chroma-js with culori for perceptually uniform color manipulation
21+
- Each gradient type has fundamentally different visual character
22+
- Neutral/grayscale palettes now get injected color variety
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"dopeshot-app": minor
3+
---
4+
5+
Introduce palette-matched gradients driven by screenshot color analysis
6+
7+
- Extract dominant/accent/muted hues from screenshots for gradient matching
8+
- Generate six gradient styles with multi-hue variation and alternate secondary palettes
9+
- Add gradient playground for previewing palettes
10+
- Make radial "beam" gradient directional for Peak layouts and shift it downward
11+
- Avoid showing fallback gradients while screenshot analysis is in progress

apps/app/package.json

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@
1010
"lint": "eslint",
1111
"knip": "knip",
1212
"typecheck": "tsc --noEmit",
13-
"test:domain": "tsx tests/color-extraction.test.ts && tsx tests/gradient-generation.test.ts && tsx tests/monochromatic-gradients.test.ts",
1413
"test:ui": "vitest run",
1514
"test:e2e": "playwright test",
1615
"test:e2e:install": "playwright install --with-deps chromium",
17-
"test": "pnpm test:domain && pnpm test:ui && pnpm test:e2e",
18-
"test:ci": "pnpm typecheck && pnpm test:domain && pnpm test:ui && pnpm test:e2e",
19-
"check": "pnpm lint && pnpm typecheck && pnpm test:domain && pnpm test:ui && pnpm test:e2e",
16+
"test": "pnpm test:ui && pnpm test:e2e",
17+
"test:ci": "pnpm typecheck && pnpm test:ui && pnpm test:e2e",
18+
"check": "pnpm lint && pnpm typecheck && pnpm test:ui && pnpm test:e2e",
2019
"release": "pnpm -w changeset:version && pnpm check",
2120
"db:dev": "prisma migrate dev"
2221
},
@@ -33,20 +32,18 @@
3332
"@supabase/supabase-js": "^2.88.0",
3433
"bcryptjs": "^3.0.3",
3534
"better-auth": "^1.4.7",
36-
"chroma-js": "^3.2.0",
3735
"class-variance-authority": "^0.7.1",
3836
"clsx": "^2.1.1",
39-
"culori": "^4.0.2",
4037
"flags": "^4.0.2",
4138
"framer-motion": "^11.11.17",
4239
"geist": "^1.5.1",
4340
"html-to-image": "^1.11.13",
4441
"jotai": "^2.15.2",
4542
"lucide-react": "^0.473.0",
43+
"mesh-gradient": "^1.1.0",
4644
"nanoid": "^5.1.6",
4745
"next": "16.1.1",
4846
"next-themes": "^0.4.6",
49-
"node-vibrant": "^4.0.3",
5047
"pg": "^8.16.3",
5148
"prisma": "^7.2.0",
5249
"prisma-json-types-generator": "^4.0.1",
@@ -55,7 +52,6 @@
5552
"resend": "^6.6.0",
5653
"server-only": "^0.0.1",
5754
"shadcn": "^3.6.2",
58-
"sharp": "0.32.6",
5955
"sonner": "^2.0.7",
6056
"tailwind-merge": "^2.6.0",
6157
"tiny-invariant": "^1.3.3",
@@ -68,8 +64,6 @@
6864
"@testing-library/dom": "^10.4.1",
6965
"@testing-library/jest-dom": "^6.9.1",
7066
"@testing-library/react": "^16.3.0",
71-
"@types/chroma-js": "^3.1.2",
72-
"@types/culori": "^4.0.1",
7367
"@types/node": "^20.17.9",
7468
"@types/pg": "^8.16.0",
7569
"@types/react": "19.2.7",

apps/app/src/app/(playground)/_components/playground-page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,6 @@ function PlaygroundPageInner({
362362
statusMessage,
363363
hasCustomScreenshot,
364364
isProcessingUpload,
365-
isAnalyzingColors,
366365
showFocusHint,
367366
hasScreenshot,
368367
canExport,
@@ -488,7 +487,6 @@ function PlaygroundPageInner({
488487
onEmptyStateClick={openFilePicker}
489488
canvasHeight={canvas.height}
490489
canvasWidth={canvas.width}
491-
isAnalyzingColors={isAnalyzingColors}
492490
showFocusHint={showFocusHint}
493491
hasScreenshot={hasScreenshot}
494492
/>

apps/app/src/app/(playground)/_components/playground-workspace.tsx

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
screenshotZoomAtom,
1111
configAtom,
1212
orientationAtom,
13+
gradientOptionsAtom,
14+
isAnalyzingColorsAtom,
1315
type Orientation,
1416
} from "@/hooks/atoms";
1517
import {
@@ -42,7 +44,6 @@ interface PlaygroundWorkspaceProps {
4244
onEmptyStateClick: () => void;
4345
canvasWidth: number;
4446
canvasHeight: number;
45-
isAnalyzingColors: boolean;
4647
showFocusHint: boolean;
4748
hasScreenshot: boolean;
4849
}
@@ -56,19 +57,22 @@ export function PlaygroundWorkspace({
5657
onEmptyStateClick,
5758
canvasHeight,
5859
canvasWidth,
59-
isAnalyzingColors,
6060
showFocusHint,
6161
hasScreenshot,
6262
}: PlaygroundWorkspaceProps) {
6363
const [screenshotZoom, setScreenshotZoom] = useAtom(screenshotZoomAtom);
6464
const [orientation, setOrientation] = useAtom(orientationAtom);
6565
const config = useAtomValue(configAtom);
6666
const setConfig = useSetAtom(configAtom);
67+
const gradientOptions = useAtomValue(gradientOptionsAtom);
68+
const isAnalyzingColors = useAtomValue(isAnalyzingColorsAtom);
6769
const [bottomWhitespace, setBottomWhitespace] = useState(0);
6870
const isMobile = useMobileDetection();
6971
const hasAutoSetOrientation = useRef(false);
7072

71-
const isBackdropLayout = config.layoutId === "adaptive-stage" || config.layoutId === "full-visual";
73+
const isBackdropLayout =
74+
config.layoutId === "adaptive-stage" || config.layoutId === "full-visual";
75+
const shouldHoldCanvas = hasScreenshot && (isAnalyzingColors || gradientOptions.length === 0);
7276

7377
// Set mobile as default orientation on mobile devices
7478
useEffect(() => {
@@ -81,15 +85,12 @@ export function PlaygroundWorkspace({
8185
hasAutoSetOrientation.current = true;
8286
}, [isMobile, orientation, setOrientation]);
8387

84-
const handleViewportMetricsChange = useCallback(
85-
(metrics: { bottomWhitespace: number }) => {
86-
const nextValue = Math.round(metrics.bottomWhitespace);
87-
setBottomWhitespace((previousValue) =>
88-
previousValue === nextValue ? previousValue : nextValue
89-
);
90-
},
91-
[]
92-
);
88+
const handleViewportMetricsChange = useCallback((metrics: { bottomWhitespace: number }) => {
89+
const nextValue = Math.round(metrics.bottomWhitespace);
90+
setBottomWhitespace((previousValue) =>
91+
previousValue === nextValue ? previousValue : nextValue,
92+
);
93+
}, []);
9394

9495
const handleOrientationChange = (newOrientation: Orientation) => {
9596
// Check if current layout supports new orientation
@@ -125,8 +126,8 @@ export function PlaygroundWorkspace({
125126
fontSize: config.fontSize,
126127
screenshotFrame: config.screenshotFrame,
127128
},
128-
{ preserveEmptyText: true }
129-
)
129+
{ preserveEmptyText: true },
130+
),
130131
);
131132
layoutChanged = compatibleLayout.id !== previousLayoutId;
132133
newLayoutId = compatibleLayout.id;
@@ -156,7 +157,7 @@ export function PlaygroundWorkspace({
156157
"h-7 w-7 rounded transition-colors",
157158
orientation === "mobile"
158159
? "bg-foreground text-background shadow-sm"
159-
: "text-muted-foreground hover:text-foreground"
160+
: "text-muted-foreground hover:text-foreground",
160161
)}
161162
>
162163
<Smartphone className="h-3.5 w-3.5" />
@@ -172,7 +173,7 @@ export function PlaygroundWorkspace({
172173
"h-7 w-7 rounded transition-colors",
173174
orientation === "desktop"
174175
? "bg-foreground text-background shadow-sm"
175-
: "text-muted-foreground hover:text-foreground"
176+
: "text-muted-foreground hover:text-foreground",
176177
)}
177178
>
178179
<Monitor className="h-3.5 w-3.5" />
@@ -191,7 +192,7 @@ export function PlaygroundWorkspace({
191192
"h-7 w-7 rounded transition-colors",
192193
orientation === "desktop"
193194
? "bg-foreground text-background shadow-sm"
194-
: "text-muted-foreground hover:text-foreground"
195+
: "text-muted-foreground hover:text-foreground",
195196
)}
196197
>
197198
<Monitor className="h-3.5 w-3.5" />
@@ -207,7 +208,7 @@ export function PlaygroundWorkspace({
207208
"h-7 w-7 rounded transition-colors",
208209
orientation === "mobile"
209210
? "bg-foreground text-background shadow-sm"
210-
: "text-muted-foreground hover:text-foreground"
211+
: "text-muted-foreground hover:text-foreground",
211212
)}
212213
>
213214
<Smartphone className="h-3.5 w-3.5" />
@@ -231,8 +232,8 @@ export function PlaygroundWorkspace({
231232
: "border-border text-muted-foreground hover:border-foreground/50 hover:text-foreground",
232233
)}
233234
>
234-
{isAspectLocked
235-
? `Locked · ${orientation === "mobile" ? "Mobile" : "16:9"}`
235+
{isAspectLocked
236+
? `Locked · ${orientation === "mobile" ? "Mobile" : "16:9"}`
236237
: `Lock to ${orientation === "mobile" ? "Mobile" : "16:9"}`}
237238
</Button>
238239
) : null}
@@ -243,15 +244,20 @@ export function PlaygroundWorkspace({
243244
className={orientation === "mobile" ? "max-h-[85%]" : undefined}
244245
surfaceWidth={canvasWidth}
245246
surfaceHeight={canvasHeight}
246-
isLoading={isAnalyzingColors}
247-
loadingText="Analyzing colors..."
248247
onViewportMetricsChange={handleViewportMetricsChange}
249248
>
250-
<CoverPreview
251-
showEmptyState={showEmptyState}
252-
showLoadingState={showLoadingState}
253-
onEmptyStateClick={onEmptyStateClick}
254-
/>
249+
<div
250+
className={cn(
251+
"transition-opacity duration-300",
252+
shouldHoldCanvas ? "opacity-0 pointer-events-none" : "opacity-100",
253+
)}
254+
>
255+
<CoverPreview
256+
showEmptyState={showEmptyState}
257+
showLoadingState={showLoadingState}
258+
onEmptyStateClick={onEmptyStateClick}
259+
/>
260+
</div>
255261
</PreviewViewport>
256262
{showFocusHint ? (
257263
<div className="pointer-events-none absolute inset-x-0 top-4 flex justify-center">
@@ -262,13 +268,11 @@ export function PlaygroundWorkspace({
262268
) : null}
263269
</div>
264270

265-
{hasScreenshot ? (
271+
{hasScreenshot && !shouldHoldCanvas ? (
266272
<div className="relative z-10">
267273
<div
268274
style={
269-
bottomWhitespace
270-
? { transform: `translateY(-${bottomWhitespace}px)` }
271-
: undefined
275+
bottomWhitespace ? { transform: `translateY(-${bottomWhitespace}px)` } : undefined
272276
}
273277
>
274278
<ScreenshotZoomSlider

apps/app/src/app/api/generate-gradient/route.ts

Lines changed: 0 additions & 91 deletions
This file was deleted.

apps/app/src/app/page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import { notFound } from "next/navigation";
12
import { PlaygroundPage } from "@/app/(playground)/_components/playground-page";
23
import { verifySession } from "@/lib/auth/session";
34
import { getUserDb } from "@/lib/data/dal";
45
import { getUserTier } from "@/lib/tier";
56
import { isBrandPersonality } from "@/lib/types/brand";
67

8+
const isPlaygroundEnabled = process.env.NODE_ENV !== "production";
9+
710
const BRAND_ONBOARDING_STEP = "brand_profile";
811

912
function looksCompleteFromProfile(profile: {
@@ -22,6 +25,10 @@ function looksCompleteFromProfile(profile: {
2225
}
2326

2427
export default async function Page() {
28+
if (!isPlaygroundEnabled) {
29+
notFound();
30+
}
31+
2532
const session = await verifySession();
2633

2734
if (session.isAuth && session.userId) {

0 commit comments

Comments
 (0)