From 7cd12e739492c25643e5a5e6eb2fb87a7c613339 Mon Sep 17 00:00:00 2001 From: Moira Daniella A Sebastian Date: Thu, 12 Feb 2026 16:15:34 +0100 Subject: [PATCH 01/65] Feature: Add top bar --- pkg/web/components/base/range_templ.go | 201 +--------- pkg/web/components/layout.templ | 75 +++- pkg/web/components/layout_templ.go | 530 +++++++++++++++++++++++-- pkg/web/routes/layout.templ | 4 + pkg/web/routes/layout_templ.go | 7 +- 5 files changed, 571 insertions(+), 246 deletions(-) diff --git a/pkg/web/components/base/range_templ.go b/pkg/web/components/base/range_templ.go index 47e5f26..aa54231 100644 --- a/pkg/web/components/base/range_templ.go +++ b/pkg/web/components/base/range_templ.go @@ -22,7 +22,6 @@ type RangeProps struct { Size string } -// Note: usage requires alpine.js func Range(props RangeProps) 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 @@ -51,7 +50,7 @@ func Range(props RangeProps) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("{ value: %d }", props.Value)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 22, Col: 53} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 21, Col: 53} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -91,7 +90,7 @@ func Range(props RangeProps) templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 32, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 31, Col: 18} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -124,7 +123,7 @@ func Range(props RangeProps) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(props.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 37, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 36, Col: 18} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -142,7 +141,7 @@ func Range(props RangeProps) templ.Component { var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(props.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 39, Col: 21} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 38, Col: 21} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -155,7 +154,7 @@ func Range(props RangeProps) templ.Component { var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", props.Min)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 40, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 39, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -168,7 +167,7 @@ func Range(props RangeProps) templ.Component { var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", props.Max)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 41, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 40, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -194,7 +193,7 @@ func Range(props RangeProps) templ.Component { var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", props.Step)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 50, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/base/range.templ`, Line: 49, Col: 40} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -208,190 +207,4 @@ func Range(props RangeProps) templ.Component { }) } -// Note: usage requires datastar.js -func DatastarRange(props RangeProps) 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_Var13 := templ.GetChildren(ctx) - if templ_7745c5c3_Var13 == nil { - templ_7745c5c3_Var13 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - var _ = templruntime.GeneratedTemplate diff --git a/pkg/web/components/layout.templ b/pkg/web/components/layout.templ index c094a05..10ca88f 100644 --- a/pkg/web/components/layout.templ +++ b/pkg/web/components/layout.templ @@ -6,16 +6,15 @@ templ Layout(props LayoutProps){ {props.Title} - + @Head(props.Head) - + @Header(props.Header) -
+
{ children... }
- @Footer(props.Footer) } @@ -54,17 +53,71 @@ type Script struct { } templ Header(props HeaderProps){ -
- + @Topbar(){ + @TopbarBrand(props.BrandName) + @TopbarCreateButton(props.CreateLabel, props.CreateHref) + @TopbarSpacer() + @TopbarSearch(props.SearchPlaceholder) + @TopbarSpacer() + @TopbarActions(){ + @TopbarIconButton("Notifications"){ @IconBell() } + @TopbarIconButton("User menu"){ @IconUser() } + } + } +} + +templ Topbar(){ +
+ { children... }
} +templ TopbarBrand(brandName string){ + { brandName } +} + +templ TopbarCreateButton(label string, href string){ + if href != "" { + { label } + } else { + + } +} + +templ TopbarSpacer(){ +
+} + +templ TopbarSearch(placeholder string){ + +} + +templ TopbarActions(){ +
+ { children... } +
+} + +templ TopbarIconButton(ariaLabel string){ + +} + +templ IconBell(){ + +} + +templ IconUser(){ + +} + type HeaderProps struct { - Routes []NavRoute + BrandName string + CreateLabel string + CreateHref string + SearchPlaceholder string + Routes []NavRoute } type NavRoute struct { diff --git a/pkg/web/components/layout_templ.go b/pkg/web/components/layout_templ.go index 3b5aa99..bcddf7e 100644 --- a/pkg/web/components/layout_templ.go +++ b/pkg/web/components/layout_templ.go @@ -42,7 +42,7 @@ func Layout(props LayoutProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -71,7 +71,7 @@ func Layout(props LayoutProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -79,15 +79,7 @@ func Layout(props LayoutProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = Footer(props.Footer).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -125,65 +117,65 @@ func Head(props HeadProps) templ.Component { } ctx = templ.ClearChildren(ctx) for _, link := range props.Links { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } for _, script := range props.Scripts { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -228,43 +220,497 @@ func Header(props HeaderProps) templ.Component { templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func TopbarBrand(brandName string) 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_Var15 := templ.GetChildren(ctx) + if templ_7745c5c3_Var15 == nil { + templ_7745c5c3_Var15 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(brandName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/layout.templ`, Line: 76, Col: 80} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func TopbarCreateButton(label string, href string) 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_Var17 := templ.GetChildren(ctx) + if templ_7745c5c3_Var17 == nil { + templ_7745c5c3_Var17 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + if href != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" style=\"padding:6px 12px;font-size:13px;background:#fff;color:#0969da;border:none;border-radius:4px;cursor:pointer;text-decoration:none;display:inline-block;\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(route.Name) + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(label) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/layout.templ`, Line: 60, Col: 51} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/layout.templ`, Line: 81, Col: 190} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + return nil + }) +} + +func TopbarSpacer() 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_Var21 := templ.GetChildren(ctx) + if templ_7745c5c3_Var21 == nil { + templ_7745c5c3_Var21 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func TopbarSearch(placeholder string) 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_Var22 := templ.GetChildren(ctx) + if templ_7745c5c3_Var22 == nil { + templ_7745c5c3_Var22 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func TopbarActions() 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_Var24 := templ.GetChildren(ctx) + if templ_7745c5c3_Var24 == nil { + templ_7745c5c3_Var24 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ_7745c5c3_Var24.Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func TopbarIconButton(ariaLabel string) 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_Var25 := templ.GetChildren(ctx) + if templ_7745c5c3_Var25 == nil { + templ_7745c5c3_Var25 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func IconBell() 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_Var27 := templ.GetChildren(ctx) + if templ_7745c5c3_Var27 == nil { + templ_7745c5c3_Var27 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func IconUser() 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_Var28 := templ.GetChildren(ctx) + if templ_7745c5c3_Var28 == nil { + templ_7745c5c3_Var28 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -273,7 +719,11 @@ func Header(props HeaderProps) templ.Component { } type HeaderProps struct { - Routes []NavRoute + BrandName string + CreateLabel string + CreateHref string + SearchPlaceholder string + Routes []NavRoute } type NavRoute struct { @@ -297,9 +747,9 @@ func Footer(props FooterProps) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var12 := templ.GetChildren(ctx) - if templ_7745c5c3_Var12 == nil { - templ_7745c5c3_Var12 = templ.NopComponent + templ_7745c5c3_Var29 := templ.GetChildren(ctx) + if templ_7745c5c3_Var29 == nil { + templ_7745c5c3_Var29 = templ.NopComponent } ctx = templ.ClearChildren(ctx) return nil diff --git a/pkg/web/routes/layout.templ b/pkg/web/routes/layout.templ index 4aa4d3d..ecf784b 100644 --- a/pkg/web/routes/layout.templ +++ b/pkg/web/routes/layout.templ @@ -16,6 +16,10 @@ var baseLayout = components.LayoutProps{ }, }, Header: components.HeaderProps{ + BrandName: "LazyPM Dashboard", + CreateLabel: "+ Create", + CreateHref: "", + SearchPlaceholder: "Search", }, Footer: components.FooterProps{ }, diff --git a/pkg/web/routes/layout_templ.go b/pkg/web/routes/layout_templ.go index 3440109..e082625 100644 --- a/pkg/web/routes/layout_templ.go +++ b/pkg/web/routes/layout_templ.go @@ -23,7 +23,12 @@ var baseLayout = components.LayoutProps{ {Src: "/assets/js/ajax.min.js", Defer: true}, }, }, - Header: components.HeaderProps{}, + Header: components.HeaderProps{ + BrandName: "LazyPM Dashboard", + CreateLabel: "+ Create", + CreateHref: "", + SearchPlaceholder: "Search", + }, Footer: components.FooterProps{}, } From 54c0845f2848a92cb1eb686fdfc015f3f22dea9f Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 13 Feb 2026 13:38:54 +0100 Subject: [PATCH 02/65] handles routes in routes --- pkg/web/handler/handler.go | 21 ------------------ pkg/web/handler/pages.go | 44 -------------------------------------- 2 files changed, 65 deletions(-) delete mode 100644 pkg/web/handler/handler.go delete mode 100644 pkg/web/handler/pages.go diff --git a/pkg/web/handler/handler.go b/pkg/web/handler/handler.go deleted file mode 100644 index 597333c..0000000 --- a/pkg/web/handler/handler.go +++ /dev/null @@ -1,21 +0,0 @@ -package handler - -import ( - "net/http" - - "github.com/LazyBachelor/LazyPM/internal/service" -) - -type Route struct { - Pattern string - Handler http.Handler -} - -func GetRoutes(svc *service.Services) []Route { - var routes []Route - - routes = append(routes, PagesRoutes(svc)...) - routes = append(routes, IssuesRoutes(svc)...) - - return routes -} diff --git a/pkg/web/handler/pages.go b/pkg/web/handler/pages.go deleted file mode 100644 index 82c6c63..0000000 --- a/pkg/web/handler/pages.go +++ /dev/null @@ -1,44 +0,0 @@ -package handler - -import ( - "net/http" - - "github.com/LazyBachelor/LazyPM/internal/service" - "github.com/LazyBachelor/LazyPM/pkg/web/components" - "github.com/LazyBachelor/LazyPM/pkg/web/routes" -) - -func PagesRoutes(svc *service.Services) []Route { - return []Route{ - {Pattern: "/", Handler: IndexHandler(svc)}, - } -} - -func IndexHandler(svc *service.Services) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - if r.URL.Path != "/" { - handleNotFound(w, r) - return - } - - issues, err := svc.Beads.AllIssues(r.Context()) - - if err != nil { - http.Error(w, "Failed to retrieve issues", - http.StatusInternalServerError) - return - } - - props := routes.IndexProps{ - IssueTable: components.IssueTableProps{ - Issues: issues, - }, - } - routes.Index(props).Render(r.Context(), w) - } -} - -func handleNotFound(w http.ResponseWriter, _ *http.Request) { - http.Error(w, "Page not found", http.StatusNotFound) -} From f13d361a7c049f332c33e4634998a8024f163496 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 13 Feb 2026 13:39:11 +0100 Subject: [PATCH 03/65] use chi as router --- pkg/web/server/routes.go | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/pkg/web/server/routes.go b/pkg/web/server/routes.go index 0911f81..a3f43b3 100644 --- a/pkg/web/server/routes.go +++ b/pkg/web/server/routes.go @@ -8,29 +8,45 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/web/handler" "github.com/NYTimes/gziphandler" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" "github.com/rs/cors" ) func (s *Server) RegisterRoutes(assets embed.FS) http.Handler { - mux := http.NewServeMux() + r := chi.NewRouter() - s.handleAssets(mux, assets) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(cors.AllowAll().Handler) + r.Use(middleware.CleanPath) - for _, route := range handler.GetRoutes(s.Services) { - mux.Handle(route.Pattern, route.Handler) - } + r.Use(handler.HTMXMiddleware) + r.Use(handler.ServicesMiddleware(s.Services)) - handler := cors.Default().Handler(mux) + s.handleAssets(r, assets) - return gziphandler.GzipHandler(handler) + r.Get("/", handler.IndexHandler) + + r.Route("/issues", func(r chi.Router) { + r.Get("/", handler.ListIssues) + r.Post("/", handler.CreateIssue) + + r.Route("/{id}", func(r chi.Router) { + r.Use(handler.IssueCtx) + r.Get("/", handler.GetIssue) + r.Put("/", handler.UpdateIssue) + r.Delete("/", handler.DeleteIssue) + }) + }) + return gziphandler.GzipHandler(r) } -func (s *Server) handleAssets(mux *http.ServeMux, assets embed.FS) { - mux.Handle("/assets/", - http.StripPrefix("/assets/", - http.FileServer(http.Dir("pkg/web/assets")))) +func (s *Server) handleAssets(r chi.Router, assets embed.FS) { + r.Handle("/assets/*", http.StripPrefix("/assets/", + http.FileServer(http.Dir("pkg/web/assets")))) - mux.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) { + r.HandleFunc("GET /robots.txt", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache") http.ServeContent(w, r, "robots.txt", time.Now(), strings.NewReader("User-agent: *\nAllow: /")) }) From b4fb4ad0666e6758ca0c74059f3bdf7bc60527df Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 13 Feb 2026 13:39:32 +0100 Subject: [PATCH 04/65] add services and htmx middleware --- pkg/web/handler/context.go | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 pkg/web/handler/context.go diff --git a/pkg/web/handler/context.go b/pkg/web/handler/context.go new file mode 100644 index 0000000..bdb1b51 --- /dev/null +++ b/pkg/web/handler/context.go @@ -0,0 +1,42 @@ +package handler + +import ( + "context" + "net/http" + + "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/donseba/go-htmx" +) + +type contextKey string + +const ( + servicesKey contextKey = "services" + htmxKey contextKey = "htmx" +) + +func ServicesMiddleware(svc *service.Services) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), servicesKey, svc) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +func HTMXMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + htmxInstance := htmx.New() + handler := htmxInstance.NewHandler(w, r) + ctx := context.WithValue(r.Context(), htmxKey, handler) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func Services(r *http.Request) *service.Services { + return r.Context().Value(servicesKey).(*service.Services) +} + +func HTMX(r *http.Request) *htmx.Handler { + return r.Context().Value(htmxKey).(*htmx.Handler) +} From 8728b321abac752fb5f0b058ebf25f61c520b5b1 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 13 Feb 2026 13:40:00 +0100 Subject: [PATCH 05/65] rename to index and use new middlewares and chi --- pkg/web/handler/index.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 pkg/web/handler/index.go diff --git a/pkg/web/handler/index.go b/pkg/web/handler/index.go new file mode 100644 index 0000000..6cec171 --- /dev/null +++ b/pkg/web/handler/index.go @@ -0,0 +1,26 @@ +package handler + +import ( + "net/http" + + "github.com/LazyBachelor/LazyPM/pkg/web/components" + "github.com/LazyBachelor/LazyPM/pkg/web/routes" +) + +func IndexHandler(w http.ResponseWriter, r *http.Request) { + svc := Services(r) + + issues, err := svc.Beads.AllIssues(r.Context()) + if err != nil { + http.Error(w, "failed to retrieve issues", http.StatusInternalServerError) + return + } + + props := routes.IndexProps{ + IssueTable: components.IssueTableProps{ + Issues: issues, + }, + } + + routes.Index(props).Render(r.Context(), w) +} From 029b9cbf69c232bb463807fe1a3200901144e6bc Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 13 Feb 2026 13:40:16 +0100 Subject: [PATCH 06/65] make generic form parser --- pkg/web/handler/forms.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 pkg/web/handler/forms.go diff --git a/pkg/web/handler/forms.go b/pkg/web/handler/forms.go new file mode 100644 index 0000000..960464a --- /dev/null +++ b/pkg/web/handler/forms.go @@ -0,0 +1,30 @@ +package handler + +import ( + "net/http" + + "github.com/go-playground/form/v4" + "github.com/go-playground/validator/v10" +) + +var ( + decoder = form.NewDecoder() + validate = validator.New(validator.WithRequiredStructEnabled()) +) + +func ParseForm[T any](r *http.Request) (*T, error) { + if err := r.ParseForm(); err != nil { + return nil, err + } + + var data T + if err := decoder.Decode(&data, r.PostForm); err != nil { + return nil, err + } + + return &data, nil +} + +func ValidateForm[T any](data *T) error { + return validate.Struct(data) +} From 912bbdbacdcadb0244600be2f5ab0e74d814b23b Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 13 Feb 2026 13:40:30 +0100 Subject: [PATCH 07/65] add issues endpoints --- pkg/web/handler/issues.go | 170 ++++++++++++++++++++++++++------------ 1 file changed, 116 insertions(+), 54 deletions(-) diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index bbe2fe0..a9bb23a 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -1,72 +1,134 @@ package handler import ( - "encoding/json" - "fmt" + "context" "net/http" "github.com/LazyBachelor/LazyPM/internal/models" - "github.com/LazyBachelor/LazyPM/internal/service" + "github.com/go-chi/chi/v5" ) -func IssuesRoutes(svc *service.Services) []Route { - return []Route{ - {Pattern: "/issues", Handler: GetAllIssues(svc)}, - {Pattern: "POST /create-issue", Handler: CreateIssue(svc)}, +type IssueForm struct { + Title string `form:"title" validate:"required,max=255"` + Description string `form:"description" validate:"required,max=2000"` + Status models.Status `form:"status" validate:"required,oneof=open in_progress closed"` + IssueType models.IssueType `form:"issue_type" validate:"required,oneof=task bug feature chore"` + Priority int `form:"priority" validate:"gte=0,lte=4"` +} + +func (f *IssueForm) ToIssue() models.Issue { + return models.Issue{ + Title: f.Title, + Description: f.Description, + Status: f.Status, + IssueType: f.IssueType, + Priority: f.Priority, } } -func CreateIssue(svc *service.Services) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } +func CreateIssue(w http.ResponseWriter, r *http.Request) { + svc := Services(r) + hx := HTMX(r) - // 1. Parse Form instead of JSON - if err := r.ParseForm(); err != nil { - http.Error(w, "Failed to parse form", http.StatusBadRequest) - return - } - - // 2. Map form values to your struct manually - // (Or use a library like 'gorilla/schema') - issue := models.Issue{ - Title: r.FormValue("title"), - Description: r.FormValue("description"), - Status: models.Status(r.FormValue("status")), - IssueType: models.IssueType(r.FormValue("issue_type")), - } - - err := svc.Beads.CreateIssue(r.Context(), &issue, "") - if err != nil { - http.Error(w, "Failed to create issue: "+err.Error(), http.StatusInternalServerError) - return - } - - // 3. HTMX usually expects HTML back, not JSON - w.Header().Set("Content-Type", "text/html") - fmt.Fprintf(w, "

Created issue: %s

", issue.Title) + form, err := ParseForm[IssueForm](r) + if err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return } + + if err := ValidateForm(form); err != nil { + w.WriteHeader(http.StatusUnprocessableEntity) + if hx.IsHxRequest() { + hx.WriteString("
Please fix the form errors
") + } else { + hx.WriteJSON(map[string]interface{}{"error": err.Error()}) + } + return + } + + issue := form.ToIssue() + if err := svc.Beads.CreateIssue(r.Context(), &issue, ""); err != nil { + http.Error(w, "Failed to create issue: "+err.Error(), http.StatusInternalServerError) + return + } + + if hx.IsHxRequest() { + hx.WriteString("
Issue created successfully
") + return + } + + hx.WriteJSON(map[string]any{ + "title": issue.Title, + "status": issue.Status, + }) } -func GetAllIssues(svc *service.Services) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - issues, err := svc.Beads.AllIssues(r.Context()) +func ListIssues(w http.ResponseWriter, r *http.Request) { + svc := Services(r) + hx := HTMX(r) - if err != nil { - http.Error(w, "Failed to retrieve issues", http.StatusInternalServerError) - return - } - - jsonData, err := json.Marshal(issues) - - if err != nil { - http.Error(w, "Failed to marshal issues", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(jsonData) + issues, err := svc.Beads.AllIssues(r.Context()) + if err != nil { + http.Error(w, "Failed to retrieve issues", http.StatusInternalServerError) + return } + + hx.WriteJSON(issues) +} + +func IssueCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + svc := Services(r) + + id := chi.URLParam(r, "id") + issue, err := svc.Beads.GetIssue(r.Context(), id) + if err != nil { + http.Error(w, "Issue not found", http.StatusNotFound) + return + } + + ctx := context.WithValue(r.Context(), "issue", issue) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + +} + +func GetIssue(w http.ResponseWriter, r *http.Request) { + issue := r.Context().Value("issue").(*models.Issue) + hx := HTMX(r) + + hx.WriteJSON(issue) +} + +func UpdateIssue(w http.ResponseWriter, r *http.Request) { + issue := r.Context().Value("issue").(*models.Issue) + svc := Services(r) + hx := HTMX(r) + + changes := make(map[string]any) + + if err := svc.Beads.UpdateIssue(r.Context(), issue.ID, changes, ""); err != nil { + http.Error(w, "Failed to update issue", http.StatusInternalServerError) + return + } + + issue, err := svc.Beads.GetIssue(r.Context(), issue.ID) + if err != nil { + http.Error(w, "Failed to retrieve updated issue", http.StatusInternalServerError) + return + } + + hx.WriteJSON(issue) +} + +func DeleteIssue(w http.ResponseWriter, r *http.Request) { + issue := r.Context().Value("issue").(*models.Issue) + + svc := Services(r) + if err := svc.Beads.DeleteIssue(r.Context(), issue.ID); err != nil { + http.Error(w, "Failed to delete issue", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) } From cc1f84a4bda02cbafbf93b78d849aaf8bb846643 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 13 Feb 2026 13:40:41 +0100 Subject: [PATCH 08/65] uprate deps --- go.mod | 9 +++++++++ go.sum | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/go.mod b/go.mod index 521c31d..40e1777 100644 --- a/go.mod +++ b/go.mod @@ -47,13 +47,21 @@ require ( github.com/cli/browser v1.3.0 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect github.com/clipperhouse/uax29/v2 v2.6.0 // indirect + github.com/donseba/go-htmx v1.12.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/go-chi/chi/v5 v5.2.5 // indirect + github.com/go-playground/form/v4 v4.3.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -84,6 +92,7 @@ require ( github.com/tetratelabs/wazero v1.11.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20260209203927-2842357ff358 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.50.0 // indirect diff --git a/go.sum b/go.sum index d774a58..021bd89 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/donseba/go-htmx v1.12.1 h1:ZO9TWLyZYN3KL2s/N3ZasCf/B3dmX1xzmZoIkJIT+C0= +github.com/donseba/go-htmx v1.12.1/go.mod h1:8PTAYvNKf8+QYis+DpAsggKz+sa2qljtMgvdAeNBh5s= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -80,6 +82,18 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-playground/form/v4 v4.3.0 h1:OVttojbQv2WNCs4P+VnjPtrt/+30Ipw4890W3OaFlvk= +github.com/go-playground/form/v4 v4.3.0/go.mod h1:Cpe1iYJKoXb1vILRXEwxpWMGWyQuqplQ/4cvPecy+Jo= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -96,6 +110,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -187,6 +203,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c= golang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= From aaf8eacf3cd15233386fefa8333d158b37413d50 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Fri, 13 Feb 2026 13:41:24 +0100 Subject: [PATCH 09/65] add some margins to the index page resize form --- pkg/web/assets/css/styles.css | 1238 ++++++++++++++++++++++++ pkg/web/components/base/input.templ | 3 +- pkg/web/components/base/input_templ.go | 27 +- pkg/web/components/base/range.templ | 2 +- pkg/web/components/base/range_templ.go | 203 +--- pkg/web/components/issue.templ | 5 +- pkg/web/components/issue_templ.go | 15 +- pkg/web/routes/index.templ | 12 +- pkg/web/routes/index_templ.go | 12 +- 9 files changed, 1286 insertions(+), 231 deletions(-) diff --git a/pkg/web/assets/css/styles.css b/pkg/web/assets/css/styles.css index f70a7c6..ccc6fa8 100644 --- a/pkg/web/assets/css/styles.css +++ b/pkg/web/assets/css/styles.css @@ -7,12 +7,31 @@ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --color-green-100: oklch(96.2% 0.044 156.743); + --color-green-800: oklch(44.8% 0.119 151.328); + --color-teal-700: oklch(51.1% 0.096 186.391); + --color-gray-500: oklch(55.1% 0.027 264.364); + --color-gray-600: oklch(44.6% 0.03 256.802); + --color-gray-800: oklch(27.8% 0.033 256.848); --color-black: #000; + --color-white: #fff; --spacing: 0.25rem; + --container-xs: 20rem; + --container-lg: 32rem; + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); --text-2xl: 1.5rem; --text-2xl--line-height: calc(2 / 1.5); --text-3xl: 1.875rem; --text-3xl--line-height: calc(2.25 / 1.875); + --font-weight-semibold: 600; + --font-weight-bold: 700; --ease-out: cubic-bezier(0, 0, 0.2, 1); --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); --default-transition-duration: 150ms; @@ -170,6 +189,305 @@ } } @layer utilities { + .diff { + @layer daisyui.l1.l2.l3 { + position: relative; + display: grid; + width: 100%; + overflow: hidden; + webkit-user-select: none; + user-select: none; + grid-template-rows: 1fr 1.8rem 1fr; + direction: ltr; + container-type: inline-size; + grid-template-columns: auto 1fr; + &:focus-visible, &:has(.diff-item-1:focus-visible) { + outline-style: var(--tw-outline-style); + outline-width: 2px; + outline-offset: 1px; + outline-color: var(--color-base-content); + } + &:focus-visible { + outline-style: var(--tw-outline-style); + outline-width: 2px; + outline-offset: 1px; + outline-color: var(--color-base-content); + .diff-resizer { + min-width: 95cqi; + max-width: 95cqi; + } + } + &:has(.diff-item-1:focus-visible) { + outline-style: var(--tw-outline-style); + outline-width: 2px; + outline-offset: 1px; + .diff-resizer { + min-width: 5cqi; + max-width: 5cqi; + } + } + @supports (-webkit-overflow-scrolling: touch) and (overflow: -webkit-paged-x) { + &:focus { + .diff-resizer { + min-width: 5cqi; + max-width: 5cqi; + } + } + &:has(.diff-item-1:focus) { + .diff-resizer { + min-width: 95cqi; + max-width: 95cqi; + } + } + } + } + } + .fab { + @layer daisyui.l1.l2.l3 { + pointer-events: none; + position: fixed; + inset-inline-end: calc(0.25rem * 4); + bottom: calc(0.25rem * 4); + z-index: 999; + display: flex; + flex-direction: column-reverse; + align-items: flex-end; + gap: calc(0.25rem * 2); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + white-space: nowrap; + > * { + pointer-events: auto; + display: flex; + align-items: center; + gap: calc(0.25rem * 2); + &:hover, &:has(:focus-visible) { + z-index: 1; + } + } + > [tabindex] { + &:first-child { + position: relative; + display: grid; + transition-property: opacity, visibility, rotate; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + } + } + .fab-close { + position: absolute; + inset-inline-end: calc(0.25rem * 0); + bottom: calc(0.25rem * 0); + } + .fab-main-action { + position: absolute; + inset-inline-end: calc(0.25rem * 0); + bottom: calc(0.25rem * 0); + } + &:focus-within { + &:has(.fab-close), &:has(.fab-main-action) { + > [tabindex] { + rotate: 90deg; + opacity: 0%; + } + } + > [tabindex]:first-child { + pointer-events: none; + } + > :nth-child(n + 2) { + visibility: visible; + --tw-scale-x: 100%; + --tw-scale-y: 100%; + --tw-scale-z: 100%; + scale: var(--tw-scale-x) var(--tw-scale-y); + opacity: 100%; + } + } + > :nth-child(n + 2) { + visibility: hidden; + --tw-scale-x: 80%; + --tw-scale-y: 80%; + --tw-scale-z: 80%; + scale: var(--tw-scale-x) var(--tw-scale-y); + opacity: 0%; + transition-property: opacity, scale, visibility; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + &.fab-main-action, &.fab-close { + --tw-scale-x: 100%; + --tw-scale-y: 100%; + --tw-scale-z: 100%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + } + > :nth-child(3) { + transition-delay: 30ms; + } + > :nth-child(4) { + transition-delay: 60ms; + } + > :nth-child(5) { + transition-delay: 90ms; + } + > :nth-child(6) { + transition-delay: 120ms; + } + } + } + .tooltip { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-block; + --tt-bg: var(--color-neutral); + --tt-off: calc(100% + 0.5rem); + --tt-tail: calc(100% + 1px + 0.25rem); + & > .tooltip-content, &[data-tip]:before { + position: absolute; + max-width: 20rem; + border-radius: var(--radius-field); + padding-inline: calc(0.25rem * 2); + padding-block: calc(0.25rem * 1); + text-align: center; + white-space: normal; + color: var(--color-neutral-content); + opacity: 0%; + font-size: 0.875rem; + line-height: 1.25; + background-color: var(--tt-bg); + width: max-content; + pointer-events: none; + z-index: 2; + --tw-content: attr(data-tip); + content: var(--tw-content); + } + &:after { + opacity: 0%; + background-color: var(--tt-bg); + content: ""; + pointer-events: none; + width: 0.625rem; + height: 0.25rem; + display: block; + position: absolute; + mask-repeat: no-repeat; + mask-position: -1px 0; + --mask-tooltip: url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A"); + mask-image: var(--mask-tooltip); + } + @media (prefers-reduced-motion: no-preference) { + & > .tooltip-content, &[data-tip]:before, &:after { + transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 75ms; + } + } + &:is([data-tip]:not([data-tip=""]), :has(.tooltip-content:not(:empty))) { + &.tooltip-open, &:hover, &:has(:focus-visible) { + & > .tooltip-content, &[data-tip]:before, &:after { + opacity: 100%; + --tt-pos: 0rem; + @media (prefers-reduced-motion: no-preference) { + transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s, transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) 0s; + } + } + } + } + } + @layer daisyui.l1.l2 { + > .tooltip-content, &[data-tip]:before { + transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem)); + inset: auto auto var(--tt-off) 50%; + } + &:after { + transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem)); + inset: auto auto var(--tt-tail) 50%; + } + } + } + .tab { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-flex; + cursor: pointer; + appearance: none; + flex-wrap: wrap; + align-items: center; + justify-content: center; + text-align: center; + webkit-user-select: none; + user-select: none; + &:hover { + @media (hover: hover) { + color: var(--color-base-content); + } + } + --tab-p: 0.75rem; + --tab-bg: var(--color-base-100); + --tab-border-color: var(--color-base-300); + --tab-radius-ss: 0; + --tab-radius-se: 0; + --tab-radius-es: 0; + --tab-radius-ee: 0; + --tab-order: 0; + --tab-radius-min: calc(0.75rem - var(--border)); + --tab-radius-limit: min(var(--radius-field), var(--tab-radius-min)); + --tab-radius-grad: #0000 calc(69% - var(--border)), + var(--tab-border-color) calc(69% - var(--border) + 0.25px), + var(--tab-border-color) 69%, + var(--tab-bg) calc(69% + 0.25px); + border-color: #0000; + order: var(--tab-order); + height: var(--tab-height); + font-size: 0.875rem; + padding-inline: var(--tab-p); + &:is(input[type="radio"]) { + min-width: fit-content; + &:after { + --tw-content: attr(aria-label); + content: var(--tw-content); + } + } + &:is(label) { + position: relative; + input { + position: absolute; + inset: calc(0.25rem * 0); + cursor: pointer; + appearance: none; + opacity: 0%; + } + } + &:checked, &:is(label:has(:checked)), &:is(.tab-active, [aria-selected="true"], [aria-current="true"], [aria-current="page"]) { + & + .tab-content { + display: block; + } + } + &:not( :checked, label:has(:checked), :hover, .tab-active, [aria-selected="true"], [aria-current="true"], [aria-current="page"] ) { + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 50%, transparent); + } + } + &:not(input):empty { + flex-grow: 1; + cursor: default; + } + &:focus { + --tw-outline-style: none; + outline-style: none; + @media (forced-colors: active) { + outline: 2px solid transparent; + outline-offset: 2px; + } + } + &:focus-visible, &:is(label:has(:checked:focus-visible)) { + outline: 2px solid currentColor; + outline-offset: -5px; + } + &[disabled] { + pointer-events: none; + opacity: 40%; + } + } + } .menu { @layer daisyui.l1.l2.l3 { display: flex; @@ -610,6 +928,20 @@ } } } + .loading { + @layer daisyui.l1.l2.l3 { + pointer-events: none; + display: inline-block; + aspect-ratio: 1 / 1; + background-color: currentcolor; + vertical-align: middle; + width: calc(var(--size-selector, 0.25rem) * 6); + mask-size: 100%; + mask-repeat: no-repeat; + mask-position: center; + mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E"); + } + } .collapse { &:not(td, tr, colgroup) { visibility: revert-layer; @@ -776,6 +1108,36 @@ } } } + .validator-hint { + @layer daisyui.l1.l2.l3 { + visibility: hidden; + margin-top: calc(0.25rem * 2); + font-size: 0.75rem; + } + } + .validator { + @layer daisyui.l1.l2.l3 { + &:user-valid, &:has(:user-valid) { + &, &:focus, &:checked, &[aria-checked="true"], &:focus-within { + --input-color: var(--color-success); + } + } + &:user-invalid, &:has(:user-invalid), &[aria-invalid]:not([aria-invalid="false"]), &:has([aria-invalid]:not([aria-invalid="false"])) { + &, &:focus, &:checked, &[aria-checked="true"], &:focus-within { + --input-color: var(--color-error); + } + & ~ .validator-hint { + visibility: visible; + color: var(--color-error); + } + } + } + &:user-invalid, &:has(:user-invalid), &[aria-invalid]:not([aria-invalid="false"]), &:has([aria-invalid]:not([aria-invalid="false"])) { + & ~ .validator-hint { + display: revert-layer; + } + } + } .collapse-open { @layer daisyui.l1.l2 { grid-template-rows: max-content 1fr; @@ -795,6 +1157,65 @@ .visible { visibility: visible; } + .list { + @layer daisyui.l1.l2.l3 { + display: flex; + flex-direction: column; + font-size: 0.875rem; + .list-row { + --list-grid-cols: minmax(0, auto) 1fr; + position: relative; + display: grid; + grid-auto-flow: column; + gap: calc(0.25rem * 4); + border-radius: var(--radius-box); + padding: calc(0.25rem * 4); + word-break: break-word; + grid-template-columns: var(--list-grid-cols); + } + & > :not(:last-child) { + &.list-row, .list-row { + &:after { + content: ""; + border-bottom: var(--border) solid; + inset-inline: var(--radius-box); + position: absolute; + bottom: calc(0.25rem * 0); + border-color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-color: color-mix(in oklab, var(--color-base-content) 5%, transparent); + } + } + } + } + } + @layer daisyui.l1.l2 { + .list-row { + &:has(.list-col-grow:nth-child(1)) { + --list-grid-cols: 1fr; + } + &:has(.list-col-grow:nth-child(2)) { + --list-grid-cols: minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(3)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(4)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(5)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) 1fr; + } + &:has(.list-col-grow:nth-child(6)) { + --list-grid-cols: minmax(0, auto) minmax(0, auto) minmax(0, auto) minmax(0, auto) + minmax(0, auto) 1fr; + } + > * { + grid-row-start: 1; + } + } + } + } .toast { @layer daisyui.l1.l2.l3 { position: fixed; @@ -938,6 +1359,23 @@ } } } + .indicator { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-flex; + width: max-content; + :where(.indicator-item) { + z-index: 1; + position: absolute; + white-space: nowrap; + top: var(--indicator-t, 0); + bottom: var(--indicator-b, auto); + left: var(--indicator-s, auto); + right: var(--indicator-e, 0); + translate: var(--indicator-x, 50%) var(--indicator-y, -50%); + } + } + } .table { @layer daisyui.l1.l2.l3 { font-size: 0.875rem; @@ -1102,6 +1540,27 @@ } } } + .diff-resizer { + @layer daisyui.l1.l2.l3 { + position: relative; + isolation: isolate; + z-index: 2; + grid-column-start: 1; + grid-row-start: 2; + height: calc(0.25rem * 3); + width: 50cqi; + max-width: calc(100cqi - 1rem); + min-width: 1rem; + resize: horizontal; + overflow: hidden; + opacity: 0%; + transform: scaleY(5) translate(0.32rem, 50%); + cursor: ew-resize; + transform-origin: 100% 100%; + clip-path: inset(calc(100% - 0.75rem) 0 0 calc(100% - 0.75rem)); + transition: min-width 0.3s ease-out, max-width 0.3s ease-out; + } + } .select { @layer daisyui.l1.l2.l3 { border: var(--border) solid #0000; @@ -1539,6 +1998,15 @@ } } } + .stats { + @layer daisyui.l1.l2.l3 { + position: relative; + display: inline-grid; + grid-auto-flow: column; + overflow-x: auto; + border-radius: var(--radius-box); + } + } .progress { @layer daisyui.l1.l2.l3 { position: relative; @@ -1589,9 +2057,97 @@ } } } + .absolute { + position: absolute; + } + .relative { + position: relative; + } .static { position: static; } + .tooltip-top { + @layer daisyui.l1.l2 { + > .tooltip-content, &[data-tip]:before { + transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem)); + inset: auto auto var(--tt-off) 50%; + } + &:after { + transform: translateX(-50%) translateY(var(--tt-pos, 0.25rem)); + inset: auto auto var(--tt-tail) 50%; + } + } + } + .join { + display: inline-flex; + align-items: stretch; + --join-ss: 0; + --join-se: 0; + --join-es: 0; + --join-ee: 0; + :where(.join-item) { + border-start-start-radius: var(--join-ss, 0); + border-start-end-radius: var(--join-se, 0); + border-end-start-radius: var(--join-es, 0); + border-end-end-radius: var(--join-ee, 0); + * { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + } + > .join-item:where(:first-child) { + --join-ss: var(--radius-field); + --join-se: 0; + --join-es: var(--radius-field); + --join-ee: 0; + } + :first-child:not(:last-child) { + :where(.join-item) { + --join-ss: var(--radius-field); + --join-se: 0; + --join-es: var(--radius-field); + --join-ee: 0; + } + } + > .join-item:where(:last-child) { + --join-ss: 0; + --join-se: var(--radius-field); + --join-es: 0; + --join-ee: var(--radius-field); + } + :last-child:not(:first-child) { + :where(.join-item) { + --join-ss: 0; + --join-se: var(--radius-field); + --join-es: 0; + --join-ee: var(--radius-field); + } + } + > .join-item:where(:only-child) { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + :only-child { + :where(.join-item) { + --join-ss: var(--radius-field); + --join-se: var(--radius-field); + --join-es: var(--radius-field); + --join-ee: var(--radius-field); + } + } + > :where(:focus, :has(:focus)) { + z-index: 1; + } + @media (hover: hover) { + > :where(.btn:hover, :has(.btn:hover)) { + isolation: isolate; + } + } + } .textarea { @layer daisyui.l1.l2.l3 { border: var(--border) solid #0000; @@ -1667,6 +2223,110 @@ } } } + .stack { + @layer daisyui.l1.l2.l3 { + display: inline-grid; + grid-template-columns: 3px 4px 1fr 4px 3px; + grid-template-rows: 3px 4px 1fr 4px 3px; + & > * { + height: 100%; + width: 100%; + &:nth-child(n + 2) { + width: 100%; + opacity: 70%; + } + &:nth-child(2) { + z-index: 2; + opacity: 90%; + } + &:nth-child(1) { + z-index: 3; + width: 100%; + } + } + } + @layer daisyui.l1.l2 { + &, &.stack-bottom { + > * { + grid-column: 3 / 4; + grid-row: 3 / 6; + &:nth-child(2) { + grid-column: 2 / 5; + grid-row: 2 / 5; + } + &:nth-child(1) { + grid-column: 1 / 6; + grid-row: 1 / 4; + } + } + } + &.stack-top { + > * { + grid-column: 3 / 4; + grid-row: 1 / 4; + &:nth-child(2) { + grid-column: 2 / 5; + grid-row: 2 / 5; + } + &:nth-child(1) { + grid-column: 1 / 6; + grid-row: 3 / 6; + } + } + } + &.stack-start { + > * { + grid-column: 1 / 4; + grid-row: 3 / 4; + &:nth-child(2) { + grid-column: 2 / 5; + grid-row: 2 / 5; + } + &:nth-child(1) { + grid-column: 3 / 6; + grid-row: 1 / 6; + } + } + } + &.stack-end { + > * { + grid-column: 3 / 6; + grid-row: 3 / 4; + &:nth-child(2) { + grid-column: 2 / 5; + grid-row: 2 / 5; + } + &:nth-child(1) { + grid-column: 1 / 4; + grid-row: 1 / 6; + } + } + } + } + } + .tab-content { + @layer daisyui.l1.l2.l3 { + order: var(--tabcontent-order); + display: none; + border-color: transparent; + --tabcontent-radius-ss: var(--radius-box); + --tabcontent-radius-se: var(--radius-box); + --tabcontent-radius-es: var(--radius-box); + --tabcontent-radius-ee: var(--radius-box); + --tabcontent-order: 1; + width: 100%; + height: calc(100% - var(--tab-height) + var(--border)); + margin: var(--tabcontent-margin); + border-width: var(--border); + border-start-start-radius: var(--tabcontent-radius-ss); + border-start-end-radius: var(--tabcontent-radius-se); + border-end-start-radius: var(--tabcontent-radius-es); + border-end-end-radius: var(--tabcontent-radius-ee); + } + } + .m-5 { + margin: calc(var(--spacing) * 5); + } .filter { @layer daisyui.l1.l2.l3 { display: flex; @@ -1712,6 +2372,126 @@ } } } + .mx-auto { + margin-inline: auto; + } + .input-lg { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 12); + font-size: max(var(--font-size, 1.125rem), 1.125rem); + &[type="number"] { + &::-webkit-inner-spin-button { + margin-block: calc(0.25rem * -3); + margin-inline-end: calc(0.25rem * -3); + } + } + } + } + .input-sm { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 8); + font-size: max(var(--font-size, 0.75rem), 0.75rem); + &[type="number"] { + &::-webkit-inner-spin-button { + margin-block: calc(0.25rem * -2); + margin-inline-end: calc(0.25rem * -3); + } + } + } + } + .input-xl { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 14); + font-size: max(var(--font-size, 1.375rem), 1.375rem); + &[type="number"] { + &::-webkit-inner-spin-button { + margin-block: calc(0.25rem * -4); + margin-inline-end: calc(0.25rem * -3); + } + } + } + } + .input-xs { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 6); + font-size: max(var(--font-size, 0.6875rem), 0.6875rem); + &[type="number"] { + &::-webkit-inner-spin-button { + margin-block: calc(0.25rem * -1); + margin-inline-end: calc(0.25rem * -3); + } + } + } + } + .label { + @layer daisyui.l1.l2.l3 { + display: inline-flex; + align-items: center; + gap: calc(0.25rem * 1.5); + white-space: nowrap; + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 60%, transparent); + } + &:has(input) { + cursor: pointer; + } + &:is(.input > *, .select > *) { + display: flex; + height: calc(100% - 0.5rem); + align-items: center; + padding-inline: calc(0.25rem * 3); + white-space: nowrap; + font-size: inherit; + &:first-child { + margin-inline-start: calc(0.25rem * -3); + margin-inline-end: calc(0.25rem * 3); + border-inline-end: var(--border) solid currentColor; + @supports (color: color-mix(in lab, red, red)) { + border-inline-end: var(--border) solid color-mix(in oklab, currentColor 10%, #0000); + } + } + &:last-child { + margin-inline-start: calc(0.25rem * 3); + margin-inline-end: calc(0.25rem * -3); + border-inline-start: var(--border) solid currentColor; + @supports (color: color-mix(in lab, red, red)) { + border-inline-start: var(--border) solid color-mix(in oklab, currentColor 10%, #0000); + } + } + } + } + } + .mt-0 { + margin-top: calc(var(--spacing) * 0); + } + .mt-0\! { + margin-top: calc(var(--spacing) * 0) !important; + } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + .fieldset-legend { + @layer daisyui.l1.l2.l3 { + margin-bottom: calc(0.25rem * -1); + display: flex; + align-items: center; + justify-content: space-between; + gap: calc(0.25rem * 2); + padding-block: calc(0.25rem * 2); + color: var(--color-base-content); + font-weight: 600; + } + } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + .mb-12 { + margin-bottom: calc(var(--spacing) * 12); + } .status { @layer daisyui.l1.l2.l3 { display: inline-block; @@ -1759,6 +2539,103 @@ padding-inline: calc(var(--size) / 2 - var(--border)); } } + .kbd { + box-shadow: none; + @layer daisyui.l1.l2.l3 { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-field); + background-color: var(--color-base-200); + vertical-align: middle; + padding-inline: 0.5em; + border: var(--border) solid var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border: var(--border) solid color-mix(in srgb, var(--color-base-content) 20%, #0000); + } + border-bottom: calc(var(--border) + 1px) solid var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + border-bottom: calc(var(--border) + 1px) solid color-mix(in srgb, var(--color-base-content) 20%, #0000); + } + --size: calc(var(--size-selector, 0.25rem) * 6); + font-size: 0.875rem; + height: var(--size); + min-width: var(--size); + } + } + .stat { + @layer daisyui.l1.l2.l3 { + display: inline-grid; + width: 100%; + column-gap: calc(0.25rem * 4); + padding-inline: calc(0.25rem * 6); + padding-block: calc(0.25rem * 4); + grid-template-columns: repeat(1, 1fr); + &:not(:last-child) { + border-inline-end: var(--border) dashed currentColor; + @supports (color: color-mix(in lab, red, red)) { + border-inline-end: var(--border) dashed color-mix(in oklab, currentColor 10%, #0000); + } + border-block-end: none; + } + } + } + .fieldset-label { + @layer daisyui.l1.l2.l3 { + display: flex; + align-items: center; + gap: calc(0.25rem * 1.5); + color: var(--color-base-content); + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, var(--color-base-content) 60%, transparent); + } + &:has(input) { + cursor: pointer; + } + } + } + .alert { + border-width: var(--border); + border-color: var(--alert-border-color, var(--color-base-200)); + @layer daisyui.l1.l2.l3 { + border-style: solid; + --alert-border-color: var(--color-base-200); + display: grid; + align-items: center; + gap: calc(0.25rem * 4); + border-radius: var(--radius-box); + padding-inline: calc(0.25rem * 4); + padding-block: calc(0.25rem * 3); + color: var(--color-base-content); + background-color: var(--alert-color, var(--color-base-200)); + justify-content: start; + justify-items: start; + grid-auto-flow: column; + grid-template-columns: auto; + text-align: start; + font-size: 0.875rem; + line-height: 1.25rem; + background-size: auto, calc(var(--noise) * 100%); + background-image: none, var(--fx-noise); + box-shadow: 0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * 0.08)) inset, 0 1px #000, 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * 0.08)); + @supports (color: color-mix(in lab, red, red)) { + box-shadow: 0 3px 0 -2px oklch(100% 0 0 / calc(var(--depth) * 0.08)) inset, 0 1px color-mix( in oklab, color-mix(in oklab, #000 20%, var(--alert-color, var(--color-base-200))) calc(var(--depth) * 20%), #0000 ), 0 4px 3px -2px oklch(0% 0 0 / calc(var(--depth) * 0.08)); + } + &:has(:nth-child(2)) { + grid-template-columns: auto minmax(auto, 1fr); + } + } + } + .fieldset { + @layer daisyui.l1.l2.l3 { + display: grid; + gap: calc(0.25rem * 1.5); + padding-block: calc(0.25rem * 1); + font-size: 0.75rem; + grid-template-columns: 1fr; + grid-auto-rows: max-content; + } + } .mask { @layer daisyui.l1.l2.l3 { display: inline-block; @@ -1768,15 +2645,48 @@ mask-position: center; } } + .block { + display: block; + } + .flex { + display: flex; + } + .hidden { + display: none; + } .inline { display: inline; } .table { display: table; } + .h-6 { + height: calc(var(--spacing) * 6); + } + .w-full { + width: 100%; + } + .max-w-7 { + max-width: calc(var(--spacing) * 7); + } + .max-w-lg { + max-width: var(--container-lg); + } + .max-w-xs { + max-width: var(--container-xs); + } + .flex-1 { + flex: 1; + } .flex-shrink { flex-shrink: 1; } + .flex-grow { + flex-grow: 1; + } + .grow { + flex-grow: 1; + } .border-collapse { border-collapse: collapse; } @@ -1820,30 +2730,227 @@ .resize { resize: both; } + .flex-col { + flex-direction: column; + } .flex-wrap { flex-wrap: wrap; } + .items-center { + align-items: center; + } + .items-start { + align-items: flex-start; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-6 { + gap: calc(var(--spacing) * 6); + } + .space-y-1 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-2 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-x-1 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse))); + } + } + .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .overflow-x-auto { + overflow-x: auto; + } + .rounded { + border-radius: 0.25rem; + } .border { border-style: var(--tw-border-style); border-width: 1px; } + .bg-green-100 { + background-color: var(--color-green-100); + } + .bg-white { + background-color: var(--color-white); + } .mask-repeat { mask-repeat: repeat; } + .p-0 { + padding: calc(var(--spacing) * 0); + } + .p-0\! { + padding: calc(var(--spacing) * 0) !important; + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .select-lg { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 12); + font-size: 1.125rem; + option { + padding-inline: calc(0.25rem * 4); + padding-block: calc(0.25rem * 1.5); + } + } + } + .select-sm { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 8); + font-size: 0.75rem; + option { + padding-inline: calc(0.25rem * 2.5); + padding-block: calc(0.25rem * 1); + } + } + } + .select-xl { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 14); + font-size: 1.375rem; + option { + padding-inline: calc(0.25rem * 5); + padding-block: calc(0.25rem * 1.5); + } + } + } + .select-xs { + @layer daisyui.l1.l2 { + --size: calc(var(--size-field, 0.25rem) * 6); + font-size: 0.6875rem; + option { + padding-inline: calc(0.25rem * 2); + padding-block: calc(0.25rem * 1); + } + } + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } .text-2xl { font-size: var(--text-2xl); line-height: var(--tw-leading, var(--text-2xl--line-height)); } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + .textarea-lg { + @layer daisyui.l1.l2 { + font-size: max(var(--font-size, 1.125rem), 1.125rem); + } + } + .textarea-sm { + @layer daisyui.l1.l2 { + font-size: max(var(--font-size, 0.75rem), 0.75rem); + } + } + .textarea-xl { + @layer daisyui.l1.l2 { + font-size: max(var(--font-size, 1.375rem), 1.375rem); + } + } + .textarea-xs { + @layer daisyui.l1.l2 { + font-size: max(var(--font-size, 0.6875rem), 0.6875rem); + } + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } .text-wrap { text-wrap: wrap; } + .alert-error { + @layer daisyui.l1.l2 { + color: var(--color-error-content); + --alert-border-color: var(--color-error); + --alert-color: var(--color-error); + } + } + .alert-success { + @layer daisyui.l1.l2 { + color: var(--color-success-content); + --alert-border-color: var(--color-success); + --alert-color: var(--color-success); + } + } + .text-error { + color: var(--color-error); + } + .text-gray-500 { + color: var(--color-gray-500); + } + .text-gray-600 { + color: var(--color-gray-600); + } + .text-gray-800 { + color: var(--color-gray-800); + } + .text-green-800 { + color: var(--color-green-800); + } + .text-primary { + color: var(--color-primary); + } .underline { text-decoration-line: underline; } + .shadow { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } .outline { outline-style: var(--tw-outline-style); outline-width: 1px; } + .invert { + --tw-invert: invert(100%); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } .filter { filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } @@ -1866,6 +2973,41 @@ --btn-fg: var(--color-primary-content); } } + .range-lg { + @layer daisyui.l1.l2 { + --range-thumb-size: calc(var(--size-selector, 0.25rem) * 7); + } + } + .range-sm { + @layer daisyui.l1.l2 { + --range-thumb-size: calc(var(--size-selector, 0.25rem) * 5); + } + } + .range-xl { + @layer daisyui.l1.l2 { + --range-thumb-size: calc(var(--size-selector, 0.25rem) * 8); + } + } + .range-xs { + @layer daisyui.l1.l2 { + --range-thumb-size: calc(var(--size-selector, 0.25rem) * 4); + } + } + .textarea-error { + @layer daisyui.l1.l2 { + &, &:focus, &:focus-within { + --input-color: var(--color-error); + } + } + } + .hover\:shadow { + &:hover { + @media (hover: hover) { + --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + } } @layer base { :where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] { @@ -2234,11 +3376,90 @@ syntax: "*"; inherits: false; } +@property --tw-space-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-space-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} @property --tw-border-style { syntax: "*"; inherits: false; initial-value: solid; } +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} @property --tw-outline-style { syntax: "*"; inherits: false; @@ -2309,7 +3530,24 @@ --tw-rotate-z: initial; --tw-skew-x: initial; --tw-skew-y: initial; + --tw-space-y-reverse: 0; + --tw-space-x-reverse: 0; --tw-border-style: solid; + --tw-font-weight: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; --tw-outline-style: solid; --tw-blur: initial; --tw-brightness: initial; diff --git a/pkg/web/components/base/input.templ b/pkg/web/components/base/input.templ index 87c5049..f23657e 100644 --- a/pkg/web/components/base/input.templ +++ b/pkg/web/components/base/input.templ @@ -6,6 +6,7 @@ type InputProps struct { Label string Type string // defaults to "text" Value string + Class string Placeholder string Description string Error string @@ -37,7 +38,7 @@ templ Input(props InputProps) {