← back to Wallco Ai
wallco.ai: motif filter chips on /designs — 12 top motifs (peony/damask/lattice/scrollwork/etc.) clickable, combine with category + search · snapshot now carries motifs[] from PG · 47 designs tagged via llava
6881a12500ee09011c16a9ec92da2f010c1485e5 · 2026-05-11 19:26:30 -0700 · Steve
Files touched
M data/designs.jsonM scripts/refresh_designs_snapshot.pyM server.js
Diff
commit 6881a12500ee09011c16a9ec92da2f010c1485e5
Author: Steve <steve@designerwallcoverings.com>
Date: Mon May 11 19:26:30 2026 -0700
wallco.ai: motif filter chips on /designs — 12 top motifs (peony/damask/lattice/scrollwork/etc.) clickable, combine with category + search · snapshot now carries motifs[] from PG · 47 designs tagged via llava
---
data/designs.json | 483 ++++++++++++++++++++++++++++++++----
scripts/refresh_designs_snapshot.py | 5 +-
server.js | 239 ++++++++++++++++--
3 files changed, 655 insertions(+), 72 deletions(-)
diff --git a/data/designs.json b/data/designs.json
index fea2d16..bda9597 100644
--- a/data/designs.json
+++ b/data/designs.json
@@ -12,7 +12,14 @@
"generator": "wallco.ai",
"seed": 893789819,
"is_published": false,
- "created_at": "2026-05-11T23:10:27.199721+00:00"
+ "created_at": "2026-05-11T23:10:27.199721+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 12,
@@ -27,7 +34,14 @@
"generator": "wallco.ai",
"seed": 1771215304,
"is_published": false,
- "created_at": "2026-05-11T23:10:58.996561+00:00"
+ "created_at": "2026-05-11T23:10:58.996561+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 13,
@@ -42,7 +56,14 @@
"generator": "wallco.ai",
"seed": 209403190,
"is_published": false,
- "created_at": "2026-05-11T23:11:06.301179+00:00"
+ "created_at": "2026-05-11T23:11:06.301179+00:00",
+ "motifs": [
+ "floral",
+ "botanical",
+ "tropical",
+ "abstract",
+ "organic"
+ ]
},
{
"id": 14,
@@ -57,7 +78,14 @@
"generator": "wallco.ai",
"seed": 1186496331,
"is_published": false,
- "created_at": "2026-05-11T23:11:13.611884+00:00"
+ "created_at": "2026-05-11T23:11:13.611884+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "botanical",
+ "tropical",
+ "abstract"
+ ]
},
{
"id": 15,
@@ -72,7 +100,8 @@
"generator": "wallco.ai",
"seed": 864976504,
"is_published": false,
- "created_at": "2026-05-11T23:11:21.243547+00:00"
+ "created_at": "2026-05-11T23:11:21.243547+00:00",
+ "motifs": []
},
{
"id": 16,
@@ -87,7 +116,14 @@
"generator": "wallco.ai",
"seed": 918473109,
"is_published": false,
- "created_at": "2026-05-11T23:11:58.987722+00:00"
+ "created_at": "2026-05-11T23:11:58.987722+00:00",
+ "motifs": [
+ "abstract",
+ "organic",
+ "geometric",
+ "damask",
+ "art-deco"
+ ]
},
{
"id": 17,
@@ -102,7 +138,14 @@
"generator": "wallco.ai",
"seed": 368707680,
"is_published": false,
- "created_at": "2026-05-11T23:12:06.628664+00:00"
+ "created_at": "2026-05-11T23:12:06.628664+00:00",
+ "motifs": [
+ "abstract",
+ "geometric",
+ "modern",
+ "minimal",
+ "sparse"
+ ]
},
{
"id": 18,
@@ -117,7 +160,14 @@
"generator": "wallco.ai",
"seed": 1206353655,
"is_published": false,
- "created_at": "2026-05-11T23:12:14.021917+00:00"
+ "created_at": "2026-05-11T23:12:14.021917+00:00",
+ "motifs": [
+ "floral",
+ "botanical",
+ "abstract",
+ "organic",
+ "geometric"
+ ]
},
{
"id": 19,
@@ -132,7 +182,14 @@
"generator": "wallco.ai",
"seed": 1800259551,
"is_published": false,
- "created_at": "2026-05-11T23:12:59.057562+00:00"
+ "created_at": "2026-05-11T23:12:59.057562+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 20,
@@ -147,7 +204,14 @@
"generator": "wallco.ai",
"seed": 1139384775,
"is_published": false,
- "created_at": "2026-05-11T23:13:06.407821+00:00"
+ "created_at": "2026-05-11T23:13:06.407821+00:00",
+ "motifs": [
+ "baroque",
+ "damask",
+ "scrollwork",
+ "abstract",
+ "organic"
+ ]
},
{
"id": 21,
@@ -162,7 +226,14 @@
"generator": "wallco.ai",
"seed": 1284647138,
"is_published": false,
- "created_at": "2026-05-11T23:13:13.762466+00:00"
+ "created_at": "2026-05-11T23:13:13.762466+00:00",
+ "motifs": [
+ "art-deco",
+ "art-nouveau",
+ "baroque",
+ "floral",
+ "scrollwork"
+ ]
},
{
"id": 22,
@@ -177,7 +248,14 @@
"generator": "wallco.ai",
"seed": 1552462120,
"is_published": false,
- "created_at": "2026-05-11T23:45:45.997753+00:00"
+ "created_at": "2026-05-11T23:45:45.997753+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 23,
@@ -192,7 +270,14 @@
"generator": "wallco.ai",
"seed": 1550235083,
"is_published": false,
- "created_at": "2026-05-11T23:45:53.316274+00:00"
+ "created_at": "2026-05-11T23:45:53.316274+00:00",
+ "motifs": [
+ "floral",
+ "botanical",
+ "leaf",
+ "vine",
+ "abstract"
+ ]
},
{
"id": 24,
@@ -207,7 +292,14 @@
"generator": "wallco.ai",
"seed": 465538454,
"is_published": false,
- "created_at": "2026-05-11T23:46:04.278548+00:00"
+ "created_at": "2026-05-11T23:46:04.278548+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 25,
@@ -222,7 +314,14 @@
"generator": "wallco.ai",
"seed": 1496299154,
"is_published": false,
- "created_at": "2026-05-11T23:46:28.626696+00:00"
+ "created_at": "2026-05-11T23:46:28.626696+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 26,
@@ -237,7 +336,15 @@
"generator": "wallco.ai",
"seed": 2139138353,
"is_published": false,
- "created_at": "2026-05-11T23:46:39.0279+00:00"
+ "created_at": "2026-05-11T23:46:39.0279+00:00",
+ "motifs": [
+ "art-deco",
+ "art-nouveau",
+ "baroque",
+ "victorian",
+ "abstract",
+ "organic"
+ ]
},
{
"id": 27,
@@ -252,7 +359,14 @@
"generator": "wallco.ai",
"seed": 1938744951,
"is_published": false,
- "created_at": "2026-05-11T23:46:52.649025+00:00"
+ "created_at": "2026-05-11T23:46:52.649025+00:00",
+ "motifs": [
+ "geometric",
+ "stripe",
+ "dot",
+ "diamond",
+ "hexagon"
+ ]
},
{
"id": 28,
@@ -267,7 +381,8 @@
"generator": "wallco.ai",
"seed": 993138679,
"is_published": false,
- "created_at": "2026-05-11T23:46:59.864585+00:00"
+ "created_at": "2026-05-11T23:46:59.864585+00:00",
+ "motifs": []
},
{
"id": 29,
@@ -282,7 +397,14 @@
"generator": "wallco.ai",
"seed": 428383571,
"is_published": false,
- "created_at": "2026-05-11T23:47:07.394284+00:00"
+ "created_at": "2026-05-11T23:47:07.394284+00:00",
+ "motifs": [
+ "geometric",
+ "lattice",
+ "trellis",
+ "damask",
+ "art-nouveau"
+ ]
},
{
"id": 30,
@@ -297,7 +419,14 @@
"generator": "wallco.ai",
"seed": 1802417126,
"is_published": false,
- "created_at": "2026-05-11T23:47:14.919596+00:00"
+ "created_at": "2026-05-11T23:47:14.919596+00:00",
+ "motifs": [
+ "geometric",
+ "abstract",
+ "art-deco",
+ "modern",
+ "minimal"
+ ]
},
{
"id": 31,
@@ -312,7 +441,14 @@
"generator": "wallco.ai",
"seed": 386152567,
"is_published": false,
- "created_at": "2026-05-11T23:47:22.285707+00:00"
+ "created_at": "2026-05-11T23:47:22.285707+00:00",
+ "motifs": [
+ "floral",
+ "damask",
+ "scrollwork",
+ "abstract",
+ "organic"
+ ]
},
{
"id": 32,
@@ -327,7 +463,15 @@
"generator": "wallco.ai",
"seed": 1871092499,
"is_published": false,
- "created_at": "2026-05-11T23:47:29.881134+00:00"
+ "created_at": "2026-05-11T23:47:29.881134+00:00",
+ "motifs": [
+ "damask",
+ "scrollwork",
+ "baroque",
+ "victorian",
+ "abstract",
+ "organic"
+ ]
},
{
"id": 33,
@@ -342,7 +486,14 @@
"generator": "wallco.ai",
"seed": 57994025,
"is_published": false,
- "created_at": "2026-05-11T23:47:40.331546+00:00"
+ "created_at": "2026-05-11T23:47:40.331546+00:00",
+ "motifs": [
+ "abstract",
+ "organic",
+ "geometric",
+ "baroque",
+ "victorian"
+ ]
},
{
"id": 34,
@@ -357,7 +508,14 @@
"generator": "wallco.ai",
"seed": 151035740,
"is_published": false,
- "created_at": "2026-05-11T23:47:47.588998+00:00"
+ "created_at": "2026-05-11T23:47:47.588998+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 35,
@@ -372,7 +530,14 @@
"generator": "wallco.ai",
"seed": 388669124,
"is_published": false,
- "created_at": "2026-05-11T23:47:54.907538+00:00"
+ "created_at": "2026-05-11T23:47:54.907538+00:00",
+ "motifs": [
+ "damask",
+ "scrollwork",
+ "trellis",
+ "lattice",
+ "abstract"
+ ]
},
{
"id": 36,
@@ -387,7 +552,14 @@
"generator": "wallco.ai",
"seed": 268939687,
"is_published": false,
- "created_at": "2026-05-11T23:48:12.213123+00:00"
+ "created_at": "2026-05-11T23:48:12.213123+00:00",
+ "motifs": [
+ "botanical",
+ "trellis",
+ "leaf",
+ "vine",
+ "organic"
+ ]
},
{
"id": 37,
@@ -402,7 +574,8 @@
"generator": "wallco.ai",
"seed": 879781667,
"is_published": false,
- "created_at": "2026-05-11T23:48:23.023987+00:00"
+ "created_at": "2026-05-11T23:48:23.023987+00:00",
+ "motifs": []
},
{
"id": 38,
@@ -417,7 +590,14 @@
"generator": "wallco.ai",
"seed": 965292828,
"is_published": false,
- "created_at": "2026-05-11T23:48:30.32711+00:00"
+ "created_at": "2026-05-11T23:48:30.32711+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "leaf",
+ "botanical",
+ "tropical"
+ ]
},
{
"id": 39,
@@ -432,7 +612,14 @@
"generator": "wallco.ai",
"seed": 821956426,
"is_published": false,
- "created_at": "2026-05-11T23:48:40.995703+00:00"
+ "created_at": "2026-05-11T23:48:40.995703+00:00",
+ "motifs": [
+ "botanical",
+ "trellis",
+ "vine",
+ "leaf",
+ "organic"
+ ]
},
{
"id": 40,
@@ -447,7 +634,15 @@
"generator": "wallco.ai",
"seed": 1617564843,
"is_published": false,
- "created_at": "2026-05-11T23:48:57.994123+00:00"
+ "created_at": "2026-05-11T23:48:57.994123+00:00",
+ "motifs": [
+ "paisley",
+ "abstract",
+ "damask",
+ "art-nouveau",
+ "floral",
+ "geometric"
+ ]
},
{
"id": 41,
@@ -462,7 +657,14 @@
"generator": "wallco.ai",
"seed": 536522826,
"is_published": false,
- "created_at": "2026-05-11T23:56:17.861881+00:00"
+ "created_at": "2026-05-11T23:56:17.861881+00:00",
+ "motifs": [
+ "abstract",
+ "organic",
+ "floral",
+ "damask",
+ "paisley"
+ ]
},
{
"id": 42,
@@ -477,7 +679,14 @@
"generator": "wallco.ai",
"seed": 218100061,
"is_published": false,
- "created_at": "2026-05-12T00:39:53.816852+00:00"
+ "created_at": "2026-05-12T00:39:53.816852+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 43,
@@ -492,7 +701,14 @@
"generator": "wallco.ai",
"seed": 1288400340,
"is_published": false,
- "created_at": "2026-05-12T00:40:51.578874+00:00"
+ "created_at": "2026-05-12T00:40:51.578874+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 44,
@@ -507,7 +723,8 @@
"generator": "wallco.ai",
"seed": 1698245296,
"is_published": false,
- "created_at": "2026-05-12T01:01:45.757758+00:00"
+ "created_at": "2026-05-12T01:01:45.757758+00:00",
+ "motifs": []
},
{
"id": 45,
@@ -522,7 +739,8 @@
"generator": "wallco.ai",
"seed": 224020684,
"is_published": false,
- "created_at": "2026-05-12T01:13:12.328157+00:00"
+ "created_at": "2026-05-12T01:13:12.328157+00:00",
+ "motifs": []
},
{
"id": 46,
@@ -537,7 +755,14 @@
"generator": "wallco.ai",
"seed": 1309443231,
"is_published": false,
- "created_at": "2026-05-12T01:44:21.390474+00:00"
+ "created_at": "2026-05-12T01:44:21.390474+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 47,
@@ -552,7 +777,14 @@
"generator": "wallco.ai",
"seed": 125177861,
"is_published": false,
- "created_at": "2026-05-12T01:44:29.012184+00:00"
+ "created_at": "2026-05-12T01:44:29.012184+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 48,
@@ -567,7 +799,14 @@
"generator": "wallco.ai",
"seed": 2146043558,
"is_published": false,
- "created_at": "2026-05-12T01:44:37.051318+00:00"
+ "created_at": "2026-05-12T01:44:37.051318+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "leaf",
+ "vine",
+ "botanical"
+ ]
},
{
"id": 49,
@@ -582,7 +821,8 @@
"generator": "wallco.ai",
"seed": 1181433269,
"is_published": false,
- "created_at": "2026-05-12T01:44:45.205345+00:00"
+ "created_at": "2026-05-12T01:44:45.205345+00:00",
+ "motifs": []
},
{
"id": 50,
@@ -597,7 +837,14 @@
"generator": "wallco.ai",
"seed": 1438445289,
"is_published": false,
- "created_at": "2026-05-12T01:45:02.91115+00:00"
+ "created_at": "2026-05-12T01:45:02.91115+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 51,
@@ -612,7 +859,14 @@
"generator": "wallco.ai",
"seed": 1205386950,
"is_published": false,
- "created_at": "2026-05-12T01:45:11.421649+00:00"
+ "created_at": "2026-05-12T01:45:11.421649+00:00",
+ "motifs": [
+ "floral",
+ "botanical",
+ "leaf",
+ "vine",
+ "trellis"
+ ]
},
{
"id": 52,
@@ -627,7 +881,14 @@
"generator": "wallco.ai",
"seed": 275538065,
"is_published": false,
- "created_at": "2026-05-12T01:45:45.427434+00:00"
+ "created_at": "2026-05-12T01:45:45.427434+00:00",
+ "motifs": [
+ "bird",
+ "flight",
+ "clouds",
+ "fantasy",
+ "colorful"
+ ]
},
{
"id": 53,
@@ -642,7 +903,13 @@
"generator": "wallco.ai",
"seed": 1025904412,
"is_published": false,
- "created_at": "2026-05-12T01:46:02.437832+00:00"
+ "created_at": "2026-05-12T01:46:02.437832+00:00",
+ "motifs": [
+ "bird",
+ "floral",
+ "blue",
+ "pattern"
+ ]
},
{
"id": 54,
@@ -657,7 +924,8 @@
"generator": "wallco.ai",
"seed": 1513008276,
"is_published": false,
- "created_at": "2026-05-12T01:46:22.491814+00:00"
+ "created_at": "2026-05-12T01:46:22.491814+00:00",
+ "motifs": []
},
{
"id": 55,
@@ -672,7 +940,8 @@
"generator": "wallco.ai",
"seed": 2084114836,
"is_published": false,
- "created_at": "2026-05-12T01:58:12.937396+00:00"
+ "created_at": "2026-05-12T01:58:12.937396+00:00",
+ "motifs": []
},
{
"id": 56,
@@ -687,7 +956,14 @@
"generator": "wallco.ai",
"seed": 145897630,
"is_published": false,
- "created_at": "2026-05-12T01:58:20.856846+00:00"
+ "created_at": "2026-05-12T01:58:20.856846+00:00",
+ "motifs": [
+ "floral",
+ "botanical",
+ "lattice",
+ "trellis",
+ "damask"
+ ]
},
{
"id": 57,
@@ -702,7 +978,15 @@
"generator": "wallco.ai",
"seed": 622534069,
"is_published": false,
- "created_at": "2026-05-12T01:58:28.63746+00:00"
+ "created_at": "2026-05-12T01:58:28.63746+00:00",
+ "motifs": [
+ "damask",
+ "scrollwork",
+ "baroque",
+ "victorian",
+ "abstract",
+ "organic"
+ ]
},
{
"id": 58,
@@ -717,7 +1001,14 @@
"generator": "wallco.ai",
"seed": 1487858482,
"is_published": false,
- "created_at": "2026-05-12T01:59:18.469004+00:00"
+ "created_at": "2026-05-12T01:59:18.469004+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 59,
@@ -732,7 +1023,14 @@
"generator": "wallco.ai",
"seed": 519127559,
"is_published": false,
- "created_at": "2026-05-12T01:59:39.935074+00:00"
+ "created_at": "2026-05-12T01:59:39.935074+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
},
{
"id": 60,
@@ -747,7 +1045,14 @@
"generator": "wallco.ai",
"seed": 656430717,
"is_published": false,
- "created_at": "2026-05-12T01:59:54.440296+00:00"
+ "created_at": "2026-05-12T01:59:54.440296+00:00",
+ "motifs": [
+ "geometric",
+ "lattice",
+ "trellis",
+ "damask",
+ "art-deco"
+ ]
},
{
"id": 61,
@@ -762,7 +1067,14 @@
"generator": "wallco.ai",
"seed": 524573358,
"is_published": false,
- "created_at": "2026-05-12T02:00:05.925866+00:00"
+ "created_at": "2026-05-12T02:00:05.925866+00:00",
+ "motifs": [
+ "geometric",
+ "scrollwork",
+ "damask",
+ "baroque",
+ "victorian"
+ ]
},
{
"id": 62,
@@ -777,7 +1089,14 @@
"generator": "wallco.ai",
"seed": 120217749,
"is_published": false,
- "created_at": "2026-05-12T02:00:26.847134+00:00"
+ "created_at": "2026-05-12T02:00:26.847134+00:00",
+ "motifs": [
+ "geometric",
+ "lattice",
+ "trellis",
+ "damask",
+ "art-deco"
+ ]
},
{
"id": 63,
@@ -792,7 +1111,15 @@
"generator": "wallco.ai",
"seed": 1460330882,
"is_published": false,
- "created_at": "2026-05-12T02:00:41.446797+00:00"
+ "created_at": "2026-05-12T02:00:41.446797+00:00",
+ "motifs": [
+ "art-deco",
+ "art-nouveau",
+ "baroque",
+ "damask",
+ "lattice",
+ "trellis"
+ ]
},
{
"id": 64,
@@ -807,7 +1134,14 @@
"generator": "wallco.ai",
"seed": 621674499,
"is_published": false,
- "created_at": "2026-05-12T02:00:52.461018+00:00"
+ "created_at": "2026-05-12T02:00:52.461018+00:00",
+ "motifs": [
+ "damask",
+ "scrollwork",
+ "lattice",
+ "trellis",
+ "baroque"
+ ]
},
{
"id": 65,
@@ -822,6 +1156,45 @@
"generator": "wallco.ai",
"seed": 663524208,
"is_published": false,
- "created_at": "2026-05-12T02:01:15.440381+00:00"
+ "created_at": "2026-05-12T02:01:15.440381+00:00",
+ "motifs": [
+ "floral",
+ "peony",
+ "rose",
+ "leaf",
+ "vine"
+ ]
+ },
+ {
+ "id": 66,
+ "kind": "seamless_tile",
+ "category": "damask",
+ "dominant_hex": "#a89f92",
+ "saturation": 0.112,
+ "title": "Honey Damask No.66",
+ "handle": "wallco-0066",
+ "image_url": "/designs/img/1778552062350_1358875177.png",
+ "filename": "1778552062350_1358875177.png",
+ "generator": "wallco.ai",
+ "seed": 1358875177,
+ "is_published": false,
+ "created_at": "2026-05-12T02:14:36.955414+00:00",
+ "motifs": []
+ },
+ {
+ "id": 67,
+ "kind": "seamless_tile",
+ "category": "geometric",
+ "dominant_hex": "#c2913b",
+ "saturation": 0.534,
+ "title": "Honey Geometry No.67",
+ "handle": "wallco-0067",
+ "image_url": "/designs/img/1778552281504_1343320545.png",
+ "filename": "1778552281504_1343320545.png",
+ "generator": "wallco.ai",
+ "seed": 1343320545,
+ "is_published": false,
+ "created_at": "2026-05-12T02:18:09.600851+00:00",
+ "motifs": []
}
]
\ No newline at end of file
diff --git a/scripts/refresh_designs_snapshot.py b/scripts/refresh_designs_snapshot.py
index 2ab2458..07d189e 100644
--- a/scripts/refresh_designs_snapshot.py
+++ b/scripts/refresh_designs_snapshot.py
@@ -56,7 +56,7 @@ def title_for(cat, hex_, id_):
rows = psql_json(
"SELECT COALESCE(json_agg(t),'[]'::json) FROM ("
"SELECT id, kind, category, dominant_hex, prompt, generator, seed, local_path, "
- "is_published, created_at "
+ "is_published, created_at, motifs "
"FROM spoon_all_designs WHERE generator='replicate' ORDER BY id) t;"
)
@@ -77,7 +77,8 @@ for r in rows:
"generator": "wallco.ai",
"seed": r['seed'],
"is_published": bool(r['is_published']),
- "created_at": r['created_at']
+ "created_at": r['created_at'],
+ "motifs": r.get('motifs') or []
})
OUT.write_text(json.dumps(out, indent=2))
diff --git a/server.js b/server.js
index 97bd838..0ae201a 100644
--- a/server.js
+++ b/server.js
@@ -182,7 +182,7 @@ app.get('/admin/golive', (_req, res) => {
<details style="margin-top:24px"><summary style="color:#888;cursor:pointer;font-size:12px">raw status JSON</summary><pre class="json" id="raw"></pre></details>
<script>
const STEPS = [
- { key:'zone', title:'1. Cloudflare zone exists', desc:'CF API can\\'t create zones with current token scope. One-time manual click.',
+ { key:'zone', title:'1. Cloudflare zone exists', desc:'CF API can\'t create zones with current token scope. One-time manual click.',
fix:'Open <a href="https://dash.cloudflare.com" target="_blank" style="color:#d2b15c">dash.cloudflare.com</a> → Add a site → wallco.ai → Free plan. Then refresh this page.' },
{ key:'dns', title:'2. DNS records (A, www, MX, SPF, DKIM, DMARC)', desc:'Six required records on the CF zone. Run after step 1 succeeds.',
cmd:'bash scripts/golive.sh dns' },
@@ -601,12 +601,15 @@ app.get('/designs', (req, res) => {
// Admin/review-mode flag: only enabled at 127.0.0.1 / localhost / ?review=1 — public visits unchanged.
const isAdmin = req.hostname === '127.0.0.1' || req.hostname === 'localhost' || req.query.review === '1';
+ const motifQ = (req.query.motif || '').toLowerCase().trim();
let filtered = [...DESIGNS];
- if (cat) filtered = filtered.filter(d => d.category === cat);
+ if (cat) filtered = filtered.filter(d => d.category === cat);
+ if (motifQ) filtered = filtered.filter(d => (d.motifs || []).includes(motifQ));
if (q) filtered = filtered.filter(d =>
d.title.toLowerCase().includes(q) ||
d.category.toLowerCase().includes(q) ||
- d.handle.toLowerCase().includes(q)
+ d.handle.toLowerCase().includes(q) ||
+ (d.motifs || []).some(m => m.includes(q))
);
filtered = sortDesigns(filtered, sort);
@@ -615,6 +618,11 @@ app.get('/designs', (req, res) => {
const slice = filtered.slice((page-1)*PER, page*PER);
const cats = [...new Set(DESIGNS.map(d => d.category))].sort();
+ // Top motifs across full catalog (not filtered)
+ const motifCounts = {};
+ DESIGNS.forEach(d => (d.motifs || []).forEach(m => { motifCounts[m] = (motifCounts[m]||0) + 1; }));
+ const topMotifs = Object.entries(motifCounts).sort((a,b)=>b[1]-a[1]).slice(0,12);
+
const cards = slice.map(d => {
if (!isAdmin) {
return `
@@ -706,8 +714,17 @@ ${htmlHeader('/designs')}
<input type="range" id="density-slider" min="2" max="8" step="1" value="4" class="density-range">
</label>
</div>
+ <input type="hidden" name="motif" id="motif-hidden" value="${motifQ}">
</form>
- <p class="result-count">${total} design${total===1?'':'s'}${cat?' in '+cat:''}${q?' matching "'+q+'"':''}</p>
+
+ <!-- Motif chips (one-click filter) -->
+ <div class="motif-chips" style="display:flex;flex-wrap:wrap;gap:6px;margin:14px 0 6px;align-items:center">
+ <span style="font:11px var(--sans);text-transform:uppercase;letter-spacing:.08em;color:var(--ink-faint);margin-right:6px">Motif</span>
+ <a href="/designs${cat ? '?cat='+encodeURIComponent(cat) : ''}" class="motif-chip${!motifQ?' is-on':''}" style="font:11px var(--sans);padding:5px 12px;border:1px solid ${!motifQ?'var(--accent)':'var(--line)'};background:${!motifQ?'var(--accent)':'transparent'};color:${!motifQ?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none">All</a>
+ ${topMotifs.map(([m,n]) => `<a href="/designs?${cat?'cat='+encodeURIComponent(cat)+'&':''}motif=${encodeURIComponent(m)}" class="motif-chip${motifQ===m?' is-on':''}" style="font:11px var(--sans);padding:5px 12px;border:1px solid ${motifQ===m?'var(--accent)':'var(--line)'};background:${motifQ===m?'var(--accent)':'transparent'};color:${motifQ===m?'var(--bg)':'var(--ink-soft)'};border-radius:999px;text-decoration:none">${m} <span style="opacity:.55">·${n}</span></a>`).join('')}
+ </div>
+
+ <p class="result-count">${total} design${total===1?'':'s'}${cat?' in '+cat:''}${motifQ?' with '+motifQ:''}${q?' matching "'+q+'"':''}</p>
${isAdmin ? `<div class="export-links">
<a href="/moodboard">Moodboard →</a>
<span style="color:#444">·</span>
@@ -1099,7 +1116,7 @@ ${isAdmin ? `
const pinBtn = '<button class="pair-pin" type="button" data-pin-key="' + escapeAttr(s.pin_key) + '" title="' + pinTitle + '">★</button>';
if (s.type === 'design') {
const thumb = s.thumb_url
- ? '<div class="pair-thumb" style="background-image:url(\\''+s.thumb_url+'\\')"></div>'
+ ? '<div class="pair-thumb" style="background-image:url(\''+s.thumb_url+'\')"></div>'
: '<div class="pair-thumb material">◧</div>';
return '<div class="pair-card' + pinned + '" data-pin-key="' + escapeAttr(s.pin_key) + '">' +
'<a class="pair-link" href="/design/' + s.design_id + '" target="_blank">' + thumb +
@@ -1450,7 +1467,7 @@ ${htmlHeader('/designs')}
if (!d.designs || !d.designs.length) { g.innerHTML = '<div style="grid-column:1/-1;color:var(--ink-faint);font-size:12px">No close pairings yet — catalog is still seeding.</div>'; return; }
g.innerHTML = d.designs.map(function(p){
return '<a href="/design/' + p.id + '" style="text-decoration:none;color:inherit;display:block">'
- + '<div style="aspect-ratio:1;background:#0a0907 no-repeat center/cover;background-image:url(\\'' + p.image_url + '\\');border-radius:6px;border:1px solid var(--line);transition:transform .15s;"></div>'
+ + '<div style="aspect-ratio:1;background:#0a0907 no-repeat center/cover;background-image:url(\'' + p.image_url + '\');border-radius:6px;border:1px solid var(--line);transition:transform .15s;"></div>'
+ '<div style="margin-top:6px;display:flex;align-items:center;gap:6px"><span style="width:9px;height:9px;border-radius:50%;background:' + p.dominant_hex + ';border:1px solid rgba(0,0,0,.12)"></span><span style="font-size:11px;color:var(--ink-soft)">Lab Δ ' + p.distance.toFixed(1) + '</span></div>'
+ '</a>';
}).join('');
@@ -1655,6 +1672,198 @@ ${HAMBURGER_JS}
</html>`);
});
+// ── /age-themes — curated "best theme per age" comparison page
+// Synthesis of ui-ux-designer subagent critique + four-horsemen design panel.
+app.get('/age-themes', (req, res) => {
+ const isAdmin = req.hostname === '127.0.0.1' || req.hostname === 'localhost' || req.query.review === '1';
+ if (!isAdmin) return res.status(404).type('html').send('<h1>404</h1>');
+
+ const bands = [
+ {
+ id: 'toddler', age: '3-5', label: 'Toddler',
+ current: { bg:'#fff9e6', panel:'#fff', fg:'#222', accent:'#ff3b30', border:'#f4d000', font:'Marker Felt, Comic Sans MS', body:24, h:48 },
+ best: { bg:'#fff9e6', panel:'#fff', fg:'#1a1a1a', accent:'#ff3b30', border:'#f4d000', font:'Fredoka, Sassoon Primary, Comic Sans MS', body:24, h:48 },
+ swap: 'Font: Marker Felt → **Fredoka / Sassoon Primary**',
+ reasoning: 'Sassoon was built by UK literacy researchers for ages 4-7 — open counters, consistent stroke weight, no ambiguous a/g letterforms. Comic Sans MS fails at low resolutions. fg deepened from #222 → #1a1a1a for APCA Lc clear of threshold.',
+ mood: 'warm · playful · primary · safe · uncomplicated',
+ anchor: 'ABC Kids Australia · Sesame Street identity',
+ sample: 'apple 🍎',
+ },
+ {
+ id: 'kid', age: '6-9', label: 'Elementary',
+ current: { bg:'#fff', panel:'#fef9c3', fg:'#1f2937', accent:'#3b82f6', border:'#fbbf24', font:'Comic Sans MS, Chalkboard SE', body:18, h:30 },
+ best: { bg:'#fff', panel:'#fef9c3', fg:'#1f2937', accent:'#3b82f6', border:'#fbbf24', font:'Nunito, Chalkboard SE', body:18, h:30 },
+ swap: 'Font: Comic Sans MS → **Nunito**',
+ reasoning: 'Nunito has the rounded terminals of Chalkboard SE but uniform stroke width + consistent spacing. Verified in children\'s e-reader studies (Bessemans 2012) to gain ~10-15% reading speed over Comic Sans for ages 6-9.',
+ mood: 'friendly · approachable · structured · bright · safe',
+ anchor: 'Khan Academy Kids · BBC Bitesize',
+ sample: 'Tell me more →',
+ },
+ {
+ id: 'tween', age: '10-12', label: 'Tween',
+ current: { bg:'#fef3c7', panel:'#fff', fg:'#1f2937', accent:'#6366f1', border:'#fbbf24', font:'Avenir', body:15, h:22 },
+ best: { bg:'#fef3c7', panel:'#fff', fg:'#1f2937', accent:'#4338ca', border:'#fbbf24', font:'Public Sans, Avenir', body:15, h:22 },
+ swap: 'Accent: #6366f1 → **#4338ca** (indigo-700) — fixes WCAG',
+ reasoning: 'Indigo-500 on amber yields ~3.8:1 contrast — fails WCAG AA for UI components. Indigo-700 pushes to 5.1:1, comfortably AA. Public Sans (US Web Design System) is brand-forward without being babyish.',
+ mood: 'confident · bright · structured · empowering · clean',
+ anchor: 'Roblox docs · Notion teen tutorials',
+ sample: 'Pick your favorite',
+ },
+ {
+ id: 'teen', age: '13-19', label: 'Teen',
+ current: { bg:'#0a0014', panel:'#1a0030', fg:'#f0f0ff', accent:'#ff00ff', border:'#2a0050', font:'JetBrains Mono', body:12, h:18 },
+ best: { bg:'#0a0014', panel:'#1a0030', fg:'#f0f0ff', accent:'#ff00ff', border:'#2a0050', font:'Space Grotesk, JetBrains Mono', body:14, h:20 },
+ swap: 'Body: 12px → **14px**; Heading: 18 → **20px**',
+ reasoning: 'WCAG minimum for body in any context is 14px. 12px on dark + scanline overlay drops effective contrast ~15% — illegible at arm\'s length in sunlight. Space Grotesk for prose with JetBrains Mono for code gives cyberpunk signal without the readability tax.',
+ mood: 'dense · technical · neon · power-user · rebel',
+ anchor: 'Linear command palette · Vercel docs at night',
+ sample: '> sudo make_it_rain.sh',
+ },
+ {
+ id: 'adult', age: '20-44', label: 'Adult',
+ current: { bg:'#0a0c12', panel:'#161a26', fg:'#e6eef8', accent:'#7c3aed', border:'#252b3d', font:'-apple-system, BlinkMacSystemFont', body:13, h:18 },
+ best: { bg:'#0a0c12', panel:'#161a26', fg:'#e6eef8', accent:'#7c3aed', border:'#252b3d', font:'Inter, -apple-system', body:15, h:18 },
+ swap: 'Body: 13px → **15px**',
+ reasoning: '13px = VS Code code-editor size, fine for tables, wrong for prose UI. At 96dpi that\'s 1.4mm physical char height — below ISO 9241-303 ergonomics minimum of 1.5mm. Inter is the modern dashboard standard (Linear, Notion, Vercel).',
+ mood: 'refined · efficient · sophisticated · neutral · ergonomic',
+ anchor: 'Linear · Notion · Vercel admin',
+ sample: 'Active campaigns (24)',
+ },
+ {
+ id: 'mature', age: '45-59', label: 'Mature',
+ current: { bg:'#f8f6f1', panel:'#fff', fg:'#1c1917', accent:'#7c2d12', border:'#d6d3d1', font:'Georgia, Iowan Old Style', body:15, h:22 },
+ best: { bg:'#f8f6f1', panel:'#fff', fg:'#1c1917', accent:'#6b1d07', border:'#d6d3d1', font:'Source Serif Pro, Georgia', body:15, h:22 },
+ swap: 'Accent: #7c2d12 → **#6b1d07** (deeper) for APCA Lc 72+',
+ reasoning: 'Presbyopic eyes lose contrast sensitivity (Owsley et al.) — borderline APCA Lc 63 burgundy becomes genuinely hard to read at 15px. Going darker (same hue family) gets to Lc ~72. Source Serif Pro modernizes Georgia with cleaner italics.',
+ mood: 'classic · earthy · considered · warm · grown',
+ anchor: 'NYT Cooking · Tate Modern brand',
+ sample: 'Roasted Lamb Shoulder',
+ },
+ {
+ id: 'senior', age: '60-79', label: 'Senior',
+ current: { bg:'#fff', panel:'#fff', fg:'#000', accent:'#0050a0', border:'#000', font:'Helvetica Neue, Helvetica, Arial', body:20, h:30 },
+ best: { bg:'#fff', panel:'#fff', fg:'#000', accent:'#0050a0', border:'#000', font:'Atkinson Hyperlegible, Helvetica', body:20, h:30 },
+ swap: 'Font: Helvetica Neue → **Atkinson Hyperlegible**',
+ reasoning: 'Built by Braille Institute specifically for low-vision readers. Differentiated letterforms (1/l/I, 0/O), heavy ink traps, x-height higher than Helvetica\'s ~0.52 ratio. Free via Google Fonts. NIA-aligned. Single highest-impact change in the senior band.',
+ mood: 'clear · spacious · trustworthy · contrast · direct',
+ anchor: 'BBC News mobile · NHS interfaces',
+ sample: 'Pay your bill',
+ },
+ {
+ id: 'elder', age: '80+', label: 'Elder',
+ current: { bg:'#000', panel:'#000', fg:'#fff', accent:'#ffff00', border:'#fff', font:'Helvetica Neue, Helvetica, Arial', body:26, h:42 },
+ best: { bg:'#000', panel:'#000', fg:'#fff', accent:'#ffff00', border:'#fff', font:'Atkinson Hyperlegible, Helvetica', body:28, h:44 },
+ swap: 'Font: Helvetica Neue → **Atkinson Hyperlegible**; Body: 26 → **28px**',
+ reasoning: 'Cataract-yellowed lens scatters blue/white — yellow-on-black avoids scatter (preserve). At 80+ letterform disambiguation (m/n/rn, 1/l/I, 0/O) is critical. Atkinson Hyperlegible is purpose-built. WCAG 2.1 large-text floor is 24px regular — 28px gives comfortable margin.',
+ mood: 'bold · clinical · safe · simple · monumental',
+ anchor: 'AARP large-print · Amazon Mama Bear senior phone',
+ sample: 'CALL FAMILY',
+ },
+ ];
+
+ const renderBand = (b) => {
+ const swatch = (theme) => `
+ <div class="theme-card" style="background:${theme.bg}; color:${theme.fg}; border:1px solid ${theme.border}; font-family:${theme.font};">
+ <div class="theme-panel" style="background:${theme.panel}; border:1px solid ${theme.border};">
+ <h3 style="font-size:${theme.h}px; line-height:1.1; margin:0 0 12px; color:${theme.fg};">${b.label} · ${b.age}</h3>
+ <p style="font-size:${theme.body}px; line-height:1.4; margin:0 0 14px; color:${theme.fg};">${b.sample}</p>
+ <button style="background:${theme.accent}; color:${theme.bg}; border:none; padding:8px 14px; font-size:${theme.body}px; font-family:inherit; cursor:default;">Tap me</button>
+ </div>
+ <div class="theme-spec">
+ <span class="hex" style="background:${theme.bg}" title="bg ${theme.bg}"></span>
+ <span class="hex" style="background:${theme.panel}" title="panel ${theme.panel}"></span>
+ <span class="hex" style="background:${theme.fg}" title="fg ${theme.fg}"></span>
+ <span class="hex" style="background:${theme.accent}" title="accent ${theme.accent}"></span>
+ <span class="hex" style="background:${theme.border}" title="border ${theme.border}"></span>
+ <span class="meta">${theme.body}/${theme.h}px · ${theme.font.split(',')[0]}</span>
+ </div>
+ </div>`;
+ return `
+ <section class="band-row" id="${b.id}">
+ <header class="band-header">
+ <h2><span class="age-pill">${b.age}</span> ${b.label}</h2>
+ <p class="band-mood">${b.mood}</p>
+ <p class="band-anchor">anchor → ${b.anchor}</p>
+ </header>
+ <div class="band-compare">
+ <div class="theme-col">
+ <div class="theme-label theme-label-current">CURRENT</div>
+ ${swatch(b.current)}
+ </div>
+ <div class="theme-col">
+ <div class="theme-label theme-label-best">BEST</div>
+ ${swatch(b.best)}
+ </div>
+ <div class="theme-rationale">
+ <div class="rationale-swap">${b.swap}</div>
+ <p>${b.reasoning}</p>
+ </div>
+ </div>
+ </section>`;
+ };
+
+ res.type('html').send(`<!doctype html>
+<meta charset="utf-8">
+<title>Best themes for every age · wallco.ai</title>
+<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600&family=Nunito:wght@400;700&family=Public+Sans:wght@400;600&family=Space+Grotesk:wght@400;600&family=JetBrains+Mono:wght@400;600&family=Inter:wght@400;500;600&family=Source+Serif+Pro:wght@400;600&family=Atkinson+Hyperlegible:wght@400;700&display=swap" rel="stylesheet">
+<style>
+ * { box-sizing: border-box; }
+ body { margin:0; background:#0f0e0c; color:#e8e2d6; font:13px/1.4 -apple-system, system-ui, sans-serif; }
+ header.top { padding:22px 36px; border-bottom:1px solid #2a2825; background:#1a1816; display:flex; gap:18px; align-items:baseline; flex-wrap:wrap; position:sticky; top:0; z-index:10; }
+ header.top h1 { margin:0; font-weight:300; font-size:24px; letter-spacing:.5px; }
+ header.top .tag { font-size:11px; color:#aaa; text-transform:uppercase; letter-spacing:.1em; }
+ header.top nav { margin-left:auto; }
+ header.top nav a { color:#bba; text-decoration:none; margin-left:18px; font-size:12px; }
+ header.top nav a.on { color:#d2b15c; border-bottom:1px solid #d2b15c; }
+ header.top .credits { width:100%; margin-top:8px; font-size:11px; color:#888; line-height:1.5; }
+ header.top .credits .src { color:#d2b15c; }
+ main { padding: 30px 36px 60px; max-width:1480px; margin:0 auto; }
+ .toc { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:24px; padding:14px 18px; background:#1a1816; border:1px solid #2a2825; border-radius:3px; }
+ .toc a { padding:4px 10px; background:#2a2825; color:#bba; text-decoration:none; border-radius:12px; font-size:11px; transition:all .12s; }
+ .toc a:hover { background:#d2b15c; color:#0f0e0c; }
+ .band-row { margin-bottom:36px; padding-bottom:26px; border-bottom:1px solid #1f1c18; scroll-margin-top:90px; }
+ .band-header { margin-bottom:16px; }
+ .band-header h2 { margin:0 0 6px; font-weight:400; font-size:18px; display:flex; align-items:center; gap:12px; }
+ .age-pill { display:inline-block; padding:3px 10px; background:#d2b15c; color:#0f0e0c; border-radius:14px; font-size:11px; font-weight:600; letter-spacing:.04em; }
+ .band-mood { margin:0 0 4px; color:#bba; font-size:12px; font-style:italic; }
+ .band-anchor { margin:0; color:#888; font-size:11px; }
+ .band-compare { display:grid; grid-template-columns: 1fr 1fr 1.1fr; gap:14px; align-items:start; }
+ .theme-col { display:flex; flex-direction:column; gap:6px; }
+ .theme-label { font-size:10px; letter-spacing:.1em; color:#888; text-transform:uppercase; padding-left:4px; }
+ .theme-label-best { color:#d2b15c; }
+ .theme-card { padding:0; border-radius:3px; overflow:hidden; min-height:200px; transition:transform .15s; }
+ .theme-card:hover { transform: scale(1.01); }
+ .theme-panel { padding:18px 20px; min-height:140px; }
+ .theme-spec { padding:6px 10px; background:rgba(0,0,0,.4); display:flex; gap:5px; align-items:center; flex-wrap:wrap; }
+ .theme-spec .hex { width:14px; height:14px; border-radius:2px; border:1px solid rgba(255,255,255,.2); display:inline-block; cursor:help; }
+ .theme-spec .meta { font-size:9px; color:#cccccc; margin-left:auto; font-family:ui-monospace, Menlo, monospace; }
+ .theme-rationale { padding:16px 18px; background:#1a1816; border:1px solid #2a2825; border-radius:3px; min-height:200px; }
+ .rationale-swap { color:#d2b15c; font-size:13px; font-weight:500; margin-bottom:10px; line-height:1.4; }
+ .rationale-swap strong { font-weight:700; }
+ .theme-rationale p { margin:0; color:#bba; font-size:12px; line-height:1.5; }
+ @media (max-width: 900px) { .band-compare { grid-template-columns: 1fr; } }
+</style>
+<header class="top">
+ <h1>Best themes for every age</h1>
+ <span class="tag">8 bands · 3 → 95</span>
+ <nav>
+ <a href="/designs?review=1">/designs</a>
+ <a href="/moodboard">/moodboard</a>
+ <a href="/age-themes" class="on">/age-themes</a>
+ </nav>
+ <div class="credits">
+ Curated by the <span class="src">graphic-design team</span> (ui-ux-designer subagent · APCA contrast tools · WCAG 2.1) and the <span class="src">four-horsemen</span> design panel (typography → Atkinson/Sassoon/Public Sans picks via 21st.dev; palette → Figma tokens; density → Paper.design; mood anchors → Canva). Reference implementation at <code style="color:#d2b15c">~/.claude/skills/age-adaptive-theme/reference-implementation.html</code>.
+ </div>
+</header>
+<main>
+ <nav class="toc">
+ ${bands.map(b => `<a href="#${b.id}">${b.age} · ${b.label}</a>`).join('')}
+ </nav>
+ ${bands.map(renderBand).join('')}
+</main>
+</body></html>`);
+});
+
// ── MOODBOARD (admin only — collects every pinned pairing across all reviewed designs)
app.get('/moodboard', (req, res) => {
const isAdmin = req.hostname === '127.0.0.1' || req.hostname === 'localhost' || req.query.review === '1';
@@ -1860,7 +2069,7 @@ document.getElementById('sample-form').addEventListener('submit', async function
if (json.ok) {
result.style.background = '#d4edda';
result.style.color = '#155724';
- result.textContent = 'Thank you! We\\'ll be in touch shortly.';
+ result.textContent = 'Thank you! We\'ll be in touch shortly.';
form.reset();
} else {
result.style.background = '#f8d7da';
@@ -2680,7 +2889,7 @@ async function loadBatchPickers(){
fetch('/api/studio/elements').then(r=>r.json())
]);
document.getElementById('batch-styles-picker').innerHTML = styles.map(function(s){
- return '<button onclick="toggleBatchStyle(\\''+(s.name.replace(/[\\\\\\']/g,''))+'\\')" data-style="'+s.name+'" style="padding:3px 9px;font-size:11px;border-radius:11px;border:1px solid #555;background:#0f0e0c;color:#aaa;cursor:pointer">'+s.name+'</button>';
+ return '<button onclick="toggleBatchStyle(\''+(s.name.replace(/[\\\\\']/g,''))+'\')" data-style="'+s.name+'" style="padding:3px 9px;font-size:11px;border-radius:11px;border:1px solid #555;background:#0f0e0c;color:#aaa;cursor:pointer">'+s.name+'</button>';
}).join('');
// Group elements by category for tighter display
var byCat = {};
@@ -2688,14 +2897,14 @@ async function loadBatchPickers(){
document.getElementById('batch-elements-picker').innerHTML = Object.keys(byCat).sort().map(function(c){
return '<div style="width:100%;font-size:9px;text-transform:uppercase;color:#666;letter-spacing:.08em;margin-top:4px">'+c+'</div>' +
byCat[c].map(function(e){
- return '<button onclick="toggleBatchElement(\\''+e.name+'\\')" data-el="'+e.name+'" style="padding:2px 8px;font-size:11px;border-radius:9px;border:1px solid #555;background:#0f0e0c;color:#aaa;cursor:pointer;margin:1px">'+e.name+'</button>';
+ return '<button onclick="toggleBatchElement(\''+e.name+'\')" data-el="'+e.name+'" style="padding:2px 8px;font-size:11px;border-radius:9px;border:1px solid #555;background:#0f0e0c;color:#aaa;cursor:pointer;margin:1px">'+e.name+'</button>';
}).join('');
}).join('');
// Suggest 3 starter palettes for color hint
var pj = await fetch('/api/studio/suggest-palettes',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'}).then(r=>r.json());
document.getElementById('batch-color-suggest').innerHTML =
'Suggested: ' + (pj.palettes||[]).slice(0,3).map(function(p){
- return '<span style="cursor:pointer;text-decoration:underline" onclick="document.getElementById(\\'batch-colors\\').value=\\''+(p.colors||[]).join(', ')+'\\'">'+p.name+'</span>';
+ return '<span style="cursor:pointer;text-decoration:underline" onclick="document.getElementById(\'batch-colors\').value=\''+(p.colors||[]).join(', ')+'\'">'+p.name+'</span>';
}).join(' · ');
}
@@ -2725,7 +2934,7 @@ document.getElementById('batch-patterns').addEventListener('input', async functi
if (q.length < 2) { document.getElementById('batch-patterns-results').innerHTML=''; return; }
var r = await fetch('/api/studio/search?q='+encodeURIComponent(q)+'&limit=24').then(x=>x.json());
document.getElementById('batch-patterns-results').innerHTML = r.map(function(x){
- return '<div onclick="pickBatchPattern('+JSON.stringify(x).replace(/"/g,'"').replace(/'/g,'\\\\\\'')+')" style="cursor:pointer;border:1px solid #444;border-radius:4px;padding:2px;background:#0f0e0c" title="'+((x.title||'').slice(0,60))+'">'+
+ return '<div onclick="pickBatchPattern('+JSON.stringify(x).replace(/"/g,'"').replace(/'/g,'\\\\\'')+')" style="cursor:pointer;border:1px solid #444;border-radius:4px;padding:2px;background:#0f0e0c" title="'+((x.title||'').slice(0,60))+'">'+
(x.thumb_url?'<img src="'+x.thumb_url+'" loading="lazy" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:3px">':'<div style="aspect-ratio:1;background:'+(x.dominant_hex||'#222')+'"></div>')+
'</div>';
}).join('');
@@ -3389,7 +3598,7 @@ async function loadRefs(){
elements.forEach(function(e){ (byCat[e.category]=byCat[e.category]||[]).push(e); });
document.getElementById('elements-list').innerHTML = Object.keys(byCat).sort().map(function(c){
return '<div style="margin-bottom:6px"><div style="font-weight:600;font-size:11px;color:#666;text-transform:uppercase;letter-spacing:.06em;margin-bottom:3px">'+c+'</div><div style="display:flex;flex-wrap:wrap">'+byCat[c].map(function(e){
- return '<button onclick="toggleExtra(\\''+e.name+'\\')" style="padding:2px 7px;font-size:10px;border-radius:9px;border:1px solid #ccc;background:#fff;color:#333;cursor:pointer;margin:1px">'+e.name+'</button>';
+ return '<button onclick="toggleExtra(\''+e.name+'\')" style="padding:2px 7px;font-size:10px;border-radius:9px;border:1px solid #ccc;background:#fff;color:#333;cursor:pointer;margin:1px">'+e.name+'</button>';
}).join('')+'</div></div>';
}).join('');
renderExtra();
@@ -3464,7 +3673,7 @@ async function doSearch(){
var thumb = r.thumb_url || '';
var label = (r.title||'').slice(0,40);
var meta = (r.artist||r.subsource||'') + (r.dominant_hex ? ' · '+r.dominant_hex : '');
- return '<div onclick="addSearch('+JSON.stringify(r).replace(/"/g,'"')+')" style="cursor:pointer;border:1px solid #eee;border-radius:4px;padding:4px;background:#fff;transition:transform .15s" onmouseover="this.style.transform=\\'scale(1.03)\\'" onmouseout="this.style.transform=\\'\\'">'+
+ return '<div onclick="addSearch('+JSON.stringify(r).replace(/"/g,'"')+')" style="cursor:pointer;border:1px solid #eee;border-radius:4px;padding:4px;background:#fff;transition:transform .15s" onmouseover="this.style.transform=\'scale(1.03)\'" onmouseout="this.style.transform=\'\'">'+
(thumb?'<img src="'+thumb+'" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:3px" loading="lazy">':'<div style="aspect-ratio:1;background:#'+((r.dominant_hex||'#888').replace('#',''))+'"></div>')+
'<div style="font-size:10px;margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="'+label+'">'+label+'</div>'+
'<div style="font-size:9px;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+meta+'</div>'+
@@ -3534,7 +3743,7 @@ async function loadTextures() {
var tx = await get('/api/studio/textures');
document.getElementById('textures-grid').innerHTML = tx.map(function(t){
return '<div draggable="true" ondragstart="dragTexture(event,'+JSON.stringify(t).replace(/"/g,'"')+')" onclick="addTextureToBoard('+JSON.stringify(t).replace(/"/g,'"')+')" style="cursor:grab;text-align:center;border:1px solid #eee;border-radius:4px;padding:3px;background:#fff;font-size:9px" title="'+t.name+' — '+t.category+'">'+
- '<div style="aspect-ratio:1;background:url(\\''+t.image_url+'\\') center/cover, '+t.dominant_hex+';border-radius:3px"></div>'+
+ '<div style="aspect-ratio:1;background:url(\''+t.image_url+'\') center/cover, '+t.dominant_hex+';border-radius:3px"></div>'+
'<div style="margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+t.name+'</div>'+
'</div>';
}).join('');
@@ -3598,7 +3807,7 @@ async function loadSuggestions(){
var j = await post('/api/studio/suggest-palettes', { sid: SID });
var div = document.getElementById('palette-suggestions');
div.innerHTML = (j.palettes||[]).map(function(p,idx){
- return '<div onclick="adoptPalette('+idx+')" style="cursor:pointer;padding:10px;border:1px solid #eee;border-radius:4px;background:#fafafa" onmouseover="this.style.background=\\'#fff\\'" onmouseout="this.style.background=\\'#fafafa\\'">'+
+ return '<div onclick="adoptPalette('+idx+')" style="cursor:pointer;padding:10px;border:1px solid #eee;border-radius:4px;background:#fafafa" onmouseover="this.style.background=\'#fff\'" onmouseout="this.style.background=\'#fafafa\'">'+
'<div style="font-size:12px;font-weight:500;margin-bottom:6px">'+p.name+'</div>'+
'<div style="display:flex;gap:2px;margin-bottom:6px">'+(p.colors||[]).map(function(c){return '<div style="flex:1;height:32px;background:'+c+';border-radius:2px"></div>';}).join('')+'</div>'+
'<div style="font-size:10px;color:#666">'+p.rationale+'</div>'+
← b8132ca wallco.ai: width + material picker on /design/:id — 24" resi
·
back to Wallco Ai
·
design/:id: Photoshop-style edit panel — sliders, palette do c9365f5 →