designnn 0.2.0 → 0.4.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 +641 -158
- package/package.json +1 -1
- package/public/app.js +274 -68
- package/public/i18n-trends.js +92 -0
- package/public/i18n.js +207 -0
- package/public/index.html +94 -42
- package/public/style.css +448 -230
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// ============================================
|
|
2
|
-
// DESIGNNN Web UI — Client Application
|
|
2
|
+
// DESIGNNN Web UI v0.4.0 — Client Application
|
|
3
|
+
// i18n-enabled (EN / JA auto-detect + toggle)
|
|
3
4
|
// ============================================
|
|
4
5
|
|
|
5
6
|
(function () {
|
|
@@ -8,6 +9,11 @@
|
|
|
8
9
|
// --- State ---
|
|
9
10
|
let trends = [];
|
|
10
11
|
let currentFilter = "all";
|
|
12
|
+
let searchQuery = "";
|
|
13
|
+
|
|
14
|
+
// --- i18n Init ---
|
|
15
|
+
currentLang = detectLanguage();
|
|
16
|
+
setLanguage(currentLang);
|
|
11
17
|
|
|
12
18
|
// --- DOM References ---
|
|
13
19
|
const navBtns = document.querySelectorAll(".nav-btn");
|
|
@@ -15,15 +21,100 @@
|
|
|
15
21
|
const chatInput = document.getElementById("chat-input");
|
|
16
22
|
const chatSubmit = document.getElementById("chat-submit");
|
|
17
23
|
const chatResult = document.getElementById("chat-result");
|
|
18
|
-
const
|
|
24
|
+
const quickPromptsContainer = document.getElementById("quick-prompts");
|
|
19
25
|
const filterBtns = document.querySelectorAll(".filter-btn");
|
|
20
26
|
const trendsGrid = document.getElementById("trends-grid");
|
|
21
27
|
const exploreResult = document.getElementById("explore-result");
|
|
28
|
+
const exploreSearch = document.getElementById("explore-search");
|
|
29
|
+
const trendsCountLabel = document.getElementById("trends-count-label");
|
|
22
30
|
const mixSelect1 = document.getElementById("mix-select-1");
|
|
23
31
|
const mixSelect2 = document.getElementById("mix-select-2");
|
|
24
32
|
const mixContext = document.getElementById("mix-context");
|
|
25
33
|
const mixSubmit = document.getElementById("mix-submit");
|
|
26
34
|
const mixResult = document.getElementById("mix-result");
|
|
35
|
+
const statsContainer = document.getElementById("stats-container");
|
|
36
|
+
const langToggle = document.getElementById("lang-toggle");
|
|
37
|
+
const langLabel = document.getElementById("lang-label");
|
|
38
|
+
|
|
39
|
+
// ============================================
|
|
40
|
+
// i18n — Apply translations to DOM
|
|
41
|
+
// ============================================
|
|
42
|
+
function applyI18n() {
|
|
43
|
+
// Update lang label
|
|
44
|
+
langLabel.textContent = currentLang.toUpperCase();
|
|
45
|
+
document.documentElement.lang = currentLang === "ja" ? "ja" : "en";
|
|
46
|
+
|
|
47
|
+
// Set font family for Japanese
|
|
48
|
+
if (currentLang === "ja") {
|
|
49
|
+
document.body.style.fontFamily = "'Noto Sans JP', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif";
|
|
50
|
+
} else {
|
|
51
|
+
document.body.style.fontFamily = "'Inter', -apple-system, BlinkMacSystemFont, sans-serif";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// data-i18n: text content
|
|
55
|
+
document.querySelectorAll("[data-i18n]").forEach((el) => {
|
|
56
|
+
const key = el.getAttribute("data-i18n");
|
|
57
|
+
el.textContent = t(key);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// data-i18n-html: innerHTML (for spans with highlights)
|
|
61
|
+
document.querySelectorAll("[data-i18n-html]").forEach((el) => {
|
|
62
|
+
const key = el.getAttribute("data-i18n-html");
|
|
63
|
+
const trendCount = trends.length || "65+";
|
|
64
|
+
el.innerHTML = t(key, { count: trendCount });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// data-i18n-placeholder: input placeholders
|
|
68
|
+
document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
|
|
69
|
+
const key = el.getAttribute("data-i18n-placeholder");
|
|
70
|
+
el.placeholder = t(key);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Quick prompts
|
|
74
|
+
renderQuickPrompts();
|
|
75
|
+
|
|
76
|
+
// Footer
|
|
77
|
+
const footerText = document.getElementById("footer-text");
|
|
78
|
+
if (footerText) {
|
|
79
|
+
footerText.textContent = t("footerText", { version: "0.4.0" });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Re-render dynamic content
|
|
83
|
+
if (trends.length > 0) {
|
|
84
|
+
renderTrends(getFilteredTrends());
|
|
85
|
+
populateMixSelects(trends);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderQuickPrompts() {
|
|
90
|
+
const prompts = t("chatQuickPrompts");
|
|
91
|
+
if (!Array.isArray(prompts)) return;
|
|
92
|
+
|
|
93
|
+
// Keep the label, remove old buttons
|
|
94
|
+
const label = quickPromptsContainer.querySelector(".quick-label");
|
|
95
|
+
quickPromptsContainer.innerHTML = "";
|
|
96
|
+
if (label) quickPromptsContainer.appendChild(label);
|
|
97
|
+
|
|
98
|
+
prompts.forEach((p) => {
|
|
99
|
+
const btn = document.createElement("button");
|
|
100
|
+
btn.className = "quick-btn";
|
|
101
|
+
btn.dataset.prompt = p.prompt;
|
|
102
|
+
btn.textContent = p.label;
|
|
103
|
+
btn.addEventListener("click", () => {
|
|
104
|
+
chatInput.value = p.prompt;
|
|
105
|
+
handleChat(p.prompt);
|
|
106
|
+
});
|
|
107
|
+
quickPromptsContainer.appendChild(btn);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Language toggle
|
|
112
|
+
langToggle.addEventListener("click", () => {
|
|
113
|
+
const newLang = currentLang === "en" ? "ja" : "en";
|
|
114
|
+
currentLang = newLang;
|
|
115
|
+
setLanguage(newLang);
|
|
116
|
+
applyI18n();
|
|
117
|
+
});
|
|
27
118
|
|
|
28
119
|
// --- Tab Navigation ---
|
|
29
120
|
navBtns.forEach((btn) => {
|
|
@@ -33,43 +124,50 @@
|
|
|
33
124
|
tabContents.forEach((t) => t.classList.remove("active"));
|
|
34
125
|
btn.classList.add("active");
|
|
35
126
|
document.getElementById(`tab-${tab}`).classList.add("active");
|
|
127
|
+
if (tab === "stats") loadStats();
|
|
36
128
|
});
|
|
37
129
|
});
|
|
38
130
|
|
|
39
|
-
//
|
|
131
|
+
// ============================================
|
|
132
|
+
// Utility Functions
|
|
133
|
+
// ============================================
|
|
134
|
+
function escapeHtml(text) {
|
|
135
|
+
const div = document.createElement("div");
|
|
136
|
+
div.textContent = text;
|
|
137
|
+
return div.innerHTML;
|
|
138
|
+
}
|
|
139
|
+
|
|
40
140
|
function showLoading(container) {
|
|
41
141
|
container.classList.remove("hidden");
|
|
42
142
|
container.innerHTML = `
|
|
43
143
|
<div class="loading-indicator">
|
|
44
|
-
<div class="loading-
|
|
45
|
-
|
|
46
|
-
</div>
|
|
47
|
-
<span>Generating prompt...</span>
|
|
144
|
+
<div class="loading-spinner"></div>
|
|
145
|
+
<span>${t("chatLoading")}</span>
|
|
48
146
|
</div>
|
|
49
147
|
`;
|
|
50
148
|
}
|
|
51
149
|
|
|
52
|
-
// --- Utility: Show Result ---
|
|
53
150
|
function showResult(container, prompt, label) {
|
|
54
151
|
container.classList.remove("hidden");
|
|
152
|
+
const wordCount = prompt.split(/\s+/).length;
|
|
55
153
|
container.innerHTML = `
|
|
56
154
|
<div class="result-header">
|
|
57
|
-
<div class="result-title">${label || "
|
|
155
|
+
<div class="result-title">${label || t("resultGenerated")}</div>
|
|
58
156
|
<button class="btn-copy" onclick="copyPrompt(this)">
|
|
59
|
-
<svg width="
|
|
60
|
-
|
|
157
|
+
<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>
|
|
158
|
+
${t("resultCopy")}
|
|
61
159
|
</button>
|
|
62
160
|
</div>
|
|
63
161
|
<div class="result-body">
|
|
64
162
|
<pre>${escapeHtml(prompt)}</pre>
|
|
65
163
|
</div>
|
|
66
164
|
<div class="result-footer">
|
|
67
|
-
|
|
165
|
+
<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>
|
|
166
|
+
${t("resultPasteHint")} · ${wordCount} ${t("resultWords")}
|
|
68
167
|
</div>
|
|
69
168
|
`;
|
|
70
169
|
}
|
|
71
170
|
|
|
72
|
-
// --- Utility: Show Error ---
|
|
73
171
|
function showError(container, message) {
|
|
74
172
|
container.classList.remove("hidden");
|
|
75
173
|
container.innerHTML = `
|
|
@@ -77,13 +175,6 @@
|
|
|
77
175
|
`;
|
|
78
176
|
}
|
|
79
177
|
|
|
80
|
-
// --- Utility: Escape HTML ---
|
|
81
|
-
function escapeHtml(text) {
|
|
82
|
-
const div = document.createElement("div");
|
|
83
|
-
div.textContent = text;
|
|
84
|
-
return div.innerHTML;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
178
|
// --- Copy to Clipboard ---
|
|
88
179
|
window.copyPrompt = function (btn) {
|
|
89
180
|
const pre = btn.closest(".result-area").querySelector("pre");
|
|
@@ -91,14 +182,14 @@
|
|
|
91
182
|
navigator.clipboard.writeText(pre.textContent).then(() => {
|
|
92
183
|
btn.classList.add("copied");
|
|
93
184
|
btn.innerHTML = `
|
|
94
|
-
<svg width="
|
|
95
|
-
|
|
185
|
+
<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>
|
|
186
|
+
${t("resultCopied")}
|
|
96
187
|
`;
|
|
97
188
|
setTimeout(() => {
|
|
98
189
|
btn.classList.remove("copied");
|
|
99
190
|
btn.innerHTML = `
|
|
100
|
-
<svg width="
|
|
101
|
-
|
|
191
|
+
<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>
|
|
192
|
+
${t("resultCopy")}
|
|
102
193
|
`;
|
|
103
194
|
}, 2000);
|
|
104
195
|
});
|
|
@@ -110,7 +201,7 @@
|
|
|
110
201
|
async function handleChat(message) {
|
|
111
202
|
if (!message.trim()) return;
|
|
112
203
|
chatSubmit.disabled = true;
|
|
113
|
-
chatSubmit.
|
|
204
|
+
chatSubmit.innerHTML = `<div class="loading-spinner" style="width:16px;height:16px;border-width:2px"></div> ${t("chatGenerating")}`;
|
|
114
205
|
showLoading(chatResult);
|
|
115
206
|
|
|
116
207
|
try {
|
|
@@ -120,13 +211,13 @@
|
|
|
120
211
|
body: JSON.stringify({ message }),
|
|
121
212
|
});
|
|
122
213
|
const data = await res.json();
|
|
123
|
-
if (!res.ok) throw new Error(data.error || "
|
|
124
|
-
showResult(chatResult, data.prompt,
|
|
214
|
+
if (!res.ok) throw new Error(data.error || t("errorFailed"));
|
|
215
|
+
showResult(chatResult, data.prompt, t("resultPromptFor", { message }));
|
|
125
216
|
} catch (err) {
|
|
126
217
|
showError(chatResult, err.message);
|
|
127
218
|
} finally {
|
|
128
219
|
chatSubmit.disabled = false;
|
|
129
|
-
chatSubmit.
|
|
220
|
+
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> <span data-i18n="chatGenerate">${t("chatGenerate")}</span>`;
|
|
130
221
|
}
|
|
131
222
|
}
|
|
132
223
|
|
|
@@ -135,16 +226,23 @@
|
|
|
135
226
|
if (e.key === "Enter") handleChat(chatInput.value);
|
|
136
227
|
});
|
|
137
228
|
|
|
138
|
-
quickBtns.forEach((btn) => {
|
|
139
|
-
btn.addEventListener("click", () => {
|
|
140
|
-
chatInput.value = btn.dataset.prompt;
|
|
141
|
-
handleChat(btn.dataset.prompt);
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
229
|
// ============================================
|
|
146
230
|
// EXPLORE
|
|
147
231
|
// ============================================
|
|
232
|
+
function getTrendDescription(trend) {
|
|
233
|
+
if (currentLang === "ja" && window.TREND_DESC_JA && window.TREND_DESC_JA[trend.id]) {
|
|
234
|
+
return window.TREND_DESC_JA[trend.id];
|
|
235
|
+
}
|
|
236
|
+
return trend.description;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getCategoryLabel(category) {
|
|
240
|
+
if (currentLang === "ja" && window.CATEGORY_JA && window.CATEGORY_JA[category]) {
|
|
241
|
+
return window.CATEGORY_JA[category];
|
|
242
|
+
}
|
|
243
|
+
return category;
|
|
244
|
+
}
|
|
245
|
+
|
|
148
246
|
async function loadTrends() {
|
|
149
247
|
try {
|
|
150
248
|
const res = await fetch("/api/trends");
|
|
@@ -152,32 +250,64 @@
|
|
|
152
250
|
trends = data.trends;
|
|
153
251
|
renderTrends(trends);
|
|
154
252
|
populateMixSelects(trends);
|
|
253
|
+
// Apply i18n after trends loaded
|
|
254
|
+
applyI18n();
|
|
155
255
|
} catch (err) {
|
|
156
|
-
trendsGrid.innerHTML = `<div class="error-message"
|
|
256
|
+
trendsGrid.innerHTML = `<div class="error-message">${t("errorLoadTrends")}</div>`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function getFilteredTrends() {
|
|
261
|
+
let filtered = trends;
|
|
262
|
+
if (currentFilter !== "all") {
|
|
263
|
+
filtered = filtered.filter((t) => t.category === currentFilter);
|
|
264
|
+
}
|
|
265
|
+
if (searchQuery) {
|
|
266
|
+
const q = searchQuery.toLowerCase();
|
|
267
|
+
filtered = filtered.filter(
|
|
268
|
+
(t) =>
|
|
269
|
+
t.name.toLowerCase().includes(q) ||
|
|
270
|
+
t.description.toLowerCase().includes(q) ||
|
|
271
|
+
t.keywords.some((k) => k.toLowerCase().includes(q)) ||
|
|
272
|
+
(currentLang === "ja" && window.TREND_DESC_JA && window.TREND_DESC_JA[t.id] && window.TREND_DESC_JA[t.id].includes(q))
|
|
273
|
+
);
|
|
157
274
|
}
|
|
275
|
+
return filtered;
|
|
158
276
|
}
|
|
159
277
|
|
|
160
278
|
function renderTrends(list) {
|
|
279
|
+
trendsCountLabel.textContent = t("exploreShowing", { shown: list.length, total: trends.length });
|
|
280
|
+
|
|
281
|
+
if (list.length === 0) {
|
|
282
|
+
trendsGrid.innerHTML = `<div class="error-message" style="grid-column:1/-1;text-align:center">${t("exploreNoResults")}</div>`;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
161
286
|
trendsGrid.innerHTML = list
|
|
162
287
|
.map(
|
|
163
|
-
(
|
|
164
|
-
<div class="trend-card" data-id="${
|
|
288
|
+
(trend) => `
|
|
289
|
+
<div class="trend-card" data-id="${trend.id}">
|
|
165
290
|
<div class="trend-card-header">
|
|
166
|
-
<span class="trend-name">${escapeHtml(
|
|
167
|
-
<span
|
|
291
|
+
<span class="trend-name">${escapeHtml(trend.name)}</span>
|
|
292
|
+
<span>
|
|
293
|
+
<span class="trend-category">${getCategoryLabel(trend.category)}</span>
|
|
294
|
+
${trend.source === "ai-generated" ? '<span class="trend-source">AI</span>' : ""}
|
|
295
|
+
</span>
|
|
168
296
|
</div>
|
|
169
|
-
<div class="trend-desc">${escapeHtml(
|
|
170
|
-
<div class="trend-
|
|
171
|
-
<div class="popularity
|
|
172
|
-
<div class="popularity-
|
|
297
|
+
<div class="trend-desc">${escapeHtml(getTrendDescription(trend))}</div>
|
|
298
|
+
<div class="trend-meta">
|
|
299
|
+
<div class="trend-popularity">
|
|
300
|
+
<div class="popularity-bar">
|
|
301
|
+
<div class="popularity-fill" style="width: ${trend.popularity}%"></div>
|
|
302
|
+
</div>
|
|
303
|
+
<span class="popularity-value">${trend.popularity}</span>
|
|
173
304
|
</div>
|
|
174
|
-
<span class="popularity-value">${t.popularity}%</span>
|
|
175
305
|
</div>
|
|
176
306
|
<div class="trend-keywords">
|
|
177
|
-
${
|
|
307
|
+
${trend.keywords.slice(0, 4).map((k) => `<span class="keyword-tag">${escapeHtml(k)}</span>`).join("")}
|
|
178
308
|
</div>
|
|
179
309
|
<div class="trend-card-action">
|
|
180
|
-
<button class="btn-generate" onclick="generateFromTrend('${
|
|
310
|
+
<button class="btn-generate" onclick="generateFromTrend('${trend.id}')">${t("exploreGenerateBtn")}</button>
|
|
181
311
|
</div>
|
|
182
312
|
</div>
|
|
183
313
|
`
|
|
@@ -190,19 +320,20 @@
|
|
|
190
320
|
currentFilter = btn.dataset.category;
|
|
191
321
|
filterBtns.forEach((b) => b.classList.remove("active"));
|
|
192
322
|
btn.classList.add("active");
|
|
193
|
-
|
|
194
|
-
if (currentFilter === "all") {
|
|
195
|
-
renderTrends(trends);
|
|
196
|
-
} else {
|
|
197
|
-
renderTrends(trends.filter((t) => t.category === currentFilter));
|
|
198
|
-
}
|
|
323
|
+
renderTrends(getFilteredTrends());
|
|
199
324
|
exploreResult.classList.add("hidden");
|
|
200
325
|
});
|
|
201
326
|
});
|
|
202
327
|
|
|
328
|
+
if (exploreSearch) {
|
|
329
|
+
exploreSearch.addEventListener("input", (e) => {
|
|
330
|
+
searchQuery = e.target.value;
|
|
331
|
+
renderTrends(getFilteredTrends());
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
203
335
|
window.generateFromTrend = async function (trendId) {
|
|
204
336
|
showLoading(exploreResult);
|
|
205
|
-
// Scroll to result
|
|
206
337
|
exploreResult.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
207
338
|
|
|
208
339
|
try {
|
|
@@ -212,8 +343,8 @@
|
|
|
212
343
|
body: JSON.stringify({ trendId }),
|
|
213
344
|
});
|
|
214
345
|
const data = await res.json();
|
|
215
|
-
if (!res.ok) throw new Error(data.error || "
|
|
216
|
-
showResult(exploreResult, data.prompt,
|
|
346
|
+
if (!res.ok) throw new Error(data.error || t("errorFailed"));
|
|
347
|
+
showResult(exploreResult, data.prompt, t("resultTrend", { name: data.trend.name }));
|
|
217
348
|
} catch (err) {
|
|
218
349
|
showError(exploreResult, err.message);
|
|
219
350
|
}
|
|
@@ -223,12 +354,24 @@
|
|
|
223
354
|
// MIX
|
|
224
355
|
// ============================================
|
|
225
356
|
function populateMixSelects(list) {
|
|
226
|
-
const
|
|
227
|
-
|
|
357
|
+
const grouped = {};
|
|
358
|
+
list.forEach((t) => {
|
|
359
|
+
if (!grouped[t.category]) grouped[t.category] = [];
|
|
360
|
+
grouped[t.category].push(t);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const optionsHtml = Object.entries(grouped)
|
|
364
|
+
.map(
|
|
365
|
+
([cat, items]) =>
|
|
366
|
+
`<optgroup label="${getCategoryLabel(cat)}">` +
|
|
367
|
+
items.map((t) => `<option value="${t.id}">${t.name}</option>`).join("") +
|
|
368
|
+
`</optgroup>`
|
|
369
|
+
)
|
|
228
370
|
.join("");
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
371
|
+
|
|
372
|
+
const placeholder = `<option value="">${t("mixSelectPlaceholder")}</option>`;
|
|
373
|
+
mixSelect1.innerHTML = placeholder + optionsHtml;
|
|
374
|
+
mixSelect2.innerHTML = placeholder + optionsHtml;
|
|
232
375
|
}
|
|
233
376
|
|
|
234
377
|
async function handleMix() {
|
|
@@ -237,41 +380,104 @@
|
|
|
237
380
|
const ctx = mixContext.value;
|
|
238
381
|
|
|
239
382
|
if (!t1 || !t2) {
|
|
240
|
-
showError(mixResult, "
|
|
383
|
+
showError(mixResult, t("mixErrorSelect"));
|
|
241
384
|
return;
|
|
242
385
|
}
|
|
243
386
|
if (t1 === t2) {
|
|
244
|
-
showError(mixResult, "
|
|
387
|
+
showError(mixResult, t("mixErrorSame"));
|
|
245
388
|
return;
|
|
246
389
|
}
|
|
247
390
|
|
|
248
391
|
mixSubmit.disabled = true;
|
|
249
|
-
mixSubmit.
|
|
392
|
+
mixSubmit.innerHTML = `<div class="loading-spinner" style="width:16px;height:16px;border-width:2px"></div> ${t("mixMixing")}`;
|
|
250
393
|
showLoading(mixResult);
|
|
251
394
|
|
|
252
395
|
try {
|
|
253
396
|
const res = await fetch("/api/mix", {
|
|
254
397
|
method: "POST",
|
|
255
398
|
headers: { "Content-Type": "application/json" },
|
|
256
|
-
body: JSON.stringify({ trend1Id: t1, trend2Id: t2, context: ctx
|
|
399
|
+
body: JSON.stringify({ trend1Id: t1, trend2Id: t2, context: ctx }),
|
|
257
400
|
});
|
|
258
401
|
const data = await res.json();
|
|
259
|
-
if (!res.ok) throw new Error(data.error || "
|
|
402
|
+
if (!res.ok) throw new Error(data.error || t("errorFailed"));
|
|
260
403
|
showResult(
|
|
261
404
|
mixResult,
|
|
262
405
|
data.prompt,
|
|
263
|
-
|
|
406
|
+
t("resultMix", { name1: data.trend1.name, name2: data.trend2.name })
|
|
264
407
|
);
|
|
265
408
|
} catch (err) {
|
|
266
409
|
showError(mixResult, err.message);
|
|
267
410
|
} finally {
|
|
268
411
|
mixSubmit.disabled = false;
|
|
269
|
-
mixSubmit.
|
|
412
|
+
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> <span data-i18n="mixGenerate">${t("mixGenerate")}</span>`;
|
|
270
413
|
}
|
|
271
414
|
}
|
|
272
415
|
|
|
273
416
|
mixSubmit.addEventListener("click", handleMix);
|
|
274
417
|
|
|
418
|
+
// ============================================
|
|
419
|
+
// STATS
|
|
420
|
+
// ============================================
|
|
421
|
+
async function loadStats() {
|
|
422
|
+
try {
|
|
423
|
+
const res = await fetch("/api/stats");
|
|
424
|
+
const data = await res.json();
|
|
425
|
+
renderStats(data);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
statsContainer.innerHTML = `<div class="error-message">${t("errorLoadStats")}</div>`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function renderStats(data) {
|
|
432
|
+
const maxCat = Math.max(...Object.values(data.categories));
|
|
433
|
+
|
|
434
|
+
const categoryBars = Object.entries(data.categories)
|
|
435
|
+
.sort((a, b) => b[1] - a[1])
|
|
436
|
+
.map(
|
|
437
|
+
([cat, count]) => `
|
|
438
|
+
<div class="stat-bar-row">
|
|
439
|
+
<span class="stat-bar-label">${getCategoryLabel(cat)}</span>
|
|
440
|
+
<div class="stat-bar-track">
|
|
441
|
+
<div class="stat-bar-fill" style="width: ${(count / maxCat) * 100}%"></div>
|
|
442
|
+
</div>
|
|
443
|
+
<span class="stat-bar-count">${count}</span>
|
|
444
|
+
</div>
|
|
445
|
+
`
|
|
446
|
+
)
|
|
447
|
+
.join("");
|
|
448
|
+
|
|
449
|
+
statsContainer.innerHTML = `
|
|
450
|
+
<div class="stat-card accent">
|
|
451
|
+
<div class="stat-value">${data.total}</div>
|
|
452
|
+
<div class="stat-label">${t("statsTotalTrends")}</div>
|
|
453
|
+
</div>
|
|
454
|
+
<div class="stat-card">
|
|
455
|
+
<div class="stat-value">${data.builtin}</div>
|
|
456
|
+
<div class="stat-label">${t("statsBuiltin")}</div>
|
|
457
|
+
</div>
|
|
458
|
+
<div class="stat-card">
|
|
459
|
+
<div class="stat-value">${data.custom}</div>
|
|
460
|
+
<div class="stat-label">${t("statsAiGenerated")}</div>
|
|
461
|
+
</div>
|
|
462
|
+
<div class="stat-card">
|
|
463
|
+
<div class="stat-value">${Object.keys(data.categories).length}</div>
|
|
464
|
+
<div class="stat-label">${t("statsCategories")}</div>
|
|
465
|
+
</div>
|
|
466
|
+
<div class="stat-bar-container">
|
|
467
|
+
<div class="stat-bar-title">${t("statsByCategory")}</div>
|
|
468
|
+
${categoryBars}
|
|
469
|
+
</div>
|
|
470
|
+
`;
|
|
471
|
+
}
|
|
472
|
+
|
|
275
473
|
// --- Init ---
|
|
276
474
|
loadTrends();
|
|
475
|
+
|
|
476
|
+
// Keyboard shortcut: / to focus search
|
|
477
|
+
document.addEventListener("keydown", (e) => {
|
|
478
|
+
if (e.key === "/" && document.activeElement.tagName !== "INPUT" && document.activeElement.tagName !== "TEXTAREA") {
|
|
479
|
+
e.preventDefault();
|
|
480
|
+
chatInput.focus();
|
|
481
|
+
}
|
|
482
|
+
});
|
|
277
483
|
})();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// DESIGNNN i18n — Trend Descriptions (Japanese)
|
|
3
|
+
// ============================================
|
|
4
|
+
|
|
5
|
+
const TREND_DESC_JA = {
|
|
6
|
+
// === STYLES ===
|
|
7
|
+
"bento-ui": "日本の弁当箱にインスパイアされたグリッドレイアウト。異なるサイズのカードを非対称に配置し、視覚的な階層と情報密度を実現する。",
|
|
8
|
+
"glassmorphism": "すりガラス効果を用いたデザイン。背景ぼかし、透明度、繊細なボーダーでUIに奥行きとレイヤー感を生み出す。",
|
|
9
|
+
"neubrutalism": "太いボーダー、強いシャドウ、ハイコントラストな配色が特徴の大胆で荒削りな美学。ミニマリズムへのアンチテーゼ。",
|
|
10
|
+
"organic-shapes": "幾何学的な直線を避け、自然界の曲線やブロブ形状を取り入れたデザイン。親しみやすさと柔らかさを演出する。",
|
|
11
|
+
"dark-mode-first": "ダークモードを前提に設計されたUI。目の疲れを軽減し、有機ELディスプレイの省電力にも貢献する。",
|
|
12
|
+
"kinetic-typography": "動きのあるタイポグラフィ。スクロールやインタラクションに連動して文字が変形・移動し、ダイナミックな表現を実現する。",
|
|
13
|
+
"aurora-gradients": "オーロラのような多色グラデーション。メッシュグラデーションで幻想的な背景やアクセントを生み出す。",
|
|
14
|
+
"claymorphism": "3Dの粘土のような質感。ぷっくりとした丸みのある要素に、内側のシャドウと柔らかい色彩で立体感を表現する。",
|
|
15
|
+
"retro-pixel": "8ビット・16ビット時代のピクセルアートを現代UIに融合。ノスタルジックな魅力とモダンな機能性を両立する。",
|
|
16
|
+
"minimalist-mono": "モノクロ配色と極限まで削ぎ落としたミニマルデザイン。タイポグラフィと余白で洗練された印象を与える。",
|
|
17
|
+
"grain-texture": "フィルムグレインやノイズテクスチャを加えた温かみのあるデザイン。デジタルの冷たさを和らげ、手触り感を演出する。",
|
|
18
|
+
"neumorphism-v2": "ニューモーフィズムの進化版。ソフトな凹凸表現にアクセシビリティを改善し、実用的なUIコンポーネントとして昇華。",
|
|
19
|
+
"ai-native-aesthetic": "AI時代のビジュアル言語。グラデーションのオーブ、パーティクル、データフローの視覚化でAIの存在を表現する。",
|
|
20
|
+
"japandi-design": "日本の侘び寂びと北欧デザインの融合。自然素材の質感、落ち着いたアースカラー、機能美を重視する。",
|
|
21
|
+
"maximalism": "「多いほど良い」の美学。大胆な色使い、パターンの重ね合わせ、装飾的な要素で視覚的なインパクトを最大化する。",
|
|
22
|
+
"3d-immersive": "WebGLによる3Dオブジェクト、スクロール連動の3Dアニメーション、ARプレビュー。静的な画像を超えた奥行きとインタラクション。",
|
|
23
|
+
"y2k-vibrant-palette": "Y2Kノスタルジアにインスパイアされた鮮やかな配色。ネオングラデーション、ハイコントラスト、ドーパミンデザイン。",
|
|
24
|
+
"retrofuturism": "レトロが描いた未来像。ネオンアクセント、クロームテクスチャ、ピクセルアート、SF・アーケードを彷彿とさせる大胆なグラデーション。",
|
|
25
|
+
"collage-design": "スクラップブック風のクリエイティブ表現。ステッカー、破れたテクスチャ、切り抜き写真、手描き要素を意図的に散りばめる。",
|
|
26
|
+
"sensory-maximalism": "五感を刺激するデザイン。リッチなテクスチャ、大胆な色彩、ダイナミックなモーション、没入感のある高エネルギーな構成。",
|
|
27
|
+
"variable-fonts": "バリアブルフォントを活用した動的タイポグラフィ。インタラクションやスクロールに応じてウェイトや幅が流動的に変化する。",
|
|
28
|
+
|
|
29
|
+
// === COMPONENTS ===
|
|
30
|
+
"floating-action-bar": "画面下部に浮遊するアクションバー。コンテキストに応じたツールやアクションにすばやくアクセスできる。",
|
|
31
|
+
"skeleton-loading": "コンテンツ読み込み中にレイアウトの骨格を表示するプレースホルダー。体感的な待ち時間を短縮する。",
|
|
32
|
+
"command-palette": "Cmd+Kで呼び出すコマンドパレット。検索、ナビゲーション、アクション実行を一箇所に集約するパワーユーザー向け機能。",
|
|
33
|
+
"toast-notification": "画面端に一時的に表示される通知メッセージ。操作の成功・エラー・情報をユーザーの作業を中断せずに伝える。",
|
|
34
|
+
"avatar-stack": "複数のアバターを重ねて表示するコンポーネント。チームメンバーやコラボレーターの存在を視覚的に示す。",
|
|
35
|
+
"data-table": "ソート、フィルター、ページネーション、インライン編集を備えた高機能データテーブル。大量データの管理に不可欠。",
|
|
36
|
+
"empty-state": "データがない状態を魅力的に表示するデザイン。イラストとCTAでユーザーを次のアクションに導く。",
|
|
37
|
+
"ai-chat-interface": "AIチャットボットのインターフェース。メッセージバブル、タイピングインジケーター、リッチレスポンス、コンテキスト表示を含む。",
|
|
38
|
+
"stepper-progress": "マルチステップフォームの進捗表示。番号付きステップ、進行バー、バリデーション状態でユーザーを案内する。",
|
|
39
|
+
"contextual-menu": "右クリックやロングプレスで表示されるコンテキストメニュー。アイコン、キーボードショートカット、ネストされたサブメニューを含む。",
|
|
40
|
+
"voice-ui": "音声操作のインターフェース要素。波形ビジュアライザー、音声コマンドインジケーター、会話型音声アシスタント。",
|
|
41
|
+
|
|
42
|
+
// === PATTERNS ===
|
|
43
|
+
"saas-pricing": "SaaS料金ページのデザインパターン。プランの比較表、推奨プランのハイライト、FAQ、年額/月額の切り替えを含む。",
|
|
44
|
+
"onboarding-flow": "新規ユーザー向けのオンボーディングフロー。プログレスバー、スキップ可能なステップ、パーソナライズ設定を含む。",
|
|
45
|
+
"dashboard-analytics": "KPIカード、チャート、リアルタイムデータを備えた分析ダッシュボード。日付フィルターとエクスポート機能付き。",
|
|
46
|
+
"auth-minimal": "ミニマルな認証画面。ソーシャルログイン、パスワードレス認証、2段階認証を含むモダンなログイン/登録フォーム。",
|
|
47
|
+
"settings-page": "設定画面のデザインパターン。セクション分け、トグルスイッチ、フォームバリデーション、変更の自動保存を含む。",
|
|
48
|
+
"ecommerce-pdp": "ECサイトの商品詳細ページ。画像ギャラリー、バリエーション選択、レビュー、カート追加ボタンを含む。コンバージョン最適化済み。",
|
|
49
|
+
"notification-center": "カテゴリ分けされた通知パネル。既読/未読の状態管理、アクション可能な通知項目を含む。",
|
|
50
|
+
"kanban-board": "カラムベースのタスク管理ボード。ドラッグ&ドロップ対応のカード、ラベル、担当者、期日、スイムレーンを含む。",
|
|
51
|
+
"file-upload": "ドラッグ&ドロップ対応のファイルアップロードエリア。進捗インジケーター、ファイル形式の検証、アップロードキューを含む。",
|
|
52
|
+
"scrollytelling": "スクロールベースのナラティブ体験。ユーザーのスクロールに合わせてコンテンツが展開し、モーション・テキスト・ビジュアルを物語として融合する。",
|
|
53
|
+
"gamified-design": "ゲームメカニクスをUIに応用。ポイント、レベル、バッジ、プログレスバー、リーダーボード、マイクロリワードでエンゲージメントを向上。",
|
|
54
|
+
"sustainable-web": "エコ意識のデザイン。最適化されたアセット、データ転送量の削減、省エネのダークテーマ、アクセシビリティファーストのアプローチ。",
|
|
55
|
+
"agentic-ai-ui": "自律型AIエージェントのUI。タスク委任、進捗モニタリング、承認ワークフロー、マルチステップのエージェントパイプラインを含む。",
|
|
56
|
+
"tactile-ui": "物理的な反応を感じさせるUI要素。バウンスアニメーション、弾性変形、圧力感知のインタラクションを含む。",
|
|
57
|
+
"ai-design-system": "AIが動的にコンポーネントを適応・生成するデザインシステム。自己進化するトークン、自動生成バリアント、スマートテーマ。",
|
|
58
|
+
|
|
59
|
+
// === LAYOUTS ===
|
|
60
|
+
"landing-page-saas": "SaaSランディングページの完全構成。ヒーロー、機能紹介、ソーシャルプルーフ、料金、CTAセクションを含む。コンバージョン最適化済み。",
|
|
61
|
+
"hero-split": "2カラムに分割されたヒーローセクション。片側にテキストとCTA、もう片側にビジュアル(画像、イラスト、3D)を配置する。",
|
|
62
|
+
"sidebar-nav": "アイコンのみモードに折りたためるサイドバーナビゲーション。SaaSやダッシュボードアプリケーションの標準的なパターン。",
|
|
63
|
+
"mobile-bottom-sheet": "モバイルインターフェース用のドラッグ可能なボトムパネル。従来のモーダルを、より自然で親指操作しやすいインタラクションに置き換える。",
|
|
64
|
+
"responsive-cards": "ビューポートに応じてマルチカラムからシングルカラムに自動リフローするカードレイアウト。モダンなコンテンツレイアウトの基盤。",
|
|
65
|
+
"masonry-layout": "高さの異なるカードを効率的に配置するレンガ壁のようなレイアウト。画像中心のコンテンツやポートフォリオに最適。",
|
|
66
|
+
"sticky-header": "スクロール時に固定されるナビゲーションヘッダー。スクロールに応じて縮小、ぼかし追加、背景色変更などのビジュアル遷移を含む。",
|
|
67
|
+
"full-bleed-sections": "ライトとダークの背景を交互に配置するフルワイドコンテンツセクション。視覚的なリズムと明確なコンテンツ区分を生み出す。",
|
|
68
|
+
"z-pattern-layout": "画像左/テキスト右と、テキスト左/画像右を交互に配置するレイアウト。ページを下に向かってZ字パターンで視線を誘導する。",
|
|
69
|
+
"experimental-navigation": "非線形のナビゲーションパターン。ラジアルメニュー、隠しドロワー、インタラクティブマップ、探索型のジャーニーを含む。",
|
|
70
|
+
"spatial-design": "空間コンピューティング向けデザイン。フローティングウィンドウ、深度レイヤー、ガラスマテリアル、Apple Vision Pro向けの視線追跡インタラクション。",
|
|
71
|
+
|
|
72
|
+
// === INTERACTIONS ===
|
|
73
|
+
"micro-interactions": "フィードバック、ガイド、楽しさを提供する小さく目的のあるアニメーション。ボタンホバー、トグルスイッチ、成功状態など。",
|
|
74
|
+
"scroll-animations": "スクロールに応じて要素がアニメーションで表示される。フェードイン、スライドアップ、パララックス効果で魅力的なスクロール体験を実現。",
|
|
75
|
+
"gesture-navigation": "スワイプ、ピンチ、ドラッグによるモバイルナビゲーション。ボタンタップを自然なタッチ操作に置き換える。",
|
|
76
|
+
"haptic-feedback-design": "触覚フィードバックの視覚的表現。ボタン押下、トグル、確認操作などの物理的な反応を示唆するデザインキュー。",
|
|
77
|
+
"dark-light-toggle": "ダークモードとライトモードのスムーズな切り替え。アニメーション付きトグルスイッチとシームレスな配色遷移を含む。",
|
|
78
|
+
"infinite-scroll": "スクロールに応じてコンテンツを連続的に読み込む。ローディングインジケーター、スケルトン表示、コンテンツ終端のメッセージを含む。",
|
|
79
|
+
"drag-reorder": "ドラッグで並び替え可能なリストアイテム。グラブハンドル、ドロップインジケーター、スムーズな再配置を含む。",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Category name translations
|
|
83
|
+
const CATEGORY_JA = {
|
|
84
|
+
"style": "スタイル",
|
|
85
|
+
"component": "コンポーネント",
|
|
86
|
+
"pattern": "パターン",
|
|
87
|
+
"layout": "レイアウト",
|
|
88
|
+
"interaction": "インタラクション",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
window.TREND_DESC_JA = TREND_DESC_JA;
|
|
92
|
+
window.CATEGORY_JA = CATEGORY_JA;
|