Copilot / AI agent instructions for statewave-web
June 12, 2026 · View on GitHub
These conventions should be followed when generating or modifying code in this repository. They reflect deliberate UI/UX decisions, not stylistic preferences.
Heading anchors (mandatory for all section titles)
Every <h1> / <h2> / <h3> that introduces a navigable section of a page
must be rendered with the <Heading> component
(src/components/Heading.tsx) — never as a
raw <h2> tag.
import { Heading } from '../components/Heading'
// Correct
<Heading id="core-loop" className="text-2xl font-bold text-theme-primary mb-12">
The core loop
</Heading>
// Incorrect — no anchor affordance, no shareable link
<h2 className="text-2xl font-bold text-theme-primary mb-12">The core loop</h2>
Why
Visitors should be able to deep-link any section of the marketing site. The
component renders a permanently visible # icon next to the title; clicking
it copies an absolute URL with the section hash to the clipboard. Without it,
sharing a specific subsection means screenshotting or asking a teammate to
scroll.
Rules
- Always pass an explicit
id. Slug-style: lowercase, dashes, no stop words. Stable across copy edits — once shipped, the id is part of the URL contract. Examples:core-loop,privacy,who-this-is-for,vs-alternatives. - One id per page. Don't reuse an id between two
<Heading>instances or between a<Heading>and a<Section id=...>— duplicate ids are invalid HTML and break browser anchor resolution. - Default
levelis 2. Passlevel={1 | 3 | 4}when needed. Page-title<h1>doesn't need<Heading>(a hero title is rarely deep-linked). - Inherit existing typography via
className— pass the same Tailwind classes the original<h2>used. The component does not impose a font size or weight; it only adds the anchor button. scroll-mt-20is built in so anchor jumps land below the fixed navbar. Do not add scroll offsets to the className.
When NOT to use <Heading>
- Inside the chat widget, or any other floating / modal UI where the URL hash isn't meaningful.
- For decorative typography that isn't an actual section header (e.g. a hero subline styled like an h2 but not introducing a section).
Cursor affordances (handled globally)
src/index.css declares site-wide cursor rules:
<button>,<a href>,[role="button"],<summary>,<label for=…>, checkboxes, radios →cursor: pointer- Disabled /
aria-disabled="true"controls →cursor: not-allowed
You don't need to add per-element cursor-pointer Tailwind utilities —
they're redundant with the global rule. Only override when the element
should not feel clickable (e.g. a static badge styled like a chip).
Demo CTA tracking
Any page-level "Try the demo" / "Live Demo" CTA must register itself with
useTrackDemoCta(ref) (see src/lib/widget-context.tsx).
The floating chat-launcher hides itself while a tracked CTA is in the
viewport so visitors don't see the same affordance twice.
import { useTrackDemoCta } from '../lib/widget-context'
const ctaRef = useRef<HTMLButtonElement>(null)
useTrackDemoCta(ctaRef)
return <button ref={ctaRef} onClick={openWidget}>Try the demo</button>
Confirmation dialogs
Never use window.confirm() or window.alert() for destructive actions —
they break the visual frame. Use a themed in-widget modal instead
(see the reset-memory dialog in
src/components/ChatWidget.tsx for the
canonical pattern).
External docs links
When a feature or claim has matching documentation in
statewave-docs, link to it directly with target="_blank" rel="noopener noreferrer". Don't paraphrase doc content into the page when
a link will do — the docs are the source of truth.
Common destinations:
- Compiler modes →
architecture/compiler-modes.md - Privacy & data flow →
architecture/privacy-and-data-flow.md - Ranking →
architecture/ranking.md - Hardware & scaling →
deployment/hardware-and-scaling.md - API reference →
api/v1-contract.md - Getting started →
getting-started.md - Python SDK → repo
smaramwbc/statewave-py - TypeScript SDK → repo
smaramwbc/statewave-ts
Language: English-only, with one exception
The entire marketing site is in English. Every page, every section, every card, every CTA, every microcopy string. Do not localize, translate, or add non-English copy anywhere — even when the user's request is ambiguous about language scope.
The single exception: the manifesto on /why
The "Why we built Statewave" manifesto in
src/pages/WhyPage.tsx (rendered by
ManifestoHero) is the only translatable surface on the site. It has its
own translations table in
src/lib/manifesto-i18n.ts and a dedicated
language picker.
Rules for the manifesto:
- All strings live in
MANIFESTO_TRANSLATIONSkeyed by language code. - The picker auto-detects
navigator.languageon first load and persists the user's choice tolocalStorageunderstatewave:manifesto-lang. - The brand name Statewave is never translated. Other proper nouns stay in their natural rendering for each language.
- Translations are poetic, not literal. If a phrase doesn't carry in the target language, rewrite it so it does — the throughline ("only memories matter") must land emotionally in every locale, not just be grammatically correct.
If a future request asks to translate any other part of the site, push back and confirm — that's a deliberate scope expansion, not a default.
Statewave memory — MCP server statewave, subject repo:smaramwbc.statewave-web.
Before answering questions about this project, call statewave_get_context (that subject, query = the ask) and ground your answer in it.
When the user states a durable fact or decision, call statewave_ingest_episode then statewave_compile_subject (same subject). Never invent Statewave results.