Clean, modern building blocks. Copy and paste into your apps. Built with Go and templ. Open Source. Free forever.
Registry reference: these templ ports track the current shadcn/ui Base UI blocks. The public shadcn block previews can still show legacy new-york-v4/Radix variants, so their block-level details may differ.
package dashboard01 import "github.com/axadrn/shadcn-templ/v2/components/sidebar" templ Page() { @sidebar.Provider(sidebar.ProviderProps{ Attributes: templ.Attributes{ "style": "--sidebar-width: calc(var(--spacing) * 72); --sidebar-width-icon: 3rem; --header-height: calc(var(--spacing) * 12);", }, }) { @AppSidebar() @sidebar.Inset() { @SiteHeader() <div class="flex flex-1 flex-col"> <div class="@container/main flex flex-1 flex-col gap-2"> <div class="flex flex-col gap-4 py-4 md:gap-6 md:py-6"> @SectionCards() <div class="px-4 lg:px-6"> @ChartAreaInteractive() </div> @DataTable(Data) </div> </div> </div> } }}package dashboard01 // Item represents one row of data.json.type Item struct { ID int Header string SectionType string Status string Target string Limit string Reviewer string} // Data holds all rows of data.json verbatim.var Data = []Item{ {ID: 1, Header: "Cover page", SectionType: "Cover page", Status: "In Process", Target: "18", Limit: "5", Reviewer: "Eddie Lake"}, {ID: 2, Header: "Table of contents", SectionType: "Table of contents", Status: "Done", Target: "29", Limit: "24", Reviewer: "Eddie Lake"}, {ID: 3, Header: "Executive summary", SectionType: "Narrative", Status: "Done", Target: "10", Limit: "13", Reviewer: "Eddie Lake"}, {ID: 4, Header: "Technical approach", SectionType: "Narrative", Status: "Done", Target: "27", Limit: "23", Reviewer: "Jamik Tashpulatov"}, {ID: 5, Header: "Design", SectionType: "Narrative", Status: "In Process", Target: "2", Limit: "16", Reviewer: "Jamik Tashpulatov"}, {ID: 6, Header: "Capabilities", SectionType: "Narrative", Status: "In Process", Target: "20", Limit: "8", Reviewer: "Jamik Tashpulatov"}, {ID: 7, Header: "Integration with existing systems", SectionType: "Narrative", Status: "In Process", Target: "19", Limit: "21", Reviewer: "Jamik Tashpulatov"}, {ID: 8, Header: "Innovation and Advantages", SectionType: "Narrative", Status: "Done", Target: "25", Limit: "26", Reviewer: "Assign reviewer"}, {ID: 9, Header: "Overview of EMR's Innovative Solutions", SectionType: "Technical content", Status: "Done", Target: "7", Limit: "23", Reviewer: "Assign reviewer"}, {ID: 10, Header: "Advanced Algorithms and Machine Learning", SectionType: "Narrative", Status: "Done", Target: "30", Limit: "28", Reviewer: "Assign reviewer"}, {ID: 11, Header: "Adaptive Communication Protocols", SectionType: "Narrative", Status: "Done", Target: "9", Limit: "31", Reviewer: "Assign reviewer"}, {ID: 12, Header: "Advantages Over Current Technologies", SectionType: "Narrative", Status: "Done", Target: "12", Limit: "0", Reviewer: "Assign reviewer"}, {ID: 13, Header: "Past Performance", SectionType: "Narrative", Status: "Done", Target: "22", Limit: "33", Reviewer: "Assign reviewer"}, {ID: 14, Header: "Customer Feedback and Satisfaction Levels", SectionType: "Narrative", Status: "Done", Target: "15", Limit: "34", Reviewer: "Assign reviewer"}, {ID: 15, Header: "Implementation Challenges and Solutions", SectionType: "Narrative", Status: "Done", Target: "3", Limit: "35", Reviewer: "Assign reviewer"}, {ID: 16, Header: "Security Measures and Data Protection Policies", SectionType: "Narrative", Status: "In Process", Target: "6", Limit: "36", Reviewer: "Assign reviewer"}, {ID: 17, Header: "Scalability and Future Proofing", SectionType: "Narrative", Status: "Done", Target: "4", Limit: "37", Reviewer: "Assign reviewer"}, {ID: 18, Header: "Cost-Benefit Analysis", SectionType: "Plain language", Status: "Done", Target: "14", Limit: "38", Reviewer: "Assign reviewer"}, {ID: 19, Header: "User Training and Onboarding Experience", SectionType: "Narrative", Status: "Done", Target: "17", Limit: "39", Reviewer: "Assign reviewer"}, {ID: 20, Header: "Future Development Roadmap", SectionType: "Narrative", Status: "Done", Target: "11", Limit: "40", Reviewer: "Assign reviewer"}, {ID: 21, Header: "System Architecture Overview", SectionType: "Technical content", Status: "In Process", Target: "24", Limit: "18", Reviewer: "Maya Johnson"}, {ID: 22, Header: "Risk Management Plan", SectionType: "Narrative", Status: "Done", Target: "15", Limit: "22", Reviewer: "Carlos Rodriguez"}, {ID: 23, Header: "Compliance Documentation", SectionType: "Legal", Status: "In Process", Target: "31", Limit: "27", Reviewer: "Sarah Chen"}, {ID: 24, Header: "API Documentation", SectionType: "Technical content", Status: "Done", Target: "8", Limit: "12", Reviewer: "Raj Patel"}, {ID: 25, Header: "User Interface Mockups", SectionType: "Visual", Status: "In Process", Target: "19", Limit: "25", Reviewer: "Leila Ahmadi"}, {ID: 26, Header: "Database Schema", SectionType: "Technical content", Status: "Done", Target: "22", Limit: "20", Reviewer: "Thomas Wilson"}, {ID: 27, Header: "Testing Methodology", SectionType: "Technical content", Status: "In Process", Target: "17", Limit: "14", Reviewer: "Assign reviewer"}, {ID: 28, Header: "Deployment Strategy", SectionType: "Narrative", Status: "Done", Target: "26", Limit: "30", Reviewer: "Eddie Lake"}, {ID: 29, Header: "Budget Breakdown", SectionType: "Financial", Status: "In Process", Target: "13", Limit: "16", Reviewer: "Jamik Tashpulatov"}, {ID: 30, Header: "Market Analysis", SectionType: "Research", Status: "Done", Target: "29", Limit: "32", Reviewer: "Sophia Martinez"}, {ID: 31, Header: "Competitor Comparison", SectionType: "Research", Status: "In Process", Target: "21", Limit: "19", Reviewer: "Assign reviewer"}, {ID: 32, Header: "Maintenance Plan", SectionType: "Technical content", Status: "Done", Target: "16", Limit: "23", Reviewer: "Alex Thompson"}, {ID: 33, Header: "User Personas", SectionType: "Research", Status: "In Process", Target: "27", Limit: "24", Reviewer: "Nina Patel"}, {ID: 34, Header: "Accessibility Compliance", SectionType: "Legal", Status: "Done", Target: "18", Limit: "21", Reviewer: "Assign reviewer"}, {ID: 35, Header: "Performance Metrics", SectionType: "Technical content", Status: "In Process", Target: "23", Limit: "26", Reviewer: "David Kim"}, {ID: 36, Header: "Disaster Recovery Plan", SectionType: "Technical content", Status: "Done", Target: "14", Limit: "17", Reviewer: "Jamik Tashpulatov"}, {ID: 37, Header: "Third-party Integrations", SectionType: "Technical content", Status: "In Process", Target: "25", Limit: "28", Reviewer: "Eddie Lake"}, {ID: 38, Header: "User Feedback Summary", SectionType: "Research", Status: "Done", Target: "20", Limit: "15", Reviewer: "Assign reviewer"}, {ID: 39, Header: "Localization Strategy", SectionType: "Narrative", Status: "In Process", Target: "12", Limit: "19", Reviewer: "Maria Garcia"}, {ID: 40, Header: "Mobile Compatibility", SectionType: "Technical content", Status: "Done", Target: "28", Limit: "31", Reviewer: "James Wilson"}, {ID: 41, Header: "Data Migration Plan", SectionType: "Technical content", Status: "In Process", Target: "19", Limit: "22", Reviewer: "Assign reviewer"}, {ID: 42, Header: "Quality Assurance Protocols", SectionType: "Technical content", Status: "Done", Target: "30", Limit: "33", Reviewer: "Priya Singh"}, {ID: 43, Header: "Stakeholder Analysis", SectionType: "Research", Status: "In Process", Target: "11", Limit: "14", Reviewer: "Eddie Lake"}, {ID: 44, Header: "Environmental Impact Assessment", SectionType: "Research", Status: "Done", Target: "24", Limit: "27", Reviewer: "Assign reviewer"}, {ID: 45, Header: "Intellectual Property Rights", SectionType: "Legal", Status: "In Process", Target: "17", Limit: "20", Reviewer: "Sarah Johnson"}, {ID: 46, Header: "Customer Support Framework", SectionType: "Narrative", Status: "Done", Target: "22", Limit: "25", Reviewer: "Jamik Tashpulatov"}, {ID: 47, Header: "Version Control Strategy", SectionType: "Technical content", Status: "In Process", Target: "15", Limit: "18", Reviewer: "Assign reviewer"}, {ID: 48, Header: "Continuous Integration Pipeline", SectionType: "Technical content", Status: "Done", Target: "26", Limit: "29", Reviewer: "Michael Chen"}, {ID: 49, Header: "Regulatory Compliance", SectionType: "Legal", Status: "In Process", Target: "13", Limit: "16", Reviewer: "Assign reviewer"}, {ID: 50, Header: "User Authentication System", SectionType: "Technical content", Status: "Done", Target: "28", Limit: "31", Reviewer: "Eddie Lake"}, {ID: 51, Header: "Data Analytics Framework", SectionType: "Technical content", Status: "In Process", Target: "21", Limit: "24", Reviewer: "Jamik Tashpulatov"}, {ID: 52, Header: "Cloud Infrastructure", SectionType: "Technical content", Status: "Done", Target: "16", Limit: "19", Reviewer: "Assign reviewer"}, {ID: 53, Header: "Network Security Measures", SectionType: "Technical content", Status: "In Process", Target: "29", Limit: "32", Reviewer: "Lisa Wong"}, {ID: 54, Header: "Project Timeline", SectionType: "Planning", Status: "Done", Target: "14", Limit: "17", Reviewer: "Eddie Lake"}, {ID: 55, Header: "Resource Allocation", SectionType: "Planning", Status: "In Process", Target: "27", Limit: "30", Reviewer: "Assign reviewer"}, {ID: 56, Header: "Team Structure and Roles", SectionType: "Planning", Status: "Done", Target: "20", Limit: "23", Reviewer: "Jamik Tashpulatov"}, {ID: 57, Header: "Communication Protocols", SectionType: "Planning", Status: "In Process", Target: "15", Limit: "18", Reviewer: "Assign reviewer"}, {ID: 58, Header: "Success Metrics", SectionType: "Planning", Status: "Done", Target: "30", Limit: "33", Reviewer: "Eddie Lake"}, {ID: 59, Header: "Internationalization Support", SectionType: "Technical content", Status: "In Process", Target: "23", Limit: "26", Reviewer: "Jamik Tashpulatov"}, {ID: 60, Header: "Backup and Recovery Procedures", SectionType: "Technical content", Status: "Done", Target: "18", Limit: "21", Reviewer: "Assign reviewer"}, {ID: 61, Header: "Monitoring and Alerting System", SectionType: "Technical content", Status: "In Process", Target: "25", Limit: "28", Reviewer: "Daniel Park"}, {ID: 62, Header: "Code Review Guidelines", SectionType: "Technical content", Status: "Done", Target: "12", Limit: "15", Reviewer: "Eddie Lake"}, {ID: 63, Header: "Documentation Standards", SectionType: "Technical content", Status: "In Process", Target: "27", Limit: "30", Reviewer: "Jamik Tashpulatov"}, {ID: 64, Header: "Release Management Process", SectionType: "Planning", Status: "Done", Target: "22", Limit: "25", Reviewer: "Assign reviewer"}, {ID: 65, Header: "Feature Prioritization Matrix", SectionType: "Planning", Status: "In Process", Target: "19", Limit: "22", Reviewer: "Emma Davis"}, {ID: 66, Header: "Technical Debt Assessment", SectionType: "Technical content", Status: "Done", Target: "24", Limit: "27", Reviewer: "Eddie Lake"}, {ID: 67, Header: "Capacity Planning", SectionType: "Planning", Status: "In Process", Target: "21", Limit: "24", Reviewer: "Jamik Tashpulatov"}, {ID: 68, Header: "Service Level Agreements", SectionType: "Legal", Status: "Done", Target: "26", Limit: "29", Reviewer: "Assign reviewer"},}package dashboard01 import ( "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/sidebar") type User struct { Name string Email string Avatar string} type NavItem struct { Title string URL string Icon func(...icon.Props) templ.Component} type NavCloudSubItem struct { Title string URL string} type NavCloudItem struct { Title string Icon func(...icon.Props) templ.Component IsActive bool URL string Items []NavCloudSubItem} type DocumentItem struct { Name string URL string Icon func(...icon.Props) templ.Component} var appSidebarUser = User{ Name: "Axel Adrian", Email: "[email protected]", Avatar: "https://github.com/axadrn.png",} var appSidebarNavMain = []NavItem{ {Title: "Dashboard", URL: "#", Icon: icon.LayoutDashboard}, {Title: "Lifecycle", URL: "#", Icon: icon.List}, {Title: "Analytics", URL: "#", Icon: icon.ChartBar}, {Title: "Projects", URL: "#", Icon: icon.Folder}, {Title: "Team", URL: "#", Icon: icon.Users},} // The pendant of the tsx data.navClouds: like in the tsx it is defined but// never rendered by AppSidebar (kept anonymous so linters stay quiet).var _ = []NavCloudItem{ { Title: "Capture", Icon: icon.Camera, IsActive: true, URL: "#", Items: []NavCloudSubItem{ {Title: "Active Proposals", URL: "#"}, {Title: "Archived", URL: "#"}, }, }, { Title: "Proposal", Icon: icon.FileText, URL: "#", Items: []NavCloudSubItem{ {Title: "Active Proposals", URL: "#"}, {Title: "Archived", URL: "#"}, }, }, { Title: "Prompts", Icon: icon.FileText, URL: "#", Items: []NavCloudSubItem{ {Title: "Active Proposals", URL: "#"}, {Title: "Archived", URL: "#"}, }, },} var appSidebarNavSecondary = []NavItem{ {Title: "Settings", URL: "#", Icon: icon.Settings2}, {Title: "Get Help", URL: "#", Icon: icon.CircleQuestionMark}, {Title: "Search", URL: "#", Icon: icon.Search},} var appSidebarDocuments = []DocumentItem{ {Name: "Data Library", URL: "#", Icon: icon.Database}, {Name: "Reports", URL: "#", Icon: icon.FileChartColumn}, {Name: "Word Assistant", URL: "#", Icon: icon.File},} templ AppSidebar() { @sidebar.Sidebar(sidebar.Props{Collapsible: sidebar.CollapsibleOffcanvas, Variant: sidebar.VariantInset}) { @sidebar.Header() { @sidebar.Menu() { @sidebar.MenuItem() { @sidebar.MenuButton(sidebar.MenuButtonProps{ Class: "data-[slot=sidebar-menu-button]:p-1.5!", Href: "#", }) { @icon.Command(icon.Props{Class: "size-5!"}) <span class="text-base font-semibold">Acme Inc.</span> } } } } @sidebar.Content() { @NavMain(appSidebarNavMain) @NavDocuments(appSidebarDocuments) @NavSecondary(appSidebarNavSecondary, "mt-auto") } @sidebar.Footer() { @NavUser(appSidebarUser) } }}package dashboard01 import ( "time" "github.com/axadrn/shadcn-templ/v2/components/card" "github.com/axadrn/shadcn-templ/v2/components/chart" selectcomp "github.com/axadrn/shadcn-templ/v2/components/select" "github.com/axadrn/shadcn-templ/v2/components/togglegroup" "github.com/axadrn/shadcn-templ/v2/utils") // Interactive area-chart data. var chartAreaInteractiveData = []chart.Datum{ {"date": "2024-04-01", "desktop": 222, "mobile": 150}, {"date": "2024-04-02", "desktop": 97, "mobile": 180}, {"date": "2024-04-03", "desktop": 167, "mobile": 120}, {"date": "2024-04-04", "desktop": 242, "mobile": 260}, {"date": "2024-04-05", "desktop": 373, "mobile": 290}, {"date": "2024-04-06", "desktop": 301, "mobile": 340}, {"date": "2024-04-07", "desktop": 245, "mobile": 180}, {"date": "2024-04-08", "desktop": 409, "mobile": 320}, {"date": "2024-04-09", "desktop": 59, "mobile": 110}, {"date": "2024-04-10", "desktop": 261, "mobile": 190}, {"date": "2024-04-11", "desktop": 327, "mobile": 350}, {"date": "2024-04-12", "desktop": 292, "mobile": 210}, {"date": "2024-04-13", "desktop": 342, "mobile": 380}, {"date": "2024-04-14", "desktop": 137, "mobile": 220}, {"date": "2024-04-15", "desktop": 120, "mobile": 170}, {"date": "2024-04-16", "desktop": 138, "mobile": 190}, {"date": "2024-04-17", "desktop": 446, "mobile": 360}, {"date": "2024-04-18", "desktop": 364, "mobile": 410}, {"date": "2024-04-19", "desktop": 243, "mobile": 180}, {"date": "2024-04-20", "desktop": 89, "mobile": 150}, {"date": "2024-04-21", "desktop": 137, "mobile": 200}, {"date": "2024-04-22", "desktop": 224, "mobile": 170}, {"date": "2024-04-23", "desktop": 138, "mobile": 230}, {"date": "2024-04-24", "desktop": 387, "mobile": 290}, {"date": "2024-04-25", "desktop": 215, "mobile": 250}, {"date": "2024-04-26", "desktop": 75, "mobile": 130}, {"date": "2024-04-27", "desktop": 383, "mobile": 420}, {"date": "2024-04-28", "desktop": 122, "mobile": 180}, {"date": "2024-04-29", "desktop": 315, "mobile": 240}, {"date": "2024-04-30", "desktop": 454, "mobile": 380}, {"date": "2024-05-01", "desktop": 165, "mobile": 220}, {"date": "2024-05-02", "desktop": 293, "mobile": 310}, {"date": "2024-05-03", "desktop": 247, "mobile": 190}, {"date": "2024-05-04", "desktop": 385, "mobile": 420}, {"date": "2024-05-05", "desktop": 481, "mobile": 390}, {"date": "2024-05-06", "desktop": 498, "mobile": 520}, {"date": "2024-05-07", "desktop": 388, "mobile": 300}, {"date": "2024-05-08", "desktop": 149, "mobile": 210}, {"date": "2024-05-09", "desktop": 227, "mobile": 180}, {"date": "2024-05-10", "desktop": 293, "mobile": 330}, {"date": "2024-05-11", "desktop": 335, "mobile": 270}, {"date": "2024-05-12", "desktop": 197, "mobile": 240}, {"date": "2024-05-13", "desktop": 197, "mobile": 160}, {"date": "2024-05-14", "desktop": 448, "mobile": 490}, {"date": "2024-05-15", "desktop": 473, "mobile": 380}, {"date": "2024-05-16", "desktop": 338, "mobile": 400}, {"date": "2024-05-17", "desktop": 499, "mobile": 420}, {"date": "2024-05-18", "desktop": 315, "mobile": 350}, {"date": "2024-05-19", "desktop": 235, "mobile": 180}, {"date": "2024-05-20", "desktop": 177, "mobile": 230}, {"date": "2024-05-21", "desktop": 82, "mobile": 140}, {"date": "2024-05-22", "desktop": 81, "mobile": 120}, {"date": "2024-05-23", "desktop": 252, "mobile": 290}, {"date": "2024-05-24", "desktop": 294, "mobile": 220}, {"date": "2024-05-25", "desktop": 201, "mobile": 250}, {"date": "2024-05-26", "desktop": 213, "mobile": 170}, {"date": "2024-05-27", "desktop": 420, "mobile": 460}, {"date": "2024-05-28", "desktop": 233, "mobile": 190}, {"date": "2024-05-29", "desktop": 78, "mobile": 130}, {"date": "2024-05-30", "desktop": 340, "mobile": 280}, {"date": "2024-05-31", "desktop": 178, "mobile": 230}, {"date": "2024-06-01", "desktop": 178, "mobile": 200}, {"date": "2024-06-02", "desktop": 470, "mobile": 410}, {"date": "2024-06-03", "desktop": 103, "mobile": 160}, {"date": "2024-06-04", "desktop": 439, "mobile": 380}, {"date": "2024-06-05", "desktop": 88, "mobile": 140}, {"date": "2024-06-06", "desktop": 294, "mobile": 250}, {"date": "2024-06-07", "desktop": 323, "mobile": 370}, {"date": "2024-06-08", "desktop": 385, "mobile": 320}, {"date": "2024-06-09", "desktop": 438, "mobile": 480}, {"date": "2024-06-10", "desktop": 155, "mobile": 200}, {"date": "2024-06-11", "desktop": 92, "mobile": 150}, {"date": "2024-06-12", "desktop": 492, "mobile": 420}, {"date": "2024-06-13", "desktop": 81, "mobile": 130}, {"date": "2024-06-14", "desktop": 426, "mobile": 380}, {"date": "2024-06-15", "desktop": 307, "mobile": 350}, {"date": "2024-06-16", "desktop": 371, "mobile": 310}, {"date": "2024-06-17", "desktop": 475, "mobile": 520}, {"date": "2024-06-18", "desktop": 107, "mobile": 170}, {"date": "2024-06-19", "desktop": 341, "mobile": 290}, {"date": "2024-06-20", "desktop": 408, "mobile": 450}, {"date": "2024-06-21", "desktop": 169, "mobile": 210}, {"date": "2024-06-22", "desktop": 317, "mobile": 270}, {"date": "2024-06-23", "desktop": 480, "mobile": 530}, {"date": "2024-06-24", "desktop": 132, "mobile": 180}, {"date": "2024-06-25", "desktop": 141, "mobile": 190}, {"date": "2024-06-26", "desktop": 434, "mobile": 380}, {"date": "2024-06-27", "desktop": 448, "mobile": 490}, {"date": "2024-06-28", "desktop": 149, "mobile": 200}, {"date": "2024-06-29", "desktop": 103, "mobile": 160}, {"date": "2024-06-30", "desktop": 446, "mobile": 400},} var chartAreaInteractiveConfig = chart.Config{ {Key: "visitors", Label: "Visitors"}, {Key: "desktop", Label: "Desktop", Color: "var(--primary)"}, {Key: "mobile", Label: "Mobile", Color: "var(--primary)"},} // chartAreaInteractiveFilteredData is the time range filter: the last n// days counted back from 2024-06-30.func chartAreaInteractiveFilteredData(days int) []chart.Datum { ref, _ := time.Parse("2006-01-02", "2024-06-30") start := ref.AddDate(0, 0, -days) var out []chart.Datum for _, d := range chartAreaInteractiveData { t, _ := time.Parse("2006-01-02", d["date"].(string)) if !t.Before(start) { out = append(out, d) } } return out} // chartAreaInteractiveShortDate formats like the tsx tickFormatter: "Apr 1".func chartAreaInteractiveShortDate(v any) string { t, _ := time.Parse("2006-01-02", v.(string)) return t.Format("Jan 2")} var chartAreaInteractiveRanges = []struct { Key string Label string Days int}{ {"90d", "Last 3 months", 90}, {"30d", "Last 30 days", 30}, {"7d", "Last 7 days", 7},} templ ChartAreaInteractive() { @card.Card(card.Props{ Class: "@container/card", Attributes: templ.Attributes{"data-dashboard01-chart-range-card": ""}, }) { @card.Header() { @card.Title() { Total Visitors } @card.Description() { <span class="hidden @[540px]/card:block"> Total for the last 3 months </span> <span class="@[540px]/card:hidden">Last 3 months</span> } @card.Action() { @togglegroup.ToggleGroup(togglegroup.Props{ Variant: togglegroup.VariantOutline, DefaultValue: []string{"90d"}, // Spacing 0 is the joined bar of the TSX ToggleGroup default; // px-4! beats the joined mode's px-2 like upstream. Spacing: utils.Ptr(0), Class: "hidden *:data-[slot=toggle-group-item]:px-4! @[767px]/card:flex", Attributes: templ.Attributes{"data-tui-chart-range-toggle": "dashboard01-area-interactive"}, }) { for _, rng := range chartAreaInteractiveRanges { @togglegroup.Item(togglegroup.ItemProps{Value: rng.Key}) { { rng.Label } } } } @selectcomp.Select(selectcomp.Props{DefaultValue: "90d"}) { @selectcomp.Trigger(selectcomp.TriggerProps{ Class: "flex w-40 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate @[767px]/card:hidden", Size: selectcomp.SizeSm, Attributes: templ.Attributes{"aria-label": "Select a value", "data-tui-chart-range-select": "dashboard01-area-interactive"}, }) { @selectcomp.Value(selectcomp.ValueProps{Placeholder: "Last 3 months"}) } @selectcomp.Content(selectcomp.ContentProps{Class: "rounded-xl"}) { @selectcomp.Group() { for _, rng := range chartAreaInteractiveRanges { @selectcomp.Item(selectcomp.ItemProps{Value: rng.Key, Class: "rounded-lg"}) { { rng.Label } } } } } } } } @card.Content(card.ContentProps{Class: "px-2 pt-4 sm:px-6 sm:pt-6"}) { @chart.Container(chart.ContainerProps{ID: "dashboard01-area-interactive", Config: chartAreaInteractiveConfig, Class: "aspect-auto h-[250px] w-full"}) { // All three ranges render up front, the chart script toggles // them on range change (the pendant of the timeRange state). for _, rng := range chartAreaInteractiveRanges { <div data-tui-chart-range={ rng.Key } hidden?={ rng.Key != "90d" } style="width:100%;height:100%"> @chart.AreaChart(chart.AreaChartProps{ Data: chartAreaInteractiveFilteredData(rng.Days), }) { @chart.Defs() { @chart.LinearGradient(chart.LinearGradientProps{ID: "fillDesktop", X1: "0", Y1: "0", X2: "0", Y2: "1"}) { <stop offset="5%" stop-color="var(--color-desktop)" stop-opacity="1.0"></stop> <stop offset="95%" stop-color="var(--color-desktop)" stop-opacity="0.1"></stop> } @chart.LinearGradient(chart.LinearGradientProps{ID: "fillMobile", X1: "0", Y1: "0", X2: "0", Y2: "1"}) { <stop offset="5%" stop-color="var(--color-mobile)" stop-opacity="0.8"></stop> <stop offset="95%" stop-color="var(--color-mobile)" stop-opacity="0.1"></stop> } } @chart.CartesianGrid(chart.CartesianGridProps{Vertical: chart.Bool(false)}) @chart.XAxis(chart.XAxisProps{ DataKey: "date", TickMargin: 8, MinTickGap: 32, TickFormatter: chartAreaInteractiveShortDate, }) @chart.Tooltip(chart.TooltipProps{ Cursor: chart.Bool(false), Content: chart.TooltipContentProps{ Indicator: "dot", LabelFormatter: chartAreaInteractiveShortDate, }, }) @chart.Area(chart.AreaProps{ DataKey: "mobile", Type: chart.CurveNatural, Fill: "url(#fillMobile)", Stroke: "var(--color-mobile)", StackID: "a", }) @chart.Area(chart.AreaProps{ DataKey: "desktop", Type: chart.CurveNatural, Fill: "url(#fillDesktop)", Stroke: "var(--color-desktop)", StackID: "a", }) } </div> } } } } <script nonce={ templ.GetNonce(ctx) }> (() => { // The two range controls share one state like the TSX's timeRange: // a toggle pick forwards to the matching select item, so select.js // stays the single owner of value, trigger label and the // select-change event chart.js flips the panels on. if (window.__tuiDashboard01ChartRange) return; window.__tuiDashboard01ChartRange = true; const mobile = window.matchMedia("(max-width: 767px)"); const selectContent = (trigger) => { const id = trigger && trigger.getAttribute("aria-controls"); if (!id) return null; const live = document.getElementById(id); if (live) return live; for (const template of document.querySelectorAll("template[data-tui-select-portal]")) { const content = [...template.content.querySelectorAll("[data-tui-select-content]")].find((item) => item.id === id); if (content) return content; } return null; }; // useIsMobile sets 7d whenever the viewport enters mobile. Update the // same uncontrolled select state without opening or focusing it. const setRange = (card, value) => { const trigger = card && card.querySelector("[data-tui-chart-range-select]"); const content = selectContent(trigger); const item = content && content.querySelector('[data-tui-select-item][data-tui-select-value="' + value + '"]'); if (!trigger || !item) return; content.querySelectorAll("[data-tui-select-item]").forEach((candidate) => { const selected = candidate === item; candidate.toggleAttribute("data-selected", selected); candidate.setAttribute("aria-selected", String(selected)); }); const label = item.getAttribute("data-tui-select-label") || item.textContent.trim(); const valueNode = trigger.querySelector("[data-tui-select-value]"); if (valueNode) valueNode.textContent = label; trigger.removeAttribute("data-placeholder"); trigger.dispatchEvent(new CustomEvent("select-change", { bubbles: true, detail: { value, label }, })); }; const syncMobileRange = () => { if (!mobile.matches) return; document.querySelectorAll("[data-dashboard01-chart-range-card]").forEach((card) => setRange(card, "7d")); }; if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", syncMobileRange, { once: true }); else syncMobileRange(); mobile.addEventListener("change", syncMobileRange); document.addEventListener("toggle-change", (e) => { if (!(e.target instanceof Element)) return; const group = e.target.closest("[data-tui-chart-range-toggle]"); if (!group) return; const value = e.detail && e.detail.value; if (!value) return; // Single-select groups allow unpressing the active item; a // time range must stay active, so re-press it. const toggle = e.target.closest("[data-tui-toggle]"); if (toggle && e.detail.pressed === false) { toggle.setAttribute("data-pressed", ""); toggle.setAttribute("aria-pressed", "true"); } const card = group.closest("[data-slot=card]"); const trigger = card && card.querySelector("[data-tui-chart-range-select]"); const content = trigger && document.getElementById(trigger.getAttribute("aria-controls")); const item = content && content.querySelector('[data-tui-select-item][data-tui-select-value="' + value + '"]'); if (item) item.click(); }); // A pick in the mobile select presses the matching toggle; // chart.js flips the panels on the same select-change. document.addEventListener("select-change", (e) => { if (!(e.target instanceof Element)) return; const trigger = e.target.closest("[data-tui-chart-range-select]"); if (!trigger) return; const card = trigger.closest("[data-slot=card]"); const group = card && card.querySelector("[data-tui-chart-range-toggle]"); if (!group) return; group.querySelectorAll("[data-tui-toggle]").forEach((t) => { const on = t.getAttribute("data-tui-toggle-value") === e.detail.value; t.toggleAttribute("data-pressed", on); t.setAttribute("aria-pressed", String(on)); }); group.setAttribute("data-tui-toggle-group-value", e.detail.value); }); })(); </script>}package dashboard01 import ( "fmt" "github.com/axadrn/shadcn-templ/v2/components/badge" "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/chart" "github.com/axadrn/shadcn-templ/v2/components/checkbox" "github.com/axadrn/shadcn-templ/v2/components/drawer" "github.com/axadrn/shadcn-templ/v2/components/dropdownmenu" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/input" "github.com/axadrn/shadcn-templ/v2/components/label" selectcomp "github.com/axadrn/shadcn-templ/v2/components/select" "github.com/axadrn/shadcn-templ/v2/components/separator" "github.com/axadrn/shadcn-templ/v2/components/skeleton" "github.com/axadrn/shadcn-templ/v2/components/table" "github.com/axadrn/shadcn-templ/v2/components/tabs" "github.com/axadrn/shadcn-templ/v2/utils") // dataTablePageSize is the pendant of the pagination state's pageSize.const dataTablePageSize = 10 // dataTableHideableColumns are the columns the Columns menu// lists in the tsx: every accessor column that can hide.var dataTableHideableColumns = []string{"type", "status", "target", "limit", "reviewer"} var dataTableViews = []struct { Value string Label string}{ {"outline", "Outline"}, {"past-performance", "Past Performance"}, {"key-personnel", "Key Personnel"}, {"focus-documents", "Focus Documents"},} templ DataTable(data []Item) { // Pagination state lives in the URL (see data_table.go); the window, // page count and button states are computed server-side. {{ page, size := dataTablePageParams(ctx, len(data)) }} {{ pageCount := dataTablePageCount(len(data), size) }} // The swappable fragment: a fragment-aware handler (?fragment=data-table // + templ.WithFragments) renders just this container for the script's // partial updates; everywhere else the wrapper is transparent. @templ.Fragment("data-table") { @tabs.Tabs(tabs.Props{ DefaultValue: "outline", Class: "w-full flex-col justify-start gap-6", Attributes: templ.Attributes{"data-dashboard01-table": ""}, }) { <div class="flex items-center justify-between px-4 lg:px-6"> @label.Label(label.Props{For: "view-selector", Class: "sr-only"}) { View } // The view select is static: it mirrors the tabs but does not // switch them (the tsx does not wire it to the tabs either). @selectcomp.Select(selectcomp.Props{DefaultValue: "outline"}) { @selectcomp.Trigger(selectcomp.TriggerProps{ Class: "flex w-fit @4xl/main:hidden", Size: selectcomp.SizeSm, ID: "view-selector", }) { @selectcomp.Value(selectcomp.ValueProps{Placeholder: "Select a view"}) } @selectcomp.Content() { @selectcomp.Group() { for _, view := range dataTableViews { @selectcomp.Item(selectcomp.ItemProps{Value: view.Value}) { { view.Label } } } } } } @tabs.List(tabs.ListProps{Class: "hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:bg-muted-foreground/30 **:data-[slot=badge]:px-1 @4xl/main:flex"}) { @tabs.Trigger(tabs.TriggerProps{Value: "outline"}) { Outline } @tabs.Trigger(tabs.TriggerProps{Value: "past-performance"}) { Past Performance @badge.Badge(badge.Props{Variant: badge.VariantSecondary}) { 3 } } @tabs.Trigger(tabs.TriggerProps{Value: "key-personnel"}) { Key Personnel @badge.Badge(badge.Props{Variant: badge.VariantSecondary}) { 2 } } @tabs.Trigger(tabs.TriggerProps{Value: "focus-documents"}) { Focus Documents } } <div class="flex items-center gap-2"> @dropdownmenu.DropdownMenu() { @button.Button(button.Props{ Variant: button.VariantOutline, Size: button.SizeSm, Attributes: dropdownmenu.Trigger(ctx), }) { @icon.Columns3(icon.Props{Attributes: templ.Attributes{"data-icon": "inline-start"}}) Columns @icon.ChevronDown(icon.Props{Attributes: templ.Attributes{"data-icon": "inline-end"}}) } @dropdownmenu.Content(dropdownmenu.ContentProps{Align: dropdownmenu.AlignEnd, Class: "w-32"}) { // The columnVisibility pendant: dataTableScript toggles // the column's cells; the attribute carries the cell // index (after the drag, select and header columns). for i, column := range dataTableHideableColumns { @dropdownmenu.CheckboxItem(dropdownmenu.CheckboxItemProps{ Class: "capitalize", DefaultChecked: true, Attributes: templ.Attributes{"data-dashboard01-column": fmt.Sprint(i + 3)}, }) { { column } } } } } @button.Button(button.Props{Variant: button.VariantOutline, Size: button.SizeSm}) { @icon.Plus() <span class="hidden lg:inline">Add Section</span> } </div> </div> @tabs.Content(tabs.ContentProps{ Value: "outline", Class: "relative flex flex-col gap-4 overflow-auto px-4 lg:px-6", }) { <div class="overflow-hidden rounded-lg border"> @table.Table() { @table.Header(table.HeaderProps{Class: "sticky top-0 z-10 bg-muted"}) { @table.Row() { @table.Head() @table.Head() { <div class="flex items-center justify-center"> @checkbox.Checkbox(checkbox.Props{ Attributes: templ.Attributes{"aria-label": "Select all"}, }) </div> } @table.Head() { Header } @table.Head() { Section Type } @table.Head() { Status } @table.Head() { <div class="w-full text-right">Target</div> } @table.Head() { <div class="w-full text-right">Limit</div> } @table.Head() { Reviewer } @table.Head() } } @table.Body(table.BodyProps{ Class: "**:data-[slot=table-cell]:first:w-8", Attributes: templ.Attributes{"data-dashboard01-sortable": ""}, }) { if len(data) > 0 { for _, item := range dataTablePageWindow(data, page, size) { @dataTableRow(item, page, size) } } else { @table.Row() { @table.Cell(table.CellProps{ Class: "h-24 text-center", Attributes: templ.Attributes{"colspan": "9"}, }) { No results. } } } } } </div> <div class="flex items-center justify-between px-4"> <div class="hidden flex-1 text-sm text-muted-foreground lg:flex"> // Selection is view-local UI state. dataTableScript keeps the // count live and resets it on page navigation. <span data-dashboard01-selected-count>0</span> of { fmt.Sprint(len(data)) } row(s) selected. </div> <div class="flex w-full items-center gap-8 lg:w-fit"> // The rows-per-page pick is declarative htmx: the select // syncs its hidden input[name=size], whose change event // bubbles to this wrapper and fetches page one with the // new size (the setPageSize pendant, reset like tanstack). <div class="hidden items-center gap-2 lg:flex" hx-get="?page=1&fragment=data-table" hx-trigger="change" hx-include="input[name='size']" hx-target="[data-dashboard01-table]" hx-swap="outerHTML" > @label.Label(label.Props{For: "rows-per-page", Class: "text-sm font-medium"}) { Rows per page } // The tsx passes side="top" to the SelectContent; the // templ select has no side prop, the content aligns to // the trigger. @selectcomp.Select(selectcomp.Props{DefaultValue: fmt.Sprint(size), Name: "size"}) { @selectcomp.Trigger(selectcomp.TriggerProps{ Size: selectcomp.SizeSm, Class: "w-20", ID: "rows-per-page", }) { @selectcomp.Value(selectcomp.ValueProps{Placeholder: fmt.Sprint(size)}) } @selectcomp.Content() { @selectcomp.Group() { for _, pageSize := range dataTablePageSizes { @selectcomp.Item(selectcomp.ItemProps{Value: fmt.Sprint(pageSize)}) { { fmt.Sprint(pageSize) } } } } } } </div> <div class="flex w-fit items-center justify-center text-sm font-medium"> Page { fmt.Sprint(page) } of { fmt.Sprint(pageCount) } </div> <div class="ml-auto flex items-center gap-2 lg:ml-0"> // The nav buttons are links onto the page's own URL // (?page=&size=), disabled states computed server-side - // the setPageIndex/getCanPreviousPage pendant. htmx // swaps just the data-table fragment; without JS the // links navigate normally. @button.Button(button.Props{ Variant: button.VariantOutline, Class: "hidden h-8 w-8 p-0 lg:flex", Href: dataTablePageHref(1, size), Disabled: page == 1, Attributes: dataTableHxAttrs(dataTablePageHref(1, size)), }) { <span class="sr-only">Go to first page</span> @icon.ChevronsLeft() } @button.Button(button.Props{ Variant: button.VariantOutline, Class: "size-8", Size: button.SizeIcon, Href: dataTablePageHref(page-1, size), Disabled: page == 1, Attributes: dataTableHxAttrs(dataTablePageHref(page-1, size)), }) { <span class="sr-only">Go to previous page</span> @icon.ChevronLeft() } @button.Button(button.Props{ Variant: button.VariantOutline, Class: "size-8", Size: button.SizeIcon, Href: dataTablePageHref(page+1, size), Disabled: page == pageCount, Attributes: dataTableHxAttrs(dataTablePageHref(page+1, size)), }) { <span class="sr-only">Go to next page</span> @icon.ChevronRight() } @button.Button(button.Props{ Variant: button.VariantOutline, Class: "hidden size-8 lg:flex", Size: button.SizeIcon, Href: dataTablePageHref(pageCount, size), Disabled: page == pageCount, Attributes: dataTableHxAttrs(dataTablePageHref(pageCount, size)), }) { <span class="sr-only">Go to last page</span> @icon.ChevronsRight() } </div> </div> </div> } @tabs.Content(tabs.ContentProps{ Value: "past-performance", Class: "flex flex-col px-4 lg:px-6", }) { <div class="aspect-video w-full flex-1 rounded-lg border border-dashed"></div> } @tabs.Content(tabs.ContentProps{Value: "key-personnel", Class: "flex flex-col px-4 lg:px-6"}) { <div class="aspect-video w-full flex-1 rounded-lg border border-dashed"></div> } @tabs.Content(tabs.ContentProps{ Value: "focus-documents", Class: "flex flex-col px-4 lg:px-6", }) { <div class="aspect-video w-full flex-1 rounded-lg border border-dashed"></div> } } } @dataTableScript()} // dataTableScript owns the view-local interactions from the TSX: column// visibility, row selection and sortable rows. Pagination stays in the URL.templ dataTableScript() { <script nonce={ templ.GetNonce(ctx) }> (() => { if (window.__tuiDashboard01DataTable) return; window.__tuiDashboard01DataTable = true; let draggedRow = null; let keyboardOrigin = null; // Pagination and rows-per-page are htmx-driven (see the hx-* // attributes on the nav links and the rows-per-page wrapper); // this script only covers the view-local UI state below. // Column visibility (the tsx's columnVisibility state): the // Columns checkbox items hide/show their column's // cells. The dropdown content is portaled to <body>, so the // table is resolved directly (one instance per page). document.addEventListener("dropdownmenu-checked-change", (e) => { if (!(e.target instanceof Element)) return; const item = e.target.closest("[data-dashboard01-column]"); if (!item) return; const root = document.querySelector("[data-dashboard01-table]"); if (!root) return; const index = parseInt(item.getAttribute("data-dashboard01-column"), 10); const show = item.hasAttribute("data-checked"); root.querySelectorAll("thead tr, tbody tr").forEach((row) => { const cell = row.children[index]; if (cell) cell.hidden = !show; }); }); // Row selection: the header checkbox toggles the page's rows and // mirrors them as checked/indeterminate (the checkbox component // renders the mixed state natively); selected rows tint via // data-state and the footer count stays live. let syncing = false; document.addEventListener("change", (e) => { if (syncing || !(e.target instanceof Element)) return; if (!e.target.matches("[data-tui-checkbox-input]")) return; const root = e.target.closest("[data-dashboard01-table]"); if (!root) return; const all = root.querySelector("thead [data-tui-checkbox-input]"); const rows = [...root.querySelectorAll("tbody [data-tui-checkbox-input]")]; if (!all || rows.length === 0) return; syncing = true; if (e.target === all) { // toggleAllPageRowsSelected pendant. rows.forEach((input) => { if (input.checked !== all.checked) { input.checked = all.checked; input.dispatchEvent(new Event("change", { bubbles: true })); } }); } const checked = rows.filter((input) => input.checked).length; rows.forEach((input) => { const row = input.closest("tr"); if (!row) return; if (input.checked) row.setAttribute("data-state", "selected"); else row.removeAttribute("data-state"); }); // getIsAllPageRowsSelected / getIsSomePageRowsSelected pendant; // checkbox.js paints the mixed state off input.indeterminate. all.checked = checked === rows.length; all.indeterminate = checked > 0 && checked < rows.length; all.dispatchEvent(new Event("change", { bubbles: true })); syncing = false; const count = root.querySelector("[data-dashboard01-selected-count]"); if (count) count.textContent = String(checked); }); // Vanilla pendant of dnd-kit's vertical SortableContext. Pointer, // touch, native drag and keyboard input all reorder the rendered rows. const finishDrag = () => { if (draggedRow) draggedRow.removeAttribute("data-dragging"); draggedRow = null; keyboardOrigin = null; }; const moveAt = (tbody, row, clientY) => { const rect = row.getBoundingClientRect(); const target = document.elementsFromPoint(rect.left + rect.width / 2, clientY) .map((element) => element.closest && element.closest("[data-dashboard01-row]")) .find((element) => element && element.parentElement === tbody && element !== row); if (!target) return; const before = clientY < target.getBoundingClientRect().top + target.offsetHeight / 2; tbody.insertBefore(row, before ? target : target.nextSibling); }; document.addEventListener("dragstart", (e) => { if (!(e.target instanceof Element)) return; const handle = e.target.closest("[data-dashboard01-drag-handle]"); if (!handle) return; draggedRow = handle.closest("[data-dashboard01-row]"); if (!draggedRow) return; draggedRow.setAttribute("data-dragging", "true"); if (e.dataTransfer) { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", draggedRow.getAttribute("data-dashboard01-row") || ""); } }); document.addEventListener("dragover", (e) => { if (!draggedRow) return; const tbody = draggedRow.closest("[data-dashboard01-sortable]"); if (!tbody) return; e.preventDefault(); moveAt(tbody, draggedRow, e.clientY); }); document.addEventListener("dragend", finishDrag); let pointer = null; document.addEventListener("pointerdown", (e) => { if (!(e.target instanceof Element) || e.pointerType === "mouse") return; const handle = e.target.closest("[data-dashboard01-drag-handle]"); const row = handle && handle.closest("[data-dashboard01-row]"); if (!handle || !row) return; pointer = { id: e.pointerId, handle, row }; draggedRow = row; row.setAttribute("data-dragging", "true"); handle.setPointerCapture(e.pointerId); e.preventDefault(); }); document.addEventListener("pointermove", (e) => { if (!pointer || pointer.id !== e.pointerId) return; const tbody = pointer.row.closest("[data-dashboard01-sortable]"); if (tbody) moveAt(tbody, pointer.row, e.clientY); e.preventDefault(); }); const finishPointer = (e) => { if (!pointer || pointer.id !== e.pointerId) return; if (pointer.handle.hasPointerCapture(e.pointerId)) pointer.handle.releasePointerCapture(e.pointerId); pointer = null; finishDrag(); }; document.addEventListener("pointerup", finishPointer); document.addEventListener("pointercancel", finishPointer); document.addEventListener("keydown", (e) => { if (!(e.target instanceof Element)) return; const handle = e.target.closest("[data-dashboard01-drag-handle]"); if (!handle) return; const row = handle.closest("[data-dashboard01-row]"); const tbody = row && row.closest("[data-dashboard01-sortable]"); if (!row || !tbody) return; if (e.key === " " || e.key === "Enter") { e.preventDefault(); if (draggedRow === row) finishDrag(); else { finishDrag(); draggedRow = row; keyboardOrigin = row.nextSibling; row.setAttribute("data-dragging", "true"); } } else if (draggedRow === row && (e.key === "ArrowUp" || e.key === "ArrowDown")) { e.preventDefault(); const sibling = e.key === "ArrowUp" ? row.previousElementSibling : row.nextElementSibling; if (sibling) tbody.insertBefore(row, e.key === "ArrowUp" ? sibling : sibling.nextSibling); handle.focus(); } else if (draggedRow === row && e.key === "Escape") { e.preventDefault(); tbody.insertBefore(row, keyboardOrigin && keyboardOrigin.isConnected ? keyboardOrigin : null); finishDrag(); } }); // The current Base block uses toast.promise on the two inline forms. document.addEventListener("submit", (e) => { if (!(e.target instanceof HTMLFormElement) || !e.target.matches("[data-dashboard01-save]")) return; e.preventDefault(); const title = e.target.getAttribute("data-dashboard01-save") || "section"; window.tui?.toast?.promise( new Promise((resolve) => window.setTimeout(resolve, 1000)), { loading: `Saving ${title}`, success: "Done", error: "Error" }, ); }); // useIsMobile pendant for TableCellViewer: mutate the inert portal // template before drawer.js opens it, so the first open is correct. const drawerDialog = (trigger) => { const id = trigger.getAttribute("aria-controls"); if (!id) return null; const live = document.getElementById(id); if (live) return live; for (const template of document.querySelectorAll("template[data-tui-drawer-portal]")) { const dialog = [...template.content.querySelectorAll("dialog[data-tui-drawer-content]")].find((item) => item.id === id); if (dialog) return dialog; } return null; }; const syncDrawerDirection = (trigger) => { const popup = drawerDialog(trigger)?.querySelector("[data-tui-drawer-popup]"); if (!popup) return; const mobile = window.matchMedia("(max-width: 767px)").matches; popup.setAttribute("data-swipe-direction", mobile ? "down" : "right"); popup.setAttribute("data-swipe-axis", mobile ? "y" : "x"); }; document.addEventListener("pointerdown", (e) => { if (!(e.target instanceof Element)) return; const trigger = e.target.closest("[data-dashboard01-drawer-trigger]"); if (trigger) syncDrawerDirection(trigger); }, true); document.addEventListener("keydown", (e) => { if (!(e.target instanceof Element) || (e.key !== "Enter" && e.key !== " ")) return; const trigger = e.target.closest("[data-dashboard01-drawer-trigger]"); if (trigger) syncDrawerDirection(trigger); }, true); document.addEventListener("click", (e) => { if (!(e.target instanceof Element)) return; const trigger = e.target.closest("[data-dashboard01-drawer-trigger]"); if (trigger) syncDrawerDirection(trigger); }, true); })(); </script>} templ dataTableRow(item Item, page, size int) { @table.Row(table.RowProps{ Class: "relative z-0 data-[dragging=true]:z-10 data-[dragging=true]:opacity-80", Attributes: templ.Attributes{"data-dashboard01-row": fmt.Sprint(item.ID)}, }) { @table.Cell() { @button.Button(button.Props{ Variant: button.VariantGhost, Size: button.SizeIcon, Class: "size-7 text-muted-foreground hover:bg-transparent", Attributes: templ.Attributes{ "data-dashboard01-drag-handle": "", "draggable": "true", "style": "touch-action:none", }, }) { @icon.GripVertical(icon.Props{Class: "size-3 text-muted-foreground"}) <span class="sr-only">Drag to reorder</span> } } @table.Cell() { <div class="flex items-center justify-center"> @checkbox.Checkbox(checkbox.Props{ Attributes: templ.Attributes{"aria-label": "Select row"}, }) </div> } @table.Cell() { @dataTableCellViewer(item, page, size) } @table.Cell() { <div class="w-32"> @badge.Badge(badge.Props{Variant: badge.VariantOutline, Class: "px-1.5 text-muted-foreground"}) { { item.SectionType } } </div> } @table.Cell() { @badge.Badge(badge.Props{Variant: badge.VariantOutline, Class: "px-1.5 text-muted-foreground"}) { if item.Status == "Done" { @icon.CircleCheck(icon.Props{Class: "fill-green-500 dark:fill-green-400"}) } else { @icon.Loader() } { item.Status } } } @table.Cell() { <form data-dashboard01-save={ item.Header }> @label.Label(label.Props{For: fmt.Sprintf("%d-target", item.ID), Class: "sr-only"}) { Target } @input.Input(input.Props{ Class: "h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30", Value: item.Target, ID: fmt.Sprintf("%d-target", item.ID), }) </form> } @table.Cell() { <form data-dashboard01-save={ item.Header }> @label.Label(label.Props{For: fmt.Sprintf("%d-limit", item.ID), Class: "sr-only"}) { Limit } @input.Input(input.Props{ Class: "h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30", Value: item.Limit, ID: fmt.Sprintf("%d-limit", item.ID), }) </form> } @table.Cell() { if item.Reviewer != "Assign reviewer" { { item.Reviewer } } else { @label.Label(label.Props{For: fmt.Sprintf("%d-reviewer", item.ID), Class: "sr-only"}) { Reviewer } @selectcomp.Select() { @selectcomp.Trigger(selectcomp.TriggerProps{ Class: "w-38 **:data-[slot=select-value]:block **:data-[slot=select-value]:truncate", Size: selectcomp.SizeSm, ID: fmt.Sprintf("%d-reviewer", item.ID), }) { @selectcomp.Value(selectcomp.ValueProps{Placeholder: "Assign reviewer"}) } @selectcomp.Content(selectcomp.ContentProps{Align: selectcomp.AlignEnd}) { @selectcomp.Group() { @selectcomp.Item(selectcomp.ItemProps{Value: "Eddie Lake"}) { Eddie Lake } @selectcomp.Item(selectcomp.ItemProps{Value: "Jamik Tashpulatov"}) { Jamik Tashpulatov } } } } } } @table.Cell() { @dropdownmenu.DropdownMenu() { @button.Button(button.Props{ Variant: button.VariantGhost, Class: "flex size-8 text-muted-foreground data-popup-open:bg-muted", Size: button.SizeIcon, Attributes: dropdownmenu.Trigger(ctx), }) { @icon.EllipsisVertical() <span class="sr-only">Open menu</span> } @dropdownmenu.Content(dropdownmenu.ContentProps{Align: dropdownmenu.AlignEnd, Class: "w-32"}) { @dropdownmenu.Item() { Edit } @dropdownmenu.Item() { Make a copy } @dropdownmenu.Item() { Favorite } @dropdownmenu.Separator() @dropdownmenu.Item(dropdownmenu.ItemProps{Variant: dropdownmenu.ItemVariantDestructive}) { Delete } } } } }} var dataTableCellViewerChartData = []chart.Datum{ {"month": "January", "desktop": 186, "mobile": 80}, {"month": "February", "desktop": 305, "mobile": 200}, {"month": "March", "desktop": 237, "mobile": 120}, {"month": "April", "desktop": 73, "mobile": 190}, {"month": "May", "desktop": 209, "mobile": 130}, {"month": "June", "desktop": 214, "mobile": 140},} var dataTableCellViewerChartConfig = chart.Config{ {Key: "desktop", Label: "Desktop", Color: "var(--primary)"}, {Key: "mobile", Label: "Mobile", Color: "var(--primary)"},} // dataTableCellViewerMonthShort returns the three-letter month label.func dataTableCellViewerMonthShort(v any) string { s, _ := v.(string) if len(s) > 3 { return s[:3] } return s} templ dataTableCellViewer(item Item, page, size int) { @drawer.Drawer(drawer.Props{SwipeDirection: drawer.SwipeDirectionRight}) { // Lazy-load this row's templ fragment once, so the page never ships // 68 hidden charts. @button.Button(button.Props{ Variant: button.VariantLink, Class: "w-fit px-0 text-left text-foreground", Attributes: utils.MergeAttributes(drawer.Trigger(ctx), templ.Attributes{ "data-dashboard01-drawer-trigger": "", "hx-get": dataTablePageHref(page, size) + "&fragment=" + dataTableDrawerFragment(item.ID), "hx-trigger": "click once", "hx-target": "#" + dataTableDrawerBodyID(item.ID), "hx-swap": "innerHTML", }), }) { { item.Header } } @drawer.Content() { @drawer.Header(drawer.HeaderProps{Class: "gap-1"}) { @drawer.Title() { { item.Header } } @drawer.Description() { Showing total visitors for the last 6 months } } <div class="flex flex-col gap-4 overflow-y-auto px-4 text-sm" id={ dataTableDrawerBodyID(item.ID) }> if dataTableDrawerRequested(ctx, item.ID) { @templ.Fragment(dataTableDrawerFragment(item.ID)) { @dataTableDrawerBody(item) } } else { // The skeleton the fragment swap replaces on first open. @skeleton.Skeleton(skeleton.Props{Class: "aspect-video w-full"}) @skeleton.Skeleton(skeleton.Props{Class: "h-4 w-2/3"}) @skeleton.Skeleton(skeleton.Props{Class: "h-40 w-full"}) } </div> @drawer.Footer() { @button.Button() { Submit } @button.Button(button.Props{ Variant: button.VariantOutline, Attributes: drawer.Close(ctx), }) { Done } } } }} // dataTableDrawerBody is the drawer's heavy content (chart, trend, form),// rendered only for its own fragment request.templ dataTableDrawerBody(item Item) { <div class="hidden md:contents"> @chart.Container(chart.ContainerProps{Config: dataTableCellViewerChartConfig}) { @chart.AreaChart(chart.AreaChartProps{ AccessibilityLayer: true, Data: dataTableCellViewerChartData, Margin: &chart.Margin{Left: 0, Right: 10}, }) { @chart.CartesianGrid(chart.CartesianGridProps{Vertical: chart.Bool(false)}) @chart.XAxis(chart.XAxisProps{ DataKey: "month", TickMargin: 8, TickFormatter: dataTableCellViewerMonthShort, Hide: true, }) @chart.Tooltip(chart.TooltipProps{ Cursor: chart.Bool(false), Content: chart.TooltipContentProps{Indicator: "dot"}, }) @chart.Area(chart.AreaProps{ DataKey: "mobile", Type: chart.CurveNatural, Fill: "var(--color-mobile)", FillOpacity: 0.6, Stroke: "var(--color-mobile)", StackID: "a", }) @chart.Area(chart.AreaProps{ DataKey: "desktop", Type: chart.CurveNatural, Fill: "var(--color-desktop)", FillOpacity: 0.4, Stroke: "var(--color-desktop)", StackID: "a", }) } } @separator.Separator() <div class="grid gap-2"> <div class="flex gap-2 leading-none font-medium"> Trending up by 5.2% this month @icon.TrendingUp(icon.Props{Class: "size-4"}) </div> <div class="text-muted-foreground"> Showing total visitors for the last 6 months. This is just some random text to test the layout. It spans multiple lines and should wrap around. </div> </div> @separator.Separator() </div> <form class="flex flex-col gap-4"> <div class="flex flex-col gap-3"> @label.Label(label.Props{For: "header"}) { Header } @input.Input(input.Props{ID: "header", Value: item.Header}) </div> <div class="grid grid-cols-2 gap-4"> <div class="flex flex-col gap-3"> @label.Label(label.Props{For: "type"}) { Type } @selectcomp.Select(selectcomp.Props{DefaultValue: item.SectionType}) { @selectcomp.Trigger(selectcomp.TriggerProps{ID: "type", Class: "w-full"}) { @selectcomp.Value(selectcomp.ValueProps{Placeholder: "Select a type"}) } @selectcomp.Content() { @selectcomp.Group() { for _, sectionType := range []string{"Table of Contents", "Executive Summary", "Technical Approach", "Design", "Capabilities", "Focus Documents", "Narrative", "Cover Page"} { @selectcomp.Item(selectcomp.ItemProps{Value: sectionType}) { { sectionType } } } } } } </div> <div class="flex flex-col gap-3"> @label.Label(label.Props{For: "status"}) { Status } @selectcomp.Select(selectcomp.Props{DefaultValue: item.Status}) { @selectcomp.Trigger(selectcomp.TriggerProps{ID: "status", Class: "w-full"}) { @selectcomp.Value(selectcomp.ValueProps{Placeholder: "Select a status"}) } @selectcomp.Content() { @selectcomp.Group() { @selectcomp.Item(selectcomp.ItemProps{Value: "Done"}) { Done } @selectcomp.Item(selectcomp.ItemProps{Value: "In Progress"}) { In Progress } @selectcomp.Item(selectcomp.ItemProps{Value: "Not Started"}) { Not Started } } } } </div> </div> <div class="grid grid-cols-2 gap-4"> <div class="flex flex-col gap-3"> @label.Label(label.Props{For: "target"}) { Target } @input.Input(input.Props{ID: "target", Value: item.Target}) </div> <div class="flex flex-col gap-3"> @label.Label(label.Props{For: "limit"}) { Limit } @input.Input(input.Props{ID: "limit", Value: item.Limit}) </div> </div> <div class="flex flex-col gap-3"> @label.Label(label.Props{For: "reviewer"}) { Reviewer } @selectcomp.Select(selectcomp.Props{DefaultValue: item.Reviewer}) { @selectcomp.Trigger(selectcomp.TriggerProps{ID: "reviewer", Class: "w-full"}) { @selectcomp.Value(selectcomp.ValueProps{Placeholder: "Select a reviewer"}) } @selectcomp.Content() { @selectcomp.Group() { @selectcomp.Item(selectcomp.ItemProps{Value: "Eddie Lake"}) { Eddie Lake } @selectcomp.Item(selectcomp.ItemProps{Value: "Jamik Tashpulatov"}) { Jamik Tashpulatov } @selectcomp.Item(selectcomp.ItemProps{Value: "Emily Whalen"}) { Emily Whalen } } } } </div> </form>}package dashboard01 import ( "context" "net/http" "strconv" "github.com/a-h/templ") // The tsx keeps pagination in tanstack client state; the templ pendant is// the URL: ?page= and ?size= drive the server-rendered window (backend is// the state). The nav buttons are plain links, so the block works with no// script and no dependency - wire them to your swap layer of choice (HTMX// etc.) for partial updates. type requestKey struct{} // WithRequest puts the request into the render context so DataTable can// read its pagination params. Mount the block page like://// mux.Handle("GET /dashboard", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {// r = r.WithContext(dashboard01.WithRequest(r.Context(), r))// // The htmx requests ask for a templ fragment (?fragment=) and the// // clean URL rides back on HX-Push-Url:// if f := r.URL.Query().Get("fragment"); f != "" {// if f == "data-table" && r.Header.Get("HX-Request") == "true" {// q := r.URL.Query()// q.Del("fragment")// w.Header().Set("HX-Push-Url", r.URL.Path+"?"+q.Encode())// }// templ.Handler(dashboard01.Page(), templ.WithFragments(f)).ServeHTTP(w, r)// return// }// dashboard01.Page().Render(r.Context(), w)// }))//// Without it the block still renders, on page 1 with 10 rows.func WithRequest(ctx context.Context, r *http.Request) context.Context { return context.WithValue(ctx, requestKey{}, r)} // dataTablePageSizes mirrors the tsx rows-per-page options.var dataTablePageSizes = []int{10, 20, 30, 40, 50} // dataTablePageParams reads ?page= and ?size= from the request in ctx,// clamped to the option list and the page count (the tanstack state's// pageIndex/pageSize pendant).func dataTablePageParams(ctx context.Context, total int) (page, size int) { page, size = 1, dataTablePageSize r, ok := ctx.Value(requestKey{}).(*http.Request) if !ok { return page, size } if s, err := strconv.Atoi(r.URL.Query().Get("size")); err == nil { for _, option := range dataTablePageSizes { if s == option { size = s break } } } if p, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil { page = min(max(p, 1), dataTablePageCount(total, size)) } return page, size} // dataTablePageCount is the getPageCount pendant.func dataTablePageCount(total, size int) int { return max((total+size-1)/size, 1)} // dataTablePageWindow slices the current page's rows.func dataTablePageWindow(data []Item, page, size int) []Item { start := min((page-1)*size, len(data)) return data[start:min(start+size, len(data))]} // dataTablePageHref builds the ?page=&size= link of a nav button.func dataTablePageHref(page, size int) string { return "?page=" + strconv.Itoa(page) + "&size=" + strconv.Itoa(size)} // dataTableHxAttrs are the htmx attributes of a pagination control: fetch// the data-table fragment and swap this table in place. The clean URL// (without the fragment param) is pushed by the server's HX-Push-Url// response header - see the WithRequest mount example. Datastar users// translate these attributes 1:1.func dataTableHxAttrs(href string) templ.Attributes { return templ.Attributes{ "hx-get": href + "&fragment=data-table", "hx-target": "[data-dashboard01-table]", "hx-swap": "outerHTML", }} // dataTableDrawerFragment names a row drawer's templ fragment.func dataTableDrawerFragment(id int) string { return "row-drawer-" + strconv.Itoa(id)} // dataTableDrawerBodyID is the DOM id the drawer trigger's hx-target swaps// the lazy-loaded body into.func dataTableDrawerBodyID(id int) string { return "row-drawer-body-" + strconv.Itoa(id)} // dataTableDrawerRequested reports whether this row's drawer content is the// requested fragment: only then does the heavy drawer body (chart + form)// render. Every other render ships the skeleton, and the drawer trigger// lazy-loads the body on first open.func dataTableDrawerRequested(ctx context.Context, id int) bool { r, ok := ctx.Value(requestKey{}).(*http.Request) return ok && r.URL.Query().Get("fragment") == dataTableDrawerFragment(id)}package dashboard01 import ( "github.com/axadrn/shadcn-templ/v2/components/dropdownmenu" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/sidebar") templ NavDocuments(items []DocumentItem) { @sidebar.Group(sidebar.GroupProps{Class: "group-data-[collapsible=icon]:hidden"}) { @sidebar.GroupLabel() { Documents } @sidebar.Menu() { for _, item := range items { @sidebar.MenuItem() { @sidebar.MenuButton(sidebar.MenuButtonProps{Href: item.URL}) { @item.Icon() <span>{ item.Name }</span> } @dropdownmenu.DropdownMenu() { @sidebar.MenuAction(sidebar.MenuActionProps{ ShowOnHover: true, Class: "aria-expanded:bg-muted", Attributes: dropdownmenu.Trigger(ctx), }) { @icon.Ellipsis() <span class="sr-only">More</span> } @dropdownmenu.Content(dropdownmenu.ContentProps{ Class: "w-24", Side: dropdownmenu.SideRight, Align: dropdownmenu.AlignStart, MobileSide: dropdownmenu.SideBottom, MobileAlign: dropdownmenu.AlignEnd, }) { @dropdownmenu.Item() { @icon.Folder() <span>Open</span> } @dropdownmenu.Item() { @icon.Share() <span>Share</span> } @dropdownmenu.Separator() @dropdownmenu.Item(dropdownmenu.ItemProps{Variant: dropdownmenu.ItemVariantDestructive}) { @icon.Trash2() <span>Delete</span> } } } } } @sidebar.MenuItem() { @sidebar.MenuButton(sidebar.MenuButtonProps{Class: "text-sidebar-foreground/70"}) { @icon.Ellipsis(icon.Props{Class: "text-sidebar-foreground/70"}) <span>More</span> } } } }}package dashboard01 import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/sidebar") templ NavMain(items []NavItem) { @sidebar.Group() { @sidebar.GroupContent(sidebar.GroupContentProps{Class: "flex flex-col gap-2"}) { @sidebar.Menu() { @sidebar.MenuItem(sidebar.MenuItemProps{Class: "flex items-center gap-2"}) { @sidebar.MenuButton(sidebar.MenuButtonProps{ Tooltip: "Quick Create", Class: "min-w-8 bg-primary text-primary-foreground duration-200 ease-linear hover:bg-primary/90 hover:text-primary-foreground active:bg-primary/90 active:text-primary-foreground", }) { @icon.CirclePlus() <span>Quick Create</span> } @button.Button(button.Props{ Size: button.SizeIcon, Class: "size-8 group-data-[collapsible=icon]:opacity-0", Variant: button.VariantOutline, }) { @icon.Mail() <span class="sr-only">Inbox</span> } } } @sidebar.Menu() { for _, item := range items { @sidebar.MenuItem() { @sidebar.MenuButton(sidebar.MenuButtonProps{Tooltip: item.Title}) { if item.Icon != nil { @item.Icon() } <span>{ item.Title }</span> } } } } } }}package dashboard01 import "github.com/axadrn/shadcn-templ/v2/components/sidebar" templ NavSecondary(items []NavItem, class string) { @sidebar.Group(sidebar.GroupProps{Class: class}) { @sidebar.GroupContent() { @sidebar.Menu() { for _, item := range items { @sidebar.MenuItem() { @sidebar.MenuButton(sidebar.MenuButtonProps{Href: item.URL}) { @item.Icon() <span>{ item.Title }</span> } } } } } }}package dashboard01 import ( "github.com/axadrn/shadcn-templ/v2/components/avatar" "github.com/axadrn/shadcn-templ/v2/components/dropdownmenu" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/sidebar") templ NavUser(user User) { @sidebar.Menu() { @sidebar.MenuItem() { @dropdownmenu.DropdownMenu() { @sidebar.MenuButton(sidebar.MenuButtonProps{ Size: sidebar.MenuButtonSizeLg, Class: "aria-expanded:bg-muted", Attributes: dropdownmenu.Trigger(ctx), }) { @avatar.Avatar(avatar.Props{Class: "size-8 rounded-lg grayscale"}) { @avatar.Image(avatar.ImageProps{Src: user.Avatar, Alt: user.Name}) @avatar.Fallback(avatar.FallbackProps{Class: "rounded-lg"}) { AA } } <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium">{ user.Name }</span> <span class="truncate text-xs text-foreground/70"> { user.Email } </span> </div> @icon.EllipsisVertical(icon.Props{Class: "ml-auto size-4"}) } @dropdownmenu.Content(dropdownmenu.ContentProps{ Class: "min-w-56", Side: dropdownmenu.SideRight, Align: dropdownmenu.AlignEnd, MobileSide: dropdownmenu.SideBottom, SideOffset: 4, }) { @dropdownmenu.Group() { @dropdownmenu.Label(dropdownmenu.LabelProps{Class: "p-0 font-normal"}) { <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> @avatar.Avatar(avatar.Props{Class: "size-8"}) { @avatar.Image(avatar.ImageProps{Src: user.Avatar, Alt: user.Name}) @avatar.Fallback(avatar.FallbackProps{Class: "rounded-lg"}) { AA } } <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium">{ user.Name }</span> <span class="truncate text-xs text-muted-foreground"> { user.Email } </span> </div> </div> } } @dropdownmenu.Separator() @dropdownmenu.Group() { @dropdownmenu.Item() { @icon.CircleUserRound() Account } @dropdownmenu.Item() { @icon.CreditCard() Billing } @dropdownmenu.Item() { @icon.Bell() Notifications } } @dropdownmenu.Separator() @dropdownmenu.Item() { @icon.LogOut() Log out } } } } }}package dashboard01 import ( "github.com/axadrn/shadcn-templ/v2/components/badge" "github.com/axadrn/shadcn-templ/v2/components/card" "github.com/axadrn/shadcn-templ/v2/components/icon") templ SectionCards() { <div class="grid grid-cols-1 gap-4 px-4 *:data-[slot=card]:bg-linear-to-t *:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card *:data-[slot=card]:shadow-xs lg:px-6 @xl/main:grid-cols-2 @5xl/main:grid-cols-4 dark:*:data-[slot=card]:bg-card"> @card.Card(card.Props{Class: "@container/card"}) { @card.Header() { @card.Description() { Total Revenue } @card.Title(card.TitleProps{Class: "text-2xl font-semibold tabular-nums @[250px]/card:text-3xl"}) { $1,250.00 } @card.Action() { @badge.Badge(badge.Props{Variant: badge.VariantOutline}) { @icon.TrendingUp() +12.5% } } } @card.Footer(card.FooterProps{Class: "flex-col items-start gap-1.5 text-sm"}) { <div class="line-clamp-1 flex gap-2 font-medium"> Trending up this month @icon.TrendingUp(icon.Props{Class: "size-4"}) </div> <div class="text-muted-foreground"> Visitors for the last 6 months </div> } } @card.Card(card.Props{Class: "@container/card"}) { @card.Header() { @card.Description() { New Customers } @card.Title(card.TitleProps{Class: "text-2xl font-semibold tabular-nums @[250px]/card:text-3xl"}) { 1,234 } @card.Action() { @badge.Badge(badge.Props{Variant: badge.VariantOutline}) { @icon.TrendingDown() -20% } } } @card.Footer(card.FooterProps{Class: "flex-col items-start gap-1.5 text-sm"}) { <div class="line-clamp-1 flex gap-2 font-medium"> Down 20% this period @icon.TrendingDown(icon.Props{Class: "size-4"}) </div> <div class="text-muted-foreground"> Acquisition needs attention </div> } } @card.Card(card.Props{Class: "@container/card"}) { @card.Header() { @card.Description() { Active Accounts } @card.Title(card.TitleProps{Class: "text-2xl font-semibold tabular-nums @[250px]/card:text-3xl"}) { 45,678 } @card.Action() { @badge.Badge(badge.Props{Variant: badge.VariantOutline}) { @icon.TrendingUp() +12.5% } } } @card.Footer(card.FooterProps{Class: "flex-col items-start gap-1.5 text-sm"}) { <div class="line-clamp-1 flex gap-2 font-medium"> Strong user retention @icon.TrendingUp(icon.Props{Class: "size-4"}) </div> <div class="text-muted-foreground">Engagement exceed targets</div> } } @card.Card(card.Props{Class: "@container/card"}) { @card.Header() { @card.Description() { Growth Rate } @card.Title(card.TitleProps{Class: "text-2xl font-semibold tabular-nums @[250px]/card:text-3xl"}) { 4.5% } @card.Action() { @badge.Badge(badge.Props{Variant: badge.VariantOutline}) { @icon.TrendingUp() +4.5% } } } @card.Footer(card.FooterProps{Class: "flex-col items-start gap-1.5 text-sm"}) { <div class="line-clamp-1 flex gap-2 font-medium"> Steady performance increase @icon.TrendingUp(icon.Props{Class: "size-4"}) </div> <div class="text-muted-foreground">Meets growth projections</div> } } </div>}package dashboard01 import ( "github.com/axadrn/shadcn-templ/v2/components/separator" "github.com/axadrn/shadcn-templ/v2/components/sidebar") templ SiteHeader() { <header class="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"> <div class="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6"> @sidebar.Trigger(sidebar.TriggerProps{Class: "-ml-1"}) @separator.Separator(separator.Props{ Orientation: separator.OrientationVertical, Class: "mx-2 h-4 data-vertical:self-auto", }) <h1 class="text-base font-medium">Documents</h1> </div> </header>}package sidebar07 import ( "github.com/axadrn/shadcn-templ/v2/components/breadcrumb" "github.com/axadrn/shadcn-templ/v2/components/separator" "github.com/axadrn/shadcn-templ/v2/components/sidebar") templ Page() { @sidebar.Provider() { @AppSidebar() @sidebar.Inset() { <header class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12"> <div class="flex items-center gap-2 px-4"> @sidebar.Trigger(sidebar.TriggerProps{Class: "-ml-1"}) @separator.Separator(separator.Props{ Orientation: separator.OrientationVertical, Class: "mr-2 data-vertical:h-4 data-vertical:self-auto", }) @breadcrumb.Breadcrumb() { @breadcrumb.List() { @breadcrumb.Item(breadcrumb.ItemProps{Class: "hidden md:block"}) { @breadcrumb.Link(breadcrumb.LinkProps{Href: "#"}) { Build Your Application } } @breadcrumb.Separator(breadcrumb.SeparatorProps{Class: "hidden md:block"}) @breadcrumb.Item() { @breadcrumb.Page() { Data Fetching } } } } </div> </header> <div class="flex flex-1 flex-col gap-4 p-4 pt-0"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <div class="aspect-video rounded-xl bg-muted/50"></div> <div class="aspect-video rounded-xl bg-muted/50"></div> <div class="aspect-video rounded-xl bg-muted/50"></div> </div> <div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"></div> </div> } } <script nonce={ templ.GetNonce(ctx) }> (() => { // Keep the selected team details in sync. document.addEventListener("click", (e) => { if (!(e.target instanceof Element)) return; const item = e.target.closest("#sidebar07-team-menu [data-sidebar07-team-index]"); if (!item) return; const i = item.getAttribute("data-sidebar07-team-index"); const name = item.textContent.trim().replace(/⌘\d$/, "").trim(); const plan = ["Enterprise", "Startup", "Free"][parseInt(i, 10)]; document.querySelectorAll("[data-sidebar07-team-logo]").forEach((logo) => { logo.classList.toggle("hidden", logo.getAttribute("data-sidebar07-team-logo") !== i); }); const nameEl = document.querySelector("[data-sidebar07-team-name]"); const planEl = document.querySelector("[data-sidebar07-team-plan]"); if (nameEl) nameEl.textContent = name; if (planEl) planEl.textContent = plan; }); })(); </script>}package sidebar07 import ( "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/sidebar") // This is sample data.var data = struct { User User Teams []Team NavMain []NavMainItem Projects []Project}{ User: User{ Name: "Axel Adrian", Email: "[email protected]", Avatar: "https://github.com/axadrn.png", }, Teams: []Team{ {Name: "Acme Inc", Logo: icon.GalleryVerticalEnd, Plan: "Enterprise"}, {Name: "Acme Corp.", Logo: icon.AudioLines, Plan: "Startup"}, {Name: "Evil Corp.", Logo: icon.Terminal, Plan: "Free"}, }, NavMain: []NavMainItem{ { Title: "Playground", URL: "#", Icon: icon.SquareTerminal, IsActive: true, Items: []NavMainSubItem{ {Title: "History", URL: "#"}, {Title: "Starred", URL: "#"}, {Title: "Settings", URL: "#"}, }, }, { Title: "Models", URL: "#", Icon: icon.Bot, Items: []NavMainSubItem{ {Title: "Genesis", URL: "#"}, {Title: "Explorer", URL: "#"}, {Title: "Quantum", URL: "#"}, }, }, { Title: "Documentation", URL: "#", Icon: icon.BookOpen, Items: []NavMainSubItem{ {Title: "Introduction", URL: "#"}, {Title: "Get Started", URL: "#"}, {Title: "Tutorials", URL: "#"}, {Title: "Changelog", URL: "#"}, }, }, { Title: "Settings", URL: "#", Icon: icon.Settings2, Items: []NavMainSubItem{ {Title: "General", URL: "#"}, {Title: "Team", URL: "#"}, {Title: "Billing", URL: "#"}, {Title: "Limits", URL: "#"}, }, }, }, Projects: []Project{ {Name: "Design Engineering", URL: "#", Icon: icon.Frame}, {Name: "Sales & Marketing", URL: "#", Icon: icon.ChartPie}, {Name: "Travel", URL: "#", Icon: icon.Map}, },} templ AppSidebar(props ...sidebar.Props) { {{ var p sidebar.Props }} if len(props) > 0 { {{ p = props[0] }} } if p.Collapsible == "" { {{ p.Collapsible = sidebar.CollapsibleIcon }} } @sidebar.Sidebar(p) { @sidebar.Header() { @TeamSwitcher(data.Teams) } @sidebar.Content() { @NavMain(data.NavMain) @NavProjects(data.Projects) } @sidebar.Footer() { @NavUser(data.User) } @sidebar.Rail() }}package sidebar07 import ( "github.com/axadrn/shadcn-templ/v2/components/collapsible" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/sidebar") type NavMainItem struct { Title string URL string Icon func(...icon.Props) templ.Component IsActive bool Items []NavMainSubItem} type NavMainSubItem struct { Title string URL string} templ NavMain(items []NavMainItem) { @sidebar.Group() { @sidebar.GroupLabel() { Platform } @sidebar.Menu() { for _, item := range items { @collapsible.Collapsible(collapsible.Props{ DefaultOpen: item.IsActive, Class: "group/collapsible", }) { @sidebar.MenuItem() { @sidebar.MenuButton(sidebar.MenuButtonProps{ Tooltip: item.Title, Attributes: collapsible.Trigger(ctx), }) { if item.Icon != nil { @item.Icon() } <span>{ item.Title }</span> @icon.ChevronRight(icon.Props{Class: "ml-auto transition-transform duration-200 group-data-open/collapsible:rotate-90"}) } @collapsible.Content() { @sidebar.MenuSub() { for _, subItem := range item.Items { @sidebar.MenuSubItem() { @sidebar.MenuSubButton(sidebar.MenuSubButtonProps{Href: subItem.URL}) { <span>{ subItem.Title }</span> } } } } } } } } } }}package sidebar07 import ( "github.com/axadrn/shadcn-templ/v2/components/dropdownmenu" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/sidebar") type Project struct { Name string URL string Icon func(...icon.Props) templ.Component} templ NavProjects(projects []Project) { @sidebar.Group(sidebar.GroupProps{Class: "group-data-[collapsible=icon]:hidden"}) { @sidebar.GroupLabel() { Projects } @sidebar.Menu() { for _, item := range projects { @sidebar.MenuItem() { @sidebar.MenuButton(sidebar.MenuButtonProps{Href: item.URL}) { @item.Icon() <span>{ item.Name }</span> } @dropdownmenu.DropdownMenu() { @sidebar.MenuAction(sidebar.MenuActionProps{ ShowOnHover: true, Class: "aria-expanded:bg-muted", Attributes: dropdownmenu.Trigger(ctx), }) { @icon.Ellipsis() <span class="sr-only">More</span> } @dropdownmenu.Content(dropdownmenu.ContentProps{ Class: "w-fit", Side: dropdownmenu.SideRight, Align: dropdownmenu.AlignStart, MobileSide: dropdownmenu.SideBottom, MobileAlign: dropdownmenu.AlignEnd, }) { @dropdownmenu.Item() { @icon.Folder() <span>View Project</span> } @dropdownmenu.Item() { @icon.ArrowRight() <span>Share Project</span> } @dropdownmenu.Separator() @dropdownmenu.Item(dropdownmenu.ItemProps{Variant: dropdownmenu.ItemVariantDestructive}) { @icon.Trash2() <span>Delete Project</span> } } } } } @sidebar.MenuItem() { @sidebar.MenuButton(sidebar.MenuButtonProps{Class: "text-sidebar-foreground/70"}) { @icon.Ellipsis(icon.Props{Class: "text-sidebar-foreground/70"}) <span>More</span> } } } }}package sidebar07 import ( "github.com/axadrn/shadcn-templ/v2/components/avatar" "github.com/axadrn/shadcn-templ/v2/components/dropdownmenu" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/sidebar") type User struct { Name string Email string Avatar string} templ NavUser(user User) { @sidebar.Menu() { @sidebar.MenuItem() { @dropdownmenu.DropdownMenu() { @sidebar.MenuButton(sidebar.MenuButtonProps{ Size: sidebar.MenuButtonSizeLg, Class: "aria-expanded:bg-muted", Attributes: dropdownmenu.Trigger(ctx), }) { @avatar.Avatar() { @avatar.Image(avatar.ImageProps{Src: user.Avatar, Alt: user.Name}) @avatar.Fallback() { AA } } <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium">{ user.Name }</span> <span class="truncate text-xs">{ user.Email }</span> </div> @icon.ChevronsUpDown(icon.Props{Class: "ml-auto size-4"}) } @dropdownmenu.Content(dropdownmenu.ContentProps{ Class: "w-fit", Side: dropdownmenu.SideRight, Align: dropdownmenu.AlignEnd, MobileSide: dropdownmenu.SideBottom, SideOffset: 4, }) { @dropdownmenu.Group() { @dropdownmenu.Label(dropdownmenu.LabelProps{Class: "p-0 font-normal"}) { <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> @avatar.Avatar() { @avatar.Image(avatar.ImageProps{Src: user.Avatar, Alt: user.Name}) @avatar.Fallback() { AA } } <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium">{ user.Name }</span> <span class="truncate text-xs">{ user.Email }</span> </div> </div> } } @dropdownmenu.Separator() @dropdownmenu.Group() { @dropdownmenu.Item() { @icon.Sparkles() Upgrade to Pro } } @dropdownmenu.Separator() @dropdownmenu.Group() { @dropdownmenu.Item() { @icon.BadgeCheck() Account } @dropdownmenu.Item() { @icon.CreditCard() Billing } @dropdownmenu.Item() { @icon.Bell() Notifications } } @dropdownmenu.Separator() @dropdownmenu.Item() { @icon.LogOut() Log out } } } } }}package sidebar07 import ( "strconv" "github.com/axadrn/shadcn-templ/v2/components/dropdownmenu" "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/sidebar") type Team struct { Name string Logo func(...icon.Props) templ.Component Plan string} func teamLogoHidden(i int) string { if i == 0 { return "" } return " hidden"} templ TeamSwitcher(teams []Team) { if len(teams) > 0 { @sidebar.Menu() { @sidebar.MenuItem() { @dropdownmenu.DropdownMenu(dropdownmenu.Props{ID: "sidebar07-team-menu"}) { @sidebar.MenuButton(sidebar.MenuButtonProps{ Size: sidebar.MenuButtonSizeLg, Class: "data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground", Attributes: dropdownmenu.Trigger(ctx), }) { <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> // Render every logo up front; the page script shows the active one. for i, team := range teams { @team.Logo(icon.Props{ Class: teamLogoHidden(i), Attributes: templ.Attributes{"data-sidebar07-team-logo": strconv.Itoa(i)}, }) } </div> <div class="grid flex-1 text-left text-sm leading-tight"> <span class="truncate font-medium" data-sidebar07-team-name>{ teams[0].Name }</span> <span class="truncate text-xs" data-sidebar07-team-plan>{ teams[0].Plan }</span> </div> @icon.ChevronsUpDown(icon.Props{Class: "ml-auto"}) } @dropdownmenu.Content(dropdownmenu.ContentProps{ Class: "w-fit", Align: dropdownmenu.AlignStart, Side: dropdownmenu.SideRight, MobileSide: dropdownmenu.SideBottom, SideOffset: 4, }) { @dropdownmenu.Group() { @dropdownmenu.Label(dropdownmenu.LabelProps{Class: "text-xs text-muted-foreground"}) { Teams } for i, team := range teams { @dropdownmenu.Item(dropdownmenu.ItemProps{ Class: "gap-2 p-2", Attributes: templ.Attributes{"data-sidebar07-team-index": strconv.Itoa(i)}, }) { <div class="flex size-6 items-center justify-center rounded-md border"> @team.Logo() </div> { team.Name } @dropdownmenu.Shortcut() { ⌘{ strconv.Itoa(i + 1) } } } } } @dropdownmenu.Separator() @dropdownmenu.Group() { @dropdownmenu.Item(dropdownmenu.ItemProps{Class: "gap-2 p-2"}) { <div class="flex size-6 items-center justify-center rounded-md border bg-transparent"> @icon.Plus(icon.Props{Class: "size-4"}) </div> <div class="font-medium text-muted-foreground">Add team</div> } } } } } } }}package sidebar03 import ( "github.com/axadrn/shadcn-templ/v2/components/breadcrumb" "github.com/axadrn/shadcn-templ/v2/components/separator" "github.com/axadrn/shadcn-templ/v2/components/sidebar") templ Page() { @sidebar.Provider() { @AppSidebar() @sidebar.Inset() { <header class="flex h-16 shrink-0 items-center gap-2 border-b"> <div class="flex items-center gap-2 px-3"> @sidebar.Trigger() @separator.Separator(separator.Props{ Orientation: separator.OrientationVertical, Class: "mr-2 data-vertical:h-4 data-vertical:self-auto", }) @breadcrumb.Breadcrumb() { @breadcrumb.List() { @breadcrumb.Item(breadcrumb.ItemProps{Class: "hidden md:block"}) { @breadcrumb.Link(breadcrumb.LinkProps{Href: "#"}) { Build Your Application } } @breadcrumb.Separator(breadcrumb.SeparatorProps{Class: "hidden md:block"}) @breadcrumb.Item() { @breadcrumb.Page() { Data Fetching } } } } </div> </header> <div class="flex flex-1 flex-col gap-4 p-4"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <div class="aspect-video rounded-xl bg-muted/50"></div> <div class="aspect-video rounded-xl bg-muted/50"></div> <div class="aspect-video rounded-xl bg-muted/50"></div> </div> <div class="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"></div> </div> } }}package sidebar03 import ( "github.com/axadrn/shadcn-templ/v2/components/icon" "github.com/axadrn/shadcn-templ/v2/components/sidebar") // This is sample data.type navItem struct { Title string URL string Items []navSubItem} type navSubItem struct { Title string URL string IsActive bool} var navMain = []navItem{ { Title: "Getting Started", URL: "#", Items: []navSubItem{ {Title: "Installation", URL: "#"}, {Title: "Project Structure", URL: "#"}, }, }, { Title: "Build Your Application", URL: "#", Items: []navSubItem{ {Title: "Routing", URL: "#"}, {Title: "Data Fetching", URL: "#", IsActive: true}, {Title: "Rendering", URL: "#"}, {Title: "Caching", URL: "#"}, {Title: "Styling", URL: "#"}, {Title: "Optimizing", URL: "#"}, {Title: "Configuring", URL: "#"}, {Title: "Testing", URL: "#"}, {Title: "Authentication", URL: "#"}, {Title: "Deploying", URL: "#"}, {Title: "Upgrading", URL: "#"}, {Title: "Examples", URL: "#"}, }, }, { Title: "API Reference", URL: "#", Items: []navSubItem{ {Title: "Components", URL: "#"}, {Title: "File Conventions", URL: "#"}, {Title: "Functions", URL: "#"}, {Title: "next.config.js Options", URL: "#"}, {Title: "CLI", URL: "#"}, {Title: "Edge Runtime", URL: "#"}, }, }, { Title: "Architecture", URL: "#", Items: []navSubItem{ {Title: "Accessibility", URL: "#"}, {Title: "Fast Refresh", URL: "#"}, {Title: "templ Compiler", URL: "#"}, {Title: "Supported Browsers", URL: "#"}, {Title: "Turbopack", URL: "#"}, }, }, { Title: "Community", URL: "#", Items: []navSubItem{ {Title: "Contribution Guide", URL: "#"}, }, },} templ AppSidebar() { @sidebar.Sidebar() { @sidebar.Header() { @sidebar.Menu() { @sidebar.MenuItem() { @sidebar.MenuButton(sidebar.MenuButtonProps{ Size: sidebar.MenuButtonSizeLg, Href: "#", }) { <div class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"> @icon.GalleryVerticalEnd(icon.Props{Class: "size-4"}) </div> <div class="flex flex-col gap-0.5 leading-none"> <span class="font-medium">Documentation</span> <span class="">v1.0.0</span> </div> } } } } @sidebar.Content() { @sidebar.Group() { @sidebar.Menu() { for _, item := range navMain { @sidebar.MenuItem() { @sidebar.MenuButton(sidebar.MenuButtonProps{ Href: item.URL, Class: "font-medium", }) { { item.Title } } if len(item.Items) > 0 { @sidebar.MenuSub() { for _, subItem := range item.Items { @sidebar.MenuSubItem() { @sidebar.MenuSubButton(sidebar.MenuSubButtonProps{ Href: subItem.URL, IsActive: subItem.IsActive, }) { { subItem.Title } } } } } } } } } } } @sidebar.Rail() }}package login03 import "github.com/axadrn/shadcn-templ/v2/components/icon" templ Page() { <div class="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10"> <div class="flex w-full max-w-sm flex-col gap-6"> <a href="#" class="flex items-center gap-2 self-center font-medium"> <div class="flex size-6 items-center justify-center rounded-md bg-primary text-primary-foreground"> @icon.GalleryVerticalEnd(icon.Props{Class: "size-4"}) </div> Acme Inc. </a> @LoginForm() </div> </div>}package login03 import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/card" "github.com/axadrn/shadcn-templ/v2/components/field" "github.com/axadrn/shadcn-templ/v2/components/input") templ LoginForm() { <div class="flex flex-col gap-6"> @card.Card() { @card.Header(card.HeaderProps{Class: "text-center"}) { @card.Title(card.TitleProps{Class: "text-xl"}) { Welcome back } @card.Description() { Login with your Apple or Google account } } @card.Content() { <form> @field.Group() { @field.Field() { @button.Button(button.Props{Variant: button.VariantOutline, Type: button.TypeButton}) { <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" fill="currentColor" ></path> </svg> Login with Apple } @button.Button(button.Props{Variant: button.VariantOutline, Type: button.TypeButton}) { <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" fill="currentColor" ></path> </svg> Login with Google } } @field.Separator(field.SeparatorProps{Class: "*:data-[slot=field-separator-content]:bg-card"}) { Or continue with } @field.Field() { @field.Label(field.LabelProps{For: "email"}) { Email } @input.Input(input.Props{ ID: "email", Type: "email", Placeholder: "[email protected]", Required: true, }) } @field.Field() { <div class="flex items-center"> @field.Label(field.LabelProps{For: "password"}) { Password } <a href="#" class="ml-auto text-sm underline-offset-4 hover:underline" > Forgot your password? </a> </div> @input.Input(input.Props{ID: "password", Type: "password", Required: true}) } @field.Field() { @button.Button(button.Props{Type: button.TypeSubmit}) { Login } @field.Description(field.DescriptionProps{Class: "text-center"}) { Don't have an account? <a href="#">Sign up</a> } } } </form> } } @field.Description(field.DescriptionProps{Class: "px-6 text-center"}) { By clicking continue, you agree to our <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>. } </div>}package login04 templ Page() { <div class="flex min-h-svh flex-col items-center justify-center bg-muted p-6 md:p-10"> <div class="w-full max-w-sm md:max-w-4xl"> @LoginForm() </div> </div>}package login04 import ( "github.com/axadrn/shadcn-templ/v2/components/button" "github.com/axadrn/shadcn-templ/v2/components/card" "github.com/axadrn/shadcn-templ/v2/components/field" "github.com/axadrn/shadcn-templ/v2/components/input") templ LoginForm() { <div class="flex flex-col gap-6"> @card.Card(card.Props{Class: "overflow-hidden p-0"}) { @card.Content(card.ContentProps{Class: "grid p-0 md:grid-cols-2"}) { <form class="p-6 md:p-8"> @field.Group() { <div class="flex flex-col items-center gap-2 text-center"> <h1 class="text-2xl font-bold">Welcome back</h1> <p class="text-balance text-muted-foreground"> Login to your Acme Inc account </p> </div> @field.Field() { @field.Label(field.LabelProps{For: "email"}) { Email } @input.Input(input.Props{ ID: "email", Type: "email", Placeholder: "[email protected]", Required: true, }) } @field.Field() { <div class="flex items-center"> @field.Label(field.LabelProps{For: "password"}) { Password } <a href="#" class="ml-auto text-sm underline-offset-2 hover:underline" > Forgot your password? </a> </div> @input.Input(input.Props{ID: "password", Type: "password", Required: true}) } @field.Field() { @button.Button(button.Props{Type: button.TypeSubmit}) { Login } } @field.Separator(field.SeparatorProps{Class: "*:data-[slot=field-separator-content]:bg-card"}) { Or continue with } @field.Field(field.Props{Class: "grid grid-cols-3 gap-4"}) { @button.Button(button.Props{Variant: button.VariantOutline, Type: button.TypeButton}) { <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" fill="currentColor" ></path> </svg> <span class="sr-only">Login with Apple</span> } @button.Button(button.Props{Variant: button.VariantOutline, Type: button.TypeButton}) { <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" fill="currentColor" ></path> </svg> <span class="sr-only">Login with Google</span> } @button.Button(button.Props{Variant: button.VariantOutline, Type: button.TypeButton}) { <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z" fill="currentColor" ></path> </svg> <span class="sr-only">Login with Meta</span> } } @field.Description(field.DescriptionProps{Class: "text-center"}) { Don't have an account? <a href="#">Sign up</a> } } </form> <div class="relative hidden bg-muted md:block"> <img src="/placeholder.svg" alt="Image" class="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale" /> </div> } } @field.Description(field.DescriptionProps{Class: "px-6 text-center"}) { By clicking continue, you agree to our <a href="#">Terms of Service</a> and <a href="#">Privacy Policy</a>. } </div>}