-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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.
- Create a folder in
packages/cli/assets/templates/pages/with your template name - Add a
page.tsx(your template) and atemplate.doc.mjs(metadata) - Run
pnpm --filter @astryxdesign/sandbox dev - Visit
http://localhost:3000/templates/<your-folder-name>/
- Create files in
packages/cli/assets/templates/blocks/— for component-specific blocks, useblocks/components/<Component>/ - Add a
<Name>.tsxfile (the block) and a matching<Name>.doc.mjsfile (metadata) - Run
pnpm --filter @astryxdesign/sandbox dev - Visit
http://localhost:3000/templates/<Name>/
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.
| 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>/
|
/** @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. |
/** @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 |
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.
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.
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.
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/).
| 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
LayoutPanelin the layout'sstartslot.
'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>
}
/>
);
}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>
}
/>
);
}-
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
LayoutContentvia itspaddingprop (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) andheight="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.
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) -
GridwithminChildWidth— 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>Page templates used to wrap themselves in AppShell. As of PR #2528 they no longer do, for two reasons:
-
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.
-
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 — two100dvhviewport 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(orCenter), neverAppShell. (Shell -category templates are the sole exception — they useAppShell.) - 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 theheaderslot. -
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 thestartslot.
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.
'use client';
import {Button} from '@astryxdesign/core/Button';
export default function ButtonBasic() {
return <Button label="Click me" variant="primary" />;
}- 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
componentsUsedin your.doc.mjsso they show up in related-component lookups - Don't wrap blocks in
AppShell— they render inside a preview container, not as full pages
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.
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.
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).
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)
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.
# 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.
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.
# 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- 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: falsewhile you're still working on it. Flip totruewhen you're happy with it.
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.
📋 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.
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-workingStep 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 devCheck 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.
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 | 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. |
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,h1–h6,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 |
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>wrappingFormLayoutfor native submission semantics -
<input type="hidden">for form state
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 |
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="..."orclassName={...}— 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 |
Rules:
- The page root element must be
LayoutorCenter— neverAppShell, no raw<div>roots. Exception:Shell -category templates must useAppShellas their root. - No
AppShell, and noTopNav/SideNavused as global app chrome — exceptShell -category templates, which useAppShellwith globalTopNav/SideNavby design. For all other categories, in-page section nav goes in anLayoutPanel(start slot); page headers go inLayoutHeader - Responsive grids use
GridwithminChildWidth(not fixedcolumnsprop, not raw CSS grid) - Centered content uses
Center(not flexbox hacks) -
Single page only — the template is one
page.tsxfile 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="#"oronClickhandlers).
| 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 |
-
Not wrapped in
AppShell(blocks render in a preview container) - Focused on a single pattern or component usage
- 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 |
Read the .doc.mjs file and check for required fields.
type: 'page'-
name: string— display name -
description: string— one sentence isReady: boolean
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 |
Search the .tsx for image URLs or <img> tags.
-
Correct: root-relative
/template-assets/<name>.pngpaths, resolving to a file underapps/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 |
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. |
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]
Start here Astryx Philosophy Contributing with AI Assistants Contributing
Architecture System Architecture Architecture Cheat Sheet Theming Infrastructure Distribution
Building a component Component Lifecycle Component Authoring Guide API Conventions Design Conventions
Quality Component Audit Rubric Accessibility Checklist
Operations Release Process Night Watch Overview