make_post_sell/docs/design-system.md
russell@unturf.com 155f7ff66f
fix: MPS-24 Phase 2.8 — bulk tagger AJAX tag-focus + real drag-to-reorder
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).
2026-05-16 08:31:03 -04:00

357 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```