Dokumentace systému ƥablon
CatCMS nabĂzĂ modernĂ systĂ©m ĆĄablon s vykreslovĂĄnĂm na stranÄ serveru (server-side rendering), postavenĂœ na TypeScriptu, HTMX, Alpine.js a TailwindCSS.[object Object]
PĆehled
KlĂÄovĂ© vlastnosti
- VykreslovĂĄnĂ na stranÄ serveru (SSR) - RychlĂ© poÄĂĄteÄnĂ naÄtenĂ strĂĄnky s kompletnĂm HTML ze serveru
- TypovÄ bezpeÄnĂ© ĆĄablony - PlnĂĄ podpora TypeScriptu pro datovĂĄ rozhranĂ ĆĄablon
- Interakce ĆĂzenĂ© pomocĂ HTMX - DynamickĂ© aktualizace bez sloĆŸitĂ©ho JavaScriptu
- Alpine.js pro logiku na stranÄ klienta - LehkĂœ JavaScriptovĂœ framework pro interaktivitu
- Design systĂ©m s TailwindCSS - Utility-first CSS s podporou tmavĂ©ho reĆŸimu
- KomponentovĂĄ architektura - Znovu pouĆŸitelnĂ©, sklĂĄdatelnĂ© komponenty ĆĄablon
PouĆŸitĂ© technologie
TypeScript
Type-safe template functions
HTMX 2.0
HTML-driven interactions
Alpine.js 3.x
Reactive client-side logic
TailwindCSS
Utility-first styling
Hono.js
Server framework integration
Template Renderer
Handlebars-like engine
Architektura ĆĄablon
Struktura adresĂĄĆĆŻ
src/templates/
âââ layouts/ # Page layouts
â âââ admin-layout-v2.template.ts # Main admin layout
â âââ admin-layout-catalyst.template.ts # Catalyst-based admin UI
â âââ docs-layout.template.ts # Documentation layout
âââ pages/ # Full page templates
â âââ admin-dashboard.template.ts # Dashboard with stats & charts
â âââ admin-content-list.template.ts # Content listing with filters
â âââ admin-content-edit.template.ts # Content editor
â âââ admin-media-library.template.ts # Media management
â âââ admin-users-list.template.ts # User management
â âââ auth-login.template.ts # Login page
âââ components/ # Reusable components
â âââ form.template.ts # Form builder
â âââ table.template.ts # Data tables with sorting
â âââ media-grid.template.ts # Media file grid/list
â âââ alert.template.ts # Alert notifications
â âââ pagination.template.ts # Pagination controls
â âââ filter-bar.template.ts # Filter controls
â âââ logo.template.ts # Logo component
âââ utils/
âââ template-renderer.ts # Template rendering utility
Vzor funkce ĆĄablony
VĆĄechny ĆĄablony se ĆĂdĂ tĂmto vzorem v TypeScriptu:
// 1. Define the data interface
export interface MyTemplateData {
title: string
items: Array<{ id: string; name: string }>
optional?: string
}
// 2. Create the render function
export function renderMyTemplate(data: MyTemplateData): string {
return ` <div class="my-template">
<h1>${data.title}</h1>
${data.items.map(item =>`
<div>${item.name}</div>
`).join('')}
</div>
`
}
Template Pattern
Struktura ĆĄablony
TĆĂĂșrovĆovĂĄ hierarchie ĆĄablon
Layout (admin-layout-v2.template.ts)
âââ Page (admin-dashboard.template.ts)
âââ Components (table.template.ts, alert.template.ts, etc.)
1. Layouty - DefinujĂ celkovou strukturu strĂĄnky (zĂĄhlavĂ, postrannĂ panel, zĂĄpatĂ)
2. StrĂĄnky - SklĂĄdajĂ komponenty do kompletnĂch strĂĄnek
3. Komponenty - Znovu pouĆŸitelnĂ© prvky uĆŸivatelskĂ©ho rozhranĂ
PrĆŻbÄh vykreslovĂĄnĂ ĆĄablony
VykreslovacĂ engine ĆĄablon poskytuje funkcionalitu podobnou Handlebars:
import { renderTemplate, TemplateRenderer } from '../utils/template-renderer'
// Simple variable interpolation
const html = renderTemplate('Hello {{name}}!', { name: 'World' })
// Output: "Hello World!"
// Nested properties
const html = renderTemplate('{{user.name}}', { user: { name: 'John' } })
// Conditionals
const html = renderTemplate(`
{{#if isActive}}
<div>Active</div>
{{/if}}
`, { isActive: true })
// Loops
const html = renderTemplate(`
{{#each items}}
<li>{{name}}</li>
{{/each}}
`, { items: [{ name: 'Item 1' }, { name: 'Item 2' }] })
// Helper functions
const html = renderTemplate('{{titleCase field}}', { field: 'hello_world' })
// Output: "Hello World"Template Renderer
Vlastnosti vykreslovacĂho enginu
- Interpolace promÄnnĂœch:
{{variable}} - VnoĆenĂ© vlastnosti:
{{user.name}} - Surové HTML:
{{{rawHtml}}} - PodmĂnky:
{{#if condition}}...{{/if}} - SmyÄky:
{{#each array}}...{{/each}} - SpeciĂĄlnĂ promÄnnĂ© ve smyÄce:
{{@index}},{{@first}},{{@last}} - Pomocné funkce:
{{titleCase field}}
Systém layoutƯ
Layout administrace v2 (s Catalyst)
HlavnĂ layout administrace deleguje na Catalyst UI:
export interface AdminLayoutData {
title: string
pageTitle?: string
currentPath?: string
user?: {
name: string
email: string
role: string
}
content: string | HtmlEscapedString
scripts?: string[]
styles?: string[]
version?: string
dynamicMenuItems?: Array<{
label: string
path: string
icon: string
}>
}
export function renderAdminLayout(data: AdminLayoutData): string {
const { renderAdminLayoutCatalyst } = require('./admin-layout-catalyst.template')
return renderAdminLayoutCatalyst(data)
}
Admin Layout Interface
Vlastnosti layoutu
1. ResponzivnĂ navigace v postrannĂm panelu
- NĂĄstÄnka, Obsah, SbĂrky, MĂ©dia, UĆŸivatelĂ©, Pluginy, MezipamÄĆ„, Vzhled, ZĂĄznamy, NastavenĂ
- DynamickĂ© poloĆŸky menu z pluginĆŻ
- ZvĂœraznÄnĂ aktivnĂho stavu
2. HornĂ liĆĄta
- Logo s pĆizpĆŻsobitelnou velikostĂ/variantou
- TlaÄĂtko oznĂĄmenĂ
- PĆizpĆŻsobenĂ motivu pozadĂ
- RozbalovacĂ menu uĆŸivatele
3. PĆizpĆŻsobenĂ pozadĂ
- VĂce pĆechodovĂœch motivĆŻ (HlubokĂœ vesmĂr, KosmickĂĄ modĆ, MatrixovĂĄ zeleĆ, KybernetickĂĄ rĆŻĆŸovĂĄ)
- VlastnĂ PNG pozadĂ (ModrĂ© vlny, HvÄzdy, PĆŻlmÄsĂc, 3D vlny)
- PosuvnĂk pro Ășpravu tmavosti
- UklĂĄdĂĄno do localStorage
4. Podpora tmavĂ©ho reĆŸimu
- TmavĂœ reĆŸim zaloĆŸenĂœ na tĆĂdĂĄch (
dark:prefix) - Detekce systĂ©movĂœch preferencĂ
- Funkce pĆepĂnĂĄnĂ s uklĂĄdĂĄnĂm do localStorage
Knihovna komponent
1. Komponenta formulĂĄĆe
import { renderForm, FormField, FormData } from '../components/form.template'
const formData: FormData = {
id: 'my-form',
hxPost: '/api/submit',
hxTarget: '#form-response',
title: 'User Registration',
description: 'Please fill in your details',
fields: [
{
name: 'email',
label: 'Email Address',
type: 'email',
required: true,
placeholder: 'you@example.com'
},
{
name: 'bio',
label: 'Biography',
type: 'textarea',
rows: 4,
helpText: 'Tell us about yourself'
},
{
name: 'country',
label: 'Country',
type: 'select',
options: [
{ value: 'us', label: 'United States' },
{ value: 'uk', label: 'United Kingdom' }
]
},
{
name: 'newsletter',
label: 'Subscribe to newsletter',
type: 'checkbox',
value: true
}
],
submitButtons: [
{ label: 'Save', type: 'submit', className: 'btn-primary' },
{ label: 'Cancel', type: 'button', className: 'btn-secondary', onclick: 'history.back()' }
]
}
const html = renderForm(formData)Form Builder
PodporovanĂ© typy polĂ: text, email, number, date, textarea, rich_text (s EasyMDE), select, multi_select, checkbox, file
2. Komponenta tabulky
import { renderTable, TableColumn, TableData } from '../components/table.template'
const columns: TableColumn[] = [
{
key: 'name',
label: 'Name',
sortable: true,
sortType: 'string'
},
{
key: 'email',
label: 'Email',
sortable: true
},
{
key: 'created_at',
label: 'Created',
sortable: true,
sortType: 'date',
render: (value) => new Date(value).toLocaleDateString()
},
{
key: 'actions',
label: 'Actions',
render: (value, row) => `
<button hx-get="/users/${row.id}/edit" class="btn-sm btn-primary">
Edit
</button>
<button hx-delete="/users/${row.id}" class="btn-sm btn-danger">
Delete
</button>
` } ]
const tableData: TableData = {
columns,
rows: users,
selectable: true,
rowClickable: true,
rowClickUrl: (row) => `/users/${row.id}`,
emptyMessage: 'No users found'
}
const html = renderTable(tableData)
Data Table
Vlastnosti tabulky: ĆazenĂ sloupcĆŻ (ĆetÄzec, ÄĂslo, datum, boolean), vĂœbÄr ĆĂĄdkĆŻ s âvybrat vĆĄeâ, klikatelnĂ© ĆĂĄdky, prĂĄzdnĂœ stav, vlastnĂ vykreslovĂĄnĂ bunÄk
3. Komponenta upozornÄnĂ
import { renderAlert } from '../components/alert.template'
// Success alert
const success = renderAlert({
type: 'success',
title: 'Success!',
message: 'Your changes have been saved.',
dismissible: true
})
// Error alert
const error = renderAlert({
type: 'error',
message: 'An error occurred. Please try again.'
})
// Warning and info
const warning = renderAlert({ type: 'warning', message: 'This action cannot be undone.' })
const info = renderAlert({ type: 'info', message: 'You have 3 pending notifications.' })Alert Notifications
Typy upozornÄnĂ: success, error, warning, info
4. Komponenta strĂĄnkovĂĄnĂ
import { renderPagination } from '../components/pagination.template'
const pagination = renderPagination({
currentPage: 2,
totalPages: 10,
totalItems: 195,
itemsPerPage: 20,
startItem: 21,
endItem: 40,
baseUrl: '/admin/content',
queryParams: { status: 'published', model: 'blog' },
showPageNumbers: true,
maxPageNumbers: 5,
showPageSizeSelector: true,
pageSizeOptions: [10, 20, 50, 100]
})
Pagination
Vlastnosti strĂĄnkovĂĄnĂ: Navigace podle ÄĂsel strĂĄnek, tlaÄĂtka pĆedchozĂ/nĂĄsledujĂcĂ, vĂœbÄr velikosti strĂĄnky, zachovĂĄnĂ parametrĆŻ dotazu, responzivnĂ pro mobilnĂ zaĆĂzenĂ
Integrace HTMX
HTMX umoĆŸĆuje dynamickĂ© interakce ĆĂzenĂ© serverem bez nutnosti psĂĄt JavaScript.
ZĂĄkladnĂ vzory HTMX
<!-- Refresh stats every 30 seconds -->
<div
id="stats-container"
hx-get="/admin/api/stats"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
>
<div>Loading...</div>
</div>Auto-Refresh with Polling
<form
hx-post="/admin/content/create"
hx-target="#form-response"
hx-swap="innerHTML"
hx-indicator="#loading-spinner"
>
<input type="text" name="title" required />
<button type="submit">
Submit
<div id="loading-spinner" class="htmx-indicator">Saving...</div>
</button>
</form>
<div id="form-response"></div>Form Submission
<button
hx-delete="/admin/content/${id}"
hx-confirm="Are you sure you want to delete this?"
hx-target="closest .content-item"
hx-swap="outerHTML"
>
Delete
</button>Delete with Confirmation
PĆehled atributĆŻ HTMX
| Atribut | ĂÄel | PĆĂklad |
|---|---|---|
hx-get | GET poĆŸadavek | hx-get="/api/data" |
hx-post | POST poĆŸadavek | hx-post="/api/create" |
hx-put | PUT poĆŸadavek | hx-put="/api/update/123" |
hx-delete | DELETE poĆŸadavek | hx-delete="/api/delete/123" |
hx-target | Kam vloĆŸit odpovÄÄ | hx-target="#result" |
hx-swap | Jak vloĆŸit | innerHTML, outerHTML, beforeend |
hx-trigger | Kdy spustit | click, load, change, every 30s |
hx-indicator | IndikĂĄtor naÄĂtĂĄnĂ | hx-indicator="#spinner" |
hx-confirm | PotvrzovacĂ dialog | hx-confirm="Are you sure?" |
Integrace Alpine.js
Alpine.js poskytuje reaktivnĂ interakce na stranÄ klienta.
PĆĂklady Alpine.js
<div x-data="{ open: false }">
<button @click="open = !open">Toggle Menu</button>
<div x-show="open" @click.away="open = false">
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
</div>
</div>Dropdown Menu
<div x-data="{ activeTab: 'overview' }">
<div class="tabs">
<button
@click="activeTab = 'overview'"
:class="{ 'active': activeTab === 'overview' }"
>
Overview
</button>
<button
@click="activeTab = 'details'"
:class="{ 'active': activeTab === 'details' }"
>
Details
</button>
</div>
<div x-show="activeTab === 'overview'">Overview content</div>
<div x-show="activeTab === 'details'">Details content</div>
</div>Tabs
TailwindCSS & Design systém
Konfigurace TailwindCSS
CatCMS pouĆŸĂvĂĄ Tailwind pĆes CDN s vlastnĂ konfiguracĂ:
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#465FFF',
secondary: '#212A3E',
dark: '#1C1C24',
success: '#10B981',
warning: '#F59E0B',
error: '#EF4444',
info: '#3B82F6',
},
fontFamily: {
satoshi: ['Satoshi', 'sans-serif'],
},
},
},
}Tailwind Config
BÄĆŸnĂ© vzory utilit
<input
type="text"
class="w-full rounded-lg bg-white px-3 py-2 text-sm text-zinc-950 shadow-sm ring-1 ring-zinc-950/10 transition-shadow ring-inset placeholder:text-zinc-400 focus:ring-2 focus:ring-zinc-950 focus:outline-none dark:bg-zinc-800 dark:text-white dark:ring-white/10 dark:focus:ring-white"
/>Form Inputs
<!-- Primary Button -->
<button
class="inline-flex items-center justify-center rounded-lg bg-zinc-950 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-zinc-800 dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-100"
>
Save
</button>
<!-- Secondary Button -->
<button
class="inline-flex items-center justify-center rounded-lg bg-white px-3.5 py-2.5 text-sm font-semibold text-zinc-950 shadow-sm ring-1 ring-zinc-950/10 transition-colors ring-inset hover:bg-zinc-50 dark:bg-zinc-800 dark:text-white dark:ring-white/10 dark:hover:bg-zinc-700"
>
Cancel
</button>Buttons
Implementace tmavĂ©ho reĆŸimu
PĆepĂnaÄ tmavĂ©ho reĆŸimu
TmavĂœ reĆŸim pouĆŸĂvĂĄ class strategii Tailwindu:
// Dark mode toggle (in layout)
function toggleDarkMode() {
document.documentElement.classList.toggle('dark')
localStorage.setItem(
'darkMode',
document.documentElement.classList.contains('dark'),
)
}
// Initialize from localStorage
if (
localStorage.getItem('darkMode') === 'true' ||
(!('darkMode' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark')
}
Dark Mode Toggle
Utility pro tmavĂœ reĆŸim
KaĆŸdĂĄ komponenta pouĆŸĂvĂĄ varianty pro tmavĂœ reĆŸim:
<!-- Text colors -->
<p class="text-zinc-950 dark:text-white">Primary text</p>
<p class="text-zinc-500 dark:text-zinc-400">Secondary text</p>
<!-- Backgrounds -->
<div class="bg-white dark:bg-zinc-900">Content</div>
<div class="bg-zinc-50 dark:bg-zinc-800">Subtle background</div>
<!-- Borders -->
<div class="border border-zinc-950/10 dark:border-white/10">Bordered</div>
<!-- Rings (focus states) -->
<input
class="ring-1 ring-zinc-950/10 focus:ring-zinc-950 dark:ring-white/10 dark:focus:ring-white"
/>Dark Mode Variants
VytvĂĄĆenĂ vlastnĂch ĆĄablon
PrĆŻvodce krok za krokem
// src/templates/pages/my-custom-page.template.ts
export interface MyCustomPageData {
title: string
items: Array<{
id: string
name: string
description?: string
}>
user?: {
name: string
email: string
role: string
}
version?: string
}
Step 1: Define Data Interface
// src/templates/components/my-component.template.ts
export interface MyComponentData {
title: string
items: string[]
}
export function renderMyComponent(data: MyComponentData): string {
return `
<div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-6">
<h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">
${data.title}
</h3>
<ul class="space-y-2">
${data.items.map(item => `
<li class="text-sm text-zinc-500 dark:text-zinc-400">${item}</li>
`).join('')}
</ul>
</div>
`
}Step 2: Create Component
import { renderAdminLayout, AdminLayoutData } from '../layouts/admin-layout-v2.template'
import { renderMyComponent } from '../components/my-component.template'
import { renderAlert } from '../components/alert.template'
export function renderMyCustomPage(data: MyCustomPageData): string {
const pageContent = `
<div>
<!-- Page Header -->
<div class="mb-8">
<h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">
${data.title}
</h1>
<p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
Custom page description
</p>
</div>
<!-- Success Message -->
${renderAlert({
type: 'success',
message: 'Page loaded successfully!',
dismissible: true
})}
<!-- Custom Content -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
${data.items.map(item => `
<div class="rounded-xl bg-white dark:bg-zinc-900 shadow-sm ring-1 ring-zinc-950/5 dark:ring-white/10 p-6">
<h3 class="text-base font-semibold text-zinc-950 dark:text-white mb-2">
${item.name}
</h3>
${item.description ? `
<p class="text-sm text-zinc-500 dark:text-zinc-400">
${item.description}
</p>
` : ''}
<!-- HTMX Action Button -->
<button
hx-get="/api/item/${item.id}"
hx-target="#item-details"
hx-swap="innerHTML"
class="mt-4 inline-flex items-center rounded-lg bg-zinc-950 dark:bg-white px-3 py-2 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors"
>
View Details
</button>
</div>
`).join('')}
</div>
<!-- HTMX Target for Dynamic Content -->
<div id="item-details" class="mt-6"></div>
<!-- Component Usage -->
${renderMyComponent({
title: 'My Component',
items: ['Item 1', 'Item 2', 'Item 3']
})}
</div>
`
// Wrap in layout
const layoutData: AdminLayoutData = {
title: data.title,
pageTitle: data.title,
currentPath: '/admin/my-page',
user: data.user,
version: data.version,
content: pageContent
}
return renderAdminLayout(layoutData)
}
Step 3: Create Page Template
// src/routes/admin.routes.ts
import { renderMyCustomPage } from '../templates/pages/my-custom-page.template'
app.get('/admin/my-page', async (c) => {
const user = c.get('user')
// Fetch your data
const items = await db.query('SELECT * FROM items')
const pageData = {
title: 'My Custom Page',
items,
user,
version: '1.0.0'
}
return c.html(renderMyCustomPage(pageData))
})Step 4: Create Route Handler
Template Best Practices
- VĆŸdy definujte TypeScript rozhranĂ pro data ĆĄablon - RozdÄlte sloĆŸitĂ© UI na znovu pouĆŸitelnĂ© komponenty - ZahrĆte ARIA atributy a sĂ©mantickĂ© HTML pro pĆĂstupnost - VĆŸdy oĆĄetĆujte uĆŸivatelskĂœ vstup, abyste pĆedeĆĄli XSS - PouĆŸĂvejte HTMX pro ÄĂĄsteÄnĂ© aktualizace mĂsto plnĂ©ho znovunaÄtenĂ strĂĄnky - PoskytnÄte zĂĄloĆŸnĂ UI a chybovĂ© stavy - VĆŸdy poskytujte naÄĂtacĂ kostry