docs: archive the categories and tags design mockups (#23)

The wireframes behind the storefront filter layout decisions lived only in
.superpowers/brainstorm/, which is gitignored, so they were lost to anyone
reading the repo.

That directory stays ignored — it also holds a brainstorming-session token,
PID files, and absolute local paths, none of which belong in the repo. The
mockups themselves are design artifacts, so they are copied into the specs
directory and wrapped as standalone pages: the tool serves them as fragments
inside its own frame, so its style tokens and toggleSelect helper are inlined
to make them open in a browser with no server and no network.

Both rejected layouts and the rejected mobile variant are kept alongside the
chosen ones — the comparison is the part worth preserving.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 11:12:32 -05:00
co-authored by Claude Opus 5
parent 77e58c0b92
commit e40a3d5e1a
5 changed files with 804 additions and 1 deletions
+1 -1
View File
@@ -182,7 +182,7 @@ sudo docker exec -it redefined-designs-db-syn psql -U redefined -d redefined -c
- **Thom does the pushing.** Commit locally and stop; don't `git push` on his behalf. - **Thom does the pushing.** Commit locally and stop; don't `git push` on his behalf.
- **When work comes from a Gitea issue, post every clarifying question and its answer back to that issue as a comment** — including the options considered and why the rejected ones were rejected. The issue is the durable record; decisions made in a chat session are invisible to anyone reading it later. Post each round as the answers come in rather than batching everything to the end. - **When work comes from a Gitea issue, post every clarifying question and its answer back to that issue as a comment** — including the options considered and why the rejected ones were rejected. The issue is the durable record; decisions made in a chat session are invisible to anyone reading it later. Post each round as the answers come in rather than batching everything to the end.
- **Design specs live in `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`** and are committed before implementation starts. - **Design specs live in `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`** and are committed before implementation starts.
- `.superpowers/` (scratch output from brainstorming tooling) is gitignored. - **`.superpowers/` stays gitignored, but design artifacts inside it must be lifted out before they are lost.** That directory is scratch state belonging to the brainstorming tool and contains a session token, PID files, and absolute local paths — none of which belong in the repo. The mockups it holds *are* worth keeping, so copy them into `docs/superpowers/specs/<date>-<topic>-mockups/` and wrap them as standalone pages (they are served as fragments inside a tool-provided frame, so they need its style tokens and `toggleSelect` helper inlined to open on their own). Keep the rejected options, not just the chosen one — the value is in the comparison.
## Testing ## Testing
@@ -40,6 +40,10 @@ The issue's phrase "rules that dictate how the app automatically organizes items
resolved to mean manual tree assignment, matching its own follow-on sentence that categories are resolved to mean manual tree assignment, matching its own follow-on sentence that categories are
"only metadata for organizing the items into a tree-like structure." "only metadata for organizing the items into a tree-like structure."
The layout decisions were made against wireframes, archived alongside this spec in
[`2026-08-17-categories-and-tags-mockups/`](2026-08-17-categories-and-tags-mockups/) — they record
the rejected options (sidebar rail, dropdown row, bottom sheet) as well as the chosen one.
## Schema ## Schema
One new migration, created with `npm run migrate:create -- add-categories-and-tags`. One new migration, created with `npm run migrate:create -- add-categories-and-tags`.
@@ -0,0 +1,27 @@
# Categories and tags — design mockups
Wireframes produced while designing [issue #23](https://gitea.bermudalamb.synology.me/bermudalamb/redefined-designs/issues/23),
kept because they record the options that were rejected, not just the one that shipped.
Open either file directly in a browser — they are self-contained, with no server,
build step, or network access required.
| File | Question it answered | Outcome |
| --- | --- | --- |
| `filter-layout.html` | Where do the storefront's category / tag / price filters live? | **C — drawer plus removable chips.** A was a permanent sidebar rail (most discoverable, but costs ~25% of grid width); B was a row of dropdowns (full width, but hides the category tree until opened). |
| `filter-layout-mobile.html` | How does that drawer arrive on a phone? | **C1 — side drawer**, entering from the right at near full-screen. C2 was a bottom sheet: better thumb reach, but a deep category tree gets cramped. |
The reasoning behind each choice is in
[the design spec](../2026-08-17-categories-and-tags-design.md), and the
question-and-answer trail is on the issue itself.
## Why these are copies
These began as content fragments inside `.superpowers/brainstorm/`, a scratch
directory belonging to the brainstorming tool. That directory is gitignored and
should stay that way — it also holds a session token, PID files, and absolute
local paths, none of which belong in the repository.
The mockups themselves are design artifacts, so they were lifted out and wrapped
into standalone pages: the frame's style tokens and its `toggleSelect` helper are
inlined here, which is the only difference from what was reviewed.
@@ -0,0 +1,378 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mobile drawer variants</title>
<!--
Archived design mockup from the brainstorming session for issue #23.
Originally a content fragment served inside a tool-provided frame; the
frame's tokens and helper are inlined below so the file opens standalone
in any browser with no server and no network access.
-->
<style>
:root {
--border: #d9d9d9;
--bg: #ffffff;
--bg-elevated: #ffffff;
--text: #1a1a1a;
--accent: #3b82f6;
}
@media (prefers-color-scheme: dark) {
:root {
--border: #3a3a3a;
--bg: #141414;
--bg-elevated: #1d1d1d;
--text: #e8e8e8;
--accent: #60a5fa;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 32px;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.5;
max-width: 1400px;
margin-inline: auto;
}
h2 { margin: 0 0 8px; font-size: 24px; }
h3 { margin: 0 0 6px; font-size: 16px; }
h4 { margin: 0; }
.subtitle { margin: 0 0 24px; opacity: .75; font-size: 15px; }
.section { margin-bottom: 24px; }
.label {
font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
opacity: .6; font-weight: 700; margin: 0 0 6px;
}
code {
background: rgba(128,128,128,.14); padding: 1px 5px;
border-radius: 4px; font-size: .9em;
}
.archive-note {
border: 1px solid var(--border); border-left: 3px solid var(--accent);
border-radius: 6px; padding: 12px 16px; margin-bottom: 28px;
font-size: 14px; background: var(--bg-elevated);
}
</style>
</head>
<body>
<div class="archive-note">
<strong>Archived mockup — Mobile drawer variants.</strong>
Produced while designing
<a href="https://gitea.bermudalamb.synology.me/bermudalamb/redefined-designs/issues/23">issue #23</a>.
The option marked as chosen is recorded in
<a href="../2026-08-17-categories-and-tags-design.md">the design spec</a>.
</div>
<style>
.mb-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; align-items: start; justify-items: center; }
@media (max-width: 1000px) { .mb-row { grid-template-columns: 1fr; } }
.mb-col { width: 100%; max-width: 300px; }
.mb-cap { text-align: center; font-weight: 700; font-size: 14px; margin-bottom: 4px; }
.mb-sub { text-align: center; font-size: 12px; opacity: .7; margin-bottom: 12px; line-height: 1.5; min-height: 34px; }
/* phone chrome */
.phone {
border: 8px solid #2a2a2a; border-radius: 26px;
background: var(--bg-elevated, #fff);
overflow: hidden; position: relative;
height: 500px;
box-shadow: 0 6px 18px rgba(0,0,0,.22);
}
.phone-notch {
position: absolute; top: 0; left: 50%; transform: translateX(-50%);
width: 84px; height: 15px; background: #2a2a2a;
border-radius: 0 0 9px 9px; z-index: 40;
}
.scr { position: absolute; inset: 0; display: flex; flex-direction: column; font-size: 11px; }
.m-header {
display: flex; align-items: center; justify-content: space-between;
padding: 20px 10px 8px; border-bottom: 1px solid var(--border, #ccc);
background: rgba(128,128,128,.08); flex: 0 0 auto;
}
.m-brand { font-weight: 700; font-size: 12px; }
.m-icons { display: flex; gap: 6px; }
.m-ico {
width: 22px; height: 22px; border: 1px solid var(--border, #ccc); border-radius: 5px;
display: flex; align-items: center; justify-content: center; font-size: 11px;
background: rgba(128,128,128,.06);
}
.m-filterbtn {
margin: 8px 10px 0; padding: 7px; border-radius: 6px;
background: #444; color: #fff; text-align: center; font-weight: 600; font-size: 11px;
display: flex; align-items: center; justify-content: center; gap: 6px;
flex: 0 0 auto;
}
.m-count {
background: #fff; color: #444; border-radius: 9px;
padding: 0 6px; font-size: 10px; font-weight: 700;
}
.m-chips { display: flex; flex-wrap: wrap; gap: 4px; padding: 8px 10px; flex: 0 0 auto; }
.m-chip {
border: 1px solid var(--accent, #3b82f6); color: var(--accent, #3b82f6);
border-radius: 10px; padding: 2px 7px; font-size: 10px; white-space: nowrap;
}
.m-chip.clear { border-color: var(--border, #bbb); opacity: .7; color: inherit; }
.m-scroll { flex: 1; overflow: hidden; padding: 0 10px 10px; }
.m-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.m-card { border: 1px solid var(--border, #ccc); border-radius: 6px; overflow: hidden; }
.m-photo { height: 62px; background: repeating-linear-gradient(45deg, rgba(128,128,128,.14) 0 6px, rgba(128,128,128,.05) 6px 12px); }
.m-meta { padding: 5px 6px; line-height: 1.5; }
.m-meta b { display: block; font-size: 10px; font-weight: 600; }
.m-meta span { font-size: 10px; opacity: .75; }
.m-tagrow { display: flex; gap: 3px; margin-top: 4px; }
.m-tag { border-radius: 8px; padding: 0 5px; font-size: 8px; color: #fff; white-space: nowrap; }
/* drawer variants */
.m-scrim { position: absolute; inset: 0; background: rgba(0,0,0,.45); z-index: 20; }
.m-sheet-right {
position: absolute; top: 0; right: 0; bottom: 0; left: 10%;
background: var(--bg-elevated, #fff); z-index: 30;
display: flex; flex-direction: column;
box-shadow: -8px 0 18px rgba(0,0,0,.28);
}
.m-sheet-bottom {
position: absolute; left: 0; right: 0; bottom: 0; height: 78%;
background: var(--bg-elevated, #fff); z-index: 30;
border-radius: 14px 14px 0 0;
display: flex; flex-direction: column;
box-shadow: 0 -8px 18px rgba(0,0,0,.28);
}
.m-grab { width: 34px; height: 4px; border-radius: 2px; background: rgba(128,128,128,.5); margin: 7px auto 0; }
.m-sheet-head {
display: flex; align-items: center; justify-content: space-between;
padding: 10px; border-bottom: 1px solid var(--border, #ccc); font-weight: 700; font-size: 12px;
flex: 0 0 auto;
}
.m-sheet-body { flex: 1; overflow: hidden; padding: 10px; }
.m-sheet-foot {
display: flex; gap: 8px; padding: 9px 10px;
border-top: 1px solid var(--border, #ccc); flex: 0 0 auto;
}
.m-foot-btn {
flex: 1; text-align: center; padding: 7px; border-radius: 6px; font-size: 11px; font-weight: 600;
border: 1px solid var(--border, #ccc);
}
.m-foot-btn.primary { background: #444; color: #fff; border-color: #444; }
.m-lab {
font-size: 9px; letter-spacing: .07em; text-transform: uppercase;
opacity: .6; font-weight: 700; margin: 12px 0 5px;
}
.m-lab:first-child { margin-top: 0; }
.m-tree { line-height: 2; font-size: 11px; }
.m-tree .on { font-weight: 700; background: rgba(59,130,246,.2); border-radius: 4px; padding: 1px 5px; }
.m-tree .muted { opacity: .6; }
.m-pills { display: flex; flex-wrap: wrap; gap: 5px; }
.m-pill { border-radius: 10px; padding: 3px 9px; font-size: 10px; border: 1px solid transparent; }
.m-pill.off { background: rgba(128,128,128,.12); border-color: var(--border, #ccc); opacity: .8; }
.m-slider { padding: 8px 4px 0; }
.m-track { height: 4px; background: rgba(128,128,128,.3); border-radius: 2px; position: relative; }
.m-fill { position: absolute; left: 18%; right: 28%; top: 0; bottom: 0; background: var(--accent, #3b82f6); border-radius: 2px; }
.m-knob { position: absolute; top: -5px; width: 14px; height: 14px; border-radius: 50%; background: #fff; border: 3px solid var(--accent, #3b82f6); }
.m-range { display: flex; justify-content: space-between; font-size: 10px; margin-top: 8px; opacity: .85; }
.pick { border: 2px solid transparent; border-radius: 12px; padding: 6px; cursor: pointer; transition: border-color .15s; }
.pick:hover { border-color: rgba(128,128,128,.4); }
.pick.selected { border-color: var(--accent, #3b82f6); }
.magenta { background: #c41d7f; color: #fff; }
.green { background: #389e0d; color: #fff; }
.blue { background: #096dd9; color: #fff; }
</style>
<h2>Option C on a phone — and one sub-choice</h2>
<p class="subtitle">
The closed state is the same either way. What differs is how the filter panel arrives: a
<b>full-height side drawer</b> or a <b>bottom sheet</b>. Both are one antd <code>Drawer</code> with a
different <code>placement</code>, so this is purely a feel decision. Click the drawer style you prefer.
</p>
<div class="mb-row">
<!-- ===== 1. CLOSED STATE ===== -->
<div class="mb-col">
<div class="mb-cap">Closed — browsing</div>
<div class="mb-sub">Full-width button, applied filters as removable chips, two cards per row.</div>
<div class="phone">
<div class="phone-notch"></div>
<div class="scr">
<div class="m-header">
<span class="m-brand">Redefined Designs</span>
<span class="m-icons"><span class="m-ico"></span><span class="m-ico">🛒</span><span class="m-ico"></span></span>
</div>
<div class="m-filterbtn">⚙ Filters <span class="m-count">3</span></div>
<div class="m-chips">
<span class="m-chip">Coffee Tables ×</span>
<span class="m-chip">vintage ×</span>
<span class="m-chip">$120$800 ×</span>
<span class="m-chip clear">Clear all</span>
</div>
<div class="m-scroll">
<div class="m-grid">
<div class="m-card"><div class="m-photo"></div><div class="m-meta"><b>Oak coffee table</b><span>$340</span>
<div class="m-tagrow"><span class="m-tag magenta">vintage</span><span class="m-tag green">oak</span></div></div></div>
<div class="m-card"><div class="m-photo"></div><div class="m-meta"><b>Elm slab</b><span>$520</span>
<div class="m-tagrow"><span class="m-tag magenta">vintage</span></div></div></div>
<div class="m-card"><div class="m-photo"></div><div class="m-meta"><b>Walnut low</b><span>$690</span>
<div class="m-tagrow"><span class="m-tag magenta">vintage</span><span class="m-tag blue">rare</span></div></div></div>
<div class="m-card"><div class="m-photo"></div><div class="m-meta"><b>Teak round</b><span>$275</span>
<div class="m-tagrow"><span class="m-tag magenta">vintage</span></div></div></div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== 2. SIDE DRAWER ===== -->
<div class="mb-col pick" data-choice="side" onclick="toggleSelect(this)">
<div class="mb-cap">C1 — Side drawer</div>
<div class="mb-sub">Slides in from the right, near full-screen. Reads as "a page of filters".</div>
<div class="phone">
<div class="phone-notch"></div>
<div class="scr">
<div class="m-header">
<span class="m-brand">Redefined Designs</span>
<span class="m-icons"><span class="m-ico"></span><span class="m-ico">🛒</span><span class="m-ico"></span></span>
</div>
<div class="m-scroll" style="margin-top:8px;">
<div class="m-grid">
<div class="m-card"><div class="m-photo"></div><div class="m-meta"><b>Oak coffee table</b><span>$340</span></div></div>
<div class="m-card"><div class="m-photo"></div><div class="m-meta"><b>Elm slab</b><span>$520</span></div></div>
</div>
</div>
<div class="m-scrim"></div>
<div class="m-sheet-right">
<div class="m-sheet-head"><span>Filters</span><span></span></div>
<div class="m-sheet-body">
<div class="m-lab">Category</div>
<div class="m-tree">
<div class="muted">All items</div>
<div>▾ Furniture</div>
<div>&nbsp;&nbsp;▾ Tables</div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;<span class="on">Coffee Tables</span></div>
<div class="muted">&nbsp;&nbsp;&nbsp;&nbsp;Side Tables</div>
<div class="muted">▸ Decor</div>
</div>
<div class="m-lab">Tags — must have all</div>
<div class="m-pills">
<span class="m-pill magenta">vintage ✓</span>
<span class="m-pill off">handmade</span>
<span class="m-pill off">oak</span>
<span class="m-pill off">restored</span>
</div>
<div class="m-lab">Price</div>
<div class="m-slider">
<div class="m-track"><div class="m-fill"></div>
<div class="m-knob" style="left:18%"></div><div class="m-knob" style="left:calc(72% - 14px)"></div>
</div>
<div class="m-range"><span>$120</span><span>$800</span></div>
</div>
</div>
<div class="m-sheet-foot">
<div class="m-foot-btn">Clear all</div>
<div class="m-foot-btn primary">Show 12 items</div>
</div>
</div>
</div>
</div>
</div>
<!-- ===== 3. BOTTOM SHEET ===== -->
<div class="mb-col pick" data-choice="bottom" onclick="toggleSelect(this)">
<div class="mb-cap">C2 — Bottom sheet</div>
<div class="mb-sub">Rises from the bottom, ~78% height. Controls sit within thumb reach; grid stays partly visible.</div>
<div class="phone">
<div class="phone-notch"></div>
<div class="scr">
<div class="m-header">
<span class="m-brand">Redefined Designs</span>
<span class="m-icons"><span class="m-ico"></span><span class="m-ico">🛒</span><span class="m-ico"></span></span>
</div>
<div class="m-filterbtn">⚙ Filters <span class="m-count">3</span></div>
<div class="m-scroll" style="margin-top:8px;">
<div class="m-grid">
<div class="m-card"><div class="m-photo"></div><div class="m-meta"><b>Oak coffee table</b><span>$340</span></div></div>
<div class="m-card"><div class="m-photo"></div><div class="m-meta"><b>Elm slab</b><span>$520</span></div></div>
</div>
</div>
<div class="m-scrim"></div>
<div class="m-sheet-bottom">
<div class="m-grab"></div>
<div class="m-sheet-head"><span>Filters</span><span></span></div>
<div class="m-sheet-body">
<div class="m-lab">Category</div>
<div class="m-tree">
<div class="muted">All items</div>
<div>▾ Furniture &nbsp;▸ Tables</div>
<div>&nbsp;&nbsp;<span class="on">Coffee Tables</span></div>
<div class="muted">▸ Decor</div>
</div>
<div class="m-lab">Tags — must have all</div>
<div class="m-pills">
<span class="m-pill magenta">vintage ✓</span>
<span class="m-pill off">handmade</span>
<span class="m-pill off">oak</span>
</div>
<div class="m-lab">Price</div>
<div class="m-slider">
<div class="m-track"><div class="m-fill"></div>
<div class="m-knob" style="left:18%"></div><div class="m-knob" style="left:calc(72% - 14px)"></div>
</div>
<div class="m-range"><span>$120</span><span>$800</span></div>
</div>
</div>
<div class="m-sheet-foot">
<div class="m-foot-btn">Clear all</div>
<div class="m-foot-btn primary">Show 12 items</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="section" style="margin-top:26px;">
<p class="label">What carries over from desktop</p>
<p style="font-size:14px;line-height:1.7;opacity:.88;">
Identical component tree at both sizes — the drawer just changes <code>placement</code> and
<code>width</code>/<code>height</code> at the antd <code>md</code> breakpoint, so there is no second
implementation to keep in sync. The chip row is the only piece that behaves differently: on desktop it
sits inline next to the Filters button, on mobile it wraps onto its own line beneath it.
</p>
<p style="font-size:14px;line-height:1.7;opacity:.88;">
<b>Tags now confirmed as AND</b> — an item must carry <i>every</i> selected tag to show up. The panel
labels this explicitly ("must have all") so a user who selects two tags and sees the grid shrink to
nothing understands why.
</p>
</div>
<script>
// The original frame recorded clicks back to the tool. Here it only toggles
// the highlight, so the archived mockup stays explorable offline.
function toggleSelect(el) {
var multi = el.parentElement && el.parentElement.hasAttribute('data-multiselect');
if (!multi) {
Array.prototype.forEach.call(
el.parentElement.children,
function (sibling) { if (sibling !== el) sibling.classList.remove('selected'); }
);
}
el.classList.toggle('selected');
}
</script>
</body>
</html>
@@ -0,0 +1,394 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Storefront filter layout options</title>
<!--
Archived design mockup from the brainstorming session for issue #23.
Originally a content fragment served inside a tool-provided frame; the
frame's tokens and helper are inlined below so the file opens standalone
in any browser with no server and no network access.
-->
<style>
:root {
--border: #d9d9d9;
--bg: #ffffff;
--bg-elevated: #ffffff;
--text: #1a1a1a;
--accent: #3b82f6;
}
@media (prefers-color-scheme: dark) {
:root {
--border: #3a3a3a;
--bg: #141414;
--bg-elevated: #1d1d1d;
--text: #e8e8e8;
--accent: #60a5fa;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 32px;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.5;
max-width: 1400px;
margin-inline: auto;
}
h2 { margin: 0 0 8px; font-size: 24px; }
h3 { margin: 0 0 6px; font-size: 16px; }
h4 { margin: 0; }
.subtitle { margin: 0 0 24px; opacity: .75; font-size: 15px; }
.section { margin-bottom: 24px; }
.label {
font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
opacity: .6; font-weight: 700; margin: 0 0 6px;
}
code {
background: rgba(128,128,128,.14); padding: 1px 5px;
border-radius: 4px; font-size: .9em;
}
.archive-note {
border: 1px solid var(--border); border-left: 3px solid var(--accent);
border-radius: 6px; padding: 12px 16px; margin-bottom: 28px;
font-size: 14px; background: var(--bg-elevated);
}
</style>
</head>
<body>
<div class="archive-note">
<strong>Archived mockup — Storefront filter layout options.</strong>
Produced while designing
<a href="https://gitea.bermudalamb.synology.me/bermudalamb/redefined-designs/issues/23">issue #23</a>.
The option marked as chosen is recorded in
<a href="../2026-08-17-categories-and-tags-design.md">the design spec</a>.
</div>
<style>
.fl-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; align-items: start; }
@media (max-width: 1100px) { .fl-grid { grid-template-columns: 1fr; } }
.fl-card {
border: 2px solid var(--border, #d0d0d0);
border-radius: 10px;
overflow: hidden;
cursor: pointer;
transition: border-color .15s, transform .15s;
background: var(--bg-elevated, #fff);
}
.fl-card:hover { transform: translateY(-2px); }
.fl-card.selected { border-color: var(--accent, #3b82f6); }
.fl-head {
padding: 10px 14px;
border-bottom: 1px solid var(--border, #d0d0d0);
display: flex; align-items: center; gap: 10px;
font-weight: 600;
}
.fl-badge {
width: 22px; height: 22px; flex: 0 0 22px;
border-radius: 50%;
background: var(--accent, #3b82f6); color: #fff;
display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 700;
}
.fl-body { padding: 12px; }
.fl-note { padding: 0 14px 14px; font-size: 13px; line-height: 1.5; opacity: .85; }
/* --- wireframe primitives, scaled small so 3 fit side by side --- */
.wf { border: 1px solid var(--border, #ccc); border-radius: 6px; overflow: hidden; font-size: 10px; }
.wf-header {
display: flex; align-items: center; justify-content: space-between;
padding: 6px 8px; border-bottom: 1px solid var(--border, #ccc);
background: rgba(128,128,128,.10);
}
.wf-brand { font-weight: 700; font-size: 11px; }
.wf-actions { display: flex; gap: 4px; }
.wf-btn {
border: 1px solid var(--border, #ccc); border-radius: 4px;
padding: 2px 6px; font-size: 9px; background: rgba(128,128,128,.06);
white-space: nowrap;
}
.wf-btn.solid { background: #555; color: #fff; border-color: #555; }
.wf-main { display: flex; gap: 8px; padding: 8px; }
.wf-rail { flex: 0 0 33%; border-right: 1px dashed var(--border, #ccc); padding-right: 8px; }
.wf-section-label {
font-size: 8px; letter-spacing: .06em; text-transform: uppercase;
opacity: .6; margin: 8px 0 4px; font-weight: 700;
}
.wf-section-label:first-child { margin-top: 0; }
.wf-tree { line-height: 1.7; }
.wf-tree div { white-space: nowrap; }
.wf-tree .on { font-weight: 700; background: rgba(59,130,246,.18); border-radius: 3px; padding: 0 3px; }
.wf-tree .muted { opacity: .6; }
.wf-pills { display: flex; flex-wrap: wrap; gap: 3px; }
.wf-pill {
border-radius: 9px; padding: 1px 6px; font-size: 9px;
border: 1px solid transparent; white-space: nowrap;
}
.wf-pill.off { background: rgba(128,128,128,.12); border-color: var(--border, #ccc); opacity: .75; }
.wf-pill.magenta { background: #c41d7f; color: #fff; }
.wf-pill.green { background: #389e0d; color: #fff; }
.wf-pill.blue { background: #096dd9; color: #fff; }
.wf-pill.orange { background: #d46b08; color: #fff; }
.wf-slider { padding: 6px 2px 2px; }
.wf-track { height: 3px; background: rgba(128,128,128,.3); border-radius: 2px; position: relative; }
.wf-fill { position: absolute; left: 20%; right: 30%; top: 0; bottom: 0; background: var(--accent, #3b82f6); border-radius: 2px; }
.wf-knob { position: absolute; top: -3px; width: 9px; height: 9px; border-radius: 50%; background: #fff; border: 2px solid var(--accent, #3b82f6); }
.wf-range { display: flex; justify-content: space-between; font-size: 9px; margin-top: 5px; opacity: .8; }
.wf-grid { flex: 1; display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; align-content: start; }
.wf-grid.wide { grid-template-columns: repeat(4, 1fr); }
.wf-item { border: 1px solid var(--border, #ccc); border-radius: 4px; overflow: hidden; }
.wf-photo {
height: 34px; background: repeating-linear-gradient(45deg, rgba(128,128,128,.13) 0 5px, rgba(128,128,128,.05) 5px 10px);
}
.wf-meta { padding: 3px 4px; font-size: 8px; line-height: 1.4; }
.wf-meta b { display: block; font-weight: 600; }
.wf-meta span { opacity: .7; }
.wf-tagrow { display: flex; gap: 2px; margin-top: 2px; }
.wf-dot { width: 5px; height: 5px; border-radius: 50%; }
.wf-bar {
display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
padding: 7px 8px; border-bottom: 1px solid var(--border, #ccc);
}
.wf-select {
border: 1px solid var(--border, #ccc); border-radius: 4px;
padding: 3px 6px; font-size: 9px; background: rgba(128,128,128,.06);
display: flex; align-items: center; gap: 5px; white-space: nowrap;
}
.wf-caret { opacity: .5; font-size: 8px; }
.wf-chiprow { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; padding: 6px 8px; border-bottom: 1px dashed var(--border, #ccc); }
.wf-chip {
border: 1px solid var(--accent, #3b82f6); color: var(--accent, #3b82f6);
border-radius: 9px; padding: 1px 6px; font-size: 9px; white-space: nowrap;
}
.wf-drawer {
position: absolute; top: 0; right: 0; bottom: 0; width: 44%;
background: var(--bg-elevated, #fff);
border-left: 2px solid var(--accent, #3b82f6);
box-shadow: -6px 0 14px rgba(0,0,0,.18);
padding: 8px;
}
.wf-scrim { position: absolute; inset: 0; background: rgba(0,0,0,.22); }
.wf-rel { position: relative; }
</style>
<h2>Storefront filter layout — which shape fits?</h2>
<p class="subtitle">
Three filters land on the storefront: <b>category</b> (a tree), <b>tags</b> (multi-select, color-coded),
and <b>price range</b>. They differ mainly in how much screen they take from the item grid, and how well
they survive on a phone. Click the one you prefer.
</p>
<div class="fl-grid">
<!-- ============ A: SIDEBAR RAIL ============ -->
<div class="fl-card" data-choice="a" onclick="toggleSelect(this)">
<div class="fl-head"><span class="fl-badge">A</span> Sidebar filter rail</div>
<div class="fl-body">
<div class="wf">
<div class="wf-header">
<span class="wf-brand">Redefined Designs</span>
<span class="wf-actions">
<span class="wf-btn">Dark</span><span class="wf-btn">Cart</span><span class="wf-btn solid">Sign up</span>
</span>
</div>
<div class="wf-main">
<div class="wf-rail">
<div class="wf-section-label">Category</div>
<div class="wf-tree">
<div class="muted">All items</div>
<div>▾ Furniture</div>
<div>&nbsp;&nbsp;▾ Tables</div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;<span class="on">Coffee</span></div>
<div class="muted">&nbsp;&nbsp;&nbsp;&nbsp;Side</div>
<div class="muted">▸ Decor</div>
</div>
<div class="wf-section-label">Tags</div>
<div class="wf-pills">
<span class="wf-pill magenta">vintage</span>
<span class="wf-pill off">handmade</span>
<span class="wf-pill off">oak</span>
<span class="wf-pill off">restored</span>
</div>
<div class="wf-section-label">Price</div>
<div class="wf-slider">
<div class="wf-track"><div class="wf-fill"></div>
<div class="wf-knob" style="left:20%"></div><div class="wf-knob" style="left:calc(70% - 9px)"></div>
</div>
<div class="wf-range"><span>$120</span><span>$800</span></div>
</div>
</div>
<div class="wf-grid">
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Oak table</b><span>$340</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i><i class="wf-dot" style="background:#389e0d"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Elm slab</b><span>$520</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Walnut</b><span>$690</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i><i class="wf-dot" style="background:#096dd9"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Teak low</b><span>$275</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Ash round</b><span>$410</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i><i class="wf-dot" style="background:#d46b08"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Pine box</b><span>$180</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i></div></div></div>
</div>
</div>
</div>
</div>
<div class="fl-note">
Everything visible at once — the tree reads as actual folders, tag colors are on display, no clicking to
discover what's filterable. Costs ~25% of the grid width, so cards drop from 4 to 3 per row on desktop.
On mobile the rail has to collapse into an accordion above the grid.
</div>
</div>
<!-- ============ B: HORIZONTAL BAR ============ -->
<div class="fl-card" data-choice="b" onclick="toggleSelect(this)">
<div class="fl-head"><span class="fl-badge">B</span> Horizontal filter bar</div>
<div class="fl-body">
<div class="wf">
<div class="wf-header">
<span class="wf-brand">Redefined Designs</span>
<span class="wf-actions">
<span class="wf-btn">Dark</span><span class="wf-btn">Cart</span><span class="wf-btn solid">Sign up</span>
</span>
</div>
<div class="wf-bar">
<span class="wf-select">Furniture / Tables / Coffee <span class="wf-caret"></span></span>
<span class="wf-select">
<span class="wf-pill magenta">vintage</span><span class="wf-pill green">oak</span>
<span class="wf-caret"></span>
</span>
<span class="wf-select">$120 $800 <span class="wf-caret"></span></span>
<span class="wf-btn">Clear</span>
</div>
<div class="wf-main">
<div class="wf-grid wide">
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Oak table</b><span>$340</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i><i class="wf-dot" style="background:#389e0d"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Elm slab</b><span>$520</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Walnut</b><span>$690</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i><i class="wf-dot" style="background:#096dd9"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Teak low</b><span>$275</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Ash round</b><span>$410</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i><i class="wf-dot" style="background:#d46b08"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Pine box</b><span>$180</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Cedar</b><span>$230</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#389e0d"></i></div></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Maple</b><span>$610</span>
<div class="wf-tagrow"><i class="wf-dot" style="background:#c41d7f"></i></div></div></div>
</div>
</div>
</div>
</div>
<div class="fl-note">
Three compact controls in one row — an antd <i>Cascader</i> for the category tree, a tag multi-select,
and a price-range dropdown. Full grid width is preserved (4 cards per row), and the same row wraps
naturally on a phone. The tree is only visible once you open the cascader, so the category structure
is less discoverable.
</div>
</div>
<!-- ============ C: DRAWER + CHIPS ============ -->
<div class="fl-card" data-choice="c" onclick="toggleSelect(this)">
<div class="fl-head"><span class="fl-badge">C</span> Drawer + active-filter chips</div>
<div class="fl-body">
<div class="wf wf-rel">
<div class="wf-header">
<span class="wf-brand">Redefined Designs</span>
<span class="wf-actions">
<span class="wf-btn solid">Filters (3)</span><span class="wf-btn">Cart</span>
</span>
</div>
<div class="wf-chiprow">
<span class="wf-chip">Coffee Tables ×</span>
<span class="wf-chip">vintage ×</span>
<span class="wf-chip">$120$800 ×</span>
</div>
<div class="wf-main">
<div class="wf-grid wide">
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Oak table</b><span>$340</span></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Elm slab</b><span>$520</span></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Walnut</b><span>$690</span></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Teak low</b><span>$275</span></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Ash round</b><span>$410</span></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Pine box</b><span>$180</span></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Cedar</b><span>$230</span></div></div>
<div class="wf-item"><div class="wf-photo"></div><div class="wf-meta"><b>Maple</b><span>$610</span></div></div>
</div>
</div>
<div class="wf-scrim"></div>
<div class="wf-drawer">
<div style="font-weight:700;font-size:11px;margin-bottom:6px;">Filters</div>
<div class="wf-section-label">Category</div>
<div class="wf-tree">
<div class="muted">All items</div>
<div>▾ Furniture</div>
<div>&nbsp;&nbsp;▾ Tables</div>
<div>&nbsp;&nbsp;&nbsp;&nbsp;<span class="on">Coffee</span></div>
<div class="muted">▸ Decor</div>
</div>
<div class="wf-section-label">Tags</div>
<div class="wf-pills">
<span class="wf-pill magenta">vintage</span>
<span class="wf-pill off">handmade</span>
<span class="wf-pill off">oak</span>
</div>
<div class="wf-section-label">Price</div>
<div class="wf-slider">
<div class="wf-track"><div class="wf-fill"></div>
<div class="wf-knob" style="left:20%"></div><div class="wf-knob" style="left:calc(70% - 9px)"></div>
</div>
<div class="wf-range"><span>$120</span><span>$800</span></div>
</div>
</div>
</div>
</div>
<div class="fl-note">
A "Filters" button opens a slide-over panel holding the full tree, tag pills, and price slider; what's
currently applied stays visible as removable chips above the grid. Full grid width, and identical
behaviour on desktop and mobile. Trade-off: filtering is a two-step interaction, and nothing hints at
the category structure until you open the drawer.
</div>
</div>
</div>
<div class="section" style="margin-top:22px;">
<p class="label">Same for all three</p>
<p style="font-size:14px;line-height:1.6;opacity:.85;">
Filters combine with AND (category <b>and</b> tags <b>and</b> price). Multiple selected tags are the one
open sub-question — "has <i>any</i> of these tags" vs "has <i>all</i> of them" — I'll ask that next.
Filtering happens server-side via query params on <code>/api/items</code>, so the result set stays correct
no matter how many items exist.
</p>
</div>
<script>
// The original frame recorded clicks back to the tool. Here it only toggles
// the highlight, so the archived mockup stays explorable offline.
function toggleSelect(el) {
var multi = el.parentElement && el.parentElement.hasAttribute('data-multiselect');
if (!multi) {
Array.prototype.forEach.call(
el.parentElement.children,
function (sibling) { if (sibling !== el) sibling.classList.remove('selected'); }
);
}
el.classList.toggle('selected');
}
</script>
</body>
</html>