Operator with 481 untagged products (printableprompts.com) gets a one-click path to a usable categorization without hand-tagging each product. Strictly suggest-then-approve — nothing writes Tag or ProductTag rows until the operator clicks Apply on a cluster. - lib/tag_suggest.py: pure-function clusterer. Tokenize title (weight 3) + description (weight 1, capped at 100 unique tokens per product), strip markdown / URLs / HTML, English + per-shop stopwords, simple suffix-strip stemmer, group by stem, drop stems matching existing tag slugs, rank by product count, label each cluster with the most frequent original word for its stem. No new deps, no ML. - scripts/backfill_tags.py: CLI preview + --apply for a single shop. - views/shop.py: shop_tags gains action=apply_suggestion (creates tag + bulk-attaches every product in cluster) and action=dismiss_suggestion (adds the cluster's words to shop.tag_stopwords_json so it never resurfaces). ?show_suggestions=1 triggers the cluster compute. - templates/shop_tags.j2: "Suggest categories from titles + descriptions" button + suggestions well with per-cluster sample titles, Apply, and Dismiss buttons. - 15 new tests (11 unit over tokenize / stem / cluster + 4 functional over the suggest/apply/dismiss flow). 1064 total passing. On a printableprompts-style sample the clusterer surfaces Math, Reading, Literacy, Seasonal, Novel, Activities, Comprehension — matching what an operator would manually pick.
17 KiB
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 |
CSS Conventions
Layout
- CSS Grid only — never use Flexbox for layout
- Mobile-first: base styles target mobile,
@mediaqueries enhance for larger screens - Primary breakpoint:
max-width: 800pxfor mobile
Token Consumption
Components in common.css reference tokens with fallbacks:
/* 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:
/* 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
- Define the component styles in
common.css, consuming tokens fromtokens.css - Add a live example to
/styleguide(templates/styleguide.j2) - 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