Automated summary
Guide for creating and maintaining xzor apps, a Blazor-inspired TypeScript component framework built with Bun and TypeScript 7.
Strengths
- Precisely describes the anatomy of a .xzor file, directives, and template syntax ;
- Provides initialization, development, and build commands ;
- Warns against Blazor confusion by listing APIs that do not exist ;
- Reminds that generated .xzor/ and dist/ directories must not be edited.
Limitations
- Highly specific to xzor and not useful outside that framework ;
- Assumes an environment with Bun and TypeScript 7 ;
- The template syntax table is cut off in the provided content, so some template details are missing.
Use when writing, changing, or explaining .xzor components, pages, layouts, or the build of an xzor application.
Do not use for Blazor, React, or Vue projects, nor as a general reference on TypeScript components.
Security analysis
SafeThe SKILL.md is pure reference documentation (syntax tables, lifecycle, routing, DI, model tracking). It declares no allowed-tools, contains no shell pipelines, network calls, credential handling, obfuscation, or attempts to alter agent instructions. The only actions a reader would take are conventional package-install and build commands, which carry the ordinary supply-chain trust assumption of any framework guide but no elevated or malicious risk.
- •No executable, destructive, or exfiltration behavior: the file is documentation for a UI framework and contains only standard dev workflow commands (`bun add`, `bun x xzor init/build`, `bun run dev`).
- •Informational: following the guide installs and runs third-party npm packages (@source-crafter/xzor, xzor-runtime) and a build step that writes to `dist/`, `.xzor/` and `sw.js`; this is normal framework tooling but relies on trust in those packages.
Examples
Create an xzor todo app with a TodoItem component, a Routes.xzor page, main.ts, and index.html. Run bun x xzor build and fix every reported error.Explain why <Router>, @Body, and StateHasChanged do not exist in xzor, and show the correct xzor patterns for routing, layout slots, and re-rendering.Add a TodoService class and inject it into a .xzor page using @inject, then update the component to load and display todos from that service.name: xzor description: Use when writing, changing or explaining .xzor components or an xzor app (Blazor-style components for TypeScript, packages @source-crafter/xzor and @source-crafter/xzor-runtime), including pages, routes, layouts, slots, cascading values, dependency injection, main.ts or the xzor build.
xzor
Overview
A .xzor file is one component: HTML with @ expressions plus a @code { } block of TypeScript class members. xzor build compiles each file to a TypeScript class, type-checks the project with TypeScript 7, and bundles main.ts into dist/main.js with Bun.
It looks like Blazor, but it is not Blazor. Many Blazor names don't exist here (<Router>, @foreach, @Body, LayoutComponentBase, @using, NavLinkMatch, StateHasChanged, createApp). When unsure, use the forms below; never invent APIs.
Turning a request into files
- Pick components (one
.xzoreach, PascalCase file name = component name), pages (components with@page), layouts (components with@slot) and services (.tsclasses). - Write
main.ts(the entry:@Mount),Routes.xzor(only if there are pages),index.html, and checktsconfig.json. - Run
bun x xzor buildand fix every error it reports before answering. Errors point at.xzorlines.
Project setup
bun add -d @source-crafter/xzor typescript@^7
bun add @source-crafter/xzor-runtime
bun x xzor init # package.json scripts, tsconfig.json, .gitignore (adds only what is missing)
bun run dev # xzor dev: build, serve (SPA fallback), print the URL; saving a .xzor patches the open page keeping state, other changes reload
bun x xzor build # one-off production build into dist/
xzor init writes this tsconfig.json (or adds the missing parts to an existing one):
{
"compilerOptions": {
"target": "ES2022", "module": "ESNext", "moduleResolution": "bundler",
"lib": ["ES2022", "ESNext.Disposable", "DOM"],
"strict": true, "noEmit": true, "skipLibCheck": true,
"rootDirs": [".", ".xzor"]
},
"include": ["**/*.ts", ".xzor/**/*.ts"],
"exclude": ["dist", "node_modules"]
}
No experimentalDecorators: xzor uses standard decorators.
index.html loads the bundle, not main.ts:
<div id="app"></div>
<script type="module" src="./dist/main.js"></script>
Generated .xzor/ and dist/ are build output: never edit them. Don't create routes.ts at the root, and never import ./main from a component.
Component anatomy
@import { type EventCallback } from "@source-crafter/xzor-runtime"
@import TodoItem from "./TodoItem.xzor"
@code {
title = "Todos"; // a field is a prop the parent can set
todos: Todo[] = [];
changed?: EventCallback<number>; // callback prop; calling it re-renders the parent
add(text: string) { // inside @code it is plain TypeScript: use this.
this.todos.push({ text, done: false });
this.changed?.(this.todos.length);
}
}
<h2>@title (@todos.length)</h2>
- Imports go at the top as
@import ...lines (any ES import), never inside@code. Directives (@page,@import,@inject,@inherits, ...) can come in any order. - In the template, write member names bare (
@title,@add(x)): xzor addsthis.itself. Inside@code, writethis.. #privatemembers also work bare in the template:@#secret.
Template syntax
| Need | Write |
|---|---|
| Print a value | @name, @user.name, @items[0], @(a + b) for anything else; an array literal needs no parentheses: @['a', 'b'] |
| Literal @ | @@ (emails like a@b.com are fine as is) |
| Condition | @if (x) { ... } else if (y) { ... } else { ... } |
| Loop | @for (const item of items) { <li @key="item.id">@item.name</li> } (any JS for head; there is no @foreach) |
| Switch | @switch (v) { case "a": <p>A</p> break; default: <p>?</p> } |
| Statements | @{ const total = sum(items); } then @total |
| No wrapper | <text>...</text> |
| Comment | @* ... *@ |
| Attribute | class="item @kind", disabled="@(!ok)" (false/null removes it) |
| Event | @onclick="save", @onclick="() => count++", <form @onsubmit="add" @onsubmit:preventDefault> (modifiers combine with the handler) |
| Two-way binding | <input @bind="text" />, @bind:event="oninput", checkbox @bind="todo.done", numbers stay numbers |
| Bind a component | <Stepper @bind-value="n" /> (child has value and valueChanged?: EventCallback<T>) |
| DOM/instance ref | @ref="input" with a field input?: HTMLInputElement; |
| Spread attributes | @attributes="attrs" |
| Inline SVG | <svg viewBox="0 0 24 24"><path d="@d" /></svg> (keep the <svg> and its children in one place, not split across an @if/@for) |
Props on a child: <Child title="text" count="n + 1" onSave="save" />. A callback prop (onSave?: EventCallback<T>) can also be set like a DOM event: @onsave="save". A prop typed string, a union of string literals, an imported alias of string literals (type Size = "s" | "m"), or given a string default (name: Kind = "a") takes text; any other type takes a TypeScript expression. Misspelled or mistyped props are build errors.
An event re-renders only the component that owns the handler, and that render reads everything again (its fields, props and injected services). If shared state changes in another component's handler, call this.stateHasChanged() on the components that show it, or pass the value down as a prop.
Styles and module code
@module {
export type Kind = "a" | "b"; // module scope: exportable, no `this`
export const label = (k: Kind) => k.toUpperCase();
}
@code { kind: Kind = "a"; pct = 40; }
<div class="bar">@label(kind)</div>
<style>
.bar { width: @pct%; }
@if (kind === "b") { .bar { color: red; } }
</style>
- One top-level
<style>per component, or a siblingX.css.xzor(not both). Rules are scoped to the component's markup and the content it passes to children;<style global>(or@globalfirst inX.css.xzor) opts out. @name/@(expr)only in property values (one CSS custom property each);@if (...) { rules } else { rules }toggles rules;@@is a literal@;@media,@keyframes, ... stay CSS.@module { }(one per file) is module-level TypeScript: constants, types, helpers, markup values; other files import its exports from the.xzor. It can't read component members.
Lifecycle
protected override onInitialized() / onInitializedAsync() once; onParametersSet() / onParametersSetAsync() on mount and when props change; then render; then onAfterRender(first: boolean). shouldRender() can skip a render, dispose() runs on removal. A resolved ...Async promise re-renders.
Slots (layouts and wrappers)
@* Card.xzor *@
<section class="card">
@slot:header { <h3>Untitled</h3> } @* named slot with fallback *@
@slot @* default slot (this is Blazor's @Body / ChildContent) *@
</section>
<Card>
@slot header { <em>Today</em> }
<p>Everything else fills the default slot.</p>
</Card>
Slots can pass data: @slot:item(todo) in the component, @slot item(t) { <b>@t.text</b> } in the parent.
Cascading values
Provider: @cascade:theme(currentTheme) { @slot }. Consumer, at any depth: @code { @Cascade theme!: string; } (or @Cascade("theme") mode!: string;). Pass a new object to notify; in-place mutation is not detected.
Pages and routing
@* pages/TodoPage.xzor *@
@page "/todos/:id"
@code { id = 0; } @* ":id" fills the prop, converted to its type *@
<h2>Todo @id</h2>
One Routes.xzor (never itself a @page) holds the router and the layouts:
@import MainLayout from "./layouts/MainLayout.xzor"
@ClientRoutes(route) {
@notFound { <p>Nothing at @route.path</p> }
@catch(error) { <p class="error">@(String(error))</p> }
@default(page) { <MainLayout>@page</MainLayout> }
}
@pageworks in any.xzorof the project (apages/folder is only a convention). The component's class name is its file name.- A layout is an ordinary component with
@slotwhere the page goes. - Typed links:
@import { href } from "./routes"(relative path; the table is generated), then<a href="@href.TodoPage({ id: 3 })">. A page without parameters needs no argument:href.About(). @import { NavLink } from "@source-crafter/xzor-runtime", then<NavLink href="/" match="exact">Home</NavLink>: it addsclass="active"andaria-current="page"on its route;match="prefix"is the default.- Animated page changes: call
viewTransitions(true)(from@source-crafter/xzor-runtime) inmain.ts; give an element the sameview-transition-nameon both pages to morph it. - Plain
<a href="/about">links navigate without reloading. - Tag routes with
@page "/admin" as adminand give them a layout with@admin(page) { <AdminLayout>@page</AdminLayout> }. - Served under a sub-path (GitHub Pages): add
<base href="/repo/">toindex.html; routes andhreffollow it.
Services (compile-time dependency injection)
// main.ts: the entry. @Mount says which component renders into which element.
import { type IApplication, Mount, Singleton, Scoped } from "@source-crafter/xzor-runtime";
import Routes from "./Routes.xzor";
import { TodoService } from "./services/todos";
@Mount(Routes, "app") // <div id="app">
@Singleton(TodoService) // or @Singleton<ITodos>(TodoService), @Scoped(...), @Transient(...)
export class Application implements IApplication {}
@import { type TodoService } from "./services/todos" @* a type import is enough: main.ts creates it *@
@inject TodoService todos @* "Type name"; then use @todos / this.todos *@
- Constructor parameters of services are resolved by type (or by key name for
@Singleton("key", factory)). Classes are plain; no decorators on them. - Group reusable registrations in a
@ServiceProvider()class and add it with@Import(Provider)onApplication. - Never call
mount()yourself and don't give an@injectcomponent aconstructor. - Missing, ambiguous or cyclic services are build errors.
Models (state that tracks its changes)
Use a model for data that is edited and then saved: a form, a row of a table, anything where you need "what changed", "undo" or "is it valid". For plain UI state, keep using @code fields.
// models/worker.ts
import { Model, Persist, Validate, minValue, required } from "@source-crafter/xzor-runtime/model";
export class Worker extends Model {
@Validate(required) name = "";
@Validate(minValue(0)) hours = 0;
@Persist get payment() { return this.hours * 20; } // sent with the fields
}
@import { EntitySet } from "@source-crafter/xzor-runtime/model"
@import { Worker } from "../models/worker"
@code {
team = EntitySet.track(Worker, [{ name: "Ada", hours: 8 }]);
}
@for (const w of team.view) {
<input type="number" @bind="w.hours" @bind:event="oninput" /> @w.payment
<small>@(w.errors.hours?.[0])</small>
<button @onclick="() => team.delete(w)">Delete</button>
}
<button @onclick="() => team.revertChanges()" disabled="@(!team.hasChanges)">Revert</button>
<button @onclick="() => team.confirmChanges()" disabled="@(team.hasErrors)">Save</button>
- Write plain fields. The build turns them into tracked ones, and warns on stderr when a class is not tracked: fix every warning.
- Extend
Model, or another model class (its fields continue after the base's). Build one withnew Worker(data)(state Unchanged) ornew Worker()(Detached; the first edit makes it Added). If you write a constructor, callthis.assign(data)in it. - A component that reads a model while it renders draws again when that value changes, even if the change comes from a service or a timer. Do not call
stateHasChanged()for models. hasState(State.Modified | ...)(State.Detached | Unchanged | Added | Deleted | Modified, from.../model; there is nostateproperty),markAdded()/markModified()/markUnchanged()/markDeleted(),changes(what to send to the server,nullif nothing; the state is sent as$$, and$$in constructor data sets it),revertChanges(),confirmChanges(serverValues?),hasErrors,errors(messages per field, filled after the first edit),hasValidChanges.- Decorators:
@Validate(...),@Persist(a getter sent with the fields),@Ignore(kept out ofchanges),@Untracked(a plain field). Validators:required,email,phone,maxLength,minLength,requiredIdValue,nonZero,minValue,maxValue. EntitySet.track(Type, items)is a list of models:delete(item),view(without deleted),changes,hasChanges,hasErrors,revertChanges(),confirmChanges(serverItems?). Change it withpush,splice, ...;set[i] = xis not noticed. Read it throughvieworfor...of(notlength, an index orset.map) so the reader follows pushes and deletes.- Needs
"ESNext.Decorators"in the tsconfiglib. Not supported: plain object graphs with cycles, one set held by two models.
Fine components (the default)
Every component is compiled with no render pass: the DOM is built once and each dynamic part updates alone when a field it reads changes. Write @render on the first line only to get the virtual-DOM render pass instead.
- Its
@codefields are tracked like a model's:count++in a handler updates the page. Never callstateHasChanged()for that; call it only after changing something untracked (a plain array'spush, an object's property). - Put list data in an
EntitySet(or reassign a plain array) and loop with@for (const x of list.view): rows are kept by identity. - Totals and other derived values are plain expressions (
@team.view.reduce((s, w) => s + w.hours, 0)); no extra fields or setters. - The tsconfig
paths(when used) must map@source-crafter/xzor-runtime/modelas well as@source-crafter/xzor-runtime.
Hot reload and PWA
xzor devhot reload: saving a.xzorpatches the changed components in the open page and keeps their state; other changes reload. A#privatemember, a new import or a changed base class reload too, and arrow-function fields keep old code until a reload.- To make the app installable and offline, add
manifest.webmanifestnext toindex.html(name,short_name,start_url: "./",scope: "./",display, a 192x192 and a 512x512 icon, both files must exist) and<link rel="manifest" href="manifest.webmanifest">inindex.html. No decorator or code is needed. xzor buildthen writessw.jsbesideindex.htmland registers it (never inxzor dev). Don't edit it: it is generated and gitignored, but it must be deployed with the app. Offline loads fall back to the cached app; a new deploy shows up on the second load (the first fetches it in the background). Fix anyxzor: manifest.webmanifest: ...warning.
Common mistakes
| Wrong (Blazor or guess) | Right |
|---|---|
| createApp(App).mount(...), app.services.add... | @Mount(App, "app") + @Singleton(...) on Application in main.ts |
| <Router>, <RouteView>, <Found> | @ClientRoutes(route) { @default(page) { ... } } in Routes.xzor |
| @inherits LayoutComponentBase, @Body | a plain component with @slot |
| @foreach (var x in xs) | @for (const x of xs) |
| imports inside @code, @using | @import ... at the top |
| NavLinkMatch.All | match="exact" |
| <script src="main.ts"> | <script type="module" src="./dist/main.js"> |
| StateHasChanged() | this.stateHasChanged() |
| TypeScript 5, experimentalDecorators | TypeScript 7, standard decorators |
| @("<") to print < | < (HTML character references are decoded, like in Razor) |
From the file to a first result
The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.
View the source and supporting filesThis download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.
1. Download the file
Download SKILL.mdFrom the root of a test project, save the file as .skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.
Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.
mkdir -p '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/xzor-components-blazor-style-typescript-mveraolu/download' --output '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md.part' && mv '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md.part' '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md'2. Check prerequisites
Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.
No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.
The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.
3. Load the skill
After inspection, copy the complete folder into your project using one of the commands below, then open a new tool session. The command stops if a folder with the same name already exists: compare the files before replacing it.
Claude Code
test -f '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && test -s '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/xzor' && cp -R '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu' '.claude/skills/xzor'Cursor
test -f '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && test -s '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && mkdir -p '.cursor/skills' && test ! -e '.cursor/skills/xzor' && cp -R '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu' '.cursor/skills/xzor'Codex
test -f '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && test -s '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md' && mkdir -p '.agents/skills' && test ! -e '.agents/skills/xzor' && cp -R '.skill-downloads/xzor-components-blazor-style-typescript-mveraolu' '.agents/skills/xzor'OpenSkills
OpenSkills must already be installed. The local install/read procedure is checked with OpenSkills 1.5.0, which requires Node.js ≥ 20.6.0. Catalog slugs are not resolved automatically.
Official OpenSkills installationopenskills install './.skill-downloads/xzor-components-blazor-style-typescript-mveraolu'openskills read 'xzor-components-blazor-style-typescript-mveraolu'Reading should display the installed file content and its base directory. It does not run the skill or install its dependencies.
For an agent that reads AGENTS.md, then sync the index. This command changes AGENTS.md: review the diff before using it.
openskills syncManual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.
4. Try a first task
Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:
Read .skill-downloads/xzor-components-blazor-style-typescript-mveraolu/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.
React Hook Form Best Practices
Development
Master React Hook Form: avoid anti-patterns, manage subscriptions, and handle reset, dirty state, and controlled inputs correctly.
Emotion to Mantine Migration
Development
Migrate Emotion styled-components to Mantine components with style props and CSS modules. Use when converting .styled.tsx files or removing @emotion imports.
Scope Creep Detector
Development
Analyzes git diffs against a stated intent to detect scope creep, unrelated files, broad PRs, and oversized changes, then suggests keep, split, or justify actions.