diff --git a/assets/css/output.css b/assets/css/output.css index c80a1a8..3c26854 100644 --- a/assets/css/output.css +++ b/assets/css/output.css @@ -666,6 +666,10 @@ body { bottom: 1rem; } +.bottom-full { + bottom: 100%; +} + .left-0 { left: 0px; } @@ -690,6 +694,10 @@ body { top: 0.5rem; } +.top-full { + top: 100%; +} + .z-10 { z-index: 10; } @@ -711,6 +719,10 @@ body { margin-right: auto; } +.-mr-1 { + margin-right: -0.25rem; +} + .mb-1 { margin-bottom: 0.25rem; } @@ -747,6 +759,10 @@ body { margin-left: 0.25rem; } +.ml-2 { + margin-left: 0.5rem; +} + .mr-2 { margin-right: 0.5rem; } @@ -887,6 +903,10 @@ body { width: 1.25rem; } +.w-56 { + width: 14rem; +} + .w-6 { width: 1.5rem; } @@ -991,6 +1011,18 @@ body { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.scale-100 { + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-95 { + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + .transform { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } @@ -1199,6 +1231,11 @@ body { border-color: hsl(var(--foreground) / var(--tw-border-opacity)); } +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); +} + .border-input { --tw-border-opacity: 1; border-color: hsl(var(--input) / var(--tw-border-opacity)); @@ -1631,6 +1668,21 @@ body { outline-style: solid; } +.ring-1 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-black { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity)); +} + +.ring-opacity-5 { + --tw-ring-opacity: 0.05; +} + .ring-offset-background { --tw-ring-offset-color: hsl(var(--background) / 1); } @@ -1687,6 +1739,10 @@ body { transition-duration: 300ms; } +.duration-75 { + transition-duration: 75ms; +} + .ease-in { transition-timing-function: cubic-bezier(0.4, 0, 1, 1); } @@ -1749,6 +1805,11 @@ body { background-color: rgb(243 244 246 / var(--tw-bg-opacity)); } +.hover\:bg-gray-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + .hover\:bg-gray-600:hover { --tw-bg-opacity: 1; background-color: rgb(75 85 99 / var(--tw-bg-opacity)); @@ -1787,6 +1848,11 @@ body { color: rgb(107 114 128 / var(--tw-text-opacity)); } +.hover\:text-gray-900:hover { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + .hover\:text-green-700:hover { --tw-text-opacity: 1; color: rgb(21 128 61 / var(--tw-text-opacity)); @@ -1824,6 +1890,10 @@ body { --tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity)); } +.focus\:ring-offset-2:focus { + --tw-ring-offset-width: 2px; +} + .focus-visible\:outline-none:focus-visible { outline: 2px solid transparent; outline-offset: 2px; @@ -1878,6 +1948,16 @@ body { opacity: 0.7; } +.dark\:border-gray-600:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); +} + +.dark\:bg-gray-800:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); +} + .dark\:from-gray-900:is(.dark *) { --tw-gradient-from: #111827 var(--tw-gradient-from-position); --tw-gradient-to: rgb(17 24 39 / 0) var(--tw-gradient-to-position); @@ -1908,6 +1988,11 @@ body { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.dark\:ring-gray-700:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity)); +} + .dark\:file\:text-foreground:is(.dark *)::file-selector-button { --tw-text-opacity: 1; color: hsl(var(--foreground) / var(--tw-text-opacity)); @@ -1918,6 +2003,16 @@ body { background-color: rgb(55 65 81 / var(--tw-bg-opacity)); } +.dark\:hover\:text-white:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.dark\:focus\:ring-indigo-400:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(129 140 248 / var(--tw-ring-opacity)); +} + @media (min-width: 640px) { .sm\:my-8 { margin-top: 2rem; diff --git a/cmd/server/main.go b/cmd/server/main.go index 35947b0..9bf2234 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -28,6 +28,7 @@ func main() { mux.Handle("GET /docs/components/button", templ.Handler(pages.Button())) mux.Handle("GET /docs/components/card", templ.Handler(pages.Card())) mux.Handle("GET /docs/components/datepicker", templ.Handler(pages.Datepicker())) + mux.Handle("GET /docs/components/dropdown-menu", templ.Handler(pages.DropdownMenu())) mux.Handle("GET /docs/components/icon", templ.Handler(pages.Icon())) mux.Handle("GET /docs/components/input", templ.Handler(pages.Input())) mux.Handle("GET /docs/components/modal", templ.Handler(pages.Modal())) diff --git a/internals/shared/menudata.go b/internals/shared/menudata.go index 666ae7e..f613fd4 100644 --- a/internals/shared/menudata.go +++ b/internals/shared/menudata.go @@ -53,6 +53,10 @@ var Sections = []Section{ Text: "Datepicker", Href: "/docs/components/datepicker", }, + { + Text: "Dropdown Menu", + Href: "/docs/components/dropdown-menu", + }, { Text: "Icon", Href: "/docs/components/icon", diff --git a/internals/ui/pages/dropdown_menu.templ b/internals/ui/pages/dropdown_menu.templ new file mode 100644 index 0000000..888f134 --- /dev/null +++ b/internals/ui/pages/dropdown_menu.templ @@ -0,0 +1,39 @@ +package pages + +import ( + "github.com/axzilla/goilerplate/pkg/components" + "github.com/axzilla/goilerplate/internals/ui/layouts" + "github.com/axzilla/goilerplate/internals/ui/showcase" +) + +templ DropdownMenu() { + @layouts.DocsLayout() { +
+
+

Dropdown Menu

+

The Dropdown Menu component provides a way to display a list of options in a compact form.

+
+ @components.Tabs(components.TabsProps{ + Tabs: []components.Tab{ + { + ID: "preview", + Title: "Preview", + Content: showcase.DropdownMenuShowcase(), + }, + { + ID: "code", + Title: "Code", + Content: CodeSnippetFromEmbedded("dropdown_menu.templ", "go", showcase.TemplFiles), + }, + { + ID: "component", + Title: "Component", + Content: CodeSnippetFromEmbedded("dropdown_menu.templ", "go", components.TemplFiles), + }, + }, + TabsContainerClass: "md:w-1/2", + ContentContainerClass: "w-full", + }) +
+ } +} diff --git a/internals/ui/showcase/dropdown_menu.templ b/internals/ui/showcase/dropdown_menu.templ new file mode 100644 index 0000000..6fb8b8a --- /dev/null +++ b/internals/ui/showcase/dropdown_menu.templ @@ -0,0 +1,20 @@ +package showcase + +import "github.com/axzilla/goilerplate/pkg/components" + +templ DropdownMenuShowcase() { +
+
+ @components.DropdownMenu(components.DropdownMenuProps{ + Label: "Click me", + Position: "left", + Items: []components.DropdownMenuItem{ + {Label: "Option 1", Value: "option1"}, + {Label: "Option 2", Value: "option2"}, + {Label: "Option 3", Value: "option3"}, + {Label: "With Link", Href: "https://github.com/axzilla/goilerplate", Target: "_blank"}, + }, + }) +
+
+} diff --git a/pkg/components/dropdown_menu.templ b/pkg/components/dropdown_menu.templ new file mode 100644 index 0000000..d4b37f9 --- /dev/null +++ b/pkg/components/dropdown_menu.templ @@ -0,0 +1,134 @@ +package components + +import ( + "fmt" +) + +// DropdownMenuItem represents an item in the dropdown menu +type DropdownMenuItem struct { + Label string + Value string + Href string + Target string +} + +// DropdownMenuProps defines the properties for the DropdownMenu component +type DropdownMenuProps struct { + Items []DropdownMenuItem + Label string + Class string + Position string // "left" or "right", defaults to right if not specified +} + +// DropdownMenu renders a dropdown menu component. +// +// It provides a button that, when clicked, displays a list of options. +// The menu can be positioned to the left or right of the button. +// It supports both button and link items, with optional target attribute for links. +// +// Props: +// - Items: Slice of DropdownMenuItem, defining the content of the dropdown +// - Label: Text to display on the dropdown button +// - Class: Additional CSS classes to apply to the root element +// - Position: "left" or "right", determines the horizontal position of the dropdown +// +// DropdownMenuItem fields: +// - Label: Text to display for the item +// - Value: Value associated with the item (for button items) +// - Href: URL for link items +// - Target: Target attribute for link items (e.g., "_blank" for new tab) +// +// Usage: +// +// @components.DropdownMenu(components.DropdownMenuProps{ +// Label: "Options", +// Position: "right", +// Items: []components.DropdownMenuItem{ +// {Label: "Option 1", Value: "opt1"}, +// {Label: "Option 2", Value: "opt2"}, +// {Label: "External Link", Href: "https://example.com", Target: "_blank"}, +// }, +// }) +// +// The component uses Alpine.js for interactivity and Tailwind CSS for styling. +// It includes dark mode support and responsive positioning. +templ DropdownMenu(props DropdownMenuProps) { +
+ +
+ +
+
+} diff --git a/pkg/components/dropdown_menu_templ.go b/pkg/components/dropdown_menu_templ.go new file mode 100644 index 0000000..857a07c --- /dev/null +++ b/pkg/components/dropdown_menu_templ.go @@ -0,0 +1,214 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.778 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" +) + +// DropdownMenuItem represents an item in the dropdown menu +type DropdownMenuItem struct { + Label string + Value string + Href string + Target string +} + +// DropdownMenuProps defines the properties for the DropdownMenu component +type DropdownMenuProps struct { + Items []DropdownMenuItem + Label string + Class string + Position string // "left" or "right", defaults to right if not specified +} + +// DropdownMenu renders a dropdown menu component. +// +// It provides a button that, when clicked, displays a list of options. +// The menu can be positioned to the left or right of the button. +// It supports both button and link items, with optional target attribute for links. +// +// Props: +// - Items: Slice of DropdownMenuItem, defining the content of the dropdown +// - Label: Text to display on the dropdown button +// - Class: Additional CSS classes to apply to the root element +// - Position: "left" or "right", determines the horizontal position of the dropdown +// +// DropdownMenuItem fields: +// - Label: Text to display for the item +// - Value: Value associated with the item (for button items) +// - Href: URL for link items +// - Target: Target attribute for link items (e.g., "_blank" for new tab) +// +// Usage: +// +// @components.DropdownMenu(components.DropdownMenuProps{ +// Label: "Options", +// Position: "right", +// Items: []components.DropdownMenuItem{ +// {Label: "Option 1", Value: "opt1"}, +// {Label: "Option 2", Value: "opt2"}, +// {Label: "External Link", Href: "https://example.com", Target: "_blank"}, +// }, +// }) +// +// The component uses Alpine.js for interactivity and Tailwind CSS for styling. +// It includes dark mode support and responsive positioning. +func DropdownMenu(props DropdownMenuProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var2 = []any{"relative inline-block text-left", props.Class} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, item := range props.Items { + if item.Href != "" { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/components/dropdown_menu.templ`, Line: 121, Col: 19} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/styles/goilerplate.css b/pkg/styles/goilerplate.css index 7dc794e..9653113 100644 --- a/pkg/styles/goilerplate.css +++ b/pkg/styles/goilerplate.css @@ -658,6 +658,10 @@ body { bottom: 0px; } +.bottom-full { + bottom: 100%; +} + .left-0 { left: 0px; } @@ -670,6 +674,10 @@ body { top: 0px; } +.top-full { + top: 100%; +} + .z-10 { z-index: 10; } @@ -687,6 +695,10 @@ body { margin-right: auto; } +.-mr-1 { + margin-right: -0.25rem; +} + .mb-1 { margin-bottom: 0.25rem; } @@ -707,6 +719,10 @@ body { margin-left: 0.25rem; } +.ml-2 { + margin-left: 0.5rem; +} + .mr-2 { margin-right: 0.5rem; } @@ -827,6 +843,10 @@ body { width: 1.25rem; } +.w-56 { + width: 14rem; +} + .w-6 { width: 1.5rem; } @@ -897,6 +917,18 @@ body { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } +.scale-100 { + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-95 { + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + .transform { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } @@ -1065,6 +1097,11 @@ body { border-color: hsl(var(--foreground) / var(--tw-border-opacity)); } +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); +} + .border-input { --tw-border-opacity: 1; border-color: hsl(var(--input) / var(--tw-border-opacity)); @@ -1126,6 +1163,11 @@ body { background-color: hsl(var(--secondary) / var(--tw-bg-opacity)); } +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -1185,6 +1227,11 @@ body { padding-right: 2rem; } +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; @@ -1404,6 +1451,21 @@ body { outline-style: solid; } +.ring-1 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-black { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity)); +} + +.ring-opacity-5 { + --tw-ring-opacity: 0.05; +} + .ring-offset-background { --tw-ring-offset-color: hsl(var(--background) / 1); } @@ -1460,6 +1522,10 @@ body { transition-duration: 300ms; } +.duration-75 { + transition-duration: 75ms; +} + .ease-in { transition-timing-function: cubic-bezier(0.4, 0, 1, 1); } @@ -1518,6 +1584,11 @@ body { background-color: rgb(243 244 246 / var(--tw-bg-opacity)); } +.hover\:bg-gray-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + .hover\:bg-muted:hover { --tw-bg-opacity: 1; background-color: hsl(var(--muted) / var(--tw-bg-opacity)); @@ -1546,6 +1617,11 @@ body { color: rgb(107 114 128 / var(--tw-text-opacity)); } +.hover\:text-gray-900:hover { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + .hover\:text-primary\/80:hover { color: hsl(var(--primary) / 0.8); } @@ -1578,6 +1654,10 @@ body { --tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity)); } +.focus\:ring-offset-2:focus { + --tw-ring-offset-width: 2px; +} + .focus-visible\:outline-none:focus-visible { outline: 2px solid transparent; outline-offset: 2px; @@ -1624,11 +1704,26 @@ body { opacity: 1; } +.dark\:border-gray-600:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); +} + +.dark\:bg-gray-800:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); +} + .dark\:text-gray-200:is(.dark *) { --tw-text-opacity: 1; color: rgb(229 231 235 / var(--tw-text-opacity)); } +.dark\:ring-gray-700:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity)); +} + .dark\:file\:text-foreground:is(.dark *)::file-selector-button { --tw-text-opacity: 1; color: hsl(var(--foreground) / var(--tw-text-opacity)); @@ -1639,6 +1734,16 @@ body { background-color: rgb(55 65 81 / var(--tw-bg-opacity)); } +.dark\:hover\:text-white:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.dark\:focus\:ring-indigo-400:focus:is(.dark *) { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(129 140 248 / var(--tw-ring-opacity)); +} + @media (min-width: 640px) { .sm\:my-8 { margin-top: 2rem;