Operator (printableprompts.com, 481 products) reported the bulk tagger
'still refreshing the whole screen' and 'dragging tags doesn't work'
after 2.7. Two real defects the 2.7 static audit missed:
1. Tag-focus was a full-page navigation: clicking a tag chip is
<a href=?focus=slug>, and the view loaded+rendered ALL products on
EVERY GET. On a 481-product catalog every tag click reloaded a
multi-MB page. The forms were AJAX; the dominant workflow was not.
2. Drag-to-reorder never existed: shop_tags.j2 shipped draggable=true +
a handle + help text, but tag_bulk.js had ZERO drag handlers.
Fix:
- shop.py:shop_tags — all_products loads only when focus_tag or
show_suggestions (bare GET is light). New AJAX branch: is_ajax +
?focus=slug -> JSON {focus, products:[{id,title,url,attached}]}.
- shop_tags.j2 — stable [data-focus-section] (always in DOM, hidden
until focused); ?focus= chips carry data-tag-focus-link. No-JS
unchanged (real navigation, server renders the section).
- tag_bulk.js — wireFocusLinks() intercepts chip clicks, fetchFocus()
+ renderFocus() swap the list in place, active-chip + history
pushState/popstate, real-navigation fallback. wireDragAndDrop()
HTML5 DnD -> persistOrder() POSTs action=set_order&tag_slugs=…
(view already supported it) + re-syncs up/down disabled states.
.tag-list-dragging CSS added.
- Tests: TestProductTagsSpa +4 (ajax focus json, unknown-slug null,
set_order persists positions, bare GET no catalog). 1128 passed.
Docs: mps-24.md Phase 2.8, architecture.md, design-system.md, CLAUDE.md.
Deferred: AJAX 'Suggest categories' link (occasional click, not hot path).
357 lines
18 KiB
Markdown
357 lines
18 KiB
Markdown
# MPS Design System
|
||
|
||
## Overview
|
||
|
||
Make Post Sell uses a design token architecture with CSS custom properties as the single source of truth. Mobile-first. CSS Grid only (no Flexbox). Accessible. Respects reduced motion. Supports light and dark themes.
|
||
|
||
## Architecture Diagram
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ tokens.css │
|
||
│ Design Tokens (:root) │
|
||
│ │
|
||
│ Colors · Typography · Spacing · Shape · Elevation │
|
||
│ Motion · Z-index · Layout · State overlays │
|
||
│ │
|
||
│ [data-theme="dark"] overrides │
|
||
│ │
|
||
│ Base resets · Focus · Selection │
|
||
│ Typography utilities (.type-*) │
|
||
│ Elevation utilities (.elevation-*) │
|
||
│ Surface utilities (.surface-*) │
|
||
│ State layer · Ripple effect │
|
||
│ Animations · Skeleton loading · Spinner │
|
||
│ Spacing utilities (.mt-*, .mb-*, .p-*, .gap-*) │
|
||
│ Container utilities (.container, -narrow, -wide) │
|
||
└──────────────────────┬──────────────────────────────────┘
|
||
│ consumed by
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ common.css │
|
||
│ Component Styles │
|
||
│ │
|
||
│ Body layout · Navigation · Forms · Buttons │
|
||
│ Product grid · Cards · Cart · Comments │
|
||
│ Wells · Alerts · Ribbons · Footer │
|
||
│ Watch mode · Landing page · Login │
|
||
│ Responsive breakpoints │
|
||
│ │
|
||
│ References tokens via var(--token-name, fallback) │
|
||
└──────────────────────┬──────────────────────────────────┘
|
||
│ rendered in
|
||
▼
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ templates/ │
|
||
│ Jinja2 Templates │
|
||
│ │
|
||
│ base.j2 → theme toggle, nav, footer │
|
||
│ styleguide.j2 → live component reference │
|
||
│ product.j2, content.j2, shop.j2, etc. │
|
||
└─────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## File Map
|
||
|
||
| File | Lines | Purpose |
|
||
|------|-------|---------|
|
||
| `static/css/tokens.css` | 775 | Design tokens, utilities, animations, resets |
|
||
| `static/css/common.css` | 3879 | Component styles consuming tokens |
|
||
| `templates/styleguide.j2` | 1083 | Live styleguide at `/styleguide` |
|
||
| `views/misc.py:23` | — | Styleguide view controller |
|
||
|
||
## Token Categories
|
||
|
||
### Colors
|
||
|
||
```
|
||
Brand Surface Text Border
|
||
────────── ────────── ────────── ──────────
|
||
--color-green --surface-base --text-primary --border-default
|
||
--color-blue --surface-dim --text-body --border-light
|
||
--color-navy --surface-container --text-secondary --border-focus
|
||
--color-purple --surface-container- --text-muted --border-error
|
||
--color-danger high --text-faint
|
||
--color-gold --surface-inverse --text-disabled
|
||
--text-inverse
|
||
```
|
||
|
||
### Typography Scale (Major Third 1.250)
|
||
|
||
```
|
||
Token Size Use
|
||
────────── ────── ──────────────────────
|
||
--text-xs 12px Captions, overlines
|
||
--text-sm 14px Labels, small body
|
||
--text-base 16px Body text (root)
|
||
--text-md 18px Large body
|
||
--text-lg 20px Titles
|
||
--text-xl 24px Title large
|
||
--text-2xl 30px Headline 3
|
||
--text-3xl 36px Headline 2
|
||
--text-4xl 48px Headline 1
|
||
--text-5xl 60px Display
|
||
```
|
||
|
||
### Spacing Scale (4px base)
|
||
|
||
```
|
||
Token Value
|
||
────────── ──────
|
||
--space-0 0
|
||
--space-1 4px
|
||
--space-2 8px
|
||
--space-3 12px
|
||
--space-4 16px
|
||
--space-5 20px
|
||
--space-6 24px
|
||
--space-8 32px
|
||
--space-10 40px
|
||
--space-12 48px
|
||
--space-16 64px
|
||
--space-20 80px
|
||
--space-24 96px
|
||
```
|
||
|
||
### Shape (Border Radius)
|
||
|
||
```
|
||
--radius-none 0 Sharp corners
|
||
--radius-sm 4px Inputs, code blocks
|
||
--radius-md 8px Cards, buttons
|
||
--radius-lg 12px Modals, panels
|
||
--radius-xl 16px Large surfaces
|
||
--radius-2xl 24px Pills
|
||
--radius-full 9999px Circles
|
||
```
|
||
|
||
### Elevation (Box Shadow)
|
||
|
||
```
|
||
--elevation-0 none Flat
|
||
--elevation-1 subtle Cards at rest
|
||
--elevation-2 low Raised cards
|
||
--elevation-3 medium Dropdowns
|
||
--elevation-4 high Modals
|
||
--elevation-5 highest Popovers
|
||
```
|
||
|
||
### Motion
|
||
|
||
```
|
||
Durations Easing
|
||
────────────────── ──────────────────
|
||
--duration-instant 50ms --ease-standard general transitions
|
||
--duration-fast 100ms --ease-decelerate entrances
|
||
--duration-normal 200ms --ease-accelerate exits
|
||
--duration-slow 300ms --ease-emphasize emphasis
|
||
--duration-slower 400ms --ease-spring playful bounce
|
||
--duration-entrance 250ms
|
||
--duration-exit 200ms
|
||
```
|
||
|
||
### Z-Index Scale
|
||
|
||
```
|
||
--z-base 0 Default stacking
|
||
--z-dropdown 100 Dropdowns, popovers
|
||
--z-sticky 200 Sticky headers
|
||
--z-overlay 300 Overlays, backdrops
|
||
--z-modal 400 Modals
|
||
--z-toast 500 Toast notifications
|
||
--z-ribbon 600 Shop ribbon banner
|
||
```
|
||
|
||
### Layout Breakpoints
|
||
|
||
```
|
||
--content-narrow 400px Login forms, narrow content
|
||
--content-width 800px Default content width
|
||
--content-wide 1200px Wide layouts
|
||
--bp-tablet 800px Tablet breakpoint
|
||
--bp-desktop 1200px Desktop breakpoint
|
||
```
|
||
|
||
## Theme System
|
||
|
||
Light mode is default (`:root`). Dark mode activates via `[data-theme="dark"]` on the `<html>` element. The dark theme overrides all semantic tokens — surfaces, text, borders, brand colors — so components adapt automatically without per-component dark mode rules.
|
||
|
||
```
|
||
Light Dark
|
||
────────────────────── ──────────────────────
|
||
--surface-base: #FFFFFF --surface-base: #0d1117
|
||
--text-primary: #333333 --text-primary: #ffffff
|
||
--border-default: #e0e0e0 --border-default: #7ab9ff
|
||
--color-green: #a3c765 --color-green: #08e700
|
||
--color-navy: #5871ad --color-navy: #7ab9ff
|
||
```
|
||
|
||
Theme toggle: `window.setTheme('dark')` / `window.setTheme('light')`.
|
||
|
||
## Typography Utilities
|
||
|
||
CSS classes that compose token values into complete type styles:
|
||
|
||
| Class | Size | Weight | Use |
|
||
|-------|------|--------|-----|
|
||
| `.type-display` | clamp(36px, 5vw, 60px) | bold | Hero headlines |
|
||
| `.type-headline-1` | 48px | bold | Page titles |
|
||
| `.type-headline-2` | 36px | bold | Section titles |
|
||
| `.type-headline-3` | 30px | bold | Subsection titles |
|
||
| `.type-title-lg` | 24px | semibold | Card titles |
|
||
| `.type-title` | 20px | semibold | List titles |
|
||
| `.type-title-sm` | 16px | semibold | Small titles |
|
||
| `.type-body-lg` | 18px | regular | Lead paragraphs |
|
||
| `.type-body` | 16px | regular | Body text |
|
||
| `.type-body-sm` | 14px | regular | Secondary text |
|
||
| `.type-label-lg` | 14px | semibold | Form labels |
|
||
| `.type-label` | 12px | semibold, uppercase | Overline labels |
|
||
| `.type-caption` | 12px | regular | Captions |
|
||
| `.type-overline` | 11px | bold, uppercase | Section overlines |
|
||
| `.type-code` | 0.9em | mono | Inline code |
|
||
|
||
## Component Library
|
||
|
||
All components are documented with live examples at `/styleguide`. The styleguide is the single source of truth for the component library. If it is not in the styleguide, it does not exist as a pattern.
|
||
|
||
### Styleguide Sections
|
||
|
||
| Section | ID | Description |
|
||
|---------|-----|------------|
|
||
| Tokens | `#tokens` | Raw token reference table |
|
||
| Colors | `#colors` | Brand, surface, text, border, alert swatches |
|
||
| Typography | `#typography` | Type scale and utility classes |
|
||
| Elevation | `#elevation` | Shadow levels |
|
||
| Motion | `#motion` | Animations, transitions, easing |
|
||
| Spacing | `#spacing` | Spacing scale visualization |
|
||
| Shape | `#shape` | Border radius samples |
|
||
| States | `#states` | Interactive state layers |
|
||
| Loading | `#loading` | Skeleton and spinner patterns |
|
||
| Buttons | `#buttons` | Button variants (green, blue, red, navy, outline) |
|
||
| Forms | `#forms` | Input fields, textareas, selects, settings-form (`.settings-form` / `.settings-form-grid` / `.settings-field` / `.settings-field-hint`) |
|
||
| Wells | `#wells` | Content wells and containers |
|
||
| Alerts | `#alerts` | Success, info, warning, danger alerts |
|
||
| Status | `#status` | Status indicators |
|
||
| Product Cards | `#cards` | Product grid cards, profile card (`.profile-card-header` / `.profile-avatar` / `.profile-handle` / `.profile-email-reveal`), action button grid (`.action-columns` / `.action-button-grid`), MPS-24 tag chips (`#tag-chips`), MPS-24 sectioned lanes (`#tag-lanes`) |
|
||
| Cart | `#cart` | Cart and checkout components |
|
||
| Gift Cards | `#gift-cards` | Gift card purchase, balance check, management |
|
||
| Comments | `#comments` | Comment form and list |
|
||
| Toggle | `#toggle` | Toggle switches |
|
||
| Ribbon | `#ribbon` | Shop ribbon banner |
|
||
| Environment Banner | `.environment-banner` | Staging/dev environment indicator |
|
||
| Trial Banner | `.trial-banner` | Trial countdown and expiry notice |
|
||
| Task Bar | `#taskbar` | Task bar component |
|
||
| Layout | `#layout` | Grid layout patterns |
|
||
| Theme System | `#theme` | Theme toggle and dark mode |
|
||
| Footer | `#footer` | Footer component |
|
||
|
||
### MPS-20 / MPS-21 components
|
||
|
||
| Class | Where | Description |
|
||
|---|---|---|
|
||
| `.cart-negotiation-card` | `cart.j2` | Soft-green "Offer accepted / Auction won" well at the top of a negotiated cart |
|
||
| `.cart-negotiation-deadline` | `cart.j2` | Pay-by countdown line inside the negotiation card |
|
||
| `.cart-negotiation-pill` | `cart.j2` | "Offer accepted · quantity locked" pill on line items |
|
||
| `.offer-pay-cta-actions` | `offer.j2` | Two-col grid: Cancel left, Pay right (stacks on narrow viewports) |
|
||
| `.offer-pay-deadline-note` | `offer.j2` | Buyer's "Payment due in 23 hours, 14 minutes" copy |
|
||
| `.offer-pay-link-row` | `offer.j2` | Seller's shareable offer-link with Copy button |
|
||
| `.offer-respond-deadline-note` | `offer.j2` | "Respond in 5 days, or this offer auto-expires" |
|
||
| `.auction-winner-pay` | `auction.j2` | "You won this auction!" well with countdown + pay CTA |
|
||
| `.product-add-disabled-note` | `product.j2` | Caption under disabled Add To Cart when active cart is locked |
|
||
| `.shop-offers-page` | `user_offers.j2` + `user_bids.j2` + `shop_offers.j2` | Wider page (max-width 1100px) overriding `.one-column` for the offers/bids inbox tables |
|
||
| `.shop-offers-table` | inbox templates | Responsive table; collapses to per-row block list below 720px |
|
||
| `.notification-badge` | `base.j2` + `/u/settings` | Danger-color pill showing unread notification count |
|
||
| `.notification-row` + `.notification-row-unread` | `user_notifications.j2` | Notification list rows; read rows fade to 0.65 opacity, unread rows get an alert-info background + navy left border |
|
||
| `.notification-row-breadcrumbs` | `user_notifications.j2` | Shop › Product › Offer/Auction/Invoice chain under each row |
|
||
| `.billing-page` / `.billing-paypal-card` / `.billing-actions` | `billing.j2` | `/billing` redesigned with Grid + tokens (no inline styles) |
|
||
| `[data-pay-deadline]` | offer + auction + cart | Convention attribute the shared `static/js/pay-countdown.js` (and inline tickers in `offer.js` / `auction.js`) rewrite once per second with a prose human delta ("in 23 hours, 14 minutes, 8 seconds") matching the server-rendered `ago.human()` fallback |
|
||
|
||
### MPS-24 components — tags, chips, lanes
|
||
|
||
| Class | Where | Description |
|
||
|---|---|---|
|
||
| `.tag-chip-strip` | `home.j2`, `shop.j2`, `shop_tag.j2` | Horizontally scrolling row of chips at the top of the shop home; `grid-auto-flow: column` + `overflow-x: auto` so chips swipe on mobile |
|
||
| `.tag-chip` | every chip strip + bulk tagger | Pill-shaped link, tokens-only colors, hover brightens border + bg |
|
||
| `.tag-chip-active` | current filter chip | Accent-fill chip; click on home filters the grid in place via `static/js/tag_filter.js` |
|
||
| `.tag-lane` | `home.j2`, `shop.j2` (layout 2) | One `<section>` per top tag; capped product count via `shop.home_layout_per_lane_limit` |
|
||
| `.tag-lane-header` | inside `.tag-lane` | Grid 1fr / auto — lane title left, "See all →" right |
|
||
| `.tag-lane-grid` | inside `.tag-lane` | Reuses `.serp` grid pattern; horizontal lane is the *outer* layout, items inside still use product grid |
|
||
| `.tag-list` / `.tag-list-item` | `shop_tags.j2` (bulk tagger) | Operator tag overview: tag pill + product count + view / delete row |
|
||
| `.tag-product-list` / `.tag-product-row` | `shop_tags.j2` (bulk tagger) | "Apply / Applied" toggle per product per tag |
|
||
| `.tag-detail-header` | `shop_tag.j2` | Tag detail page header (tag name + back link) |
|
||
| `[data-tag-strip]` | chip strip | JS hook for `tag_filter.js` |
|
||
| `[data-tag-grid]` | flat `.serp` | JS hook — items inside carry `data-tag-slugs` for in-place filter |
|
||
| `[data-tag-slugs]` | `.serp-item` | Space-separated tag slugs the item carries; consumed by `tag_filter.js` |
|
||
| `.tag-suggest-list` / `.tag-suggest-item` | `shop_tags.j2` (Phase 2) | Suggested-cluster well rendered when operator clicks "Suggest categories from titles + descriptions"; per-cluster grid with label, sample titles, Apply / Dismiss actions |
|
||
| `.tag-suggest-actions` | `shop_tags.j2` | Apply / Dismiss button row, `grid-auto-flow: column` |
|
||
| `.product-tag-chips` / `.tag-chip-list` | `product_edit.j2` (Phase 2.7) | Per-product SPA tag editor container; `js-only`, revealed by `product_tags.js`. Chips wrap as inline-level pills (the `<ul>` is deliberately not a grid/flex parent) |
|
||
| `.tag-chip-removable` | `product_edit.j2`, `/styleguide#tagchips` | Same pill language as `.tag-chip` but an `<li>` with a label + an always-visible `.tag-chip-removable-x` remove button (no hover-only controls). `inline-grid` `auto auto` |
|
||
| `.tag-chip-add` | `product_edit.j2` | Add-a-tag row, grid `1fr auto`, stacks to one column under 600px |
|
||
| `.tag-chip-flash` | `product_edit.j2` | Toast region reusing `.tag-flash-toast` / `.tag-flash-{success,error,info}` |
|
||
| `[data-product-tags]` / `[data-product-tags-url]` | `product_edit.j2` | JS hooks for `product_tags.js`: container + the `/p/{id}/tags` endpoint URL |
|
||
| `[data-focus-section]` / `[data-focus-heading]` / `[data-focus-list]` | `shop_tags.j2` (Phase 2.8) | Stable bulk-tagger focus container — always in the DOM, `hidden` until a tag is focused; `tag_bulk.js` swaps the product list in place instead of a full reload |
|
||
| `[data-tag-focus-link]` | `shop_tags.j2` (Phase 2.8) | Tag chip in the All-tags list; `tag_bulk.js` intercepts the click and fetches `?focus=<slug>` as JSON |
|
||
| `.tag-list-dragging` / `.tag-list-drop-target` | `shop_tags.j2` (Phase 2.8) | Dragged row (dimmed) + active drop position during HTML5 drag-to-reorder; `tag_bulk.js` POSTs `set_order` on drop |
|
||
|
||
## CSS Conventions
|
||
|
||
### Layout
|
||
|
||
- **CSS Grid only** — never use Flexbox for layout
|
||
- Mobile-first: base styles target mobile, `@media` queries enhance for larger screens
|
||
- Primary breakpoint: `max-width: 800px` for mobile
|
||
|
||
### Token Consumption
|
||
|
||
Components in `common.css` reference tokens with fallbacks:
|
||
|
||
```css
|
||
/* Good — token with fallback for resilience */
|
||
background-color: var(--surface-base, #ffffff);
|
||
border-radius: var(--radius-sm, 4px);
|
||
|
||
/* Good — token without fallback (tokens.css always loaded) */
|
||
padding: var(--space-4);
|
||
```
|
||
|
||
### Media Sizing
|
||
|
||
Never combine `width: 100%` with `max-height` on media elements. Use:
|
||
|
||
```css
|
||
/* Correct */
|
||
width: auto;
|
||
max-width: 100%;
|
||
max-height: 33vh;
|
||
|
||
/* Wrong — creates dead whitespace */
|
||
width: 100%;
|
||
max-height: 33vh;
|
||
```
|
||
|
||
### Mobile Usability
|
||
|
||
Never use hover-only interactions. All interactive elements must be always visible and tappable. Design touch-first, then optionally enhance for desktop hover.
|
||
|
||
### Reduced Motion
|
||
|
||
All animations respect `prefers-reduced-motion: reduce` via a global media query in `tokens.css` that collapses durations to near-zero.
|
||
|
||
### Capability-Driven Presentation
|
||
|
||
Follow Russell Ballestrini's capability-driven presentation practice. A page need not look identical across all browsers. Accommodate what the user's browser can do. Use the `js-only` / `<noscript>` pattern for progressive enhancement.
|
||
|
||
## Adding New Components
|
||
|
||
1. Define the component styles in `common.css`, consuming tokens from `tokens.css`
|
||
2. Add a live example to `/styleguide` (`templates/styleguide.j2`)
|
||
3. If the component participates in watch mode SPA navigation, update all three layers: template, `watch.js`, `watch.py`
|
||
|
||
## Load Order
|
||
|
||
```
|
||
base.j2
|
||
└─ <link> static/css/tokens.css ← tokens + utilities + resets
|
||
└─ <link> static/css/common.css ← components consuming tokens
|
||
└─ per-page <style> blocks ← page-specific overrides
|
||
```
|