feat: add realtime-collab example - complex collaborative dashboard#11
feat: add realtime-collab example - complex collaborative dashboard#11connerohnesorge wants to merge 4 commits intomainfrom
Conversation
… dashboard This example showcases Twerge's capability to optimize TailwindCSS classes in a production-ready collaborative application featuring: - Real-time WebSocket collaboration with live cursors and presence indicators - Complex dashboard with 150+ unique component states - Multi-user project management with role-based permissions - Document collaboration with version control and comments - Responsive design optimized for mobile/tablet/desktop - Dark/light theme support with system health monitoring - Interactive components with hover/focus/active states - Comprehensive data models and in-memory store - 62 component instances for thorough Twerge optimization testing The example includes: - 15+ files with complete implementation - WebSocket hub for real-time communication - RESTful API with 20+ endpoints - Templ templates with complex conditional styling - TailwindCSS configuration with custom utilities - Code generation script covering all component states - Comprehensive documentation and setup instructions This demonstrates how Twerge can handle enterprise-level applications with complex state management while maintaining excellent developer experience and optimized runtime performance. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
WalkthroughThis update introduces two comprehensive real-world example projects—an admin dashboard and a realtime collaborative dashboard—demonstrating Twerge's capabilities in optimizing TailwindCSS usage in Go templ applications. It adds all supporting source code, templates, CSS, tests, and documentation for these examples, alongside new specifications for design system, blog CMS, e-commerce catalog, and form validation use cases. Minor fixes and documentation improvements are also included. Changes
Sequence Diagram(s)Admin Dashboard Code Generation and Build WorkflowsequenceDiagram
participant Dev as Developer
participant Gen as gen.go (Admin Dashboard)
participant Twerge as Twerge CodeGen
participant Tailwind as Tailwind CLI
Dev->>Gen: Run gen.go
Gen->>Twerge: Generate Go, CSS, HTML from components
Twerge-->>Gen: Output classes.go, input.css, classes.html
Gen->>Tailwind: Run Tailwind build on input.css
Tailwind-->>Gen: Output bundled CSS
Realtime Collab Dashboard Server Startup and Real-Time FlowsequenceDiagram
participant Main as main.go
participant Store as Data Store
participant Hub as WebSocket Hub
participant Handlers as HTTP Handlers
participant User as Browser/User
Main->>Store: Initialize sample data
Main->>Hub: Start WebSocket hub
Main->>Handlers: Setup HTTP routes (REST, WS)
User->>Handlers: HTTP/WebSocket requests
Handlers->>Store: Data queries/updates
Handlers->>Hub: Real-time events (presence, cursor, typing)
Hub->>User: Broadcast real-time updates
Twerge Code Generation (Generalized)sequenceDiagram
participant Dev as Developer
participant Gen as gen.go (Example)
participant Views as Templated Components
participant Twerge as Twerge CodeGen
Dev->>Gen: Run code generation
Gen->>Views: Instantiate components with various states
Gen->>Twerge: Pass components to CodeGen
Twerge-->>Gen: Output optimized Go, CSS, HTML
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (1.64.8)level=warning msg="[runner] Can't run linter goanalysis_metalinter: buildir: failed to load package : could not load export data: no export data for "github.com/conneroisu/twerge"" ✨ Finishing Touches
🧪 Generate Unit Tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 30
♻️ Duplicate comments (1)
examples/realtime-collab/handlers/handlers.go (1)
512-515: Ditto: byte length incalculateReadTimeSame issue as above – switch to word count via
strings.Fields.
🧹 Nitpick comments (34)
.claude/settings.json (1)
12-20: Pin Playwright MCP server version
Using@playwright/mcp@latestcan introduce unintended updates. Consider specifying an exact version (e.g.,@playwright/mcp@1.x) to guarantee reproducible test runs.input.css (1)
1-7: Add generated-file notice
This file is auto-generated between/* twerge:begin */and/* twerge:end */. Please insert a header like/* Generated by Twerge – do not edit manually */and document its generation step..gitignore (2)
12-12: Group IDE entries consistently
Consider placing.vscodealongside.ideato keep all editor/IDE patterns in one section.
14-14: Reposition.direnvnear.env
Since both relate to environment configs, moving.direnvnext to.envimproves readability and grouping in the ignore list.examples/realtime-collab/package.json (1)
6-15: *Scripts are nix-only; breaks on Windows shells
rm -rfand chained quotes are POSIX-specific. If cross-platform support matters, replace withrimraf(already a zero-dep npm package) and usenpm-run-all/cross-envfor command-chaining.- "clean": "rm -rf _static/dist classes/classes.go classes/classes.html" + "clean": "rimraf _static/dist classes"Likewise, consider
concurrently’s JSON syntax to avoid escaped quotes:- "dev": "concurrently \"npm run dev:css\" \"npm run dev:templ\" \"go run main.go\"", + "dev": "concurrently -n css,templ,go \"npm:dev:css\" \"npm:dev:templ\" \"go run main.go\"",examples/realtime-collab/main.go (1)
43-46: Duplicate host prefix in log output
server.Addralready contains the leading:(e.g.:8080).
http://localhost%stherefore printshttp://localhost:8080– fine.
Howeverlog.Printf("🚀 Server starting on %s", server.Addr)will show:8080, not the full URL. Consider harmonising the messages for clarity.examples/admin-dashboard/main.go (1)
45-66: Re-implementing a static file server is unnecessaryInstead of manual slicing and switch logic, use
http.StripPrefix+http.FileServer:http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))Reduces code and automatically sets correct MIME types.
examples/admin-dashboard/input.css (1)
6-23: Add Firefox-friendly scrollbar rulesThe custom
.scrollbar-thinrules only style WebKit browsers. Firefox will silently ignore them unless you also set thescrollbar-widthand, optionally,scrollbar-colorproperties.@layer utilities { .scrollbar-thin::-webkit-scrollbar { width: 6px; height: 6px; } + + /* Firefox */ + .scrollbar-thin { + scrollbar-width: thin; /* 6 px track */ + scrollbar-color: theme(colors.gray.400) transparent; + }examples/admin-dashboard/gen.go (4)
17-31: Avoid silent working-directory failures
os.Chdir(*cwd)will panic if the path is invalid, but the panic message loses the user-supplied path. Wrap the error to make debugging easier:- err := os.Chdir(*cwd) - if err != nil { - panic(err) - } + if err := os.Chdir(*cwd); err != nil { + panic(fmt.Errorf("cannot chdir to %q: %w", *cwd, err)) + }
38-40: Use separate timers for each phase
startis overwritten, which makes the first defer print a meaningless duration. Track phase timers instead:- start := time.Now() - defer func() { fmt.Printf("(update-css) Done in %s.\n", time.Since(start)) }() + overallStart := time.Now() + defer func() { fmt.Printf("(update-css) Done in %s.\n", time.Since(overallStart)) }()
41-71: Pass components as a slice for readabilityThe 25-argument call to
twerge.CodeGenis hard to scan and easy to break. Consider assembling the components in a slice and splatting:comps := []templ.Component{ views.Dashboard(false), views.Dashboard(true), // …snip… views.StatusBadge(false), } if err := twerge.CodeGen(twerge.Default(), "classes/classes.go", "input.css", "classes/classes.html", - // Dashboard with sidebar open - // … - views.StatusBadge(false), - ); + comps...); // spread
86-91: Fail gracefully when Tailwind is missing
exec.Command("tailwindcss", …)will return “executable file not found” only after attempting to run. A quick pre-flight avoids an opaque stack trace:func runTailwind() { @@ - cmd := exec.Command("tailwindcss", "-i", "input.css", "-o", "static/dist/styles.css") + bin, lookErr := exec.LookPath("tailwindcss") + if lookErr != nil { + panic("tailwindcss CLI not found in PATH") + } + cmd := exec.Command(bin, "-i", "input.css", "-o", "static/dist/styles.css")examples/admin-dashboard/playwright.config.js (1)
70-75: Increase dev-server startup timeoutLarge Go builds or first-run module downloads regularly exceed 10 s. A 60 s window is safer for CI:
- timeout: 10000, + timeout: 60000,examples/realtime-collab/tailwind.config.js (1)
201-226: UnusedhasSubmenuparam & extra perf cost
getNavItemClasses()is called withhasSubmenubut the function discards it.
Drop the arg or use it to append the"group"class inside the helper to avoid repeated string concatenation and additional Twerge look-ups.examples/admin-dashboard/views/dashboard.templ (1)
247-256:strconvimported globally for tiny template need
strconv.Itoainside template loops is OK but the global import pulls it into every compilation unit.
Consider localising with{{ $i := printf "%d" (add 1 i) }}or move sample-data generation to Go code to keep templates clean.examples/admin-dashboard/tests/dashboard.test.js (1)
141-157:toHaveClass(/tw-0/)assumes body only has one Twerge class
bodymay receive multiple short classes over time; regex anchored to single token will break.Use
expect(await page.evaluate(() => document.body.className)).toMatch(/tw-\d+/)or drop style assertions in favour of functional checks.specs/admin-dashboard.md (1)
161-176: Code snippet passes generator twice
g.CodeGen(g, ...)is incorrect API based on earlier examples. Should betwerge.CodeGen(g, ...)org.CodeGen(...)depending on signature.Fix before developers copy-paste.
LLMS.txt (1)
73-87: Duplicate generator parameter
err := g.CodeGen( g, "views/gen_twerge.go", ...
gshouldn’t be both receiver and first arg.err := g.CodeGen( "views/gen_twerge.go", ... )examples/realtime-collab/input.css (1)
38-49: Glassmorphism blocks lack graceful fallback & are costly to render
backdrop-filteris still unsupported in several browsers and incurs a noticeable GPU cost. Consider:-.glass { - background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.2); -} +/* Fallback first, heavy effect last */ +.glass { + background: rgba(255, 255, 255, 0.8); /* ≤ Safari <15, Firefox <103 */ + @apply bg-white/10; /* Tailwind utility */ + border: 1px solid rgb(255 255 255 / .2); + @supports (backdrop-filter: blur(10px)) { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + } +}This preserves readability when the property is missing and avoids unnecessary GPU work on low-end devices.
examples/realtime-collab/gen.go (2)
223-240: Non-deterministic stats hamper reproducible builds
createMinimalStats(and most helpers) calltime.Now(). Every run regenerates different timestamps which changes the generated HTML and CSS fingerprints, breaking cacheability.Consider injecting a fixed
now time.Time(e.g. via a flag) and using it across helpers for deterministic output.
323-338: Minor:notificationsslice pre-allocationInside
createManyNotificationsyou append 15 items but start from an empty slice. Small, but pre-allocating improves clarity and avoids re-allocations:notifications := make([]types.Notification, 0, 15)CLAUDE.md (1)
1-4: Consider renamingCLAUDE.mdtoDEVELOPER_GUIDE.mdThe content is a general developer manual, not specific to Claude. A conventional name makes it discoverable to humans and documentation tooling.
specs/blog-cms.md (1)
140-161: Code snippet will not compile (min,strconv,slicesmissing)Readers copying the snippet will hit undefined identifiers/imports. Add the required imports or helper functions, e.g.:
import ( "strconv" "slices" "math" ) func min(a, b int) int { if a < b { return a }; return b }examples/realtime-collab/websocket/hub.go (1)
532-534: Improve message ID generation to avoid collisionsUsing
UnixNanofor message IDs could result in collisions in high-frequency scenarios. Consider using a more robust ID generation method.+import "github.com/google/uuid" func generateMessageID() string { - return fmt.Sprintf("%d", time.Now().UnixNano()) + return uuid.New().String() }Alternatively, if you want to avoid external dependencies:
+import "crypto/rand" +import "encoding/hex" func generateMessageID() string { - return fmt.Sprintf("%d", time.Now().UnixNano()) + bytes := make([]byte, 16) + if _, err := rand.Read(bytes); err != nil { + // Fallback to timestamp if random generation fails + return fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Int()) + } + return hex.EncodeToString(bytes) }examples/realtime-collab/handlers/handlers.go (1)
240-255: Broadcast may reference stale project nameAfter
UpdateProject, the in-memory copyprojectisn’t refreshed, so a rename broadcast shows the old name. Fetch the updated record before emitting the activity.examples/realtime-collab/types/models.go (1)
244-253: Consideromitemptyon optionalMetadata& booleansLarge zero-value payloads bloat network traffic. Adding
omitemptytoMetadata,IsImportantetc. keeps JSON lean.specs/design-system.md (8)
41-64: Clarify default enum fallback.
Usingdefaultin the const block is fine, but in the generatedget…Classesfunctions you recurse for the primary case. Consider making the default case explicit (VariantPrimary) without recursion to improve readability.
67-107: Break up long class concatenation and add ARIA attributes.
The inline string concatenation is hard to maintain. Consider building class lists via slices/join or helper functions. Also addaria-busy={loading}to the<button>for accessibility during loading states.
115-130: Duplicate size cases.
SizeSMandSizeMDcurrently return the same classes. If this is intentional, document it; otherwise, adjust the MD case to its intended padding/text size.
132-173: Consolidate variant styling logic.
ThegetButtonVariantClassesfunction is lengthy and repeats dark-mode prefixes. Consider extracting shared patterns or using a small DSL/helper to reduce duplication.
175-183: Enhance loading state behavior.
Whenloadingis true you setcursor-waitbut interactions are still possible. Consider addingpointer-events-noneor disabling the button to prevent clicks during the load state.
284-348: Explicit error state rather than default.
defaultingetInputStateClassesmaps to an “error” style, but there’s noStateErrorenum. IntroduceStateErroror rename the default to avoid conflating an unrecognized state with the error state.
596-668: Extract nested template into subcomponents.
The demo rendering loop is deeply nested. For readability and maintainability, consider breaking out the code table, preview, and details into smaller helper templates.
674-689: Prefer semantic class names over numeric.
Using generic.tw-1,.tw-2, etc. may conflict later. Consider.tw-modal-backdrop,.tw-modal-centeror clearly document the numbering scheme.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (4)
cmd/conneroh/_static/dist/style.cssis excluded by!**/dist/**examples/admin-dashboard/go.sumis excluded by!**/*.sumexamples/admin-dashboard/static/dist/styles.cssis excluded by!**/dist/**go.sumis excluded by!**/*.sum
📒 Files selected for processing (42)
.claude/settings.json(1 hunks).gitignore(1 hunks)CLAUDE.md(1 hunks)LLMS.txt(1 hunks)examples/admin-dashboard/classes/classes.go(1 hunks)examples/admin-dashboard/classes/classes.html(1 hunks)examples/admin-dashboard/gen.go(1 hunks)examples/admin-dashboard/go.mod(1 hunks)examples/admin-dashboard/input.css(1 hunks)examples/admin-dashboard/main.go(1 hunks)examples/admin-dashboard/package.json(1 hunks)examples/admin-dashboard/playwright.config.js(1 hunks)examples/admin-dashboard/tailwind.config.js(1 hunks)examples/admin-dashboard/tests/dashboard.test.js(1 hunks)examples/admin-dashboard/views/dashboard.templ(1 hunks)examples/admin-dashboard/views/dashboard_templ.go(1 hunks)examples/dashboard/views/dashboard_templ.go(52 hunks)examples/dashboard/views/report_templ.go(89 hunks)examples/dashboard/views/settings_templ.go(51 hunks)examples/dashboard/views/view_templ.go(15 hunks)examples/realtime-collab/README.md(1 hunks)examples/realtime-collab/data/store.go(1 hunks)examples/realtime-collab/gen.go(1 hunks)examples/realtime-collab/go.mod(1 hunks)examples/realtime-collab/handlers/handlers.go(1 hunks)examples/realtime-collab/input.css(1 hunks)examples/realtime-collab/main.go(1 hunks)examples/realtime-collab/package.json(1 hunks)examples/realtime-collab/tailwind.config.js(1 hunks)examples/realtime-collab/types/models.go(1 hunks)examples/realtime-collab/views/dashboard.templ(1 hunks)examples/realtime-collab/views/project.templ(1 hunks)examples/realtime-collab/websocket/hub.go(1 hunks)examples/simple-debug/views/view_templ.go(23 hunks)examples/simple/views/view_templ.go(1 hunks)go.mod(1 hunks)input.css(1 hunks)specs/admin-dashboard.md(1 hunks)specs/blog-cms.md(1 hunks)specs/design-system.md(1 hunks)specs/ecommerce-catalog.md(1 hunks)specs/form-validation.md(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (5)
examples/admin-dashboard/playwright.config.js (1)
examples/admin-dashboard/tests/dashboard.test.js (1)
require(2-2)
examples/admin-dashboard/gen.go (2)
tw.go (1)
CodeGen(26-56)examples/admin-dashboard/views/dashboard_templ.go (9)
Dashboard(31-248)Sidebar(251-440)Header(443-787)MetricsGrid(1029-1097)NavItem(790-1026)MetricCard(1100-1282)DataTable(1285-1499)TableHeader(1502-1620)StatusBadge(1623-1689)
examples/admin-dashboard/classes/classes.go (1)
twerge.go (3)
Default(35-35)Handler(72-76)CacheValue(17-26)
examples/realtime-collab/gen.go (6)
examples/realtime-collab/data/store.go (1)
NewStore(33-48)examples/realtime-collab/views/dashboard_templ.go (12)
DashboardLayout(17-182)NavigationSidebar(184-545)SystemHealthWidget(789-953)TopBar(1155-1378)DashboardContent(2176-2323)WelcomeHero(2325-2581)StatsOverview(2583-2648)RecentActivityFeed(2881-3122)ProjectStatusChart(3534-3633)TopContributors(3813-3922)ActiveProjects(4241-4428)NotificationCenter(4948-5267)examples/realtime-collab/types/models.go (54)
Notification(333-346)ActivityEvent(244-253)Project(42-58)Document(123-143)User(7-20)StatusOffline(28-28)RoleViewer(38-38)StatusBusy(27-27)RoleEditor(37-37)StatusOnline(25-25)RoleGuest(39-39)RoleAdmin(35-35)DashboardStats(414-429)ProjectStatus(78-78)ProjectStatusDraft(81-81)ProjectPriority(89-89)PriorityLow(92-92)ChartDataPoint(431-436)SystemHealth(438-446)ProjectStatusActive(82-82)ProjectStatusCompleted(84-84)ProjectStatusOnHold(83-83)PriorityCritical(95-95)PriorityHigh(94-94)PriorityMedium(93-93)ProjectStatusArchived(85-85)HealthStatusHealthy(451-451)HealthStatusWarning(452-452)HealthStatusCritical(453-453)NotificationType(348-348)NotificationTypeInfo(351-351)NotificationTypeWarning(353-353)NotificationTypeSuccess(352-352)NotificationPriority(358-358)NotificationPriorityLow(361-361)NotificationPriorityMedium(362-362)NotificationPriorityHigh(363-363)ActivityType(255-255)ActivityDocumentCreated(264-264)ActivityDocumentUpdated(265-265)ActivityCommentAdded(267-267)ActivityProjectUpdated(259-259)ActivityMemberAdded(261-261)ActivityTarget(273-277)ProjectAnalytics(111-121)DocumentType(145-145)DocumentStatus(157-157)ProjectMember(60-66)ProjectRole(68-68)ProjectRoleOwner(71-71)ProjectRoleAdmin(72-72)ProjectRoleCollaborator(73-73)ProjectRoleReviewer(74-74)ProjectRoleObserver(75-75)examples/realtime-collab/views/project_templ.go (10)
ProjectView(17-178)ProjectSidebar(180-538)ProjectHeader(1005-1226)ProjectContent(1860-2003)ProjectOverview(2005-2677)QuickStats(2679-2744)RecentDocuments(2986-3161)ProjectTimeline(3779-3954)TeamMembers(4455-4620)ProjectTasks(5019-5216)tw.go (1)
CodeGen(26-56)twerge.go (1)
Default(35-35)
examples/realtime-collab/websocket/hub.go (1)
examples/realtime-collab/types/models.go (15)
Room(322-331)User(7-20)Cursor(279-287)UserStatus(22-22)WebSocketMessage(296-303)MessageTypeUserJoined(310-310)MessageTypeUserLeft(311-311)MessageTypeJoinRoom(308-308)MessageTypeLeaveRoom(309-309)MessageTypeCursorMove(313-313)Selection(289-294)MessageTypeUserTyping(312-312)MessageTypeDocumentChange(314-314)MessageTypeCommentAdded(315-315)MessageTypePresenceUpdate(317-317)
🪛 HTMLHint (1.5.0)
examples/admin-dashboard/classes/classes.html
[error] 4-4: Doctype must be declared before any non-comment content.
(doctype-first)
🪛 LanguageTool
examples/realtime-collab/README.md
[style] ~342-~342: Consider using a less common alternative to make your writing sound more unique and professional.
Context: ...collaborative applications with Twerge. Feel free to: 1. Extend the data models - Add n...
(FEEL_FREE_TO_STYLE_ME)
CLAUDE.md
[uncategorized] ~472-~472: Loose punctuation mark.
Context: ...-ready implementations: - simple/: Basic integration pattern, minimal setu...
(UNLIKELY_OPENING_PUNCTUATION)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: checks
🔇 Additional comments (31)
go.mod (1)
6-6: Verify thetempldependency version
Ensure thatv0.3.898is released and compatible with the new collaborative and admin-dashboard examples. After merging, rungo mod tidyto confirm all imports resolve correctly..claude/settings.json (1)
3-11: Permissions configuration looks appropriate
The allow-list fornix develop -c :*andnpm run test:*and deny-list forcurl:*align with the intended security posture.examples/dashboard/views/view_templ.go (2)
3-3: Bump templ version to v0.3.898
Updated the generated code comment to reflect the new templ runtime version.
41-41: Use full relative file path in error reports
Alltempl.Errorreturn statements now includeexamples/dashboard/views/view.templfor clearer error context. This matches the conventions in other example templates.Also applies to: 63-63, 85-85, 108-108, 130-130, 152-152, 174-174, 196-196, 218-218, 240-240, 262-262, 288-288, 310-310, 332-332
examples/simple-debug/views/view_templ.go (2)
3-3: Bump templ version to v0.3.898
The generated code comment is updated to the current templ runtime version.
50-50: Use full relative file path in error reports
Thetempl.Errorreturns now referenceexamples/simple-debug/views/view.templ, ensuring consistent and accurate error reporting across templates.Also applies to: 72-72, 94-94, 116-116, 138-138, 160-160, 182-182, 204-204, 226-226, 248-248, 270-270, 292-292, 314-314, 336-336, 358-358, 381-381, 403-403, 425-425, 447-447, 469-469, 491-491, 513-513
examples/admin-dashboard/classes/classes.html (1)
1-64: Add purge-safe Tailwind class container
Introduces a generated HTML fragment listing all optimizedtw-0–tw-59classes to ensure they are retained during Tailwind's purge process.🧰 Tools
🪛 HTMLHint (1.5.0)
[error] 4-4: Doctype must be declared before any non-comment content.
(doctype-first)
examples/admin-dashboard/package.json (1)
1-14: Add Playwright test configuration
Defines project metadata and scripts for running and debugging Playwright tests in the admin-dashboard example.examples/dashboard/views/settings_templ.go (2)
3-3: Generated-code version bump looks fineNo functional impact.
58-60: Path prefix update improves error tracesSwitching to the full relative path will make stack traces far easier to follow.
examples/dashboard/views/dashboard_templ.go (2)
3-3: Generated-code version bump acknowledgedNothing else to flag here.
58-60: Consistent file-path in templ.ErrorGood catch; unified paths aid debugging across examples.
examples/realtime-collab/go.mod (1)
3-3: Verify Go toolchain version
go 1.23is not released yet. Unless you’re intentionally targeting the dev-branch toolchain, bumping prematurely can break CI and downstream consumers.-go 1.23 +go 1.22examples/dashboard/views/report_templ.go (1)
55-60: Path prefix update looks goodGenerated error wrappers now contain the full relative path, improving stack-traces. No functional impact.
Also applies to: 77-82
examples/admin-dashboard/input.css (1)
25-327: Verify duplicate utility bundlesThere are two almost-identical bundles (
tw-4vstw-58) that differ only in the base translation class (tw-3vstw-2). If both variants are required, no action is needed, otherwise consider deduplicating to keep the generated CSS leaner (~5 KB per duplicate).examples/admin-dashboard/playwright.config.js (1)
22-23: Confirm port alignmentThe dashboard server started by
go run main.gomust serve on 8081 or Playwright navigation will fail. Double-checkmain.go(and the README) so the example doesn’t 404 in CI.examples/admin-dashboard/classes/classes.go (1)
6-8: EnsureSetCache()is invokedThe cache is registered only when
SetCache()is called. If no other file imports this package and calls the function (e.g. ininit()), the template classes will expand to full strings at runtime, defeating the optimisation.examples/simple/views/view_templ.go (1)
518-519: Missing script asset
/static/js/main.jsis referenced but not generated anywhere in this PR. Either remove the tag or commit a stub JS file to avoid a 404.examples/admin-dashboard/views/dashboard.templ (1)
160-178: String concatenation defeats Twerge deduping
getNavItemClasses(...) + " group"appends a raw utility after Twerge has already generated a short class.
The final string becomes"tw-10 group"(OK) but you lose the chance for Twerge to merge"group"with others and risk duplicates elsewhere.
Prefer passing the full set into a singletwerge.It()call.examples/realtime-collab/input.css (1)
320-333:prefers-contrast: highhas near-zero browser supportOnly very recent Safari Technology Preview recognises this media query. Shipping it alone gives a false sense of accessibility coverage. At minimum, add a comment and a TODO to revisit when support stabilises, or fall back to a user-controlled “high-contrast” toggle.
examples/realtime-collab/README.md (1)
71-78: Go 1.23 is not released yetThe current stable release is 1.22.x. Depending on an unreleased tool-chain blocks contributors and CI. Please verify the minimum required version and update the docs (or pin to the latest stable).
specs/design-system.md (10)
109-113: Button base classes helper is concise and clear.
No issues found here.
200-218: CardHeader structure is well-defined.
The conditional rendering of subtitle and actions is concise and accessible.
220-223: CardContent wrapper is minimal and to the point.
No concerns.
226-248: CardFooter alignment helper is clear.
The switch covers all justify cases with a sensible default.
251-279: Ensure required imports and typed variants.
You usefmt.Sprintfin the template but don’t show importingfmt. Also, hard-coded strings like"success"could be replaced by aBadgeVarianttype to reduce typos.
350-354: Input base classes helper is solid.
Well-structured and clear.
356-371: Confirm size definitions.
BothSizeSMandSizeMDmap totext-sm. Verify that MD shouldn’t perhaps usetext-base.
555-568: Modal size helper is concise.
No issues.
573-585: ComponentShowcase layout is clear and extensible.
Good use of iteration for demos.
588-595: ComponentDemo struct fields are appropriate.
Props map allows flexibility; consider documenting expected types in comments.
| for _, category := range categories { | ||
| @FilterCheckbox( | ||
| category, | ||
| slices.Contains(selected, category), | ||
| len(getProductsInCategory(category)) | ||
| ) | ||
| } |
There was a problem hiding this comment.
Undefined categories identifier in filter loop
The code for _, category := range categories should iterate over the passed-in slice (e.g., filters.Categories), not an undefined categories variable.
🤖 Prompt for AI Agents
In specs/ecommerce-catalog.md around lines 346 to 352, the loop uses an
undefined variable `categories`. Replace `categories` with the correct slice
variable passed into the function, such as `filters.Categories`, to properly
iterate over the intended category list.
| templ ProductCard(product Product, inCart bool, favorited bool) { | ||
| <div class={twerge.It( | ||
| "group relative bg-white dark:bg-gray-800 rounded-xl shadow-sm hover:shadow-lg " + | ||
| "transition-all duration-300 overflow-hidden border border-gray-200 dark:border-gray-700 " + | ||
| "hover:border-gray-300 dark:hover:border-gray-600 " + | ||
| twerge.If(product.Featured, "ring-2 ring-blue-500 ring-opacity-50", "") + |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Missing imports in ProductCard snippet
The template references fmt.Sprintf, twerge.If, and getColorClass without showing an import block. Add imports for "fmt" and "github.com/conneroisu/twerge", and document or define getColorClass.
🤖 Prompt for AI Agents
In specs/ecommerce-catalog.md around lines 28 to 33, the ProductCard template
uses fmt.Sprintf, twerge.If, and getColorClass without importing the necessary
packages or defining getColorClass. Add an import block including "fmt" and
"github.com/conneroisu/twerge" at the top of the snippet, and either provide a
definition or documentation for the getColorClass function to clarify its usage.
| go 1.24.1 | ||
|
|
||
| require ( | ||
| github.com/a-h/templ v0.3.857 |
There was a problem hiding this comment.
Sync templ dependency version
The generated templates target github.com/a-h/templ v0.3.898, but this module requires v0.3.857. Update to match the generated code.
Apply this diff:
-require (
- github.com/a-h/templ v0.3.857
+require (
+ github.com/a-h/templ v0.3.898Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In examples/admin-dashboard/go.mod at line 6, the templ dependency version is
outdated (v0.3.857) and should be updated to v0.3.898 to match the generated
templates. Change the version number of github.com/a-h/templ from v0.3.857 to
v0.3.898 to ensure compatibility.
| github.com/conneroisu/twerge v0.0.0 | ||
| github.com/go-chi/chi/v5 v5.1.0 |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Pin a real version for github.com/conneroisu/twerge
v0.0.0 is a pseudo placeholder. Use a tagged or commit-hash version to make the example reproducible; the local replace directive remains fine for iterative work.
🤖 Prompt for AI Agents
In examples/realtime-collab/go.mod at lines 7 to 8, the dependency
github.com/conneroisu/twerge is pinned to the placeholder version v0.0.0, which
is not a real version. Replace this with a specific tagged version or a commit
hash to ensure reproducibility of the example, while keeping the local replace
directive for iterative development if needed.
| corePlugins: { | ||
| // Ensure group utilities are available | ||
| group: true, | ||
| }, |
There was a problem hiding this comment.
corePlugins.group is not a valid Tailwind core-plugin
group is a variant that’s always available – it isn’t a core plugin that can be toggled.
Leaving this block in will make Tailwind fail with: “group is not a core plugin”.
- corePlugins: {
- // Ensure group utilities are available
- group: true,
- },
+ // No need to touch `corePlugins`; `group-hover` / `group-focus` etc. just work.🤖 Prompt for AI Agents
In examples/admin-dashboard/tailwind.config.js around lines 39 to 42, the
configuration incorrectly sets corePlugins.group to true, but 'group' is not a
valid core plugin in Tailwind CSS. Remove the entire corePlugins block that
references 'group' to prevent Tailwind from failing with an invalid core plugin
error.
| func getInputStateClasses(state ComponentState) string { | ||
| switch state { | ||
| case StateDefault: | ||
| return "border-gray-300 dark:border-gray-600 " + | ||
| "focus:border-blue-500 dark:focus:border-blue-400 " + | ||
| "focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" | ||
| case StateFocus: | ||
| return "border-blue-500 dark:border-blue-400 " + | ||
| "ring-2 ring-blue-500 ring-opacity-50" | ||
| case StateDisabled: | ||
| return "border-gray-200 dark:border-gray-700 " + | ||
| "bg-gray-50 dark:bg-gray-900 cursor-not-allowed opacity-50" | ||
| default: | ||
| // Error state | ||
| return "border-red-500 dark:border-red-400 " + | ||
| "focus:border-red-500 dark:focus:border-red-400 " + | ||
| "focus:ring-2 focus:ring-red-500 focus:ring-opacity-50" | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Separate error from default state.
Current default branch returns the error style. Consider making the error state explicit and handling unknown states separately.
🤖 Prompt for AI Agents
In specs/design-system.md around lines 373 to 391, the default case in
getInputStateClasses returns the error state styles, which conflates error with
unknown states. Refactor the function to explicitly handle an error state case
with its own styles, and add a separate default case to handle unknown states,
possibly returning a safe fallback style or an empty string.
| // Card component system with composition | ||
| templ Card(variant string, padding bool, shadow bool, children ...templ.Component) { | ||
| <div class={twerge.It( | ||
| "bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 " + | ||
| twerge.If(padding, "p-6", "") + | ||
| getCardShadowClasses(shadow) + | ||
| getCardVariantClasses(variant) | ||
| )}> | ||
| { children... } | ||
| </div> | ||
| } |
There was a problem hiding this comment.
Missing helper implementations.
getCardShadowClasses and getCardVariantClasses are referenced but not defined in this spec. Include or document these functions to ensure completeness.
🤖 Prompt for AI Agents
In specs/design-system.md around lines 188 to 198, the functions
getCardShadowClasses and getCardVariantClasses are used but not defined or
documented. To fix this, add the implementations or detailed documentation for
these helper functions within the spec to clarify their behavior and ensure the
Card component is fully understandable and functional.
| //go:build ignore | ||
|
|
||
| package main | ||
|
|
||
| import ( | ||
| "github.com/conneroisu/twerge" | ||
| "github.com/yourproject/design-system/components" | ||
| "github.com/yourproject/design-system/types" | ||
| ) | ||
|
|
||
| func main() { | ||
| var demoComponents []templ.Component | ||
|
|
||
| // Generate all button combinations | ||
| sizes := []types.ComponentSize{types.SizeXS, types.SizeSM, types.SizeMD, types.SizeLG, types.SizeXL} | ||
| variants := []types.ComponentVariant{ | ||
| types.VariantPrimary, types.VariantSecondary, | ||
| types.VariantOutline, types.VariantGhost, types.VariantLink, | ||
| } | ||
| states := []bool{false, true} // disabled states | ||
|
|
||
| for _, size := range sizes { | ||
| for _, variant := range variants { | ||
| for _, disabled := range states { | ||
| for _, loading := range states { | ||
| demoComponents = append(demoComponents, | ||
| components.Button(variant, size, disabled, loading, "", "", "Sample Button"), | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Generate all input combinations | ||
| inputStates := []types.ComponentState{ | ||
| types.StateDefault, types.StateFocus, types.StateDisabled, | ||
| } | ||
|
|
||
| for _, size := range sizes { | ||
| for _, state := range inputStates { | ||
| demoComponents = append(demoComponents, | ||
| components.Input("text", "Placeholder", "", size, state, "", "", "", ""), | ||
| components.Input("text", "With left icon", "", size, state, "search", "", "", ""), | ||
| components.Input("text", "With right icon", "", size, state, "", "check", "", ""), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // Generate toast variations | ||
| toastTypes := []types.ToastType{ | ||
| types.ToastSuccess, types.ToastError, types.ToastWarning, types.ToastInfo, | ||
| } | ||
|
|
||
| for _, toastType := range toastTypes { | ||
| demoComponents = append(demoComponents, | ||
| components.Toast(toastType, "Sample Toast", "This is a sample message", true, false, 5), | ||
| components.Toast(toastType, "Auto-close Toast", "This will auto-close", true, true, 3), | ||
| ) | ||
| } | ||
|
|
||
| // Generate modal variations | ||
| for _, size := range []types.ComponentSize{types.SizeSM, types.SizeMD, types.SizeLG, types.SizeXL} { | ||
| demoComponents = append(demoComponents, | ||
| components.Modal(true, size, true, "Sample Modal", components.Text("Modal content here")), | ||
| ) | ||
| } | ||
|
|
||
| if err := twerge.CodeGen( | ||
| twerge.Default(), | ||
| "design-system/generated.go", | ||
| "design-system/input.css", | ||
| "design-system/classes.html", | ||
| demoComponents..., | ||
| ); err != nil { | ||
| panic(err) | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Update placeholder imports and improve error handling.
The script imports github.com/yourproject/...—replace these with the actual module path. Rather than panic, consider returning/logging the error for smoother CI integration.
🤖 Prompt for AI Agents
In specs/design-system.md around lines 700 to 776, replace the placeholder
import paths "github.com/yourproject/..." with the actual module paths used in
your project to ensure correct package resolution. Additionally, modify the
error handling after the twerge.CodeGen call to avoid using panic; instead,
return the error or log it appropriately to allow smoother integration with CI
pipelines and better error management.
| // Design tokens as structured data | ||
| type DesignTokens struct { | ||
| Colors ColorPalette | ||
| Spacing SpacingScale | ||
| Typography TypographyScale | ||
| Shadows ShadowScale | ||
| Borders BorderScale | ||
| } | ||
|
|
||
| type ColorPalette struct { | ||
| Primary ColorScale | ||
| Secondary ColorScale | ||
| Neutral ColorScale | ||
| Success ColorScale | ||
| Warning ColorScale | ||
| Error ColorScale | ||
| } | ||
|
|
||
| type ColorScale struct { | ||
| 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 string | ||
| } |
There was a problem hiding this comment.
Invalid struct field identifiers.
Go struct fields cannot be numeric literals (e.g., 50, 100, …). This will fail to compile. Consider renaming fields (e.g., F50, Level50) or switching to a map-based representation for scales.
🤖 Prompt for AI Agents
In specs/design-system.md around lines 18 to 38, the struct ColorScale uses
numeric literals as field names, which is invalid in Go. Rename these fields to
valid identifiers such as prefixing with a letter (e.g., F50, Level50) or
alternatively change the ColorScale struct to use a map[string]string to
represent the scale levels dynamically.
| templ Modal( | ||
| open bool, | ||
| size ComponentSize, | ||
| closable bool, | ||
| title string, | ||
| children ...templ.Component, | ||
| ) { | ||
| if open { | ||
| <!-- Backdrop --> | ||
| <div class={twerge.It( | ||
| "fixed inset-0 z-40 bg-black bg-opacity-50 " + | ||
| "transition-opacity duration-300 " + | ||
| "animate-fade-in" | ||
| )} | ||
| onclick="closeModal()" | ||
| ></div> | ||
|
|
||
| <!-- Modal --> | ||
| <div class={twerge.It( | ||
| "fixed inset-0 z-50 flex items-center justify-center p-4 " + | ||
| "animate-fade-in" | ||
| )}> | ||
| <div class={twerge.It( | ||
| "relative w-full max-h-full bg-white dark:bg-gray-800 " + | ||
| "rounded-lg shadow-xl " + | ||
| "transform transition-all duration-300 " + | ||
| "animate-scale-in " + | ||
| getModalSizeClasses(size) | ||
| )}> | ||
| <!-- Header --> | ||
| if title != "" || closable { | ||
| <div class={twerge.It( | ||
| "flex items-center justify-between p-6 " + | ||
| "border-b border-gray-200 dark:border-gray-700" | ||
| )}> | ||
| if title != "" { | ||
| <h3 class={twerge.It("text-lg font-semibold text-gray-900 dark:text-white")}> | ||
| { title } | ||
| </h3> | ||
| } | ||
| if closable { | ||
| <button class={twerge.It( | ||
| "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 " + | ||
| "transition-colors duration-200 p-1 rounded-md " + | ||
| "hover:bg-gray-100 dark:hover:bg-gray-700" | ||
| )} | ||
| onclick="closeModal()" | ||
| > | ||
| @Icon("x", "lg") | ||
| </button> | ||
| } | ||
| </div> | ||
| } | ||
|
|
||
| <!-- Content --> | ||
| <div class={twerge.It("p-6")}> | ||
| { children... } | ||
| </div> | ||
| </div> | ||
| </div> | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Define or stub closeModal() and add accessibility roles.
The backdrop and modal reference a closeModal() JS handler that isn’t shown. Also add role="dialog" and aria-modal="true" for screen readers.
🤖 Prompt for AI Agents
In specs/design-system.md around lines 492 to 553, the Modal component uses a
closeModal() function in the backdrop and close button onclick handlers, but
this function is not defined or stubbed. Define or stub a closeModal()
JavaScript handler to handle modal closing. Additionally, add accessibility
attributes role="dialog" and aria-modal="true" to the modal container div to
improve screen reader support.
Summary
This PR adds a sophisticated real-time collaborative dashboard example that demonstrates Twerge's capability to optimize TailwindCSS classes in production-ready applications with complex state management.
Features Added
✨ Real-time Collaboration
🏗️ Complex Architecture
🎨 UI Complexity
📊 Example Includes
Technical Implementation
Testing
The example has been tested and verified to:
go run gen.goFiles Added
How to Run
Why This Matters
This example demonstrates that Twerge can handle enterprise-level applications with:
It serves as a comprehensive reference for building collaborative applications while leveraging Twerge's optimization capabilities.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests
Chores
Style
Refactor