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 +249 -5
- package/package.json +1 -1
- package/public/app.js +146 -31
- package/public/index.html +79 -26
- package/public/style.css +421 -231
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
|
-
|
|
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(
|
|
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
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-
|
|
45
|
-
|
|
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="
|
|
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
|
-
|
|
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) · ${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="
|
|
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="
|
|
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.
|
|
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.
|
|
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
|
|
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-
|
|
171
|
-
<div class="popularity
|
|
172
|
-
<div class="popularity-
|
|
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
|
|
227
|
-
|
|
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 +
|
|
231
|
-
mixSelect2.innerHTML = placeholder +
|
|
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.
|
|
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}
|
|
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.
|
|
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
|
})();
|