designnn 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -12754,6 +12754,246 @@ var init_trends = __esm(() => {
12754
12754
  ],
12755
12755
  source: "builtin"
12756
12756
  },
12757
+ {
12758
+ id: "3d-immersive",
12759
+ name: "3D Immersive Elements",
12760
+ category: "style",
12761
+ description: "WebGL-powered 3D objects, scroll-triggered 3D animations, and AR previews. Depth and interaction move beyond static images.",
12762
+ keywords: ["3d", "webgl", "immersive", "ar", "depth"],
12763
+ popularity: 89,
12764
+ year: 2026,
12765
+ figmaPromptHints: [
12766
+ "Include 3D product renders or interactive model placeholders",
12767
+ "Use layered depth with parallax scrolling effects",
12768
+ "Add perspective transforms on card hover",
12769
+ "Design AR preview button with camera icon"
12770
+ ],
12771
+ source: "builtin"
12772
+ },
12773
+ {
12774
+ id: "experimental-navigation",
12775
+ name: "Experimental Navigation",
12776
+ category: "interaction",
12777
+ description: "Non-linear navigation patterns: radial menus, hidden drawers, interactive maps, and exploration-based journeys.",
12778
+ keywords: ["navigation", "radial", "experimental", "nonlinear", "exploration"],
12779
+ popularity: 76,
12780
+ year: 2026,
12781
+ figmaPromptHints: [
12782
+ "Design a radial or circular navigation menu",
12783
+ "Use hidden drawer navigation revealed by gesture",
12784
+ "Create an interactive map-based navigation",
12785
+ "Include breadcrumb trail for nonlinear journeys"
12786
+ ],
12787
+ source: "builtin"
12788
+ },
12789
+ {
12790
+ id: "y2k-vibrant-palette",
12791
+ name: "Y2K Vibrant Palette",
12792
+ category: "style",
12793
+ description: "Bright, saturated color palettes inspired by Y2K nostalgia. Neon gradients, high-contrast pairings, and dopamine design aesthetics.",
12794
+ keywords: ["y2k", "vibrant", "neon", "dopamine", "nostalgia"],
12795
+ popularity: 83,
12796
+ year: 2026,
12797
+ figmaPromptHints: [
12798
+ "Use saturated neon colors: hot pink, electric blue, lime green",
12799
+ "Apply bold gradient combinations across backgrounds",
12800
+ "Include Y2K-inspired decorative elements: stars, bubbles, chrome",
12801
+ "Use high-contrast color pairings for maximum visual impact"
12802
+ ],
12803
+ source: "builtin"
12804
+ },
12805
+ {
12806
+ id: "scrollytelling",
12807
+ name: "Scrollytelling",
12808
+ category: "interaction",
12809
+ description: "Scroll-based narrative experiences where content unfolds as the user scrolls. Combines motion, text, and visuals into a story.",
12810
+ keywords: ["scroll", "storytelling", "narrative", "motion", "immersive"],
12811
+ popularity: 84,
12812
+ year: 2026,
12813
+ figmaPromptHints: [
12814
+ "Design a vertical narrative with scroll-triggered content reveals",
12815
+ "Use full-screen sections that transition on scroll",
12816
+ "Include progress indicator showing story position",
12817
+ "Combine text, images, and data visualizations in sequence"
12818
+ ],
12819
+ source: "builtin"
12820
+ },
12821
+ {
12822
+ id: "gamified-design",
12823
+ name: "Gamified Design",
12824
+ category: "pattern",
12825
+ description: "Game mechanics applied to UI: points, levels, badges, progress bars, leaderboards, and micro-rewards to boost engagement.",
12826
+ keywords: ["gamification", "badges", "points", "leaderboard", "rewards"],
12827
+ popularity: 80,
12828
+ year: 2026,
12829
+ figmaPromptHints: [
12830
+ "Include progress bars and level indicators",
12831
+ "Design achievement badges with unlock animations",
12832
+ "Add streak counters and daily challenge cards",
12833
+ "Create a leaderboard component with rank, avatar, and score"
12834
+ ],
12835
+ source: "builtin"
12836
+ },
12837
+ {
12838
+ id: "retrofuturism",
12839
+ name: "Retrofuturism",
12840
+ category: "style",
12841
+ description: "Vintage visions of the future: neon accents, chrome textures, pixel art, bold gradients inspired by sci-fi and arcade aesthetics.",
12842
+ keywords: ["retro", "futurism", "neon", "chrome", "sci-fi"],
12843
+ popularity: 72,
12844
+ year: 2026,
12845
+ figmaPromptHints: [
12846
+ "Use neon glow effects on text and borders",
12847
+ "Apply chrome/metallic gradient textures",
12848
+ "Include retro-futuristic typography with scan lines",
12849
+ "Combine dark backgrounds with vibrant neon accents"
12850
+ ],
12851
+ source: "builtin"
12852
+ },
12853
+ {
12854
+ id: "collage-design",
12855
+ name: "Collage Design",
12856
+ category: "style",
12857
+ description: "Scrapbook-style creativity with sticker graphics, torn textures, cutout photos, and hand-drawn elements. Messy on purpose.",
12858
+ keywords: ["collage", "scrapbook", "cutout", "handmade", "texture"],
12859
+ popularity: 68,
12860
+ year: 2026,
12861
+ figmaPromptHints: [
12862
+ "Layer cutout photos with torn paper edges",
12863
+ "Add sticker-like decorative elements",
12864
+ "Use hand-drawn fonts and doodle illustrations",
12865
+ "Mix textures: paper, tape, stamps, ink splashes"
12866
+ ],
12867
+ source: "builtin"
12868
+ },
12869
+ {
12870
+ id: "sustainable-web",
12871
+ name: "Sustainable Web Design",
12872
+ category: "pattern",
12873
+ description: "Eco-conscious design: optimized assets, reduced data transfer, dark themes for energy saving, and accessibility-first approach.",
12874
+ keywords: ["sustainable", "eco", "green", "accessible", "performance"],
12875
+ popularity: 71,
12876
+ year: 2026,
12877
+ figmaPromptHints: [
12878
+ "Use system fonts and minimal custom assets",
12879
+ "Design with dark mode default for OLED energy saving",
12880
+ "Include accessibility indicators: contrast ratios, focus states",
12881
+ "Minimize decorative elements, maximize content clarity"
12882
+ ],
12883
+ source: "builtin"
12884
+ },
12885
+ {
12886
+ id: "voice-ui",
12887
+ name: "Voice UI Interface",
12888
+ category: "component",
12889
+ description: "Voice-activated interface elements: waveform visualizers, voice command indicators, and conversational voice assistants.",
12890
+ keywords: ["voice", "speech", "audio", "waveform", "assistant"],
12891
+ popularity: 74,
12892
+ year: 2026,
12893
+ figmaPromptHints: [
12894
+ "Design a voice input button with pulsing animation",
12895
+ "Include audio waveform visualizer during speech",
12896
+ "Show voice command suggestions as floating chips",
12897
+ "Add visual feedback states: listening, processing, responding"
12898
+ ],
12899
+ source: "builtin"
12900
+ },
12901
+ {
12902
+ id: "agentic-ai-ui",
12903
+ name: "Agentic AI Interface",
12904
+ category: "pattern",
12905
+ description: "UI for autonomous AI agents: task delegation, progress monitoring, approval workflows, and multi-step agent pipelines.",
12906
+ keywords: ["agent", "ai", "autonomous", "workflow", "pipeline"],
12907
+ popularity: 92,
12908
+ year: 2026,
12909
+ figmaPromptHints: [
12910
+ "Design a task pipeline view with agent status indicators",
12911
+ "Include approval/rejection buttons for agent actions",
12912
+ "Show real-time progress with step-by-step breakdown",
12913
+ "Add confidence scores and decision explanations"
12914
+ ],
12915
+ source: "builtin"
12916
+ },
12917
+ {
12918
+ id: "sensory-maximalism",
12919
+ name: "Sensory Maximalism",
12920
+ category: "style",
12921
+ description: "Multi-sensory design that engages all senses: rich textures, bold colors, dynamic motion, and immersive high-energy compositions.",
12922
+ keywords: ["sensory", "maximalism", "immersive", "texture", "energy"],
12923
+ popularity: 75,
12924
+ year: 2026,
12925
+ figmaPromptHints: [
12926
+ "Layer multiple textures: gradients, grain, patterns",
12927
+ "Use bold, clashing color combinations intentionally",
12928
+ "Include dynamic motion indicators and animated elements",
12929
+ "Create visual density with overlapping elements"
12930
+ ],
12931
+ source: "builtin"
12932
+ },
12933
+ {
12934
+ id: "spatial-design",
12935
+ name: "Spatial Design (visionOS)",
12936
+ category: "layout",
12937
+ description: "Design for spatial computing: floating windows, depth layers, glass materials, and eye-tracking interactions for Apple Vision Pro.",
12938
+ keywords: ["spatial", "visionos", "ar", "vr", "floating"],
12939
+ popularity: 78,
12940
+ year: 2026,
12941
+ figmaPromptHints: [
12942
+ "Design floating window panels with glass material",
12943
+ "Use depth layers with z-axis spacing between elements",
12944
+ "Apply frosted glass with high blur and transparency",
12945
+ "Include gaze-based hover states and hand gesture indicators"
12946
+ ],
12947
+ source: "builtin"
12948
+ },
12949
+ {
12950
+ id: "variable-fonts",
12951
+ name: "Variable Font Typography",
12952
+ category: "style",
12953
+ description: "Dynamic typography using variable fonts that respond to interaction, scroll position, or data. Fluid weight and width transitions.",
12954
+ keywords: ["variable-font", "typography", "fluid", "responsive", "dynamic"],
12955
+ popularity: 77,
12956
+ year: 2026,
12957
+ figmaPromptHints: [
12958
+ "Use variable fonts with dramatic weight changes (100-900)",
12959
+ "Apply fluid font sizes that scale with viewport",
12960
+ "Design text that changes weight on hover or scroll",
12961
+ "Combine ultra-thin and ultra-bold weights in same layout"
12962
+ ],
12963
+ source: "builtin"
12964
+ },
12965
+ {
12966
+ id: "tactile-ui",
12967
+ name: "Tactile / Squishy UI",
12968
+ category: "interaction",
12969
+ description: "UI elements that feel physically responsive: bouncy animations, elastic deformations, and pressure-sensitive interactions.",
12970
+ keywords: ["tactile", "squishy", "bounce", "elastic", "physical"],
12971
+ popularity: 73,
12972
+ year: 2026,
12973
+ figmaPromptHints: [
12974
+ "Design buttons with spring-bounce press animation",
12975
+ "Use elastic deformation on drag interactions",
12976
+ "Include rubber-band scroll overscroll effects",
12977
+ "Apply soft, inflated appearance with subtle inner shadows"
12978
+ ],
12979
+ source: "builtin"
12980
+ },
12981
+ {
12982
+ id: "ai-design-system",
12983
+ name: "AI-Generated Design System",
12984
+ category: "pattern",
12985
+ description: "Design systems that adapt and generate components dynamically using AI. Self-evolving tokens, auto-generated variants, and smart theming.",
12986
+ keywords: ["design-system", "ai", "tokens", "auto-generate", "adaptive"],
12987
+ popularity: 86,
12988
+ year: 2026,
12989
+ figmaPromptHints: [
12990
+ "Design a token-based system with color, spacing, and type scales",
12991
+ "Include auto-generated component variants grid",
12992
+ "Show theme switching with AI-suggested palettes",
12993
+ "Add component documentation with usage guidelines"
12994
+ ],
12995
+ source: "builtin"
12996
+ },
12757
12997
  {
12758
12998
  id: "micro-interactions",
12759
12999
  name: "Micro-interactions",
@@ -36351,10 +36591,14 @@ __export(exports_server, {
36351
36591
  });
36352
36592
  import path3 from "path";
36353
36593
  import { fileURLToPath } from "url";
36354
- function createServer(port = 3333) {
36594
+ async function createServer(port = 3333) {
36355
36595
  const app = import_express.default();
36356
36596
  app.use(import_express.default.json());
36357
- app.use(import_express.default.static(path3.join(__dirname2, "../../public")));
36597
+ const publicDir = path3.join(__dirname2, "../../public");
36598
+ const publicDirAlt = path3.join(__dirname2, "../public");
36599
+ const fs_check = await import("fs");
36600
+ const resolvedPublic = fs_check.existsSync(publicDir) ? publicDir : publicDirAlt;
36601
+ app.use(import_express.default.static(resolvedPublic));
36358
36602
  app.get("/api/trends", (req, res) => {
36359
36603
  const { category, search, top } = req.query;
36360
36604
  let results;
@@ -36440,12 +36684,12 @@ function createServer(port = 3333) {
36440
36684
  }
36441
36685
  });
36442
36686
  app.get("/{*splat}", (_req, res) => {
36443
- res.sendFile(path3.join(__dirname2, "../../public/index.html"));
36687
+ res.sendFile(path3.join(resolvedPublic, "index.html"));
36444
36688
  });
36445
36689
  return app;
36446
36690
  }
36447
- function startWebServer(port = 3333) {
36448
- const app = createServer(port);
36691
+ async function startWebServer(port = 3333) {
36692
+ const app = await createServer(port);
36449
36693
  app.listen(port, () => {
36450
36694
  const stats = getTrendStats();
36451
36695
  console.log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "designnn",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Trend-driven design prompt engine for Figma AI. Generate high-quality UI/UX prompts powered by real-time design trend analysis.",
5
5
  "type": "module",
6
6
  "bin": {
package/public/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // ============================================
2
- // DESIGNNN Web UI — Client Application
2
+ // DESIGNNN Web UI v0.3.0 — Client Application
3
3
  // ============================================
4
4
 
5
5
  (function () {
@@ -8,6 +8,7 @@
8
8
  // --- State ---
9
9
  let trends = [];
10
10
  let currentFilter = "all";
11
+ let searchQuery = "";
11
12
 
12
13
  // --- DOM References ---
13
14
  const navBtns = document.querySelectorAll(".nav-btn");
@@ -19,11 +20,14 @@
19
20
  const filterBtns = document.querySelectorAll(".filter-btn");
20
21
  const trendsGrid = document.getElementById("trends-grid");
21
22
  const exploreResult = document.getElementById("explore-result");
23
+ const exploreSearch = document.getElementById("explore-search");
24
+ const trendsCountLabel = document.getElementById("trends-count-label");
22
25
  const mixSelect1 = document.getElementById("mix-select-1");
23
26
  const mixSelect2 = document.getElementById("mix-select-2");
24
27
  const mixContext = document.getElementById("mix-context");
25
28
  const mixSubmit = document.getElementById("mix-submit");
26
29
  const mixResult = document.getElementById("mix-result");
30
+ const statsContainer = document.getElementById("stats-container");
27
31
 
28
32
  // --- Tab Navigation ---
29
33
  navBtns.forEach((btn) => {
@@ -33,6 +37,9 @@
33
37
  tabContents.forEach((t) => t.classList.remove("active"));
34
38
  btn.classList.add("active");
35
39
  document.getElementById(`tab-${tab}`).classList.add("active");
40
+
41
+ // Load stats when switching to stats tab
42
+ if (tab === "stats") loadStats();
36
43
  });
37
44
  });
38
45
 
@@ -41,10 +48,8 @@
41
48
  container.classList.remove("hidden");
42
49
  container.innerHTML = `
43
50
  <div class="loading-indicator">
44
- <div class="loading-dots">
45
- <span></span><span></span><span></span>
46
- </div>
47
- <span>Generating prompt...</span>
51
+ <div class="loading-spinner"></div>
52
+ <span>Generating prompt with AI...</span>
48
53
  </div>
49
54
  `;
50
55
  }
@@ -52,11 +57,12 @@
52
57
  // --- Utility: Show Result ---
53
58
  function showResult(container, prompt, label) {
54
59
  container.classList.remove("hidden");
60
+ const wordCount = prompt.split(/\s+/).length;
55
61
  container.innerHTML = `
56
62
  <div class="result-header">
57
63
  <div class="result-title">${label || "Generated Prompt"}</div>
58
64
  <button class="btn-copy" onclick="copyPrompt(this)">
59
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
65
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
60
66
  Copy
61
67
  </button>
62
68
  </div>
@@ -64,7 +70,8 @@
64
70
  <pre>${escapeHtml(prompt)}</pre>
65
71
  </div>
66
72
  <div class="result-footer">
67
- Paste this prompt into Figma AI (Ctrl+I / Cmd+I)
73
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg>
74
+ Paste into Figma AI (Ctrl+I / Cmd+I) &middot; ${wordCount} words
68
75
  </div>
69
76
  `;
70
77
  }
@@ -91,13 +98,13 @@
91
98
  navigator.clipboard.writeText(pre.textContent).then(() => {
92
99
  btn.classList.add("copied");
93
100
  btn.innerHTML = `
94
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5"/></svg>
101
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5"/></svg>
95
102
  Copied!
96
103
  `;
97
104
  setTimeout(() => {
98
105
  btn.classList.remove("copied");
99
106
  btn.innerHTML = `
100
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
107
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg>
101
108
  Copy
102
109
  `;
103
110
  }, 2000);
@@ -110,7 +117,7 @@
110
117
  async function handleChat(message) {
111
118
  if (!message.trim()) return;
112
119
  chatSubmit.disabled = true;
113
- chatSubmit.textContent = "Generating...";
120
+ chatSubmit.innerHTML = `<div class="loading-spinner" style="width:16px;height:16px;border-width:2px"></div> Generating...`;
114
121
  showLoading(chatResult);
115
122
 
116
123
  try {
@@ -126,7 +133,7 @@
126
133
  showError(chatResult, err.message);
127
134
  } finally {
128
135
  chatSubmit.disabled = false;
129
- chatSubmit.textContent = "Generate";
136
+ chatSubmit.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg> Generate`;
130
137
  }
131
138
  }
132
139
 
@@ -152,29 +159,61 @@
152
159
  trends = data.trends;
153
160
  renderTrends(trends);
154
161
  populateMixSelects(trends);
162
+ // Update trend count in hero
163
+ const trendCountEl = document.getElementById("trend-count");
164
+ if (trendCountEl) trendCountEl.textContent = `${trends.length}`;
155
165
  } catch (err) {
156
166
  trendsGrid.innerHTML = `<div class="error-message">Failed to load trends</div>`;
157
167
  }
158
168
  }
159
169
 
170
+ function getFilteredTrends() {
171
+ let filtered = trends;
172
+ if (currentFilter !== "all") {
173
+ filtered = filtered.filter((t) => t.category === currentFilter);
174
+ }
175
+ if (searchQuery) {
176
+ const q = searchQuery.toLowerCase();
177
+ filtered = filtered.filter(
178
+ (t) =>
179
+ t.name.toLowerCase().includes(q) ||
180
+ t.description.toLowerCase().includes(q) ||
181
+ t.keywords.some((k) => k.toLowerCase().includes(q))
182
+ );
183
+ }
184
+ return filtered;
185
+ }
186
+
160
187
  function renderTrends(list) {
188
+ trendsCountLabel.textContent = `Showing ${list.length} of ${trends.length} trends`;
189
+
190
+ if (list.length === 0) {
191
+ trendsGrid.innerHTML = `<div class="error-message" style="grid-column:1/-1;text-align:center">No trends found matching your criteria.</div>`;
192
+ return;
193
+ }
194
+
161
195
  trendsGrid.innerHTML = list
162
196
  .map(
163
197
  (t) => `
164
198
  <div class="trend-card" data-id="${t.id}">
165
199
  <div class="trend-card-header">
166
200
  <span class="trend-name">${escapeHtml(t.name)}</span>
167
- <span class="trend-category">${t.category}</span>
201
+ <span>
202
+ <span class="trend-category">${t.category}</span>
203
+ ${t.source === "ai-generated" ? '<span class="trend-source">AI</span>' : ""}
204
+ </span>
168
205
  </div>
169
206
  <div class="trend-desc">${escapeHtml(t.description)}</div>
170
- <div class="trend-popularity">
171
- <div class="popularity-bar">
172
- <div class="popularity-fill" style="width: ${t.popularity}%"></div>
207
+ <div class="trend-meta">
208
+ <div class="trend-popularity">
209
+ <div class="popularity-bar">
210
+ <div class="popularity-fill" style="width: ${t.popularity}%"></div>
211
+ </div>
212
+ <span class="popularity-value">${t.popularity}</span>
173
213
  </div>
174
- <span class="popularity-value">${t.popularity}%</span>
175
214
  </div>
176
215
  <div class="trend-keywords">
177
- ${t.keywords.map((k) => `<span class="keyword-tag">${escapeHtml(k)}</span>`).join("")}
216
+ ${t.keywords.slice(0, 4).map((k) => `<span class="keyword-tag">${escapeHtml(k)}</span>`).join("")}
178
217
  </div>
179
218
  <div class="trend-card-action">
180
219
  <button class="btn-generate" onclick="generateFromTrend('${t.id}')">Generate Prompt</button>
@@ -190,19 +229,20 @@
190
229
  currentFilter = btn.dataset.category;
191
230
  filterBtns.forEach((b) => b.classList.remove("active"));
192
231
  btn.classList.add("active");
193
-
194
- if (currentFilter === "all") {
195
- renderTrends(trends);
196
- } else {
197
- renderTrends(trends.filter((t) => t.category === currentFilter));
198
- }
232
+ renderTrends(getFilteredTrends());
199
233
  exploreResult.classList.add("hidden");
200
234
  });
201
235
  });
202
236
 
237
+ if (exploreSearch) {
238
+ exploreSearch.addEventListener("input", (e) => {
239
+ searchQuery = e.target.value;
240
+ renderTrends(getFilteredTrends());
241
+ });
242
+ }
243
+
203
244
  window.generateFromTrend = async function (trendId) {
204
245
  showLoading(exploreResult);
205
- // Scroll to result
206
246
  exploreResult.scrollIntoView({ behavior: "smooth", block: "nearest" });
207
247
 
208
248
  try {
@@ -223,12 +263,24 @@
223
263
  // MIX
224
264
  // ============================================
225
265
  function populateMixSelects(list) {
226
- const options = list
227
- .map((t) => `<option value="${t.id}">${t.name} [${t.category}]</option>`)
266
+ const grouped = {};
267
+ list.forEach((t) => {
268
+ if (!grouped[t.category]) grouped[t.category] = [];
269
+ grouped[t.category].push(t);
270
+ });
271
+
272
+ const optionsHtml = Object.entries(grouped)
273
+ .map(
274
+ ([cat, items]) =>
275
+ `<optgroup label="${cat.charAt(0).toUpperCase() + cat.slice(1)}">` +
276
+ items.map((t) => `<option value="${t.id}">${t.name}</option>`).join("") +
277
+ `</optgroup>`
278
+ )
228
279
  .join("");
280
+
229
281
  const placeholder = `<option value="">Select a trend...</option>`;
230
- mixSelect1.innerHTML = placeholder + options;
231
- mixSelect2.innerHTML = placeholder + options;
282
+ mixSelect1.innerHTML = placeholder + optionsHtml;
283
+ mixSelect2.innerHTML = placeholder + optionsHtml;
232
284
  }
233
285
 
234
286
  async function handleMix() {
@@ -246,7 +298,7 @@
246
298
  }
247
299
 
248
300
  mixSubmit.disabled = true;
249
- mixSubmit.textContent = "Mixing...";
301
+ mixSubmit.innerHTML = `<div class="loading-spinner" style="width:16px;height:16px;border-width:2px"></div> Mixing...`;
250
302
  showLoading(mixResult);
251
303
 
252
304
  try {
@@ -260,18 +312,81 @@
260
312
  showResult(
261
313
  mixResult,
262
314
  data.prompt,
263
- `Mix: ${data.trend1.name} × ${data.trend2.name}`
315
+ `Mix: ${data.trend1.name} + ${data.trend2.name}`
264
316
  );
265
317
  } catch (err) {
266
318
  showError(mixResult, err.message);
267
319
  } finally {
268
320
  mixSubmit.disabled = false;
269
- mixSubmit.textContent = "Mix & Generate";
321
+ mixSubmit.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"/></svg> Mix & Generate`;
270
322
  }
271
323
  }
272
324
 
273
325
  mixSubmit.addEventListener("click", handleMix);
274
326
 
327
+ // ============================================
328
+ // STATS
329
+ // ============================================
330
+ async function loadStats() {
331
+ try {
332
+ const res = await fetch("/api/stats");
333
+ const data = await res.json();
334
+ renderStats(data);
335
+ } catch (err) {
336
+ statsContainer.innerHTML = `<div class="error-message">Failed to load stats</div>`;
337
+ }
338
+ }
339
+
340
+ function renderStats(data) {
341
+ const maxCat = Math.max(...Object.values(data.categories));
342
+
343
+ const categoryBars = Object.entries(data.categories)
344
+ .sort((a, b) => b[1] - a[1])
345
+ .map(
346
+ ([cat, count]) => `
347
+ <div class="stat-bar-row">
348
+ <span class="stat-bar-label">${cat}</span>
349
+ <div class="stat-bar-track">
350
+ <div class="stat-bar-fill" style="width: ${(count / maxCat) * 100}%"></div>
351
+ </div>
352
+ <span class="stat-bar-count">${count}</span>
353
+ </div>
354
+ `
355
+ )
356
+ .join("");
357
+
358
+ statsContainer.innerHTML = `
359
+ <div class="stat-card accent">
360
+ <div class="stat-value">${data.total}</div>
361
+ <div class="stat-label">Total Trends</div>
362
+ </div>
363
+ <div class="stat-card">
364
+ <div class="stat-value">${data.builtin}</div>
365
+ <div class="stat-label">Built-in</div>
366
+ </div>
367
+ <div class="stat-card">
368
+ <div class="stat-value">${data.custom}</div>
369
+ <div class="stat-label">AI-Generated</div>
370
+ </div>
371
+ <div class="stat-card">
372
+ <div class="stat-value">${Object.keys(data.categories).length}</div>
373
+ <div class="stat-label">Categories</div>
374
+ </div>
375
+ <div class="stat-bar-container">
376
+ <div class="stat-bar-title">Trends by Category</div>
377
+ ${categoryBars}
378
+ </div>
379
+ `;
380
+ }
381
+
275
382
  // --- Init ---
276
383
  loadTrends();
384
+
385
+ // Keyboard shortcut: / to focus search
386
+ document.addEventListener("keydown", (e) => {
387
+ if (e.key === "/" && document.activeElement.tagName !== "INPUT" && document.activeElement.tagName !== "TEXTAREA") {
388
+ e.preventDefault();
389
+ chatInput.focus();
390
+ }
391
+ });
277
392
  })();