Skip to content

feat(genesis): add @paper/genesis docs design system#240

Merged
johnleider merged 46 commits into
masterfrom
worktree-paper-genesis
May 26, 2026
Merged

feat(genesis): add @paper/genesis docs design system#240
johnleider merged 46 commits into
masterfrom
worktree-paper-genesis

Conversation

@johnleider
Copy link
Copy Markdown
Member

@johnleider johnleider commented May 19, 2026

Summary

Adds @paper/genesis — a focused docs-primitives library that ships the first wave of components a documentation site needs. Genesis is a paper design system in the categorical sense: built on @vuetify/v0, self-contained, with no @vuetify/paper runtime dependency. If patterns prove shared with Emerald or future paper DSs, they can graduate into @vuetify/paper later — until then, Paper's runtime stays optional and the category is the only contract.

What's in the box

  • GnDocsExample suite — 8 compound components (GnDocsExample root + Description, Preview, Code, Tabs, Panel, Peek, Actions)
  • Consumes v0 tokens directly — every component references var(--v0-*) (surface, surface-tint, on-surface, divider, primary, etc.). The earlier --gn-* indirection layer was dropped in d5a79aee so the design system has one fewer hop to debug and stays aligned with v0's theme switching for free
  • Headless on what varies — code highlighting is consumer-injected via the default slot on GnDocsExampleCode; icons are consumer-injectable via 5 named slots (reset-icon, playground-icon, bin-icon, combine-icon, split-icon) forwarded from GnDocsExample down into GnDocsExampleTabs, each with inline SVG defaults using MDI paths
  • Background decoration slotGnDocsExamplePreview exposes a decoration slot (forwarded through GnDocsExample) so consumers can layer absolute-positioned background elements (dot grids, mesh gradients) behind the preview content
  • No GnDocsIcon component — replaced by inline-SVG defaults so consumers aren't forced to adopt a bundled icon helper

Docs-app integration

apps/docs/src/components/docs/DocsGenesisExample.vue wires the genesis suite to the docs site:

  • Auto-resolves single- and multi-file examples from apps/docs/src/examples/** via useExamples
  • Provides AppIcon (restart, vuetify-play, vuetify-bin, combine, split) for all five icon slots
  • Renders AppDotGrid in the decoration slot
  • Per-pane hover-actions overlay (Copy, Wrap, Bin, plus Playground in single-file mode) with :focus-within keyboard reach and mobile-pinned visibility
  • Line-wrap toggle persists through useSettings().lineWrap via useSyncedRef, matching the legacy DocsExampleCodePane pattern

Design doc

  • packages/genesis/SPEC.md — long-lived design reference (identity, component contracts, non-goals, Phase 2 roadmap)

Test plan

  • pnpm --filter @paper/genesis build produces dist/index.mjs + dist/style.css
  • pnpm typecheck clean across @vuetify/v0, @vuetify/paper, @paper/genesis
  • pnpm test:run — 5845 tests pass (18 skipped)
  • pnpm lint:fix clean
  • pnpm repo:check — currently flags dev/src/DevShikiBlock.vue unused + shiki unused dep in dev/package.json; clean these up before merge
  • Zero GnDocsIcon references in packages/genesis/src/
  • Branch rebased onto current master
  • Visual smoke test on apps/docs aspect-ratio page (single-file + multi-file examples, hover actions, decoration grid, AppIcon swap)

Phase 2 roadmap

Next features to pull out of the v0 docs, in order:

  1. GnDocsCallout (TIP / WARNING / ERROR / INFO admonitions)
  2. GnDocsCodeGroup (tabbed code blocks)
  3. GnDocsKbd, GnDocsBadge, GnDocsCard (atomic primitives)
  4. GnDocsApi (API reference tables with prop / event / slot sections + hover popovers)

Parked questions

What is Paper, formally? Concept vs. foundation — parked as "category for now." See SPEC.md "Deferred / parked questions" — revisit once Emerald and Genesis have shipped real users and we can see which patterns naturally converge.

Headless docs primitives extracted from apps/docs DocsExample, split
into six composable components so consumers can use the whole bundle
or any piece standalone:

- GnDocsExample        — orchestrator root
- GnDocsExampleDescription — header strip with collapse toggle
- GnDocsExamplePreview — splitter-resizable preview with drag indicator
- GnDocsExampleCode    — single-file code chrome (consumer supplies highlighter via slot)
- GnDocsExampleTabs    — multi-file tabs with overflow → dropdown and action emits
- GnDocsExamplePeek    — floating expand/collapse trigger for truncated mode

Genesis ships zero highlighter code; the docs site already loads Shiki
and pipes highlighted HTML in through the #code slot.
The only consumer in dev/ is the gitignored Playground.vue sandbox, so
knip can't see the import and flags the dep as unused.
- Drop @vuetify/paper dependency
- Add createGenesisPlugin, GenesisStyleSheetAdapter, theme.ts
- Retool every component to reference --gn-* directly
  (no --v0-* refs, no per-component indirection layer)
- Replace GnDocsIcon with slot-based icons + inline SVG defaults
- Wire dev Playground to createGenesisPlugin + DevShikiBlock
SPEC.md captures the design contract for @paper/genesis (paper DS
as category, --gn-* token namespace, headless icon/highlighter slots,
non-goals, phase 2 roadmap). PLAN.md is the implementation plan that
produced this PR.
v0's createThemePlugin uses createPluginContext('v0:theme', ...). The
install gate inside createPlugin is keyed by namespace and silently
no-ops on the second install for the same key, so calling
createThemePlugin a second time from createGenesisPlugin (after a
consumer has already installed v0's theme plugin) collided and Genesis's
adapter never ran — no stylesheet, no --gn-* tokens, components rendered
with no resolved colors.

Build Genesis's own theme trinity via createPluginContext('genesis:theme',
...) reusing createTheme as the factory. Genesis now installs alongside
v0's theme system without collision; both adapters write their own
stylesheets to adoptedStyleSheets, both cascades resolve on overlapping
subtrees. Default target moves from 'html' to 'body' so v0 (typically on
html or #app) and Genesis (on body) never fight for data-theme on the
same element.

Also exports useGenesisTheme() for consumers who want to drive Genesis
theme switching independently from v0's useTheme().
Phase 1 originally shipped only a light `genesis` theme. Adds
`genesisDarkColors` and registers both `genesis` (light) and
`genesis-dark` in the plugin defaults so consumers can pair Genesis
with either light or dark chrome without writing their own dark token
table from scratch.

Dev playground switches to `default: 'genesis-dark'` since v0's dev
theme is `dark` — Genesis components inside body now match the chrome.

Light remains the package default; consumers pass
`theme: { default: 'genesis-dark' }` to switch.
…s bleed

When GnDocsExampleTabs is the last child of a rounded container (e.g.
a hand-composed shell that pairs GnDocsExamplePreview + GnDocsExampleTabs),
the container's nth-last-child border-radius applies to the tabs box,
but the inner GnDocsExampleCode paints var(--gn-code-bg) with square
corners and bleeds past the radius.

Adding overflow: hidden to the tabs container makes tabs clip their own
contents — works in any rounded parent without requiring the parent to
have its own overflow:hidden (which would clip the splitter grip in a
sibling preview). The smart GnDocsExample root continues to work via
its existing __code wrapper; this just makes tabs safe in hand-composed
layouts too.
@johnleider johnleider force-pushed the worktree-paper-genesis branch from b558a48 to 0e72247 Compare May 19, 2026 16:27
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 19, 2026

Open in StackBlitz

commit: dff2ce3

johnleider added 20 commits May 19, 2026 11:45
Phase 1 shipped Genesis as a "paper DS with own theme namespace" — its
own createGenesisPlugin, createGenesisThemePlugin (createPluginContext
on 'genesis:theme'), GenesisStyleSheetAdapter, and a --gn-* token map
with genesis/genesis-dark themes. Integrating into apps/docs surfaced
two problems:

- A future GnDocsExample feature lets consumers render the preview in
  a specific theme (e.g., theme="corporateIndigo"). For the example
  UI inside the preview to actually re-theme, the override must affect
  --v0-* tokens — the theme system is v0's. A parallel --gn-* layer
  doesn't help.
- Components and the example content rendered inside them must inherit
  the same theme cascade — otherwise chrome and inner example look like
  they're from different apps when the page theme changes.

Genesis is now a thin component layer over v0 tokens. Every scoped
<style> references var(--v0-*, fallback). When v0's theme plugin is
installed, components inherit the active theme via the cascade; when
Genesis is used in a v0-less app, fallbacks provide reasonable light
defaults. No Genesis-specific plugin, adapter, theme, or stylesheet.

Deletes packages/genesis/src/adapter.ts, plugin.ts, theme.ts, and the
genesis-dark theme variant. Simplifies src/index.ts to re-export only
components. Removes the createGenesisPlugin install from dev/main.ts.
SPEC.md rewritten to reflect the v0-token-consuming architecture, with
the Phase 1 → revised history captured at the bottom. PLAN.md (the
abandoned implementation plan) removed.
Adds @paper/genesis as a workspace dependency of apps/docs. Adds the
matching @paper/genesis and #genesis path aliases to apps/docs/vite.config.ts
so source-mode resolution works in dev.

Genesis components consume v0 tokens via the cascade, so no plugin install
is needed — Genesis components placed inside the docs site simply inherit
whatever theme v0 has applied (via the docs' existing createThemePlugin).

Adds an exploratory /genesis-sandbox page that renders GnDocsExample with
single-file, multi-file, and Shiki-highlighted variants. Uses the docs site's
existing useHighlightCode composable via the code slot. Confirms Genesis
components blend visually with the docs chrome (light or dark, theme-driven
from v0).
Adds `theme?: string` prop to GnDocsExamplePreview and GnDocsExample.
When set, applies `data-theme="<name>"` to the preview __panel div,
scoping v0's theme cascade for the example content. The example UI
inside (consuming --v0-* tokens via UnoCSS utility classes or scoped
styles) renders in the named theme, independent of the page.

Implementation: a single attribute binding on the panel div, no extra
wrapper element, no JS theme switching. The theme name must match an
entry registered with v0's createThemePlugin; if it doesn't, the
cascade resolves to the parent's variables (no-op).

Adds a per-example theme demo section to the apps/docs sandbox showing
the same code rendering in light / dark / tailwind themes side by side.
Adds showPlayground and showBin props (default false) to GnDocsExampleTabs,
with matching playground and bin emits that receive the current displayFiles.
Inline-SVG defaults for playground-icon and bin-icon slots; consumers can
override. Restored the actions slot for custom buttons inserted before
the built-in ones. Forwards the same props/emits through GnDocsExample.

These hooks let downstream docs sites wire their own playground/bin URL
builders without modifying Genesis.
Adds DocsGenesisExample.vue — the apps/docs-side wrapper that pairs
GnDocsExample with the docs site's features:

- Auto-resolves example components + code via useExamples (filePath /
  filePaths props, same surface as the existing DocsExample.vue)
- Wires playground/bin emits to usePlayground + getMultiFileBinUrl
- Injects Shiki highlighting via a small DocsGenesisShikiBlock helper
  that uses the existing useHighlightCode composable
- Mirrors DocsExample's public prop surface (file, filePath, filePaths,
  fileOrders, title, id, code, collapse, files, imports, peek, peekLines)
  so pages can opt in by changing only the import

Updates the /genesis-sandbox page to demo the wrapper rendering the
Snackbar basic example via filePath auto-resolution.

Side-by-side migration: pages opt in by switching their import from
DocsExample to DocsGenesisExample. The legacy DocsExample stays in
place; nothing breaks during transition.
First real page migration. Replaces the ::: example markdown directives
with explicit <DocsGenesisExample> Vue tags + script setup import.
Markdown content inside the description slot (h3 heading, mermaid
flowchart, paragraphs, table) renders correctly through
unplugin-vue-markdown.

Side-by-side migration validated: legacy ::: example directive still
emits <DocsExample> for every other page; this page is the only opt-in
so far.
Refactors apps/docs/build/markdown.ts to share the example-container
logic between two parallel directives:

- `::: example` → emits <DocsExample> (legacy, unchanged)
- `::: gn-example` → emits <DocsGenesisExample> (new opt-in)

The container factory `registerExampleContainer(name, tag)` carries the
emit tag through `env._exampleTag`. Pages opt into Genesis by swapping
the directive name; no script-setup imports, no inline Vue tags.

Reverts aspect-ratio.md to directive form, now using ::: gn-example.
Browser smoke-tested: 2 examples rendering, description markdown
(h3 + mermaid + table) inside the slot, preview content resolving via
useExamples.
…Example a real wrapper

GnDocsExample now exposes two scoped slots that consumers can use to
inject highlighting or custom code rendering:

- code (single-file mode) — { code, fileName, language }; forwarded to
  the inner GnDocsExampleCode's default slot when set.
- panel (multi-file mode) — { file }; forwarded to GnDocsExampleTabs's
  default slot when set. Consumer renders their own per-tab content.

Both fall through to GnDocsExampleCode's <pre> default when unset.

DocsGenesisExample is now a thin wrapper that delegates the entire
structure to GnDocsExample. It plugs DocsGenesisShikiBlock into both
slots, wires playground/bin emits to the docs site's usePlayground +
getMultiFileBinUrl, and auto-resolves filePath/filePaths through
useExamples — the docs-site features stack on top of Genesis's
structural primitives.

This way the directive (::: gn-example) and the wrapper share one
visual rendering — driven by GnDocsExample's own template, classes,
and CSS.
Side-by-side comparison with legacy DocsExample on the aspect-ratio
page revealed a 56px height discrepancy: Genesis was rendering the
filename + reset-button toolbar even in peek mode, while the legacy
hides it (toolbar only appears once Show Code expands the source).

Adds v-if="!peek" on .genesis-docs-example__code-bar so peek mode
shows only preview + code peek + expand pill — matching the legacy
visual. Post-fix delta vs legacy is 1px (border rounding).
The docs site has a global .shiki rule (App.vue) that paints a
divider-colored border and 8px border-radius on every Shiki block.
Inside the example container that creates a visible inner border and
double-rounded corners that compete with the example's own outline.

Mirrors the legacy .docs-example-code .shiki override: zero border,
zero radius. The parent .genesis-docs-example__code wrapper already has
overflow:hidden + a bottom border-radius via nth-last-child, so the
shiki block's square corners get visually clipped to the example
container's rounded outline without needing its own radius.
Swap border-top to border-bottom on the tabs __bar so the toolbar
carries the divider between itself and the code panels, mirroring
the single-file __code-bar pattern.
- Override .docs-genesis-shiki-block :deep(pre code) padding to 0 so
  the wrapping <pre> owns the padding (was double-padded by the global
  .markdown-body .shiki code rule in App.vue).
- Wrap shiki blocks with an absolute-positioned actions overlay
  (Copy, Wrap, Bin, plus Playground for single-file) that fades in on
  hover/focus and is forced visible on mobile.
- Wire line-wrap toggle through useSettings().lineWrap via useSyncedRef
  and apply pre-wrap when the --wrap modifier is set.
Pass reset-icon, playground-icon, bin-icon, combine-icon, and split-icon
slots through to GnDocsExampleTabs so consumers can override the default
inline SVGs once at the GnDocsExample level. The single-file reset-icon
slot already existed; sharing the slot name means consumers only declare
each icon once and it applies in both single-file and multi-file modes.
Override reset-icon, playground-icon, bin-icon, combine-icon, and
split-icon with the docs-app AppIcon (restart, vuetify-play, vuetify-bin,
combine, split) to match the legacy DocsExample toolbar style.
Keep horizontal/top padding at 0 so the wrapping <pre> owns the inset,
but add 0.5rem bottom padding back to <code> for breathing room below
the last line (and clearance above the horizontal scrollbar when one
appears).
Render a named decoration slot at the preview container level (before
the splitter/panel content), so consumers can layer absolute-positioned
background elements like dot grids or mesh gradients behind the example
preview. Forward the slot through GnDocsExample so it can be supplied
from the top level.
Restore the dot-grid background that the legacy DocsExample renders
behind each example preview, via the new GnDocsExamplePreview decoration
slot.
Pass `combined` alongside `file` to the default slot so consumers can
branch their per-file rendering on the current tabs/stacked mode (e.g.
hide a filename badge in tabs view where the tab itself already labels
the file). Forward the prop through GnDocsExample's panel slot.
When multi-file examples render as tabs, the tab itself shows the
filename — the per-block badge is redundant. Pass `hide-filename` to
GnDocsExampleCode whenever `combined` is false, restoring the badge
when files are stacked.
johnleider added 13 commits May 21, 2026 13:40
BREAKING CHANGE: GnDocsExample no longer exposes a #panel named slot.
The #code slot now handles both single- and multi-file render with a
unified scope: { code, file, combined, language }. In single-file mode
file is undefined and combined is false; in multi-file mode file is the
GnDocsExampleFile and combined reflects the tabs/stacked state.

GnDocsExampleTabs renders GnDocsExamplePanel + GnDocsExampleCode itself
and forwards GnDocsExampleCode's default slot, attaching :file-name to
the inner code only when combined (the tab labels handle the filename
in non-combined mode). This eliminates the redundant per-file badge in
the tabs view without consumer wiring.
Drop the #panel template now that #code covers both single- and
multi-file rendering. The single #code template branches on
file presence: playground button visible when file is undefined
(single-file), title falls back to fileName from props.
Add position: sticky to the toggle bar, single-file code bar, and
multi-file tabs bar so they stay visible while scrolling long examples.
Offset is driven by two CSS variables consumers set on the root:

- --gn-docs-example-sticky-top: viewport offset of the outer header
  (default 0)
- --gn-docs-example-toggle-h: height of the toggle bar (default 45px),
  used to stack the inner toolbar directly under it

Switch the __code wrapper and tabs root from overflow: hidden to
overflow: clip so they keep clipping rounded-corner overflow without
establishing a non-scrolling container that traps sticky descendants.
Pass --gn-docs-example-sticky-top through inline style as
calc(48px + var(--app-banner-h, 0px)) so the genesis example toolbars
stick below the docs-site header and (when shown) announcement banner,
matching the legacy DocsExample behavior.
Apply var(--v0-glass-surface) + backdrop-filter blur(12px) to the
sticky toggle, code, and tabs toolbars via :deep() overrides so they
read as a continuous surface with the AppBar while scrolling. Scoped
to the docs site because --v0-glass-surface is a docs-theme token, not
a v0-core token, and other genesis consumers may not define it.
When both sticky toolbars stack at the viewport top, the divider that
normally lives on .genesis-docs-example__code (border-top) is scrolled
out of view, leaving the toggle bar and the inner code/tabs toolbar
visually fused. Add border-bottom on the toggle bar whenever
data-expanded is set so the divider follows the toolbars into the
sticky position.
Always render the bottom border on the toggle bar and drop the
border-top on .__code so the divider has a single source of truth.
The toggle bar's border now follows it into the sticky position
without the previous data-expanded gate.
The toggle bar's bottom border only makes sense when there's an inner
toolbar or code area below it to separate from. Restore the
[data-expanded] selector so a collapsed example doesn't render a
dangling bottom border above its rounded corner.
Drop the bar's vertical padding from 0.75rem to 0.5rem so the toolbar
sits closer to the toggle bar and the code area below. Horizontal
padding stays at 0.75rem.
Increase min-width and height from 30px to 32px so the toolbar action
buttons render as a square hit target instead of 32x30.
…icons

DocsCodeActions defaults to AppIconButton at 26x26 with 18px icons.
Override locally inside the hover-actions overlay so the per-pane
buttons match the GnDocsExampleActions toolbar (32x32 hit target,
16x16 icons) for a consistent visual rhythm.
AppIconButton is inline-flex but doesn't set align/justify, so the
smaller icon (after the size override) was anchored to a corner.
Add align-items: center + justify-content: center to the override and
size the inner <i> wrapper to 16x16 so the SVG sits dead-center in the
32x32 hit target.
Drop the /playground redirect (to v0play.vuetifyjs.com) and the
/genesis-sandbox demo page from the v0 docs site. Genesis will
document itself in its own site, and the v0 docs no longer need to
host either route.

Also strip the inbound /playground links from the homepage cards
(HomeDx, HomeFoundation) and from why-vuetify0.md + getting-started.md
so navigation doesn't 404. The per-example "Open in Playground"
buttons in code panes stay — they still link consumers out to v0play
on demand.
@johnleider johnleider force-pushed the worktree-paper-genesis branch from 7764503 to 0c95564 Compare May 22, 2026 16:44
DevShikiBlock was a local Shiki integration used only by the
gitignored dev/src/Playground.vue scratchpad. With the playground
reset to its template, the block has no consumer and shiki is no
longer needed in dev. Resolves the long-standing knip warnings
(unused file + unused dep) flagged by repo-integrity CI.
Genesis will document itself in its own site; the v0 docs no longer
need to host the sandbox route. The /playground redirect stays since
the homepage and intro pages still link to it.
…folder

Move all eight GnDocsExample* .vue files from individual sibling
folders into a single packages/genesis/src/components/GnDocsExample/
directory. The eight subfolder index.ts files collapse into one
index.ts that re-exports the whole suite.

BREAKING CHANGE: deep imports from the old subfolder paths
(`@paper/genesis/src/components/GnDocsExampleActions` etc.) no longer
resolve. Consumers of the package barrel (`import { X } from
'@paper/genesis'`) are unaffected.
@johnleider johnleider self-assigned this May 26, 2026
@johnleider johnleider merged commit a45b574 into master May 26, 2026
16 of 17 checks passed
@johnleider johnleider deleted the worktree-paper-genesis branch May 26, 2026 13:48
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