diff --git a/.impeccable/design.json b/.impeccable/design.json new file mode 100644 index 0000000..0fa7959 --- /dev/null +++ b/.impeccable/design.json @@ -0,0 +1,454 @@ +{ + "schemaVersion": 2, + "generatedAt": "2026-08-08T09:31:01Z", + "title": "Design System: httphq", + "extensions": { + "colorMeta": { + "beacon-periwinkle": { + "role": "primary", + "displayName": "Beacon Periwinkle", + "canonical": "oklch(63.2% 0.157 275.252)", + "tonalRamp": [ + "oklch(15% 0.157 275.252)", + "oklch(26% 0.157 275.252)", + "oklch(37% 0.157 275.252)", + "oklch(48% 0.157 275.252)", + "oklch(59% 0.157 275.252)", + "oklch(70% 0.157 275.252)", + "oklch(82% 0.157 275.252)", + "oklch(93% 0.157 275.252)" + ] + }, + "signal-indigo": { + "role": "primary", + "displayName": "Signal Indigo", + "canonical": "oklch(51.1% 0.262 276.966)", + "tonalRamp": [ + "oklch(25.7% 0.09 281.288)", + "oklch(35.9% 0.144 278.697)", + "oklch(39.8% 0.195 277.366)", + "oklch(45.7% 0.24 277.023)", + "oklch(51.1% 0.262 276.966)", + "oklch(67.3% 0.182 276.935)", + "oklch(87% 0.065 274.039)", + "oklch(96.2% 0.018 272.314)" + ] + }, + "ink-body": { + "role": "neutral", + "displayName": "Body Ink", + "canonical": "oklch(27.9% 0.041 260.031)", + "tonalRamp": [ + "oklch(12.9% 0.042 264.695)", + "oklch(20.8% 0.042 265.755)", + "oklch(27.9% 0.041 260.031)", + "oklch(37.2% 0.044 257.287)", + "oklch(44.6% 0.043 257.281)", + "oklch(70.4% 0.04 256.788)", + "oklch(92.9% 0.013 255.508)", + "oklch(98.4% 0.003 247.858)" + ] + }, + "board-field": { + "role": "neutral", + "displayName": "Board Field", + "canonical": "oklch(98.4% 0.003 247.858)", + "tonalRamp": [ + "oklch(12.9% 0.042 264.695)", + "oklch(20.8% 0.042 265.755)", + "oklch(27.9% 0.041 260.031)", + "oklch(37.2% 0.044 257.287)", + "oklch(44.6% 0.043 257.281)", + "oklch(70.4% 0.04 256.788)", + "oklch(92.9% 0.013 255.508)", + "oklch(98.4% 0.003 247.858)" + ] + }, + "method-get-ink": { + "role": "tertiary", + "displayName": "Instrument Blue", + "canonical": "oklch(48.8% 0.243 264.376)", + "tonalRamp": [ + "oklch(28.2% 0.091 267.935)", + "oklch(37.9% 0.146 265.522)", + "oklch(42.4% 0.199 265.638)", + "oklch(48.8% 0.243 264.376)", + "oklch(54.6% 0.245 262.881)", + "oklch(70.7% 0.165 254.624)", + "oklch(88.2% 0.059 254.128)", + "oklch(97% 0.014 254.604)" + ] + }, + "method-post-ink": { + "role": "tertiary", + "displayName": "Bench Emerald", + "canonical": "oklch(50.8% 0.118 165.612)", + "tonalRamp": [ + "oklch(26.2% 0.051 172.552)", + "oklch(37.8% 0.077 168.94)", + "oklch(43.2% 0.095 166.913)", + "oklch(50.8% 0.118 165.612)", + "oklch(59.6% 0.145 163.225)", + "oklch(76.5% 0.177 163.223)", + "oklch(90.5% 0.093 164.15)", + "oklch(97.9% 0.021 166.113)" + ] + }, + "method-put-ink": { + "role": "tertiary", + "displayName": "Signal Amber", + "canonical": "oklch(55.5% 0.163 48.998)", + "tonalRamp": [ + "oklch(27.9% 0.077 45.635)", + "oklch(41.4% 0.112 45.904)", + "oklch(47.3% 0.137 46.201)", + "oklch(55.5% 0.163 48.998)", + "oklch(66.6% 0.179 58.318)", + "oklch(82.8% 0.189 84.429)", + "oklch(92.4% 0.12 95.746)", + "oklch(98.7% 0.022 95.277)" + ] + }, + "method-patch-ink": { + "role": "tertiary", + "displayName": "Filament Violet", + "canonical": "oklch(49.1% 0.27 292.581)", + "tonalRamp": [ + "oklch(28.3% 0.141 291.089)", + "oklch(38% 0.189 293.745)", + "oklch(43.2% 0.232 292.759)", + "oklch(49.1% 0.27 292.581)", + "oklch(54.1% 0.281 293.009)", + "oklch(70.2% 0.183 293.541)", + "oklch(89.4% 0.057 293.283)", + "oklch(96.9% 0.016 293.756)" + ] + }, + "method-delete-ink": { + "role": "tertiary", + "displayName": "Warning Rose", + "canonical": "oklch(51.4% 0.222 16.935)", + "tonalRamp": [ + "oklch(27.1% 0.105 12.094)", + "oklch(41% 0.159 10.272)", + "oklch(45.5% 0.188 13.697)", + "oklch(51.4% 0.222 16.935)", + "oklch(58.6% 0.253 17.585)", + "oklch(71.2% 0.194 13.428)", + "oklch(89.2% 0.058 10.001)", + "oklch(96.9% 0.015 12.422)" + ] + }, + "method-options-ink": { + "role": "tertiary", + "displayName": "Preflight Sky", + "canonical": "oklch(50% 0.134 242.749)", + "tonalRamp": [ + "oklch(29.3% 0.066 243.157)", + "oklch(39.1% 0.09 240.876)", + "oklch(44.3% 0.11 240.79)", + "oklch(50% 0.134 242.749)", + "oklch(58.8% 0.158 241.966)", + "oklch(74.6% 0.16 232.661)", + "oklch(90.1% 0.058 230.902)", + "oklch(97.7% 0.013 236.62)" + ] + }, + "syntax-key": { + "role": "tertiary", + "displayName": "Key Blue", + "canonical": "#005cc5", + "note": "Vendored from the highlight.js GitHub light theme; no tonal ramp because the borrowed palette has no scale. Covers hljs-attr, hljs-number, hljs-literal, hljs-meta." + }, + "syntax-string": { + "role": "tertiary", + "displayName": "String Navy", + "canonical": "#032f62", + "note": "Vendored highlight.js theme colour for hljs-string. Not a system token; do not restyle in isolation." + }, + "syntax-name": { + "role": "tertiary", + "displayName": "Element Green", + "canonical": "#22863a", + "note": "Vendored highlight.js theme colour for XML element names (hljs-name)." + }, + "syntax-keyword": { + "role": "tertiary", + "displayName": "Keyword Red", + "canonical": "#d73a49", + "note": "Vendored highlight.js theme colour for hljs-keyword." + }, + "alert-rose": { + "role": "secondary", + "displayName": "Alert Rose", + "canonical": "oklch(58.6% 0.253 17.585)", + "tonalRamp": [ + "oklch(27.1% 0.105 12.094)", + "oklch(41% 0.159 10.272)", + "oklch(45.5% 0.188 13.697)", + "oklch(51.4% 0.222 16.935)", + "oklch(58.6% 0.253 17.585)", + "oklch(71.2% 0.194 13.428)", + "oklch(89.2% 0.058 10.001)", + "oklch(96.9% 0.015 12.422)" + ] + } + }, + "typographyMeta": { + "display": { + "displayName": "Display", + "purpose": "The home hero headline. Appears once, on one page; steps down to 1.875rem below 40rem." + }, + "headline": { + "displayName": "Headline", + "purpose": "Page titles outside the hero, e.g. Get in touch." + }, + "title": { + "displayName": "Title", + "purpose": "Card headings and the disclosure summary. Medium weight, never semibold." + }, + "lede": { + "displayName": "Lede", + "purpose": "The single paragraph under the hero, capped at 36rem." + }, + "body": { + "displayName": "Body", + "purpose": "The working size of the entire application chrome. Most of the product is 14px, not 16px." + }, + "label": { + "displayName": "Field Label", + "purpose": "Uppercase region labels: TIME, CLIENT IP, PATH, HEADERS, QUERY STRING, BODY." + }, + "badge": { + "displayName": "Method Badge", + "purpose": "HTTP method chips only. One of exactly two uppercase uses in the system." + }, + "mono": { + "displayName": "Evidence Mono", + "purpose": "Every captured value — headers, paths, IPs, query strings, bodies, endpoint URL, request UUID." + } + }, + "shadows": [ + { + "name": "resting-whisper", + "value": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)", + "purpose": "Every panel, card, disclosure and primary button at rest. Its only job is to stop white from looking pasted onto the slate field." + }, + { + "name": "hover-lift", + "value": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", + "purpose": "Applied only to the six home-page use-case cards, and only on hover. Nothing above the resting whisper exists at rest." + } + ], + "motion": [ + { + "name": "default-transition", + "value": "150ms cubic-bezier(0.4, 0, 0.2, 1)", + "purpose": "Every hover and focus state change: button fills, text-action colour shifts, card lift." + }, + { + "name": "disclosure-chevron", + "value": "transform 150ms cubic-bezier(0.4, 0, 0.2, 1)", + "purpose": "The Send-a-test-request summary chevron rotates 180 degrees on open." + }, + { + "name": "waiting-ellipsis", + "value": "1.2s steps(4, end) infinite", + "purpose": "The four-step CSS content animation in the waiting state. The only looping motion in the product." + }, + { + "name": "copy-confirm", + "value": "1500ms label swap", + "purpose": "Copy controls swap their label to Copied! and revert after 1.5s. Send status clears after 2s." + } + ], + "breakpoints": [ + { + "name": "sm", + "value": "40rem" + } + ] + }, + "components": [ + { + "name": "Primary Button", + "kind": "button", + "refersTo": "button-primary", + "description": "The one indigo-filled action per view. Two sizes only: page action (1rem type) and in-panel (0.875rem).", + "html": "", + "css": ".ds-btn-primary { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; background: oklch(51.1% 0.262 276.966); color: #fff; font-family: ui-sans-serif, system-ui, sans-serif; font-size: 1rem; font-weight: 500; line-height: 1.5; padding: 0.75rem 1.5rem; border: none; border-radius: 0.375rem; box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); cursor: pointer; transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1); } .ds-btn-primary:hover { background: oklch(58.5% 0.233 277.117); } .ds-btn-primary:focus-visible { outline: none; box-shadow: 0 0 0 2px #fff, 0 0 0 4px oklch(58.5% 0.233 277.117); } .ds-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }" + }, + { + "name": "Secondary Button", + "kind": "button", + "refersTo": "button-secondary", + "description": "White fill with a control-edge stroke. Every non-primary action that still needs a button shape.", + "html": "", + "css": ".ds-btn-secondary { display: inline-flex; align-items: center; justify-content: center; gap: 0.375rem; background: #fff; color: oklch(37.2% 0.044 257.287); font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.875rem; font-weight: 500; line-height: 1.429; padding: 0.5rem 0.75rem; border: 1px solid oklch(86.9% 0.022 252.894); border-radius: 0.375rem; cursor: pointer; transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1); } .ds-btn-secondary:hover { background: oklch(98.4% 0.003 247.858); } .ds-btn-secondary:focus-visible { outline: none; box-shadow: 0 0 0 2px oklch(58.5% 0.233 277.117); } .ds-btn-secondary:disabled { opacity: 0.5; cursor: not-allowed; }" + }, + { + "name": "Destructive Button", + "kind": "button", + "refersTo": "button-destructive", + "description": "Rose wash with a rose stroke. Reserved for actions that destroy captured data.", + "html": "", + "css": ".ds-btn-destructive { display: inline-flex; align-items: center; gap: 0.375rem; background: oklch(96.9% 0.015 12.422); color: oklch(51.4% 0.222 16.935); font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.875rem; font-weight: 500; line-height: 1.429; padding: 0.375rem 0.75rem; border: 1px solid oklch(89.2% 0.058 10.001); border-radius: 0.375rem; cursor: pointer; transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1); } .ds-btn-destructive:hover { background: oklch(94.1% 0.03 12.58); } .ds-btn-destructive:focus-visible { outline: none; box-shadow: 0 0 0 2px oklch(64.5% 0.246 16.439); }" + }, + { + "name": "Text Action", + "kind": "button", + "refersTo": "button-secondary", + "description": "No fill, no border. Label ink resolving to signal indigo on hover — or alert rose when the action deletes.", + "html": "
", + "css": ".ds-action { display: inline-flex; align-items: center; gap: 0.25rem; background: none; border: none; padding: 0; color: oklch(55.4% 0.046 257.417); font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.875rem; line-height: 1.429; cursor: pointer; transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1); } .ds-action:hover { color: oklch(51.1% 0.262 276.966); } .ds-action-danger:hover { color: oklch(58.6% 0.253 17.585); } .ds-action:focus-visible { outline: none; box-shadow: 0 0 0 2px oklch(58.5% 0.233 277.117); border-radius: 0.25rem; }" + }, + { + "name": "Labelled Input", + "kind": "input", + "refersTo": "input-field", + "description": "Uppercase field label above a white field with a control-edge stroke. Focus tightens to a 1px indigo ring plus a border shift.", + "html": "", + "css": ".ds-field { display: block; } .ds-field-label { display: block; margin-bottom: 0.25rem; font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.75rem; font-weight: 500; line-height: 1.333; letter-spacing: 0.025em; text-transform: uppercase; color: oklch(55.4% 0.046 257.417); } .ds-input { display: block; width: 100%; box-sizing: border-box; background: #fff; color: oklch(27.9% 0.041 260.031); font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.875rem; line-height: 1.429; padding: 0.5rem 0.75rem; border: 1px solid oklch(86.9% 0.022 252.894); border-radius: 0.375rem; } .ds-input-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; } .ds-input::placeholder { color: oklch(70.4% 0.04 256.788); } .ds-input:focus { outline: none; border-color: oklch(58.5% 0.233 277.117); box-shadow: 0 0 0 1px oklch(67.3% 0.182 276.935); }" + }, + { + "name": "Select", + "kind": "input", + "refersTo": "input-field", + "description": "Native select with appearance stripped and a slate chevron inlined as a data-URI, 1.1em at 0.5rem from the right.", + "html": "", + "css": ".ds-select { appearance: none; -webkit-appearance: none; background-color: #fff; background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%2364748b'%3E%3Cpath fill-rule='evenodd' d='M5.23 7.21a.75.75 0 011.06.02L10 11.083l3.71-3.853a.75.75 0 111.08 1.04l-4.25 4.41a.75.75 0 01-1.08 0L5.21 8.27a.75.75 0 01.02-1.06z' clip-rule='evenodd'/%3E%3C/svg%3E\"); background-repeat: no-repeat; background-position: right 0.5rem center; background-size: 1.1em; color: oklch(27.9% 0.041 260.031); font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.875rem; line-height: 1.429; padding: 0.5rem 2rem 0.5rem 0.75rem; border: 1px solid oklch(86.9% 0.022 252.894); border-radius: 0.375rem; cursor: pointer; } .ds-select:focus { outline: none; border-color: oklch(58.5% 0.233 277.117); box-shadow: 0 0 0 1px oklch(67.3% 0.182 276.935); }" + }, + { + "name": "Method Badge", + "kind": "chip", + "refersTo": "method-badge", + "description": "The signature component. Seven wash-and-ink pairs; the only saturated colour in the request stream. Unknown methods fall back to the HEAD pair.", + "html": "
GETPOSTPUTPATCHDELETEHEADOPTIONS
", + "css": ".ds-badge { display: inline-flex; align-items: center; justify-content: center; font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.75rem; font-weight: 600; line-height: 1.333; letter-spacing: 0.025em; text-transform: uppercase; padding: 0.125rem 0.5rem; border-radius: 0.25rem; } .ds-badge-get { background: oklch(97% 0.014 254.604); color: oklch(48.8% 0.243 264.376); } .ds-badge-post { background: oklch(97.9% 0.021 166.113); color: oklch(50.8% 0.118 165.612); } .ds-badge-put { background: oklch(98.7% 0.022 95.277); color: oklch(55.5% 0.163 48.998); } .ds-badge-patch { background: oklch(96.9% 0.016 293.756); color: oklch(49.1% 0.27 292.581); } .ds-badge-delete { background: oklch(96.9% 0.015 12.422); color: oklch(51.4% 0.222 16.935); } .ds-badge-head { background: oklch(96.8% 0.007 247.896); color: oklch(37.2% 0.044 257.287); } .ds-badge-options { background: oklch(97.7% 0.013 236.62); color: oklch(50% 0.134 242.749); }" + }, + { + "name": "Request Panel", + "kind": "card", + "refersTo": "panel", + "description": "One captured request: a rule-separated header carrying the badge and UUID, then labelled regions separated by hairline-faint rules rather than nested boxes. Key/value rows are flex — a 10rem label column above 40rem, label stacked above value below it.", + "html": "
POST3f9a1c72-8d40-4e11-b6a2-9c7f0e5d2a18
Details
Time
2 minutes ago
Client IP
203.0.113.42
Path
/to/quiet-sunset-4821
Body
{\n  \"event\": \"payment.succeeded\",\n  \"amount\": 4200\n}
", + "css": ".ds-panel { background: #fff; border: 1px solid oklch(92.9% 0.013 255.508); border-radius: 0.5rem; box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); overflow: hidden; font-family: ui-sans-serif, system-ui, sans-serif; } .ds-panel-head { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1.25rem; border-bottom: 1px solid oklch(92.9% 0.013 255.508); } .ds-uuid { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.75rem; color: oklch(55.4% 0.046 257.417); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ds-uuid:hover { color: oklch(51.1% 0.262 276.966); } .ds-panel-body { padding: 1.25rem; } .ds-label { font-size: 0.75rem; font-weight: 500; line-height: 1.333; letter-spacing: 0.025em; text-transform: uppercase; color: oklch(55.4% 0.046 257.417); margin-bottom: 0.5rem; } .ds-rows { margin: 0; font-size: 0.875rem; color: oklch(27.9% 0.041 260.031); } .ds-row { display: flex; flex-direction: column; padding: 0.375rem 0; border-bottom: 1px solid oklch(96.8% 0.007 247.896); } .ds-row:last-child { border-bottom: none; } .ds-row dt { color: oklch(55.4% 0.046 257.417); } .ds-row dd { margin: 0; min-width: 0; } @media (min-width: 40rem) { .ds-row { flex-direction: row; gap: 0.75rem; } .ds-row dt { width: 10rem; flex-shrink: 0; } } .ds-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.75rem; } .ds-well { margin: 0; background: oklch(98.4% 0.003 247.858); border: 1px solid oklch(92.9% 0.013 255.508); border-radius: 0.25rem; padding: 0.75rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.75rem; line-height: 1.333; color: oklch(27.9% 0.041 260.031); overflow: auto; }" + }, + { + "name": "Body Rendering", + "kind": "custom", + "refersTo": "code-well", + "description": "A captured body in its code well, highlighted by the vendored highlight.js GitHub theme. Four content-type paths land in this same container: pretty-printed JSON, a multipart part list serialised as JSON, XML, or escaped raw text.", + "html": "
Body
{\n  \"type\": \"payment_intent.succeeded\",\n  \"amount\": 4200,\n  \"livemode\": false\n}
Query string

None

", + "css": ".ds-label { font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.75rem; font-weight: 500; line-height: 1.333; letter-spacing: 0.025em; text-transform: uppercase; color: oklch(55.4% 0.046 257.417); margin-bottom: 0.5rem; } .ds-well { margin: 0; background: oklch(98.4% 0.003 247.858); border: 1px solid oklch(92.9% 0.013 255.508); border-radius: 0.25rem; padding: 0.75rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.75rem; line-height: 1.333; color: oklch(27.9% 0.041 260.031); max-height: 24rem; overflow: auto; } .ds-hl-punct { color: oklch(27.9% 0.041 260.031); } .ds-hl-attr { color: #005cc5; } .ds-hl-string { color: #032f62; } .ds-hl-name { color: #22863a; } .ds-hl-keyword { color: #d73a49; } .ds-none { margin: 0; font-family: ui-sans-serif, system-ui, sans-serif; font-size: 0.75rem; font-style: italic; color: oklch(70.4% 0.04 256.788); }" + }, + { + "name": "Waiting State", + "kind": "custom", + "refersTo": "empty-state", + "description": "The dashed-means-waiting container. Its four-step ellipsis is the only looping animation in the product.", + "html": "

Waiting for requests

Requests sent to your URL will appear here in real time.

", + "css": ".ds-empty { background: #fff; border: 1px dashed oklch(86.9% 0.022 252.894); border-radius: 0.5rem; padding: 3rem 1rem; text-align: center; color: oklch(55.4% 0.046 257.417); font-family: ui-sans-serif, system-ui, sans-serif; } .ds-empty-title { margin: 0; font-size: 1rem; line-height: 1.5; } .ds-empty-sub { margin: 0.5rem 0 0; font-size: 0.875rem; line-height: 1.429; } .ds-dots::after { content: ''; display: inline-block; width: 1.5ch; text-align: left; animation: ds-dots 1.2s steps(4, end) infinite; } @keyframes ds-dots { 0% { content: ''; } 25% { content: '.'; } 50% { content: '..'; } 75%, 100% { content: '...'; } }" + }, + { + "name": "Wordmark & Footer", + "kind": "nav", + "refersTo": "button-secondary", + "description": "There is no navigation bar. A centred wordmark links home; every other route lives in the centred footer.", + "html": "
httphq
", + "css": ".ds-chrome { font-family: ui-sans-serif, system-ui, sans-serif; text-align: center; } .ds-wordmark { display: inline-block; padding: 1.5rem 0; font-size: 1.875rem; font-weight: 700; letter-spacing: -0.025em; color: oklch(37.2% 0.044 257.287); text-decoration: none; } .ds-footer { padding-top: 2rem; font-size: 0.875rem; line-height: 1.429; color: oklch(55.4% 0.046 257.417); } .ds-footer > div + div { margin-top: 0.75rem; } .ds-footer-link { display: inline-flex; align-items: center; gap: 0.375rem; color: inherit; text-decoration: none; transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1); } .ds-footer-link:hover { color: oklch(51.1% 0.262 276.966); text-decoration: underline; } .ds-footer-link:focus-visible { outline: none; box-shadow: 0 0 0 2px oklch(58.5% 0.233 277.117); border-radius: 0.25rem; }" + } + ], + "narrative": { + "northStar": "The Arrivals Board", + "overview": "httphq is a board you watch, not a console you operate. Requests land at the top of the stream, get colour-coded by class, and expire. The whole visual system is built so a developer working in another window can glance over and read the board — status first, detail on approach — then go back to what they were doing. Everything that isn't the arriving traffic is chassis: a slate field, white panels, hairline seams, and exactly one indigo control per view.\n\nThe register is precise, quiet and fast. Restraint here is not minimalism as a style choice; it is what makes the board readable. The interface never claims more than it can prove, never decorates a value it captured, and never puts a second thing in colour next to a method badge. Type is system-native and loads instantly — there is no webfont anywhere, and that is a feature of a tool whose entire promise is being usable within seconds of arrival.\n\nFour looks are rejected outright, and all four are confirmed prohibitions rather than taste: marketing-SaaS gloss (gradient meshes, floating mockups, testimonial carousels), the dense enterprise console (dark chrome, packed toolbars, panels nested in panels), terminal cosplay (green-on-black, ASCII framing, faux-CRT effects), and playful dev-tool mascotry (cartoon characters, blob illustrations, jokey empty states). Monospace appears throughout, but as evidence handling — never as costume.", + "keyCharacteristics": [ + "A slate-50 field with white panels; hairline seams, not shadows, make the edges", + "One indigo action per view; every other control is a slate outline or bare text", + "Full-spectrum colour is spent entirely on the seven HTTP-method badges", + "Everything the client sent is monospace; everything httphq says is sans", + "A single breakpoint (40rem) governs the entire responsive system", + "Liveness is signalled in the browser chrome — tab title count, favicon dot — not by animating the page" + ], + "rules": [ + { + "name": "The Borrowed Palette Rule", + "body": "The four syntax colours are a vendored theme, not system tokens. Never restyle individual hljs-* classes to bring them closer to the slate-and-indigo world — either swap the whole theme or leave it alone. A half-retinted syntax palette reads as a bug in the highlighter.", + "section": "colors" + }, + { + "name": "The Board Rule", + "body": "Full-spectrum colour belongs to the method badge and nothing else. If a new element wants blue, emerald, amber, violet or sky, the answer is no — that vocabulary means \"this is the class of traffic that arrived\", and every additional user dilutes the only colour-coding on the page.", + "section": "colors" + }, + { + "name": "The One Indigo Rule", + "body": "At most one indigo-filled control exists per view. On the home page it is Create endpoint; on the endpoint page it is Send; on contact it is Send message. Everything else that is actionable is a slate outline or bare label ink that turns indigo on hover.", + "section": "colors" + }, + { + "name": "The Wash-and-Ink Rule", + "body": "Coloured chips are always a ~97%-lightness wash carrying a ~50%-lightness ink of the same hue. There are no saturated fills with white text anywhere except the single primary button.", + "section": "colors" + }, + { + "name": "The Rose Is Removal Rule", + "body": "Rose means something is about to be destroyed — the Delete all button, the per-request delete hover, the DELETE badge. Its one sanctioned exception is the unread dot painted onto the favicon, where it means \"traffic landed while you were away\". Rose is never used for form validation or generic error text.", + "section": "colors" + }, + { + "name": "The Evidence Rule", + "body": "Anything the user's client actually sent is set in monospace; anything httphq says about it is set in sans. A captured value never appears in sans, and interface copy never borrows mono for flavour.", + "section": "typography" + }, + { + "name": "The Two Uppercase Rule", + "body": "Uppercase exists in exactly two places: field labels and method badges. Both are 0.75rem with +0.025em tracking. Nothing else in the system is uppercased — not buttons, not navigation, not headings.", + "section": "typography" + }, + { + "name": "The Hairline-First Rule", + "body": "If a surface needs to be distinguished, give it a hairline seam before you give it a shadow. A panel with no border and a heavier shadow is wrong in this system even when it looks fine in isolation.", + "section": "elevation" + }, + { + "name": "The Shadow-Is-A-Response Rule", + "body": "Nothing above the resting whisper exists at rest. shadow-md appears only as a hover response; there is no shadow-lg or above anywhere, and adding one would break the flatness the board depends on.", + "section": "elevation" + }, + { + "name": "The Three Radii Rule", + "body": "0.25 for data, 0.375 for controls, 0.5 for panels. A new element takes the radius of the category it belongs to, not the radius that looks best next to its neighbour.", + "section": "shapes" + }, + { + "name": "The Dashed-Means-Waiting Rule", + "body": "A dashed border means a container that is correctly empty and expecting content. It is used once — the waiting state — and must never be borrowed for a disabled, errored, or drop-target surface.", + "section": "shapes" + } + ], + "dos": [ + "Do put every new surface on the board field with a panel-white fill, a 1px hairline seam, and the resting whisper shadow. That trio is the system's default surface.", + "Do set anything the client sent in monospace at 0.75rem, and anything httphq says in sans.", + "Do give a new region an uppercase 0.75rem/500/+0.025em label ink heading, and separate its rows with hairline-faint rules instead of nesting another bordered box.", + "Do pair outline: none with a visible focus-visible ring every single time — 2px signal-indigo-lit plus a white offset ring on buttons, 1px focus-indigo plus a border shift on fields.", + "Do take the radius from the category: 0.25rem for data, 0.375rem for controls, 0.5rem for panels.", + "Do design to the 40rem breakpoint alone, stacking below it and going horizontal above it.", + "Do keep icons at 24×24 viewBox, stroke-width 2, fill none, aria-hidden, beside a text label.", + "Do signal background activity in the tab title and favicon rather than in the page." + ], + "donts": [ + "Don't spend blue, emerald, amber, violet or sky on anything but a method badge. That spectrum is the traffic classification and nothing else.", + "Don't add a second indigo-filled button to a view. One primary action per screen, every other control outlined or bare.", + "Don't introduce a saturated fill with white text; coloured chips are a ~97% wash with a ~50% ink of the same hue.", + "Don't use rose for validation errors or generic failure text — rose means removal, plus the unread favicon dot.", + "Don't reach past the hover lift for elevation. There is no shadow-lg in this system, and a borderless card with a bigger shadow is wrong here even when it looks fine alone.", + "Don't add a webfont. The system stack is a performance commitment on a tool whose promise is a working URL in seconds.", + "Don't add a second breakpoint. If a layout can't resolve with sm alone, simplify the layout.", + "Don't uppercase anything that isn't a field label or a method badge.", + "Don't borrow the dashed border for disabled, errored, or drop-target surfaces; it means \"correctly empty, expecting content\".", + "Don't animate the page to announce arrivals, auto-scroll the stream, or add a second looping animation beside the waiting ellipsis.", + "Don't half-migrate the accent toward the mark's periwinkle. Until that convergence is done deliberately across the mark, favicons and UI together, signal-indigo remains the interface accent everywhere." + ] + } +} diff --git a/.impeccable/live/config.json b/.impeccable/live/config.json new file mode 100644 index 0000000..d9ceb1b --- /dev/null +++ b/.impeccable/live/config.json @@ -0,0 +1,6 @@ +{ + "files": ["src/views/layouts/main.html"], + "insertBefore": "", + "commentSyntax": "html", + "cspChecked": true +} diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..6edfcf5 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,522 @@ +--- +name: httphq +description: A live arrivals board for HTTP requests — neutral chassis, one indigo control, color reserved for classifying traffic. +colors: + beacon-periwinkle: "#707ee7" + signal-indigo: "oklch(51.1% 0.262 276.966)" + signal-indigo-lit: "oklch(58.5% 0.233 277.117)" + signal-indigo-deep: "oklch(45.7% 0.24 277.023)" + signal-indigo-wash: "oklch(96.2% 0.018 272.314)" + focus-indigo: "oklch(67.3% 0.182 276.935)" + panel-white: "#ffffff" + board-field: "oklch(98.4% 0.003 247.858)" + hairline-faint: "oklch(96.8% 0.007 247.896)" + hairline: "oklch(92.9% 0.013 255.508)" + edge-control: "oklch(86.9% 0.022 252.894)" + ink-quiet: "oklch(70.4% 0.04 256.788)" + ink-label: "oklch(55.4% 0.046 257.417)" + ink-prose: "oklch(44.6% 0.043 257.281)" + ink-control: "oklch(37.2% 0.044 257.287)" + ink-body: "oklch(27.9% 0.041 260.031)" + ink-strong: "oklch(20.8% 0.042 265.755)" + method-get-ink: "oklch(48.8% 0.243 264.376)" + method-get-wash: "oklch(97% 0.014 254.604)" + method-post-ink: "oklch(50.8% 0.118 165.612)" + method-post-wash: "oklch(97.9% 0.021 166.113)" + method-put-ink: "oklch(55.5% 0.163 48.998)" + method-put-wash: "oklch(98.7% 0.022 95.277)" + method-patch-ink: "oklch(49.1% 0.27 292.581)" + method-patch-wash: "oklch(96.9% 0.016 293.756)" + method-delete-ink: "oklch(51.4% 0.222 16.935)" + method-delete-wash: "oklch(96.9% 0.015 12.422)" + method-options-ink: "oklch(50% 0.134 242.749)" + method-options-wash: "oklch(97.7% 0.013 236.62)" + alert-rose: "oklch(58.6% 0.253 17.585)" + alert-rose-deep: "oklch(51.4% 0.222 16.935)" + alert-rose-edge: "oklch(89.2% 0.058 10.001)" + alert-rose-wash: "oklch(96.9% 0.015 12.422)" + alert-rose-wash-hover: "oklch(94.1% 0.03 12.58)" + syntax-key: "#005cc5" + syntax-string: "#032f62" + syntax-name: "#22863a" + syntax-keyword: "#d73a49" +typography: + display: + fontFamily: "ui-sans-serif, system-ui, sans-serif" + fontSize: "3rem" + fontWeight: 600 + lineHeight: 1 + letterSpacing: "-0.025em" + headline: + fontFamily: "ui-sans-serif, system-ui, sans-serif" + fontSize: "1.875rem" + fontWeight: 600 + lineHeight: 1.2 + letterSpacing: "normal" + title: + fontFamily: "ui-sans-serif, system-ui, sans-serif" + fontSize: "1rem" + fontWeight: 500 + lineHeight: 1.5 + lede: + fontFamily: "ui-sans-serif, system-ui, sans-serif" + fontSize: "1.125rem" + fontWeight: 400 + lineHeight: 1.556 + body: + fontFamily: "ui-sans-serif, system-ui, sans-serif" + fontSize: "0.875rem" + fontWeight: 400 + lineHeight: 1.429 + label: + fontFamily: "ui-sans-serif, system-ui, sans-serif" + fontSize: "0.75rem" + fontWeight: 500 + lineHeight: 1.333 + letterSpacing: "0.025em" + badge: + fontFamily: "ui-sans-serif, system-ui, sans-serif" + fontSize: "0.75rem" + fontWeight: 600 + lineHeight: 1.333 + letterSpacing: "0.025em" + mono: + fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace" + fontSize: "0.75rem" + fontWeight: 400 + lineHeight: 1.333 +rounded: + sm: "0.25rem" + md: "0.375rem" + lg: "0.5rem" +spacing: + "2": "0.5rem" + "3": "0.75rem" + "4": "1rem" + "5": "1.25rem" + "6": "1.5rem" + "8": "2rem" + "12": "3rem" +components: + button-primary: + backgroundColor: "{colors.signal-indigo}" + textColor: "{colors.panel-white}" + typography: "{typography.body}" + rounded: "{rounded.md}" + padding: "0.75rem 1.5rem" + button-primary-hover: + backgroundColor: "{colors.signal-indigo-lit}" + button-primary-compact: + backgroundColor: "{colors.signal-indigo}" + textColor: "{colors.panel-white}" + typography: "{typography.body}" + rounded: "{rounded.md}" + padding: "0.5rem 1rem" + button-secondary: + backgroundColor: "{colors.panel-white}" + textColor: "{colors.ink-control}" + typography: "{typography.body}" + rounded: "{rounded.md}" + padding: "0.5rem 0.75rem" + button-secondary-hover: + backgroundColor: "{colors.board-field}" + button-destructive: + backgroundColor: "{colors.alert-rose-wash}" + textColor: "{colors.alert-rose-deep}" + typography: "{typography.body}" + rounded: "{rounded.md}" + padding: "0.375rem 0.75rem" + button-destructive-hover: + backgroundColor: "{colors.alert-rose-wash-hover}" + input-field: + backgroundColor: "{colors.panel-white}" + textColor: "{colors.ink-body}" + typography: "{typography.body}" + rounded: "{rounded.md}" + padding: "0.5rem 0.75rem" + panel: + backgroundColor: "{colors.panel-white}" + rounded: "{rounded.lg}" + padding: "1.25rem" + code-well: + backgroundColor: "{colors.board-field}" + textColor: "{colors.ink-body}" + typography: "{typography.mono}" + rounded: "{rounded.sm}" + padding: "0.75rem" + method-badge: + typography: "{typography.badge}" + rounded: "{rounded.sm}" + padding: "0.125rem 0.5rem" + icon-tile: + backgroundColor: "{colors.signal-indigo-wash}" + textColor: "{colors.signal-indigo}" + rounded: "{rounded.md}" + width: "2.25rem" + height: "2.25rem" + empty-state: + backgroundColor: "{colors.panel-white}" + textColor: "{colors.ink-label}" + rounded: "{rounded.lg}" + padding: "3rem 1rem" +--- + +# Design System: httphq + +## Overview + +**Creative North Star: "The Arrivals Board"** + +httphq is a board you watch, not a console you operate. Requests land at the +top of the stream, get colour-coded by class, and expire. The whole visual +system is built so a developer working in another window can glance over and +read the board — status first, detail on approach — then go back to what they +were doing. Everything that isn't the arriving traffic is chassis: a slate +field, white panels, hairline seams, and exactly one indigo control per view. + +The register is precise, quiet and fast. Restraint here is not minimalism as a +style choice; it is what makes the board readable. The interface never claims +more than it can prove, never decorates a value it captured, and never puts a +second thing in colour next to a method badge. Type is system-native and +loads instantly — there is no webfont anywhere, and that is a feature of a tool +whose entire promise is being usable within seconds of arrival. + +Four looks are rejected outright, and all four are confirmed prohibitions +rather than taste: marketing-SaaS gloss (gradient meshes, floating mockups, +testimonial carousels), the dense enterprise console (dark chrome, packed +toolbars, panels nested in panels), terminal cosplay (green-on-black, ASCII +framing, faux-CRT effects), and playful dev-tool mascotry (cartoon characters, +blob illustrations, jokey empty states). Monospace appears throughout, but as +evidence handling — never as costume. + +**Key Characteristics:** + +- A slate-50 field with white panels; hairline seams, not shadows, make the edges +- One indigo action per view; every other control is a slate outline or bare text +- Full-spectrum colour is spent entirely on the seven HTTP-method badges +- Everything the client sent is monospace; everything httphq says is sans +- A single breakpoint (40rem) governs the entire responsive system +- Liveness is signalled in the browser chrome — tab title count, favicon dot — not by animating the page + +## Colors + +A near-neutral chassis of eleven slate steps, one indigo accent that carries +every action, and a seven-hue spectrum spent exclusively on classifying +traffic. + +### Primary + +- **Beacon Periwinkle** (`#707ee7`): the brand primary, carried by the diamond + mark in `public/logo.svg` and the favicon set. It is lighter and considerably + softer than the interface accent below. **Recorded intent:** this is the true + brand primary and the interface accent should converge toward it in a future + pass. Until that happens the two coexist and neither moves — do not + half-migrate individual components. +- **Signal Indigo** (`oklch(51.1% 0.262 276.966)`): the current interface + accent. It fills the one primary button per view, sets the endpoint URL in + its deeper step, tints the use-case icon tiles in its lightest wash, and is + the hover colour every quiet control resolves to. Nothing else is indigo. + +### Tertiary + +The method spectrum. Seven hues, each existing only as a wash-and-ink pair on a +badge, and defined in `public/endpoint.js` rather than in markup: + +- **GET — Instrument Blue** (ink `oklch(48.8% 0.243 264.376)` on wash `oklch(97% 0.014 254.604)`): reads, the default arrival. +- **POST — Bench Emerald** (ink `oklch(50.8% 0.118 165.612)` on wash `oklch(97.9% 0.021 166.113)`): creates, and the most common arrival on a webhook endpoint. +- **PUT — Signal Amber** (ink `oklch(55.5% 0.163 48.998)` on wash `oklch(98.7% 0.022 95.277)`): replaces. +- **PATCH — Filament Violet** (ink `oklch(49.1% 0.27 292.581)` on wash `oklch(96.9% 0.016 293.756)`): modifies. +- **DELETE — Warning Rose** (ink `oklch(51.4% 0.222 16.935)` on wash `oklch(96.9% 0.015 12.422)`): removes; shares its hue with the destructive controls, deliberately. +- **HEAD — Neutral Slate** (ink `oklch(37.2% 0.044 257.287)` on wash `oklch(96.8% 0.007 247.896)`): metadata only, so it gets no hue at all. +- **OPTIONS — Preflight Sky** (ink `oklch(50% 0.134 242.749)` on wash `oklch(97.7% 0.013 236.62)`): negotiation, usually a browser's preflight. + +Any method outside this set falls back to the HEAD pair. + +### Neutral + +- **Board Field** (`oklch(98.4% 0.003 247.858)`): the page ground the panels sit on, and — reused deliberately — the fill of every code well and the endpoint URL chip, so raw data reads as recessed into the board. +- **Panel White** (`#ffffff`): every card, disclosure, input and empty state. +- **Hairline** (`oklch(92.9% 0.013 255.508)`): the 1px seam around panels, code wells and the sticky filter rail. The primary edge in the system. +- **Hairline Faint** (`oklch(96.8% 0.007 247.896)`): the internal rules between header rows and detail rows, one step quieter than a panel edge so nested structure never out-shouts the container. +- **Control Edge** (`oklch(86.9% 0.022 252.894)`): the visible stroke on inputs, selects, and secondary buttons; also the dashed edge of the waiting state. +- **Quiet Ink** (`oklch(70.4% 0.04 256.788)`): placeholders and the italic "None" for an absent query string or body. +- **Label Ink** (`oklch(55.4% 0.046 257.417)`): field labels, meta rows, timestamps, and the resting colour of every text-only control. +- **Prose Ink** (`oklch(44.6% 0.043 257.281)`): descriptive sentences on the home and contact pages. +- **Control Ink** (`oklch(37.2% 0.044 257.287)`): secondary button labels, the disclosure summary, and the wordmark. +- **Body Ink** (`oklch(27.9% 0.041 260.031)`): default document text. +- **Strong Ink** (`oklch(20.8% 0.042 265.755)`): headings and card titles. + +### Syntax Highlighting + +The one part of the palette httphq does not author. Captured JSON, XML and +multipart bodies are highlighted by the pinned highlight.js GitHub light theme +(`@highlightjs/cdn-assets@11.10.0/styles/github.min.css`), which ships plain +sRGB hex outside the OKLCH system above. Verified against rendered captures: + +- **Key Blue** (`#005cc5`): object keys, numbers, booleans and the XML prolog — `hljs-attr`, `hljs-number`, `hljs-literal`, `hljs-meta`. +- **String Navy** (`#032f62`): every quoted string value — `hljs-string`. +- **Element Green** (`#22863a`): XML element names — `hljs-name`. +- **Keyword Red** (`#d73a49`): language keywords — `hljs-keyword`. +- Punctuation, braces and tag brackets carry no colour of their own; `hljs-punctuation` and `hljs-tag` inherit Body Ink, which is what keeps a highlighted payload from turning into confetti. + +### Named Rules + +**The Borrowed Palette Rule.** The four syntax colours are a vendored theme, not +system tokens. Never restyle individual `hljs-*` classes to bring them closer to +the slate-and-indigo world — either swap the whole theme or leave it alone. A +half-retinted syntax palette reads as a bug in the highlighter. + +**The Board Rule.** Full-spectrum colour belongs to the method badge and +nothing else. If a new element wants blue, emerald, amber, violet or sky, the +answer is no — that vocabulary means "this is the class of traffic that +arrived", and every additional user dilutes the only colour-coding on the page. + +**The One Indigo Rule.** At most one indigo-filled control exists per view. On +the home page it is *Create endpoint*; on the endpoint page it is *Send*; +on contact it is *Send message*. Everything else that is actionable is a slate +outline or bare label ink that turns indigo on hover. + +**The Wash-and-Ink Rule.** Coloured chips are always a ~97%-lightness wash +carrying a ~50%-lightness ink of the same hue. There are no saturated fills +with white text anywhere except the single primary button. + +**The Rose Is Removal Rule.** Rose means something is about to be destroyed — +the *Delete all* button, the per-request delete hover, the DELETE badge. Its one +sanctioned exception is the unread dot painted onto the favicon, where it means +"traffic landed while you were away". Rose is never used for form validation or +generic error text. + +## Typography + +**Display / Body Font:** system UI sans (`ui-sans-serif, system-ui, sans-serif`) +**Label/Mono Font:** system monospace (`ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace`) + +**Character:** There is no webfont, and that is a decision, not an omission — a +tool promising a working URL in seconds cannot spend its first paint on a font +request. The pairing is the operating system's own voice against its own +terminal voice, which is exactly the contrast the product needs: httphq speaks +in sans, the captured traffic speaks in mono. + +### Hierarchy + +- **Display** (600, 3rem desktop / 1.875rem below 40rem, line-height 1, -0.025em): the home hero headline. Appears once, on one page. +- **Headline** (600, 1.875rem / 1.5rem below 40rem, line-height 1.2): page titles outside the hero, e.g. *Get in touch*. +- **Title** (500, 1rem): card headings and the disclosure summary. Medium weight, never semibold — titles mark a region, they don't compete with the display. +- **Lede** (400, 1.125rem, line-height ~1.56): the one paragraph under the hero, capped at 36rem. +- **Body** (400, 0.875rem, line-height ~1.43): the working size of the entire application chrome. Most of the product is set at 14px, not 16px. +- **Label** (500, 0.75rem, +0.025em, uppercase): field labels — TIME, CLIENT IP, PATH, HEADERS, QUERY STRING, BODY, YOUR UNIQUE URL. +- **Badge** (600, 0.75rem, +0.025em, uppercase): method badges only. +- **Mono** (400, 0.75rem): every captured value — headers, paths, IPs, query strings, bodies, the endpoint URL, and the request UUID. + +### Named Rules + +**The Evidence Rule.** Anything the user's client actually sent is set in +monospace; anything httphq says about it is set in sans. A captured value never +appears in sans, and interface copy never borrows mono for flavour. + +**The Two Uppercase Rule.** Uppercase exists in exactly two places: field +labels and method badges. Both are 0.75rem with +0.025em tracking. Nothing else +in the system is uppercased — not buttons, not navigation, not headings. + +## Layout + +A single centred column: `max-w-5xl` (64rem) with 1rem gutters, widening to +1.5rem at the 40rem breakpoint. The page is a flex column with the footer +pushed to the bottom, so short pages still plant the footer at the viewport +edge rather than floating it mid-screen. + +Measure is capped per content type rather than globally: the hero block at +42rem, its lede paragraph at 36rem, the use-case grid at 56rem, and the contact +form at 36rem. Nothing runs the full 64rem except the request stream, which +needs the width for header tables and body payloads. + +**One breakpoint.** The entire system responds at `sm` (40rem) and nowhere +else — there is no `md`, `lg`, or `xl` anywhere in the templates. Below it the +layout is a single stacked column with 1rem gutters and the smaller step of +each type pair; above it the use-case grid becomes two columns, the URL row and +filter bar become horizontal, and secondary button labels appear next to their +icons. Design new surfaces to the same discipline: if a layout needs a second +breakpoint, it is probably too complex for this product. + +Vertical rhythm runs on the 0.25rem base scale, using 0.5 / 0.75 / 1 / 1.25 / +1.5 / 2 / 3rem steps. Panels carry 1rem of internal padding below the +breakpoint and 1.25rem above it; the contact form is the one exception at 1.5 → +2rem, because it is a destination rather than a working surface. + +The filter rail on the endpoint page is sticky at the top of the viewport, +bleeding into the gutters with a negative inline margin so its rules run edge +to edge, and it uses a 95%-opaque field colour over an 8px backdrop blur so the +stream reads as passing underneath it. It is the only sticky element and the +only backdrop filter in the system. + +Detail and header rows are flex rows, not a two-column grid. Above the +breakpoint the label holds a fixed 10rem column with a 0.75rem gap and the value +takes the remainder; below it the label stacks above its value and both run the +full row width, so a long path or header value gets the whole screen instead of +competing with a label column that a phone cannot afford. Each row is its own +element, so its hairline-faint divider spans the entire row rather than sitting +over one column. + +Header lists scroll internally at 16rem, bodies at 24rem — long payloads never +push the next request off the board. + +## Elevation & Depth + +Hairline-first. The 1px hairline seam *is* the edge of a surface; the shadow +underneath it is a whisper whose only job is to stop a white panel from looking +pasted onto the slate field. Depth in this system is carried by the tonal step +between board field and panel white, reinforced by a seam — not by lift. + +### Shadow Vocabulary + +- **Resting whisper** (`box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)`): every panel, card, disclosure and primary button at rest. Barely perceptible by design. +- **Hover lift** (`box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)`): applied only to the six use-case cards on the home page, and only on hover. + +### Named Rules + +**The Hairline-First Rule.** If a surface needs to be distinguished, give it a +hairline seam before you give it a shadow. A panel with no border and a heavier +shadow is wrong in this system even when it looks fine in isolation. + +**The Shadow-Is-A-Response Rule.** Nothing above the resting whisper exists at +rest. `shadow-md` appears only as a hover response; there is no `shadow-lg` or +above anywhere, and adding one would break the flatness the board depends on. + +## Shapes + +Three radii, and each one encodes what a thing is: + +- **0.25rem** — data. Code wells, the endpoint URL chip, method badges. The tightest corner, for things that hold captured bytes. +- **0.375rem** — controls. Every button, input, select, textarea and the use-case icon tiles. +- **0.5rem** — panels. Cards, disclosures, the empty state, request articles. + +Borders are 1px and always present on a surface that has an edge; there are no +borderless cards. Request articles clip their contents so the header's bottom +rule meets the panel's rounded corner cleanly. + +Iconography is a single family: 24×24 viewBox, `fill="none"`, +`stroke="currentColor"`, `stroke-width="2"`, rendered at 1rem inside controls +and 1.25rem inside icon tiles. Icons inherit their colour from the control and +are always `aria-hidden`, because every icon in this system sits beside a text +label rather than replacing one. + +The family has no exceptions: there is no emoji anywhere in the product. The +retention notice carries a stroked alert triangle from the same set, inheriting +label ink like every other icon. Keep it that way — an emoji brings its own +colour and its own per-platform rendering, and one is enough to break the +uniformity that makes this icon set read as a system. + +### Named Rules + +**The Three Radii Rule.** 0.25 for data, 0.375 for controls, 0.5 for panels. A +new element takes the radius of the category it belongs to, not the radius that +looks best next to its neighbour. + +**The Dashed-Means-Waiting Rule.** A dashed border means a container that is +correctly empty and expecting content. It is used once — the waiting state — and +must never be borrowed for a disabled, errored, or drop-target surface. + +## Components + +### Buttons + +- **Character:** calm instruments. Sized for accuracy rather than presence; nothing asks to be admired. +- **Shape:** control radius (0.375rem) on every variant. +- **Primary:** signal indigo fill, white label, medium weight, resting whisper shadow. Two sizes only — 0.75rem/1.5rem padding at 1rem type for the page's main action, and 0.5rem/1rem at 0.875rem inside panels. +- **Secondary:** white fill, control-edge stroke, control-ink label at 0.875rem medium, 0.5rem/0.75rem padding. Hovers to the board field. +- **Destructive:** rose wash fill, rose edge stroke, deep rose label, 0.375rem/0.75rem padding. Hovers one wash step darker. +- **Text-only:** no fill, no border, label ink at 0.75–0.875rem, resolving to signal indigo on hover — or to alert rose when the action deletes. Used for *Copy*, *Copy request*, and per-request *Delete*. +- **Hover / Focus:** fills shift one step lighter on primary, one step darker on destructive. Focus is never suppressed: `outline: none` is always paired with a 2px `focus-visible` ring in **signal indigo lit** — the lighter step, not the fill colour — with a 2px white offset ring on filled buttons, so the ring reads against the indigo it sits on. Destructive controls ring in alert rose instead. +- **Disabled:** 50% opacity and `not-allowed` cursor; used on *Copy all* when the stream is empty. + +### Cards / Containers + +- **Corner Style:** panel radius (0.5rem). +- **Background:** panel white on the board field. +- **Shadow Strategy:** resting whisper only; the six home-page use-case cards are the sole surfaces that lift on hover. +- **Border:** 1px hairline, always. +- **Internal Padding:** 1rem below 40rem, 1.25rem above. +- **Composition:** a request article is a header rule-separated from its body, with the method badge and UUID on the left and the copy/delete actions on the right; the body is a stack of labelled regions rather than a nested set of boxes. Its key/value rows follow the flex behaviour described in Layout — 10rem label column above the breakpoint, label stacked above value below it. + +### Inputs / Fields + +- **Style:** white fill, 1px control-edge stroke, control radius, 0.5rem/0.75rem padding, 0.875rem type. Textareas and header/body fields use the mono stack with `spellcheck="false"`; ordinary text fields use sans. +- **Focus:** `outline: none` paired with a 1px **focus indigo** ring and a **signal indigo lit** border — a tighter, quieter treatment than the 2px ring on buttons, because a focused field is already unambiguous. Note the three-way split: fields ring in indigo-400, buttons ring in indigo-500, and only fills use indigo-600. +- **Labels:** the uppercase label style, 0.25–0.375rem above the field. The contact form is the one place labels are sentence-case medium body text, because it is a public form rather than an instrument panel. +- **Select:** native `appearance: none` with a slate chevron inlined as a data-URI background, 1.1em, positioned 0.5rem from the right with 2rem of padding reserved. Note this chevron is the single literal hex in the system (`#64748b`) and is the slate-500 equivalent. + +### Navigation + +There is none, and that is deliberate: the header is a centred wordmark linking +home, at 1.875rem bold with tight tracking in control ink. Every other route is +reachable from the footer — feedback, the GitHub repository with an inline +brand glyph, and the Formspark credit — set at 0.875rem label ink, centred, +hovering to signal indigo. + +### Method Badge + +The signature component. A 0.25rem chip, 0.5rem/0.125rem padding, 0.75rem +semibold uppercase with +0.025em tracking, carrying one of the seven wash-and-ink +pairs. It is the first thing on a request header and the only saturated colour +in the stream. Its class map lives in `public/endpoint.js`, not in markup, so a +new method is a one-line addition in one place. + +### Body Rendering + +A captured body is displayed through one of four paths, chosen by its +`Content-Type`. All four land in the same code well — board-field fill, hairline +border, data radius, 0.75rem mono, scrolling internally at 24rem — so the +container never signals which path ran; only the content does. + +- **JSON** — reparsed, pretty-printed at two-space indent, then highlighted. +- **multipart/form-data** — parsed into a part list and serialised as a JSON array of `{name, value}` for fields and `{name, filename, contentType, size}` for files, then highlighted as JSON. A file's bytes are never shown, only its declared metadata. +- **XML** — highlighted in place, unformatted; the payload keeps whatever whitespace it arrived with. +- **Anything else** — HTML-escaped raw text, no highlighting, no reformatting. + +An absent body or query string renders as an italic *None* in quiet ink rather +than an empty well, so a card with nothing in it still reads as a complete +record. Escaping happens on every path — a captured body is attacker-controlled +text and is never trusted as markup. + +### Waiting State + +Panel-white, dashed control-edge border, 3rem/1rem padding, centred label ink: +a 1rem line reading *Waiting for requests* followed by an animated ellipsis, and +a 0.875rem line explaining that requests will appear in real time. The ellipsis +is a CSS `content` animation on four steps over 1.2s — the only looping motion +in the product. + +### Liveness Indicator + +When a request arrives while the tab is hidden, the count is prefixed to the +document title and the favicon is repainted on a canvas: a slate-800 disc, a +white lowercase *h*, and a rose-600 dot at the upper right. Restoring +visibility clears both. This is where the board announces itself — the page +body never flashes, animates, or auto-scrolls to claim attention. + +## Do's and Don'ts + +### Do: + +- **Do** put every new surface on the board field with a panel-white fill, a 1px hairline seam, and the resting whisper shadow. That trio is the system's default surface. +- **Do** set anything the client sent in monospace at 0.75rem, and anything httphq says in sans. +- **Do** give a new region an uppercase 0.75rem/500/+0.025em label ink heading, and separate its rows with hairline-faint rules instead of nesting another bordered box. +- **Do** pair `outline: none` with a visible `focus-visible` ring every single time — 2px signal-indigo-lit plus a white offset ring on buttons, 1px focus-indigo plus a border shift on fields. +- **Do** take the radius from the category: 0.25rem for data, 0.375rem for controls, 0.5rem for panels. +- **Do** design to the 40rem breakpoint alone, stacking below it and going horizontal above it. +- **Do** keep icons at 24×24 viewBox, `stroke-width="2"`, `fill="none"`, `aria-hidden`, beside a text label. +- **Do** signal background activity in the tab title and favicon rather than in the page. + +### Don't: + +- **Don't** spend blue, emerald, amber, violet or sky on anything but a method badge. That spectrum is the traffic classification and nothing else. +- **Don't** add a second indigo-filled button to a view. One primary action per screen, every other control outlined or bare. +- **Don't** introduce a saturated fill with white text; coloured chips are a ~97% wash with a ~50% ink of the same hue. +- **Don't** use rose for validation errors or generic failure text — rose means removal, plus the unread favicon dot. +- **Don't** reach past the hover lift for elevation. There is no `shadow-lg` in this system, and a borderless card with a bigger shadow is wrong here even when it looks fine alone. +- **Don't** add a webfont. The system stack is a performance commitment on a tool whose promise is a working URL in seconds. +- **Don't** add a second breakpoint. If a layout can't resolve with `sm` alone, simplify the layout. +- **Don't** uppercase anything that isn't a field label or a method badge. +- **Don't** borrow the dashed border for disabled, errored, or drop-target surfaces; it means "correctly empty, expecting content". +- **Don't** animate the page to announce arrivals, auto-scroll the stream, or add a second looping animation beside the waiting ellipsis. +- **Don't** half-migrate the accent toward the mark's periwinkle. Until that convergence is done deliberately across the mark, favicons and UI together, `signal-indigo` remains the interface accent everywhere. diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..61ce696 --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,157 @@ +# Product + + + +## Platform + +web + +## Users + +Two primary users, designed for together rather than in sequence: + +- **The mid-debug first-timer.** Arrives from a search result or a colleague's + link while stuck on a webhook, form post, or third-party integration. Needs a + working capture URL within seconds of landing, and leaves once they + understand what was actually sent. Has no prior knowledge of httphq and will + not read documentation to get started. +- **The returning power user.** Already knows the tool and comes back + regularly. Values speed, method filtering, full-text search, HAR export, and + the send-a-test-request panel over any explanation of what httphq is. + +Both reach the same surfaces; nothing gates or personalises for either, because +there are no accounts. Self-hosters (Docker, env-var configuration) are a real +audience served by the project, but not the audience the interface is designed +around. + +## Product Purpose + +httphq generates a unique, disposable HTTP endpoint and shows every request +sent to it in real time — method, path, client IP, headers, query string, and +body — so a developer can see exactly what a client, provider, or device puts +on the wire. + +Success is a developer answering "what did it actually send?" in the shortest +possible path from landing to certainty: one click to a URL, point a client at +it, watch the request appear without a refresh. + +## Positioning + +Three things a neighbouring tool could not truthfully copy in combination: + +- **No signup, no friction.** One click produces a working URL. No account, no + email, no verification, no quota wall. +- **Open source and self-hostable.** MIT licensed, a single Go binary and a + Docker image, so the same tool can be run against traffic that must not reach + a third-party service. +- **Honest, unpolluted captures.** Generic forwarding headers (`X-Forwarded-*`, + `Via`, `Trace*`, `X-Real-Ip`) and the configured platform's vendor headers + (`Cf-*`, `Fly-*`) are stripped before display, so users inspect their own + payload rather than the noise of whatever host sits in front of httphq. + +Formspark sponsors the project and is credited in the footer and README. That +sponsorship is a fact to preserve, not a positioning claim: httphq is not +positioned as a marketing surface for Formspark. + +## Operating Context + +- The user is mid-task in another tool — a provider dashboard, a terminal, an + HTML form, a device, a scheduler — and httphq is the second window they keep + open beside it. +- The capture URL is pasted into somewhere else entirely (Stripe/GitHub/Slack + webhook settings, a form `action`, a curl command, firmware config) and then + the user returns to httphq to watch. +- Capture URLs are routinely shared: anyone holding the URL can read the same + live stream, which is how two people on opposite sides of an integration + settle whose payload is malformed. +- Sessions are short and bursty. A page may sit empty and waiting, then receive + a burst of requests, then be abandoned. + +## Capabilities and Constraints + +Confirmed functionality: + +- Create an endpoint with one POST; the ID is a generated haiku-style slug + (`lowercase-words-and-digits`, max 64 chars). Capture URL is `/to/`; + the inspection page is `/`. +- Captured per request: UUID, method, path, query string, body, headers, + client IP, timestamp. +- Live delivery over WebSocket (`/ws/`), with the scheme tracking the page + scheme so HTTPS pages use `wss://`. +- Filter by HTTP method; server-side substring search across headers, query + string, and body. +- Delete a single request or every request for an endpoint. +- Copy a single request or all visible requests as HAR-shaped JSON; copy + headers or body alone. +- Send a test request to your own endpoint from the page (method, headers, + body). +- Content-type-aware body rendering: pretty-printed and highlighted JSON, + multipart/form-data part list, XML highlighting, escaped raw text otherwise. +- Pages: home (`/`), endpoint (`/`), contact (`/contact`). `/api/health` + and `/api/debug` exist for operations, not for users. + +Technical constraints: + +- Retention is 4 hours; a cron sweep runs every 5 minutes. +- Storage is SQLite on the container's writable layer. Capture history is lost + on restart, by design. No durable store, no migration path. +- Request body limit is 1 MiB. +- The request list returns at most 128 requests, newest first. +- Rate limit is 125 requests per minute per client IP in production, bucketed + on the platform-resolved IP. +- Client IP resolution is a trust decision driven by the `PLATFORM` env var; + setting it trusts that platform's header unconditionally. +- The listen port (8080) is a constant, not configurable. +- Endpoint IDs are generated words, not secrets. There is no authentication and + no access control on an endpoint; possession of the URL is the only gate, and + IDs are guessable in principle. +- `robots.txt` allows only `/` and `/contact`; endpoint pages are disallowed. + +Terminology: *endpoint* (the generated capture target), *request* (one captured +call), *capture URL* (`/to/`), *HAR* (the export shape). + +Undecided / not established: whether the 4-hour window, the 128-request list +cap, or the 1 MiB body limit should ever be surfaced as configurable to users. + +## Brand Commitments + +- Name is lowercase `httphq`, always. Canonical host is `httphq.com`. +- Existing marks: `public/logo.svg` (a periwinkle-indigo diamond, `#707ee7`), + `public/logo.png`, and a full favicon set (`favicon.ico`, 16/32 PNG, + `apple-touch-icon.png`, Android Chrome 192/512). +- The footer credits Formspark as sponsor and links the GitHub repository + (`formspark/httphq`). Both must remain reachable. +- Voice in existing copy is plain, concrete, developer-to-developer, with no + marketing inflation. Sentence case; no exclamation marks. + +## Evidence on Hand + +Real: the working product itself (a live capture stream is the demonstration), +the MIT licence, the public GitHub repository and its CI badges, the Formspark +sponsorship, and the logo/favicon assets above. + +Absent, and must not be fabricated: testimonials, named customers, usage or +traffic numbers, uptime or performance benchmarks, awards, press coverage, +pricing, team or company claims beyond the Formspark sponsorship, and any +security or compliance certification. + +## Product Principles + +1. **Nothing stands between landing and a live URL.** Any addition that delays + or conditions the first capture is working against the product. +2. **Free and anonymous, permanently.** No accounts, no auth, no quotas, no + paid tier. Design and feature decisions may not assume a logged-in user or a + future one. +3. **Ephemeral is the product, not a limitation.** Short retention and loss on + restart are deliberate. Do not design toward archives, history, or durable + storage. +4. **Show their traffic, not ours.** Infrastructure and vendor noise stays + hidden; what is displayed should be what the client actually sent. +5. **Serve the first-timer and the regular in one surface.** The fast path must + stay obvious to someone who has never seen httphq, without slowing down + someone who uses it weekly. + +## Accessibility & Inclusion + +No product-specific standard has been established. Nothing in the product's +audience or context relaxes ordinary accessibility expectations for a web tool. diff --git a/docs/scripts.md b/docs/scripts.md index dc677ae..dc41af1 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -16,7 +16,7 @@ go mod tidy Run project: ```bash -go run ./src/application.go +go run ./src ``` Run unit tests: diff --git a/src/application.go b/src/application.go index bdb6545..9f0c49a 100644 --- a/src/application.go +++ b/src/application.go @@ -95,6 +95,35 @@ func resolvePlatform(name string) platformConfig { return platforms["direct"] } +// contentSecurityPolicy is the CSP sent on every response, assembled once at +// startup because its only variable part is fixed for the process lifetime. +// +// - script-src needs 'unsafe-eval' for Alpine (it compiles directive +// expressions via the Function constructor) and the Tailwind Play CDN +// (compiles utility classes at runtime). All page scripts are external so +// script-src does NOT need 'unsafe-inline'. +// - style-src needs 'unsafe-inline' because Tailwind Play CDN injects +// generated styles into