← back to Dw Yolo Loop
Romo landing: 578-product snapshot + lexicon attribution (color/style/motif facets)
671aec7c48ff60b727856b5679623b671c288f74 · 2026-08-03 10:51:27 -0700 · steve-office
- site.config.js: add romo block (DW house chrome, collection handle 'romo')
- build-romo-enrich.js: attribute live products onto interior-designer lexicon from Shopify tags
- server.js: styles/motifs on each record + in /api/facets; vendor-aware startup banner
- index.html: Style + Motif filter selects + color bar (auto-hide when empty)
- lines/romo-colors.json: 578/578 color buckets, 522 styles, 525 motifs
Files touched
A artmura-site/build-romo-enrich.jsM artmura-site/package.jsonM artmura-site/public/index.htmlM artmura-site/server.jsM artmura-site/site.config.js
Diff
commit 671aec7c48ff60b727856b5679623b671c288f74
Author: steve-office <steve@designerwallcoverings.com>
Date: Mon Aug 3 10:51:27 2026 -0700
Romo landing: 578-product snapshot + lexicon attribution (color/style/motif facets)
- site.config.js: add romo block (DW house chrome, collection handle 'romo')
- build-romo-enrich.js: attribute live products onto interior-designer lexicon from Shopify tags
- server.js: styles/motifs on each record + in /api/facets; vendor-aware startup banner
- index.html: Style + Motif filter selects + color bar (auto-hide when empty)
- lines/romo-colors.json: 578/578 color buckets, 522 styles, 525 motifs
---
artmura-site/build-romo-enrich.js | 98 +++++++++++++++++++++++++++++++++++++++
artmura-site/package.json | 8 +++-
artmura-site/public/index.html | 23 ++++++++-
artmura-site/server.js | 11 +++--
artmura-site/site.config.js | 27 +++++++++++
5 files changed, 160 insertions(+), 7 deletions(-)
diff --git a/artmura-site/build-romo-enrich.js b/artmura-site/build-romo-enrich.js
new file mode 100644
index 0000000..6be3919
--- /dev/null
+++ b/artmura-site/build-romo-enrich.js
@@ -0,0 +1,98 @@
+#!/usr/bin/env node
+/*
+ * build-romo-enrich.js — attribute Romo products onto the interior-designer lexicon.
+ * SOURCE OF TRUTH = each live product's own Shopify `tags` in lines/romo.json (covers all 578).
+ * romo_catalog.dw_sku supplies a precise dominant hex when the key matches (refinement only).
+ * Emits lines/romo-colors.json { "<dwsku>": { bucket,hue,sat,val,colorway,styles,motifs } }
+ * keyed to match server.js `colors[r.mfr_sku]`. Read-only; $0 local.
+ */
+const fs = require('fs'), path = require('path'), { execSync } = require('child_process');
+const HERE = __dirname;
+const SNAP = path.join(HERE,'lines','romo.json');
+const OUT = path.join(HERE,'lines','romo-colors.json');
+
+// ---- controlled STYLE vocab (interior-designer lexicon) ----
+const STYLES = new Set(['Contemporary','Traditional','Minimalist','Scandinavian','Coastal','Farmhouse',
+ 'Chinoiserie','Mid-Century Modern','Art Deco','Industrial','Transitional','Modern','Victorian',
+ 'Bohemian','Rustic','Eclectic','Cottage','Organic Modern','Hollywood Regency','Maximalist','Grandmillennial']);
+const STYLE_ALIAS = { 'mid-century modern':'Mid-Century Modern','midcentury modern':'Mid-Century Modern' };
+
+// ---- controlled MOTIF vocab; tag(lower) -> canonical motif ----
+const MOTIF = {
+ 'grasscloth':'Grasscloth','solid':'Solid','stripe':'Stripe','striped':'Stripe','textured':'Textured',
+ 'texture':'Textured','linen texture':'Textured','silk texture':'Textured','woven':'Textured',
+ 'geometric':'Geometric','floral':'Floral','botanical':'Botanical','herringbone':'Herringbone',
+ 'chevron':'Chevron','damask':'Damask','trellis':'Trellis','animal':'Animalier','animals':'Animalier',
+ 'animalier':'Animalier','toile':'Toile','ikat':'Ikat','paisley':'Paisley','abstract':'Abstract',
+ 'mural':'Mural / Scenic','wood grain':'Faux Bois','faux bois':'Faux Bois','polka dot':'Polka Dot',
+ 'checkerboard':'Check','brick':'Brick','tropical':'Tropical / Palm','border':'Border','moire':'Moiré',
+};
+
+// ---- colorway tag(lower) -> one of the 12 frontend BUCKET_HEX keys ----
+const B = (k)=>k;
+const COLOR_BUCKET = {};
+const add=(bucket,names)=>names.forEach(n=>COLOR_BUCKET[n.toLowerCase()]=bucket);
+add('white',['White','Off-white','Off White','Bone','Alabaster','Chalk','Ivory','Cream','Cotton','Pearl','Porcelain','Eggshell','Swiss Coffee','Marshmallow','Dove White','Antique White','Snow']);
+add('grey', ['Grey','Gray','Ash','Silver','Silver Gray','Light Gray','Light Grey','Dove','Fog','Smoke','Pewter','Nickel','Flannel','Slate','Zinc','Charcoal Gray','Steel']);
+add('black',['Black','Ink','Noir','Onyx','Ebony','Jet','Obsidian','Coal','Charcoal','Graphite','Gunmetal']);
+add('brown',['Brown','Light Brown','Taupe','Greige','Oatmeal','Flax','String','Mushroom','Putty','Stone','Fawn','Wheat','Sand','Biscuit','Almond','Pebble','Beige','Light Beige','Pale Beige','Tan','Buff','Chamois','Khaki','Camel','Caramel','Pecan','Cognac','Chestnut','Tobacco','Walnut','Mocha','Espresso','Natural','Sepia','Linen']);
+add('gold', ['Gold','Pale Gold','Champagne','Brass','Honey','Ochre','Mustard','Maize','Citrine','Saffron','Amber','Buttercream','Yellow']);
+add('green',['Green','Celadon','Pistachio','Sage','Eucalyptus','Sea Glass','Moss','Fern','Olive','Loden','Juniper','Hunter','Forest','Emerald','Verdigris','Bottle Green']);
+add('teal', ['Teal','Peacock','Aqua','Turquoise']);
+add('blue', ['Blue','Powder','Sky','Wedgwood','Cornflower','French Blue','Cerulean','Denim','Slate Blue','Prussian','Indigo','Navy','Midnight']);
+add('orange',['Orange','Apricot','Peach','Clay','Terracotta','Persimmon','Pumpkin','Marmalade','Copper','Rust','Sienna','Coral']);
+add('red', ['Red','Cinnabar','Brick Red','Oxblood','Claret','Garnet','Burgundy','Crimson','Scarlet']);
+add('purple',['Purple','Lavender','Lilac','Wisteria','Orchid','Heather','Mauve','Amethyst','Plum','Aubergine','Violet']);
+add('pink', ['Pink','Blush','Rose','Dusty Rose','Magenta','Fuchsia']);
+
+// representative HSV per bucket (for light->dark / color-wheel sorts when no real hex)
+const BUCKET_HSV = { white:{h:40,s:.05,v:.95}, grey:{h:0,s:0,v:.62}, black:{h:0,s:0,v:.13},
+ brown:{h:30,s:.34,v:.45}, red:{h:5,s:.6,v:.55}, orange:{h:26,s:.6,v:.7}, gold:{h:45,s:.5,v:.76},
+ green:{h:120,s:.38,v:.48}, teal:{h:180,s:.4,v:.5}, blue:{h:218,s:.4,v:.55}, purple:{h:280,s:.34,v:.5}, pink:{h:340,s:.34,v:.72} };
+
+function hexToHsv(hex){
+ const m=/^#?([0-9a-f]{6})$/i.exec((hex||'').trim()); if(!m) return null;
+ const n=parseInt(m[1],16), r=(n>>16&255)/255,g=(n>>8&255)/255,b=(n&255)/255;
+ const mx=Math.max(r,g,b),mn=Math.min(r,g,b),d=mx-mn; let h=0;
+ if(d){ if(mx===r)h=((g-b)/d)%6; else if(mx===g)h=(b-r)/d+2; else h=(r-g)/d+4; h*=60; if(h<0)h+=360; }
+ return { h:Math.round(h), s:+(mx?d/mx:0).toFixed(3), v:+mx.toFixed(3) };
+}
+
+// optional precise hex from the mirror, keyed by dw_sku
+let hexByKey={};
+try{
+ const sql="COPY (SELECT dw_sku||E'\\t'||COALESCE(dominant_color_hex,color_hex,'') FROM romo_catalog WHERE dw_sku IS NOT NULL) TO STDOUT";
+ const raw=execSync(`psql "host=/tmp dbname=dw_unified" -tAc ${JSON.stringify(sql)}`,{maxBuffer:64*1024*1024}).toString();
+ for(const line of raw.split('\n')){ const [k,h]=line.split('\t'); if(k&&h&&/^#?[0-9a-f]{6}$/i.test(h)) hexByKey[k]=h.startsWith('#')?h:('#'+h); }
+}catch(e){ console.error('(hex refinement skipped:', e.message,')'); }
+
+const snap=JSON.parse(fs.readFileSync(SNAP,'utf8'));
+const out={}; let n=0;
+for(const p of snap.products){
+ const key=p.mfr_sku;
+ const tags=(p.tags||[]).map(t=>String(t));
+ const low=tags.map(t=>t.toLowerCase());
+ const styles=new Set(), motifs=new Set();
+ tags.forEach((t,i)=>{
+ if(STYLES.has(t)) styles.add(t);
+ else if(STYLE_ALIAS[low[i]]) styles.add(STYLE_ALIAS[low[i]]);
+ if(MOTIF[low[i]]) motifs.add(MOTIF[low[i]]);
+ });
+ // colorway: first tag that is a known colorway name; bucket from it
+ let colorway=null, bucket=null;
+ for(let i=0;i<tags.length;i++){ if(COLOR_BUCKET[low[i]]){ colorway=tags[i]; bucket=COLOR_BUCKET[low[i]]; break; } }
+ const hsv = hexByKey[key] ? hexToHsv(hexByKey[key]) : (bucket ? BUCKET_HSV[bucket] : null);
+ const e={ styles:[...styles], motifs:[...motifs] };
+ if(bucket){ e.bucket=bucket; e.colorway=colorway; }
+ if(hexByKey[key]) e.hex=hexByKey[key];
+ if(hsv){ e.hue=hsv.h; e.sat=hsv.s; e.val=hsv.v; }
+ out[key]=e; n++;
+}
+fs.writeFileSync(OUT, JSON.stringify(out));
+const tally=(sel)=>{const c={};for(const k in out)for(const v of out[k][sel]||[])c[v]=(c[v]||0)+1;return Object.entries(c).sort((a,b)=>b[1]-a[1]);};
+const bc={}; for(const k in out){const b=out[k].bucket; if(b)bc[b]=(bc[b]||0)+1;}
+console.log(`romo-enrich: ${n} products →`, path.relative(HERE,OUT));
+console.log(' precise-hex from DB:', Object.keys(hexByKey).length ? Object.values(out).filter(e=>e.hex).length : 0, '/', n);
+console.log(' styles :', tally('styles'));
+console.log(' motifs :', tally('motifs'));
+console.log(' buckets:', Object.entries(bc).sort((a,b)=>b[1]-a[1]));
diff --git a/artmura-site/package.json b/artmura-site/package.json
index c5eb02f..cbdd4d8 100644
--- a/artmura-site/package.json
+++ b/artmura-site/package.json
@@ -4,6 +4,10 @@
"private": true,
"description": "Artmura wallcoverings microsite (DW vendor onboard, stage-only preview)",
"main": "server.js",
- "scripts": { "start": "node server.js" },
- "dependencies": { "express": "^4.19.2" }
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "express": "^4.22.2"
+ }
}
diff --git a/artmura-site/public/index.html b/artmura-site/public/index.html
index 276bf69..04ab4fd 100644
--- a/artmura-site/public/index.html
+++ b/artmura-site/public/index.html
@@ -46,6 +46,14 @@
<label for="pattern">Pattern</label>
<select id="pattern"><option value="">All patterns</option></select>
</div>
+ <div class="ctl">
+ <label for="style">Style</label>
+ <select id="style"><option value="">All styles</option></select>
+ </div>
+ <div class="ctl">
+ <label for="motif">Motif</label>
+ <select id="motif"><option value="">All motifs</option></select>
+ </div>
<div class="ctl">
<label for="sort">Sort</label>
<select id="sort">
@@ -87,7 +95,7 @@
<script>
const $=s=>document.querySelector(s), grid=$('#grid');
-let ALL=[], view=[], activeBook=null, activeColor=null, activeSeries=null, FACETS=null, LINE='';
+let ALL=[], view=[], activeBook=null, activeColor=null, activeSeries=null, activeStyle=null, activeMotif=null, FACETS=null, LINE='';
const LS={sort:'dwline.sort', dens:'dwline.density'};
const BUCKET_HEX={white:'#efeae0',grey:'#9e978c',black:'#2a2722',red:'#a23b2e',orange:'#c8763a',brown:'#7b5a3c',gold:'#b8923f',green:'#5d7150',teal:'#3f8a86',blue:'#5a7494',purple:'#7a5f86',pink:'#bb8aa0'};
@@ -135,6 +143,8 @@ function render(){
if(activeBook) items = items.filter(p=>p.book===activeBook);
if(activeColor) items = items.filter(p=>p.color_bucket===activeColor);
if(activeSeries) items = items.filter(p=>p.series===activeSeries);
+ if(activeStyle) items = items.filter(p=>(p.styles||[]).includes(activeStyle));
+ if(activeMotif) items = items.filter(p=>(p.motifs||[]).includes(activeMotif));
items = sortItems(items, $('#sort').value);
view=items;
$('#cnt').textContent=items.length;
@@ -172,10 +182,17 @@ function applyDensity(v){document.documentElement.style.setProperty('--cols',v);
$('#density').addEventListener('input',e=>{applyDensity(e.target.value);localStorage.setItem(LS.dens,e.target.value);});
$('#sort').addEventListener('change',()=>{localStorage.setItem(LS.sort,$('#sort').value);render();});
$('#pattern').addEventListener('change',()=>{activeSeries=$('#pattern').value||null;render();$('#catalog').scrollIntoView({behavior:'smooth'});});
+$('#style').addEventListener('change',()=>{activeStyle=$('#style').value||null;render();$('#catalog').scrollIntoView({behavior:'smooth'});});
+$('#motif').addEventListener('change',()=>{activeMotif=$('#motif').value||null;render();$('#catalog').scrollIntoView({behavior:'smooth'});});
function fillPatterns(facets){
const sel=$('#pattern');
sel.innerHTML='<option value="">All patterns</option>'+facets.series.map(([s,n])=>`<option value="${s}">${s} (${n})</option>`).join('');
}
+function fillSelect(id,rows,label){
+ const sel=$(id); if(!sel) return;
+ if(!rows||!rows.length){ const c=sel.closest('.ctl'); if(c)c.style.display='none'; return; }
+ sel.innerHTML=`<option value="">${label}</option>`+rows.map(([s,n])=>`<option value="${s}">${s} (${n})</option>`).join('');
+}
// hide hero corners + show bar wordmark once scrolled into catalog
addEventListener('scroll',()=>{document.body.classList.toggle('scrolled',scrollY>innerHeight*0.82);});
@@ -242,7 +259,9 @@ function renderShare(url, title){
applyConfig(cfg);
ALL=pd.products; FACETS=fc;
const sb=$('#shopbar'); if(sb) sb.innerHTML=`The ${cfg.line||'collection'} collection is live at <b>Designer Wallcoverings</b> — shop all ${ALL.length} designs →`;
- startHero(ALL); chips(fc); colorbar(fc); fillPatterns(fc); render();
+ startHero(ALL); chips(fc); colorbar(fc); fillPatterns(fc);
+ fillSelect('#style', fc.styles, 'All styles'); fillSelect('#motif', fc.motifs, 'All motifs');
+ render();
})();
</script>
</body>
diff --git a/artmura-site/server.js b/artmura-site/server.js
index cf23ca6..9729c7c 100644
--- a/artmura-site/server.js
+++ b/artmura-site/server.js
@@ -60,7 +60,8 @@ function load() {
store_url: liveHandle ? `${CFG.storeBase}/products/${liveHandle}` : CFG.storeBase,
display_eyebrow: nm.eyebrow, display_name: nm.name,
hex: c.hex || null, hue: c.hue ?? null, color_bucket: c.bucket || null,
- sat: c.sat ?? null, val: c.val ?? null,
+ sat: c.sat ?? null, val: c.val ?? null, colorway: c.colorway || null,
+ styles: c.styles || [], motifs: c.motifs || [],
handle: r.handle,
sku: r.mfr_sku,
series: r.pattern_series,
@@ -125,17 +126,21 @@ app.get('/api/pairs/:handle', (req, res) => {
});
app.get('/api/facets', (_req, res) => {
- const books = {}, series = {}, colors = {};
+ const books = {}, series = {}, colors = {}, styles = {}, motifs = {};
for (const p of CATALOG) {
if (p.book) books[p.book] = (books[p.book] || 0) + 1;
if (p.series) series[p.series] = (series[p.series] || 0) + 1;
if (p.color_bucket) colors[p.color_bucket] = (colors[p.color_bucket] || 0) + 1;
+ for (const s of p.styles || []) styles[s] = (styles[s] || 0) + 1;
+ for (const m of p.motifs || []) motifs[m] = (motifs[m] || 0) + 1;
}
res.json({
total: CATALOG.length,
books: Object.entries(books).sort((a, b) => b[1] - a[1]),
series: Object.entries(series).sort((a, b) => b[1] - a[1]),
colors: Object.entries(colors).sort((a, b) => b[1] - a[1]),
+ styles: Object.entries(styles).sort((a, b) => b[1] - a[1]),
+ motifs: Object.entries(motifs).sort((a, b) => b[1] - a[1]),
});
});
@@ -147,4 +152,4 @@ app.use(express.static(path.join(__dirname, 'public')));
app.get('/product/:handle', (_req, res) =>
res.sendFile(path.join(__dirname, 'public', 'product.html')));
-app.listen(PORT, () => console.log(`Artmura site → http://127.0.0.1:${PORT}`));
+app.listen(PORT, () => console.log(`${CFG.line} landing (${CFG.house}) → http://127.0.0.1:${PORT}`));
diff --git a/artmura-site/site.config.js b/artmura-site/site.config.js
index 869ab76..457953a 100644
--- a/artmura-site/site.config.js
+++ b/artmura-site/site.config.js
@@ -119,4 +119,31 @@ module.exports = {
storeBase: 'https://www.designerwallcoverings.com',
palette: { bg: '#edf1ea', paper: '#fafcf5', ink: '#232a20', accent: '#586b4d', gold: '#9d8442', line: '#d7ddcf', taupe: '#a4ae96' }, // Cole & Son — English botanical
},
+
+ romo: {
+ house: 'Designer Wallcoverings',
+ houseUrl: 'https://www.designerwallcoverings.com',
+ houseTagline: 'Designer Wallcoverings & Fabrics · To the Trade',
+ nav: [
+ { label: 'All Wallcoverings', href: 'https://www.designerwallcoverings.com/collections/all' },
+ { label: 'The Collection', href: '#catalog' },
+ { label: 'Trade', href: 'https://www.designerwallcoverings.com/pages/trade' },
+ { label: 'Shop the Collection ↗', href: 'https://www.designerwallcoverings.com/collections/romo' },
+ ],
+ collectionUrl: 'https://www.designerwallcoverings.com/collections/romo',
+ vendor: 'Romo', line: 'Romo',
+ title: 'Romo Wallcoverings | Designer Wallcoverings',
+ wordmark: 'Designer Wallcoverings',
+ eyebrow: 'A Designer Wallcoverings Collection',
+ kicker: 'British Design House · England',
+ tagline: 'Colour-led textures, weaves & wallcoverings — curated by Designer Wallcoverings.',
+ booksHeading: 'The Romo Collections',
+ metaDescription: 'The Romo collection at Designer Wallcoverings — colour-led British wallcoverings, textures and weaves, to the trade.',
+ dataFile: 'artmura-site/lines/romo.json',
+ colorsFile: 'artmura-site/lines/romo-colors.json',
+ handlesFile: 'artmura-site/lines/romo-handles.json',
+ imagePrefix: '', localImages: false,
+ storeBase: 'https://www.designerwallcoverings.com',
+ palette: { bg: '#f6f2ec', paper: '#fcfaf5', ink: '#211d18', accent: '#8c7a5f', gold: '#a98c54', line: '#e2dccf', taupe: '#a9a094' }, // Romo — warm colour-led neutral
+ },
};
← 0510e2b cycle 92: officer vp-dw-commerce CONFIRMED + reorder/guardra
·
back to Dw Yolo Loop
·
Romo ADD canary: create script + 10 DRAFT products on live s e6d0701 →