feat: bootstrap design kit and vaporwave demo baseline

This commit is contained in:
2026-02-24 01:13:58 +03:00
commit d0cffab6a1
95 changed files with 11949 additions and 0 deletions

14
.gitattributes vendored Normal file
View File

@@ -0,0 +1,14 @@
* text=auto eol=lf
# Common binary assets
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.woff binary
*.woff2 binary
*.ttf binary
*.otf binary

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
bin/
dist/
*.exe
*.test
.DS_Store
designsync
demo-server
.gocache_*/

15
AGENTS.md Normal file
View File

@@ -0,0 +1,15 @@
# UI Design Code — Agent Instructions
Read and follow `bible/README.md` as the architecture source of truth.
This repository is a submodule-first design kit for Go web projects. Keep shared assets in
`kit/` generic and reusable; project branding belongs in host repositories.
## Rules
- Architecture and integration contracts live in `bible/` (English only).
- Significant decisions go to `bible/decisions/10-decisions.md`.
- Public bundle contracts live in `exports/`; keep them in sync with `kit/`.
- Prefer updating shared templates/patterns over adding host-specific logic here.
- Do not broaden the repository scope beyond the currently approved area unless the user
explicitly requests it; when in doubt, stay focused on the current scope.

13
CHANGELOG.md Normal file
View File

@@ -0,0 +1,13 @@
# Changelog
All notable changes to the public design kit surface are documented here.
## Unreleased
### Added
- Submodule-first repository layout (`kit/`, `exports/`, `tools/designsync`, `demo/`)
- Initial bundle manifests (`ai-rules`, `bible-core`, `go-web-skeleton`, `ui-pattern-table`, `ui-pattern-modal`)
- `designsync` MVP CLI (`list`, `validate`, `plan`, `apply`)
- Demo app (`demo/`) with bundle catalog UI

17
CLAUDE.md Normal file
View File

@@ -0,0 +1,17 @@
# UI Design Code — Instructions for Claude
Read and follow the project Bible before making any changes:
**[`bible/README.md`](bible/README.md)**
The Bible is the single source of truth for architecture, public integration contracts,
bundle manifests, and reusable pattern conventions.
Every significant architectural decision must be recorded in
[`bible/decisions/10-decisions.md`](bible/decisions/10-decisions.md).
If a change affects `kit/`, `exports/`, or `tools/designsync`, update the relevant Bible
docs and `CHANGELOG.md` in the same change set.
Do not expand the repository scope beyond the currently approved work area unless the user
explicitly requests that expansion.

41
README.md Normal file
View File

@@ -0,0 +1,41 @@
# UI Design Code
Submodule-first design-code kit for Go web applications and AI coding agents.
This repository is meant to be added to host projects as a git submodule and used to
copy/sync canonical documentation, AI instruction templates, scaffolds, and UI pattern
building blocks.
## Intended Usage
1. Add as submodule (recommended path: `tools/ui-design-code`)
2. Use `tools/designsync` to list/plan/apply bundles into the host repo
3. Review changes and commit in the host repo
## Public Surface (stable)
- `kit/`
- `exports/`
- `tools/designsync/`
- `README.md`
- `VERSIONING.md`
- `CHANGELOG.md`
`demo/` is a runnable reference app and may change faster.
## Quick Start (this repo)
```bash
go test ./...
go build ./tools/designsync
cd demo
go test ./...
go run ./cmd/demo-server
```
## Documentation
- Architecture source of truth: `bible/README.md`
- AI instructions: `CLAUDE.md`, `AGENTS.md`

24
VERSIONING.md Normal file
View File

@@ -0,0 +1,24 @@
# Versioning Policy
This repository uses `v0.x.y` tags while the public bundle contracts are stabilizing.
## Public Compatibility Surface
Breaking changes are changes to:
- `kit/` file paths or template variables
- `exports/` manifest schema or bundle ids
- `tools/designsync` CLI behavior/flags
## Tagging Rules (`v0.x.y`)
- `y` (patch): fixes, docs updates, non-breaking improvements
- `x` (minor): any breaking public-surface change during `0.x`
## Release Checklist
1. Update `CHANGELOG.md`
2. Validate manifests (`designsync validate`)
3. Run tests (`go test ./...` and `cd demo && go test ./...`)
4. Tag release

56
bible/README.md Normal file
View File

@@ -0,0 +1,56 @@
# UI Design Code Bible
> **Documentation language:** English only. All maintained architecture documentation must be
> written in English.
>
> **Architectural decisions:** Every significant architectural decision **must** be recorded in
> [`decisions/10-decisions.md`](decisions/10-decisions.md) before or alongside the code change.
>
> **Single source of truth:** Architecture and technical design documentation belongs in `bible/`.
> Keep `README.md`, `CLAUDE.md`, and `AGENTS.md` minimal to avoid duplicate documentation.
This directory is the single source of truth for UI Design Code architecture, bundle contracts,
and reusable pattern conventions. It is structured so both humans and AI assistants can navigate
it quickly.
---
## Reading Map (Hierarchical)
### 1. Foundations (read first)
| File | What it covers |
|------|----------------|
| [architecture/system-overview.md](architecture/system-overview.md) | Product purpose, scope, repository composition |
| [architecture/submodule-integration-contract.md](architecture/submodule-integration-contract.md) | How host repositories consume this kit |
| [architecture/ui-pattern-catalog.md](architecture/ui-pattern-catalog.md) | Canonical pattern taxonomy and bundle status |
### 2. Runtime & Delivery
| File | What it covers |
|------|----------------|
| [architecture/demo-runtime-flows.md](architecture/demo-runtime-flows.md) | Demo app routes, render flow, failure behavior |
| [governance/documentation-policy.md](governance/documentation-policy.md) | Rules for maintaining architecture docs |
### 3. Governance (always current)
| File | What it covers |
|------|----------------|
| [decisions/10-decisions.md](decisions/10-decisions.md) | Architectural Decision Log (ADL) |
| [synthesis/source-repos.md](synthesis/source-repos.md) | Source repositories and Bible entrypoints |
| [synthesis/common-invariants.md](synthesis/common-invariants.md) | Shared rules extracted from source repos |
| [synthesis/normalization-matrix.md](synthesis/normalization-matrix.md) | Source → canonical convention mapping |
| [synthesis/ui-pattern-coverage-matrix.md](synthesis/ui-pattern-coverage-matrix.md) | UI pattern checklist and coverage status |
---
## Quick Orientation for AI Assistants
- Public reusable artifacts: `kit/`
- Bundle manifests: `exports/bundles/`
- Bundle index: `exports/index.yaml`
- Sync/apply tool: `tools/designsync/`
- Runnable reference app: `demo/`
Read order for most changes: system overview → submodule contract → affected bundle docs →
decision log.

View File

@@ -0,0 +1,41 @@
# Demo Runtime Flows
## Demo App Purpose
The demo app (`demo/`) is a runnable reference for shared UI patterns. It is not the public
integration surface for host repositories.
## HTTP Routes (initial)
- `GET /` - pattern bundle catalog page
- `GET /healthz` - health endpoint
- `GET /static/*` - static assets
## Demo Shell Visual Mode (Current Baseline)
- The main demo shell uses a dual visual baseline:
- `Vapor Soft` for light/system-light mode
- `Vapor Night` for dark/system-dark mode
- Theme selection in the main demo shell is system-driven (`prefers-color-scheme`) only.
- The style playground route may exist for internal experimentation, but it is not part of the
primary demo navigation/catalog when the baseline is under active refinement.
## Page Render Flow (`GET /`)
1. Request hits `net/http` server mux
2. Handler prepares bundle/pattern catalog view model
3. Embedded templates render HTML response
4. Static CSS/JS loaded from embedded assets
## Interaction Flow Guardrail (Server-Rendered UI)
- Query-driven interactions (filters, pagination, segmented tabs, bulk actions, modal open/step state)
must preserve the user's reading position on the page.
- Canonical mechanism in this repo: anchor-to-module links/forms (for example `#table-filters`,
`#table-list`, `#controls-selection`, `#timeline-drilldown`).
- JS scroll restoration is optional and not required for the base contracts.
## Failure Behavior
- Template parse/render errors return `500`
- No DB calls or external services in bootstrap demo

View File

@@ -0,0 +1,31 @@
# Legacy Aqua Freeze (Reference Only)
## Status
This document records the outcome of the Aqua-style exploration and its freeze status.
- Status: `legacy`
- Active use: `archive reference`
- Purpose: preserve the explored style decisions as a reusable reference snapshot bundle
## Locked Decisions (Aqua Exploration)
- Aqua-inspired component language was explored across buttons, segmented controls, tables, filter controls, modal chrome, and status badges.
- The exploration introduced a consistent control-surface vocabulary and modal/window chrome experiments.
- The exploration is preserved as an archive snapshot for reference and rollback safety.
## Current Direction
- Active visual baseline for the demo and shared styling is **Aqua-first (macOS Aqua-inspired)**.
- This document remains as an archive record of the freeze/snapshot event and stored artifacts.
## Storage / Bundle
- Legacy Aqua snapshot bundle: `ui-theme-aqua-legacy`
- Bundle manifest: `exports/bundles/ui-theme-aqua-legacy.yaml`
- Snapshot assets and notes: `kit/patterns/theme-aqua-legacy/`
## Usage Policy
- The snapshot may be used as a legacy reference, migration source, or comparison artifact when explicitly needed.
- The active baseline is maintained in the live demo/scaffold styles, not in this frozen snapshot file.

View File

@@ -0,0 +1,44 @@
# Submodule Integration Contract
## Canonical Consumption Model
Host repositories consume this project as a git submodule and copy/sync selected artifacts
from the kit into their own repository.
Direct runtime dependency on submodule paths is not the default integration pattern.
## Recommended Host Placement
- `tools/ui-design-code`
- `third_party/ui-design-code`
## Stable Public Surface
Host repositories may rely on:
- `kit/`
- `exports/`
- `tools/designsync/`
- `VERSIONING.md`
- `CHANGELOG.md`
`demo/` is reference-only and not part of the stable integration contract.
## Bundles
Bundle definitions live in `exports/bundles/*.yaml` and are indexed by `exports/index.yaml`.
Each bundle must define:
- stable `id`
- version
- source paths under `kit/`
- target mapping rules
- conflict policy
- template variables (if any)
## Safety Rules
- Bundle manifests must not write outside the declared host target root.
- `designsync` must reject path traversal in manifest source/target paths.

View File

@@ -0,0 +1,33 @@
# System Overview
## Product
UI Design Code is a submodule-first design kit for Go web applications and AI coding agents.
It provides:
- canonical documentation skeletons ("Bible")
- AI instruction templates
- reusable scaffolds and UI pattern building blocks
- a sync/apply tool (`designsync`) for host repositories
- a runnable demo app for pattern reference
## Primary Consumers
- Host Go repositories using this repo as a git submodule
- AI coding agents (Codex, Claude Code, etc.)
- Developers maintaining shared UI/architecture conventions
## Runtime Composition
- Root module: tools and bundle management (`tools/designsync`)
- `kit/`: copy/sync source artifacts for host repos
- `exports/`: machine-readable bundle manifests
- `demo/`: separate Go module for browsing patterns in action
## Non-Goals (Phase 0 / bootstrap)
- No production-ready shared Go UI framework
- No host-project-specific business rules or branding
- No database/runtime dependencies for the demo

View File

@@ -0,0 +1,46 @@
# UI Pattern Catalog
Canonical pattern taxonomy for reusable Go web UI patterns.
Visual baseline for the canonical demo and scaffold is Vapor Soft (light) / Vapor Night (dark),
with system color-scheme detection as the active mode selection mechanism. The hidden style
playground remains a comparative laboratory and not part of the main demo navigation. A frozen
Aqua snapshot bundle is still kept as a legacy archive for reference.
## Pattern Families
- Table + filters + pagination
- Modal workflows (create/edit/remove/confirm)
- Status indicators and health badges
- Timeline cards and grouped history views
- Forms with validation and server round-trips
- Operator/admin tools dashboards (queue + batch actions)
## Initial Bundle Status
| Pattern | Bundle | Status |
|---|---|---|
| Table + pagination | `ui-pattern-table` | placeholder contract + files |
| Controls + selection | `ui-pattern-controls` | contract + files |
| Forms + validation + suggestions | `ui-pattern-forms` | contract + files |
| Operator tools dashboard | `ui-pattern-operator-tools` | contract + files |
| Modal workflow | `ui-pattern-modal` | contract + files |
| Import / export | `ui-pattern-io` | contract + files |
| Status indicators | bundled inside controls/table demos | partial |
| Timeline cards | `ui-pattern-timeline` | contract + files |
## Synthesis Direction
Initial canonical behaviors are synthesized from multiple existing Go web applications with:
- detailed UI interaction contracts
- operational admin workflows
- lean `net/http` + template runtime structures
## Component Baseline Notes
- Buttons, segmented controls, status badges, filter inputs, and table surfaces share one baseline component language.
- Canonical demo visuals target a two-mode baseline (`Vapor Soft` and `Vapor Night`) and should
not expose ad-hoc per-page visual toggles in the main demo shell.
- Segmented controls use joined-button geometry (shared shell, rounded outer edges only, straight internal separators).
- Context-specific active-state color is allowed (for example dark scope tabs vs blue theme preset selector) without changing segmented geometry.

View File

@@ -0,0 +1,106 @@
# Architectural Decision Log
## ADL-001 — Submodule-first repository layout
**Date:** 2026-02-23
**Context:** The repository must be consumable by multiple host projects with minimal coupling.
**Decision:** The root repository is the reusable kit surface (`kit/`, `exports/`, `tools/designsync`),
while the runnable reference app lives in `demo/` as a separate module.
**Consequences:**
- Host repositories can pin and sync shared artifacts without depending on demo runtime code.
- Public compatibility boundaries are clearer.
- Documentation and changelog discipline are required for bundle contracts.
## ADL-002 — Copy/sync is the default integration model
**Date:** 2026-02-23
**Context:** Host repositories need stable, reviewable changes and should avoid hidden runtime
dependency on submodule internals.
**Decision:** Canonical integration is copy/sync from submodule bundles using `designsync`.
**Consequences:**
- Host repos review concrete file changes in their own tree.
- Runtime coupling to submodule paths is avoided.
- Bundle manifests and sync tooling become a critical public interface.
## ADL-003 — Scope expansion requires explicit user direction
**Date:** 2026-02-23
**Context:** The repository can easily grow from UI/UX design code into broader application
architecture standards, which risks losing focus and slowing delivery.
**Decision:** Keep the repository focused on the currently approved scope. Do not expand into new
design-code domains unless the user explicitly requests that expansion.
**Consequences:**
- Work remains concentrated on current deliverables and consistency.
- New domains are added intentionally instead of opportunistically.
- Agents should prefer improving existing patterns/docs over inventing adjacent scope.
## ADL-004 — Aqua style work is frozen as legacy; active rewrite direction is Win9x
**Date:** 2026-02-23
**Context:** A substantial Aqua-inspired visual exploration was implemented during demo refinement.
The result is useful as a reference archive, but it should not continue to drive the active baseline.
The next visual rewrite direction is a simpler classic Windows 95-2000 (Win9x-style) baseline.
**Decision:** Freeze Aqua-related visual work as a legacy snapshot bundle. Keep it documented for
reference, but do not treat it as the active baseline for ongoing demo/scaffold styling. Continue
visual work on the Win9x rewrite path.
**Consequences:**
- Aqua styles remain available for legacy reference and extraction.
- New styling changes should target the Win9x baseline instead of extending Aqua.
- Documentation must clearly mark Aqua as legacy / not active to avoid ambiguity.
## ADL-005 — Restore Aqua as the active visual baseline
**Date:** 2026-02-23
**Context:** The temporary Win9x-wide rewrite pass was evaluated and rejected for the current demo
catalog. The team decided to continue from the existing Aqua-based visual work.
**Decision:** Restore Aqua-first styling as the active baseline for the demo/scaffold and keep the
Win9x attempt as a discarded experiment. Preserve the Aqua snapshot bundle/docs as archive
artifacts, but continue active iteration on the live Aqua baseline.
**Consequences:**
- Demo pages continue to evolve on top of Aqua-first styles.
- Legacy bundle artifacts remain available for archival reference.
- Documentation should describe Aqua as active baseline and avoid marking Win9x as current direction.
## ADL-006 — Main demo shell baseline is Vapor Soft / Vapor Night with system-only mode selection
**Date:** 2026-02-23
**Context:** The demo catalog visual work moved from Aqua-first experimentation to a new Vaporwave
inspired UI direction intended to remain usable for day-long operation. Manual day/night toggles in
the shell caused state conflicts and inconsistent rendering relative to system color-scheme.
**Decision:** Use a dual baseline in the main demo shell:
- `Vapor Soft` for light mode
- `Vapor Night` for dark mode
Mode selection follows system settings only (`prefers-color-scheme`). The style playground may
remain available as an internal route for visual experimentation, but it is hidden from the main
demo navigation/catalog while the baseline is actively refined.
**Consequences:**
- The main demo shell keeps one stable visual contract while still supporting light/dark operation.
- Manual theme override UI/state is removed from the main shell to avoid visual mismatches.
- Vapor-themed background composition (including dark-mode retro-grid variant) becomes part of the
canonical demo presentation and must remain subordinate to UI readability.

17
bible/decisions/README.md Normal file
View File

@@ -0,0 +1,17 @@
# Architecture Decision Records
Use this directory to capture architecture decisions and significant updates.
## Required
- Every architecture decision must be recorded in the Bible.
- If a decision replaces an older one, update the older document in the same change.
- Keep entries short, explicit, and linked to the affected architecture files.
## Minimal Entry Template
- Date (`YYYY-MM-DD`)
- Decision
- Context
- Consequences
- Supersedes (if any)

View File

@@ -0,0 +1,29 @@
# Documentation Policy
## Purpose
This policy defines how architectural knowledge is captured and maintained.
## Mandatory Rules
- Record every architecture decision in the Bible before or together with implementation.
- Use English for all architecture documentation.
- Keep only current architecture in active sections.
- When a solution is replaced, update or remove obsolete guidance in the same change.
- Keep architecture details centralized in `bible/`; top-level docs should only reference it.
## Change Workflow
1. Update the relevant file(s) in `bible/architecture/` or `bible/synthesis/`.
2. If behavior changed, add or update a decision note in `bible/decisions/10-decisions.md`.
3. Update `CHANGELOG.md` when public bundle/tooling contracts change.
4. Remove duplicated or outdated statements from `README.md`, `CLAUDE.md`, and `AGENTS.md`.
## Scope Expansion Guardrail
- Do not expand the design-code scope beyond the currently approved work area by default.
- Expansion into new domains (for example API contracts, data/model architecture, observability,
security, or backend service patterns) requires an explicit user request.
- If the request is ambiguous, keep focus on the current scope and ask for confirmation before
broadening the repository mission.
- Prefer depth and consistency in the current approved scope over breadth.

View File

@@ -0,0 +1,18 @@
# Common Invariants Across Source Repos
## Documentation
- Architecture docs are authoritative and must be kept current with code changes.
- English-only Bible documentation is enforced (or being enforced).
- AI instruction files point to the Bible entrypoint.
## Engineering Practices
- Go is the implementation language.
- Web UI is server-rendered or template-driven.
- Explicit operational/build commands are documented for AI agents.
## Design-Kit Implication
This repository standardizes documentation and reusable pattern contracts while leaving room for
host-project branding and business rules.

View File

@@ -0,0 +1,8 @@
# Normalization Matrix (Initial)
| Source Variant | Source Convention | Canonical Rule in This Repo |
|---|---|---|
| Variant A | `bible/README.md` | Root `bible/README.md` as source of truth |
| Variant B | `bible/BIBLE.md` | Root `bible/README.md` entrypoint (English docs) |
| Variant C | `docs/bible/README.md` | Normalize to root `bible/` for new repos |
| Mature UI Contract Repo | Detailed UI interaction contracts | Use pattern contracts in `kit/patterns/*/contract.md` |

View File

@@ -0,0 +1,15 @@
# Reference Applications (Anonymized)
The initial design-code synthesis draws from several existing Go web applications that use a
Bible-style architecture documentation system.
## Observed Variants
- Bible entrypoint at `bible/README.md`
- Bible entrypoint at `bible/BIBLE.md`
- Bible entrypoint at `docs/bible/README.md`
## Key Observation
All source applications treat the Bible (or equivalent folder) as the architecture source of
truth, but folder layout and UI/runtime stacks differ.

View File

@@ -0,0 +1,47 @@
# UI Pattern Coverage Matrix (Reference Apps → Design Kit)
This file tracks UI/UX pattern extraction from reference Go web applications into this
repository's current scope (UI/UX + interaction contracts + demo pages + reusable pattern docs).
## Scope Reminder
- This matrix covers only the currently approved design-code scope.
- It does **not** expand into backend architecture, API contract frameworks, or observability.
## Pattern Checklist
| Pattern Family | Reference Evidence (types of usage observed) | Demo Coverage | Kit Contract / Bundle | Status |
|---|---|---|---|---|
| Table lists | multiple list/detail/admin tables | `/patterns/table`, `/patterns/controls` | `ui-pattern-table` | covered (core) |
| Pagination | shared pagination style, prev/next/page links | `/patterns/table` | `ui-pattern-table` | covered (core) |
| Server-side filters | header filters, URL query semantics | `/patterns/table` | `ui-pattern-table` | covered (core) |
| Datalist/autocomplete filters | datalist-backed header filters and suggestions | `/patterns/forms` | `ui-pattern-forms` | covered |
| Buttons hierarchy | primary/secondary/danger/quiet controls | `/patterns/controls` | `ui-pattern-controls` | covered |
| Checkboxes + selection | row select, select visible, bulk actions | `/patterns/controls` | `ui-pattern-controls` | covered |
| Segmented/tabs | active/archived, mode switches, tab buttons | `/patterns/controls`, `/patterns/forms` | `ui-pattern-controls`, `ui-pattern-forms` | covered |
| Status badges/indicators | entity status, source/status labels, warning chips | `/patterns/controls`, `/patterns/timeline` | `ui-pattern-controls` | covered (core) |
| Modals (single-step) | create/edit/remove dialogs | `/patterns/modals` | `ui-pattern-modal` | covered |
| Modals (multi-step confirm) | review/confirm/submit flows | `/patterns/modals`, `/patterns/forms` | `ui-pattern-modal`, `ui-pattern-forms` | covered |
| Import workflow | file input, preview, confirm | `/patterns/io`, `/patterns/forms` | `ui-pattern-io`, `ui-pattern-forms` | covered |
| Export workflow | explicit scope/format, CSV download | `/patterns/io` | `ui-pattern-io` | covered |
| CSV export compatibility details | BOM + delimiter for spreadsheet UX | `/patterns/io/export.csv` | `ui-pattern-io` | covered |
| Timeline cards | grouped by day/action/source | `/patterns/timeline` | `ui-pattern-timeline` | covered |
| Timeline drilldown | single drilldown panel/modal, event detail | `/patterns/timeline` | `ui-pattern-timeline` | covered |
| Empty states | table/list/filter empty states | multiple demo pages | multiple bundles | covered |
| Inline validation messages | form-level and field-level messages | `/patterns/forms` | `ui-pattern-forms` | covered |
| File upload controls | `<input type="file">` and import affordances | `/patterns/forms`, `/patterns/io` | `ui-pattern-io`, `ui-pattern-forms` | covered |
| Global selection across paginated pages | persistent selection across pages/storage | `/patterns/controls` (paginated selection + select visible/filtered + preserved `sel`) | `ui-pattern-controls` | covered |
| Advanced admin tools / repair dashboards | complex operator tooling with many tables | `/patterns/operator-tools` (canonicalized operator dashboard) | `ui-pattern-operator-tools` | covered (simplified canonical) |
## Notes on Intentional Simplifications
- Some complex operator workflows are represented as simplified demos to keep the repository
focused on reusable interaction contracts rather than domain-specific UI logic.
- Global selection is demonstrated via query-driven selected IDs preserved across paginated views.
- Client-side persistence (for example local storage) is intentionally not required for the canonical contract.
## Next In-Scope Improvements (Optional, not automatic)
- Stronger global-selection demo (cross-page or local-storage persistence)
- Dedicated "operator dashboard tools" pattern page (still within UI/UX scope)
- Richer datalist/autocomplete disambiguation candidate flow

12
demo/Makefile Normal file
View File

@@ -0,0 +1,12 @@
run:
go run ./cmd/demo-server
build:
go build ./cmd/demo-server
test:
go test ./...
fmt:
gofmt -w $$(find . -name '*.go' -type f)

7
demo/README.md Normal file
View File

@@ -0,0 +1,7 @@
# UI Design Code Demo
Runnable reference app for shared UI patterns and bundle catalog.
This app is not the public submodule contract; host repositories should consume `kit/` via
`tools/designsync`.

4
demo/go.mod Normal file
View File

@@ -0,0 +1,4 @@
module git.mchus.pro/mchus/ui-design-code-demo
go 1.24.0

File diff suppressed because it is too large Load Diff

340
demo/internal/web/server.go Normal file
View File

@@ -0,0 +1,340 @@
package web
import (
"fmt"
"html/template"
"io/fs"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
appweb "git.mchus.pro/mchus/ui-design-code-demo/web"
)
type Server struct {
mux *http.ServeMux
tmpl *template.Template
}
type PatternCard struct {
ID string
Name string
Bundle string
Link string
Status string
Summary string
}
type IndexViewData struct {
Title string
CurrentPath string
Bundles []PatternCard
Patterns []PatternCard
}
type tableDemoRow struct {
ID int
Name string
Category string
Status string
Owner string
Updated string
}
type tableDemoFilters struct {
Query string
Category string
Status string
}
type tableDemoPager struct {
Page int
PerPage int
TotalItems int
TotalPages int
From int
To int
PrevURL string
NextURL string
Links []tableDemoPageLink
}
type tableDemoPageLink struct {
Label string
URL string
Current bool
Ellipsis bool
}
type tableDemoPageData struct {
Title string
CurrentPath string
Rows []tableDemoRow
Filters tableDemoFilters
Pager tableDemoPager
PerPage int
PerPageOpts []int
Categories []string
Statuses []string
}
func NewServer() (*Server, error) {
tmpl, err := template.New("demo").Funcs(template.FuncMap{
"withQuery": withQuery,
"dict": dict,
}).ParseFS(appweb.Assets, "templates/*.html")
if err != nil {
return nil, err
}
s := &Server{mux: http.NewServeMux(), tmpl: tmpl}
s.registerRoutes()
return s, nil
}
func (s *Server) Handler() http.Handler { return s.mux }
func (s *Server) registerRoutes() {
s.mux.HandleFunc("/", s.handleIndex)
s.mux.HandleFunc("/patterns/table", s.handleTablePattern)
s.mux.HandleFunc("/patterns/controls", s.handleControlsPattern)
s.mux.HandleFunc("/patterns/modals", s.handleModalPattern)
s.mux.HandleFunc("/patterns/io", s.handleIOPattern)
s.mux.HandleFunc("/patterns/io/export.csv", s.handleIOExportCSV)
s.mux.HandleFunc("/patterns/forms", s.handleFormsPattern)
s.mux.HandleFunc("/patterns/style-playground", s.handleStylePlaygroundPattern)
s.mux.HandleFunc("/patterns/operator-tools", s.handleOperatorToolsPattern)
s.mux.HandleFunc("/patterns/timeline", s.handleTimelinePattern)
s.mux.HandleFunc("/healthz", s.handleHealthz)
staticFS, err := fs.Sub(appweb.Assets, "static")
if err == nil {
s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
}
}
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
data := IndexViewData{
Title: "UI Design Code",
CurrentPath: "/",
Bundles: []PatternCard{
{ID: "ai-rules", Name: "AI Rules", Bundle: "ai-rules", Status: "ready", Summary: "CLAUDE/AGENTS templates + shared architecture doc policy."},
{ID: "bible-core", Name: "Bible Core", Bundle: "bible-core", Status: "ready", Summary: "Canonical Bible skeleton for Go web projects using AI coding agents."},
{ID: "go-web-skeleton", Name: "Go Web Skeleton", Bundle: "go-web-skeleton", Status: "ready", Summary: "Minimal net/http + templates scaffold for host repos."},
},
Patterns: []PatternCard{
{ID: "table-pagination", Name: "Table + Pagination", Bundle: "ui-pattern-table", Link: "/patterns/table", Status: "ready", Summary: "Server-side filters + pagination contract for canonical list pages."},
{ID: "controls-selection", Name: "Controls + Selection", Bundle: "ui-pattern-controls", Link: "/patterns/controls", Status: "ready", Summary: "Buttons, checkboxes, bulk-selection, segmented actions, status chips."},
{ID: "modal-workflows", Name: "Modal Workflows", Bundle: "ui-pattern-modal", Link: "/patterns/modals", Status: "ready", Summary: "Create/edit/remove + confirm modal workflow contracts for admin and detail pages."},
{ID: "import-export", Name: "Import / Export", Bundle: "ui-pattern-io", Link: "/patterns/io", Status: "ready", Summary: "File import forms, preview/confirm UX, CSV export controls and download endpoint."},
{ID: "forms-validation", Name: "Forms + Validation", Bundle: "ui-pattern-forms", Link: "/patterns/forms", Status: "ready", Summary: "Inline validation, datalist suggestions, tabs/steps, confirm-before-submit forms."},
{ID: "operator-tools", Name: "Operator Tools", Bundle: "ui-pattern-operator-tools", Link: "/patterns/operator-tools", Status: "ready", Summary: "Complex operator/admin dashboards: queue tables, batch actions, safety checks, and run states."},
{ID: "timeline-cards", Name: "Timeline Cards", Bundle: "ui-pattern-timeline", Link: "/patterns/timeline", Status: "ready", Summary: "Grouped timeline cards with drilldown modal and in-card filtering."},
},
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.tmpl.ExecuteTemplate(w, "base.html", data); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
}
}
func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}
func (s *Server) handleTablePattern(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/patterns/table" {
http.NotFound(w, r)
return
}
filters := tableDemoFilters{
Query: strings.TrimSpace(r.URL.Query().Get("q")),
Category: strings.TrimSpace(r.URL.Query().Get("category")),
Status: strings.TrimSpace(r.URL.Query().Get("status")),
}
allRows := demoTableRows()
filtered := make([]tableDemoRow, 0, len(allRows))
for _, row := range allRows {
if !matchesTableFilters(row, filters) {
continue
}
filtered = append(filtered, row)
}
page := parsePositiveInt(r.URL.Query().Get("page"), 1)
perPage := parseAllowedInt(r.URL.Query().Get("per_page"), 10, []int{5, 10, 20})
rowsPage, pager := paginateTableRows(r.URL, filtered, page, perPage)
data := tableDemoPageData{
Title: "Table + Pagination Pattern",
CurrentPath: "/patterns/table",
Rows: rowsPage,
Filters: filters,
Pager: pager,
PerPage: perPage,
PerPageOpts: []int{5, 10, 20},
Categories: []string{"Compute", "Networking", "Power", "Storage"},
Statuses: []string{"ready", "warning", "review"},
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.tmpl.ExecuteTemplate(w, "table_pattern.html", data); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
}
}
func matchesTableFilters(row tableDemoRow, f tableDemoFilters) bool {
if f.Query != "" {
q := strings.ToLower(f.Query)
haystack := strings.ToLower(strings.Join([]string{row.Name, row.Category, row.Status, row.Owner}, " "))
if !strings.Contains(haystack, q) {
return false
}
}
if f.Category != "" && !strings.EqualFold(row.Category, f.Category) {
return false
}
if f.Status != "" && !strings.EqualFold(row.Status, f.Status) {
return false
}
return true
}
func paginateTableRows(u *url.URL, rows []tableDemoRow, page, perPage int) ([]tableDemoRow, tableDemoPager) {
totalItems := len(rows)
totalPages := 1
if totalItems > 0 {
totalPages = (totalItems + perPage - 1) / perPage
}
if page < 1 {
page = 1
}
if page > totalPages {
page = 1
}
start := 0
end := 0
from := 0
to := 0
pageRows := []tableDemoRow{}
if totalItems > 0 {
start = (page - 1) * perPage
end = start + perPage
if end > totalItems {
end = totalItems
}
from = start + 1
to = end
pageRows = rows[start:end]
}
pager := tableDemoPager{
Page: page,
PerPage: perPage,
TotalItems: totalItems,
TotalPages: totalPages,
From: from,
To: to,
Links: buildPageLinks(u, page, totalPages),
}
return pageRows, pager
}
func pageURL(u *url.URL, page, totalPages int) string {
if totalPages < 1 {
totalPages = 1
}
if page < 1 || page > totalPages {
return ""
}
clone := *u
q := clone.Query()
q.Set("page", strconv.Itoa(page))
clone.RawQuery = q.Encode()
return clone.String()
}
func buildPageLinks(u *url.URL, current, totalPages int) []tableDemoPageLink {
if totalPages <= 1 {
return nil
}
if totalPages <= 4 {
out := make([]tableDemoPageLink, 0, totalPages)
for p := 1; p <= totalPages; p++ {
out = append(out, tableDemoPageLink{
Label: strconv.Itoa(p),
URL: pageURL(u, p, totalPages),
Current: p == current,
})
}
return out
}
pages := map[int]bool{
1: true, 2: true,
totalPages: true, totalPages - 1: true,
current: true, current - 1: true, current + 1: true,
}
keys := make([]int, 0, len(pages))
for p := range pages {
if p < 1 || p > totalPages {
continue
}
keys = append(keys, p)
}
sort.Ints(keys)
out := make([]tableDemoPageLink, 0, len(keys)+2)
prev := 0
for _, p := range keys {
if prev != 0 && p-prev > 1 {
out = append(out, tableDemoPageLink{Label: "...", Ellipsis: true})
}
out = append(out, tableDemoPageLink{
Label: strconv.Itoa(p),
URL: pageURL(u, p, totalPages),
Current: p == current,
})
prev = p
}
return out
}
func parsePositiveInt(v string, fallback int) int {
n, err := strconv.Atoi(v)
if err != nil || n < 1 {
return fallback
}
return n
}
func parseAllowedInt(v string, fallback int, allowed []int) int {
n := parsePositiveInt(v, fallback)
for _, candidate := range allowed {
if n == candidate {
return n
}
}
return fallback
}
func demoTableRows() []tableDemoRow {
rows := make([]tableDemoRow, 0, 36)
categories := []string{"Compute", "Networking", "Storage", "Power"}
statuses := []string{"ready", "warning", "review"}
owners := []string{"Ops", "Infra", "QA", "Procurement"}
for i := 1; i <= 36; i++ {
rows = append(rows, tableDemoRow{
ID: i,
Name: fmt.Sprintf("Component Spec %02d", i),
Category: categories[(i-1)%len(categories)],
Status: statuses[(i-1)%len(statuses)],
Owner: owners[(i-1)%len(owners)],
Updated: fmt.Sprintf("2026-02-%02d", (i%23)+1),
})
}
// Seed a few targeted names to make filter behavior testable and demonstrative.
rows[4].Name = "Rack Controller Alpha"
rows[11].Name = "Rack Controller Beta"
rows[20].Name = "Rack Controller Gamma"
return rows
}

View File

@@ -0,0 +1,292 @@
package web
import (
"bytes"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestRoutes(t *testing.T) {
srv, err := NewServer()
if err != nil {
t.Fatalf("NewServer: %v", err)
}
t.Run("index", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "UI Design Code") {
t.Fatalf("unexpected body: %s", rr.Body.String())
}
})
t.Run("healthz", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
})
t.Run("static", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/static/css/app.css", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
})
t.Run("table pattern page", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/table", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "Canonical List Page") {
t.Fatalf("missing table demo content in body")
}
if !strings.Contains(body, "Showing 110 of 36") {
t.Fatalf("missing summary: %s", body)
}
})
t.Run("controls pattern page", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/controls?segment=warning", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "Selection Table") {
t.Fatalf("missing controls page content")
}
})
t.Run("controls selection toggle persists in query links", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/controls?segment=all&selection_action=toggle&id=2", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "1 selected") {
t.Fatalf("expected selected counter, got body")
}
if !strings.Contains(body, "sel=2") {
t.Fatalf("expected selected id in follow-up links")
}
})
t.Run("controls select visible", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/controls?segment=warning&selection_action=select_visible", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "4 selected") {
t.Fatalf("expected all visible selected")
}
})
t.Run("modal pattern page", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/modals?open=edit&stage=confirm&sel=1", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "Confirmation Summary") || !strings.Contains(body, "Single-modal workflow progression") {
t.Fatalf("missing modal page markers")
}
})
t.Run("io pattern page", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/io?import_mode=confirm&export_ready=1", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "Import / Export Pattern") || !strings.Contains(body, "Download CSV") {
t.Fatalf("missing io page markers")
}
})
t.Run("forms pattern page", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/forms?mode=register&step=review", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "Forms") || !strings.Contains(body, "Validation Pattern") || !strings.Contains(body, "Review Summary") {
t.Fatalf("missing forms page markers")
}
if !strings.Contains(body, "Server serial is required") {
t.Fatalf("expected validation message")
}
})
t.Run("style playground pattern page", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/style-playground?style=slate", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "Style Playground") || !strings.Contains(body, "Theme Presets") {
t.Fatalf("missing style playground markers")
}
if !strings.Contains(body, "theme-slate") {
t.Fatalf("expected theme class in body")
}
})
t.Run("style playground extra presets", func(t *testing.T) {
for _, tc := range []struct {
name string
style string
expectMark string
}{
{name: "y2k silver", style: "y2k-silver", expectMark: "theme-y2k-silver"},
{name: "vaporwave alias", style: "vaporwave", expectMark: "theme-vaporwave"},
{name: "vaporwave soft", style: "vaporwave-soft", expectMark: "theme-vaporwave"},
{name: "vaporwave night", style: "vaporwave-night", expectMark: "theme-vaporwave-night"},
{name: "aqua", style: "aqua", expectMark: "theme-aqua"},
{name: "win9x", style: "win9x", expectMark: "theme-win9x"},
} {
t.Run(tc.name, func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/style-playground?style="+tc.style, nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, tc.expectMark) {
t.Fatalf("expected theme marker %q", tc.expectMark)
}
if !strings.Contains(body, "Y2K Silver") || !strings.Contains(body, "Vapor Soft") || !strings.Contains(body, "Vapor Night") || !strings.Contains(body, "Aqua") || !strings.Contains(body, "Win9x") {
t.Fatalf("expected preset tabs in switcher")
}
})
}
})
t.Run("operator tools pattern page", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/operator-tools?scope=components&queue=failed", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "Operator Tools Pattern") || !strings.Contains(body, "Operations Queue") {
t.Fatalf("missing operator tools page markers")
}
})
t.Run("timeline pattern page", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/timeline?open=c1", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "Timeline Cards Pattern") || !strings.Contains(body, "Open details") {
t.Fatalf("missing timeline page markers")
}
if !strings.Contains(body, "Event Detail") {
t.Fatalf("expected drilldown panel when card is open")
}
})
t.Run("io export csv endpoint", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/io/export.csv?scope=selected", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
if got := rr.Header().Get("Content-Type"); !strings.Contains(got, "text/csv") {
t.Fatalf("content-type=%q", got)
}
if got := rr.Header().Get("Content-Disposition"); !strings.Contains(got, "items.csv") {
t.Fatalf("content-disposition=%q", got)
}
body := rr.Body.Bytes()
if len(body) < 3 || !bytes.Equal(body[:3], []byte{0xEF, 0xBB, 0xBF}) {
t.Fatalf("missing UTF-8 BOM")
}
if !strings.Contains(string(body), "Code;Name;Category;Status;Qty") {
t.Fatalf("unexpected csv header")
}
})
t.Run("table pattern filters before pagination", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/table?q=Rack&page=99", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "Showing 13 of 3") {
t.Fatalf("expected filtered summary, got: %s", body)
}
if !strings.Contains(body, "Rack Controller Alpha") {
t.Fatalf("expected filtered rows")
}
if strings.Contains(body, "Component Spec 36") {
t.Fatalf("unexpected unrelated row present")
}
})
t.Run("table pattern keeps filter params in pager links", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/table?status=ready", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "status=ready") {
t.Fatalf("expected status filter in pagination links")
}
})
t.Run("table pattern empty result", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/patterns/table?q=does-not-exist", nil)
srv.Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("status=%d", rr.Code)
}
body := rr.Body.String()
if !strings.Contains(body, "No rows match current filters.") {
t.Fatalf("expected empty-state message")
}
if !strings.Contains(body, "Showing 0 of 0") {
t.Fatalf("expected zero summary")
}
})
}

6
demo/web/embed.go Normal file
View File

@@ -0,0 +1,6 @@
package web
import "embed"
//go:embed templates/* static/*
var Assets embed.FS

4547
demo/web/static/css/app.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
document.documentElement.classList.add("js");
document.documentElement.setAttribute("data-theme-mode", "auto");

View File

@@ -0,0 +1,311 @@
{{ define "demo_doc_start" }}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .Title }}</title>
<link rel="stylesheet" href="/static/css/app.css">
</head>
<body>
{{ template "demo_nav" . }}
{{ template "demo_app_shell" . }}
<main class="page">
{{ end }}
{{ define "demo_doc_end" }}
</main>
<script src="/static/js/app.js" defer></script>
</body>
</html>
{{ end }}
{{ define "demo_masthead" }}
<header class="masthead">
<p class="label">{{ index . "label" }}</p>
<h1>{{ index . "title" }}</h1>
<p class="lead">{{ index . "lead" }}</p>
{{ if index . "back_url" }}
<p class="meta" style="margin-top:10px;"><a class="text-link" href="{{ index . "back_url" }}">{{ index . "back_text" }}</a></p>
{{ end }}
{{ if index . "links_html" }}
{{ index . "links_html" }}
{{ end }}
</header>
{{ end }}
{{ define "demo_app_shell" }}
<header class="app-shell" id="app-shell">
<div class="app-shell-inner">
<div>
<p class="app-shell-kicker">Demo Application Shell</p>
<h1 class="app-shell-title">Universal Operations Console</h1>
<p class="app-shell-subtitle">Reference shell for server-rendered Go web apps using shared design-code contracts.</p>
</div>
<div class="app-shell-statuses" aria-label="Application status">
<span class="shell-pill shell-pill-env">ENV: demo</span>
<span class="shell-pill shell-pill-db">DB: connected</span>
<span class="shell-pill shell-pill-user">operator@demo · admin</span>
</div>
</div>
</header>
{{ end }}
{{ define "base.html" }}
{{ template "demo_doc_start" . }}
<header class="masthead" id="home-overview">
<p class="label">Submodule-First Design Kit</p>
<h1>{{ .Title }}</h1>
<p class="lead">A universal design-code workspace for Go web applications: reusable rules, demo-first UI patterns, and canonical development contracts for AI-assisted implementation.</p>
<div class="button-demo-row" style="margin-top:12px;">
{{ range .Patterns }}
{{ if .Link }}
<a class="chip-link" href="{{ .Link }}">{{ .Name }}</a>
{{ end }}
{{ end }}
</div>
</header>
<section class="panel" id="home-design-approach">
<div class="panel-head"><h2>Design Approach</h2></div>
<div class="grid">
<article class="card">
<h3>Contract First</h3>
<p>UI behavior is defined as explicit contracts (filters, pagination, modal steps, timeline grouping) before implementation details. Demo pages act as executable specs.</p>
</article>
<article class="card">
<h3>Server-Rendered by Default</h3>
<p>Patterns target Go server-rendered apps first (net/http or Gin with templates). Interactivity is additive and must preserve deterministic URL/query contracts.</p>
</article>
<article class="card">
<h3>Reusable, Not Branded</h3>
<p>Shared patterns standardize behavior and structure while leaving visual branding, domain terminology, and business logic to each host project.</p>
</article>
</div>
</section>
<section class="panel" id="home-workflow">
<div class="panel-head"><h2>Development Workflow Standard</h2></div>
<div class="timeline-cards">
<article class="timeline-card">
<div class="timeline-card-top"><h3>1. Describe the contract</h3></div>
<p class="meta">Update Bible + pattern contract first: routes, query params, states, edge cases, and UI semantics.</p>
</article>
<article class="timeline-card">
<div class="timeline-card-top"><h3>2. Implement in demo</h3></div>
<p class="meta">Build the pattern in the demo app as a live reference page with realistic state transitions and test coverage.</p>
</article>
<article class="timeline-card">
<div class="timeline-card-top"><h3>3. Publish as bundle</h3></div>
<p class="meta">Encode reusable docs and templates in the design kit and expose them as bundles for host repositories.</p>
</article>
<article class="timeline-card">
<div class="timeline-card-top"><h3>4. Apply in host repos</h3></div>
<p class="meta">Use the sync workflow to plan and apply changes, then adapt domain-specific rules without breaking canonical UI contracts.</p>
</article>
</div>
</section>
<section class="panel" id="home-standardizes">
<div class="panel-head"><h2>What the Demo Standardizes</h2></div>
<div class="chip-row">
<span class="chip">URL-driven filters</span>
<span class="chip">Server-side pagination</span>
<span class="chip">Bulk selection semantics</span>
<span class="chip">Modal state machines</span>
<span class="chip">Import preview / confirm</span>
<span class="chip">CSV export behavior</span>
<span class="chip">Operator tooling dashboards</span>
<span class="chip">Timeline card grouping</span>
<span class="chip">Drilldown UX</span>
</div>
</section>
<section class="panel" id="home-anti-patterns">
<div class="panel-head"><h2>Anti-Patterns (Do Not Implement)</h2></div>
<div class="grid">
<article class="card">
<h3>Page-local filters on paginated tables</h3>
<p>Do not filter only the currently rendered page slice. Filters must apply to the full dataset/query scope before pagination.</p>
</article>
<article class="card">
<h3>Nested modals</h3>
<p>Do not open one modal from another modal. Use a single modal state machine with explicit stages (edit, confirm, done).</p>
</article>
<article class="card">
<h3>Implicit export scope</h3>
<p>Do not export without clear scope selection when ambiguity exists (selected, filtered, all). Make the scope explicit in UI and request.</p>
</article>
<article class="card">
<h3>Undocumented UI contracts</h3>
<p>Do not implement new interaction behavior without updating the design code (Bible, pattern contract, and demo reference page).</p>
</article>
</div>
</section>
<section class="panel" id="home-bundles">
<div class="panel-head">
<h2>Bundles</h2>
<a href="/healthz" class="pill">healthz</a>
</div>
<div class="grid">
{{ range .Bundles }}
<article class="card">
<div class="row">
<h3>{{ .Name }}</h3>
<span class="status status-{{ .Status }}">{{ .Status }}</span>
</div>
<p>{{ .Summary }}</p>
<p class="meta"><code>{{ .Bundle }}</code></p>
{{ if .Link }}
<p class="meta"><a class="text-link" href="{{ .Link }}">Open demo</a></p>
{{ end }}
</article>
{{ end }}
</div>
</section>
<section class="panel" id="home-roadmap">
<div class="panel-head">
<h2>Pattern Roadmap</h2>
</div>
<div class="grid">
{{ range .Patterns }}
<article class="card">
<div class="row">
<h3>{{ .Name }}</h3>
<span class="status status-{{ .Status }}">{{ .Status }}</span>
</div>
<p>{{ .Summary }}</p>
<p class="meta"><code>{{ .Bundle }}</code></p>
</article>
{{ end }}
</div>
</section>
{{ template "demo_doc_end" . }}
{{ end }}
{{ define "demo_nav" }}
<nav class="demo-topnav" aria-label="Demo navigation">
<div class="demo-topnav-inner">
<a class="demo-topnav-brand {{ if eq .CurrentPath "/" }}active{{ end }}" href="/">Demo Catalog</a>
<a class="demo-topnav-link {{ if eq .CurrentPath "/patterns/table" }}active{{ end }}" href="/patterns/table">Table</a>
<a class="demo-topnav-link {{ if eq .CurrentPath "/patterns/controls" }}active{{ end }}" href="/patterns/controls">Controls</a>
<a class="demo-topnav-link {{ if eq .CurrentPath "/patterns/modals" }}active{{ end }}" href="/patterns/modals">Modals</a>
<a class="demo-topnav-link {{ if eq .CurrentPath "/patterns/io" }}active{{ end }}" href="/patterns/io">Import/Export</a>
<a class="demo-topnav-link {{ if eq .CurrentPath "/patterns/forms" }}active{{ end }}" href="/patterns/forms">Forms</a>
<a class="demo-topnav-link {{ if eq .CurrentPath "/patterns/operator-tools" }}active{{ end }}" href="/patterns/operator-tools">Operator Tools</a>
<a class="demo-topnav-link {{ if eq .CurrentPath "/patterns/timeline" }}active{{ end }}" href="/patterns/timeline">Timeline</a>
</div>
</nav>
{{ end }}
{{ define "table_pattern.html" }}
{{ template "demo_doc_start" . }}
{{ template "demo_masthead" (dict "label" "Pattern Demo" "title" .Title "lead" "Server-side filtering and pagination. Filters apply to the full dataset before pagination." "back_url" "/" "back_text" "← Back to catalog") }}
<section class="panel panel-composite" id="table-module">
<div class="panel-subsection" id="table-filters">
<div class="panel-head">
<h2>Filters</h2>
<div class="button-demo-row" style="margin-top:0;">
<a href="/patterns/table#table-filters" class="btn btn-ghost btn-pair">Reset</a>
<button class="btn btn-primary btn-pair" form="table-filters-form" type="submit">Apply</button>
</div>
</div>
<form id="table-filters-form" class="filters" method="get" action="/patterns/table#table-filters">
<label>
Search
<input type="text" name="q" value="{{ .Filters.Query }}" placeholder="name / owner / status" />
</label>
<label>
Category
<select name="category">
<option value="">All</option>
{{ range .Categories }}
<option value="{{ . }}" {{ if eq $.Filters.Category . }}selected{{ end }}>{{ . }}</option>
{{ end }}
</select>
</label>
<label>
Status
<select name="status">
<option value="">All</option>
{{ range .Statuses }}
<option value="{{ . }}" {{ if eq $.Filters.Status . }}selected{{ end }}>{{ . }}</option>
{{ end }}
</select>
</label>
<label>
Rows per page
<select name="per_page">
{{ range .PerPageOpts }}
<option value="{{ . }}" {{ if eq $.PerPage . }}selected{{ end }}>{{ . }}</option>
{{ end }}
</select>
</label>
</form>
</div>
<div class="panel-subsection panel-subsection-divider" id="table-list">
<div class="panel-head">
<h2>Canonical List Page</h2>
<div class="meta">
{{ if gt .Pager.TotalItems 0 }}
Showing {{ .Pager.From }}{{ .Pager.To }} of {{ .Pager.TotalItems }}
{{ else }}
Showing 0 of 0
{{ end }}
</div>
</div>
<div class="table-wrap">
<table class="ui-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Category</th>
<th class="status-col">Status</th>
<th>Owner</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
{{ if .Rows }}
{{ range .Rows }}
<tr>
<td>{{ .ID }}</td>
<td>{{ .Name }}</td>
<td>{{ .Category }}</td>
<td class="status-col"><span class="status status-{{ .Status }}">{{ .Status }}</span></td>
<td>{{ .Owner }}</td>
<td>{{ .Updated }}</td>
</tr>
{{ end }}
{{ else }}
<tr>
<td colspan="6" class="empty-cell">No rows match current filters.</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ if gt .Pager.TotalPages 1 }}
<nav class="pager pager-dots" aria-label="Pagination">
{{ range .Pager.Links }}
{{ if .Ellipsis }}
<span class="ellipsis" aria-hidden="true"></span>
{{ else if .Current }}
<a class="current" aria-current="page" href="{{ .URL }}#table-list" aria-label="Page {{ .Label }}, current">{{ .Label }}</a>
{{ else }}
<a href="{{ .URL }}#table-list" aria-label="Go to page {{ .Label }}">{{ .Label }}</a>
{{ end }}
{{ end }}
</nav>
{{ end }}
</div>
</section>
{{ template "demo_doc_end" . }}
{{ end }}

View File

@@ -0,0 +1,114 @@
{{ define "controls_pattern.html" }}
{{ template "demo_doc_start" . }}
{{ template "demo_masthead" (dict "label" "Pattern Demo" "title" .Title "lead" "Canonical actions, segmented filters, row selection and bulk-action control bar." "back_url" "/" "back_text" "← Back to catalog") }}
<section class="panel" id="controls-buttons">
<div class="panel-head"><h2>Buttons</h2></div>
<div class="button-demo-row">
<button class="btn btn-primary" type="button">Apply changes</button>
<button class="btn btn-secondary" type="button">Review selection</button>
<button class="btn btn-danger" type="button">Archive selected</button>
<button class="btn btn-ghost" type="button">Reset filters</button>
<button class="btn" type="button" disabled>Disabled</button>
{{ if .SimulateLoading }}
<a class="btn btn-secondary is-loading" aria-disabled="true" href="{{ .ClearLoadingURL }}">Loading…</a>
{{ else }}
<a class="btn btn-secondary" href="{{ .SimulateLoadingURL }}">Simulate loading</a>
{{ end }}
</div>
<div class="button-demo-row">
<a class="btn btn-primary" href="/patterns/io?export_ready=1">Export</a>
<a class="btn btn-secondary" href="/patterns/io">Import</a>
<a class="btn btn-ghost" href="/patterns/modals?open=edit&stage=edit#modal-open-states">Open modal</a>
</div>
</section>
<section class="panel panel-composite" id="controls-workbench">
<div class="panel-subsection" id="controls-segments">
<div class="panel-head">
<h2>Segmented Status Filter</h2>
<div class="meta">{{ .Pager.TotalItems }} filtered · page {{ .Pager.Page }}/{{ .Pager.TotalPages }} · {{ .SelectedCount }} selected</div>
</div>
<div class="segmented">
<a class="segment {{ if eq .Segment "all" }}active{{ end }}" href="{{ index .SegmentURLs "all" }}">All ({{ index .SegmentedCounts "all" }})</a>
<a class="segment {{ if eq .Segment "ready" }}active{{ end }}" href="{{ index .SegmentURLs "ready" }}">Ready ({{ index .SegmentedCounts "ready" }})</a>
<a class="segment {{ if eq .Segment "warning" }}active{{ end }}" href="{{ index .SegmentURLs "warning" }}">Warning ({{ index .SegmentedCounts "warning" }})</a>
<a class="segment {{ if eq .Segment "review" }}active{{ end }}" href="{{ index .SegmentURLs "review" }}">Review ({{ index .SegmentedCounts "review" }})</a>
</div>
</div>
<div class="panel-subsection panel-subsection-divider" id="controls-selection">
<div class="panel-head">
<h2>Selection Table</h2>
<div class="meta">Checkbox rows + bulk action preview + global selection across pages</div>
</div>
{{ if .ActionMessage }}<div class="notice">{{ .ActionMessage }}</div>{{ end }}
<p class="meta" style="margin-bottom:12px;">
Visible on this page: {{ .VisibleCount }} · Selected on this page: {{ .SelectedVisible }}{{ if gt .SelectedHidden 0 }} · Selected on other page(s): {{ .SelectedHidden }}{{ end }}
</p>
<div class="bulk-bar">
<a class="btn btn-secondary" href="{{ .SelectVisibleURL }}">Select visible</a>
<a class="btn btn-secondary" href="{{ .SelectFilteredURL }}">Select filtered</a>
<a class="btn btn-secondary" href="{{ .ClearVisibleURL }}">Clear visible</a>
<a class="btn btn-secondary" href="{{ .ClearFilteredURL }}">Clear filtered</a>
<a class="btn btn-primary" href="{{ .OpenEditSelectedURL }}">Edit selected</a>
<a class="btn btn-secondary" href="{{ .OpenDeleteSelectedURL }}">Remove selected</a>
<a class="btn btn-ghost" href="{{ .ClearSelectionURL }}">Clear selection</a>
<details class="inline-menu">
<summary class="btn btn-secondary">More actions</summary>
<div class="inline-menu-list">
<a class="menu-item" href="{{ .BulkReviewURL }}">Mark for review</a>
<a class="menu-item" href="{{ .BulkExportURL }}">Export selected</a>
<a class="menu-item" href="{{ .BulkRetrySyncURL }}">Retry sync</a>
<a class="menu-item danger" href="{{ .BulkArchiveURL }}">Archive</a>
</div>
</details>
</div>
<div class="table-wrap">
<table class="ui-table">
<thead>
<tr>
<th>Select</th>
<th>Name</th>
<th>Category</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{ range .Rows }}
<tr>
<td><a class="check-toggle {{ if .Selected }}checked{{ end }}" href="{{ .ToggleURL }}" aria-label="Toggle row {{ .ID }}">{{ if .Selected }}☑{{ else }}☐{{ end }}</a></td>
<td>{{ .Name }}</td>
<td>{{ .Type }}</td>
<td><span class="status status-{{ .Status }}">{{ .Status }}</span></td>
<td class="action-cell">
<a class="text-link" href="{{ .EditURL }}">Edit</a>
<a class="text-link" href="{{ .RemoveURL }}">Remove</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ if gt .Pager.TotalPages 1 }}
<nav class="pager pager-dots" aria-label="Pagination">
{{ range .Pager.Links }}
{{ if .Ellipsis }}
<span class="ellipsis" aria-hidden="true"></span>
{{ else if .Current }}
<a class="current" aria-current="page" href="{{ .URL }}" aria-label="Page {{ .Label }}, current">{{ .Label }}</a>
{{ else }}
<a href="{{ .URL }}" aria-label="Go to page {{ .Label }}">{{ .Label }}</a>
{{ end }}
{{ end }}
</nav>
{{ end }}
</div>
</section>
{{ template "demo_doc_end" . }}
{{ end }}

View File

@@ -0,0 +1,95 @@
{{ define "forms_pattern.html" }}
{{ template "demo_doc_start" . }}
{{ template "demo_masthead" (dict "label" "Pattern Demo" "title" .Title "lead" "Tabbed/step-based forms with datalist suggestions, inline validation, and explicit review/confirm workflow." "back_url" "/" "back_text" "← Back to catalog") }}
<section class="panel" id="forms-mode">
<div class="panel-head"><h2>Mode Switch (Tabs)</h2></div>
<div class="segmented">
<a class="segment {{ if eq .Mode "register" }}active{{ end }}" href="{{ index .ModeURLs "register" }}">Manual register</a>
<a class="segment {{ if eq .Mode "import" }}active{{ end }}" href="{{ index .ModeURLs "import" }}">Import-assisted</a>
</div>
<p class="meta" style="margin-top:10px;">Tabs preserve entered values while changing workflow mode.</p>
</section>
<section class="panel" id="forms-steps">
<div class="panel-head"><h2>Step Flow</h2></div>
<div class="segmented">
<a class="segment {{ if eq .Step "edit" }}active{{ end }}" href="{{ index .StepURLs "edit" }}">Edit</a>
<a class="segment {{ if eq .Step "review" }}active{{ end }}" href="{{ index .StepURLs "review" }}">Review</a>
<a class="segment {{ if eq .Step "confirm" }}active{{ end }}" href="{{ index .StepURLs "confirm" }}">Done</a>
</div>
{{ if .Message }}<div class="notice" style="margin-top:12px;">{{ .Message }}</div>{{ end }}
</section>
<section class="panel" id="forms-contract">
<div class="panel-head"><h2>Form Contract Demo</h2></div>
<form class="forms-grid" method="get" action="/patterns/forms#forms-contract">
<input type="hidden" name="mode" value="{{ .Mode }}">
<input type="hidden" name="step" value="review">
<label>Server serial {{ if index .FieldErrors "server_serial" }}<span class="field-error">{{ index .FieldErrors "server_serial" }}</span>{{ end }}
<input class="{{ if index .FieldErrors "server_serial" }}input-error{{ end }}" name="server_serial" value="{{ .ServerSerial }}" list="forms-server-list" placeholder="SRV-001">
<datalist id="forms-server-list">{{ range .ServerOptions }}<option value="{{ . }}"></option>{{ end }}</datalist>
</label>
<label>Location / slot {{ if index .FieldErrors "location" }}<span class="field-error">{{ index .FieldErrors "location" }}</span>{{ end }}
<input class="{{ if index .FieldErrors "location" }}input-error{{ end }}" name="location" value="{{ .Location }}" list="forms-location-list" placeholder="AOC#1">
<datalist id="forms-location-list">{{ range .LocationOptions }}<option value="{{ . }}"></option>{{ end }}</datalist>
</label>
<label>Component serial {{ if index .FieldErrors "component_serial" }}<span class="field-error">{{ index .FieldErrors "component_serial" }}</span>{{ end }}
<input class="{{ if index .FieldErrors "component_serial" }}input-error{{ end }}" name="component_serial" value="{{ .ComponentSerial }}" list="forms-component-list" placeholder="NIC-AX210-001">
<datalist id="forms-component-list">{{ range .ComponentOptions }}<option value="{{ . }}"></option>{{ end }}</datalist>
</label>
<label>Event date {{ if index .FieldErrors "event_date" }}<span class="field-error">{{ index .FieldErrors "event_date" }}</span>{{ end }}
<input class="{{ if index .FieldErrors "event_date" }}input-error{{ end }}" type="date" name="event_date" value="{{ .EventDate }}">
</label>
<label class="full-row">Details
<textarea name="details" rows="3" placeholder="Optional human-readable note">{{ .Details }}</textarea>
</label>
<label class="full-row">Import file (demo control)
<input type="file" name="upload" accept=".csv,text/csv,application/json">
<span class="meta">UI-only demo control: actual upload/parse flow is demonstrated in Import/Export pattern.</span>
</label>
<label class="checkbox-row full-row"><input type="checkbox" checked> Show suggestions from full query scope (not current page only)</label>
<label class="checkbox-row full-row"><input type="checkbox"> Allow force action on ambiguous match (requires explicit review)</label>
<div class="button-demo-row full-row">
<button class="btn btn-primary" type="submit">Review</button>
<a class="btn btn-secondary" href="{{ index .StepURLs "edit" }}">Stay in edit</a>
<a class="btn btn-ghost" href="/patterns/forms#forms-contract">Reset</a>
</div>
</form>
</section>
{{ if or (eq .Step "review") (eq .Step "confirm") }}
<section class="panel" id="forms-review">
<div class="panel-head"><h2>{{ if eq .Step "confirm" }}Result Summary{{ else }}Review Summary{{ end }}</h2></div>
<div class="table-wrap">
<table class="ui-table">
<tbody>
<tr><th style="width:30%;">Mode</th><td>{{ .Mode }}</td></tr>
<tr><th>Server serial</th><td>{{ if .ServerSerial }}{{ .ServerSerial }}{{ else }}<span class="meta"></span>{{ end }}</td></tr>
<tr><th>Location</th><td>{{ if .Location }}{{ .Location }}{{ else }}<span class="meta"></span>{{ end }}</td></tr>
<tr><th>Component serial</th><td>{{ if .ComponentSerial }}{{ .ComponentSerial }}{{ else }}<span class="meta"></span>{{ end }}</td></tr>
<tr><th>Date</th><td>{{ .EventDate }}</td></tr>
<tr><th>Details</th><td>{{ if .Details }}{{ .Details }}{{ else }}<span class="meta">No note</span>{{ end }}</td></tr>
</tbody>
</table>
</div>
<div class="button-demo-row" style="margin-top:12px;">
{{ if eq .Step "review" }}
<a class="btn btn-secondary" href="{{ index .StepURLs "edit" }}">Back to edit</a>
<a class="btn btn-primary" href="{{ index .StepURLs "confirm" }}">Confirm</a>
{{ else }}
<a class="btn btn-primary" href="{{ index .StepURLs "edit" }}">Start new</a>
{{ end }}
</div>
</section>
{{ end }}
{{ template "demo_doc_end" . }}
{{ end }}

View File

@@ -0,0 +1,99 @@
{{ define "io_pattern.html" }}
{{ template "demo_doc_start" . }}
{{ template "demo_masthead" (dict "label" "Pattern Demo" "title" .Title "lead" "Canonical file transfer UX: import preview/confirm and export with explicit scope/format selection." "back_url" "/" "back_text" "← Back to catalog") }}
<section class="panel" id="io-import">
<div class="panel-head"><h2>Import Workflow</h2></div>
<div class="notice">{{ .ImportMessage }}</div>
<form class="filters" method="get" action="/patterns/io#io-import">
<input type="hidden" name="import_mode" value="{{ .ImportMode }}">
<label>Source file
<input type="text" name="file" value="{{ .FileName }}" placeholder="items.csv">
</label>
<label>Step
<select name="import_mode">
<option value="preview" {{ if eq .ImportMode "preview" }}selected{{ end }}>Preview</option>
<option value="confirm" {{ if eq .ImportMode "confirm" }}selected{{ end }}>Confirm</option>
</select>
</label>
<label>Validation profile
<select>
<option selected>strict</option>
<option>lenient</option>
</select>
</label>
<div class="filter-actions">
<button class="btn btn-primary btn-pair" type="submit">Render state</button>
</div>
</form>
<div class="table-wrap" style="margin-top:12px;">
<table class="ui-table">
<thead>
<tr>
<th>Row</th>
<th>Code</th>
<th>Name</th>
<th>Qty</th>
<th>Validation</th>
</tr>
</thead>
<tbody>
{{ range .PreviewRows }}
<tr>
<td>{{ .RowNo }}</td>
<td>{{ .ItemCode }}</td>
<td>{{ .Name }}</td>
<td>{{ .Qty }}</td>
<td><span class="status status-{{ if eq .Status "error" }}warning{{ else }}ready{{ end }}">{{ .Status }}</span></td>
</tr>
{{ end }}
</tbody>
</table>
</div>
<div class="button-demo-row" style="margin-top:12px;">
{{ if eq .ImportMode "preview" }}
<a class="btn btn-primary" href="/patterns/io?import_mode=confirm&file={{ .FileName }}#io-import">Review Import</a>
{{ else }}
<a class="btn btn-secondary" href="/patterns/io?import_mode=preview&file={{ .FileName }}#io-import">Back to preview</a>
<a class="btn btn-primary" href="/patterns/io?import_mode=preview&file={{ .FileName }}#io-import">Confirm & Import (demo)</a>
{{ end }}
</div>
</section>
<section class="panel" id="io-export">
<div class="panel-head"><h2>Export Workflow</h2></div>
<div class="notice">{{ .ExportMessage }}</div>
<form class="filters" method="get" action="/patterns/io#io-export">
<input type="hidden" name="export_ready" value="1">
<label>Format
<select name="format">
<option value="csv" {{ if eq .ExportFormat "csv" }}selected{{ end }}>CSV</option>
<option value="json" {{ if eq .ExportFormat "json" }}selected{{ end }}>JSON (planned)</option>
</select>
</label>
<label>Scope
<select name="scope">
<option value="filtered" {{ if eq .ExportScope "filtered" }}selected{{ end }}>Filtered rows</option>
<option value="selected" {{ if eq .ExportScope "selected" }}selected{{ end }}>Selected rows</option>
<option value="all" {{ if eq .ExportScope "all" }}selected{{ end }}>All rows</option>
</select>
</label>
<label>Include headers
<select>
<option selected>Yes</option>
<option>No</option>
</select>
</label>
<div class="filter-actions">
<button class="btn btn-primary btn-pair" type="submit">Prepare export</button>
</div>
</form>
<div class="button-demo-row" style="margin-top:12px;">
<a class="btn btn-primary" href="/patterns/io/export.csv?scope={{ .ExportScope }}">Download CSV</a>
<a class="btn btn-ghost" href="/patterns/io#io-export">Reset</a>
</div>
<p class="meta" style="margin-top:10px;">Demo export endpoint includes UTF-8 BOM and semicolon delimiter to illustrate spreadsheet compatibility patterns.</p>
</section>
{{ template "demo_doc_end" . }}
{{ end }}

View File

@@ -0,0 +1,74 @@
{{ define "modal_pattern.html" }}
{{ template "demo_doc_start" . }}
{{ template "demo_masthead" (dict "label" "Pattern Demo" "title" .Title "lead" "Single-modal workflow progression: edit → confirm → complete, with explicit cancel/close paths." "back_url" "/" "back_text" "← Back to catalog") }}
<section class="panel" id="modal-open-states">
<div class="panel-head"><h2>Open States</h2></div>
<div class="button-demo-row">
<a class="btn btn-primary" href="/patterns/modals?open=edit&stage=edit&sel=1&sel=3#modal-open-states">Open Edit Modal</a>
<a class="btn btn-secondary" href="/patterns/modals?open=delete&stage=confirm&sel=2#modal-open-states">Open Confirm Modal</a>
<a class="btn btn-ghost" href="/patterns/modals#modal-open-states">No modal open</a>
</div>
<div class="notice">{{ .Message }}</div>
</section>
<section class="panel" id="modal-context">
<div class="panel-head"><h2>Page Context</h2></div>
<div class="card">
<div class="row"><h3>Selected items</h3><span class="status status-{{ if .SelectedIDs }}ready{{ else }}review{{ end }}">{{ if .SelectedIDs }}{{ len .SelectedIDs }} selected{{ else }}none{{ end }}</span></div>
<p class="meta">{{ if .SelectedIDs }}IDs: {{ range $i, $id := .SelectedIDs }}{{ if $i }}, {{ end }}{{ $id }}{{ end }}{{ else }}Select rows on the controls page to carry context into modal demos.{{ end }}</p>
</div>
</section>
{{ if .Open }}
<div class="demo-modal-backdrop">
<section class="demo-modal">
<div class="demo-modal-titlebar">
<a class="demo-modal-close-dot" href="/patterns/modals#modal-open-states" aria-label="Close modal"></a>
<div class="demo-modal-title">{{ if eq .Open "delete" }}Confirm Destructive Action{{ else }}Edit Items{{ end }}</div>
</div>
<div class="demo-modal-body">
<p class="meta">{{ .Message }}</p>
{{ if eq .Stage "edit" }}
<div class="modal-grid">
<label>Display name <input type="text" value="Example group change"></label>
<label>Status after action
<select>
<option>ready</option>
<option selected>review</option>
<option>warning</option>
</select>
</label>
<label class="checkbox-row"><input type="checkbox" checked> Apply to all selected items</label>
<label class="checkbox-row"><input type="checkbox"> Clear optional field values</label>
</div>
<div class="button-demo-row">
<a class="btn btn-secondary" href="/patterns/modals#modal-open-states">Cancel</a>
<a class="btn btn-primary" href="/patterns/modals?open={{ .Open }}&stage=confirm{{ range .SelectedIDs }}&sel={{ . }}{{ end }}#modal-open-states">Review Changes</a>
</div>
{{ else if eq .Stage "confirm" }}
<div class="card">
<h3 style="margin:0;">Confirmation Summary</h3>
<ul>
<li>Selected rows are listed and reviewed before submit.</li>
<li>Destructive actions require explicit confirmation wording.</li>
<li>No nested modals; stay within one modal state machine.</li>
</ul>
</div>
<div class="button-demo-row">
<a class="btn btn-secondary" href="/patterns/modals?open={{ .Open }}&stage=edit{{ range .SelectedIDs }}&sel={{ . }}{{ end }}#modal-open-states">Back</a>
<a class="btn {{ if eq .Open "delete" }}btn-danger{{ else }}btn-primary{{ end }}" href="/patterns/modals?open={{ .Open }}&stage=done{{ range .SelectedIDs }}&sel={{ . }}{{ end }}#modal-open-states">Confirm</a>
</div>
{{ else }}
<div class="notice success">Action completed. Show human-readable summary and next actions.</div>
<div class="button-demo-row">
<a class="btn btn-primary" href="/patterns/modals#modal-open-states">Done</a>
</div>
{{ end }}
</div>
</section>
</div>
{{ end }}
{{ template "demo_doc_end" . }}
{{ end }}

View File

@@ -0,0 +1,114 @@
{{ define "operator_tools_pattern.html" }}
{{ template "demo_doc_start" . }}
{{ template "demo_masthead" (dict "label" "Pattern Demo" "title" .Title "lead" "Universal operator/admin dashboard pattern: tool queue, batch actions, safety checks, import/export shortcuts, and explicit confirmation paths." "back_url" "/" "back_text" "← Back to catalog") }}
<section class="panel panel-composite" id="operator-workbench">
<div class="panel-subsection" id="operator-filters">
<div class="panel-head">
<h2>Tool Scope Tabs</h2>
<div class="meta">{{ .VisibleCount }} visible jobs · {{ .SelectedCount }} selected</div>
</div>
<div class="segmented">
<a class="segment {{ if eq .Scope "assets" }}active{{ end }}" href="{{ index .ScopeURLs "assets" }}">Assets</a>
<a class="segment {{ if eq .Scope "components" }}active{{ end }}" href="{{ index .ScopeURLs "components" }}">Components</a>
<a class="segment {{ if eq .Scope "imports" }}active{{ end }}" href="{{ index .ScopeURLs "imports" }}">Imports</a>
<a class="segment {{ if eq .Scope "maintenance" }}active{{ end }}" href="{{ index .ScopeURLs "maintenance" }}">Maintenance</a>
<a class="segment {{ if eq .Scope "all" }}active{{ end }}" href="{{ index .ScopeURLs "all" }}">All</a>
</div>
<div class="segmented" style="margin-top:10px;">
<a class="segment {{ if eq .Queue "all" }}active{{ end }}" href="{{ index .QueueURLs "all" }}">All queue states</a>
<a class="segment {{ if eq .Queue "queued" }}active{{ end }}" href="{{ index .QueueURLs "queued" }}">Queued</a>
<a class="segment {{ if eq .Queue "running" }}active{{ end }}" href="{{ index .QueueURLs "running" }}">Running</a>
<a class="segment {{ if eq .Queue "failed" }}active{{ end }}" href="{{ index .QueueURLs "failed" }}">Failed</a>
<a class="segment {{ if eq .Queue "done" }}active{{ end }}" href="{{ index .QueueURLs "done" }}">Done</a>
</div>
</div>
<div class="panel-subsection panel-subsection-divider" id="operator-actions">
<div class="panel-head">
<h2>Operator Tooling Actions</h2>
<div class="meta">Batch controls must keep scope/filter context explicit</div>
</div>
{{ if .ActionMessage }}<div class="notice">{{ .ActionMessage }}</div>{{ end }}
<p class="meta" style="margin-bottom:12px;">
Selected on this view: {{ .SelectedVisible }}{{ if gt .SelectionOutside 0 }} · Selected outside current filter: {{ .SelectionOutside }}{{ end }}
</p>
<div class="bulk-bar">
<a class="btn btn-secondary" href="{{ .SelectVisibleURL }}">Select visible</a>
<a class="btn btn-secondary" href="{{ .ClearVisibleURL }}">Clear visible</a>
<a class="btn btn-ghost" href="{{ .ClearSelectionURL }}">Clear selection</a>
<a class="btn btn-primary" href="{{ .RunSelectedURL }}">Run selected</a>
<a class="btn btn-secondary" href="{{ .RetrySelectedURL }}">Retry selected</a>
<a class="btn btn-danger" href="{{ .CancelSelectedURL }}">Cancel selected</a>
<a class="btn btn-secondary" href="{{ .OpenReviewModalURL }}">Open confirm modal</a>
</div>
<div class="button-demo-row" style="margin-top:12px;">
<a class="btn btn-secondary" href="{{ .ImportPreviewURL }}">Import batch preview</a>
<a class="btn btn-secondary" href="{{ .ExportFilteredURL }}">Export filtered</a>
<a class="btn btn-secondary" href="{{ .ExportSelectedURL }}">Export selected</a>
</div>
</div>
<div class="panel-subsection panel-subsection-divider" id="operator-queue">
<div class="panel-head">
<h2>Operations Queue</h2>
<div class="meta">Complex dashboards may include multiple tables; standardize row actions and statuses first.</div>
</div>
<div class="table-wrap">
<table class="ui-table">
<thead>
<tr>
<th>Select</th>
<th>Job ID</th>
<th>Tool</th>
<th>Scope</th>
<th>Mode</th>
<th>Status</th>
<th>Owner</th>
<th>Started</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{ range .Rows }}
<tr>
<td><a class="check-toggle {{ if .Selected }}checked{{ end }}" href="{{ .ToggleURL }}" aria-label="Toggle {{ .ID }}">{{ if .Selected }}☑{{ else }}☐{{ end }}</a></td>
<td><code>{{ .ID }}</code></td>
<td>{{ .Tool }}</td>
<td>{{ .Scope }}</td>
<td>{{ .Mode }}</td>
<td><span class="status status-{{ .Status }}">{{ .Status }}</span></td>
<td>{{ .Owner }}</td>
<td>{{ .StartedAt }}</td>
<td class="action-cell">
<a class="text-link" href="{{ .RetryURL }}">Retry</a>
<a class="text-link" href="{{ .CancelURL }}">Cancel</a>
<a class="text-link" href="{{ .ExportURL }}">Export</a>
<a class="text-link" href="{{ .InspectURL }}">Inspect</a>
</td>
</tr>
{{ else }}
<tr><td colspan="9"><span class="meta">No queued items for this scope/filter combination.</span></td></tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</section>
<section class="panel" id="operator-safety">
<div class="panel-head"><h2>Safety Checklist</h2></div>
<ul class="meta">
{{ range .SafetyChecklist }}
<li>{{ . }}</li>
{{ end }}
</ul>
<div class="panel-head" style="margin-top:14px;"><h2>Recent Operator Notes</h2></div>
<ul class="meta">
{{ range .RecentActivityNotes }}
<li>{{ . }}</li>
{{ end }}
</ul>
</section>
{{ template "demo_doc_end" . }}
{{ end }}

View File

@@ -0,0 +1,177 @@
{{ define "style_playground_pattern.html" }}
{{ template "demo_doc_start" . }}
{{ template "demo_masthead" (dict "label" "Pattern Demo" "title" .Title "lead" "Experiment with visual directions on identical UI modules. Behavior contracts stay the same; only presentation changes." "back_url" "/" "back_text" "← Back to catalog") }}
<div class="style-playground {{ .StyleClass }}">
<section class="panel" id="style-presets">
<div class="panel-head">
<h2>Theme Presets</h2>
<div class="meta">Current: {{ .StyleLabel }}</div>
</div>
<div class="segmented status-filter-tabs status-filter-tabs-blue">
<a class="segment {{ if eq .Style "linen" }}active{{ end }}" href="{{ index .StyleURLs "linen" }}">Linen</a>
<a class="segment {{ if eq .Style "slate" }}active{{ end }}" href="{{ index .StyleURLs "slate" }}">Slate</a>
<a class="segment {{ if eq .Style "signal" }}active{{ end }}" href="{{ index .StyleURLs "signal" }}">Signal</a>
<a class="segment {{ if eq .Style "y2k-silver" }}active{{ end }}" href="{{ index .StyleURLs "y2k-silver" }}">Y2K Silver</a>
<a class="segment {{ if eq .Style "vaporwave-soft" }}active{{ end }}" href="{{ index .StyleURLs "vaporwave-soft" }}">Vapor Soft</a>
<a class="segment {{ if eq .Style "vaporwave-night" }}active{{ end }}" href="{{ index .StyleURLs "vaporwave-night" }}">Vapor Night</a>
<a class="segment {{ if eq .Style "aqua" }}active{{ end }}" href="{{ index .StyleURLs "aqua" }}">Aqua</a>
<a class="segment {{ if eq .Style "win9x" }}active{{ end }}" href="{{ index .StyleURLs "win9x" }}">Win9x</a>
</div>
<p class="meta" style="margin-top:10px;">Use this page to compare visual directions on identical UI modules. Behavior contracts stay the same; only presentation changes.</p>
</section>
<section class="panel" id="style-components">
<div class="panel-head">
<h2>Component Preview</h2>
<div class="button-demo-row" style="margin-top:0;">
{{ if .LoadingDemo }}
<a class="btn btn-ghost" href="{{ .ClearLoadingURL }}">Stop loading demo</a>
{{ else }}
<a class="btn btn-secondary" href="{{ .LoadingDemoURL }}">Simulate loading state</a>
{{ end }}
</div>
</div>
<div class="grid">
<article class="card">
<h3 style="margin-top:0;">Buttons</h3>
<div class="button-demo-row">
<button class="btn btn-primary" type="button">Apply</button>
<button class="btn btn-secondary" type="button">Review</button>
<button class="btn btn-ghost" type="button">Reset</button>
<button class="btn btn-danger" type="button">Archive</button>
{{ if .LoadingDemo }}
<a class="btn btn-secondary is-loading" aria-disabled="true" href="{{ .ClearLoadingURL }}">Loading…</a>
{{ else }}
<button class="btn" type="button" disabled>Disabled</button>
{{ end }}
</div>
</article>
<article class="card">
<h3 style="margin-top:0;">Status + Chips</h3>
<div class="chip-row">
<span class="chip">URL-driven state</span>
<span class="chip">Server-rendered</span>
<span class="chip">Anchor restore</span>
</div>
<div class="button-demo-row" style="margin-top:12px;">
<span class="status status-ready">ready</span>
<span class="status status-warning">warning</span>
<span class="status status-review">review</span>
<span class="status status-failed">failed</span>
</div>
</article>
</div>
</section>
<section class="panel" id="style-status-filter">
<div class="panel-head">
<h2>Segmented Status Filter</h2>
<div class="meta">12 filtered • page 1/3 • 0 selected</div>
</div>
<div class="status-filter-shell">
<div class="segmented status-filter-tabs status-filter-tabs-dark" role="tablist" aria-label="Status filter">
<button class="segment active" type="button" role="tab" aria-selected="true">All (12)</button>
<button class="segment" type="button" role="tab" aria-selected="false">Ready (4)</button>
<button class="segment" type="button" role="tab" aria-selected="false">Warning (4)</button>
<button class="segment" type="button" role="tab" aria-selected="false">Review (4)</button>
</div>
</div>
</section>
<section class="panel" id="style-filters">
<div class="panel-head">
<h2>Compact Filter Module</h2>
<div class="button-demo-row" style="margin-top:0;">
<button class="btn btn-primary btn-pair" type="button">Apply</button>
<button class="btn btn-ghost btn-pair" type="button">Reset</button>
</div>
</div>
<form class="filters" action="/patterns/style-playground#style-filters" method="get">
<input type="hidden" name="style" value="{{ .Style }}">
<label>
Search
<input type="text" name="q" value="rack / owner / status">
</label>
<label>
Category
<select name="category">
<option selected>All</option>
<option>Compute</option>
<option>Networking</option>
</select>
</label>
<label>
Status
<select name="status">
<option selected>All</option>
<option>ready</option>
<option>warning</option>
<option>review</option>
</select>
</label>
<label>
Rows per page
<select name="per_page">
<option>5</option>
<option selected>10</option>
<option>20</option>
</select>
</label>
</form>
</section>
<section class="panel" id="style-surface">
<div class="panel-head">
<h2>Surface + Table Readability</h2>
<div class="meta">Same content under a different visual direction</div>
</div>
<div class="table-wrap">
<table class="ui-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Category</th>
<th class="status-col">Status</th>
<th>Owner</th>
</tr>
</thead>
<tbody>
<tr>
<td>12</td>
<td>Rack Controller Alpha</td>
<td>Compute</td>
<td class="status-col"><span class="status status-ready">ready</span></td>
<td>Ops</td>
</tr>
<tr>
<td>13</td>
<td>Patch Panel Group</td>
<td>Networking</td>
<td class="status-col"><span class="status status-warning">warning</span></td>
<td>Infra</td>
</tr>
<tr>
<td>14</td>
<td>Mapping Repair Queue</td>
<td>Storage</td>
<td class="status-col"><span class="status status-review">review</span></td>
<td>QA</td>
</tr>
</tbody>
</table>
</div>
<div class="pager pager-dots" style="margin-top:12px;" aria-label="Pagination preview">
<a class="current" aria-current="page" href="{{ index .StyleURLs .Style }}#style-surface" aria-label="Page 1, current">1</a>
<a href="{{ index .StyleURLs .Style }}#style-surface" aria-label="Go to page 2">2</a>
<span class="ellipsis" aria-hidden="true"></span>
<a href="{{ index .StyleURLs .Style }}#style-surface" aria-label="Go to page 7">7</a>
<a href="{{ index .StyleURLs .Style }}#style-surface" aria-label="Go to page 8">8</a>
</div>
</section>
</div>
{{ template "demo_doc_end" . }}
{{ end }}

View File

@@ -0,0 +1,118 @@
{{ define "timeline_pattern.html" }}
{{ template "demo_doc_start" . }}
{{ template "demo_masthead" (dict "label" "Pattern Demo" "title" .Title "lead" "Grouped timeline cards by day with source/action filters and single drilldown modal." "back_url" "/" "back_text" "← Back to catalog") }}
<section class="panel panel-composite" id="timeline-module">
<div class="panel-subsection" id="timeline-filters">
<div class="panel-head"><h2>Timeline Filters</h2></div>
<div class="segmented" style="margin-bottom:10px;">
<a class="segment {{ if eq .ActionFilter "" }}active{{ end }}" href="{{ index .ActionURLs "" }}">All actions</a>
<a class="segment {{ if eq .ActionFilter "installation" }}active{{ end }}" href="{{ index .ActionURLs "installation" }}">Installation</a>
<a class="segment {{ if eq .ActionFilter "removal" }}active{{ end }}" href="{{ index .ActionURLs "removal" }}">Removal</a>
<a class="segment {{ if eq .ActionFilter "edit" }}active{{ end }}" href="{{ index .ActionURLs "edit" }}">Edit</a>
</div>
<div class="segmented">
<a class="segment {{ if eq .SourceFilter "" }}active{{ end }}" href="{{ index .SourceURLs "" }}">All sources</a>
<a class="segment {{ if eq .SourceFilter "manual" }}active{{ end }}" href="{{ index .SourceURLs "manual" }}">Manual</a>
<a class="segment {{ if eq .SourceFilter "ingest" }}active{{ end }}" href="{{ index .SourceURLs "ingest" }}">Ingest</a>
<a class="segment {{ if eq .SourceFilter "system" }}active{{ end }}" href="{{ index .SourceURLs "system" }}">System</a>
</div>
</div>
<div class="panel-subsection panel-subsection-divider" id="timeline-cards">
<div class="panel-head"><h2>Grouped Cards</h2></div>
{{ if .Cards }}
<div class="timeline-cards">
{{ range .Cards }}
<article class="timeline-card">
<div class="timeline-card-top">
<div>
<p class="timeline-day">{{ .Day }}</p>
<h3>{{ .Title }}</h3>
</div>
<div class="timeline-meta">
<span class="status status-review">{{ .Action }}</span>
<span class="status status-{{ if eq .Source "manual" }}ready{{ else if eq .Source "ingest" }}warning{{ else }}review{{ end }}">{{ .Source }}</span>
</div>
</div>
<div class="timeline-summary-grid">
<div>
<div class="meta">Models / Types</div>
<div class="chip-row">{{ range .SummaryLeft }}<span class="chip">{{ . }}</span>{{ end }}</div>
</div>
<div>
<div class="meta">Slots / Scope</div>
<div class="chip-row">{{ range .SummaryRight }}<span class="chip">{{ . }}</span>{{ end }}</div>
</div>
</div>
<div class="timeline-card-actions">
<span class="meta">{{ .Count }} event(s)</span>
<a class="btn btn-secondary" href="{{ .OpenURL }}">Open details</a>
</div>
</article>
{{ end }}
</div>
{{ else }}
<div class="notice">No timeline cards match current filters.</div>
{{ end }}
</div>
</section>
{{ if .OpenCard }}
<div class="demo-modal-backdrop">
<section class="demo-modal timeline-modal">
<div class="demo-modal-titlebar">
<a class="demo-modal-close-dot" href="{{ .ClearCardURL }}" aria-label="Close modal"></a>
<div class="demo-modal-title">{{ .OpenCard.Title }}</div>
</div>
<div class="demo-modal-body">
<p class="meta">{{ .OpenCard.Day }} · {{ .OpenCard.Source }} · {{ .OpenCard.Action }}</p>
<form id="timeline-drilldown" class="filters" method="get" action="/patterns/timeline#timeline-drilldown" style="margin-top:12px;">
<input type="hidden" name="action" value="{{ .ActionFilter }}">
<input type="hidden" name="source" value="{{ .SourceFilter }}">
<input type="hidden" name="open" value="{{ .OpenCard.ID }}">
<label>Filter events in card
<input type="search" name="q" value="{{ .CardSearch }}" placeholder="serial / slot / model / source">
</label>
<div></div><div></div>
<div class="filter-actions">
<button class="btn btn-primary btn-pair" type="submit">Apply</button>
<a class="btn btn-ghost btn-pair" href="{{ .ClearCardURL }}">Reset</a>
</div>
</form>
<div class="timeline-drill-grid">
<div class="timeline-drill-list">
{{ if .OpenCard.Items }}
{{ range .OpenCard.Items }}
<a class="timeline-item {{ if and $.ActiveEvent (eq $.ActiveEvent.ID .ID) }}active{{ end }}" href="/patterns/timeline?action={{ $.ActionFilter }}&source={{ $.SourceFilter }}&open={{ $.OpenCard.ID }}{{ if $.CardSearch }}&q={{ $.CardSearch }}{{ end }}&event={{ .ID }}#timeline-drilldown">
<div class="timeline-item-title">{{ .Action }} · {{ .At }}</div>
<div class="timeline-item-meta">{{ .Entity }} · {{ .Target }}</div>
<div class="timeline-item-meta">{{ .Slot }} · {{ .Device }} · {{ .Source }}</div>
</a>
{{ end }}
{{ else }}
<div class="notice">No events match the card filter.</div>
{{ end }}
</div>
<div class="timeline-drill-detail">
{{ if .ActiveEvent }}
<h3 style="margin-top:0;">Event Detail</h3>
<div class="meta">When: {{ .ActiveEvent.At }}</div>
<div class="meta">Action: {{ .ActiveEvent.Action }}</div>
<div class="meta">Source: {{ .ActiveEvent.Source }}</div>
<div class="meta">Entity: {{ .ActiveEvent.Entity }}</div>
<div class="meta">Target: {{ .ActiveEvent.Target }}</div>
<div class="meta">Slot / Device: {{ .ActiveEvent.Slot }} · {{ .ActiveEvent.Device }}</div>
<div class="notice" style="margin-top:12px;">{{ .ActiveEvent.Detail }}</div>
{{ else }}
<div class="meta">Select an event to view details.</div>
{{ end }}
</div>
</div>
</div>
</section>
</div>
{{ end }}
{{ template "demo_doc_end" . }}
{{ end }}

View File

@@ -0,0 +1,14 @@
{
"id": "ai-rules",
"version": 1,
"description": "Canonical AI instruction templates and shared architecture doc policy notes.",
"conflict_policy": "merge-manual",
"variables": [
{ "name": "project_name", "required": false, "default": "Project" }
],
"entries": [
{ "from": "ai/claude/CLAUDE.template.md", "to": "CLAUDE.md", "template": true, "mode": "file" },
{ "from": "ai/codex/AGENTS.template.md", "to": "AGENTS.md", "template": true, "mode": "file" },
{ "from": "ai/shared/ARCH_DOC_POLICY.md", "to": "docs/ARCH_DOC_POLICY.md", "template": false, "mode": "file" }
]
}

View File

@@ -0,0 +1,9 @@
{
"id": "bible-core",
"version": 1,
"description": "Canonical Bible skeleton for Go web projects using AI coding agents.",
"conflict_policy": "merge-manual",
"entries": [
{ "from": "docs/bible-skeleton", "to": "bible", "mode": "dir" }
]
}

View File

@@ -0,0 +1,18 @@
{
"id": "go-web-skeleton",
"version": 1,
"description": "Minimal net/http + html/template web skeleton for Go projects.",
"conflict_policy": "merge-manual",
"variables": [
{ "name": "module_path", "required": true },
{ "name": "binary_name", "required": false, "default": "app" },
{ "name": "project_name", "required": false, "default": "Demo" }
],
"entries": [
{ "from": "scaffolds/go-nethttp-web/cmd/demo-server/main.go.tmpl", "to": "cmd/{{ .binary_name }}/main.go", "template": true, "mode": "file" },
{ "from": "scaffolds/go-nethttp-web/internal/web", "to": "internal/web", "mode": "dir" },
{ "from": "scaffolds/go-nethttp-web/web", "to": "web", "mode": "dir" },
{ "from": "scaffolds/go-nethttp-web/Makefile", "to": "Makefile", "mode": "file" },
{ "from": "scaffolds/go-nethttp-web/README.md", "to": "docs/ui-design-skeleton.md", "mode": "file" }
]
}

View File

@@ -0,0 +1,10 @@
{
"id": "ui-pattern-controls",
"version": 1,
"description": "Buttons, checkboxes, segmented filters, and bulk-selection control patterns.",
"conflict_policy": "merge-manual",
"entries": [
{ "from": "patterns/controls-selection", "to": "docs/ui-patterns/controls-selection", "mode": "dir" }
]
}

View File

@@ -0,0 +1,10 @@
{
"id": "ui-pattern-forms",
"version": 1,
"description": "Form validation, datalist suggestions, and review/confirm workflow patterns.",
"conflict_policy": "merge-manual",
"entries": [
{ "from": "patterns/forms-validation", "to": "docs/ui-patterns/forms-validation", "mode": "dir" }
]
}

View File

@@ -0,0 +1,10 @@
{
"id": "ui-pattern-io",
"version": 1,
"description": "Import/export workflow patterns for file upload preview and downloads.",
"conflict_policy": "merge-manual",
"entries": [
{ "from": "patterns/import-export", "to": "docs/ui-patterns/import-export", "mode": "dir" }
]
}

View File

@@ -0,0 +1,9 @@
{
"id": "ui-pattern-modal",
"version": 1,
"description": "Modal workflow pattern docs and starter templates.",
"conflict_policy": "merge-manual",
"entries": [
{ "from": "patterns/modal-workflows", "to": "docs/ui-patterns/modal-workflows", "mode": "dir" }
]
}

View File

@@ -0,0 +1,9 @@
{
"id": "ui-pattern-operator-tools",
"version": 1,
"description": "Universal operator/admin dashboard pattern: queue tables, batch actions, and safety guardrails.",
"conflict_policy": "merge-manual",
"entries": [
{ "from": "patterns/operator-tools", "to": "docs/ui-patterns/operator-tools", "mode": "dir" }
]
}

View File

@@ -0,0 +1,9 @@
{
"id": "ui-pattern-table",
"version": 1,
"description": "Table + server-side filter + pagination pattern docs and starter templates.",
"conflict_policy": "merge-manual",
"entries": [
{ "from": "patterns/table-pagination", "to": "docs/ui-patterns/table-pagination", "mode": "dir" }
]
}

View File

@@ -0,0 +1,10 @@
{
"id": "ui-pattern-timeline",
"version": 1,
"description": "Timeline cards with grouped summaries and drilldown modal patterns.",
"conflict_policy": "merge-manual",
"entries": [
{ "from": "patterns/timeline-cards", "to": "docs/ui-patterns/timeline-cards", "mode": "dir" }
]
}

View File

@@ -0,0 +1,9 @@
{
"id": "ui-theme-aqua-legacy",
"version": 1,
"description": "Legacy snapshot bundle of the Aqua-style visual exploration (reference only, not active baseline).",
"conflict_policy": "merge-manual",
"entries": [
{ "from": "patterns/theme-aqua-legacy", "to": "docs/ui-themes/aqua-legacy", "mode": "dir" }
]
}

15
exports/index.yaml Normal file
View File

@@ -0,0 +1,15 @@
{
"bundles": [
{ "id": "ai-rules", "manifest": "exports/bundles/ai-rules.yaml" },
{ "id": "bible-core", "manifest": "exports/bundles/bible-core.yaml" },
{ "id": "go-web-skeleton", "manifest": "exports/bundles/go-web-skeleton.yaml" },
{ "id": "ui-pattern-controls", "manifest": "exports/bundles/ui-pattern-controls.yaml" },
{ "id": "ui-pattern-forms", "manifest": "exports/bundles/ui-pattern-forms.yaml" },
{ "id": "ui-pattern-io", "manifest": "exports/bundles/ui-pattern-io.yaml" },
{ "id": "ui-pattern-operator-tools", "manifest": "exports/bundles/ui-pattern-operator-tools.yaml" },
{ "id": "ui-pattern-table", "manifest": "exports/bundles/ui-pattern-table.yaml" },
{ "id": "ui-pattern-modal", "manifest": "exports/bundles/ui-pattern-modal.yaml" },
{ "id": "ui-pattern-timeline", "manifest": "exports/bundles/ui-pattern-timeline.yaml" },
{ "id": "ui-theme-aqua-legacy", "manifest": "exports/bundles/ui-theme-aqua-legacy.yaml" }
]
}

View File

@@ -0,0 +1,39 @@
$schema: "informational"
title: UI Design Code Bundle Manifest (informal schema)
type: object
required:
- id
- version
- description
- entries
properties:
id:
type: string
version:
type: integer
description:
type: string
conflict_policy:
type: string
enum: [overwrite, skip, merge-manual]
variables:
type: array
items:
type: object
required: [name]
properties:
name: { type: string }
required: { type: boolean }
default: {}
entries:
type: array
items:
type: object
required: [from, to]
properties:
from: { type: string, description: "path under kit/" }
to: { type: string, description: "path under host repo root" }
template: { type: boolean }
mode: { type: string, enum: [file, dir] }
conflict_policy: { type: string, enum: [overwrite, skip, merge-manual] }

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module git.mchus.pro/mchus/ui-design-code
go 1.24.0

View File

@@ -0,0 +1,11 @@
# {{ .project_name }} — Instructions for Claude
Read and follow the project Bible before making any changes:
**[`bible/README.md`](bible/README.md)**
The Bible is the single source of truth for architecture, data models, API contracts, and UI
pattern conventions.
Every significant architectural decision must be recorded in the appropriate Bible decision log.

View File

@@ -0,0 +1,10 @@
# {{ .project_name }} — Agent Instructions
Read and follow `bible/README.md` as the architecture source of truth.
## Rules
- Keep architecture documentation in English.
- Update Bible docs in the same change set as architecture/code contract changes.
- Keep README and agent docs minimal; avoid duplicating Bible architecture content.

View File

@@ -0,0 +1,11 @@
# Architecture Documentation Policy
This project uses a Bible-style architecture documentation folder as the single source of truth.
## Mandatory Rules
- Record architecture decisions before or together with implementation.
- Use English for architecture documentation.
- Remove or update obsolete architectural guidance in the same change.
- Keep non-Bible docs concise and reference the Bible instead of duplicating it.

View File

@@ -0,0 +1,20 @@
# Project Bible
The Bible is the single source of truth for this project's architecture.
## Documentation Rules
- All architecture decisions must be recorded in this Bible.
- The documentation language is English only.
- If a decision is superseded, update the Bible immediately and remove or mark obsolete content.
- Do not duplicate architecture documentation in `README.md`, `CLAUDE.md`, or other files.
## Structure
- `governance/documentation-policy.md` - mandatory rules for maintaining architecture docs.
- `architecture/system-overview.md` - current scope, boundaries, and high-level system composition.
- `architecture/api-surface.md` - HTTP API and UI route contracts.
- `architecture/runtime-flows.md` - critical runtime behavior and failure invariants.
- `architecture/ui-information-architecture.md` - required UI structure and semantics.
- `decisions/README.md` - how to capture new and updated architecture decisions.

View File

@@ -0,0 +1,10 @@
# API Surface
Document all HTTP endpoints and UI routes that are part of the active contract.
## Rules
- Keep request/response shapes explicit.
- Record compatibility aliases and deprecations.
- Update this file when route behavior changes.

View File

@@ -0,0 +1,11 @@
# Runtime Flows
Document the critical runtime flows and failure invariants.
Recommended sections:
- Startup flow
- Request handling flow
- Background tasks
- Failure / retry / cancellation behavior

View File

@@ -0,0 +1,32 @@
# System Overview
## Product
Describe the product in one paragraph.
## Active Scope
- List current in-scope modules/features
- List user-visible surfaces
- List operational responsibilities
## Explicitly Removed / Out of Scope
- List legacy or excluded modules to avoid accidental restoration
## Runtime Composition
- HTTP server package(s)
- Domain/service orchestration
- Persistence layer(s)
- Migrations / background workers (if any)
## Local Execution
- `make run`
- `go run ./cmd/<binary>`
## Verification
- `go test ./...`

View File

@@ -0,0 +1,7 @@
# UI Information Architecture
Document page structure, required section order, UI interaction contracts, and list/filter/
pagination behavior.
Use explicit route names and action contracts so AI agents can implement changes consistently.

View File

@@ -0,0 +1,18 @@
# Architecture Decision Records
Use this directory to capture architecture decisions and significant updates.
## Required
- Every architecture decision must be recorded in the Bible.
- If a decision replaces an older one, update the older document in the same change.
- Keep entries short, explicit, and linked to the affected architecture files.
## Minimal Entry Template
- Date (`YYYY-MM-DD`)
- Decision
- Context
- Consequences
- Supersedes (if any)

View File

@@ -0,0 +1,21 @@
# Documentation Policy
## Purpose
This policy defines how architectural knowledge is captured and maintained.
## Mandatory Rules
- Record every architecture decision in the Bible before or together with implementation.
- Use English for all architecture documentation.
- Keep only current architecture in active sections.
- When a solution is replaced, update or remove obsolete guidance in the same change.
- Keep architecture details centralized in `bible/`; other top-level docs should only reference it.
## Change Workflow
1. Update the relevant file(s) in `bible/architecture/`.
2. If behavior changed, add or update a decision note in `bible/decisions/`.
3. Remove duplicated or outdated statements from non-Bible docs.
4. Validate consistency against current code paths.

View File

@@ -0,0 +1,7 @@
# Gin Admin App Integration Notes (Example)
Suggested use:
- use `ai-rules` templates only if they do not conflict with existing agent instructions
- reuse UI pattern bundles for admin tables and modal workflows
- keep domain-specific hard rules in project-local Bible docs

View File

@@ -0,0 +1,9 @@
# Greenfield App Integration Notes (Example)
Suggested use:
- apply `ai-rules`
- apply `bible-core` (or merge selectively if a Bible already exists)
- copy selected UI patterns into new modules/pages
Keep domain-specific business rules in the host project's own Bible.

View File

@@ -0,0 +1,9 @@
# net/http Enterprise App Integration Notes (Example)
This example assumes the host repo already has a mature Bible structure and detailed UI contracts.
Use this kit primarily for:
- shared AI instruction wording
- pattern bundles that codify existing host UI contracts
- future cross-project standardization docs

View File

@@ -0,0 +1,13 @@
# Controls + Selection Pattern
Canonical interactive controls for server-rendered admin/list pages:
- button hierarchy (primary / secondary / danger / quiet / disabled)
- segmented filters
- row checkboxes + select-all-visible semantics
- bulk-action bar with explicit preview/confirm steps
- status badges for list rows and workflow state
This pattern standardizes control language and interaction shape while leaving branding and
domain terminology to the host project.

View File

@@ -0,0 +1,39 @@
# Contract: Controls + Selection
## Buttons
- Use a small stable button taxonomy: `primary`, `secondary`, `ghost`, `danger`, `disabled`.
- The canonical visual baseline for demo/scaffold examples is the active dual baseline used in the
demo/scaffold (`Vapor Soft` / `Vapor Night`, system-selected). A frozen Aqua snapshot bundle may
be kept for archival reference, but examples should follow the active baseline.
- Destructive actions (`archive`, `delete`, `remove`) must use danger styling and explicit labels.
- Button text should describe the action outcome, not implementation detail.
- Buttons are text-first; icons are optional and must not replace labels on primary/danger actions.
- Base examples must show both `disabled` and `loading` states.
## Checkbox Selection
- Row checkboxes support bulk actions from a shared action bar.
- Header checkbox semantics must be explicit:
- select visible rows only, or
- select all rows in query scope (must be clearly labeled)
- Selection summary must show count (`N selected`) near bulk actions.
- In paginated views, the UI should distinguish selection on the current page from selection across the filtered/query scope.
- Selection state should survive pagination/filter navigation via explicit state (query params, server session, or another deterministic mechanism).
- Same-page interactions should preserve reading position (module anchor pattern is preferred in canonical server-rendered flows).
## Segmented Filters
- Segment/toggle filters are allowed for small enumerations (status, active/archived).
- Segmented controls are grouped joined-buttons:
- one shared outer shell,
- rounded corners only on the first/last segment,
- straight internal separators between middle segments.
- Active segment color may vary by context (for example blue preset selector vs dark status scope tabs), but geometry must remain consistent.
- Segment changes should preserve other active filters where possible.
## Bulk Action Safety
- Bulk action click should preview count and target scope before execution.
- Destructive bulk actions require confirmation.
- Dense bulk action bars may move rare actions into an explicit overflow menu (`More actions`) while keeping primary actions visible.

View File

@@ -0,0 +1,12 @@
# Forms + Validation Pattern
Canonical patterns for server-rendered form workflows in Go web applications:
- tabbed / mode-switched forms
- datalist/autocomplete suggestions
- inline validation messages
- review / confirm step before submit
- explicit reset and error handling states
This pattern standardizes interaction flow and validation UX, not domain-specific field sets.

View File

@@ -0,0 +1,34 @@
# Contract: Forms + Validation + Suggestions
## Form Structure
- Group fields semantically and keep labels explicit.
- Inputs requiring suggestions may use `datalist` or equivalent autocomplete UI.
- Suggestion sources must represent the full relevant scope (not only visible rows from paginated tables).
## Validation
- Surface validation errors inline near fields and in a form-level summary when helpful.
- Validation messages must be human-readable and action-oriented.
- Do not hide required-field errors behind generic submit failures.
## Multi-Step Flow
Recommended stages:
1. `edit`
2. `review`
3. `confirm/submit`
4. `result`
Rules:
- Users must be able to return from review to edit without losing entered values.
- Destructive or irreversible actions require explicit confirmation.
- Query- or state-driven step transitions should be deterministic and testable.
## File Inputs in Forms
- If a workflow includes upload, the file control should be clearly labeled and the supported formats explicit.
- Import parsing/preview may be delegated to a dedicated import/export pattern, but the form contract must remain clear.

View File

@@ -0,0 +1,13 @@
# Import / Export Pattern
Canonical file transfer UX patterns for Go web applications:
- file import forms (CSV/JSON and similar)
- validation preview tables before confirm
- confirm step with human-readable summary
- export controls (format + scope + options)
- predictable file download behavior and filenames
This pattern covers UI and UX contracts. Business-specific validation and file schemas remain in
the host project's own architecture docs.

View File

@@ -0,0 +1,32 @@
# Contract: Import / Export Workflows
## Import Workflow
Recommended stages:
1. `Upload`
2. `Preview / Validate`
3. `Confirm`
4. `Execute`
5. `Result summary`
Rules:
- Validation preview must be human-readable (table/list), not raw JSON only.
- Warnings and errors should be shown per row and in aggregate summary.
- Confirm step should clearly communicate scope and side effects.
## Export Workflow
- User must explicitly choose export scope (`selected`, `filtered`, `all`) when ambiguity exists.
- Export format should be explicit (`csv`, `json`, etc.).
- Download response should set:
- `Content-Type`
- `Content-Disposition`
- If CSV targets spreadsheet users, document delimiter and BOM policy.
## Error Handling
- Import errors should map to clear user-facing messages.
- Export errors after streaming starts must degrade gracefully (human-readable fallback).

View File

@@ -0,0 +1,9 @@
# Modal Workflow Pattern
This pattern package captures modal-based create/edit/remove workflows for server-rendered Go
web UIs.
Synthesis sources:
- detailed enterprise UI interaction contracts
- operational admin workflows in Go web apps

View File

@@ -0,0 +1,9 @@
# Contract: Modal Workflows
## Shared Rules
- Destructive actions require explicit confirmation.
- Validation and backend errors are rendered in human-readable form.
- UI state transitions are explicit (`open` / `submit` / `success` / `error` / `cancel`).
- API contracts and UI copy should be documented in the host project's Bible.

View File

@@ -0,0 +1,13 @@
# Operator Tools Pattern
Universal pattern for complex operator/admin dashboards in Go web applications.
This pattern standardizes:
- scope tabs and queue/status filters
- operations queue tables with row actions
- batch actions with explicit selection
- import/export shortcuts near operator workflows
- safety checklists and confirmation routing
See `contract.md` for behavior rules.

View File

@@ -0,0 +1,36 @@
# Contract: Operator Tools Dashboard
## Purpose
Provide a canonical structure for high-density operator/admin screens without encoding
domain-specific terminology or business rules.
## Required UI Regions
- Scope tabs (or equivalent segmented navigation)
- Queue/status filters
- Batch action bar with explicit selection count
- Primary queue table/list with stable row actions
- Safety/guardrail region (checklist, warnings, runbook notes)
## Selection and Batch Actions
- Batch actions must require explicit selection; never infer hidden rows by default.
- The UI must display selection counts for the current view and, if applicable, outside the current view/filter.
- Destructive or high-impact actions must route through an explicit confirmation step (modal or dedicated confirm state).
## Status and Queue Semantics
- Queue/status labels must be consistent across filters, row badges, and summaries.
- Failed items must remain easy to filter and retry without losing current scope context.
- Running/queued states should be visually distinct from done/failed states.
## Import / Export Placement
- Import preview and export actions should be discoverable near operator workflows.
- Export scope must remain explicit (`selected`, `filtered`, `all`) when ambiguity exists.
## Reuse Boundary
- Keep the pattern generic: host projects provide their own tool names, domain fields, and backend execution logic.
- This contract standardizes interaction semantics and layout zones, not business workflows.

View File

@@ -0,0 +1,4 @@
# Status Indicators Pattern (Planned)
Reserved for health badges, sync status indicators, and severity labels.

View File

@@ -0,0 +1,7 @@
# Table + Pagination Pattern
This pattern package captures a shared contract for table-based UI pages with server-side
filters and pagination.
It is derived from mature server-rendered admin UIs and list-page conventions across multiple
Go web codebases.

View File

@@ -0,0 +1,3 @@
// Placeholder template for pagination helpers.
// Adapt from existing host codebase conventions (net/http or Gin).

View File

@@ -0,0 +1,27 @@
# Contract: Table + Server-Side Filters + Pagination
## Required Behavior
- Filters apply to the full query scope before pagination (not page-local filtering).
- Pagination state is controlled by URL query parameters.
- Changing a filter resets the page parameter to page `1`.
- UI shows a summary (`Showing XY of N`) and an explicit active page state in pagination controls.
- Canonical demo visual style uses a centered dot pager while preserving page semantics via URL parameters and accessible labels.
- Demo/base contract includes a `rows per page` control.
- Status badge columns should be centered when the column semantics are status-only values.
- Small modules (no internal scroll, no more than 2 actions) should place actions in the module header row.
- Shared table/filter controls should inherit the repository baseline component theme
(currently Vapor Soft / Vapor Night in the canonical demo/scaffold) rather than redefining
per-pattern primitives.
## Recommended Query Parameters
- `page` for single-list pages
- `per_page` when the page-size control is user-visible
- section-specific params (for example `active_page`, `chronology_page`) for multi-list pages
## Interaction Guardrails
- Same-page filter/pagination interactions should preserve user reading position (for example via module anchors such as `#table-filters` / `#table-list`).
- Table status badges should remain readable under the shared theme baseline (including tinted badge variants when a host project opts into a legacy or alternate visual preset).
- Dot pager interactions should remain keyboard accessible and expose page meaning via labels/`aria-current`, even if numbers are visually hidden.

View File

@@ -0,0 +1,2 @@
/* Placeholder styles for table pagination pattern. */

View File

@@ -0,0 +1,2 @@
<!-- Placeholder table + pager partial. -->

View File

@@ -0,0 +1,14 @@
# macOS Aqua Theme Snapshot (Legacy)
Status: legacy reference only (not active baseline)
This directory preserves the Aqua-style visual exploration snapshot that was developed during demo
refinement. It is stored as a reusable archive bundle so it can be referenced later without
driving the active baseline.
Active visual baseline for the demo/scaffold is Aqua-first. This package is a frozen snapshot for archival/reference use.
Contents:
- `demo-aqua-freeze.css` — frozen snapshot of the demo stylesheet at the time of the freeze
- `notes.md` — summary of what was explored and how to treat the snapshot

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
# Aqua Legacy Snapshot Notes
This snapshot is kept for historical reference and selective reuse.
## What it captures
- Aqua-style control surfaces and button hierarchy
- segmented controls and tab-like grouped buttons
- modal window chrome experiments (titlebar + close button)
- table/filter visual refinements and dot-pager styling
- style-playground Aqua preset refinements
## What it is not
- Not a required dependency for host projects
- Not the live source of truth for the current Aqua baseline (the live baseline is in demo/scaffold CSS)
- Not the target for normal iterative styling work unless explicitly requested
## Current policy
- Active baseline styling remains Aqua-first in the live demo/scaffold assets.
- Use this snapshot only when explicitly needed for archival comparison or extraction.

View File

@@ -0,0 +1,12 @@
# Timeline Cards Pattern
Canonical timeline UX for operational history views:
- cards grouped by day
- summary chips for aggregated actions
- source labels (`ingest`, `manual`, `system`, etc.)
- single drilldown modal (no nested modals)
- in-card search/filtering for event lists
This pattern is intended for history/audit/timeline surfaces in server-rendered Go web UIs.

View File

@@ -0,0 +1,29 @@
# Contract: Timeline Cards + Drilldown
## Card Grouping
- Render timeline as grouped cards (typically by day), not raw event rows.
- Correlated or visually equivalent events may be collapsed into one card with counts.
- Card summaries should be human-readable and avoid repeating page-scope identity labels.
## Filters
- Timeline filters are server-side when timeline is paginated or large.
- Filters should preserve grouping semantics and timezone behavior.
- Scope-invariant filters may be hidden on entity detail pages.
## Drilldown
- One card opens one drilldown modal/panel.
- Drilldown contains:
- event list (left/top)
- selected event details (right/bottom)
- No nested modal inside drilldown.
- Card-local search/filter may be applied within the drilldown.
## UX Rules
- Use human-readable source labels.
- Prefer action-oriented card titles (`Installed N components`, `Removed N components`).
- Empty states must explain whether filters removed all results.

View File

@@ -0,0 +1,12 @@
run:
go run ./cmd/demo-server
build:
go build ./cmd/demo-server
test:
go test ./...
fmt:
gofmt -w $$(find . -name '*.go' -type f)

View File

@@ -0,0 +1,10 @@
# Go net/http Web Skeleton
Minimal starter skeleton for a Go web app using:
- `net/http`
- `html/template`
- embedded templates and static assets
This scaffold is intended as a starting point that host repositories can adapt.

View File

@@ -0,0 +1,23 @@
package web
import (
"net/http"
)
type IndexViewData struct {
Title string
}
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
_ = s.render(w, "base.html", IndexViewData{Title: "Home"})
}
func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}

View File

@@ -0,0 +1,37 @@
package web
import (
"html/template"
"io/fs"
"net/http"
appweb "{{ .module_path }}/web"
)
type Server struct {
mux *http.ServeMux
tmpl *template.Template
}
func NewServer() (*Server, error) {
tmpl, err := parseTemplates()
if err != nil {
return nil, err
}
s := &Server{
mux: http.NewServeMux(),
tmpl: tmpl,
}
s.registerRoutes()
return s, nil
}
func (s *Server) Handler() http.Handler { return s.mux }
func (s *Server) registerRoutes() {
s.mux.HandleFunc("/", s.handleIndex)
s.mux.HandleFunc("/healthz", s.handleHealthz)
staticFS, _ := fs.Sub(appweb.Assets, "static")
s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
}

View File

@@ -0,0 +1,27 @@
package web
import (
"fmt"
"html/template"
"net/http"
appweb "{{ .module_path }}/web"
)
func parseTemplates() (*template.Template, error) {
t, err := template.ParseFS(appweb.Assets, "templates/*.html")
if err != nil {
return nil, fmt.Errorf("parse templates: %w", err)
}
return t, nil
}
func (s *Server) render(w http.ResponseWriter, name string, data any) error {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.tmpl.ExecuteTemplate(w, name, data); err != nil {
http.Error(w, "template error", http.StatusInternalServerError)
return fmt.Errorf("render %s: %w", name, err)
}
return nil
}

View File

@@ -0,0 +1,9 @@
package web
import "embed"
// Assets contains templates and static files.
//
//go:embed templates/* static/*
var Assets embed.FS

View File

@@ -0,0 +1,46 @@
:root {
--bg: #e6e8ee;
--ink: #1f2023;
--muted: #5d646f;
--card: linear-gradient(180deg, rgba(252,252,253,0.99), rgba(240, 244, 249, 0.97) 44%, rgba(228, 238, 248, 0.96) 78%, rgba(221, 234, 248, 0.96));
--line: #b9cbe0;
--accent: linear-gradient(180deg, #85b8ef 0%, #93c6fb 26%, #abd8ff 55%, #c7e8ff 82%, #def2ff 100%);
--accent-line: #88a4d4;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Lucida Grande", "Helvetica Neue", Helvetica, Arial, sans-serif;
background:
radial-gradient(760px 360px at 14% 6%, rgba(255,255,255,0.82), transparent 72%),
radial-gradient(760px 360px at 88% 8%, rgba(154, 203, 247, 0.16), transparent 74%),
repeating-linear-gradient(to bottom, rgba(255,255,255,0.05) 0 1px, rgba(170,182,198,0.03) 1px 3px),
linear-gradient(#eef1f6, #dde2ea);
color: var(--ink);
}
.shell {
max-width: 880px;
margin: 0 auto;
padding: 24px 16px 48px;
}
.hero { margin-bottom: 16px; }
.eyebrow {
margin: 0;
color: #5f6772;
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 12px;
}
h1 { margin: 6px 0 0; font-size: 32px; }
.card {
background:
linear-gradient(180deg, rgba(255,255,255,0.70), rgba(255,255,255,0.18) 28%, rgba(255,255,255,0.0) 29%),
repeating-linear-gradient(to bottom, rgba(255,255,255,0.26) 0 1px, rgba(188,205,223,0.10) 1px 3px),
var(--card);
border: 1px solid var(--line);
border-radius: 14px;
padding: 16px;
box-shadow: 0 10px 20px rgba(78, 107, 139, 0.09), inset 0 1px 0 rgba(255,255,255,0.96);
}
code { background: #eef3fb; padding: 1px 5px; border-radius: 6px; }

View File

@@ -0,0 +1,2 @@
document.documentElement.classList.add("js");

View File

@@ -0,0 +1,22 @@
{{ define "base.html" }}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ .Title }}</title>
<link rel="stylesheet" href="/static/css/app.css">
</head>
<body>
<main class="shell">
<header class="hero">
<p class="eyebrow">Go Web Scaffold</p>
<h1>{{ .Title }}</h1>
</header>
{{ template "content" . }}
</main>
<script src="/static/js/app.js" defer></script>
</body>
</html>
{{ end }}

View File

@@ -0,0 +1,11 @@
{{ define "content" }}
<section class="card">
<p>Minimal net/http + html/template scaffold.</p>
<ul>
<li><code>GET /</code> page</li>
<li><code>GET /healthz</code> healthcheck</li>
<li><code>/static/*</code> embedded assets</li>
</ul>
</section>
{{ end }}