teqo-backup/DESIGN.md
Francisco Solla cc4b344a3b feat(C242/C243): abre a busca por selfie a qualquer visitante (escopo B) + seção na homepage
O escopo A/C do gate (PR #1370) é superado por decisão do dono (2026-10-01, aval jurídico declarado): o índice vira anônimo (archivePhotoFace, foto + descritor 128-d, sem nome/vínculo), o enrollment por pessoa morre (faceSubject/faces:enroll removidos) e qualquer visitante encontra as próprias fotos aprovadas.

- faces:index persiste os descritores por foto (transação substitui as linhas + marker = modelo); desaprovar/remover/deletar foto apaga os descritores (hooks + FK cascade)
- POST /api/fotos/selfie casa o vetor da consulta contra o índice em memória (TTL 60 s) e devolve só a interseção com o read aprovado do C233, sem score/nome de terceiro; leave-index apaga os descritores que casarem
- Consentimentos seguem fail-closed: busca-selfie-fotos (consulta) e busca-selfie-indice (aviso público do índice), textos versionados em scripts/lib/faceConsentTexts.mjs e provisionados por pnpm seed:face-consents
- pnpm archive:publish aprova o acervo em lote (draft -> approved; removed nunca) com guardas e recibo
- C243: seção condicional SelfieSearchHomeSection (port do design hi-fi, cenas 390/1280) entre a Central e a newsletter; sem engine/captura na home e sem CTA morto quando fechada
- docs: planos C242/C243, AGENTS-public, runbook §C242, changelog

Provas: gate:fast 457 arquivos/5.055 testes; int full 109/1.075; e2e frontendFotosSelfie 9/9; knip verde.
2026-10-01 12:29:04 -03:00

18 KiB
Raw Permalink Blame History

name description colors typography rounded spacing components
Teqo Field ops and public voice for Jorge Solla — campaign desk first, owned channel second.
primary primary-foreground brand-header brand-header-foreground background foreground card muted muted-foreground secondary secondary-foreground accent accent-foreground border input ring destructive sidebar sidebar-foreground scope scope-foreground support-engaged support-engaged-foreground support-disputed support-disputed-foreground support-negative support-negative-foreground estimate-confirmed estimate-confirmed-foreground estimate-pending estimate-pending-foreground cadence-overdue editorial-bg editorial-fg petition-bg petition-cta petition-cta-foreground
#c51414 #ffffff #ae1603 #fff8f2 #ffffff #1c1917 #ffffff #f4f4f5 #6b7280 #f5f5f4 #292524 #f4f4f5 #1c1917 #e7e5e4 #d6d3d1 #c51414 #b42318 #fafaf9 #1c1917 #fff5ed #6b2d15 #dcfce7 #166534 #fef3c7 #92400e #fee2e2 #991b1b #dcfce7 #166534 #fef3c7 #92400e #b42318 #f6f4f3 #201b1a #fff8f2 #ffcf2e #332300
headline title body label
fontFamily fontSize fontWeight lineHeight letterSpacing
Inter, ui-sans-serif, system-ui, sans-serif 1.5rem 600 1.25 -0.025em
fontFamily fontSize fontWeight lineHeight
Inter, ui-sans-serif, system-ui, sans-serif 1rem 500 1.375
fontFamily fontSize fontWeight lineHeight
Inter, ui-sans-serif, system-ui, sans-serif 0.875rem 400 1.5
fontFamily fontSize fontWeight lineHeight
Inter, ui-sans-serif, system-ui, sans-serif 0.75rem 500 1
sm md lg xl full
4px 6px 8px 12px 9999px
xs sm md lg xl
4px 8px 16px 24px 32px
button-primary button-primary-hover button-secondary button-outline button-ghost input-field badge-status card-surface
backgroundColor textColor rounded padding height typography
{colors.primary} {colors.primary-foreground} {rounded.lg} 8px 10px 32px {typography.label}
backgroundColor
#c51414cc
backgroundColor textColor rounded padding height
{colors.secondary} {colors.secondary-foreground} {rounded.lg} 8px 10px 32px
backgroundColor textColor rounded padding height
{colors.background} {colors.foreground} {rounded.lg} 8px 10px 32px
backgroundColor textColor rounded padding height
transparent {colors.foreground} {rounded.lg} 8px 10px 32px
backgroundColor textColor rounded padding height
{colors.background} {colors.foreground} {rounded.md} 8px 16px 32px
backgroundColor textColor rounded padding height typography
{colors.muted} {colors.foreground} {rounded.full} 2px 8px 20px {typography.label}
backgroundColor textColor rounded padding
{colors.card} {colors.foreground} {rounded.xl} 16px

Design System: Teqo

1. Overview

Creative North Star: "The Field Desk"

Teqo’s visual system is a sober ops surface for people organizing under pressure. Design defaults to /campanha: stone-white surfaces, Inter, and brand red as a signal (primary actions, selection, status urgency) — never as spectacle or brochure chrome. Staff screens can be information-dense; leadership paths stay phone-first with larger touch targets and obvious next actions.

Public themes (editorial, petition, and the home brand-red body) share the same token contract and primary red, but they are surface variants. When inventing new UI, start from the campaign desk unless the task is explicitly a public/marketing page.

The system rejects generic SaaS dashboards (purple gradients, hero-metric templates, identical card grids, AI-startup chrome) and traditional Brazilian campaign kitsch (clipart flags, shouty all-caps, brochure clutter). Sovereignty and clarity beat decoration.

Key Characteristics:

  • Product register: familiar shadcn affordances, earned consistency over surprise
  • Restrained color strategy: neutrals dominate; primary red ≤ ~10% of any screen
  • Hybrid elevation: flat lists/forms; light lift only for priority or overlay surfaces
  • Confident and compact controls — denser desktop, phone-first leadership paths
  • Semantic status colors (support / estimate / cadence) carry meaning, not fashion

2. Colors

A stone-neutral desk with one mobilizing red. Semantic pastels mark operational state; they never replace the primary action color.

Primary

  • Mandate Red (#c51414): Primary buttons, focus rings, active nav, brand emphasis on /campanha and editorial/petition themes. Use for the one clear next action.
  • Header Crimson (#ae1603): Public site header bar (--site-header) and home body gradient anchor — inherited across themes so the masthead stays constant.

Secondary

  • Stone Wash (#f5f5f4 / #f4f4f5): Secondary buttons, muted fills, hover washes. Quiet structure, not a second brand.

Tertiary

  • Petition Gold (#ffcf2e): Petition-theme CTA only (data-theme='petition'). Forbidden as a general accent on campaign screens.

Neutral

  • Desk White (#ffffff): Page and card background on campaign.
  • Ink Stone (#1c1917): Body text and strong labels.
  • Quiet Ink (#6b7280): Muted labels, secondary metadata — keep contrast ≥ 4.5:1 on white.
  • Hairline Stone (#e7e5e4 / rgb(28 25 23 / 12%)): Borders and dividers.
  • Field Border (#d6d3d1): Visible input stroke on white cards.
  • Rail Mist (#fafaf9): Campaign sidebar rail.

Semantic (campaign ops)

  • Scope / territory chips: warm peach (#fff5ed / #6b2d15)
  • Support & estimates: green engaged/confirmed, amber disputed/pending, red negative/overdue — use only via Badge variants, never invent parallel hues

Named Rules

The Signal Red Rule. Mandate Red is for primary action, focus, and urgency. It must not wash backgrounds, decorate cards, or paint inactive chrome.

The Theme Contract Rule. New surfaces reuse the shadcn token names (primary, muted, field-*, etc.). Do not introduce one-off hex outside a data-theme block.

3. Typography

Display Font: Inter (with ui-sans-serif, system-ui, sans-serif) Body Font: Inter (same stack) Label/Mono Font: Inter for UI labels; mono only for code snippets

Character: One tuned sans for the whole product desk — no display serif in app chrome. Public article heroes may use larger extrabold Inter; campaign headings stay left-aligned section titles.

Hierarchy

  • Headline (600, 1.5rem / text-2xl, tight tracking): Page and section titles on /campanha.
  • Title (500, 1rem / text-base): Card titles, dialog titles.
  • Body (400, 0.875rem / text-sm, leading ~1.5): Default UI copy; prose max ~65–75ch when reading length matters.
  • Label (500, 0.75rem / text-xs): Badges, meta, table headers. Never all-caps tracked eyebrows as section scaffolding.

Named Rules

The One Family Rule. Inter everywhere in product UI. No decorative display faces in buttons, labels, or data.

The Fixed Scale Rule. Prefer rem/Tailwind steps over fluid clamp() headings inside the campaign shell.

4. Elevation

Hybrid: most of the desk is flat. Depth comes from tonal layers (sidebar rail, muted fills, hairline rings) and from overlays when the user must focus.

Lists, forms, filters, and table rows stay flat. Cards may use a light ring (ring-1 ring-foreground/10) without drop shadow. Soft ambient lift is reserved for priority overview panels and for floating UI (dialogs, sheets, drawers, popovers).

Shadow Vocabulary

  • Flat desk (no shadow): Default for rows, forms, filters, most cards.
  • Priority lift (soft ambient, low opacity): Overview / insight panels that must read as “above” the list — keep blur soft and shadow light; if it looks like a 2014 app, the shadow is too dark.
  • Overlay (modal/sheet/popover system shadow): Only for temporary focus layers.

Named Rules

The Flat-By-Default Rule. Surfaces are flat at rest. Lift appears for priority or overlay — never as default card decoration across a grid.

5. Components

Controls feel confident and compact: short primary buttons, dense toolbars, phone-first tap targets where leaderships work.

Buttons

  • Shape: Gently rounded (rounded-lg ≈ 8px; smaller sizes clamp toward --radius-md)
  • Primary: Mandate Red fill, white text, height 32px default; primary submit may use font-bold
  • Hover / Focus: Primary hover at ~80% opacity; focus-visible ring ring-3 ring-ring/50 with border-ring
  • Secondary / Outline / Ghost / Destructive: Standard shadcn variants; destructive is tinted text/fill, not a second brand red for routine actions
  • Active: Slight translate-y-px press

Chips / Badges

  • Style: Pill (rounded-4xl), height 20px, text-xs medium
  • State: Semantic variants (support-*, estimate-*, scope, tse) — status meaning only; do not invent decorative chip colors

Cards / Containers

  • Corner Style: rounded-xl (~12px)
  • Background: Card white on desk white — separation via hairline ring, not heavy shadow
  • Shadow Strategy: Flat by default; priority lift only when the panel is an overview/insight surface
  • Internal Padding: --card-spacing default 16px (spacing(4)), sm 12px

Inputs / Fields

  • Style: Campaign forces --radius-md (~6px) on input/select triggers (overrides public rounded-full input). Visible --field-border on white.
  • Focus: Border primary + ring-3 ring-primary/30
  • Error / Disabled: Destructive border/ring; disabled uses field-disabled tokens at reduced opacity
  • Touch: Keep leadership-facing controls generous; public Input uses larger py for form comfort

Navigation

  • Campaign shell: Light stone sidebar rail on desktop; mobile top bar in Mandate Red with white wordmark — intentional field-mode contrast (phone in the street vs desk rail), not Signal Red Rule drift.
  • Active: Primary-tinted or bold text — red signals location, not decoration
  • Public header: Header Crimson bar, constant across themes

Status Badge (signature)

Operational meaning for leadership/support/estimates. Always use Badge variants tied to CSS tokens — never hardcode greens/ambers that drift from --support-* / --estimate-*.

Territorial class (E10) is a four-step ladder scanned down a 25-row column, so its variants must stay mutually legible: reduto estimate-confirmed (green, achieved), expansão estimate-pending (amber, asks for work), manutenção secondary (grey, routine), marginal outline (hairline). sem_base gets no badge — absent data is the em dash the neighboring columns already use. The map lives in territorialClassBadgeVariant; the class never renders without its "por quê" line.

The same ladder has a second, unavoidable encoding: B13 colors the map's proportional bubbles by territorial class, and Leaflet paints SVG with literal hex, not CSS variables. territorialClassMapFill (municipalityLabels.ts) is therefore the one place allowed to restate this palette as hex, and it must be re-derived by hand whenever --support-* / --estimate-* move — the two ladders are the same semantic scale and drifting them apart breaks the list↔map read. sem_base there is the shared NO_DATA_FILL (choroplethColorScale.ts), the same grey the choropleth uses for missing data.

Campaign list system (Pass 2 W1 — canonical, do not fork)

Every /campanha list surface composes the shared pieces in src/components/campaign/shared/: CampaignTable (columns as data over house ui/Table; rounded-xl border container), CampaignSearchForm / CampaignFilterChips as the toolbar, CampaignListFooter (count + pagination), CampaignListEmptyState (Empty with icon + CTA, min-h-72 border), all inside CampaignListPendingBoundary so navigation dims the RESULTS region ("Feel the action"). Rich headers (sort + filter popovers) are the municipality islands (MunicipalitySortableHead/MunicipalityHeaderFilter) — the reference implementation, not yet generalized. Cell-level explanation IS generalized (B23 seam, first consumer E10's "Classe"): a column declares cellTooltip: (row) => ReactNode and CampaignTable wraps that cell in CampaignCellTooltip; returning null for a row leaves it untouched. The contract is that the tooltip is REDUNDANT — the same content must already be text in the cell (visible, or sr-only behind a badge), because Radix mounts tooltip content only on open and the table adds no tab stops. The hover primitive is shared/CampaignHoverTooltip (hover + keyboard focus + touch tap). New list = new column definitions + these shells; a bespoke table needs a documented exception (precedents: planos cards, TerritoryOverviewTable).

6. Do's and Don'ts

Do:

  • Do treat /campanha as the design default: stone desk, Inter, Signal Red for the next action.
  • Do keep primary red rare — one clear CTA per region beats a sea of red buttons.
  • Do use semantic Badge tokens for support/estimate/cadence state.
  • Do keep leadership paths phone-first: obvious actions, larger taps, plain language.
  • Do meet WCAG 2.2 AA (contrast, keyboard, focus rings, prefers-reduced-motion).
  • Do reuse shadcn primitives (Button, Input, Badge, Card, dialogs/sheets) for consistency.

Don't:

  • Don't ship generic SaaS dashboards — purple gradients, hero-metric templates, identical card grids, AI-startup chrome.
  • Don't ship traditional Brazilian campaign kitsch — clipart flags, shouty all-caps, brochure clutter.
  • Don't use side-stripe borders (border-left/border-right > 1px) as accent decoration.
  • Don't use gradient text or glassmorphism as default chrome.
  • Don't put tiny uppercase tracked eyebrows above every section.
  • Don't use Petition Gold or home body gradients inside the campaign desk.
  • Don't invent display fonts for UI labels, buttons, or data.
  • Don't lift every card; flat is the desk’s resting state.

7. Evolution

This file is the living owner of Teqo's visual decisions — a working document, not a snapshot. The old single-source-of-brand guardrail (kit 1313 as the only authority; "never re-derive lockups or palette") is retired. The official kit assets in public/campaign-kit/ stay available — and their technical usage quirks still apply when used — but they no longer bind new design work.

Public campaign archive actions (C233, 2026-09-26). On campaign-branded public archive/read surfaces, the primary action is campaign red with white text. Kit yellow is an orientation and keyboard-focus signal — hero edge, restrained eyebrow, current-location underline, and high-contrast focus outline — never the competing CTA fill. This keeps one action dominant, separates public campaign expression from Petition Gold, and makes keyboard location unmistakable over both light and dark media surfaces.

Multi-media content gallery (C240, 2026-10-01). A public piece with multiple media uses one stable stage plus position (“N de M”), previous/next controls, and a horizontally scrollable thumbnail strip; never shrink every thumbnail to force the whole set on screen. Download belongs to the selected media, while the single primary action shares the official source post — do not offer an ambiguous “download all” or make media-sharing compete with the source link. Partial arrival is an internal neutral processing state; the public gallery appears only when the complete set is ready. This keeps the action model honest across image-only, mixed video, and large carousels without inventing a second album product.

Public light-surface focus separation (C243, 2026-10-01). When the campaign-yellow focus outline sits on a cream, white, or yellow-tinted public surface, keep the 3px yellow signal but add a dark blue outer separator. Yellow alone does not separate reliably from these backgrounds; the second edge preserves the archive's established focus identity while making keyboard location unmistakable without turning blue into a competing action color.

How rules change:

  • Decide, then update the owner. When a better pattern wins — anti-slop review (design-taste-frontend / gpt-taste), a reference study (awesome-design-md), a redesign pass (redesign-existing-projects), or implementation judgment — update this file in the same change and say why. A pattern that only lives in code or in an agent's head is drift, not design.
  • Scope by surface. Sections 1–6 describe the /campanha operate desk first; public/persuade surfaces (site, petition, campaign page) may pick a different visual world, but they inherit the token contract and the access/consent constraints.
  • Tokens move through code. Change colors, typography, rounded, components via the existing shadcn / data-theme plumbing; this file records intent, the CSS stays the executable truth. Keep both in sync in the same change.
  • Material changes keep the human gate. A material visual change (visible identity, hierarchy, brand direction) still goes through the plan-issue/work-issue hi-fi gate: propose, show, merge. Token-neutral refinements follow the normal PR review.
  • Keep it current. Stale rules here mislead every agent that reads this file — delete what no longer holds instead of accumulating contradictions.