Skip to content

feat(dtpr-ai): taxonomy catalog + element/category pages + Cmd-K + locale#276

Merged
pichot merged 19 commits intomainfrom
feat/dtpr-ai-taxonomy
May 5, 2026
Merged

feat(dtpr-ai): taxonomy catalog + element/category pages + Cmd-K + locale#276
pichot merged 19 commits intomainfrom
feat/dtpr-ai-taxonomy

Conversation

@pichot
Copy link
Copy Markdown
Member

@pichot pichot commented May 5, 2026

Summary

Builds out the DTPR-AI documentation site's schema-browsing surface from scratch. Two plans land together — the catalog (/taxonomy) and the per-element / per-category pages plus unified Cmd-K search and a ?locale= URL convention.

What ships

  • /taxonomy catalog: every element grouped by category, sticky category sidebar with scroll-spy, copy-link anchors, schema version selector (?v=), per-row + per-category "Open page" affordances.
  • /taxonomy/elements/<id>: hero via <DtprElementDetail> with an interactive Playground (Variables form, Context chip row, Default/Dark style toggle) backed by deriveElementDisplay.
  • /taxonomy/categories/<id>: title, description, read-only context palette, elements list linking to element pages.
  • Editorial overlays: drop a content/taxonomy/{elements,categories}/<id>.md to add authored narrative; Cmd-K indexes it automatically; real markdown wins on collisions with the synthetic group.
  • Cmd-K: shadowed app.vue injects two synthetic groups (Elements + Categories) into <UContentSearch>, scoped to the active version + locale.
  • Locale: shared useDtprState composable wires ?v= and ?locale= consistently across catalog, element pages, category pages, and Cmd-K. DTPR-derived surfaces only — docus prose stays English-only.
  • Top-nav Taxonomy link via shadowed AppHeaderCTA (left of the dark-mode toggle).

Plans

  • docs/plans/2026-04-27-001-feat-dtpr-ai-taxonomy-view-plan.md — catalog (U1–U7).
  • docs/plans/2026-05-05-001-feat-dtpr-taxonomy-pages-and-search-plan.md — element/category pages, Cmd-K, locale (U1–U7).

Test plan

  • /taxonomy renders all categories grouped, sidebar scroll-spy works, copy-link button copies #element-<id> URL.
  • /taxonomy/elements/<id> renders hero + Playground; typing in a variable input live-updates the rendered title/description; clicking a context chip recolors the icon; Default/Dark toggle swaps the icon variant.
  • /taxonomy/categories/<id> renders description, context palette swatches, and the elements list; clicking an element row navigates with ?v= and ?locale= carried forward.
  • Cmd-K shows "Elements" and "Categories" groups alongside prose docs; selecting an item routes to its page.
  • Switching locale updates the URL and re-renders schema strings (or falls back to en per field).
  • Switching version updates the URL; pinned ?v=ai@2026-04-16-beta deep links resolve correctly; unknown versions show the inline alert.
  • Drop a fixture md at content/taxonomy/elements/<known_id>.md — body renders below the playground; Cmd-K dedupes the synthetic entry.

🤖 Generated with Claude Code

pichot and others added 15 commits April 27, 2026 16:12
Add /taxonomy page that browses every element of the active AI schema
version. Resolves the latest canonical version client-side via /schemas
(the API does not honor an @latest path alias today), normalizes the
REST response's singular category_id into the plural shape the grouping
helper expects, and renders DtprCategorySection + DtprElementGrid
+ DtprElement using deriveElementDisplay with composed icon URLs.

Wires @dtpr/ui as a workspace dependency on dtpr-ai.
Add a sticky header with a UInput that filters the catalog
case-insensitively against locale-resolved title and description,
hides categories whose elements all drop out, and renders an empty
state when the query matches nothing.
Add per-row and per-category UButton affordances that copy the
fully-qualified deep-link to the clipboard and emit a useToast()
notification. Read window.location.hash on mount and on hashchange
to drive a temporary outline+tint highlight on the targeted row,
mirroring the legacy taxonomy's deep-link UX without modifying any
@dtpr/ui internals.
Render a UNavigationMenu sidebar with one entry per visible category
(label, badge=filtered count, anchor href). A passive scroll listener
updates the active state to the topmost section above a 100px offset.
On viewports below lg, the sidebar collapses behind a hamburger toggle
in the sticky header with a backdrop overlay.
Read the active schema version from ?v=<canonical-id>, falling back
to the most recent registered version when the param is absent. When
?v= names a version not in the schemas index, render a UAlert and
fall back to latest. The USelectMenu hides itself when only one
version is registered today and surfaces automatically once additional
schemas ship.
Add a content stub at content/0.taxonomy.md whose 'navigation'
frontmatter publishes a sidebar entry for the route. The static
app/pages/taxonomy.vue page outranks the catch-all so visitors
clicking the nav land on the live catalog rather than the stub body.
Apply safe_auto fixes from ce-code-review (run 20260427-162447-da890dd9):

- Type ElementApi as Omit<Element, 'category_id'> so the singular field
  is actually relaxed (the prior Omit<..., 'category_ids'> was a no-op
  since Element has no category_ids field) [kieran-1, corr-3]
- Preserve route.hash when switching schema version so deep links
  survive selector changes [corr-2]
- Add 8s timeout to /schemas, /categories, /elements fetches so a slow
  upstream cannot exhaust the Cloudflare Workers SSR wall-clock budget
  [rel-002]
- Track scrollToTarget setTimeout in a ref, clearTimeout on re-entry
  and on unmount, so rapid clicks and route changes don't leak handlers
  [julik-2]
- Throttle handleScroll with requestAnimationFrame and read offsetTop
  at most once per frame instead of on every scroll event [julik-3,
  rel-007]
- Suppress scroll-spy briefly during programmatic smooth scroll so the
  sidebar highlight no longer flickers through every category between
  source and destination [julik-1]
- Auto-close the mobile sidebar after a category is selected so the
  overlay doesn't cover the destination on touch viewports [julik-6]
- Move history.replaceState + targetId update inside the clipboard
  success branch so a failed copy no longer rewrites the URL the toast
  just told the user wasn't copied [julik-5, rel-005]
Adds the origin brainstorm and the two plan documents:

- 2026-04-27-001: prior catalog plan (already shipped)
- 2026-05-05-001: this feature — element pages, category pages, Cmd-K
  search, and a `?locale=` URL convention

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single source of truth for ?v= and ?locale= consumed by the catalog,
both new routes, and the synthetic Cmd-K injection. Lifts the version
computed-chain from taxonomy.vue and adds locale-aware setters that
push to the router via replace.

Default locale is en; default version resolves to ai@latest. Scoped
strictly to DTPR-derived surfaces — docus prose stays English-only at
the site shell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… overlays (U2–U5)

- /taxonomy/elements/<id>: hero via DtprElementDetail, an interactive
  Playground (Variables form, Context chip row, Default/Dark style
  toggle) backed by deriveElementDisplay, and an optional editorial
  body when an authored md exists at content/taxonomy/elements/<id>.md.
- /taxonomy/categories/<id>: title, description, read-only context
  palette swatches, elements list linking to element pages, and an
  optional editorial body at content/taxonomy/categories/<id>.md.
- DtprPageHeader: shared sticky-header strip owning the version +
  locale selectors, used across catalog and detail pages.
- Editorial overlay convention documented in 0.taxonomy.md; required
  frontmatter (navigation: false, title:) keeps overlays out of the
  docus left-rail nav while letting Cmd-K index them automatically.

Both pages handle 404 and "not in this version" inline rather than
crashing hydration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…page link (U6)

Migrates the catalog from its local version computed-chain to the
shared useDtprState composable, adds locale awareness end-to-end
(?locale= query, &locales=<active>,en API params, locale-aware
deriveElementDisplay), and consumes DtprPageHeader so the catalog,
element pages, and category pages all share one header strip.

Adds per-row "Open page" affordances to every element row and each
category section header. The existing copy-link button and the
hash-scroll behavior are preserved; the link button is purely
additive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(U7)

Local override of docus's app.vue mirrors the upstream verbatim plus a
single bound prop — :groups="dtprGroups" on <LazyUContentSearch>.
useDtprSearchOverlay runs a single client-only useAsyncData keyed on
the surface's active version + locale, builds two synthetic groups
(Elements and Categories), and dedupes against authored editorial md
so real markdown wins on collisions.

Cmd-K results scope to whatever surface the visitor is currently on:
opening the modal on /taxonomy?v=ai@2026-04-16-beta surfaces beta
entries; opening it elsewhere uses ai@latest. The boot fetch never
blocks initial paint (server: false).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With both `pages/taxonomy.vue` and `pages/taxonomy/elements/[id].vue`,
Nuxt's file-based router treats them as parent + nested child. The
parent has no `<NuxtPage>` slot, so visiting `/taxonomy/elements/<id>`
silently rendered the catalog instead of the element page.

Moving the catalog into `taxonomy/index.vue` makes the three routes
siblings (`/taxonomy`, `/taxonomy/elements/:id`, `/taxonomy/categories/:id`)
and the deep links resolve correctly.

Also swaps `v-show` for `v-if` on the search-clear button to silence
a Vue warn about runtime directives on a component with a fragment
root (UButton → ULink → NuxtLink).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, right-align locale

- Shadow docus's AppHeaderCenter locally to add a "Taxonomy" button
  next to the Cmd-K search trigger. Docus 5.10 has no first-class
  header.links config, so component shadowing is the standard
  extension point.
- Set `navigation: false` on content/0.taxonomy.md so the entry no
  longer appears in the docus left-rail sidebar.
- Drop the in-page "Search elements…" filter from /taxonomy. Cmd-K
  is now the unified search surface; the in-page input duplicated
  effort and competed visually with the new taxonomy top-nav link.
  Removes searchQuery, filteredDecorated/Grouped/SortedCategories,
  hasResults, clearSearch, and the no-results empty state — sidebar
  scroll-spy reads from the unfiltered sortedCategories now.
- Locale select right-aligns via `margin-left: auto` and the popup
  widens to `min-w-[12rem]` so language names like "Português" don't
  wrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gnment

- Replace the AppHeaderCenter shadow with a shadowed AppHeaderCTA.
  AppHeaderCTA is docus's canonical empty-by-default extension point on
  the right side of the header, rendering left of UColorModeButton and
  the GitHub link — exactly where a "Taxonomy" nav link belongs.
- AppHeaderCenter falls back to docus's default (search trigger only),
  so Cmd-K stays the unified search affordance with no competing link.
- Make the DtprPageHeader heading flex-grow so version + locale +
  actions get pushed to the right edge of the row. The earlier
  `margin-left: auto` on the locale select alone wasn't enough — the
  heading was non-flex, leaving the right cluster anchored next to it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 5, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
dtpr-ai a383f4d May 05 2026, 07:58 PM

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR builds out the DTPR-AI documentation site's schema-browsing surface: a /taxonomy catalog with scroll-spy sidebar, per-element and per-category detail pages, a Cmd-K search overlay that injects synthetic DTPR groups, and a ?v= / ?locale= URL convention wired through a shared useDtprState composable.

  • New pages (taxonomy/index.vue, elements/[id].vue, categories/[id].vue): each surfaces versioned DTPR schema data with an interactive playground (element page), context palette (category page), and forwarded ?v=/?locale= query params derived from validated state rather than raw route params.
  • New composables (useDtprState, useDtprSearchOverlay): useDtprState centralises version + locale resolution; useDtprSearchOverlay builds Cmd-K groups client-side, includes a truncation warning when a schema exceeds 200 elements, and deduplicates synthetic entries against authored markdown.
  • App shell changes: shadowed app.vue wires dtprGroups into <LazyUContentSearch>, and a shadowed AppHeaderCTA adds the Taxonomy nav link.

Confidence Score: 5/5

The PR adds a large but self-contained documentation surface; all new code paths are read-only API fetches and UI rendering with no mutations to shared state outside the router.

The core concerns raised in earlier review rounds — stale payload on SPA navigation, raw query param propagation, limit=200 truncation, and the context chip dark-mode override — have each been addressed in this revision. The element page now uses a dynamic useAsyncData key keyed on element ID; queryString in all three pages is derived from validated activeVersion/activeLocale rather than raw route params; truncation warnings are in place in both taxonomy/index.vue and useDtprSearchOverlay; and DtprPlayground uses effectiveContextId/chipsLocked to correctly reflect the dark-mode override in chip visual state. No new correctness gaps were found across the composables, pages, or components.

No files require special attention. The categories/[id].vue elements fetch still omits a meta truncation check (unlike the catalog and Cmd-K composable), but that was already noted in a prior review thread.

Important Files Changed

Filename Overview
dtpr-ai/app/composables/useDtprState.ts New composable: centralises version/locale resolution from route query params, validates against a fetched schemas list, exposes writable computeds that call router.replace. Clean design, correct deduplication key for useFetch.
dtpr-ai/app/composables/useDtprSearchOverlay.ts New composable: synthesises Cmd-K groups from DTPR API, correctly guards against truncation with a console warning when meta.total exceeds ELEMENTS_PAGE_LIMIT, and deduplicates against authored md paths.
dtpr-ai/app/pages/taxonomy/index.vue New catalog page: scroll-spy sidebar, hash anchor copy, queryString built from validated state. Truncation alert mirrors the composable. Unused route variable declared but never read — cosmetic dead code.
dtpr-ai/app/pages/taxonomy/elements/[id].vue New element detail page: dynamic useAsyncData key eliminates stale payload on SPA navigation; queryString derived from validated activeVersion/activeLocale; notFound detection via elementError.
dtpr-ai/app/pages/taxonomy/categories/[id].vue New category detail page: static key for all-categories fetch is intentional (data covers all categories, client-side find handles filtering); dynamic key for per-category elements fetch; elements response omits meta/truncation check unlike other consumers.
dtpr-ai/app/components/DtprPlayground.vue New playground component: effectiveContextId correctly mirrors chip visual state to the dark-mode override; variableValues reset on element.id change handles key diffing correctly.
dtpr-ai/app/app.vue Shadowed docus app.vue: single intentional divergence (dtprGroups prop on LazyUContentSearch), well-documented sync requirement for upstream upgrades.
dtpr-ai/app/components/DtprPageHeader.vue New shared sticky header component: version/locale selectors emitted as update events, version-missing alert, slot-based composition for page-specific headings and actions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    AppVue["app.vue (shadowed)"]
    useDtprSearchOverlay["useDtprSearchOverlay\n(Cmd-K groups)"]
    useDtprState["useDtprState\n(?v= / ?locale= resolver)"]
    DTPR_API["DTPR API\n/schemas/{version}/..."]

    AppVue -->|"passes files ref"| useDtprSearchOverlay
    useDtprSearchOverlay -->|"calls"| useDtprState
    useDtprSearchOverlay -->|"fetches elements + categories\n(server:false, watches activeVersion/Locale)"| DTPR_API

    TaxonomyIndex["/taxonomy\n(catalog + scroll-spy)"]
    ElementPage["/taxonomy/elements/[id]\n(hero + DtprPlayground)"]
    CategoryPage["/taxonomy/categories/[id]\n(palette + elements list)"]

    TaxonomyIndex -->|"calls"| useDtprState
    ElementPage -->|"calls"| useDtprState
    CategoryPage -->|"calls"| useDtprState

    TaxonomyIndex -->|"fetches categories + elements\n(dynamic key, limit=200)"| DTPR_API
    ElementPage -->|"fetches element + categories\n(key = id, watches id+version+locale)"| DTPR_API
    CategoryPage -->|"fetches all categories\n(static key)\nfetches elements by category\n(dynamic key, limit=200)"| DTPR_API

    useDtprState -->|"reads ?v= ?locale=\nwrites via router.replace"| RouteQuery["Route query params"]

    DtprPageHeader["DtprPageHeader\n(sticky version/locale selectors)"]
    TaxonomyIndex --> DtprPageHeader
    ElementPage --> DtprPageHeader
    CategoryPage --> DtprPageHeader

    DtprPlayground["DtprPlayground\n(variables form + context chips)"]
    ElementPage --> DtprPlayground
Loading

Reviews (2): Last reviewed commit: "fix(dtpr-ai): address greptile review (s..." | Re-trigger Greptile

Comment thread dtpr-ai/app/pages/taxonomy/elements/[id].vue
Comment thread dtpr-ai/app/pages/taxonomy/elements/[id].vue
Comment thread dtpr-ai/app/composables/useDtprSearchOverlay.ts
Comment thread dtpr-ai/app/components/DtprPlayground.vue
pichot and others added 4 commits May 5, 2026 15:23
… scroll

- Remove the mobile hamburger toggle, slide-in drawer, and scrim. The
  toggle was leaking out next to the locale select at responsive
  widths and the in-page sidebar isn't load-bearing enough on mobile
  to justify the drawer pattern. Below 1024px the sidebar simply
  hides — categories are still browseable by scrolling.
- Tighten the catalog sidebar's sticky offset so it locks the moment
  the page begins scrolling, instead of inching upward by ~0.5rem
  before catching its sticky position. Layout padding-top reduced
  from 2rem to 1.5rem so the sidebar's natural position equals
  --ui-header-height + 6rem.
- Bump scroll-margin-top on .taxonomy-category and .taxonomy-element-row
  from 6rem to `--ui-header-height + 5.5rem` so anchor jumps clear
  both the docus header and the sticky DtprPageHeader instead of
  landing the title behind them. Scroll-spy threshold raised to
  140px so the active highlight tracks the new effective top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…esolve

Cloudflare Workers Builds runs a fresh `pnpm install --frozen-lockfile`
and then `pnpm run build` — it never builds workspace dependencies
ahead of time. Vite/Rollup can't resolve `@dtpr/ui/core` because the
package's exports map points at `./dist/core/index.js`, and dist/ does
not exist on the build runner.

Local builds masked this: my checkout has a stale dist/ from prior
@dtpr/ui builds. Repro: `rm -rf packages/ui/dist && pnpm --filter
./dtpr-ai build`.

Fix: dtpr-ai's `build` script now runs `pnpm --filter @dtpr/ui build`
first, then nuxt build. Same change to `dev` so a clean clone can
pnpm dev without first hand-building the workspace package.

Also drop the `.gitkeep` files in content/taxonomy/{elements,categories}
— @nuxt/content emits parser-failed warnings on them, and the
directories aren't load-bearing (queryCollection returns null cleanly
when no overlay md exists, and authors create the dir when they drop
the first file).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y rollup

Previous fix built @dtpr/ui first but Cloudflare's build still failed
on `Rollup failed to resolve import "@dtpr/api/validator" from
packages/ui/dist/core/index.js`.

Cause: @dtpr/ui's core/index.js imports `validateInstance` from
@dtpr/api/validator at runtime, and @dtpr/api's exports map points at
`./dist/{schema,validator}/index.js`. Those bundles come from the
`build:schema` tsup pass — not from `wrangler deploy --dry-run`, which
is @dtpr/api's default `build` script and produces only the worker
bundle.

Fix: chain `pnpm --filter @dtpr/api build:schema` before the @dtpr/ui
build. Verified with `rm -rf packages/ui/dist api/dist && pnpm --filter
./dtpr-ai build` from a fully clean state — succeeds without TS2307
warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ion, dark chip)

- Stale element / category data on SPA navigation: useAsyncData keys
  now include the route id (`dtpr-element-detail-${elementId}` and
  `dtpr-category-detail-elements-${categoryId}`) so Nuxt drops the
  prior payload immediately on navigation instead of holding it
  through the watch-triggered refetch.
- Forwarded query strings on /taxonomy, /taxonomy/elements/<id>, and
  /taxonomy/categories/<id> now derive from useDtprState's validated
  activeVersion / activeLocale (gated on requestedVersion + !versionMissing
  for `?v=`, gated on locale !== 'en' for `?locale=`), so an invalid
  `?locale=xyz` no longer propagates through every link the visitor
  clicks afterward.
- Catalog and Cmd-K boot fetch use a named ELEMENTS_PAGE_LIMIT (200,
  the API's documented cap) and now check meta.total: the catalog
  surfaces an inline "Catalog truncated" UAlert when total > limit,
  and useDtprSearchOverlay logs a console.warn with the count so the
  truncation can't hide if a future schema outgrows the single page.
- Dark-mode interaction in DtprPlayground: a previously-selected
  context chip no longer renders in its solid/active visual state
  while dark mode silently overrides it. New `effectiveContextId`
  computed returns null while styleMode is 'dark', driving chip
  variant + style + aria-pressed; chips are also `:disabled` so
  clicks can't accumulate state that won't take effect. The
  underlying `contextId` is preserved so switching back to default
  restores the visitor's selection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pichot
Copy link
Copy Markdown
Member Author

pichot commented May 5, 2026

@greptile

@pichot pichot merged commit 7cfff65 into main May 5, 2026
6 checks passed
@pichot pichot deleted the feat/dtpr-ai-taxonomy branch May 5, 2026 20:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant