.claude/skills/fosmvvm-leaf-view-generator/SKILL.md
Generate Leaf templates for FOSMVVM WebApps. Create full-page views and HTML-over-the-wire fragments that render ViewModels.
npx skillsauth add foscomputerservices/FOSUtilities fosmvvm-leaf-view-generatorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Generate Leaf templates that render ViewModels for web clients.
Architecture context: See FOSMVVMArchitecture.md | OpenClaw reference
In FOSMVVM, Leaf templates are the View in M-V-VM for web clients:
Model → ViewModel → Leaf Template → HTML
↑ ↑
(localized) (renders it)
Key principle: The ViewModel is already localized when it reaches the template. The template just renders what it receives.
The Leaf filename should match the ViewModel it renders.
Sources/
{ViewModelsTarget}/
ViewModels/
{Feature}ViewModel.swift ←──┐
{Entity}CardViewModel.swift ←──┼── Same names
│
{WebAppTarget}/ │
Resources/Views/ │
{Feature}/ │
{Feature}View.leaf ────┤ (renders {Feature}ViewModel)
{Entity}CardView.leaf ────┘ (renders {Entity}CardViewModel)
This alignment provides:
Render a complete page with layout, navigation, CSS/JS includes.
{Feature}View.leaf
├── Extends base layout
├── Includes <html>, <head>, <body>
├── Renders {Feature}ViewModel
└── May embed fragment templates for components
Use for: Initial page loads, navigation destinations.
Render a single component - no layout, no page structure.
{Entity}CardView.leaf
├── NO layout extension
├── Single root element
├── Renders {Entity}CardViewModel
├── Has data-* attributes for state
└── Returned to JS for DOM swapping
Use for: Partial updates, HTML-over-the-wire responses.
For dynamic updates without full page reloads:
JS Event → WebApp Route → ServerRequest.processRequest() → Controller
↓
ViewModel
↓
HTML ← JS DOM swap ← WebApp returns ← Leaf renders ←────────┘
The WebApp route:
app.post("move-{entity}") { req async throws -> Response in
let body = try req.content.decode(Move{Entity}Request.RequestBody.self)
let serverRequest = Move{Entity}Request(requestBody: body)
guard let response = try await serverRequest.processRequest(baseURL: app.serverBaseURL) else {
throw Abort(.internalServerError)
}
// Render fragment template with ViewModel
return try await req.view.render(
"{Feature}/{Entity}CardView",
["card": response.viewModel]
).encodeResponse(for: req)
}
JS receives HTML and swaps it into the DOM - no JSON parsing, no client-side rendering.
Fragments must embed all state that JS needs for future actions:
<div class="{entity}-card"
data-{entity}-id="#(card.id)"
data-status="#(card.status)"
data-category="#(card.category)"
draggable="true">
Rules:
data-{entity}-id for the primary identifierdata-{field} for state values (kebab-case)const request = {
{entity}Id: element.dataset.{entity}Id,
newStatus: targetColumn.dataset.status
};
FOSMVVM's LeafDataRepresentable conformance handles Localizable types automatically.
In templates, just use the property:
<span class="date">#(card.createdAt)</span>
<!-- Renders: "Dec 27, 2025" (localized) -->
If Localizable types render incorrectly (showing [ds: "2", ls: "...", v: "..."]):
Localizable+Leaf.swift exists with conformancesswift package clean && swift buildViewModels should provide both raw values (for data attributes) and localized strings (for display). For enum localization, see the Enum Localization Pattern.
@ViewModel
public struct {Entity}CardViewModel {
public let id: ModelIdType // For data-{entity}-id
public let status: {Entity}Status // Raw enum for data-status
public let statusDisplay: LocalizableString // Localized (stored, not @LocalizedString)
}
<div data-status="#(card.status)"> <!-- Raw: "queued" for JS -->
<span class="badge">#(card.statusDisplay)</span> <!-- Localized: "In Queue" -->
</div>
Fragments are minimal - just the component:
<!-- {Entity}CardView.leaf -->
<div class="{entity}-card"
data-{entity}-id="#(card.id)"
data-status="#(card.status)">
<div class="card-content">
<p class="text">#(card.contentPreview)</p>
</div>
<div class="card-footer">
<span class="creator">#(card.creatorName)</span>
<span class="date">#(card.createdAt)</span>
</div>
</div>
Rules:
#extend("base") - fragments don't use layoutsFull pages extend a base layout:
<!-- {Feature}View.leaf -->
#extend("base"):
#export("content"):
<div class="{feature}-container">
<header class="{feature}-header">
<h1>#(viewModel.title)</h1>
</header>
<main class="{feature}-content">
#for(card in viewModel.cards):
#extend("{Feature}/{Entity}CardView")
#endfor
</main>
</div>
#endexport
#endextend
#if(card.isHighPriority):
<span class="priority-badge">#(card.priorityLabel)</span>
#endif
#if(card.assignee):
<div class="assignee">
<span class="name">#(card.assignee.name)</span>
</div>
#else:
<div class="unassigned">#(card.unassignedLabel)</div>
#endif
<div class="column" data-status="#(column.status)">
<div class="column-header">
<h3>#(column.displayName)</h3>
<span class="count">#(column.count)</span>
</div>
<div class="column-cards">
#for(card in column.cards):
#extend("{Feature}/{Entity}CardView")
#endfor
#if(column.cards.count == 0):
<div class="empty-state">#(column.emptyMessage)</div>
#endif
</div>
</div>
Sources/{WebAppTarget}/Resources/Views/
├── base.leaf # Base layout (all pages extend this)
├── {Feature}/
│ ├── {Feature}View.leaf # Full page → {Feature}ViewModel
│ ├── {Entity}CardView.leaf # Fragment → {Entity}CardViewModel
│ ├── {Entity}RowView.leaf # Fragment → {Entity}RowViewModel
│ └── {Modal}View.leaf # Fragment → {Modal}ViewModel
└── Shared/
├── HeaderView.leaf # Shared components
└── FooterView.leaf
Leaf provides useful functions for working with arrays:
<!-- Count items -->
#if(count(cards) > 0):
<p>You have #count(cards) cards</p>
#endif
<!-- Check if array contains value -->
#if(contains(statuses, "active")):
<span class="badge">Active</span>
#endif
Inside #for loops, Leaf provides progress variables:
#for(item in items):
#if(isFirst):<span class="first">#endif
#(item.name)
#if(!isLast):, #endif
#endfor
| Variable | Description |
|----------|-------------|
| isFirst | True on first iteration |
| isLast | True on last iteration |
| index | Current iteration (0-based) |
Direct array subscripts (array[0]) are not documented in Leaf. For accessing specific elements, pre-compute in the ViewModel:
public let firstCard: CardViewModel?
public init(cards: [CardViewModel]) {
self.cards = cards
self.firstCard = cards.first
}
Swift's synthesized Codable only encodes stored properties. Since ViewModels are passed to Leaf via Codable encoding, computed properties won't be available.
// Computed property - NOT encoded by Codable, invisible in Leaf
public var hasCards: Bool { !cards.isEmpty }
// Stored property - encoded by Codable, available in Leaf
public let hasCards: Bool
If you need a derived value in a Leaf template, calculate it in init() and store it:
public let hasCards: Bool
public let cardCount: Int
public init(cards: [CardViewModel]) {
self.cards = cards
self.hasCards = !cards.isEmpty
self.cardCount = cards.count
}
IMPORTANT: Even though Leaf templates don't use vmId directly, the ViewModels being rendered must initialize vmId correctly for SwiftUI clients.
❌ WRONG - Never use this:
public var vmId: ViewModelId = .init() // NO! Generic identity
✅ MINIMUM - Use type-based identity:
public var vmId: ViewModelId = .init(type: Self.self)
✅ IDEAL - Use data-based identity when available:
public struct TaskCardViewModel {
public let id: ModelIdType
public var vmId: ViewModelId
public init(id: ModelIdType, /* other params */) {
self.id = id
self.vmId = .init(id: id) // Ties view identity to data identity
// ...
}
}
Why this matters for Leaf ViewModels:
.id(vmId) to determine when to recreate vs update views.init(id:)) is best practice<!-- BAD - JS can't identify this element -->
<div class="{entity}-card">
<!-- GOOD - JS reads data-{entity}-id -->
<div class="{entity}-card" data-{entity}-id="#(card.id)">
<!-- BAD - localized string can't be sent to server -->
<div data-status="#(card.statusDisplayName)">
<!-- GOOD - raw enum value works for requests -->
<div data-status="#(card.status)">
<!-- BAD - fragment should not extend layout -->
#extend("base"):
#export("content"):
<div class="card">...</div>
#endexport
#endextend
<!-- GOOD - fragment is just the component -->
<div class="card">...</div>
<!-- BAD - not localizable -->
<span class="status">Queued</span>
<!-- GOOD - ViewModel provides localized value -->
<span class="status">#(card.statusDisplayName)</span>
<!-- BAD - breaks RTL languages and locale-specific word order -->
#(conversation.messageCount) #(conversation.messagesLabel)
<!-- GOOD - ViewModel composes via @LocalizedSubs -->
#(conversation.messageCountDisplay)
Template-level concatenation assumes left-to-right order. Use @LocalizedSubs in the ViewModel so YAML can define locale-appropriate ordering:
en:
ConversationViewModel:
messageCountDisplay: "%{messageCount} %{messagesLabel}"
ar:
ConversationViewModel:
messageCountDisplay: "%{messagesLabel} %{messageCount}"
<!-- BAD - hardcoded format, not locale-aware, concatenation issue -->
<span>#(content.createdPrefix) #date(content.createdAt, "MMM d, yyyy")</span>
<!-- GOOD - LocalizableDate handles locale formatting, @LocalizedSubs composes -->
<span>#(content.createdDisplay)</span>
Use LocalizableDate in the ViewModel - it formats according to user locale. If combining with a prefix, use @LocalizedSubs:
public let createdAt: LocalizableDate
@LocalizedSubs(\.createdPrefix, \.createdAt)
public var createdDisplay
<!-- BAD - filename doesn't match ViewModel -->
ViewModel: UserProfileCardViewModel
Template: ProfileCard.leaf
<!-- GOOD - aligned names -->
ViewModel: UserProfileCardViewModel
Template: UserProfileCardView.leaf
// ❌ BAD - Generic identity (breaks SwiftUI clients)
public var vmId: ViewModelId = .init()
// ✅ MINIMUM - Type-based identity
public var vmId: ViewModelId = .init(type: Self.self)
// ✅ IDEAL - Data-based identity (when id available)
public init(id: ModelIdType) {
self.id = id
self.vmId = .init(id: id)
}
ViewModels rendered by Leaf are often shared with SwiftUI clients. Correct vmId initialization is critical for SwiftUI's view identity system.
When a WebApp route catches an error, the error type is known at compile time. You don't need generic "ErrorViewModel" patterns:
// WebApp route - you KNOW the request type, so you KNOW the error type
app.post("move-idea") { req async throws -> Response in
let body = try req.content.decode(MoveIdeaRequest.RequestBody.self)
let serverRequest = MoveIdeaRequest(requestBody: body)
do {
try await serverRequest.processRequest(mvvmEnv: req.application.mvvmEnv)
// success path...
} catch let error as MoveIdeaRequest.ResponseError {
// I KNOW this is MoveIdeaRequest.ResponseError
// I KNOW it has .code and .message
return try await req.view.render(
"Shared/ToastView",
["message": error.message.value, "type": "error"]
).encodeResponse(for: req)
}
}
The anti-pattern (JavaScript brain):
// ❌ WRONG - treating errors as opaque
catch let error as ServerRequestError {
// "How do I extract the message? The protocol doesn't guarantee it!"
// This is wrong thinking. You catch the CONCRETE type.
}
Each route handles its own specific error type. There's no mystery about what properties are available.
Invocation: /fosmvvm-leaf-view-generator
Prerequisites:
Workflow integration: This skill is used when creating Leaf templates for web clients. The skill references conversation context automatically—no file paths or Q&A needed. Typically follows fosmvvm-viewmodel-generator.
This skill references conversation context to determine template structure:
From conversation context, the skill identifies:
From ViewModel purpose:
For each ViewModel property:
id: ModelIdType → data-{entity}-id="#(vm.id)" (for JS)data-{field}="#(vm.field)" (for state)LocalizableString → #(vm.displayName) (display text)LocalizableDate → #(vm.createdAt) (formatted date)Based on JS interaction needs:
Full-page:
Fragment:
Skill references information from:
| Version | Date | Changes | |---------|------|---------| | 1.0 | 2025-12-24 | Initial Kairos-specific skill | | 2.0 | 2025-12-27 | Generalized for FOSMVVM, added View-ViewModel alignment principle, full-page templates, architecture connection | | 2.1 | 2026-01-08 | Added Leaf Built-in Functions section (count, contains, loop variables). Clarified Codable/computed properties. Corrected earlier false claims about #count() not working. | | 2.2 | 2026-01-19 | Updated Pattern 3 to use stored LocalizableString for dynamic enum displays; linked to Enum Localization Pattern. Added anti-patterns for concatenating localized values and formatting dates in templates. | | 2.3 | 2026-01-20 | Added "Rendering Errors in Leaf Templates" section - error types are known at compile time, no need for generic ErrorViewModel patterns. Prevents JavaScript-brain thinking about runtime type discovery. | | 2.4 | 2026-01-24 | Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths. |
development
Generate new Claude Code skills following the context-aware pattern. Scaffolds SKILL.md, reference docs, and frontmatter.
testing
Generate ViewModel tests with codable round-trip, versioning stability, and multi-locale translation verification.
data-ai
Generate FOSMVVM ViewModels for SwiftUI screens, pages, and components. Scaffolds RequestableViewModel, localization bindings, and stub factories.
testing
Generate UI tests for FOSMVVM SwiftUI views using XCTest and FOSTestingUI. Covers accessibility identifiers, ViewModelOperations, and test data transport.