feat: bootstrap design kit and vaporwave demo baseline
This commit is contained in:
12
demo/Makefile
Normal file
12
demo/Makefile
Normal 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
7
demo/README.md
Normal 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
4
demo/go.mod
Normal file
@@ -0,0 +1,4 @@
|
||||
module git.mchus.pro/mchus/ui-design-code-demo
|
||||
|
||||
go 1.24.0
|
||||
|
||||
1189
demo/internal/web/patterns_more.go
Normal file
1189
demo/internal/web/patterns_more.go
Normal file
File diff suppressed because it is too large
Load Diff
340
demo/internal/web/server.go
Normal file
340
demo/internal/web/server.go
Normal 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
|
||||
}
|
||||
292
demo/internal/web/server_test.go
Normal file
292
demo/internal/web/server_test.go
Normal 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 1–10 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 1–3 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
6
demo/web/embed.go
Normal 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
4547
demo/web/static/css/app.css
Normal file
File diff suppressed because it is too large
Load Diff
2
demo/web/static/js/app.js
Normal file
2
demo/web/static/js/app.js
Normal file
@@ -0,0 +1,2 @@
|
||||
document.documentElement.classList.add("js");
|
||||
document.documentElement.setAttribute("data-theme-mode", "auto");
|
||||
311
demo/web/templates/base.html
Normal file
311
demo/web/templates/base.html
Normal 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 }}
|
||||
114
demo/web/templates/controls_pattern.html
Normal file
114
demo/web/templates/controls_pattern.html
Normal 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 }}
|
||||
95
demo/web/templates/forms_pattern.html
Normal file
95
demo/web/templates/forms_pattern.html
Normal 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 }}
|
||||
99
demo/web/templates/io_pattern.html
Normal file
99
demo/web/templates/io_pattern.html
Normal 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 }}
|
||||
74
demo/web/templates/modal_pattern.html
Normal file
74
demo/web/templates/modal_pattern.html
Normal 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 }}
|
||||
114
demo/web/templates/operator_tools_pattern.html
Normal file
114
demo/web/templates/operator_tools_pattern.html
Normal 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 }}
|
||||
177
demo/web/templates/style_playground_pattern.html
Normal file
177
demo/web/templates/style_playground_pattern.html
Normal 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 }}
|
||||
118
demo/web/templates/timeline_pattern.html
Normal file
118
demo/web/templates/timeline_pattern.html
Normal 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 }}
|
||||
Reference in New Issue
Block a user