zen-temple Svelte Themes

Zen Temple

Zen-Temple: A zero-build, zero-magic component system for backend craftsmen to build Svelte-like UI using pure Vanilla JS classes wrapped in Jinja macros.

zen-temple

zen-temple is a zero-build, zero-magic frontend component system for backend developers. Built on Jinja2, HTMX, and Alpine.js โ€” fully transparent, fully controllable.

๐ŸŽฏ Philosophy

zen-temple follows the Zero Template philosophy:

  • No build step required - Edit templates and see changes immediately
  • No hidden abstractions - What you write is what runs
  • Template-centered design - Templates are the source of truth
  • Jinja Macros for components - Encapsulate HTML + JavaScript together
  • Class-based state - Use new ComponentState(), never inline objects
  • Explicit data flow - HTMX fetches JSON, passes to Alpine methods
  • Computed properties via getters - Use JavaScript get accessors
  • Loose coupling - Components are independent and self-contained

๐Ÿ›๏ธ Zero-Legacy Architecture

NEW: zen-temple implements the "Logic is Pure, Bridge is Minimal" design principle:

  • Pure Logic Layer: Business logic in vanilla Python classes (no framework dependencies)
  • Minimal Bridge: Jinja macros connect logic to templates
  • Zero Legacy: Clean separation ensures easy testing, reusability, and migration
from zen_temple import PureLogic, TemplateManager

# 1. Pure Logic (vanilla Python)
class CounterLogic(PureLogic):
    def __init__(self, initial_value=0):
        self._value = initial_value
    
    def increment(self):
        self._value += 1
    
    def to_context(self):
        return {"count": self._value}

# 2. Minimal Bridge (automatic via TemplateManager)
counter = CounterLogic(initial_value=0)
manager = TemplateManager()
html = manager.render_component("counter", logic=counter)

See ZERO_LEGACY_ARCHITECTURE.md for details.

Inspired by Svelte's reactive design philosophy, but without the build step.

๐Ÿš€ Quick Start

Installation

# Using uv (recommended)
uv pip install zen-temple

# Or using pip
pip install zen-temple

Create Your First Project

# Create a new project with examples
zen-temple new my-app

# Create with a Flask development server
zen-temple new my-app --with-server

# Create without examples
zen-temple new my-app --no-examples

Project Structure

my-app/
โ”œโ”€โ”€ templates/
โ”‚   โ”œโ”€โ”€ layouts/
โ”‚   โ”‚   โ””โ”€โ”€ base.html          # Base layout with CDN imports
โ”‚   โ”œโ”€โ”€ components/
โ”‚   โ”‚   โ”œโ”€โ”€ counter.html       # Example: Alpine.js reactivity
โ”‚   โ”‚   โ”œโ”€โ”€ todo.html          # Example: State management
โ”‚   โ”‚   โ””โ”€โ”€ data_fetch.html    # Example: HTMX communication
โ”‚   โ””โ”€โ”€ index.html             # Main page
โ”œโ”€โ”€ static/
โ”‚   โ”œโ”€โ”€ css/                   # Custom styles (optional)
โ”‚   โ””โ”€โ”€ js/                    # Custom Alpine.js stores (optional)
โ”œโ”€โ”€ app/                       # Flask server (if --with-server)
โ”‚   โ””โ”€โ”€ main.py
โ””โ”€โ”€ zen-temple.yaml            # Project configuration

๐Ÿ“š Core Concepts

Components as Jinja Macros

Components are defined as Jinja macros with encapsulated JavaScript classes:

<!-- templates/components/counter.html -->
{%- macro counter(initial_count=0) -%}
<div 
    x-data="new CounterState({{ initial_count }})"
    class="bg-white rounded-lg shadow p-6"
>
    <h3 class="text-xl font-semibold mb-4">Counter</h3>
    <button @click="increment()">+</button>
    <span x-text="count"></span>
    <button @click="decrement()">-</button>
</div>

<script>
// CounterState - Encapsulated component logic
class CounterState {
    constructor(initialCount = 0) {
        this.count = initialCount;
    }
    
    increment() {
        this.count++;
    }
    
    decrement() {
        this.count--;
    }
    
    // Computed property using getter
    get double() {
        return this.count * 2;
    }
}
</script>
{%- endmacro -%}

Use components by importing and calling the macro:

{% extends "layouts/base.html" %}
{% from "components/counter.html" import counter %}

{% block content %}
    {{ counter(initial_count=5) }}
{% endblock %}

Class-Based State Management

All state must be managed through JavaScript classes instantiated with new:

  • โœ… Do: x-data="new ComponentState()"
  • โŒ Don't: x-data="{ count: 0 }" (inline objects)
  • โŒ Don't: Alpine.store() or Alpine.data() (global state)

HTMX with Explicit Data Flow

HTMX fetches JSON data, which is then explicitly passed to Alpine.js methods:

<div 
    x-data="new DataState()"
    @htmx:after-request="sync($event.detail.xhr.response)"
>
    <button 
        hx-get="/api/data"
        hx-swap="none"
        class="bg-blue-500 text-white px-4 py-2 rounded"
    >
        Load Data
    </button>
    <ul>
        <template x-for="item in items" :key="item.id">
            <li x-text="item.name"></li>
        </template>
    </ul>
</div>

<script>
class DataState {
    constructor() {
        this.items = [];
    }
    
    // Explicit data sync method called by HTMX
    sync(jsonData) {
        const data = JSON.parse(jsonData);
        this.items = data.items || [];
    }
}
</script>

Server returns JSON (not HTML):

@app.route('/api/data')
def get_data():
    return jsonify({'items': [...]})

Computed Properties

Use JavaScript getters for computed values:

class TodoState {
    constructor() {
        this.todos = [];
    }
    
    // Computed property - automatically reactive in Alpine
    get completedCount() {
        return this.todos.filter(t => t.completed).length;
    }
}

Access in template: <span x-text="completedCount"></span>

๐Ÿ› ๏ธ CLI Commands

Create a New Project

zen-temple new <project-name> [OPTIONS]

Options:
  --path TEXT          Parent directory (default: current directory)
  --no-examples        Skip example components
  --with-server        Include Flask development server

Generate Components

zen-temple component <component-name> [OPTIONS]

Options:
  --type [basic|form|list|card]  Component type (default: basic)
  --output TEXT                  Output directory

Component types:

  • basic - Simple component with Alpine.js state
  • form - Form component with validation
  • list - List component with data loading
  • card - Card/widget component

Initialize Configuration

zen-temple init [OPTIONS]

Options:
  --project-name TEXT   Name of your project (prompted if not provided)
  --template-dir TEXT   Templates directory (default: templates)

Validate Components

zen-temple validate <component-path>

Checks for:

  • Inline JavaScript (should use Alpine.js instead)
  • Inline event handlers (should use Alpine.js directives)
  • Proper HTMX usage
  • Template structure

List Components

zen-temple list-components [OPTIONS]

Options:
  --template-dir TEXT  Templates directory (default: templates)

Show Philosophy

zen-temple philosophy

Displays the zen-temple design philosophy and principles.

๐Ÿงช Python API

Template Manager

from zen_temple import TemplateManager
from pathlib import Path

# Initialize with template directories
manager = TemplateManager(template_dirs=[Path("templates")])

# Render a component
html = manager.render_component("counter", count=0)

# Render from string
html = manager.render_string("<div>{{ message }}</div>", {"message": "Hello"})

# List available components
components = manager.list_components()

# Check if component exists
if manager.component_exists("my-component"):
    html = manager.render_component("my-component")

Pure Logic Layer (Zero-Legacy Architecture)

from zen_temple import PureLogic, TemplateManager
from typing import Dict, Any

# Define pure business logic (no framework dependencies)
class TodoListLogic(PureLogic):
    def __init__(self):
        self._todos = []
    
    def add_todo(self, text: str):
        self._todos.append({"id": len(self._todos), "text": text, "done": False})
    
    def to_context(self) -> Dict[str, Any]:
        """Minimal bridge to templates"""
        return {"todos": self._todos}

# Use logic with templates
todos = TodoListLogic()
todos.add_todo("Learn zen-temple")
todos.add_todo("Build an app")

manager = TemplateManager()
html = manager.render_component("todo_list", logic=todos)

Component Validator

from zen_temple import ComponentValidator
from pathlib import Path

validator = ComponentValidator()

# Validate a component file
result = validator.validate_component(Path("templates/components/my-component.html"))

if result.is_valid:
    print("โœ“ Component is valid")
else:
    for error in result.errors:
        print(f"โœ— {error}")
    for warning in result.warnings:
        print(f"โš  {warning}")

# Validate component content
result = validator.validate_string("<div x-data='{ count: 0 }'></div>", "inline")

Scaffold Generator

from zen_temple import ScaffoldGenerator
from pathlib import Path

generator = ScaffoldGenerator(project_root=Path.cwd())

# Generate a complete project
created_paths = generator.generate_project(
    project_name="my-app",
    include_examples=True,
    include_server=False
)

# Generate a single component
component_path = generator.generate_component(
    component_name="my-widget",
    component_type="basic",
    output_dir=Path("templates/components")
)

๐Ÿ“ฆ Technology Stack

  • HTMX - AJAX, WebSockets, and Server-Sent Events
  • Alpine.js - Reactive and declarative JavaScript
  • Jinja2 - Template engine
  • Tailwind CSS - Utility-first CSS (via CDN)

All dependencies are loaded from CDN - no build step required!

๐ŸŽ“ Examples

Counter Component with Macro

A reactive counter using Jinja macro and class-based state:

<!-- templates/components/counter.html -->
{%- macro counter(initial_count=0) -%}
<div x-data="new CounterState({{ initial_count }})">
    <button @click="decrement()">-</button>
    <span x-text="count"></span>
    <button @click="increment()">+</button>
    <p>Double: <span x-text="double"></span></p>
</div>

<script>
class CounterState {
    constructor(initialCount = 0) {
        this.count = initialCount;
    }
    
    increment() { this.count++; }
    decrement() { this.count--; }
    
    get double() { return this.count * 2; }
}
</script>
{%- endmacro -%}

Usage:

{% from "components/counter.html" import counter %}
{{ counter(initial_count=5) }}

Todo List with Class State

A todo list with class-based state management:

{%- macro todo() -%}
<div x-data="new TodoState()">
    <input x-model="newTodo" @keyup.enter="addTodo()" />
    <button @click="addTodo()">Add</button>
    
    <ul>
        <template x-for="todo in todos" :key="todo.id">
            <li>
                <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo.id)" />
                <span :class="{ 'line-through': todo.completed }" x-text="todo.text"></span>
                <button @click="removeTodo(todo.id)">Delete</button>
            </li>
        </template>
    </ul>
    
    <p>Completed: <span x-text="completedCount"></span></p>
</div>

<script>
class TodoState {
    constructor() {
        this.todos = [];
        this.newTodo = '';
    }
    
    addTodo() {
        if (this.newTodo.trim()) {
            this.todos.push({
                id: Date.now(),
                text: this.newTodo,
                completed: false
            });
            this.newTodo = '';
        }
    }
    
    toggleTodo(id) {
        const todo = this.todos.find(t => t.id === id);
        if (todo) todo.completed = !todo.completed;
    }
    
    removeTodo(id) {
        this.todos = this.todos.filter(t => t.id !== id);
    }
    
    get completedCount() {
        return this.todos.filter(t => t.completed).length;
    }
}
</script>
{%- endmacro -%}

HTMX Data Fetching with Explicit Flow

Fetch JSON data and sync to Alpine class:

{%- macro data_fetch() -%}
<div 
    x-data="new DataState()"
    @htmx:after-request="sync($event.detail.xhr.response)"
>
    <button hx-get="/api/data" hx-swap="none">
        Load Data
    </button>
    
    <ul>
        <template x-for="item in items" :key="item.id">
            <li x-text="item.name"></li>
        </template>
    </ul>
</div>

<script>
class DataState {
    constructor() {
        this.items = [];
    }
    
    sync(jsonData) {
        const data = JSON.parse(jsonData);
        this.items = data.items || [];
    }
}
</script>
{%- endmacro -%}

Server endpoint:

@app.route('/api/data')
def get_data():
    return jsonify({'items': [{'id': 1, 'name': 'Item 1'}]})

## ๐Ÿง‘โ€๐Ÿ’ป Development

### Setting Up Development Environment

```bash
# Clone the repository
git clone https://github.com/mame7743/zen-temple.git
cd zen-temple

# Install with uv (recommended)
uv pip install -e ".[dev]"

# Or with pip
pip install -e ".[dev]"

Running Tests

pytest

Running with Coverage

pytest --cov=zen_temple --cov-report=html

Linting and Formatting

# Run ruff
ruff check src/

# Format code
ruff format src/

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Acknowledgments

  • Inspired by Svelte's reactive design philosophy
  • Built on the shoulders of HTMX, Alpine.js, and Jinja2
  • Special thanks to the Python web development community

๐Ÿ“ž Support


zen-temple: Zero Template, Zero Build, Zero Magic โœจ

Top categories

Loading Svelte Themes