Features Page

One page that outlines every user-facing surface in the app: each feature's name, a one-line description, its route, and a link to its long-form doc, grouped into seven categories and filtered by a single search box.

Why this exists

Nothing in the app listed everything it does:

  • The global dock exposes four destinations (Videos, Shared, Debate, Docs) plus a Settings menu that is a flat, unexplained list of forty-odd items.
  • /research and /coach each tab across the panels of one package.

So a debater arriving at the app had no way to learn what it does short of opening the Settings menu and clicking through every entry. This page is that map.

What it shows

ElementSource
Header count linebuildFeatureCatalogSummaryText — e.g. "50 features across 7 categories: Core Workspaces (8), …"
Search boxsearchFeatures, matching title, description, route, and hidden tags
Jump-to-category rowOne pill per non-empty section, anchored to its id
Section heading + blurbFEATURE_CATEGORY_LABELS / FEATURE_CATEGORY_DESCRIPTIONS
Feature cardTitle (links to the route), description, the route itself, and a Docs link when the feature has one

The count line always describes the whole catalog, not the current filter, so it stays a stable "how big is this app" answer while someone types.

Categories

CategoryWhat it groups
Core WorkspacesVideo archive, card search, the flow spreadsheet, Reason docs/editor, the research/coach hubs, the community hub
Evidence & ResearchEvidence and argument libraries, contributions feed, card scoring, peer review, revision incentives, coverage, progress, task inbox
Team CollaborationBrainstorm boards, collaboration mode, prep room, prep notes, notifications, coaching programs, coach materials
Flowing & Round AnalysisArgument tree outline, transcript summaries, flow annotations, response-outcome charts, speech documents, word-count speeches
Pre-Round IntelligenceOpponent and judge profiles, briefings, scout-to-strategy, paradigm picker, AI judge decision
Practice & CoachingPractice round simulator, versus-AI rounds, opponent personas, AI coach mode, drills
Recognition & ProgressLeaderboard, awards, daily best card, quests, streaks, unlocks, group challenges, team rankings

How it works

  • packages/debate-feature-catalog/src/feature-catalog.ts holds the pure data and helpers: APP_FEATURES, buildFeatureSections, searchFeatures, featureDocUrl, and buildFeatureCatalogSummaryText. Like debate-community's narrower community-hub directory it has no store — every entry links to a surface that already persists (or doesn't need to persist) its own state. It is a dependency-free leaf package (the same shape as debate-data-sync) that the app and debate-community both import from directly — see "One shared catalog" below for why this needed its own package rather than living in either.
  • apps/debate-ai.com/app/features/page.tsx mounts apps/debate-ai.com/lib/ui/features/FeaturesPanel.tsx, which renders the catalog (an aurora hero, counted-up totals, a marquee, and scroll-reveal spotlight cards) holding only the search string in local state. It links each card's doc to the app's own same-origin docs site via featureDocUrl's default options.

Per-card hover colour

The grid at rest is painted in the one app accent, but each card hovers in a colour of its own: the cursor spotlight, the rotating border beam and the card title all come up in a hue no neighbouring card shares.

cardHueShift(index) in effects.tsx turns a card's position in the catalog into a rotation off --accent-hue, stepping by the golden angle (137.508°) so consecutive cards land far apart on the wheel rather than in the near-duplicates a plain index * 40 would give. SpotlightCard writes that angle to the card as --da-card-hue and tags it da-card-tint; globals.css turns the angle into the --da-card-accent / --da-card-accent-3 pair, which the hover visuals read with a fallback to the page-wide --da-accent.

Three details are load-bearing:

  • The angle is unitless. It is added to the bare --accent-hue inside one calc(), and calc(192 + 138deg) is invalid — a deg suffix would silently drop the colour to its fallback.
  • The rotation is resolved in CSS, not inline. A custom property inherits the value it was substituted with, not the expression, so a card that only overrode --da-card-hue would never recompute an --da-accent declared on an ancestor. The card has to declare its own colour tokens — and doing it in the stylesheet keeps the light/dark lightness split the rest of the accent tokens use.
  • The hue comes from the full catalog, not the filtered grid. A feature keeps its colour while someone types, instead of every card changing hue on each keystroke.

Only the hover visuals read the per-card tokens. The resting page — the Docs links, the section icons, the aurora, the shimmering headline — stays on --da-accent, so the grid still reads as one surface until a card is pointed at.

One shared catalog

APP_FEATURES used to exist in three hand-synced places — this app's own copy, the old shared UI kit's seed copy, and packages/debate-contributor-progress's News Stream copy — because none of the packages involved shared a dependency edge that would let one import another's copy. The UI kit was deliberately not a runtime dependency of the app or of debate-community, and it has since been dissolved into a per-package ui/ folder (see Packages).

That's now resolved by promoting the catalog into its own dependency-free leaf package, debate-feature-catalog (the same shape as debate-data-sync: a package.json with no exports/main, consumers importing its one source file by deep path). All three sites depend on it directly ("debate-feature-catalog": "workspace:*" in each of their package.jsons) and import the same APP_FEATURES, buildFeatureSections, searchFeatures, and buildFeatureCatalogSummaryText:

ConsumerRenders via
apps/debate-ai.com (/features, live)apps/debate-ai.com/lib/ui/features/FeaturesPanel.tsx
packages/debate-contributor-progress (News Stream "Tool spotlight" posts)src/lib/news-stream.ts's buildAutoFeatureNews()

Where a feature's doc link points is a parameter rather than a forked file: featureDocUrl(entry, options) defaults to the live app's same-origin, extensionless docs route (/docs/features/task-inbox), and a renderer with no docs site of its own can pass { baseUrl: GITHUB_DOCS_BASE_URL, stripExtension: false } to link to the doc's source on GitHub instead. buildAutoFeatureNews doesn't call featureDocUrl at all, so it's unaffected either way.

Titles and descriptions are copied from each route's own metadata export (or, where a route has none, from its feature doc's opening lines) so a card reads the same as the page a reader lands on after clicking it.

Each entry may also carry tags: search terms that don't appear in the visible copy — package names, synonyms, and the jargon a debater would actually type. "elo" finds Team Rankings, "rfd" finds AI Judge Decision, and "verbatim" finds both the Reason Editor and Speech Documents, none of which mention those words on screen.

debate-feature-catalog is its own package, rather than living inside any of its three consumers, because the catalog spans every package: it names surfaces from debate-round, debate-research-evidence, debate-videos, debate-speech-writer, and reason-editor, so it can't live inside any one of them without inverting the dependency graph, and putting it in the app itself would have put it outside the root Vitest projects, which only cover packages/*.

Tests

packages/debate-feature-catalog/test/feature-catalog.test.ts covers the catalog's own invariants (unique ids and routes, every entry categorized, every category used, docs pointing at .md files), section grouping and ordering, search across all four matched fields, featureDocUrl in both its default and GitHub-blob-URL forms, and summary-line pluralization. apps/debate-ai.com/lib/ui/__tests__/features-panel.test.tsx renders the panel and asserts every catalogued route, the category headings, the summary line, a docs link, the jump nav, and that a one-entry catalog renders without the jump nav. It also covers the per-card hover colour: that every card renders a --da-card-hue, that the angle follows the card's catalog position rather than its place in a filtered grid, and that cardHueShift stays inside one turn of the wheel while keeping consecutive cards more than 80° apart.

apps/debate-ai.com/lib/__tests__/tool-catalog-consistency.test.ts cross-checks this catalog's routes against /tools' ALL_TOOLS and the Reason Editor's WORKSPACE_LINKS, and separately walks every static route under apps/debate-ai.com/app/ to assert each appears in at least one of the three.

Known gaps

  • Fixed: the catalog is a hand-maintained registry, so a new route used to have to be added to it by hand with nothing checking that it was — the tests only asserted the catalog's internal consistency, not that it covered every file under apps/debate-ai.com/app/, since the app is outside the packages Vitest runs over. tool-catalog-consistency.test.ts now also walks apps/debate-ai.com/app/ for every static route with its own page.tsx (skipping dynamic-segment detail routes, which don't get their own catalog entry) and asserts each appears in at least one of ALL_TOOLS/APP_FEATURES/WORKSPACE_LINKS, unless it's in that test's own ROUTES_WITHOUT_A_CATALOG_ENTRY (the homepage, /admin, an auth-flow step, a legal page, /login, or the editor's own settings pages — none of which are "a tool" to list). This is a test-only relative import into the app's own route tree via node:fs, so it adds no runtime dependency; a new route added to app/ and to none of the three catalogs now fails this test instead of silently reaching none of them.
  • Fixed: the catalog used to exist as three hand-synced copies (see "One shared catalog" above), and they drifted once — /contacts was added to the app's copy but never carried over to the other two, so News Stream's auto-generated "Tool spotlight" post silently never fired for it, and nothing caught it because each copy's own test suite only checked its own internal consistency. Promoting the catalog into the shared debate-feature-catalog package removes the drift risk entirely — there's only one file to edit now, so this class of bug can no longer recur.
  • /login is intentionally absent: it's a step on the way to a feature rather than a feature, and it's already reachable from the Settings menu.
  • The live /features page's Docs links stay same-origin; a renderer with no docs site of its own would have to pass GITHUB_DOCS_BASE_URL to featureDocUrl to point at the feature docs on GitHub's master branch instead.