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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "designnn",
3
- "version": "0.2.0",
3
+ "version": "0.4.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,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 quickBtns = document.querySelectorAll(".quick-btn");
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
- // --- Utility: Show Loading ---
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-dots">
45
- <span></span><span></span><span></span>
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 || "Generated Prompt"}</div>
155
+ <div class="result-title">${label || t("resultGenerated")}</div>
58
156
  <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>
60
- Copy
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
- Paste this prompt into Figma AI (Ctrl+I / Cmd+I)
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")} &middot; ${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="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6L9 17l-5-5"/></svg>
95
- Copied!
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="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>
101
- Copy
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.textContent = "Generating...";
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 || "Failed to generate");
124
- showResult(chatResult, data.prompt, `Prompt for: "${message}"`);
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.textContent = "Generate";
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">Failed to load trends</div>`;
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
- (t) => `
164
- <div class="trend-card" data-id="${t.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(t.name)}</span>
167
- <span class="trend-category">${t.category}</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(t.description)}</div>
170
- <div class="trend-popularity">
171
- <div class="popularity-bar">
172
- <div class="popularity-fill" style="width: ${t.popularity}%"></div>
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
- ${t.keywords.map((k) => `<span class="keyword-tag">${escapeHtml(k)}</span>`).join("")}
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('${t.id}')">Generate Prompt</button>
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 || "Failed to generate");
216
- showResult(exploreResult, data.prompt, `Trend: ${data.trend.name}`);
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 options = list
227
- .map((t) => `<option value="${t.id}">${t.name} [${t.category}]</option>`)
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
- const placeholder = `<option value="">Select a trend...</option>`;
230
- mixSelect1.innerHTML = placeholder + options;
231
- mixSelect2.innerHTML = placeholder + options;
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, "Please select two trends to mix.");
383
+ showError(mixResult, t("mixErrorSelect"));
241
384
  return;
242
385
  }
243
386
  if (t1 === t2) {
244
- showError(mixResult, "Please select two different trends.");
387
+ showError(mixResult, t("mixErrorSame"));
245
388
  return;
246
389
  }
247
390
 
248
391
  mixSubmit.disabled = true;
249
- mixSubmit.textContent = "Mixing...";
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 || undefined }),
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 || "Failed to mix");
402
+ if (!res.ok) throw new Error(data.error || t("errorFailed"));
260
403
  showResult(
261
404
  mixResult,
262
405
  data.prompt,
263
- `Mix: ${data.trend1.name} × ${data.trend2.name}`
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.textContent = "Mix & Generate";
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;