diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3de0e07..9bf4dd9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,6 +19,22 @@ jobs: go-version-file: "go.mod" cache: false - run: go test ./... + styles: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version-file: ".tool-versions" + - run: npm ci + # public/app.css is committed so the binary and container need no Node. + # A rebuild that changes it means a template or script gained a class the + # committed sheet does not carry, and the running app would be missing it. + - name: Rebuild stylesheet and check it matches the committed one + run: | + npm run css + git diff --exit-code --stat public/app.css \ + || { echo "::error::public/app.css is stale. Run 'npm run css' and commit the result"; exit 1; } e2e: runs-on: ubuntu-latest defaults: diff --git a/.impeccable/critique/2026-08-08T10-17-31Z__src-views-index-html.md b/.impeccable/critique/2026-08-08T10-17-31Z__src-views-index-html.md new file mode 100644 index 0000000..856b6d0 --- /dev/null +++ b/.impeccable/critique/2026-08-08T10-17-31Z__src-views-index-html.md @@ -0,0 +1,115 @@ +--- +target: landing page +total_score: 21 +max_score: 40 +na_heuristics: +p0_count: 2 +p1_count: 2 +timestamp: 2026-08-08T10-17-31Z +slug: src-views-index-html +--- +Method: dual-agent (A: design review · B: detector + browser evidence, run isolated and in parallel) + +## Design Health Score + +| # | Heuristic | Score | Key Issue | +|---|-----------|-------|-----------| +| 1 | Visibility of System Status | 2 | `Create endpoint` has no submit state: no disabled, no pending label. On a slow link the primary action is inert for seconds. | +| 2 | Match System / Real World | 3 | The lede promises "timing", but the product captures a relative `Time` timestamp, not duration or latency. "Endpoint" is system jargon for what the visitor wants: a URL to paste. | +| 3 | User Control and Freedom | 3 | One action, no traps, footer exits present: but no preview of what you get before an irreversible-feeling commit. | +| 4 | Consistency and Standards | 3 | `Common use cases` is an uppercase h2 at 0.875rem, breaking DESIGN.md's Two Uppercase Rule and its 0.75rem Label token. `hover:shadow-md` sits on non-interactive articles. | +| 5 | Error Prevention | 1 | No double-submit guard, no CSRF token on `POST /endpoint`, and no statement that the capture URL is public and unauthenticated before you point live webhook traffic at it. | +| 6 | Recognition Rather Than Recall | 1 | The entire product must be imagined. No screenshot, no sample capture, no method badge, no JSON body anywhere on the page. | +| 7 | Flexibility and Efficiency | 2 | Not n/a: PRODUCT.md names the returning regular a co-primary user who transits this page weekly. No autofocus, no Enter-to-create, no browser-local list of endpoints they made today. | +| 8 | Aesthetic and Minimalist Design | 3 | Genuinely restrained; all four anti-references avoided. But ~60% of mobile height is six cards carrying about three distinct ideas. | +| 9 | Error Recovery | 2 | Production rate limit is 125 req/min per IP; a rejected `POST /endpoint` has no designed surface on this page. | +| 10 | Help and Documentation | 1 | Not n/a: the first-timer needs four facts before clicking: free? account? how long does it live? is it public? None appear. | +| **Total** | | **21/40 (52.5%)** | **Acceptable** | + +No heuristic scored n/a. Heuristics 7 and 10 both genuinely apply: 7 because a returning power user is a stated co-primary persona who passes through this exact page, and 10 because "no docs needed" covers operating the tool, not deciding to adopt it. + +## Design Specificity Verdict + +**LLM assessment: a neighbouring dev tool could ship this unchanged.** Swap the wordmark, the button label and the six card titles and this is Webhook.site, RequestBin, Beeceptor or Pipedream. The structure is the canonical open-source-dev-tool template, centred wordmark, centred display h1 with a hard `
`, 18px lede, one filled CTA with a trailing arrow, uppercase micro-label, 2×3 bordered card grid, three grey footer links, executed cleanly and without a single deviation. + +The chassis is genuinely authored and the page obeys DESIGN.md exactly. But the chassis is the deliberately neutral part of the system, and this page uses only the neutral part: + +- **The method badge, DESIGN.md's named signature component and the one place the system licenses full-spectrum colour, appears zero times.** +- The mono/sans evidence split survives as exactly one token: `action` in card two. +- `public/logo.svg`, the periwinkle diamond named in PRODUCT.md as the brand mark, is referenced by zero templates. The page renders 0 `` elements. +- The six icons are stock Feather glyphs chosen by literal noun-matching: a clock for cron, a phone for mobile. None encodes anything about HTTP. + +The North Star is "The Arrivals Board." The front of the arrivals board shows no arrivals. + +**Deterministic scan: 0 findings, exit 0, across all four templates.** The detector did not choke on Go template syntax. But treat this as weak evidence, not a clean bill of health: these files are fragments with no `` and no stylesheet, and every visual property lives in Tailwind utility strings resolved at runtime, so the static scanner had no CSS to analyse. The mode that would have covered rendered CSS, URL scanning, failed: `puppeteer is required for URL scanning`, and **it exited 0 anyway**, so a CI job invoking URL mode would silently pass on a broken toolchain. + +**Overlay: injection succeeded and the detector ran in the page.** Console reported exactly one message: `[impeccable] No anti-patterns found.` Explicit re-runs of `impeccableDetectAsync()` and `impeccableScanAsync()` both returned `[]`. The overlay session has since been torn down, so no overlay is visible in the browser now. + +## Overall Impression + +The craft is real and the restraint is correct. What is missing is not polish, it is evidence and argument. This page describes a live stream of HTTP requests in 170 words of prose and demonstrates it nowhere, then asks for a click. Every one of PRODUCT.md's three "a competitor could not truthfully copy this" claims, no signup, open source and self-hostable, unpolluted captures: is absent from the rendered page. Word counts on the live DOM: `account` 0, `free` 0, `open source` 0, `self-host` 0, `4 hour` 0. + +The single biggest opportunity is the cheapest one on the page: render one honest static capture, a POST badge, a path, three headers, a five-line JSON body, using components that already exist, and put two sentences of trust copy under the button. + +## What's Working + +**The restraint is real and correct for this product.** All four confirmed anti-references are genuinely avoided: no gradient mesh, no tilted dashboard mockup, no testimonial carousel, no green-on-black, no mascot. Measured contrast passes everywhere: h1 at 17.04:1, body copy at 7.58:1, CTA label at 6.46:1, worst case 4.55:1. The page earns "precise, quiet, fast" instead of claiming it. + +**The hero is single-action and that shape is right.** One indigo fill, no competing CTA, no newsletter, no nav to get lost in, five tab stops on the whole page. The One Indigo Rule is honoured exactly, and the CTA's focus ring is the only authored focus style on the page: a 2px indigo-500 ring over a 2px white offset, measured and correct. For a user who is mid-debug and impatient, "click or don't" is the right decision space. + +**Sentence-level copy voice.** "point any client at it", "see exactly what it puts on the wire: headers, body, every byte", and card six's *"is the payload from your side malformed, or mine?"* are written by someone who has had that argument at 6pm on a Friday. That last line is the only sentence on the page a competitor could not have written: and it is buried at the bottom of the sixth card, below the fold on every viewport tested. + +## Priority Issues + +### [P0] The product is invisible on the page that has to sell it +**Why it matters:** PRODUCT.md's Evidence section says it plainly: *"a live capture stream is the demonstration."* The only real evidence this project has, it declines to show. Heuristic 6 and the working-memory checklist item both fail as a direct consequence, and the visitor is asked to spend a click on faith at the moment they are least patient. +**Fix:** Put one static, honest capture between the CTA and the grid: a POST badge in Bench Emerald, `/to/`, three header rows (`content-type`, `user-agent`, `stripe-signature`), five lines of pretty-printed JSON in the borrowed hljs palette. Pure markup, zero JS, on existing panel/hairline/code-well components. This fabricates nothing: it renders the product's own output, not a testimonial or a metric. +**Suggested command:** `$impeccable bolder` + +### [P0] Every positioning claim and every reassurance at the commit point is absent +**Why it matters:** The three claims are free to state, true, and unfalsifiable by a competitor. Meanwhile the CTA creates a resource that is public, unauthenticated, guessable-in-principle and dead in four hours: and states none of that before the click. Retention surfaces only afterward, on the endpoint page. The no-auth fact is never stated anywhere; card six sells shareability as a benefit and never names its corollary. +**Fix:** One line of body copy directly under `Create endpoint`: *"No account, no email, free forever. Requests are deleted after 4 hours, and anyone with the URL can read them."* Plus a footer-adjacent line: *"Open source, MIT licensed, self-hostable: a single Go binary."* Two sentences close the persuasion gap, the trust gap and the proximity failure at once. +**Suggested command:** `$impeccable clarify` + +### [P1] The page ships 42.9% of its bytes to code it cannot use, and compiles 100% of its CSS in the browser +**Why it matters:** Measured on `/`: 158,273 B transferred, 82.2% third-party, of which **67,910 B (42.9%) is dead weight**, highlight.js (43,616 B) with 0 highlight targets on the page, Alpine (16,136 B) with 0 directives, plus `endpoint.js`, `har.js`, `render-body.js`. `document.styleSheets` shows **no first-party stylesheet at all**: every style is generated at runtime by a render-blocking 271 KB third-party script that Tailwind documents as development-only. If jsDelivr is slow, the page renders unstyled. This on a project whose design system explicitly rejects webfonts as a performance commitment. Also: no `preconnect`/`dns-prefetch` for either third-party origin, and `favicon.ico` alone is 15,706 B, 56% of first-party bytes. +**Fix:** Build Tailwind at compile time into a static stylesheet served from the Go binary. Move highlight.js, Alpine, `endpoint.js`, `har.js` and `render-body.js` out of `layouts/main.html` into `endpoint.html` where they are used. +**Suggested command:** `$impeccable optimize` + +### [P1] Six cards advertise interactivity they do not have +**Why it matters:** Each `
` carries `hover:shadow-md transition` with `cursor: auto`, no href, no handler, no tabindex, no role. DESIGN.md's Shadow-Is-A-Response Rule says elevation above the resting whisper exists only as a response: and a response implies an action. Six surfaces lift under the cursor and do nothing, on a page where the visitor is hunting for what to click. Worse, the grid is a six-option decision point where every option resolves to no action, and cards one, three and five are three framings of the same activity. +**Fix:** Pick a side. Either make each card a real link that creates an endpoint and lands with a matching example pre-filled in the *Send a test request* panel, or delete `hover:shadow-md` and let them sit flat. Cut six to three either way. +**Suggested command:** `$impeccable distill` + +### [P2] Neither arrival path PRODUCT.md names is served by the page's metadata +**Why it matters:** The `` is literally `httphq`. No `og:*`, no `twitter:*`, no canonical. PRODUCT.md says the first-timer *"arrives from a search result or a colleague's link"*: a search result shows the bare word "httphq"; a Slack paste unfurls as a naked grey link with no image, title or description. `robots.txt` already allows only `/` and `/contact`, so this is the indexed surface. +**Fix:** `<title>httphq: inspect HTTP requests in real time`, the four `og:` tags, `twitter:card=summary_large_image`, and a canonical. The `og:image` is the obvious first home for `public/logo.svg`, currently used by nothing a human sees at size. +**Suggested command:** `$impeccable clarify` + +## Persona Red Flags + +**Jordan (first-timer, mid-debug, from a search result)**: `Create endpoint` is the wrong noun, Jordan wants a URL to paste into the Stripe webhook field open in the next tab, not an "endpoint". Nothing on the page says free or no-signup, and Jordan has been burned before. The lede promises "timing", which the product does not capture, a credibility debit inside the first 90 seconds. Jordan's first sight of the actual product is an empty dashed box reading "Waiting for requests…", because the landing page did nothing to establish what will fill it. + +**Riley (stress tester)**: Double-click on `Create endpoint` over a slow link fires two POSTs, no disabled state, nothing in `index.js`. The form has **no CSRF token and no confirmation step**, which Assessment B demonstrated accidentally: its automated interaction probe submitted the hero form and created a real endpoint record on the running server. Any cross-origin form post can mint endpoints. The 125 req/min rate limit has no designed error surface here. Riley reads card six, correctly infers there is no auth, and finds nothing to size the risk with. The header `` is 976px wide at 1440, clicking anywhere in the header band navigates. + +**Casey (mobile, 390×844)**: All three footer links fail the WCAG 2.2 AA 24×24 minimum: measured 119.5×17, 138×20 and 71×17, with 13.5px and 16.5px vertical gaps, under the 24px spacing exception that would otherwise rescue them. They are the only escape hatches on the page. 1.87 screens of scroll, ~900px of it the six stacked cards. `Create endpoint` is a 195px pill inside a 358px column rather than the full-width control the system's own stacking implies. No repeated or sticky CTA, so the only action is off-screen for the last 1.2 screens. + +**Sam (the integrations engineer who opens httphq three times a week, derived from PRODUCT.md's returning power user)**: Sam's entire relationship with `/` is land, click, leave, and Sam re-reads the same six cards every visit, extracting nothing. Method filtering, full-text search, HAR export and the send-a-test-request panel, the four things PRODUCT.md names as *why Sam returns*, appear on this page zero times; Sam cannot send a colleague a link to a page describing what the tool does for a regular. Sam wants the endpoint from twenty minutes ago and there is no path to it: a browser-local list of "endpoints you created here (they expire in 4 hours)" honours both the no-accounts and the ephemeral principle exactly, and its absence reads as a decision never made rather than one made and rejected. + +## Minor Observations + +- `
` duplicates the visible `

`: screen readers announce the name twice. Use `aria-labelledby` or drop one. +- Heading order is clean (h1 → h2 → 6× h3, no skips), `lang="en"` present, all 5 interactive elements have accessible names, all 8 SVGs correctly hidden, `rel="noopener"` present. Zero console messages and 10/10 successful requests on load. +- Only the CTA has an authored focus style; the four links fall back to Chrome's UA default blue, which is engine-specific and unrelated to the indigo system. The CTA's ring also *fades in* via `transition` rather than appearing instantly. +- LCP element is the hero lede `

`, not the `

`: the largest painted element is the supporting copy, not the headline. +- No `prefers-reduced-motion` rule exists in any loaded stylesheet while 7 elements carry `.transition`. +- Parallelism breaks on card four: five titles are verb-first (Test, Inspect, Debug, Verify, Share), and "Mobile & IoT traffic" names a channel instead of a job. +- Footer says *"A community project by Formspark"*; PRODUCT.md records Formspark as **sponsor**. Worth aligning. +- Measured performance is excellent where localhost can measure it: LCP 128ms, CLS 0, TTFB 2ms. The payload issue above is specifically the cold-cache, real-network case localhost cannot reproduce. + +## Questions to Consider + +1. The North Star is "The Arrivals Board." What is the argument for describing a live stream in six paragraphs instead of rendering one honest static capture beneath the button, from components that already exist and cost nothing to place? +2. Three claims sit in PRODUCT.md as the things no neighbouring tool could truthfully copy, and none of the three appear on this page. What is the six-card grid buying that those three sentences aren't? +3. The CTA creates a public, unauthenticated, guessable-in-principle URL that dies in four hours, and says none of that before the click. Which of those six facts would you be uncomfortable stating above the button: and what does that discomfort tell you? +4. Half this page's height exists to persuade a first-timer who "will not read documentation to get started", while the regular has read those cards fifty times. If neither audience reads the grid, who is it for? diff --git a/.impeccable/design.json b/.impeccable/design.json index 0fa7959..3fd676c 100644 --- a/.impeccable/design.json +++ b/.impeccable/design.json @@ -225,7 +225,7 @@ }, "mono": { "displayName": "Evidence Mono", - "purpose": "Every captured value — headers, paths, IPs, query strings, bodies, endpoint URL, request UUID." + "purpose": "Every captured value: headers, paths, IPs, query strings, bodies, endpoint URL, request UUID." } }, "shadows": [ @@ -237,7 +237,7 @@ { "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." + "purpose": "Defined but currently unused. Elevation change is reserved for surfaces that respond to the cursor; every card in the product is static content." } ], "motion": [ @@ -260,6 +260,11 @@ "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." + }, + { + "name": "reduced-motion", + "value": "@media (prefers-reduced-motion: reduce)", + "purpose": "Transitions and the waiting ellipsis collapse to a resting state. All motion here is decorative, so removing it substitutes cleanly." } ], "breakpoints": [ @@ -298,7 +303,7 @@ "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.", + "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; }" }, @@ -330,7 +335,7 @@ "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.", + "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": "
", "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; }" }, @@ -361,24 +366,24 @@ ], "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.", + "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" + "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.", + "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.", + "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" }, { @@ -393,7 +398,7 @@ }, { "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.", + "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" }, { @@ -403,7 +408,7 @@ }, { "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.", + "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" }, { @@ -423,7 +428,7 @@ }, { "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.", + "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" } ], @@ -431,7 +436,7 @@ "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 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.", @@ -441,7 +446,7 @@ "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 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.", diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..908f4a7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +# Generated by `npm run css`. Prettier would reformat the minified output and +# the next build would revert it, so the CI freshness check could never pass. +public/app.css + +# Build output and dependencies +bin +node_modules +e2e/test-results +e2e/playwright-report + +# Impeccable critique archives are generated snapshots, not source. +.impeccable/critique diff --git a/DESIGN.md b/DESIGN.md index 6edfcf5..d013d3a 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,6 +1,6 @@ --- name: httphq -description: A live arrivals board for HTTP requests — neutral chassis, one indigo control, color reserved for classifying traffic. +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)" @@ -170,7 +170,7 @@ components: 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 +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. @@ -178,7 +178,7 @@ 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 +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 @@ -187,7 +187,7 @@ 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. +evidence handling: never as costume. **Key Characteristics:** @@ -196,7 +196,7 @@ evidence handling — never as costume. - 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 +- Liveness is signalled in the browser chrome, tab title count, favicon dot, not by animating the page ## Colors @@ -210,7 +210,7 @@ traffic. 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 + 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 @@ -222,19 +222,19 @@ traffic. 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. +- **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. +- **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. @@ -253,35 +253,34 @@ 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`. +- **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 +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 +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 +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 +**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. @@ -291,22 +290,24 @@ generic error text. **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 +**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. +request. The same commitment governs the stylesheet: it is built ahead of time +from `src/styles/app.css` into a first-party `/app.css`, so no page waits on a +third party to have a layout. 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. +- **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, because titles mark a region rather than competing 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. +- **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. +- **Mono** (400, 0.75rem): every captured value: headers, paths, IPs, query strings, bodies, the endpoint URL, and the request UUID. ### Named Rules @@ -316,7 +317,7 @@ 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. +in the system is uppercased: not buttons, not navigation, not headings. ## Layout @@ -331,7 +332,7 @@ 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 +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 @@ -357,20 +358,20 @@ 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 +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 +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. +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. +- **Hover lift** (`box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)`): defined but **currently unused**. It was on the home page's use-case cards, which are not interactive: a surface that lifts under the cursor and then does nothing reads as a broken link. Available for a genuinely interactive card if one ever exists; not to be reintroduced on static content. ### Named Rules @@ -386,9 +387,9 @@ above anywhere, and adding one would break the flatness the board depends on. 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. +- **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 @@ -402,7 +403,7 @@ 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 +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. @@ -413,7 +414,7 @@ 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 +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 @@ -422,36 +423,44 @@ must never be borrowed for a disabled, errored, or drop-target surface. - **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. +- **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. +- **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. +- **Shadow Strategy:** resting whisper only. No surface in the product lifts on hover: elevation change is reserved for something that responds to the cursor, and every card here is static content. - **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. +- **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. +- **Focus:** `outline: none` paired with a 1px **focus indigo** ring and a **signal indigo lit** border. That is 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. +There is none, and that is deliberate: the header is the periwinkle mark beside +a centred wordmark linking home, at 1.875rem bold with tight tracking in control +ink. The link wraps only the mark and wordmark, never the full header band. +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. + +Every link carries its own padding and a 2.75rem minimum height. As bare inline +text their hit area collapses to the text box, which lands under the 24px target +minimum and makes the page's only exits hard to hit on a phone. Links also carry +the same authored `focus-visible` ring as buttons rather than falling back to the +browser's default outline, which is engine-specific and belongs to no design +system. ### Method Badge @@ -464,34 +473,47 @@ 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 +`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. +- **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 +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 +record. Escaping happens on every path: a captured body is attacker-controlled text and is never trusted as markup. +### Browser Surfaces + +The parts the product does not draw still carry its palette, set once in the +base layer: text selection is indigo wash behind indigo ink rather than the +browser's blue, and `accent-color` is signal indigo so native checkboxes and the +search field's clear affordance match the one accent. `color-scheme` is declared +light, so form controls and scrollbars render in the world the system actually +commits to rather than inverting under a dark OS preference. + +Motion respects `prefers-reduced-motion`: transitions and the waiting ellipsis +collapse to a resting state. Every animation in this product is decorative, none carries meaning that is lost when it is removed, so reducing it is a clean +substitution, not a degradation. + ### 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 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 +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 +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 @@ -501,7 +523,7 @@ body never flashes, animates, or auto-scrolls to claim attention. - **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** 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. @@ -512,7 +534,7 @@ body never flashes, animates, or auto-scrolls to claim attention. - **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** 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. diff --git a/PRODUCT.md b/PRODUCT.md index 61ce696..20cf20f 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -27,8 +27,8 @@ 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 +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 @@ -55,8 +55,8 @@ 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 +- 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 @@ -107,8 +107,8 @@ Technical constraints: 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). +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. diff --git a/docs/scripts.md b/docs/scripts.md index dc41af1..4068bbc 100644 --- a/docs/scripts.md +++ b/docs/scripts.md @@ -4,6 +4,7 @@ Install dependencies: ```bash go mod download +npm install cd e2e && npm install && npx playwright install chromium ``` @@ -13,6 +14,18 @@ Upkeep dependencies: go mod tidy ``` +Build the stylesheet: + +```bash +npm run css +``` + +`public/app.css` is generated from `src/styles/app.css` and is committed, so the +binary, the container, and `go run` never need Node. Rebuild it whenever a +template or a script gains a class the sheet does not already carry, and commit +the result; CI fails if the committed file differs from a fresh build. During +design work, `npm run css:watch` regenerates on save. + Run project: ```bash diff --git a/e2e/tests/home-screen.spec.ts b/e2e/tests/home-screen.spec.ts index 0bd56f3..5cc6383 100644 --- a/e2e/tests/home-screen.spec.ts +++ b/e2e/tests/home-screen.spec.ts @@ -6,7 +6,9 @@ test.describe("Home screen", () => { }); test("title should be correct", async ({ page }) => { - await expect(page).toHaveTitle("httphq"); + await expect(page).toHaveTitle( + "httphq: inspect HTTP requests in real time", + ); }); test("page renders the hero copy", async ({ page }) => { @@ -33,6 +35,18 @@ test.describe("Home screen", () => { const section = page.locator('[data-test="use-cases"]'); await expect(section).toBeVisible(); await expect(section).toContainText("Test webhooks"); - await expect(section).toContainText("Inspect form payloads"); + await expect(section).toContainText("Inspect payloads"); + }); + + test("example capture shows a rendered request", async ({ page }) => { + const example = page.locator('[data-test="example-capture"]'); + await expect(example).toBeVisible(); + await expect(example).toContainText("POST"); + await expect(example).toContainText("content-type"); + await expect(example).toContainText("payment_intent.succeeded"); + }); + + test("the page ships no javascript", async ({ page }) => { + await expect(page.locator("script")).toHaveCount(0); }); }); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1eed89d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1030 @@ +{ + "name": "httphq-assets", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "httphq-assets", + "devDependencies": { + "@tailwindcss/cli": "4.2.4", + "tailwindcss": "4.2.4" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/cli": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.4.tgz", + "integrity": "sha512-e87GGhuXxnyQPyA0TS8an/3wNpj+OUmx8u0F4BicYr48TF72032AIu5917rRYaWm7HorXi3GSZ/uG+ohqP6AKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "@tailwindcss/node": "4.2.4", + "@tailwindcss/oxide": "4.2.4", + "enhanced-resolve": "^5.19.0", + "mri": "^1.2.0", + "picocolors": "^1.1.1", + "tailwindcss": "4.2.4" + }, + "bin": { + "tailwindcss": "dist/index.mjs" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", + "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", + "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-arm64": "4.2.4", + "@tailwindcss/oxide-darwin-x64": "4.2.4", + "@tailwindcss/oxide-freebsd-x64": "4.2.4", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", + "@tailwindcss/oxide-linux-x64-musl": "4.2.4", + "@tailwindcss/oxide-wasm32-wasi": "4.2.4", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", + "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", + "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", + "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", + "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", + "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", + "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", + "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", + "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", + "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", + "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", + "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", + "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", + "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ac93bf8 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "httphq-assets", + "private": true, + "description": "Build-time stylesheet generation. The Go application does not depend on npm at runtime; public/app.css is committed and served from the binary's static directory.", + "scripts": { + "css": "tailwindcss --input ./src/styles/app.css --output ./public/app.css --minify", + "css:watch": "tailwindcss --input ./src/styles/app.css --output ./public/app.css --watch" + }, + "devDependencies": { + "@tailwindcss/cli": "4.2.4", + "tailwindcss": "4.2.4" + } +} diff --git a/public/app.css b/public/app.css new file mode 100644 index 0000000..42422c4 --- /dev/null +++ b/public/app.css @@ -0,0 +1,2 @@ +/*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-amber-50:oklch(98.7% .022 95.277);--color-amber-700:oklch(55.5% .163 48.998);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-700:oklch(50.8% .118 165.612);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-700:oklch(50% .134 242.749);--color-blue-50:oklch(97% .014 254.604);--color-blue-700:oklch(48.8% .243 264.376);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-indigo-900:oklch(35.9% .144 278.697);--color-violet-50:oklch(96.9% .016 293.756);--color-violet-700:oklch(49.1% .27 292.581);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-100:oklch(94.1% .03 12.58);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-700:oklch(51.4% .222 16.935);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-xl:36rem;--container-2xl:42rem;--container-4xl:56rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}button:not(:disabled),summary,[role=button]:not(:disabled){cursor:pointer}::selection{background-color:var(--color-indigo-100);color:var(--color-indigo-900)}:root{accent-color:var(--color-indigo-600);color-scheme:light}}@layer components{.app-select{appearance:none;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-position:right .5rem center;background-repeat:no-repeat;background-size:1.1em;padding-right:2rem}.loading-dots:after{content:"";text-align:left;width:1.5ch;animation:1.2s steps(4,end) infinite httphq-dots;display:inline-block}}@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.top-0{top:calc(var(--spacing) * 0)}.z-10{z-index:10}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.-mx-4{margin-inline:calc(var(--spacing) * -4)}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-auto{margin-top:auto}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-9{height:calc(var(--spacing) * 9)}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-96{max-height:calc(var(--spacing) * 96)}.min-h-11{min-height:calc(var(--spacing) * 11)}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-9{width:calc(var(--spacing) * 9)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.flex-shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-rose-200{border-color:var(--color-rose-200)}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-blue-50{background-color:var(--color-blue-50)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-rose-50{background-color:var(--color-rose-50)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-50\/95{background-color:#f8fafcf2}@supports (color:color-mix(in lab, red, red)){.bg-slate-50\/95{background-color:color-mix(in oklab, var(--color-slate-50) 95%, transparent)}}.bg-slate-100{background-color:var(--color-slate-100)}.bg-violet-50{background-color:var(--color-violet-50)}.bg-white{background-color:var(--color-white)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pt-12{padding-top:calc(var(--spacing) * 12)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pb-10{padding-bottom:calc(var(--spacing) * 10)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-pretty{text-wrap:pretty}.break-all{word-break:break-all}.text-\[\#005cc5\]{color:#005cc5}.text-\[\#032f62\]{color:#032f62}.text-amber-700{color:var(--color-amber-700)}.text-blue-700{color:var(--color-blue-700)}.text-emerald-700{color:var(--color-emerald-700)}.text-indigo-600{color:var(--color-indigo-600)}.text-indigo-700{color:var(--color-indigo-700)}.text-rose-700{color:var(--color-rose-700)}.text-sky-700{color:var(--color-sky-700)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-violet-700{color:var(--color-violet-700)}.text-white{color:var(--color-white)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.no-underline{text-decoration-line:none}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.select-all{-webkit-user-select:all;user-select:all}.group-open\:rotate-180:is(:where(.group):is([open],:popover-open,:open) *){rotate:180deg}.placeholder\:text-slate-400::placeholder{color:var(--color-slate-400)}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.hover\:bg-rose-100:hover{background-color:var(--color-rose-100)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:text-indigo-600:hover{color:var(--color-indigo-600)}.hover\:text-rose-600:hover{color:var(--color-rose-600)}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}}.focus\:border-indigo-500:focus{border-color:var(--color-indigo-500)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-indigo-400:focus{--tw-ring-color:var(--color-indigo-400)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-visible\:ring-indigo-500:focus-visible{--tw-ring-color:var(--color-indigo-500)}.focus-visible\:ring-rose-500:focus-visible{--tw-ring-color:var(--color-rose-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:40rem){.sm\:-mx-6{margin-inline:calc(var(--spacing) * -6)}.sm\:inline{display:inline}.sm\:w-40{width:calc(var(--spacing) * 40)}.sm\:w-auto{width:auto}.sm\:max-w-\[10rem\]{max-width:10rem}.sm\:shrink-0{flex-shrink:0}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-8{padding:calc(var(--spacing) * 8)}.sm\:px-5{padding-inline:calc(var(--spacing) * 5)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:py-8{padding-block:calc(var(--spacing) * 8)}.sm\:py-12{padding-block:calc(var(--spacing) * 12)}.sm\:pt-16{padding-top:calc(var(--spacing) * 16)}.sm\:pb-5{padding-bottom:calc(var(--spacing) * 5)}.sm\:pb-8{padding-bottom:calc(var(--spacing) * 8)}.sm\:pb-12{padding-bottom:calc(var(--spacing) * 12)}.sm\:pb-14{padding-bottom:calc(var(--spacing) * 14)}.sm\:text-right{text-align:right}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.sm\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.sm\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}}}@keyframes httphq-dots{0%{content:""}25%{content:"."}50%{content:".."}75%,to{content:"..."}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}.loading-dots:after{content:"...";animation:none}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false} \ No newline at end of file diff --git a/public/index.js b/public/index.js index 24d0a3d..db181da 100644 --- a/public/index.js +++ b/public/index.js @@ -1,4 +1,5 @@ -/* Shared helpers loaded on every page. */ +/* Shared helpers for the endpoint page. Loaded only where the capture stream + renders; the marketing and contact pages ship no JavaScript at all. */ const TIME_DIVISIONS = [ { amount: 60, name: "seconds" }, @@ -61,22 +62,3 @@ window.parseHeaderLines = function (text) { } return out; }; - -/* App-wide style overrides (loading-dots animation, native-select chevron, - pointer cursor on actionable elements). Injected once at startup. */ - -(function injectAppStyles() { - if (document.getElementById("httphq-app-style")) return; - const style = document.createElement("style"); - style.id = "httphq-app-style"; - style.textContent = [ - // Loading dots in the empty state. - ".loading-dots::after{content:'';display:inline-block;width:1.5ch;text-align:left;animation:httphq-dots 1.2s steps(4,end) infinite}", - "@keyframes httphq-dots{0%{content:''}25%{content:'.'}50%{content:'..'}75%,100%{content:'...'}}", - // Tailwind v4 doesn't set cursor:pointer on buttons by default; restore it. - "button:not(:disabled),summary,[role=button]:not(:disabled){cursor:pointer}", - // Custom select chevron, anchored right after the value with sane padding. - "select.app-select{appearance:none;-webkit-appearance:none;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;padding-right:2rem}", - ].join(""); - document.head.appendChild(style); -})(); diff --git a/public/robots.txt b/public/robots.txt index 917d9ed..1c62193 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,4 +1,7 @@ User-agent: * Allow: /$ Allow: /contact$ +Allow: /app.css +Allow: /social-card.png +Allow: /logo.svg Disallow: / diff --git a/public/social-card.png b/public/social-card.png new file mode 100644 index 0000000..fb3c270 Binary files /dev/null and b/public/social-card.png differ diff --git a/src/application.go b/src/application.go index 9f0c49a..d14ab08 100644 --- a/src/application.go +++ b/src/application.go @@ -98,13 +98,14 @@ func resolvePlatform(name string) platformConfig { // 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