Skip to content

Contributing Templates

cixzhang edited this page Aug 11, 2026 · 16 revisions

Contributing Templates

Templates are reusable UI examples built with Astryx. There are two types:

  • Page templates — Full-page application layouts you copy to scaffold a route. Displayed full-width.
  • Block templates — Smaller UI patterns (a form card, a stats row, a hero section) you drop into an existing page. Displayed centered in a contained preview.

Both live in packages/cli/assets/templates/ and are automatically discovered by the sandbox and CLI.

Quick Start

Page templates

  1. Create a folder in packages/cli/assets/templates/pages/ with your template name
  2. Add a page.tsx (your template) and a template.doc.mjs (metadata)
  3. Run pnpm --filter @astryxdesign/sandbox dev
  4. Visit http://localhost:3000/templates/<your-folder-name>/

Block templates

  1. Create files in packages/cli/assets/templates/blocks/ — for component-specific blocks, use blocks/components/<Component>/
  2. Add a <Name>.tsx file (the block) and a matching <Name>.doc.mjs file (metadata)
  3. Run pnpm --filter @astryxdesign/sandbox dev
  4. Visit http://localhost:3000/templates/<Name>/

File Structure

packages/cli/templates/
  pages/                          # Full-page templates
    dashboard/
      page.tsx                    # The template (React component, default export)
      template.doc.mjs            # Metadata
    login/
      page.tsx
      template.doc.mjs
    ...

  blocks/                         # Block templates
    components/                   # Component-specific blocks
      Button/
        ButtonBasic.tsx           # Block component (default export)
        ButtonBasic.doc.mjs       # Block metadata
        ButtonWithIcon.tsx
        ButtonWithIcon.doc.mjs
      Table/
        TableSortable.tsx
        TableSortable.doc.mjs
        ...

Discovery is recursive — the CLI and sandbox scan templates/pages/ and templates/blocks/ at any depth, finding paired .tsx + .doc.mjs files. The subfolder structure (e.g. components/Button/) is organizational metadata, not functionally meaningful.

Page vs Block: How to Decide

If you're building... Type Why
A complete page with navigation, layout, and content Page It's scaffolding — someone copies the whole file to start a route
A reusable UI pattern or component example Block It's an ingredient — someone browses it and drops it into their page
A login/dashboard/settings page Page Full app pages are always page templates
A form card, data table with filters, hero section Block Focused patterns that compose into pages
A realistic usage example for a component Block Put it in blocks/components/<Component>/

template.doc.mjs

Page template doc

/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
export const doc = {
  type: 'page',
  name: 'Your Template Name',
  description: 'One sentence about what this template does.',
  isReady: false,
};
Field Type Description
type 'page' Must be 'page' for page templates
name string Display name shown in the sandbox gallery and CLI
description string One-sentence description
isReady boolean Set to false while developing. Shows "(WIP)" in the gallery and CLI. Set to true when it's done.

Block template doc

/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
export const doc = {
  type: 'block',
  name: 'Button — With Icon',
  description: 'Button with a leading icon and text label.',
  isReady: true,
  aspectRatio: 1,
  componentsUsed: ['Button'],
};
Field Type Description
type 'block' Must be 'block' for block templates
name string Display name, typically "Component — Example"
description string One-sentence description
isReady boolean Set to false while developing
aspectRatio number Width-to-height ratio for preview containers
scale number Optional transform scale for the preview (default 1). Use 2 or 3 for tiny components like Badge, StatusDot, or single icons so they're visible in the preview box.
componentsUsed string[] Component names this block uses, for cross-referencing

How to Pick an Aspect Ratio (blocks only)

The aspect ratio controls the preview container that blocks render inside. The block is centered in a box with this ratio, constrained to a max width of 600px. The goal is to make the component fit the box naturally without excessive whitespace.

Component shape Ratio Examples
Wide/horizontal 16 / 4 TopNav, Breadcrumbs, Banner, Toolbar, TabList, Divider
Square 1 Button, Badge, Avatar, Icon, Spinner, StatusDot
Tall/vertical 3 / 4 Calendar, SideNav, List, TreeList
Content/form 4 / 3 Card, Dialog, Table, TextInput, FormLayout

How to tweak: Open the block preview in the sandbox (/templates/<Name>/). Below the preview box you'll see the current aspect ratio and a hint. Adjust the aspectRatio value in your .doc.mjs file until the component fills the box without clipping or excessive padding.

Scale (blocks only)

For small components (Badge, StatusDot, single Button, Icon), the default preview can look tiny inside the aspect-ratio box. Set scale in your .doc.mjs to enlarge the component in the preview:

export const doc = {
  type: 'block',
  name: 'Badge — Text Badge',
  description: 'Simple text badge with the default neutral variant.',
  isReady: true,
  aspectRatio: 1,
  scale: 2,           // renders at 200% in the preview
  componentsUsed: ['Badge'],
};

The preview toolbar also has zoom controls (+/−) so reviewers can adjust the scale live without editing the doc file. The scale field sets the default.


Building Page Templates

If you're building a page template, follow these rules.

Your template is a React component with a default export. It renders fullscreen (no sandbox shell wrapping it), so you control the entire page layout. Use 'use client'; at the top.

Templates are content-only — do NOT use AppShell (except the Shell category)

The rule: page templates must not render AppShell (or TopNav / SideNav as global app chrome). The page root is Layout (or Center for centered single-card pages).

Exception — the Shell category. Templates whose category begins with Shell - (e.g. Shell - Top Nav, Shell - Left Sidebar, Shell - Top Nav + Left Sidebar) exist specifically to demonstrate app shell + navigation composition. They must use AppShell as their root, render TopNav and/or SideNav as global chrome, and typically fill the body with Skeleton placeholders. Every other category is content-only and uses Layout (or Center) as its root, per the rest of this section.

Templates are page content, not whole applications. Whoever consumes a template owns the surrounding chrome:

  • An internal product wraps it in its own app shell (with company nav).
  • The OSS sandbox/docsite wraps it in a standard demo shell.
  • A standalone tool may use no shell at all.

A template that shipped its own AppShell would collide with the host's: you'd get a shell nested inside a shell (two viewport-claiming 100dvh containers, doubled scroll regions, duplicated chrome), and the host would need conditional logic to detect and strip the inner shell before injecting its own. By keeping templates content-only, the host decides the shell exactly once, and there is no conditionality to maintain. (See the "Why content-only?" note below.)

If you want to demonstrate shell composition itself (TopNav + SideNav + banner) as a full page, use a Shell - category page template (root AppShell). Smaller, isolated shell-composition examples can also live in the AppShell blocks (blocks/components/AppShell/).

Page root element

I'm building... Root element Why
Any full app page (dashboard, table, settings, editor) Layout Content frame with header / content / start / end / footer slots. The host supplies the shell + nav around it.
A page with its own in-page section navigation (settings tabs, a docs catalog that switches the content pane) Layout with a start panel This nav is part of the page, not app chrome — render it as an LayoutPanel in the start slot.
A page-level header (title + page-scoped search/actions) Layout with a header slot Page headers belong to the page; put them in LayoutHeader. (App chrome — global top nav — does not.)
A standalone page with no chrome (login, auth, marketing, onboarding) Center Centered single-card layout.
A sub-layout inside the content area Layout nested inside the content Split panes, detail panels, IDE-style multi-pane layouts.
A Shell - category template (showcasing app shell + nav) AppShell The whole point is to demonstrate global chrome — render TopNav / SideNav in the shell and Skeleton placeholders for the body.

Valid page root elements: Layout or Center — except Shell - category templates, which use AppShell as their root. No raw <div> roots.

Distinguish app chrome from in-page navigation. App chrome is global navigation that belongs to the whole product (top-level nav between sections, a product logo, account menus, a File/Edit/View menubar) — delete it; the host provides it. In-page navigation switches content within this one page (settings tabs, a documentation component switcher, a VSCode-style activity bar) — keep it, rendered as an LayoutPanel in the layout's start slot.

Minimal page template

'use client';

import {Layout, LayoutContent} from '@astryxdesign/core/Layout';
import {Heading, Text} from '@astryxdesign/core/Text';

export default function MyTemplate() {
  return (
    <Layout
      content={
        <LayoutContent>
          <Heading level={1}>Page Title</Heading>
          <Text type="body">Page content goes here.</Text>
        </LayoutContent>
      }
    />
  );
}

Page with a header and in-page section navigation

Use the header slot for the page title/actions and the start slot for in-page section nav. (Note: this is in-page nav — switching sections within the page — not global app chrome.)

'use client';

import {useState} from 'react';
import {
  Layout,
  LayoutContent,
  LayoutHeader,
  LayoutPanel,
} from '@astryxdesign/core/Layout';
import {Heading} from '@astryxdesign/core/Text';
import {List, ListItem} from '@astryxdesign/core/List';

const SECTIONS = ['Profile', 'Account', 'Notifications'];

export default function MyTemplate() {
  const [active, setActive] = useState('Profile');
  return (
    <Layout
      height="auto"
      header={
        <LayoutHeader hasDivider>
          <Heading level={1}>Settings</Heading>
        </LayoutHeader>
      }
      start={
        <LayoutPanel hasDivider={false} padding={0}>
          <List density="balanced">
            {SECTIONS.map(s => (
              <ListItem
                key={s}
                label={s}
                isSelected={active === s}
                onClick={() => setActive(s)}
              />
            ))}
          </List>
        </LayoutPanel>
      }
      content={
        <LayoutContent padding={4}>
          {/* Section content for `active` */}
        </LayoutContent>
      }
    />
  );
}

Key Layout props

  • height: 'fill' (100% of host container, content scrolls internally) | 'auto' (grows with content, page scrolls as a whole)
  • contentWidth: max content width in px (centers content; dividers stay full-bleed)
  • Slots: header (LayoutHeader), content (LayoutContent), start / end (LayoutPanel), footer (LayoutFooter)
  • Content padding lives on LayoutContent via its padding prop (0 for dashboards/tables, 4 for forms/settings)

Don't paint a background or claim the viewport in the template — that's the host shell's job. Pick height="fill" for app-style pages whose content scrolls internally (the host gives them a full-height region) and height="auto" for document-style pages.

Use Astryx components from @astryxdesign/core/*. StyleX works for custom styles. Check npx astryx component --list to see what's available.

Composition rules for pages

Icons: always use Icon. Wrap SVG icons in <Icon> to get consistent sizing, color tokens, and alignment. Don't render raw <svg> elements at arbitrary sizes.

// ✅ Correct — themed color + consistent size
import {Icon} from '@astryxdesign/core/Icon';
import {ShieldCheckIcon} from '@heroicons/react/24/outline';

<Icon icon={ShieldCheckIcon} size="md" color="secondary" />

// ❌ Wrong — raw SVG with hardcoded dimensions
<ShieldCheckIcon style={{width: 20, height: 20}} />

Icon sizes: xsm (12px), sm (16px), md (20px), lg (24px).

Exception: Components that accept an icon prop (like SideNavItem, Button) handle wrapping internally — pass the component reference directly:

// ✅ SideNavItem wraps it for you
<SideNavItem label="Dashboard" icon={HomeIcon} />

// ❌ Don't double-wrap
<SideNavItem label="Dashboard" icon={<Icon icon={HomeIcon} />} />

Responsive grids: use Grid, not stacks. Use Grid with minChildWidth for any grid of cards, tiles, or items that should reflow. Do NOT use HStack with wrapping or manual media queries.

// ✅ Auto-reflows based on available width
<Grid minChildWidth={280} gap={4}>
  <Card>Item 1</Card>
  <Card>Item 2</Card>
  <Card>Item 3</Card>
</Grid>

// ❌ Doesn't reflow, breaks on small screens
<HStack gap={4}>
  <Card>Item 1</Card>
  <Card>Item 2</Card>
  <Card>Item 3</Card>
</HStack>

Stacks vs Grid:

  • VStack / HStack — fixed-direction layouts (forms, toolbars, button rows)
  • Grid with minChildWidth — responsive grids that reflow (KPI cards, product tiles, galleries)

Centering: use Center. For centering content (empty states, login forms, loading screens), use Center instead of flexbox or StyleX hacks.

<Center height="fill">
  <Card>Centered content</Card>
</Center>

Why content-only? (background)

Page templates used to wrap themselves in AppShell. As of PR #2528 they no longer do, for two reasons:

  1. Consistency. Templates were inconsistent about whether they included navigation chrome. Some shipped a full sideNav, some a topNav, some none — so copying two templates into one app gave you mismatched, competing shells. Making every template content-only removes that variance: a template is always just the page body.

  2. No internal-vs-external shell conditional. Consumers each have their own shell — an internal product injects its company app chrome, the OSS sandbox uses a demo shell, some tools use none. The host swaps in its shell around the template. If the template also shipped an AppShell, the host would have to branch: "does this template already have a shell? if so, unwrap it before adding mine." That conditional is exactly what we wanted to avoid. A shell-inside-a-shell is also broken on its own — two 100dvh viewport containers, nested scroll regions, and doubled chrome padding.

The fix is structural, not cosmetic: the template never provides a shell, so the host owns it unconditionally. Shell + nav is a separate, swappable layer — demonstrated either in the Shell - category page templates (root AppShell) or in the AppShell blocks (blocks/components/AppShell/), which is where shell-composition examples belong. This content-only rule applies to every category except Shell -, whose entire purpose is to showcase the shell.

What this means when authoring or migrating a template (every category except Shell -):

  • Root is Layout (or Center), never AppShell. (Shell - category templates are the sole exception — they use AppShell.)
  • App chrome → delete it. Global/product navigation (top-level section nav, product logo, account menu, File/Edit/View menubar) is the host's responsibility.
  • Page header → LayoutHeader. A title plus page-scoped search/actions belongs to the page (hosts don't supply page headers). Put it in the header slot.
  • In-page section nav → LayoutPanel. Navigation that switches content within this page (settings tabs, a docs catalog, an IDE activity bar) stays in the template, rendered in the start slot.

Building Block Templates

If you're building a block template, follow these rules.

Blocks are smaller, focused components. They render centered in a contained preview, not fullscreen. Keep them to 20-100 lines. Include all necessary imports. Use 'use client'; at the top.

Minimal block template

'use client';

import {Button} from '@astryxdesign/core/Button';

export default function ButtonBasic() {
  return <Button label="Click me" variant="primary" />;
}

Block guidelines

  • Keep blocks focused on one pattern or one component usage
  • Include all imports — blocks must be self-contained
  • Use realistic mock data (real names, realistic values)
  • List all Astryx components used in componentsUsed in your .doc.mjs so they show up in related-component lookups
  • Don't wrap blocks in AppShell — they render inside a preview container, not as full pages

How It Works

A sync script (scripts/sync-templates.js) recursively scans templates/pages/ and templates/blocks/ and generates:

  • Route wrappers under (fullscreen)/templates/ so the sandbox can render each template and block at /templates/<slug>/
  • Registry files (templateRegistry.ts, blockRegistry.ts) with metadata (name, description, component, aspectRatio, etc.)
  • Source registry for the code view

This runs automatically via predev and prebuild hooks. You don't need to run it manually.

Unified /templates/ route

Both page templates and blocks live under /templates/<slug>/. There is no separate /blocks/ route. The preview shell automatically detects whether a slug is a block (by checking the block registry) and renders it accordingly:

  • Page templates render fullscreen with viewport controls (desktop/tablet/mobile). Tablet and mobile use an iframe so CSS media queries respond to the actual viewport width.
  • Blocks render centered in an aspect-ratio box (max-width 600px) with the ratio displayed below. No viewport controls.

Official Templates table

The sandbox has an Official Templates page at /templates/ (accessible from the sidebar). It shows a sortable, filterable table of all templates and blocks with columns for Type, Name, Component, Summary, and copy-to-clipboard paths for Code and Doc files. Sort and filter state syncs to URL query params so you can share filtered views (e.g. ?filter.type=Block&filter.component=Button).

Sidebar tree navigation

In the preview shell, clicking the sidebar icon (left of the toolbar) opens a tree view:

  • Pages — all page templates
  • Components — blocks grouped by component (e.g. Button > Basic, With Icon)

Block organization

Blocks can be component-specific or general:

  • Component-specific blocks live in blocks/components/<ComponentName>/ (e.g. blocks/components/Button/ButtonBasic.tsx). They appear grouped under their component in the sidebar tree and the templates table.
  • General blocks can live anywhere under blocks/. The parent folder name becomes the component association.

Local Development

# From the repo root
pnpm --filter @astryxdesign/sandbox dev

# All templates and blocks:
# http://localhost:3000/templates/<slug>/

# Official Templates table:
# http://localhost:3000/templates/

Hot reload works. Edit your files and see changes immediately in the browser.

PR Preview

When you open a PR, GitHub Actions builds the sandbox and deploys a preview. Your template will be visible at:

https://studious-broccoli-o7e61n3.pages.github.io/pr/<PR-number>/sandbox/templates/<folder-name>/

The PR comment includes a link to the sandbox preview.

CLI

# List all templates (pages and blocks)
npx astryx template --list

# List only page templates
npx astryx template --list --type page

# List only block templates
npx astryx template --list --type block

# Show a template's source
npx astryx template <name>

# Copy a template to your project
npx astryx template <name> ./src/pages/my-page

# View a component with its related blocks
npx astryx component Button

General Tips

  • Keep templates self-contained. All imports should come from @astryxdesign/core/* or be defined in the template itself.
  • Use realistic fake data. Templates should look like a real app, not a placeholder.
  • Set isReady: false while you're still working on it. Flip to true when you're happy with it.

How to Use Images in Templates

Templates that need images use the checked-in demo assets under apps/docsite/public/template-assets/, referenced by a root-relative path.

  • ✅ Reference /template-assets/<name>.png — 101 assets ship in the repo today
  • ❌ Do NOT link to external URLs (unsplash, placehold.co, picsum, …)
  • ❌ Do NOT paste a signed CDN URL (https://scontent.xx.fbcdn.net/…?oh=…&oe=…) — those carry time-limited tokens and expire within days

Why checked-in, not a CDN. A template's images have to survive being scaffolded into someone else's project, offline, on a fork, with no network. The CLI handles that: stripTemplateAssetRefs (packages/cli/foundation/discovery/template-adapter.mjs) rewrites every /template-assets/… reference to a self-contained placeholder data URI when a template is scaffolded, so the page renders with zero setup and the builder drops in their own images. That rewrite keys on the root-relative path — a CDN URL would slip straight through it and leave the scaffolded project pointing at an asset it does not own.

Naming follows {style}-{category}-{orientation}-{number}, e.g. /template-assets/light-home-square-1.png.

What's available

📋 Browsing? See Template Assets for the full, categorized list of every image and video in the set, with direct links to preview each one.

The astryx set currently has ~140 images plus one video. Most images follow the pattern {style}-{category}-{orientation}-{number}:

Dimension Options
Style colorful, light, moody, illustrative
Category home, lifestyle, working, product, scene
Orientation horizontal, vertical, square

Some assets have unique names outside this pattern (e.g., building, theme-example families like Butter-* / Y2K-*, generic demo people DATA-*). Not all combinations exist — always check the full list before assuming a name is valid.

How to find and use an image

Step 1: Find the asset. Browse apps/docsite/public/template-assets/ in the repo, or the Template Assets page for a categorized list with previews.

ls apps/docsite/public/template-assets/ | grep light-working

Step 2: Reference it root-relative. The path is /template-assets/ plus the filename — no host, no origin:

<img src="/template-assets/light-working-vertical-1.png" alt="…" />

@2x variants exist for some assets (DATA-Ami-Pena@2x.png). If the image you need is not in the set, add the file under apps/docsite/public/template-assets/ in the same PR, following the {style}-{category}-{orientation}-{number} naming.

Step 3: Verify it renders. Run the sandbox or docsite and look at the template — a path typo produces a broken image, not a build error.

pnpm -F @astryxdesign/sandbox dev

Check the scaffolded form too, since that is what a consumer gets: stripTemplateAssetRefs swaps every /template-assets/… reference for a placeholder data URI, so the template must still read sensibly with placeholder images in place of yours.


Template Grading Rubric

Quality rubric for grading templates. Use this to evaluate any template (page or block) against the guidelines above. Every template in the gallery should score B or above.

Grade Scale

Grade Score Meaning
A 90–100 Exemplary. Copy-paste ready. No excuses needed.
B 75–89 Good. Minor issues a reviewer would flag but approve.
C 60–74 Needs work. Would get "request changes" in review.
D 40–59 Poor. Significant rewrites needed.
F 0–39 Failing. Actively teaches bad patterns.

Scoring Categories (100 points)

1. Astryx Component Purity (30 pts)

Count every JSX opening tag in the .tsx file. Classify each as Astryx or raw HTML.

  • Astryx element: Any component imported from @astryxdesign/core (e.g. <Button>, <VStack>, <Card>). Component names are unprefixed.
  • Raw HTML element: Any lowercase intrinsic JSX tag: div, span, button, nav, aside, main, section, article, header, footer, figure, ul, ol, li, p, h1h6, form, input, textarea, select, option, label, a, table, thead, tbody, tr, td, th, dl, dt, dd, hr, br, pre, code, blockquote, details, summary, dialog, menu, img, picture, video, audio, canvas, iframe
  • Don't count: React fragments (<> / <Fragment>), locally-defined PascalCase components (e.g. <Sidebar>, <KPICard>) — these are fine as long as they're defined in the same file and internally use Astryx components

For each raw HTML tag found, judge whether it's necessary (no Astryx equivalent) or unnecessary (an Astryx component could replace it).

Raw HTML elements Points
0 30
1–2 (all necessary) 25
1–2 (any unnecessary) 20
3–5 15
6–10 8
11–20 4
21+ 0
Astryx replacement reference

Use this to judge "necessary" vs "unnecessary":

Raw HTML Astryx Replacement
<div> for layout VStack, HStack, Card, Section, Center
<div> for grid Grid
<span> for text Text
<p>, <h1><h6> Text type="body", Heading level={N}
<button> Button, IconButton
<a> Link
<nav>, <aside> In-page nav → LayoutPanel (start slot). Global app nav → omit; the host shell provides it.
<header>, <main> LayoutHeader, LayoutContent slots of Layout
<ul>, <ol>, <li> List, ListItem
<input> TextInput, NumberInput, CheckboxInput, etc.
<textarea> TextArea
<select> Selector
<table>, <tr>, <td> Table (data-driven API)
<img> Acceptable IF src is a /template-assets/… path
<form> FormLayout
<hr> Divider
<dialog> Dialog
<details>, <summary> Collapsible

Necessary exceptions (don't penalize):

  • <img> with a /template-assets/… src (Astryx doesn't have a general Image component)
  • <form> wrapping FormLayout for native submission semantics
  • <input type="hidden"> for form state

2. Icon Purity (15 pts)

Every icon must use one of these patterns:

  • <Icon icon={SomeHeroIcon} /> — standalone icon
  • icon={SomeHeroIcon} as a prop on Astryx components (Button, SideNavItem, etc.)

Violations:

  • Raw <svg> elements used as icons
  • Inline SVG components defined in the file (function ChevronIcon() { return <svg>... })
  • Heroicons rendered without Icon wrapper (<ChevronDownIcon className="w-5" />)
  • Any <svg>, <path>, <circle>, <rect>, <line>, <polyline>, <polygon>, <ellipse>, <g> elements
Raw SVG icon instances Points
0 15
1–2 10
3–5 5
6+ 0

3. Custom CSS (15 pts)

Templates should style through Astryx component props (gap, padding, variant, size, color, etc.), not custom CSS.

What counts as custom CSS (count individual CSS properties, not call sites):

  • Properties inside stylex.create({...}) — e.g. { root: { padding: 8, display: 'flex' } } = 2 declarations
  • Properties inside style={{...}} JSX props — e.g. style={{ display: 'flex', gap: 8 }} = 2 declarations
  • className="..." or className={...} — 1 per usage
  • stylex.props(...) in JSX — 1 per usage

What does NOT count:

  • Astryx component props that accept design-token values: gap={4}, padding={3}, variant="primary", size="md", color="secondary", level={2}, minChildWidth={280}, contentPadding={4}, height="fill" — these are the system working as designed
Total custom style declarations Points
0 15
1–3 (justified — no Astryx alternative) 12
1–3 (unjustified — Astryx prop exists) 8
4–10 5
11–20 2
21+ 0

4. Layout & Structure (15 pts)

For page templates

Rules:

  1. The page root element must be Layout or Center — never AppShell, no raw <div> roots. Exception: Shell - category templates must use AppShell as their root.
  2. No AppShell, and no TopNav / SideNav used as global app chrome — except Shell - category templates, which use AppShell with global TopNav / SideNav by design. For all other categories, in-page section nav goes in an LayoutPanel (start slot); page headers go in LayoutHeader
  3. Responsive grids use Grid with minChildWidth (not fixed columns prop, not raw CSS grid)
  4. Centered content uses Center (not flexbox hacks)
  5. Single page only — the template is one page.tsx file rendering one page. No nested routes, no multi-page navigation that actually works, no router integration. The template demonstrates one page layout. Navigation links can exist but should be inert (href="#" or onClick handlers).
Condition Points
Correct root for the category — AppShell for Shell - templates, Layout / Center for all others — plus responsive grids, proper centering, single page 15
Valid root element but with issues (fixed columns, multi-page) 8
Wrong root for the category (AppShell in a non-Shell template, or a non-AppShell root in a Shell - template), raw div root, or no Astryx root element 0
For block templates
  1. Not wrapped in AppShell (blocks render in a preview container)
  2. Focused on a single pattern or component usage
  3. Between 20–100 lines (under 20 is too thin to be useful, over 100 is unfocused)
Condition Points
No AppShell, single-pattern focus, reasonable length 15
Minor issues (slightly long, slightly unfocused) 10
Wrapped in AppShell OR deeply unfocused 0

5. Doc Metadata (10 pts)

Read the .doc.mjs file and check for required fields.

Page templates — required fields:
  • type: 'page'
  • name: string — display name
  • description: string — one sentence
  • isReady: boolean
Block templates — required fields:
  • type: 'block'
  • name: string — typically "Component — Variant"
  • description: string — one sentence
  • isReady: boolean
  • aspectRatio: number — must match component shape (see table)
  • componentsUsed: string[] — must list ALL Astryx components actually used in the .tsx

Aspect ratio guide (blocks):

Component shape Ratio Examples
Wide/horizontal 16 / 4 TopNav, Breadcrumbs, Banner, Toolbar, TabList, Divider, Pagination, SegmentedControl
Square 1 Button, Badge, Avatar, Icon, Spinner, StatusDot
Tall/vertical 3 / 4 Calendar, SideNav, List, TreeList
Content/form 4 / 3 Card, Dialog, Table, TextInput, FormLayout, TextArea

Scale (blocks with small components): Badge, StatusDot, single Icon, Spinner → scale: 2 or 3.

componentsUsed accuracy: Every component imported in the .tsx must appear in componentsUsed (e.g. Button'Button').

Condition Points
All fields present, aspectRatio correct, componentsUsed accurate, scale where needed 10
All fields present but 1 inaccuracy 7
Missing 1 required field 4
Missing 2+ fields or no .doc.mjs 0

6. Image Handling (5 pts)

Search the .tsx for image URLs or <img> tags.

  • Correct: root-relative /template-assets/<name>.png paths, resolving to a file under apps/docsite/public/template-assets/. Names follow {style}-{category}-{orientation}-{number}.
  • Incorrect: any absolute URL — a CDN or lookaside link bypasses stripTemplateAssetRefs — plus external URLs (unsplash, placehold.co, picsum), and data URIs standing in for real images.
Condition Points
No images needed, or every src is a /template-assets/… path that exists 5
A /template-assets/… path that does not resolve to a checked-in file 2
Placeholder services (placehold.co, picsum, …) 2
Any absolute URL — CDN, lookaside, or external 0

7. Code Quality (10 pts)

2 points each:

Check Points How to verify
'use client' directive 2 First line of .tsx is 'use client';
Default export 2 File has export default function ComponentName()
Self-contained imports 2 ALL imports from @astryxdesign/core/* or @heroicons/react/* or defined in same file. No external packages.
Realistic mock data 2 Uses names like "Sarah Chen", amounts like "$12,450", dates, etc. NOT "Lorem ipsum", "Item 1", "Placeholder", "Example", "Test", "foo", "bar".
No dead code 2 No unused imports, no unused variables, no commented-out blocks, no defined-but-never-called functions.

Grading Output Format

When grading a template, produce this format:

# Template Grade: [NAME]

**Type**: page | block
**File**: [path]
**Grade**: [LETTER] ([SCORE]/100)

## Scorecard

| Category | Points | Max | Details |
|----------|--------|-----|---------|
| Astryx Component Purity | X | 30 | |
| Icon Purity | X | 15 | |
| Custom CSS | X | 15 | |
| Layout & Structure | X | 15 | |
| Doc Metadata | X | 10 | |
| Image Handling | X | 5 | |
| Code Quality | X | 10 | |
| **TOTAL** | **X** | **100** | |

## Detailed Findings

### Raw HTML Elements ([count])
[line number, tag, necessary or has Astryx replacement]

### Raw SVG Icons ([count])
[line number, what icon]

### Custom CSS Declarations ([count])
[line number, property, what Astryx prop could replace it]

### Layout Issues
[description]

### Doc Metadata Issues
[missing or inaccurate fields]

### Image Issues
[problematic URLs]

### Code Quality Issues
[violations]

## Top 3 Fixes
1. [highest impact]
2. [second]
3. [third]

Clone this wiki locally