From a61d12cd3a14db222ea886a8931c09f96a75075f Mon Sep 17 00:00:00 2001 From: Adnaan Date: Sat, 28 Mar 2026 09:27:27 +0530 Subject: [PATCH 1/5] feat: migrate all examples to Pico CSS Replace all custom CSS with Pico CSS semantic markup across all 16 examples. Templates now use Pico's classless styling (article, hgroup, mark, ins/del, progress, fieldset role=group, etc.) instead of custom style blocks. - Remove ~660 lines of custom CSS (gradients, buttons, forms, layouts) - Add Pico CSS CDN link to all templates - Use / for flash/status messages (Pico styles natively) - Use
for cards, for badges, for bars - Use aria-invalid + for form validation (Pico auto-colors) - Use .grid, .secondary, .contrast, .outline Pico classes - Only functional CSS remains: .visually-hidden, .js-mode, chat messages - Update CLAUDE.md with Pico CSS guidelines - Update test selectors for new semantic HTML structure Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 17 + avatar-upload/avatar-upload.tmpl | 332 +++--------------- chat/chat.tmpl | 32 +- chat/chat_e2e_test.go | 4 +- counter/counter.tmpl | 29 +- flash-messages/flash.tmpl | 255 ++++---------- flash-messages/flash_test.go | 56 +-- graceful-shutdown/counter.tmpl | 29 +- live-preview/preview.tmpl | 22 +- login/templates/auth.html | 202 +++-------- observability/counter.tmpl | 29 +- production/single-host/app.tmpl | 196 ++--------- profile-progressive/profile.tmpl | 87 +++-- .../profile_progressive_test.go | 12 +- .../progressive-enhancement.tmpl | 72 ++-- .../progressive_enhancement_test.go | 52 +-- testing/01_basic/welcome.tmpl | 11 +- todos-components/todos-components.tmpl | 67 ++-- todos-progressive/todos.tmpl | 106 +++--- todos-progressive/todos_progressive_test.go | 28 +- 20 files changed, 532 insertions(+), 1106 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 64f91af..b86bde7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,3 +67,20 @@ When a form is submitted, the framework resolves the action in this order: - `profile-progressive/` — Simple Tier 1 form with validation - `live-preview/` — Tier 1 with `Change()` method for live updates - `chat/` — Tier 1+2 (uses `lvt-scroll` for auto-scroll) + +## CSS + +All examples use [Pico CSS](https://picocss.com/docs) exclusively: + +- Include via CDN: `` +- Use semantic HTML — Pico auto-styles: `
` (cards), `` (modals), `
` (accordions), ``, `
+ + {{range .Items}} + + + + + {{end}} + +
{{.}} +
+ + +
+
+ {{else}} + No items. Add some above! + {{end}} +
+ + +
+ About Flash Messages: +
    +
  • Flash messages show once and clear after each action
  • +
  • They don't affect ResponseMetadata.Success
  • +
  • Types: success, error, warning, info
  • +
  • Set via: ctx.SetFlash("success", "message")
  • +
  • Read via: .lvt.Flash "success", .lvt.HasFlash "success"
  • +
+
+ {{if .lvt.DevMode}} diff --git a/flash-messages/flash_test.go b/flash-messages/flash_test.go index f2152f6..cfba096 100644 --- a/flash-messages/flash_test.go +++ b/flash-messages/flash_test.go @@ -59,9 +59,9 @@ func TestFlash_ShowsInTemplate(t *testing.T) { t.Errorf("Expected success flash 'Added item: Test Item' in response, got:\n%s", body) } - // Should have flash-success class - if !strings.Contains(body, "flash-success") { - t.Errorf("Expected flash-success class in response") + // Should have data-flash="success" attribute + if !strings.Contains(body, `data-flash="success"`) { + t.Errorf("Expected data-flash=\"success\" attribute in response") } } @@ -149,36 +149,36 @@ func TestFlash_DifferentTypes(t *testing.T) { name string action string item string - wantClass string + wantAttr string wantText string }{ { - name: "success flash", - action: "addItem", - item: "New Item", - wantClass: "flash-success", - wantText: "Added item: New Item", + name: "success flash", + action: "addItem", + item: "New Item", + wantAttr: `data-flash="success"`, + wantText: "Added item: New Item", }, { - name: "warning flash (duplicate)", - action: "addItem", - item: "New Item", // Same item = duplicate - wantClass: "flash-warning", - wantText: "Item already exists", + name: "warning flash (duplicate)", + action: "addItem", + item: "New Item", // Same item = duplicate + wantAttr: `data-flash="warning"`, + wantText: "Item already exists", }, { - name: "info flash", - action: "removeItem", - item: "New Item", - wantClass: "flash-info", - wantText: "Removed item: New Item", + name: "info flash", + action: "removeItem", + item: "New Item", + wantAttr: `data-flash="info"`, + wantText: "Removed item: New Item", }, { - name: "error flash", - action: "simulateError", - item: "", - wantClass: "flash-error", - wantText: "Something went wrong", + name: "error flash", + action: "simulateError", + item: "", + wantAttr: `data-flash="error"`, + wantText: "Something went wrong", }, } @@ -197,8 +197,8 @@ func TestFlash_DifferentTypes(t *testing.T) { body := readBody(t, resp) resp.Body.Close() - if !strings.Contains(body, tt.wantClass) { - t.Errorf("Expected %s in response", tt.wantClass) + if !strings.Contains(body, tt.wantAttr) { + t.Errorf("Expected %s in response", tt.wantAttr) } if !strings.Contains(body, tt.wantText) { t.Errorf("Expected '%s' in response, got:\n%s", tt.wantText, body) @@ -243,8 +243,8 @@ func TestFlash_FieldErrorsStillWork(t *testing.T) { if !strings.Contains(body, "Item name is required") { t.Error("Expected field error 'Item name is required'") } - if !strings.Contains(body, "field-error") { - t.Error("Expected field-error class") + if !strings.Contains(body, "Item name is required") { + t.Error("Expected field error in element") } } diff --git a/graceful-shutdown/counter.tmpl b/graceful-shutdown/counter.tmpl index db32af0..75f25af 100644 --- a/graceful-shutdown/counter.tmpl +++ b/graceful-shutdown/counter.tmpl @@ -4,20 +4,25 @@ {{.Title}} + -

{{.Title}}

-
-

Counter: {{.Counter}}

-
- - - -
-
-
-

Last updated: {{.LastUpdated}}

-
+
+
+
+

{{.Title}}

+
+

Counter: {{.Counter}}

+
+ + + +
+
+ Last updated: {{.LastUpdated}} +
+
+
{{if .lvt.DevMode}} diff --git a/live-preview/preview.tmpl b/live-preview/preview.tmpl index 445fd95..15c1d4a 100644 --- a/live-preview/preview.tmpl +++ b/live-preview/preview.tmpl @@ -4,15 +4,23 @@ Live Preview + -

Live Preview

-
- - - -
-
{{.Preview}}
+
+
+
+

Live Preview

+
+
+ + +
+
{{.Preview}}
+
+
{{if .lvt.DevMode}} diff --git a/login/templates/auth.html b/login/templates/auth.html index 3224ccd..a394521 100644 --- a/login/templates/auth.html +++ b/login/templates/auth.html @@ -4,171 +4,57 @@ Login Example - LiveTemplate - - {{if .lvt.DevMode}} - - {{else}} - - {{end}} + -
+
{{if .IsLoggedIn}} -

Dashboard

- {{if .ServerMessage}} -
- {{.ServerMessage}} -
- {{end}} -
-

Welcome, {{.Username}}!

-

You are successfully logged in.

- {{if not .LoginTime.IsZero}} - +
+

Dashboard

+ {{if .ServerMessage}} + {{.ServerMessage}} {{end}} -
- -
- -
+
+

Welcome, {{.Username}}!

+

You are successfully logged in.

+ {{if not .LoginTime.IsZero}} + Logged in at: {{.LoginTime.Format "15:04:05"}} + {{end}} +
+ +
+ +
+
{{else}} -

Login

- {{if .Error}} -
{{.Error}}
- {{end}} - -
-
- - -
-
- - -
- -
- -
- Demo: Use any username with password secret
- After login, the server will push a welcome message via WebSocket. -
+
+

Login

+ {{if .Error}} + {{.Error}} + {{end}} + +
+ + + +
+ + Demo: Use any username with password secret
+ After login, the server will push a welcome message via WebSocket. +
+
{{end}} - + + {{if .lvt.DevMode}} + + {{else}} + + {{end}} diff --git a/observability/counter.tmpl b/observability/counter.tmpl index db32af0..75f25af 100644 --- a/observability/counter.tmpl +++ b/observability/counter.tmpl @@ -4,20 +4,25 @@ {{.Title}} + -

{{.Title}}

-
-

Counter: {{.Counter}}

-
- - - -
-
-
-

Last updated: {{.LastUpdated}}

-
+
+
+
+

{{.Title}}

+
+

Counter: {{.Counter}}

+
+ + + +
+
+ Last updated: {{.LastUpdated}} +
+
+
{{if .lvt.DevMode}} diff --git a/production/single-host/app.tmpl b/production/single-host/app.tmpl index d6eade7..d49ed49 100644 --- a/production/single-host/app.tmpl +++ b/production/single-host/app.tmpl @@ -4,171 +4,49 @@ {{.Title}} - + -
- Production Ready -

{{.Title}}

-

Showcasing production deployment features

+
+
+
+ Production Ready +
+

{{.Title}}

+

Showcasing production deployment features

+
+
-
-

Enabled Features

-
    -
  • Environment-based configuration
  • -
  • Structured JSON logging
  • -
  • Request tracing with trace IDs
  • -
  • Graceful shutdown
  • -
  • Health & readiness checks
  • -
  • Metrics endpoint
  • -
-
+
+ Enabled Features +
    +
  • Environment-based configuration
  • +
  • Structured JSON logging
  • +
  • Request tracing with trace IDs
  • +
  • Graceful shutdown
  • +
  • Health & readiness checks
  • +
  • Metrics endpoint
  • +
+
-
-
{{.Counter}}
-
Counter Value
-
+
+
+

{{.Counter}}

+

Counter Value

+
+
-
- - - -
+
+ + + +
- -
+
+ Last updated: {{.LastUpdated}} +
+ + {{if .lvt.DevMode}} diff --git a/profile-progressive/profile.tmpl b/profile-progressive/profile.tmpl index cdd0092..74f244a 100644 --- a/profile-progressive/profile.tmpl +++ b/profile-progressive/profile.tmpl @@ -2,60 +2,55 @@ Progressive Complexity: Profile Demo - + + + -

Edit Profile

-

Zero lvt-* attributes — Tier 1 only

+
+
+

Edit Profile

+

Zero lvt-* attributes — Tier 1 only

+
- {{if .Saved}} -
Profile saved successfully!
- {{end}} - - -
- - - {{if .lvt.HasError "display_name"}} -
{{.lvt.Error "display_name"}}
+ {{if .Saved}} + Profile saved successfully! {{end}} - - - {{if .lvt.HasError "email"}} -
{{.lvt.Error "email"}}
- {{end}} + + - - - {{if .lvt.HasError "bio"}} -
{{.lvt.Error "bio"}}
- {{end}} + + + - -
+ + -
-

Preview

-

Name: {{.DisplayName}}

-

Email: {{.Email}}

-

Bio: {{.Bio}}

-
+
+
Preview
+

Name: {{.DisplayName}}

+

Email: {{.Email}}

+

Bio: {{.Bio}}

+
+
{{if .lvt.DevMode}} diff --git a/profile-progressive/profile_progressive_test.go b/profile-progressive/profile_progressive_test.go index b161c15..55884d6 100644 --- a/profile-progressive/profile_progressive_test.go +++ b/profile-progressive/profile_progressive_test.go @@ -67,8 +67,8 @@ func TestProfileProgressiveE2E(t *testing.T) { var previewHTML string err := chromedp.Run(ctx, chromedp.OuterHTML("body", &html, chromedp.ByQuery), - chromedp.Evaluate(`document.querySelector('.success') !== null`, &hasSuccess), - chromedp.OuterHTML(".preview", &previewHTML, chromedp.ByQuery), + chromedp.Evaluate(`document.querySelector('ins') !== null`, &hasSuccess), + chromedp.OuterHTML("article", &previewHTML, chromedp.ByQuery), ) if err != nil { t.Fatalf("Failed to get initial state: %v", err) @@ -100,7 +100,7 @@ func TestProfileProgressiveE2E(t *testing.T) { chromedp.Clear(`textarea[name="Bio"]`, chromedp.ByQuery), chromedp.SendKeys(`textarea[name="Bio"]`, "Updated bio text.", chromedp.ByQuery), chromedp.Evaluate(`document.querySelector('button[type="submit"]').click()`, nil), - e2etest.WaitFor(`document.querySelector('.success') !== null`, 5*time.Second), + e2etest.WaitFor(`document.querySelector('ins') !== null`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to save profile: %v", err) @@ -115,7 +115,7 @@ func TestProfileProgressiveE2E(t *testing.T) { } var previewHTML string - if err := chromedp.Run(ctx, chromedp.OuterHTML(".preview", &previewHTML, chromedp.ByQuery)); err != nil { + if err := chromedp.Run(ctx, chromedp.OuterHTML("article", &previewHTML, chromedp.ByQuery)); err != nil { t.Fatalf("Failed to get preview: %v", err) } if !strings.Contains(previewHTML, "John Smith") { @@ -129,14 +129,14 @@ func TestProfileProgressiveE2E(t *testing.T) { t.Run("UpdateProfile", func(t *testing.T) { err := chromedp.Run(ctx, chromedp.Evaluate(`window.liveTemplateClient.send({action: 'submit', data: {DisplayName: 'Jane Updated', Email: 'jane.updated@example.com', Bio: 'Fixed and saved.'}})`, nil), - e2etest.WaitFor(`document.querySelector('.success') !== null`, 5*time.Second), + e2etest.WaitFor(`document.querySelector('ins') !== null`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to update profile: %v", err) } var previewHTML string - if err := chromedp.Run(ctx, chromedp.OuterHTML(".preview", &previewHTML, chromedp.ByQuery)); err != nil { + if err := chromedp.Run(ctx, chromedp.OuterHTML("article", &previewHTML, chromedp.ByQuery)); err != nil { t.Fatalf("Failed to get preview: %v", err) } if !strings.Contains(previewHTML, "Jane Updated") { diff --git a/progressive-enhancement/progressive-enhancement.tmpl b/progressive-enhancement/progressive-enhancement.tmpl index 79921e1..89272f5 100644 --- a/progressive-enhancement/progressive-enhancement.tmpl +++ b/progressive-enhancement/progressive-enhancement.tmpl @@ -6,25 +6,16 @@
-

{{.Title}}

-

This app works with or without JavaScript enabled

+
+

{{.Title}}

+

This app works with or without JavaScript enabled

+
diff --git a/progressive-enhancement/progressive_enhancement_test.go b/progressive-enhancement/progressive_enhancement_test.go index 99d08d9..5addb97 100644 --- a/progressive-enhancement/progressive_enhancement_test.go +++ b/progressive-enhancement/progressive_enhancement_test.go @@ -154,7 +154,7 @@ func TestProgressiveEnhancement_JSFormSubmission(t *testing.T) { // Count initial todos var initialTodoCount int err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &initialTodoCount), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &initialTodoCount), ) if err != nil { t.Fatalf("Failed to count initial todos: %v", err) @@ -192,7 +192,7 @@ func TestProgressiveEnhancement_JSFormSubmission(t *testing.T) { // Count todos after submission var finalTodoCount int err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &finalTodoCount), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &finalTodoCount), ) if err != nil { t.Fatalf("Failed to count final todos: %v", err) @@ -524,7 +524,7 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { chromedp.Navigate(server.URL), chromedp.WaitReady(`body`, chromedp.ByQuery), e2etest.WaitFor(`window.liveTemplateClient && window.liveTemplateClient.isReady()`, 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &initialCount), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &initialCount), ) if err != nil { t.Fatalf("Step 1 (navigate) error: %v", err) @@ -537,8 +537,8 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { chromedp.SendKeys(`input[name="title"]`, "E2E Test Todo", chromedp.ByQuery), chromedp.Evaluate(`document.querySelector('button[name="add"]').click()`, nil), // Wait for DOM to update with new item - e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('.todo-item').length === %d`, expectedAfterAdd), 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterAddCount), + e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('tbody tr').length === %d`, expectedAfterAdd), 5*time.Second), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterAddCount), ) if err != nil { t.Fatalf("Step 2 (add) error: %v", err) @@ -552,11 +552,11 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { // Step 3: Toggle the last todo (mark as complete) err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:last-child button[name="toggle"]').click()`, nil), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child button[name="toggle"]').click()`, nil), // Wait for the completed class to appear - e2etest.WaitFor(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterToggleCount), - chromedp.Evaluate(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, &hasCompletedClass), + e2etest.WaitFor(`document.querySelector('tbody tr:last-child').querySelector('s') !== null`, 5*time.Second), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterToggleCount), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child').querySelector('s') !== null`, &hasCompletedClass), ) if err != nil { t.Fatalf("Step 3 (toggle) error: %v", err) @@ -573,11 +573,11 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { // Step 4: Toggle again (mark as incomplete) err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:last-child button[name="toggle"]').click()`, nil), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child button[name="toggle"]').click()`, nil), // Wait for the completed class to be removed - e2etest.WaitFor(`!document.querySelector('.todo-item:last-child').classList.contains('completed')`, 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterUntoggleCount), - chromedp.Evaluate(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, &hasCompletedClassAfterUntoggle), + e2etest.WaitFor(`document.querySelector('tbody tr:last-child').querySelector('s') === null`, 5*time.Second), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterUntoggleCount), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child').querySelector('s') !== null`, &hasCompletedClassAfterUntoggle), ) if err != nil { t.Fatalf("Step 4 (untoggle) error: %v", err) @@ -594,10 +594,10 @@ func TestProgressiveEnhancement_WebSocketCRUD(t *testing.T) { // Step 5: Delete the last todo (the one we just added) err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:last-child button[name="delete"]').click()`, nil), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child button[name="delete"]').click()`, nil), // Wait for DOM to update with deleted item - e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('.todo-item').length === %d`, initialCount), 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterDeleteCount), + e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('tbody tr').length === %d`, initialCount), 5*time.Second), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterDeleteCount), ) if err != nil { t.Fatalf("Step 5 (delete) error: %v", err) @@ -641,7 +641,7 @@ func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { chromedp.Navigate(server.URL), chromedp.WaitReady(`body`, chromedp.ByQuery), e2etest.WaitFor(`window.liveTemplateClient && window.liveTemplateClient.isReady()`, 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &initialCount), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &initialCount), ) if err != nil { t.Fatalf("Step 1 (navigate) error: %v", err) @@ -655,10 +655,10 @@ func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { // Step 2: Delete the FIRST todo expectedAfterDelete := initialCount - 1 err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:first-child button[name="delete"]').click()`, nil), + chromedp.Evaluate(`document.querySelector('tbody tr:first-child button[name="delete"]').click()`, nil), // Wait for DOM to update with deleted item - e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('.todo-item').length === %d`, expectedAfterDelete), 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterDeleteCount), + e2etest.WaitFor(fmt.Sprintf(`document.querySelectorAll('tbody tr').length === %d`, expectedAfterDelete), 5*time.Second), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterDeleteCount), ) if err != nil { t.Fatalf("Step 2 (delete) error: %v", err) @@ -674,7 +674,7 @@ func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { // Check if it has completed class before toggle var hasCompletedClassBefore bool err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, &hasCompletedClassBefore), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child').querySelector('s') !== null`, &hasCompletedClassBefore), ) if err != nil { t.Fatalf("Step 3 (check before toggle) error: %v", err) @@ -682,17 +682,17 @@ func TestProgressiveEnhancement_DeleteThenToggle(t *testing.T) { t.Logf("Last item has completed class before toggle: %v", hasCompletedClassBefore) // Build the wait condition based on whether we expect completed class or not - toggleWaitCondition := `document.querySelector('.todo-item:last-child').classList.contains('completed')` + toggleWaitCondition := `document.querySelector('tbody tr:last-child').querySelector('s') !== null` if hasCompletedClassBefore { - toggleWaitCondition = `!document.querySelector('.todo-item:last-child').classList.contains('completed')` + toggleWaitCondition = `document.querySelector('tbody tr:last-child').querySelector('s') === null` } err = chromedp.Run(ctx, - chromedp.Evaluate(`document.querySelector('.todo-item:last-child button[name="toggle"]').click()`, nil), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child button[name="toggle"]').click()`, nil), // Wait for the completed class to change e2etest.WaitFor(toggleWaitCondition, 5*time.Second), - chromedp.Evaluate(`document.querySelectorAll('.todo-item').length`, &afterToggleCount), - chromedp.Evaluate(`document.querySelector('.todo-item:last-child').classList.contains('completed')`, &hasCompletedClass), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &afterToggleCount), + chromedp.Evaluate(`document.querySelector('tbody tr:last-child').querySelector('s') !== null`, &hasCompletedClass), ) if err != nil { t.Fatalf("Step 3 (toggle) error: %v", err) diff --git a/testing/01_basic/welcome.tmpl b/testing/01_basic/welcome.tmpl index ea2d9c0..ae7f6da 100644 --- a/testing/01_basic/welcome.tmpl +++ b/testing/01_basic/welcome.tmpl @@ -2,11 +2,16 @@ {{.Title}} + + + -

{{.Title}}

-

{{.Message}}

-

Count: {{.Count}}

+
+

{{.Title}}

+

{{.Message}}

+

Count: {{.Count}}

+
{{if .lvt.DevMode}} {{else}} diff --git a/todos-components/todos-components.tmpl b/todos-components/todos-components.tmpl index fcc9967..e9783de 100644 --- a/todos-components/todos-components.tmpl +++ b/todos-components/todos-components.tmpl @@ -10,13 +10,6 @@ {{else}} {{end}} -
@@ -36,34 +29,42 @@ {{/* Todo List */}}
{{if .Todos}} -
+ + {{range .Todos}} -
-
- - - - - -
- - - -
+ + + + + {{end}} - + +
+
+ + + +
+
+ + +
+ + +
+
{{else}}

No todos yet. Add one above!

{{end}} diff --git a/todos-progressive/todos.tmpl b/todos-progressive/todos.tmpl index 93daccf..7331e0b 100644 --- a/todos-progressive/todos.tmpl +++ b/todos-progressive/todos.tmpl @@ -2,63 +2,65 @@ Progressive Complexity: Todo Demo - + + + -

Todos ({{.ActiveCount}} remaining)

-

Zero lvt-* attributes — standard HTML only

+
+
+

Todos ({{.ActiveCount}} remaining)

+

Zero lvt-* attributes — standard HTML only

+
- -
- - -
- {{if .lvt.HasError "title"}} -
{{.lvt.Error "title"}}
- {{end}} + +
+ + +
+
+ {{if .lvt.HasError "title"}} + {{.lvt.Error "title"}} + {{end}} - -
    - {{range .FilteredItems}} -
  • -
    - - {{.Title}} - - -
    -
  • - {{end}} -
+ + + + {{range .FilteredItems}} + + + + + + {{end}} + +
+
+ + {{if .Done}}{{.Title}}{{else}}{{.Title}}{{end}} +
+
+
+ + +
+
+
+ + +
+
- -
-
-
-
-
+ +
+
+
+
+
+
{{if .lvt.DevMode}} diff --git a/todos-progressive/todos_progressive_test.go b/todos-progressive/todos_progressive_test.go index 03a9471..a2d7251 100644 --- a/todos-progressive/todos_progressive_test.go +++ b/todos-progressive/todos_progressive_test.go @@ -67,8 +67,8 @@ func TestTodosProgressiveE2E(t *testing.T) { var hasDone bool err := chromedp.Run(ctx, chromedp.OuterHTML("body", &html, chromedp.ByQuery), - chromedp.Evaluate(`document.querySelectorAll('ul li').length`, &count), - chromedp.Evaluate(`document.querySelector('ul li:nth-child(3) span').classList.contains('done')`, &hasDone), + chromedp.Evaluate(`document.querySelectorAll('tbody tr').length`, &count), + chromedp.Evaluate(`document.querySelector('tbody tr:nth-child(3) s') !== null`, &hasDone), ) if err != nil { t.Fatalf("Failed to get initial state: %v", err) @@ -97,7 +97,7 @@ func TestTodosProgressiveE2E(t *testing.T) { t.Run("AddTodo", func(t *testing.T) { err := chromedp.Run(ctx, chromedp.Evaluate(`window.liveTemplateClient.send({action: 'submit', data: {Title: 'Buy groceries'}})`, nil), - e2etest.WaitFor(`document.querySelectorAll('ul li').length === 4`, 5*time.Second), + e2etest.WaitFor(`document.querySelectorAll('tbody tr').length === 4`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to add todo: %v", err) @@ -118,13 +118,13 @@ func TestTodosProgressiveE2E(t *testing.T) { t.Run("ToggleDone", func(t *testing.T) { var itemID string if err := chromedp.Run(ctx, chromedp.Evaluate( - `document.querySelector('ul li:nth-child(1) input[name="id"]').value`, &itemID)); err != nil { + `document.querySelector('tbody tr:nth-child(1) input[name="id"]').value`, &itemID)); err != nil { t.Fatalf("Failed to get item ID: %v", err) } err := chromedp.Run(ctx, chromedp.Evaluate(fmt.Sprintf(`window.liveTemplateClient.send({action: 'toggle', data: {id: '%s'}})`, itemID), nil), - e2etest.WaitFor(`document.querySelector('ul li:nth-child(1) span').classList.contains('done')`, 5*time.Second), + e2etest.WaitFor(`document.querySelector('tbody tr:nth-child(1) s') !== null`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to toggle done: %v", err) @@ -142,13 +142,13 @@ func TestTodosProgressiveE2E(t *testing.T) { t.Run("ToggleUndo", func(t *testing.T) { var itemID string if err := chromedp.Run(ctx, chromedp.Evaluate( - `document.querySelector('ul li:nth-child(1) input[name="id"]').value`, &itemID)); err != nil { + `document.querySelector('tbody tr:nth-child(1) input[name="id"]').value`, &itemID)); err != nil { t.Fatalf("Failed to get item ID: %v", err) } err := chromedp.Run(ctx, chromedp.Evaluate(fmt.Sprintf(`window.liveTemplateClient.send({action: 'toggle', data: {id: '%s'}})`, itemID), nil), - e2etest.WaitFor(`!document.querySelector('ul li:nth-child(1) span').classList.contains('done')`, 5*time.Second), + e2etest.WaitFor(`!document.querySelector('tbody tr:nth-child(1) s') !== null`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to undo toggle: %v", err) @@ -166,13 +166,13 @@ func TestTodosProgressiveE2E(t *testing.T) { t.Run("DeleteTodo", func(t *testing.T) { var itemID string if err := chromedp.Run(ctx, chromedp.Evaluate( - `document.querySelector('ul li:last-child input[name="id"]').value`, &itemID)); err != nil { + `document.querySelector('tbody tr:last-child input[name="id"]').value`, &itemID)); err != nil { t.Fatalf("Failed to get item ID: %v", err) } err := chromedp.Run(ctx, chromedp.Evaluate(fmt.Sprintf(`window.liveTemplateClient.send({action: 'delete', data: {id: '%s'}})`, itemID), nil), - e2etest.WaitFor(`document.querySelectorAll('ul li').length === 3`, 5*time.Second), + e2etest.WaitFor(`document.querySelectorAll('tbody tr').length === 3`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to delete todo: %v", err) @@ -190,14 +190,14 @@ func TestTodosProgressiveE2E(t *testing.T) { t.Run("FilterActive", func(t *testing.T) { err := chromedp.Run(ctx, chromedp.Evaluate(`window.liveTemplateClient.send({action: 'filter', data: {filter: 'active'}})`, nil), - e2etest.WaitFor(`document.querySelectorAll('ul li').length === 2`, 5*time.Second), + e2etest.WaitFor(`document.querySelectorAll('tbody tr').length === 2`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to filter active: %v", err) } var html string - if err := chromedp.Run(ctx, chromedp.OuterHTML("ul", &html, chromedp.ByQuery)); err != nil { + if err := chromedp.Run(ctx, chromedp.OuterHTML("tbody", &html, chromedp.ByQuery)); err != nil { t.Fatalf("Failed to get HTML: %v", err) } if strings.Contains(html, "Add lvt-* only when needed") { @@ -208,14 +208,14 @@ func TestTodosProgressiveE2E(t *testing.T) { t.Run("FilterDone", func(t *testing.T) { err := chromedp.Run(ctx, chromedp.Evaluate(`window.liveTemplateClient.send({action: 'filter', data: {filter: 'done'}})`, nil), - e2etest.WaitFor(`document.querySelectorAll('ul li').length === 1`, 5*time.Second), + e2etest.WaitFor(`document.querySelectorAll('tbody tr').length === 1`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to filter done: %v", err) } var html string - if err := chromedp.Run(ctx, chromedp.OuterHTML("ul", &html, chromedp.ByQuery)); err != nil { + if err := chromedp.Run(ctx, chromedp.OuterHTML("tbody", &html, chromedp.ByQuery)); err != nil { t.Fatalf("Failed to get HTML: %v", err) } if !strings.Contains(html, "Add lvt-* only when needed") { @@ -226,7 +226,7 @@ func TestTodosProgressiveE2E(t *testing.T) { t.Run("FilterAll", func(t *testing.T) { err := chromedp.Run(ctx, chromedp.Evaluate(`window.liveTemplateClient.send({action: 'filter', data: {filter: 'all'}})`, nil), - e2etest.WaitFor(`document.querySelectorAll('ul li').length === 3`, 5*time.Second), + e2etest.WaitFor(`document.querySelectorAll('tbody tr').length === 3`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to filter all: %v", err) From 65a706b5f844260eb27390f707e743f0f7ee052a Mon Sep 17 00:00:00 2001 From: Adnaan Date: Sat, 28 Mar 2026 09:35:35 +0530 Subject: [PATCH 2/5] fix: cleanup from code review - Fix logic bug in todos-progressive ToggleUndo test (operator precedence: !querySelector !== null was always true) - Remove dead CSS comments in chat template - Remove dead
wrapping display-only text in todos-progressive - Replace nested
with
in production template - Fix comment to explain WHY not WHAT in progressive-enhancement - Normalize ins/del inline style format in ws-disabled Co-Authored-By: Claude Opus 4.6 (1M context) --- chat/chat.tmpl | 2 -- production/single-host/app.tmpl | 10 ++++------ progressive-enhancement/progressive-enhancement.tmpl | 2 +- todos-progressive/todos.tmpl | 7 +------ todos-progressive/todos_progressive_test.go | 2 +- ws-disabled/ws-disabled.tmpl | 4 ++-- 6 files changed, 9 insertions(+), 18 deletions(-) diff --git a/chat/chat.tmpl b/chat/chat.tmpl index 259fc43..d86f9eb 100644 --- a/chat/chat.tmpl +++ b/chat/chat.tmpl @@ -10,7 +10,6 @@ body { padding: 1rem; } - /* Stats bar uses Pico's but needs spacing */ .messages { border: 1px solid var(--pico-muted-border-color); border-radius: var(--pico-border-radius); @@ -42,7 +41,6 @@ .message-time { float: right; } - /* Empty state */ diff --git a/production/single-host/app.tmpl b/production/single-host/app.tmpl index d49ed49..6a9212a 100644 --- a/production/single-host/app.tmpl +++ b/production/single-host/app.tmpl @@ -29,12 +29,10 @@ -
-
-

{{.Counter}}

-

Counter Value

-
-
+
+

{{.Counter}}

+

Counter Value

+
diff --git a/progressive-enhancement/progressive-enhancement.tmpl b/progressive-enhancement/progressive-enhancement.tmpl index 89272f5..494172a 100644 --- a/progressive-enhancement/progressive-enhancement.tmpl +++ b/progressive-enhancement/progressive-enhancement.tmpl @@ -6,7 +6,7 @@ diff --git a/todos-progressive/todos.tmpl b/todos-progressive/todos.tmpl index 7331e0b..4d8f935 100644 --- a/todos-progressive/todos.tmpl +++ b/todos-progressive/todos.tmpl @@ -29,12 +29,7 @@ {{range .FilteredItems}} - - - - {{if .Done}}{{.Title}}{{else}}{{.Title}}{{end}} - - + {{if .Done}}{{.Title}}{{else}}{{.Title}}{{end}}
diff --git a/todos-progressive/todos_progressive_test.go b/todos-progressive/todos_progressive_test.go index a2d7251..e3dc344 100644 --- a/todos-progressive/todos_progressive_test.go +++ b/todos-progressive/todos_progressive_test.go @@ -148,7 +148,7 @@ func TestTodosProgressiveE2E(t *testing.T) { err := chromedp.Run(ctx, chromedp.Evaluate(fmt.Sprintf(`window.liveTemplateClient.send({action: 'toggle', data: {id: '%s'}})`, itemID), nil), - e2etest.WaitFor(`!document.querySelector('tbody tr:nth-child(1) s') !== null`, 5*time.Second), + e2etest.WaitFor(`document.querySelector('tbody tr:nth-child(1) s') === null`, 5*time.Second), ) if err != nil { t.Fatalf("Failed to undo toggle: %v", err) diff --git a/ws-disabled/ws-disabled.tmpl b/ws-disabled/ws-disabled.tmpl index e1af344..d9870ca 100644 --- a/ws-disabled/ws-disabled.tmpl +++ b/ws-disabled/ws-disabled.tmpl @@ -13,10 +13,10 @@ and applies tree-based DOM updates without page reloads.

{{if .lvt.Flash "success"}} - {{.lvt.Flash "success"}} + {{.lvt.Flash "success"}} {{end}} {{if .lvt.Flash "error"}} - {{.lvt.Flash "error"}} + {{.lvt.Flash "error"}} {{end}}
From 83e6f46ea9addde1d870940945c9a3b0ed4b3679 Mon Sep 17 00:00:00 2001 From: Adnaan Date: Sat, 28 Mar 2026 10:31:28 +0530 Subject: [PATCH 3/5] fix: wrap standalone buttons in forms for client library compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standalone - - + +
+
Last updated: {{.LastUpdated}} diff --git a/counter/counter_test.go b/counter/counter_test.go index 4ed0472..1227c48 100644 --- a/counter/counter_test.go +++ b/counter/counter_test.go @@ -94,8 +94,28 @@ func TestCounterE2E(t *testing.T) { t.Log("✅ Initial page load verified") }) - // Note: Increment/Decrement tests removed due to chromedp timing issues - // Core functionality is verified by TestWebSocketBasic + t.Run("Button_Click_Increment", func(t *testing.T) { + var counterText string + + err := chromedp.Run(ctx, + e2etest.WaitFor(`window.liveTemplateClient && window.liveTemplateClient.isReady()`, 5*time.Second), + // Click the increment button + chromedp.Evaluate(`document.querySelector('button[name="increment"]').click()`, nil), + // Wait for counter to update from 0 to 1 + e2etest.WaitFor(`document.body.innerText.includes('Counter: 1')`, 5*time.Second), + chromedp.Evaluate(`document.querySelector('p').textContent`, &counterText), + ) + + if err != nil { + t.Fatalf("Failed to click increment button: %v", err) + } + + if !strings.Contains(counterText, "Counter: 1") { + t.Errorf("Expected 'Counter: 1', got %q", counterText) + } + + t.Log("✅ Button click increment works") + }) t.Run("WebSocket Connection", func(t *testing.T) { // Check for console errors diff --git a/flash-messages/flash.tmpl b/flash-messages/flash.tmpl index f5f544e..6bf5e65 100644 --- a/flash-messages/flash.tmpl +++ b/flash-messages/flash.tmpl @@ -41,9 +41,9 @@ -
- - +
+
+
diff --git a/graceful-shutdown/counter.tmpl b/graceful-shutdown/counter.tmpl index 75f25af..2556ede 100644 --- a/graceful-shutdown/counter.tmpl +++ b/graceful-shutdown/counter.tmpl @@ -14,9 +14,9 @@

Counter: {{.Counter}}

- - - +
+
+
Last updated: {{.LastUpdated}} diff --git a/observability/counter.tmpl b/observability/counter.tmpl index 75f25af..2556ede 100644 --- a/observability/counter.tmpl +++ b/observability/counter.tmpl @@ -14,9 +14,9 @@

Counter: {{.Counter}}

- - - +
+
+
Last updated: {{.LastUpdated}} diff --git a/production/single-host/app.tmpl b/production/single-host/app.tmpl index 6a9212a..3db4b8a 100644 --- a/production/single-host/app.tmpl +++ b/production/single-host/app.tmpl @@ -35,9 +35,9 @@
- - - +
+
+
diff --git a/todos-components/todos-components.tmpl b/todos-components/todos-components.tmpl index e9783de..86be9d8 100644 --- a/todos-components/todos-components.tmpl +++ b/todos-components/todos-components.tmpl @@ -74,7 +74,7 @@ {{if .Todos}}
{{len .Todos}} todo{{if gt (len .Todos) 1}}s{{end}} - +
{{end}}
diff --git a/todos/todos.tmpl b/todos/todos.tmpl index f1b31f5..13d3993 100644 --- a/todos/todos.tmpl +++ b/todos/todos.tmpl @@ -106,29 +106,35 @@ >
  • - +
    + +
  • Page {{ .CurrentPage }} of {{ .TotalPages }}
  • - +
    + +
@@ -203,9 +209,11 @@ {{template "pagination" .}} {{ if gt .CompletedCount 0 }} - +
+ +
{{ end }} From 5abfefec488d552bd1068a24e28a1c799fdc2e39 Mon Sep 17 00:00:00 2001 From: Adnaan Date: Sat, 28 Mar 2026 10:51:06 +0530 Subject: [PATCH 4/5] fix: revert form wrappers, use standalone buttons (client supports it) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client library v0.8.7 supports orphan button detection — standalone -
-
+ + +