Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

243 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Puzzle

A SPA-first JavaScript framework with single-file components, reactive data binding, and Go-based compilation.

▶ Live demo — the music example app built with Puzzle.

  • ~200 ms production builds — compile, bundle, Tailwind, and minify, end to end (todos example, Apple Silicon)
  • 18 KB gzipped apps — the complete todos example (runtime, router, store, and views) ships at 18.3 KB gzip / 55.9 KB raw
  • Zero JavaScript toolchain — the CLI is one prebuilt Go binary; no Babel, no bundler config, no postinstall scripts

Quick start

Install the puzzle CLI once (a prebuilt Go binary — no toolchain needed):

npm install -g @magic-spells/puzzle

Then scaffold and run a new app:

puzzle init my-app
cd my-app
npm install

puzzle dev     # develop with live reload
puzzle build   # build for production

Or add Puzzle to an existing project — one dev dependency gives you both the client runtime and the CLI:

npm install -D @magic-spells/puzzle

Status: 0.1.0 — the first public release. The browser runtime, Go compiler, static generator, and CLI are implemented and covered by Go, Vitest/jsdom, type, package, example, and browser-focused checks.

constellation/doc/DOC-SPEC.md is the canonical, frozen v1 contract — its per-amendment sections (§12–§41) are the source of truth for exactly what shipped when; if anything here conflicts with it, the spec wins.

Features

  • Single-file components (.pzl) with template + scripts + styles — optional TypeScript (<script lang="ts">), scoped styles (<style scoped>), skeletons, comments, slots, and refs
  • Reactive data with automatic view updates
  • Model/store architecture with adapters, relationships, schema validation, persistence, and write sync
  • Chainable display formatters{ title | downcase | truncate(40) }
  • Nested routing with view slots — history, hash, and memory modes; scroll restoration; base paths; anchors; mode-agnostic path-shaped hrefs via the built-in link formatter
  • Virtual DOM with efficient diffing and pk-aware list keying
  • Built-in view & component animations (Web Animations API), including visibility-triggered enters and app lifecycle hooks
  • Route transitions: sequential by default; overlapping cross-fades and shared-element morphs (experimental — see below)
  • Go-based compiler for fast builds and state-preserving live reload (store and JSON-safe local view state survive edits)
  • SPA-first output with two optional prerender modesoutput: 'hybrid' (prerendered pages the SPA takes over) and output: 'static' (true static pages, no router or app.js); no request-time SSR server or hydration layer
  • Puzzle Pieces component library — ready-made .pzl components installed with puzzle add piece <name> (browse the catalog)

Experimental in 0.1.0: overlapping route transitions (transitionMode: 'overlap') and shared-element morph transitions (@magic-spells/puzzle/morph) work and are tested individually, but their interaction matrix with other opt-in features (nested reused layouts, hash/base-path routing, anchors) has had less real-world mileage than the core. They're safe to try — just expect rougher edges there, and prefer the default sequential transitions where stability matters most.

Installation

Install Puzzle as a dev dependency. This one package gives you both the client runtime (import { PuzzleView } from '@magic-spells/puzzle') and the puzzle CLI:

npm install -D @magic-spells/puzzle

The CLI is a prebuilt Go binary delivered through per-platform optional dependencies (no compiler toolchain, no postinstall step). Prebuilt binaries ship for macOS (arm64, x64) and Linux (x64, arm64); npm downloads only the one matching your machine. Once installed, puzzle is on your PATH for npm scripts:

// package.json
{
  "scripts": {
    "dev": "puzzle dev",
    "build": "puzzle build"
  }
}

Scaffold a new project

With the CLI installed globally (npm install -g @magic-spells/puzzle):

puzzle init my-app
cd my-app
npm install
npm run dev

The generated app depends on @magic-spells/puzzle locally, so collaborators who clone it only need npm install — no global CLI required.

Unsupported platforms (e.g. Windows) or building from source

The prebuilt binary covers macOS and Linux. On any other platform — or if you prefer to build the CLI yourself — install it from source with Go:

go install github.com/magic-spells/puzzle/compiler/cmd/puzzle@latest

Project Structure

my-puzzle-app/
├── app/
│   ├── app.js                # App initialization
│   ├── routes.js             # Route definitions
│   ├── models/               # Optional models and adapters
│   ├── views/                # Routed .pzl views
│   ├── components/           # Reusable .pzl components
│   ├── layouts/              # Route layouts with <Slot/>
│   ├── assets/               # Source assets, including {#svg} files
│   ├── styles/               # Tailwind entry + global CSS
│   └── public/               # Static assets + index.html
├── puzzle.config.js          # Compiler, styles, and output config
└── package.json

Template Syntax

Basic Interpolation

<p>{ user.name }</p>
<h1>{ title | capitalize }</h1>

Comments

{## Single-line hash comments ##} {## Multi-line hash comments work too These
are useful for documentation ##}

<div class="user-card">
  {## TODO: Add user avatar ##}
  <h2>{ user.name }</h2>

  {#comment}
  <div class="temporarily-disabled">
    <p>{ user.bio }</p>
  </div>
  {/comment}
</div>

Control Flow

<!-- Conditionals with {:else if} chaining -->
{#if loggedIn}
  <p>Welcome back!</p>
{:else if loading}
  <p>Loading...</p>
{:else}
  <p>Please log in</p>
{/if}

<!-- Loops -->
{#for item in items}
  <li>{ item.name }</li>
{/for}

<!-- Multi-branch; a {:when} takes comma-separated values, matching any of them -->
{#case status}
  {:when 'loading', 'paused'}
    <LoadingSpinner />
  {:when 'error', 'no-connection'}
    <ErrorMessage />
  {:else}
    <SuccessContent />
{/case}

<!-- Inverted conditional -->
{#unless user.isAdmin}
  <p>Access denied</p>
{/unless}

See constellation/doc/DOC-TEMPLATE-SYNTAX.md for the full grammar.

Events & Binding

<!-- Event handlers -->
<button @click={ handleClick }>Click me</button>
<form @submit={ handleForm(event) }>
  <!-- Controlled form property; the handler updates component/store state -->
  <input value={ searchQuery } @input={ updateSearch(event) } />
  <select value={ selectedOption }></select>
</form>

<!-- Event modifiers: prevent / stop / once + key filters, and they stack -->
<input @keydown:enter={ handleSubmit } @keydown:escape:prevent={ cancelEdit } />
<button @click:once={ claimReward }>Claim</button>

Event modifiers (prevent, stop, once, and key filters like :enter/:escape) stack; the canonical order is key-gate → once-spend → preventDefault → stopPropagation → handler. See constellation/doc/DOC-SPEC.md §5.

Slots & Nested Routing

<!-- Layout slot: the routed view renders at <Slot/> -->
<div class="user-layout">
  <nav><!-- user navigation --></nav>
  <Slot />
  <!-- routed view renders here -->
</div>

<!-- Named component slots: static slot="name" on a direct child -->
<Card>
  <h2 slot="header">Card Title</h2>
  <p>Card content</p>
  <!-- no slot attr → default slot -->
</Card>

Named slots. The child declares regions with <Slot name="header"/>, and the call site routes a direct child into one with a static slot="header" attribute (stripped from the rendered output). An unfilled marker renders its fallback body — the content between paired marker tags — or nothing when self-closing; routed views fill the default marker only. See constellation/doc/DOC-SPEC.md §24.

Reusable components declare default child content with <Children/>, and a paired marker's body is the fallback shown when the caller supplies nothing:

<article class="card">
  <Children/>
  <footer><Slot name="footer"><button>OK</button></Slot></footer>
</article>

Built-in Formatters

Formatters transform data for display without modifying the underlying values. They chain left to right with |, so each one receives the previous result:

{ title | downcase | replace('-', ' ') }
<!-- "My-Blog-Post" → "my blog post" -->
{ post.body | trim | truncate(140) | capitalize }
<!-- Chains can be any length; arguments go in parentheses -->

An unregistered formatter name never crashes a render — the value passes through that step unchanged and a single console.error names the offender.

String Formatters

{ text | trim }
<!-- Remove whitespace -->
{ name | capitalize }
<!-- First letter uppercase -->
{ title | upcase }
<!-- ALL UPPERCASE -->
{ title | downcase }
<!-- all lowercase -->
{ content | truncate(100) }
<!-- Limit to 100 chars -->
{ slug | replace('-', ' ') }
<!-- Replace characters -->

Number Formatters

{ price | currency('$', 2) }
<!-- $19.99 -->
{ progress | percentage }
<!-- 75% -->
{ count | number_with_delimiter }
<!-- 1,234,567 -->
{ rating | round(1) }
<!-- 4.3 -->

Array Formatters

{ names | join(', ') }
<!-- Join with commas -->

Date Formatters

{ createdAt | date('long') }
<!-- January 15, 2024 -->
{ updatedAt | date('short') }
<!-- 1/15/24 -->
{ publishedAt | timeago }
<!-- 2 hours ago -->

Utility Formatters

{ html | raw }
<!-- Skips entity escaping; still renders as text, not injected HTML -->
{ obj | json }
<!-- JSON stringify -->

Single-File Components

<!-- Button.pzl -->
<puzzle-view>
  <button
    class="btn { variantClass } { sizeClass }"
    @click={ handleClick }
    disabled={ disabled }>
    {#if icon}
    <Icon name={ icon } />
    {/if}
    <Children />
  </button>
</puzzle-view>

<script>
  import { PuzzleView } from '@magic-spells/puzzle';
  import Icon from './Icon.pzl';

  export default class Button extends PuzzleView {
    data(params, props) {
      const variant = props.variant || 'default'
      const size = props.size || 'medium'
      const icon = props.icon || null
      const disabled = !!props.disabled

      return {
        variant,
        size,
        icon,
        disabled,
        variantClass: `btn--${variant}`,
        sizeClass: `btn--${size}`
      }
    }

    // `click` is a callback prop: a parent writes <Button @click={ handler }>
    // and the compiler hands the child a function on this.props.click. There is
    // no this.$emit — the child gates the event, the parent's function does the work.
    events = {
      handleClick: (event) => {
        const { disabled } = this.getData()
        const { click } = this.props
        if (!disabled && typeof click === 'function') {
          click(event)
        }
      }
    }
  }
</script>

<style>
  .btn { padding: 0.75rem 1.5rem; border: 1px solid transparent; border-radius:
  0.5rem; cursor: pointer; transition: all 0.2s; } .btn--primary { background:
  var(--primary-color); color: white; } .btn--medium { font-size: 1rem; }
</style>

Imports

Component imports live in <script> and can be relative or use @, the built-in alias for your app/ directory — no configuration, works from any depth:

import Icon from './Icon.pzl';              // relative
import Icon from '@/components/Icon.pzl';   // app/components/Icon.pzl

Puzzle Pieces

Puzzle Pieces is the official component library for Puzzle — ready-made .pzl components (and their styles) you can drop into any app:

puzzle add piece <name>

Preview every piece in the live catalog at magic-spells.github.io/puzzle-pieces.

Syntax Highlighting

Editor extensions provide full .pzl highlighting — native HTML, JavaScript/TypeScript, and CSS per section, plus Puzzle's template expressions, directives, event bindings, and formatter chains:

Install instructions are in each repository's README.

Documentation

AI project memory

This repository uses Constellation MCP as long-term project memory for AI-assisted development. The cards in constellation/ preserve decisions, features, data structures, component and file relationships, flows, and plans so future AI conversations can recover the full project context and build better plans without re-deriving earlier work.

CLI Commands

# Development server with live reload
puzzle dev --port 3000

# Production build (default)
puzzle build

# True static pages (no router, no app.js; per-page mount module)
puzzle build --static

# Prerendered pages plus the SPA bundle the router takes over
puzzle build --hybrid

# Upgrade the installed CLI, or only check what is available
puzzle upgrade
puzzle upgrade --check

# Install the Puzzle agent skill for your coding tools, or refresh it later
puzzle add skills
puzzle upgrade skills

Both commands are built and verified today. puzzle build compiles .pzl files and produces a working bundle; puzzle dev watches app/, rebuilds on change, and delivers full-page live reload over SSE (the reload client is injected into index.html at serve time). Both run the declared style pipeline automatically — styles: { use: ['tailwindcss'] } in puzzle.config.js (tailwindcss-only in v1) — so Tailwind output is included in the served/built styles.css.

On an interactive terminal, build and dev also use a cached, non-blocking daily check to mention newer Puzzle releases. Set PUZZLE_NO_UPDATE_CHECK=1 to disable it; the check is skipped automatically when CI is set.

puzzle upgrade updates a project or global package-manager install; puzzle upgrade --check only reports the current and latest versions.

Agent skill

puzzle add skills installs the Puzzle agent skill — how to write .pzl files, routing, the data layer, static output — into every Claude Code, Codex, and Cursor config directory it finds (~/.claude, ~/.codex, ~/.cursor). On an interactive terminal you pick the targets from a checklist; scripts install to all of them.

The skill is compiled into the CLI binary, so it always matches the version that wrote it. Each install records which version that was, so re-running the command is how you refresh it:

  • An install matching your current CLI is skipped as up to date.
  • An older one asks before it is replaced. Declining leaves it alone but still installs anywhere that has no skill yet.
  • On a non-interactive terminal an older install is refused rather than asked about, and needs --overwrite.
  • A symlinked install (a dev checkout linked into your config dir) is reported and left alone unless you pass --overwrite.

puzzle upgrade skills refreshes only the installs you already have, and puzzle upgrade offers the same refresh automatically after it installs a new version.

The full CLI surface (see constellation/doc/DOC-SPEC.md §13): init, generate, add, doctor, and info join dev and build.

# Scaffold a project; omitting the name prompts only in an interactive terminal
puzzle init my-app --template todos

# Generate a stub (component, view, layout, or model)
puzzle generate component UserCard --path components/ui/

# Wire up Tailwind, install a piece (see Puzzle Pieces above), or run diagnostics
puzzle add tailwind
puzzle add piece <name>
puzzle add skills
puzzle doctor

License

Puzzle is released under the MIT License.


Made by Cory Schulz

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages