Skip to content

Commit 700e935

Browse files
JasonYeYuheclaude
andcommitted
feat: add Mood Palette, UI Preview, Mesh Gradient, and Color Stories
- /mood-palette/ — AI generates 5-color palette from any mood/scene description (Gemini) - /preview/ — visualize any palette on real UI components (hero, cards, form) - /mesh-gradient/ — CSS mesh gradient builder with presets, controls, CSS export, PNG download - /stories/ — color family stories (9 articles: history, psychology, design, brands) - POST /ai/mood-palette server endpoint added - html-to-image package added for PNG export Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 59b25de commit 700e935

17 files changed

Lines changed: 1634 additions & 1 deletion

File tree

app/mesh-gradient/page.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Metadata } from "next";
2+
import { SiteHeader } from "@/src/components/site-header";
3+
import { MeshGradientPage } from "@/src/components/mesh-gradient-page";
4+
5+
export const metadata: Metadata = {
6+
title: { absolute: "Mesh Gradient Generator | ColorArchive" },
7+
description:
8+
"Create beautiful CSS mesh gradients with multiple color stops. Export as CSS code or download as PNG. Free online mesh gradient maker.",
9+
alternates: { canonical: "/mesh-gradient/" },
10+
openGraph: {
11+
title: "Mesh Gradient Generator | ColorArchive",
12+
description: "Create stunning mesh gradients and export as CSS or PNG.",
13+
images: ["https://colorarchive.me/og-image-v1.png"],
14+
},
15+
};
16+
17+
export default function MeshGradientRoute() {
18+
return (
19+
<>
20+
<SiteHeader currentPath="/mesh-gradient" />
21+
<MeshGradientPage />
22+
</>
23+
);
24+
}

app/mood-palette/page.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Metadata } from "next";
2+
import { SiteHeader } from "@/src/components/site-header";
3+
import { MoodPalettePage } from "@/src/components/mood-palette-page";
4+
5+
export const metadata: Metadata = {
6+
title: { absolute: "AI Mood Palette Generator | ColorArchive" },
7+
description:
8+
"Describe a mood, scene, or emotion and get a beautiful 5-color palette generated by AI. Try 'dark academia', 'arctic morning', or anything you imagine.",
9+
alternates: { canonical: "/mood-palette/" },
10+
openGraph: {
11+
title: "AI Mood Palette Generator | ColorArchive",
12+
description: "Turn any mood or scene into a beautiful color palette. Powered by AI.",
13+
images: ["https://colorarchive.me/og-image-v1.png"],
14+
},
15+
};
16+
17+
export default function MoodPaletteRoute() {
18+
return (
19+
<>
20+
<SiteHeader currentPath="/mood-palette" />
21+
<MoodPalettePage />
22+
</>
23+
);
24+
}

app/preview/page.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Metadata } from "next";
2+
import { SiteHeader } from "@/src/components/site-header";
3+
import { PalettePreviewPage } from "@/src/components/palette-preview-page";
4+
5+
export const metadata: Metadata = {
6+
title: { absolute: "Palette UI Preview | ColorArchive" },
7+
description:
8+
"See how any color palette looks applied to real UI components — landing pages, cards, and navigation. Preview before you build.",
9+
alternates: { canonical: "/preview/" },
10+
openGraph: {
11+
title: "Palette UI Preview | ColorArchive",
12+
description: "Visualize any palette on real UI components in seconds.",
13+
images: ["https://colorarchive.me/og-image-v1.png"],
14+
},
15+
};
16+
17+
export default function PreviewRoute() {
18+
return (
19+
<>
20+
<SiteHeader currentPath="/preview" />
21+
<PalettePreviewPage />
22+
</>
23+
);
24+
}

app/stories/[slug]/page.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { Metadata } from "next";
2+
import { notFound } from "next/navigation";
3+
import { SiteHeader } from "@/src/components/site-header";
4+
import { ColorStoryPage } from "@/src/components/color-story-page";
5+
import stories from "@/src/data/color-stories.json";
6+
7+
type Story = {
8+
slug: string;
9+
name: string;
10+
hex: string;
11+
hue: string;
12+
headline: string;
13+
summary: string;
14+
origin: string;
15+
psychology: string;
16+
design: string;
17+
brands: string;
18+
palette_tip: string;
19+
};
20+
21+
export function generateStaticParams() {
22+
return Object.keys(stories).map((slug) => ({ slug }));
23+
}
24+
25+
export async function generateMetadata({
26+
params,
27+
}: {
28+
params: Promise<{ slug: string }>;
29+
}): Promise<Metadata> {
30+
const { slug } = await params;
31+
const story = (stories as Record<string, Story>)[slug];
32+
if (!story) return {};
33+
return {
34+
title: { absolute: `${story.headline} | ColorArchive` },
35+
description: story.summary,
36+
alternates: { canonical: `/stories/${slug}/` },
37+
openGraph: {
38+
title: story.headline,
39+
description: story.summary,
40+
images: ["https://colorarchive.me/og-image-v1.png"],
41+
},
42+
};
43+
}
44+
45+
export default async function StoryRoute({
46+
params,
47+
}: {
48+
params: Promise<{ slug: string }>;
49+
}) {
50+
const { slug } = await params;
51+
const story = (stories as Record<string, Story>)[slug];
52+
if (!story) notFound();
53+
54+
return (
55+
<>
56+
<SiteHeader currentPath="/stories" />
57+
<ColorStoryPage story={story} />
58+
</>
59+
);
60+
}

app/stories/page.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { Metadata } from "next";
2+
import Link from "next/link";
3+
import { SiteHeader } from "@/src/components/site-header";
4+
import stories from "@/src/data/color-stories.json";
5+
6+
export const metadata: Metadata = {
7+
title: { absolute: "Color Stories | ColorArchive" },
8+
description:
9+
"Explore the history, psychology, and cultural significance of every color family. From the passion of Red to the calm of Teal.",
10+
alternates: { canonical: "/stories/" },
11+
openGraph: {
12+
title: "Color Stories | ColorArchive",
13+
description: "The history and psychology behind every color family.",
14+
images: ["https://colorarchive.me/og-image-v1.png"],
15+
},
16+
};
17+
18+
type Story = { slug: string; name: string; hex: string; headline: string; summary: string };
19+
20+
function luminance(hex: string): number {
21+
const r = parseInt(hex.slice(1, 3), 16);
22+
const g = parseInt(hex.slice(3, 5), 16);
23+
const b = parseInt(hex.slice(5, 7), 16);
24+
return 0.299 * r + 0.587 * g + 0.114 * b;
25+
}
26+
27+
export default function StoriesIndexRoute() {
28+
const all = Object.values(stories) as Story[];
29+
30+
return (
31+
<>
32+
<SiteHeader currentPath="/stories" />
33+
<main className="min-h-screen bg-white dark:bg-neutral-950">
34+
<section className="max-w-2xl mx-auto px-4 pt-12 pb-8 text-center">
35+
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-400 mb-3">Editorial</p>
36+
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight text-neutral-900 dark:text-white mb-3">
37+
Color Stories
38+
</h1>
39+
<p className="text-slate-500 dark:text-slate-400 text-sm">
40+
The history, psychology, and cultural significance of every color family.
41+
</p>
42+
</section>
43+
44+
<section className="max-w-2xl mx-auto px-4 pb-16">
45+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
46+
{all.map((story) => {
47+
const tc = luminance(story.hex) > 140 ? "#1a1a1a" : "#ffffff";
48+
return (
49+
<Link key={story.slug} href={`/stories/${story.slug}/`}>
50+
<div
51+
className="rounded-2xl p-5 shadow-sm hover:scale-[1.02] transition-transform"
52+
style={{ backgroundColor: story.hex, minHeight: 140 }}
53+
>
54+
<p className="text-[10px] font-semibold uppercase tracking-wider mb-2 opacity-60" style={{ color: tc }}>
55+
Color Story
56+
</p>
57+
<p className="text-base font-bold leading-snug mb-1" style={{ color: tc }}>
58+
{story.name}
59+
</p>
60+
<p className="text-[11px] opacity-75 line-clamp-2 leading-relaxed" style={{ color: tc }}>
61+
{story.summary}
62+
</p>
63+
</div>
64+
</Link>
65+
);
66+
})}
67+
</div>
68+
</section>
69+
</main>
70+
</>
71+
);
72+
}

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"test:watch": "vitest"
1313
},
1414
"dependencies": {
15+
"html-to-image": "^1.11.13",
1516
"next": "16.1.7",
1617
"react": "19.2.4",
1718
"react-dom": "19.2.4"

scripts/generate-color-stories.mjs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* generate-color-stories.mjs
3+
* Run once: node scripts/generate-color-stories.mjs
4+
* Requires GOOGLE_AI_API_KEY in environment (or .env.local)
5+
* Outputs: src/data/color-stories.json
6+
*/
7+
8+
import { GoogleGenerativeAI } from "@google/generative-ai";
9+
import { writeFileSync, readFileSync, existsSync } from "fs";
10+
import { join, dirname } from "path";
11+
import { fileURLToPath } from "url";
12+
13+
const __dirname = dirname(fileURLToPath(import.meta.url));
14+
15+
// Load .env.local if available
16+
const envPath = join(__dirname, "../.env.local");
17+
if (existsSync(envPath)) {
18+
const env = readFileSync(envPath, "utf8");
19+
for (const line of env.split("\n")) {
20+
const [k, ...v] = line.split("=");
21+
if (k && v.length) process.env[k.trim()] = v.join("=").trim();
22+
}
23+
}
24+
25+
const API_KEY = process.env.GOOGLE_AI_API_KEY;
26+
if (!API_KEY) {
27+
console.error("Missing GOOGLE_AI_API_KEY");
28+
process.exit(1);
29+
}
30+
31+
const FAMILIES = [
32+
{ slug: "red", name: "Red", hex: "#e63946", hue: "warm reds and crimsons" },
33+
{ slug: "orange", name: "Orange", hex: "#f4a261", hue: "vibrant oranges and ambers" },
34+
{ slug: "yellow", name: "Yellow", hex: "#e9c46a", hue: "golden yellows and saffrons" },
35+
{ slug: "lime", name: "Lime", hex: "#90be6d", hue: "fresh lime greens and chartreuses" },
36+
{ slug: "green", name: "Green", hex: "#2a9d8f", hue: "deep greens and forest tones" },
37+
{ slug: "teal", name: "Teal", hex: "#264653", hue: "teal blues and cyan tones" },
38+
{ slug: "blue", name: "Blue", hex: "#4361ee", hue: "rich blues and cobalts" },
39+
{ slug: "purple", name: "Purple", hex: "#7209b7", hue: "purples and violets" },
40+
{ slug: "pink", name: "Pink", hex: "#f72585", hue: "pinks and magentas" },
41+
];
42+
43+
const genAI = new GoogleGenerativeAI(API_KEY);
44+
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" });
45+
46+
async function generateStory(family) {
47+
const prompt = `You are a color historian and design writer. Write a rich, engaging article about the color family "${family.name}" (${family.hue}).
48+
49+
The article should have these sections:
50+
1. "origin" — The historical and cultural origins of this color family (2-3 sentences)
51+
2. "psychology" — The psychological effects and emotional associations (2-3 sentences)
52+
3. "design" — How designers and artists use this color family effectively (2-3 sentences)
53+
4. "brands" — 3-4 well-known brands or artworks that famously use this color, and why (2-3 sentences)
54+
5. "palette_tip" — One practical tip for using this color in a palette (1-2 sentences)
55+
56+
Also provide:
57+
- "headline": A compelling 5-8 word headline for this article
58+
- "summary": A 1-sentence description for SEO meta description (max 20 words)
59+
60+
Respond ONLY with valid JSON:
61+
{
62+
"headline": "...",
63+
"summary": "...",
64+
"origin": "...",
65+
"psychology": "...",
66+
"design": "...",
67+
"brands": "...",
68+
"palette_tip": "..."
69+
}
70+
71+
No markdown, pure JSON.`;
72+
73+
try {
74+
const result = await model.generateContent(prompt);
75+
const text = result.response.text();
76+
let parsed;
77+
try {
78+
parsed = JSON.parse(text.trim());
79+
} catch {
80+
const match = text.match(/\{[\s\S]*\}/);
81+
if (match) parsed = JSON.parse(match[0]);
82+
else throw new Error("Could not parse");
83+
}
84+
console.log(`✓ ${family.name}`);
85+
return { ...family, ...parsed };
86+
} catch (err) {
87+
console.error(`✗ ${family.name}:`, err.message);
88+
return {
89+
...family,
90+
headline: `The World of ${family.name}`,
91+
summary: `Explore the history, psychology, and design applications of ${family.name.toLowerCase()} tones.`,
92+
origin: "",
93+
psychology: "",
94+
design: "",
95+
brands: "",
96+
palette_tip: "",
97+
};
98+
}
99+
}
100+
101+
const outPath = join(__dirname, "../src/data/color-stories.json");
102+
103+
// Load existing to allow resuming
104+
let existing = {};
105+
if (existsSync(outPath)) {
106+
existing = JSON.parse(readFileSync(outPath, "utf8"));
107+
console.log(`Resuming — ${Object.keys(existing).length} already generated`);
108+
}
109+
110+
const stories = { ...existing };
111+
112+
for (const family of FAMILIES) {
113+
if (stories[family.slug]) {
114+
console.log(`⏭ ${family.name} (already exists)`);
115+
continue;
116+
}
117+
const story = await generateStory(family);
118+
stories[family.slug] = story;
119+
// Save after each to allow resuming
120+
writeFileSync(outPath, JSON.stringify(stories, null, 2));
121+
// Rate limit
122+
await new Promise((r) => setTimeout(r, 1500));
123+
}
124+
125+
console.log(`\nDone! Saved to src/data/color-stories.json`);

0 commit comments

Comments
 (0)