{
  "markdown": "# Changelog\n\nAll notable changes to the Sporting Scouter design system (`ui/`, `theme.css`, and the published reference site) are documented here.\n\n## [3.5.1] - 2026-10-08\n\nConsumers: run `ds:sync`. This release changes `theme.css` (one keyframe), so a monorepo sync is needed. Two fixes for defects found by an app consuming 3.5.0 (both older than 3.5.0), and one documentation decision.\n\n### Fixed\n\n- **Dialog off-centre**: the dialog is centred with Tailwind v4 `-translate-x-1/2 -translate-y-1/2`, which sets the CSS `translate` property, while the `ss-pop-in` keyframe also centred it with `transform: translate(-50%, -50%)`. Both applied, so the dialog ended up half its width to the left and half its height up (measured: a 300x200 box ended at -150, -100 from the centre). The keyframe now animates `translate` and `scale`, and ends exactly where the recipe centres it. `theme.css` only; `dialog()` is unchanged.\n- **`ChoiceGroup` did not type-check**: it passed `error && string` and `hint && string` to `describedBy()`, which takes `string | false | null | undefined`. `error` and `hint` are `ReactNode`, so the expression can be `0` and `tsc --strict` failed (TS2345) for any app importing the `ui-react` index. It now passes `error ? ... : undefined`.\n- Guard test `dialog-motion.test.mjs`: fails on both old forms.\n\n### Changed\n\n- **Like is no longer documented as gated by a feature flag.** The review card reference page and its snippet render **Like** on every written public review (a score-only row has none). What a signed-out visitor must do to like or to report is the app's decision. No recipe change.\n\n### Notes\n\n- Copy and URLs are not defined in the design system: the wording of \"Incentivised review\", the list summary, \"How reviews are checked\" and the dispute states, and where the verification link points, belong to product and legal. The reference page's wording tables are examples.\n\n## [3.5.0] - 2026-10-08\n\nConsumers: run `ds:sync`. Additive, with one removal (`header: 'legacy'`, see Removed): no slot was removed or renamed and every default is unchanged. `bash docs/superpowers/plans/assets/review-card-recipes/check.sh` runs the real recipes (real `tailwind-variants` and `tailwind-merge`) on `main` and on this tree for the 3.4.0-era `reviewCard`, `reviewBreakdown`, `reviewList`, `reviewReply`, `scoreBar`, `ratingStars` and `button` calls: 521 slot outputs have identical class sets, with one intended exception: the review card `footer` now wraps (`flex-wrap`), so actions do not spill out of a narrow card. `theme.css` and the tokens are unchanged.\n\n### Added\n\n- **`reviewCard()` redesign** (`/review-card/`): `layout: 'row'` renders a review with no text as one compact band (the card's identity block with the date below the name, the figure and stars, and Report) instead of a near-empty box. A `headerEnd` slot holds the figure and the stars; the figure is `ratingStars().value`, so 4.6 is no longer shown as five bare stars. Padding moved from `surface` to `compoundVariants`, so a row can be shorter than a card.\n- **One action row, after the review**: every action is in `footer`, in the same place on every surface, after the body and any reply. Public: **Like** (leading, on every written public review; a `like` slot, an `aria-pressed` toggle with a constant label and a filled icon). Organiser: **Reply publicly** and **Reply in private** (trailing). **Report** is always available, on every review and every surface, for anyone who can see it, and is always the last action, at the right edge: on a score-only row too (the row's children are flat, identity, figure and stars, Report, so Report stays on the right when the row wraps).\n- **Two reply threads with different audiences**: the **public reply** (`thread`: one per review, public once the review is published) and the **private thread** (`privateThread`, `privateTitle`, `privateHint`, `privateStatus`). In the API today the private thread is the **dispute timeline** (the organiser's statement, the athlete's evidence, Sporting Scouter's decision), only while a review is held before publication, and the athlete answers from an emailed link, not a signed-in view. `reviewReply()` gains `visibility: 'public' | 'private'` (private: a chat bubble), `from: 'self' | 'other' | 'platform'` (the viewer's own messages on the right, tinted; the other party's on the left; Sporting Scouter full width with its icon) and `audience`, `attachment` and `event` slots. Who wrote what is never position or tint alone: the sender's name is always written (yours as \"{name} (you)\") with a caption of what they did. The audience and the state are written (icon plus words, `statusDot` plus words), never colour alone. While the thread is **open** the footer, right after the chat, has **Reply in private** (a \"Private message\" form that names who can see it); once it is **closed**, or the review is published, the thread stays readable and the button is gone. The public reply shows its audience in three states: only your team, public once the review is published (a held review), visible to everyone. The reference page lists the suggested wording for every dispute state and event. The package renders the private thread only where the app passes it: the API must not send it to a public payload. `ui/GAPS.md` G11 records the open product question (is \"Reply in private\" the dispute flow renamed, or new free messaging?).\n- **Incentivised-review disclosure**: a `disclosure` slot in `reviewCard()`, visible text with the `gift` icon, always the last line of the identity block (under the name and the date), on every surface and in both layouts (the same stack in a row), never a tooltip and never colour alone. It is not a new provenance component or token. The requirement (EU Omnibus Directive) and the wording are product and legal copy, to be confirmed by legal; the package ships no strings. \n- **List summary** (`reviewList().summary`, `summaryItem`): a line above every review list, outside the `<ul>`: \"12 of 156 reviews were incentivised\" (the server's total across all pages, never the visible page; shown only above zero; the `gift` icon of the per-review disclosure) and a link, always present, to how reviews are checked. Industry practice (Bazaarvoice shows incentivised against organic counts) and the Omnibus Directive's duty to say whether and how reviews are verified (UCPD Article 7(6)) are the reasons; the wording of the verification statement the link opens is product and legal copy.\n- **`reviewBreakdown()`**: `summaryMain`, `summaryAttention` and `summaryAttentionItem` slots. The collapsed organiser summary shows **Needs attention** only when a category scores below 3.5 (below `very-good` in `ratingOf()`): up to two categories as compact score bars, lowest first, so triage does not need the panel open. When every category is at 3.5 or above it shows nothing extra. The average is no longer repeated there; the header shows it. The cut is product policy.\n- **`scoreBar({ layout: 'cell' })`**: an auto-width row with a fixed 96px track. Closes the \"fixed track width\" gap listed on `/dashboard-home-structure/`; the dashboard home template can adopt it in its own release.\n- Guard test `review-card.test.mjs`: the redesign stays additive; the figure, the aria-label and the filled stars agree; every bar colour is what `ratingOf()` returns; \"Needs attention\" lists only categories below 3.5, lowest first, and is absent when none is; an incentivised review is always identified; every action is in the one footer row and none in the header; a private conversation never appears on a public surface and the public reply states its audience; both forms have a visible label, a hint that names the audience and a hidden start state; the private thread shows its state and who did what in order; the demo CSS holds no hex or palette utility; the three structure snippets call only slots and variants that exist.\n\n### Changed\n\n- On the reference page, category bars now take the colour `ratingOf()` gives their value (before, 4.5 and 4.7 were drawn in two different tiers), the reply form's inline `style` is a class, and opening one reply form closes the other.\n- The organiser's single \"Reply\" becomes **Reply publicly** and **Reply in private** (the latter only while the review is held before publication). The old placeholder (\"Respond as the event organiser…\") is gone: the hint under the visible label carries the guidance.\n\n### Fixed\n\n- The structure snippets called `button({ scale: 'xs' })`. `button` has no `scale` prop; it is `size`.\n- `/dashboard-home-structure/`: the fixed-width gap is marked closed.\n\n### Removed\n\n- `reviewCard({ header: 'legacy' })`: the avatar-beside-identity row, unused by the reference and by the apps. `header: 'split'` is still accepted, so a call that passes it explicitly keeps type-checking. `avatar`, `meta`, `translation` and `actions` stay: the apps use them.\n\n### Notes\n\n- A `gauge` is not used on the card: its recipe allows one per view, and a ring per review cannot be compared. The solid rating `badge` is not used for the categories that need attention: white on amber measures about 3.4:1.\n- New strings the apps must add (the package ships none): `reviews.incentivised`, `reviews.incentivisedSummary`, `reviews.howChecked`, `reviews.like`, `reviews.report`, `reviews.needsAttention`, `reviews.categoryCount`, `reviews.replyPublicly`, `reviews.replyInPrivate`, `reviews.audienceTeam`, `reviews.audiencePending`, `reviews.audiencePublic`, `reviews.privateThread`, `reviews.privateOnlyYouAnd`, `reviews.senderYou`, `reviews.dispute.<STATE>` and `reviews.event.<type>` (captions such as \"Submitted a dispute\", without the actor's name).\n\n## [3.4.0] - 2026-10-08\n\nConsumers: run `ds:sync`. Documentation only: no recipe, token or CSS changes.\n\n### Added\n\n- **Dashboard home: structure** (`/dashboard-home-structure/`): one copyable recipe snippet for each block of the Dashboard home template (hero, stats row, onboarding guide, tables, rating cell, needs-attention queue, one-company glance, page layout), each marked as an existing recipe or a composition. It lists what the recipes still lack (a touch-sized text button, a `steps` spoken-state slot, a narrow-screen table fold, a fixed-width `scoreBar` track, a split `itemList` row, a `grid` main-plus-aside, an ink `badge`) with a fallback for each. The Dashboard home template links to it and its catalog description points to it.\n- Guard test `dashboard-home-structure.test.mjs` reads every slot and variant from the recipe sources and checks each call in the snippets; `docs/superpowers/plans/assets/dashboard-home-snippets/check.sh` type-checks them against the real recipes in strict mode.\n\n### Notes\n\n- Measured on the card surface, `badge()` success and destructive text is 2.65:1 and 2.42:1 in the dark theme (4.88:1 and 5.66:1 in light), below the AA minimum for small text. The template draws those badges with ink text; the recipe still ships status-coloured text. Tracked as a follow-up.\n\n## [3.3.0] - 2026-10-07\n\nConsumers: run `ds:sync`. This release adds a CSS file (`@sportingscouter/ui/shell-product.css`, imported after the theme) and new exports; `theme.css` and the tokens are unchanged. See the notes at the end.\n\n### Added\n\n- **Number formatting** (`lib/format-number`): `formatCount`, `formatRating`, `roundRating`, `formatDelta` and `deltaSign`, over `Intl.NumberFormat`, with the locale passed by the app and no strings in the package. The thousands separator is forced, so pt-PT shows `1 240`. `roundRating` is the single source for the figure and the tier, so `3.45` is shown as `3.5` and coloured as `very-good`. Reference page: `/format-number/`.\n- **`stat()`**: a figure with a label, an optional signed delta and a note, in one hairline plane per row (`columns` is the number of stats). The arrow takes the tone; the delta figure and the error message stay in ink. `loading`, `empty` and `error` states for a single value. Reference page: `/stat/`.\n- **Rating `gauge()`**: a `rating` variant (the colour is the tier, never a prop; unrated shows the track and a dash), a `tier` caption slot at `lg`, and `gaugeRing()` / `gaugeArc()` in `lib/gauge`, which return the SVG numbers and `null` when there is no value. Sizes are now 40 / 64 / 96px (plus `xs` 16px) with strokes 4 / 6 / 8.\n- **`steps()` locked state**: a dashed outline marker, a muted title, a written `reason` and an optional right-aligned `time`. The steps page documents all four progress states, with `aria-current=\"step\"` and spoken words for done and current.\n- **Product shell CSS** (`ui/src/styles/shell-product.css`, exported as `@sportingscouter/ui/shell-product.css`): the rail and shell rules the `business` app used, moved into the design system, every selector scoped under `:where(.appshell--product)`. `appShell()` in `sideNav` mode now adds that class to its root, and `appShell()` and `sideNav()` gain slots for the class names the app wrote by hand (`mobbar`, `mobbrand`, `burger`, `productMain`, `bodyInner`; `railInner`, `act`, `rule`, `groupHd`, `chev`, `list`, `orgMeta`, `icon`, `avatar`, `foot`, `footLocale`, and the variants `groupKind`, `closed`, `current`, `linkKind`, `listDepth`, `nested`). Existing outputs are unchanged. `docs/superpowers/plans/assets/product-shell/HANDOFF.md` says which rules `business` may delete and which it must keep.\n- **Dashboard home template** (`/dashboard-home/`): the organiser home in five situations, composing the pieces above. Every rating bar and the gauge take the tier of their number, and guard tests enforce the layout rules.\n- Site: `shared.css` (the stat, steps and gauge mirrors, loaded by the docs and the template pages) and a dashboard block in `page.css`; guard tests for the new pages, for icon placeholders and for the built CSS.\n\n### Changed\n\n- **Gauge sizes and strokes** changed (were 16 / 32 / 48 / 64px with strokes 2 / 3 / 4 / 5). No known consumer used `gauge()`.\n- The gauge arc transition is `duration-150` with `motion-reduce:transition-none` (was `duration-300`, with no reduced-motion handling), and the figure uses fixed type steps (`text-meta`, `text-lead`, `text-h3`) so it never collides with the ring.\n- `/side-nav/`: the live sample is now the Side nav shell template in a frame, replacing the simplified `.sb` sample (organisation switcher, Home / Events / Reputation). The `.sb` code is removed.\n\n### Fixed\n\n- `typography` and `side-nav` pages: a stray `</div>` closed the page body early, so the footer, the previous / next links and the \"On this page\" rail rendered outside the grid. A test now checks `<div>` balance on every docs page.\n- Gauge page: the demo script used tier cuts of 75 / 60 / 40 and a static demo showed 1.8 as `poor`; both now use the real cuts of `ratingOf()` (90 / 70 / 50 / 30). The live demo draws no arc at 0, and the React `<Gauge>` snippet for a component that never existed is replaced by the recipe and helper.\n- Steps page: the done step's check icon was never inlined.\n- `tv()` knows `text-h4` as a font size: tailwind-merge read it as a colour and dropped a neighbouring colour or font-size class (card and dialog `sm` titles, empty-state title, heading level 4).\n- Real duration utilities: `duration-slow`, `duration-fast` and `duration-base` generate no CSS in Tailwind v4, so the progress bar, choicebox, checkbox, radio and switch fell back to the default 150ms. They now use `duration-200` and `duration-100` / `duration-150`.\n\n### Consumer notes\n\n- Import `@sportingscouter/ui/shell-product.css` from CSS, after the theme (`ui/package.json` has `\"sideEffects\": false`, so do not rely on a bare JS import). The root element must carry `appshell--product`; `appShell()` adds it in `sideNav` mode.\n- `business` can then delete the promoted rules from `shell-product-extensions.css`, but not the file: the workspace tabs and context line (`.sn__wtab*`, `.sn__workspace-hd`, `.sn__wctx*`) and the org settings hub stay there.\n- `theme.css` / tokens: no change, no token sync needed.\n\n## [3.2.2] - 2026-10-01\n\n### Changed\n\n- Agent docs: **Implementing product UI** and **Designing a new layout** in `AGENTS.md`, `/agents/`, and `llms.txt` - consumer copy-paste rule plus UX procedure for new surfaces in this repo.\n\n## [3.2.1] - 2026-09-28\n\n### Changed\n\n- Review card reference page: **Public** / **Private** / **Common information** IA; React integration snippets for athlete lists and organiser queues; catalog blurb in `nav.mjs`.\n- Review card footer patterns: public **Report** (ghost, trailing `spacer`, Lucide `flag` on written reviews); organiser **Reply** (ghost footer with `message-square-reply`, not link variant). Reference props table documents split-header surfaces only; legacy moderation demo removed from the page.\n- `reviewCard()` module comments aligned with public vs organiser `detail` variants.\n- `GAPS.md`: Lucide guidance for ReviewCard footer icons (`flag`, `message-square-reply`).\n- Reference site rail brand link padding (`.brand.brand--rail`).\n\n## [3.2.0] - 2026-09-28\n\n### Added\n\n- `pagination()` recipe, reference page, and interactive accessibility demos.\n- `localeSwitcher()` recipe and documentation (CLDR display names; disclosure chip aligned with sportingscouter/www footer).\n- `asyncState()` recipe, async state pattern page, and `buttonSpinner` size options on buttons.\n- `sideNav()` recipe for rail structure inside `appShell()` (docs vs product modes).\n- Review stack: `reviewList()`, `reviewBreakdown()`, `reviewReply()`; expanded `reviewCard()` for public lists, organiser queues, rating-only reviews, and nested organiser replies.\n- Review card reference page with raised/plain list, category breakdown, and legacy header migration demos.\n- Reference site navigation tests (`nav.test.mjs`) for layout and sidebar behaviour.\n- Package release semver: `@sportingscouter/ui` and `@sportingscouter/ui-react` at **3.2.0**, aligned with this changelog; pin consumer syncs with git tag **`ui-vX.Y.Z`**.\n- Agent docs: **Update consumer apps** (`/agents-consumer-update/`) - copy-paste Cursor prompt to run a tagged design-system sync and verify dependent workspaces.\n\n### Changed\n\n- Published reference origin and repo links: `https://design-system.sportingscouter.com` (replaces `design.sportingscouter.com`).\n- Reference site chrome: side navigation lives in `layout.html` with collapsible rail, resize handle, and aligned docs CSS/JS class names.\n- `appShell()`: `topbar`, `body`, and `scrim` slots; collapse applies `appshell--collapsed` on the shell root (rail chrome delegated to `sideNav()`).\n- Icon inlining: alias resolution, generated catalog, test script, and table SVG styling/error handling improvements.\n- Sidebar navigation: restores scroll position and highlights the current page after in-rail navigation.\n- Local `site` serve script uses a positional port argument (`3200`).\n- Design documentation pass (DESIGN-LANGUAGE, HANDOFF, MAPPING, SPEC, SYSTEM) for overlays, rating read mode, and accessibility notes.\n- `AGENTS.md`: Versioning section, release checklist, and link to the consumer update prompt; `HANDOFF.md` points to the same rules.\n- Push to **`main`** triggers **Release tag** workflow: annotated tag **`ui-vX.Y.Z`** from the top changelog entry when package versions match (`scripts/release-tag.mjs`).\n\n## [3.1.0] - 2026-09-25\n\n### Added\n\n- Top-level site areas: Docs, Components, Templates, Agent docs (Astryx-style IA).\n- App Shell documentation chrome: collapsible rail, resize handle, area tabs, nav search.\n- Layout family recipes: `stack`, `layoutPanel`, `formLayout`, `scrollable`, `center`, `aspectRatio`, `appShell`.\n- Component recipes: `emptyState`, `progressBar`, `statusDot`, `metadataList`, `itemList`, `fileInput`, `stepper`, `popover`, `dropdownMenu`, `datePicker`, `commandPalette`.\n- Agent artifacts: root `AGENTS.md`, build output `catalog.json`, `llms.txt`, `changelog.json`.\n- Agent docs: rules hub, changelog viewer, template authoring guide, MCP follow-up note.\n- Template previews: shell side nav (app layout); login (page layout); marketing organiser landing and Social Debrief.\n- Login template: split sign-in with an inverse form panel and a cover photograph.\n\n### Changed\n\n- Login template cover is a meeting photograph with the organiser money outcome on a scrim.\n- `select()` field slots now use `fieldParts` for label, hint, and error.\n- `rating()` legend aligns with shared field typography.\n- Side navigation docs replace the old sidebar page (`side-nav` component demo; `shell-side-nav` template).\n- Reference site uses dedicated `page` and `app` layouts for marketing/auth vs authenticated shell previews.\n- Icons foundation page: Lucide geometry inlined at build time from the catalog (no runtime CDN).\n- Engineering docs (`HANDOFF.md`, `SYSTEM.md`, site README) state canonical paths vs legacy `_ds_bundle` / `styles.css` (Gap 5).\n- Reference site and showcase demos use fictional English event names only (no real races or Portuguese placeholders).\n\n### Canonical vs legacy\n\n- **Canonical:** `ui/`, monorepo `theme.css`, `site/` reference, `showcase/` HTML cards.\n- **Legacy (read-only):** root `README.md` + `styles.css` + `_ds_bundle.*` - old marketing site brand layer; do not extend for new product UI.\n"
}
