feat(shop): story-37 target templates beside the hand-lowering

- view.html per feature + layout app/header/footer.html: the markup-
  first form woc will compile ({{}} auto-escaped, w:if/w:for,
  {!! !!} slots, w:component sections); inert today, verified not to
  disturb the build
- README: pair is the DX referendum — .html is the target feel,
  view.wo is today's cost; doctrine line reworded (templates compile
  or don't exist)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-24 00:01:11 +02:00
parent 0b2a5ebc32
commit 9532116243
7 changed files with 115 additions and 5 deletions

View file

@ -29,6 +29,7 @@ it and restart: the orders are still there (WAL replay).
| `product_page/`, `orders/` | one view module per feature + its root controller file | feature folders |
| `static_files/controller.wo` | `/assets/*` from disk, traversal-safe, typed | `angular.json` assets |
| `assets/style.css` | ONE real stylesheet, sectioned per feature | the `.scss` files |
| `*/view.html`, `layout/*.html` | STORY 37's TARGET — markup-first templates ({{ }} auto-escaped, w:if/w:for, {!! !!} slots) that woc will COMPILE against the view classes; inert today | `.component.html` / Vue `<template>` |
| `main.wo` | bootstrap: seed, routes, serve — nothing else | `app-routing.module.ts` + `main.ts` |
Separation is compiler-enforced where the language allows it today:
@ -38,9 +39,12 @@ sit in the root module beside `types.wo`, because of gap #1 below.
## What is deliberately different (doctrine)
- **No `.html`/`.scss` template files.** Views are `.wo` code — the
compiler type-checks them, `esc()` is the one escaping rule, and no
template engine runs at request time. Styles stay a real CSS file,
- **Templates compile or they don't exist (story 37).** The `.html`
files here are the TARGET: when 37 lands, `woc` compiles each against
its view class (typo'd field = compile error) and the hand-written
`render()` methods in `view.wo` disappear. Until then the `.wo` files
are the running hand-lowering of exactly that markup, and NO template
engine runs at request time — ever. Styles stay a real CSS file,
served statically (there is no scss preprocessor).
- **No closures, no DI.** A view is a class with fields + `render()`;
a controller is a class satisfying `Handler`. Capture = a field.
@ -64,6 +68,8 @@ sit in the root module beside `types.wo`, because of gap #1 below.
1. `types.wo` — the entire persistence layer is 20 lines.
2. `orders.controller.wo` — the whole buying flow (validate, stock
check, decrement, durable insert, render) with no framework magic.
3. `product_list/view.wo` — is markup-as-code readable enough without
templates? This file is the referendum.
3. `product_list/view.html` NEXT TO `product_list/view.wo` — the
template 37 will compile vs today's hand-lowering. The pair is the
referendum: the `.html` is what writing a view will feel like, the
`.wo` is what it costs today.
4. `main.wo` — the app at a glance: five routes, one middleware, serve.

View file

@ -0,0 +1,20 @@
<!-- layout/app.html — STORY 37's TARGET for the app shell: the document
every page shares. {!! !!} slots take prebuilt fragments (header,
footer, the page content); app.wo is the hand-lowered equivalent. -->
<template w:component="AppShell">
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{{ self.title }}</title>
<link rel="stylesheet" href="/assets/style.css">
</head>
<body>
{!! self.header !!}
<main class="site-main">{!! self.content !!}</main>
{!! self.footer !!}
</body>
</html>
</template>

View file

@ -0,0 +1,7 @@
<!-- layout/footer.html — STORY 37's TARGET; footer.wo hand-lowers it. -->
<template w:component="Footer">
<footer class="site-footer">
writeonce shop — one binary: server, database, these pages.
</footer>
</template>

View file

@ -0,0 +1,11 @@
<!-- layout/header.html — STORY 37's TARGET; header.wo hand-lowers it. -->
<template w:component="Header">
<header class="site-header">
<a href="/" class="brand">writeonce shop</a>
<nav class="site-nav">
<a href="/">Products</a>
<a href="/orders">Orders</a>
</nav>
</header>
</template>

View file

@ -0,0 +1,28 @@
<!-- orders/view.html — STORY 37's TARGET (see product_list/view.html for
the convention). view.wo is the hand-lowered equivalent. -->
<template w:component="OrderOk">
<div class="card">
<h1>Order placed</h1>
<p>{{ self.qty }} × {{ self.name }} — total € {{ self.total }}</p>
<p><a href="/orders">See all orders</a> · <a href="/">Keep shopping</a></p>
</div>
</template>
<template w:component="OrderRow">
<tr>
<td>{{ self.name }}</td>
<td>{{ self.qty }}</td>
<td>€ {{ self.total }}</td>
<td>{{ self.status }}</td>
</tr>
</template>
<template w:component="OrdersPage">
<h1>Orders</h1>
<p w:if="self.rows == ''">No orders yet — <a href="/">go buy something</a>.</p>
<table w:if="self.rows != ''" class="orders">
<tr><th>Product</th><th>Qty</th><th>Total</th><th>Status</th></tr>
{!! self.rows !!}
</table>
</template>

View file

@ -0,0 +1,20 @@
<!-- product_list/view.html — STORY 37's TARGET, not yet compiled.
When 37 lands, woc compiles this file against the view classes in
view.wo; until then view.wo's render() methods are the HAND-LOWERED
equivalent of exactly this markup. Notation ({{ }} auto-escaped,
{!! !!} raw slot, w:if / w:for) is illustrative — 37's spec settles
the final spelling. -->
<template w:component="ProductCard">
<div class="card">
<h3><a href="/p/{{ self.sku }}">{{ self.name }}</a></h3>
<p class="price">€ {{ self.price }}</p>
<p w:if="self.stock > 0" class="stock">{{ self.stock }} in stock</p>
<p w:if="self.stock == 0" class="stock out">sold out</p>
</div>
</template>
<template w:component="ProductListPage">
<h1>Products</h1>
<div class="grid">{!! self.cards !!}</div>
</template>

View file

@ -0,0 +1,18 @@
<!-- product_page/view.html — STORY 37's TARGET (see product_list/view.html
for the convention). view.wo is the hand-lowered equivalent. -->
<template w:component="ProductPage">
<div class="card detail">
<h1>{{ self.name }}</h1>
<p class="price">€ {{ self.price }}</p>
<p class="stock">{{ self.stock }} in stock</p>
<form w:if="self.stock > 0" method="POST" action="/orders/{{ self.sku }}">
<label>Quantity</label>
<input type="text" name="qty" value="1" class="field">
<button type="submit" class="btn">Buy</button>
</form>
<p w:if="self.stock == 0" class="stock out">sold out</p>
</div>
<p><a href="/">← all products</a></p>
</template>