chore: update templ and templui

This commit is contained in:
juancwu 2026-04-12 16:07:06 +00:00
commit 61eaa268ab
89 changed files with 25776 additions and 8231 deletions

View file

@ -25,6 +25,6 @@ templ ThemeSwitcher(props ...ThemeSwitcherProps) {
"aria-label": "Toggle theme",
},
}) {
@icon.Eclipse(icon.Props{Size: 20})
@icon.Eclipse(icon.Props{Class: ""})
}
}

View file

@ -1,4 +1,4 @@
// templui component accordion - version: v1.2.0 installed by templui v1.2.0
// templui component accordion - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/accordion
package accordion
@ -97,10 +97,7 @@ templ Trigger(props ...TriggerProps) {
{ p.Attributes... }
>
{ children... }
@icon.ChevronDown(icon.Props{
Size: 16,
Class: "size-4 shrink-0 translate-y-0.5 transition-transform duration-200 text-muted-foreground pointer-events-none",
})
@icon.ChevronDown(icon.Props{Class: "size-4 shrink-0 translate-y-0.5 transition-transform duration-200 text-muted-foreground pointer-events-none"})
</summary>
}

View file

@ -1,4 +1,4 @@
// templui component alert - version: v1.2.0 installed by templui v1.2.0
// templui component alert - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/alert
package alert

View file

@ -1,4 +1,4 @@
// templui component aspectratio - version: v1.2.0 installed by templui v1.2.0
// templui component aspectratio - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/aspect-ratio
package aspectratio

View file

@ -1,4 +1,4 @@
// templui component avatar - version: v1.2.0 installed by templui v1.2.0
// templui component avatar - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/avatar
package avatar
@ -92,6 +92,10 @@ templ Fallback(props ...FallbackProps) {
</span>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/avatar.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("avatar")
}
}

View file

@ -1,4 +1,4 @@
// templui component badge - version: v1.2.0 installed by templui v1.2.0
// templui component badge - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/badge
package badge

View file

@ -1,4 +1,4 @@
// templui component breadcrumb - version: v1.2.0 installed by templui v1.2.0
// templui component breadcrumb - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/breadcrumb
package breadcrumb
@ -148,7 +148,7 @@ templ Separator(props ...SeparatorProps) {
if p.UseCustom {
{ children... }
} else {
@icon.ChevronRight(icon.Props{Size: 14, Class: "text-muted-foreground"})
@icon.ChevronRight(icon.Props{Class: "size-3.5 text-muted-foreground"})
}
</span>
}

View file

@ -1,4 +1,4 @@
// templui component button - version: v1.2.0 installed by templui v1.2.0
// templui component button - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/button
package button

View file

@ -1,4 +1,4 @@
// templui component calendar - version: v1.2.0 installed by templui v1.2.0
// templui component calendar - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/calendar
package calendar
@ -35,15 +35,15 @@ var (
)
type Props struct {
ID string
Class string
LocaleTag LocaleTag
Value *time.Time
Name string
InitialMonth int // Optional: 0-11 (Default: current or from Value). Controls the initially displayed month view.
InitialYear int // Optional: (Default: current or from Value). Controls the initially displayed year view.
StartOfWeek *Day // Optional: 0-6 [Sun-Sat] (Default: 1).
RenderHiddenInput bool // Optional: Whether to render the hidden input (Default: true). Set to false when used inside DatePicker.
ID string
Class string
LocaleTag LocaleTag
Value *time.Time
Name string
InitialMonth int // Optional: 0-11 (Default: current or from Value). Controls the initially displayed month view.
InitialYear int // Optional: (Default: current or from Value). Controls the initially displayed year view.
StartOfWeek *Day // Optional: 0-6 [Sun-Sat] (Default: 1).
HideHiddenInput bool // Optional: Hide the hidden input when a parent component owns form submission.
}
templ Calendar(props ...Props) {
@ -62,13 +62,6 @@ templ Calendar(props ...Props) {
if p.LocaleTag == "" {
p.LocaleTag = LocaleDefaultTag
}
// Default to rendering hidden input unless explicitly set to false
if p.RenderHiddenInput == false && len(props) > 0 {
// Only respect false if it was explicitly passed
p.RenderHiddenInput = props[0].RenderHiddenInput
} else {
p.RenderHiddenInput = true
}
initialStartOfWeek := Monday
if p.StartOfWeek != nil {
@ -106,8 +99,12 @@ templ Calendar(props ...Props) {
// Generate short month names (English only, JS will update with localized)
monthNames := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
}}
<div class={ p.Class } id={ p.ID + "-wrapper" } data-tui-calendar-wrapper="true">
if p.RenderHiddenInput {
<div
class={ utils.TwMerge("inline-flex flex-col [--cell-size:2rem]", p.Class) }
id={ p.ID + "-wrapper" }
data-tui-calendar-wrapper="true"
>
if !p.HideHiddenInput {
<input
type="hidden"
name={ p.Name }
@ -118,6 +115,7 @@ templ Calendar(props ...Props) {
}
<div
id={ p.ID }
class="inline-flex flex-col"
data-tui-calendar-container="true"
data-tui-calendar-locale-tag={ string(p.LocaleTag) }
data-tui-calendar-initial-month={ strconv.Itoa(initialMonth) }
@ -126,7 +124,7 @@ templ Calendar(props ...Props) {
data-tui-calendar-start-of-week={ int(initialStartOfWeek) }
>
<!-- Calendar Header -->
<div class="flex items-center gap-2 mb-4">
<div class="flex w-full items-center gap-2 mb-4">
<button
type="button"
data-tui-calendar-prev
@ -152,7 +150,7 @@ templ Calendar(props ...Props) {
</select>
<span class="select-none font-medium rounded-md px-2 flex items-center justify-center gap-1 text-sm h-7 pointer-events-none" aria-hidden="true">
<span id={ p.ID + "-month-value" }>{ monthNames[currentMonth] }</span>
@icon.ChevronDown(icon.Props{Size: 14, Class: "text-muted-foreground"})
@icon.ChevronDown(icon.Props{Class: "size-3.5 text-muted-foreground"})
</span>
</div>
<!-- Year Select -->
@ -172,7 +170,7 @@ templ Calendar(props ...Props) {
</select>
<span class="select-none font-medium rounded-md px-2 flex items-center justify-center gap-1 text-sm h-7 pointer-events-none" aria-hidden="true">
<span id={ p.ID + "-year-value" }>{ strconv.Itoa(currentYear) }</span>
@icon.ChevronDown(icon.Props{Size: 14, Class: "text-muted-foreground"})
@icon.ChevronDown(icon.Props{Class: "size-3.5 text-muted-foreground"})
</span>
</div>
</div>
@ -185,13 +183,17 @@ templ Calendar(props ...Props) {
</button>
</div>
<!-- Weekday Headers -->
<div data-tui-calendar-weekdays class="grid grid-cols-7 gap-1 mb-1 place-items-center"></div>
<div data-tui-calendar-weekdays class="inline-grid grid-cols-[repeat(7,var(--cell-size))] gap-1 mb-1 place-items-center"></div>
<!-- Calendar Day Grid -->
<div data-tui-calendar-days class="grid grid-cols-7 gap-1 place-items-center"></div>
<div data-tui-calendar-days class="inline-grid grid-cols-[repeat(7,var(--cell-size))] gap-1 place-items-center"></div>
</div>
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/calendar.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("calendar")
}
}

View file

@ -1,4 +1,4 @@
// templui component card - version: v1.2.0 installed by templui v1.2.0
// templui component card - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/card
package card

View file

@ -1,4 +1,4 @@
// templui component carousel - version: v1.2.0 installed by templui v1.2.0
// templui component carousel - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/carousel
package carousel
@ -206,6 +206,10 @@ templ Indicators(props ...IndicatorsProps) {
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/carousel.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("carousel")
}
}

View file

@ -1,4 +1,4 @@
// templui component chart - version: v1.2.0 installed by templui v1.2.0
// templui component chart - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/charts
package chart
@ -53,11 +53,17 @@ type Config struct {
BeginAtZero *bool `json:"beginAtZero,omitempty"`
}
type ScriptConfig struct {
RawConfig map[string]any `json:"rawConfig,omitempty"`
GeneratedConfig *Config `json:"generatedConfig,omitempty"`
}
type Props struct {
ID string
Variant Variant
Data Data
Options Options
RawConfig map[string]any
ShowLegend bool
ShowXAxis bool
ShowYAxis bool
@ -96,27 +102,37 @@ templ Chart(props ...Props) {
<canvas id={ canvasId } data-tui-chart-id={ dataId }></canvas>
</div>
{{
chartConfig := Config{
Type: p.Variant,
Data: p.Data,
Options: p.Options,
ShowLegend: p.ShowLegend,
ShowXAxis: p.ShowXAxis,
ShowYAxis: p.ShowYAxis,
ShowXLabels: p.ShowXLabels,
ShowYLabels: p.ShowYLabels,
ShowXGrid: p.ShowXGrid,
ShowYGrid: p.ShowYGrid,
Horizontal: p.Horizontal,
Stacked: p.Stacked,
YMin: p.YMin,
YMax: p.YMax,
BeginAtZero: p.BeginAtZero,
scriptConfig := ScriptConfig{
RawConfig: p.RawConfig,
}
if p.RawConfig == nil {
generatedConfig := Config{
Type: p.Variant,
Data: p.Data,
Options: p.Options,
ShowLegend: p.ShowLegend,
ShowXAxis: p.ShowXAxis,
ShowYAxis: p.ShowYAxis,
ShowXLabels: p.ShowXLabels,
ShowYLabels: p.ShowYLabels,
ShowXGrid: p.ShowXGrid,
ShowYGrid: p.ShowYGrid,
Horizontal: p.Horizontal,
Stacked: p.Stacked,
YMin: p.YMin,
YMax: p.YMax,
BeginAtZero: p.BeginAtZero,
}
scriptConfig.GeneratedConfig = &generatedConfig
}
}}
@templ.JSONScript(dataId, chartConfig)
@templ.JSONScript(dataId, scriptConfig)
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/chart.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("chart")
}
}

View file

@ -1,4 +1,4 @@
// templui component checkbox - version: v1.2.0 installed by templui v1.2.0
// templui component checkbox - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/checkbox
package checkbox
@ -71,17 +71,21 @@ templ Checkbox(props ...Props) {
if p.Icon != nil {
@p.Icon
} else {
@icon.Check(icon.Props{Size: 14})
@icon.Check(icon.Props{Class: "size-3.5"})
}
</div>
<div
class="absolute inset-0 pointer-events-none flex items-center justify-center text-primary-foreground opacity-0 peer-indeterminate:opacity-100"
>
@icon.Minus(icon.Props{Size: 14})
@icon.Minus(icon.Props{Class: "size-3.5"})
</div>
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/checkbox.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("checkbox")
}
}

View file

@ -1,4 +1,4 @@
// templui component collapsible - version: v1.2.0 installed by templui v1.2.0
// templui component collapsible - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/collapsible
package collapsible
@ -81,6 +81,10 @@ templ Content(props ...ContentProps) {
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/collapsible.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("collapsible")
}
}

View file

@ -1,4 +1,4 @@
// templui component copybutton - version: v1.2.0 installed by templui v1.2.0
// templui component copybutton - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/copy-button
package copybutton
@ -34,15 +34,19 @@ templ CopyButton(props Props) {
Type: button.TypeButton,
}) {
<span data-copy-icon-clipboard>
@icon.Clipboard(icon.Props{Size: 16})
@icon.Clipboard(icon.Props{Class: "size-4"})
</span>
<span data-copy-icon-check class="hidden">
@icon.Check(icon.Props{Size: 16})
@icon.Check(icon.Props{Class: "size-4"})
</span>
}
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/copybutton.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("copybutton")
}
}

View file

@ -1,4 +1,4 @@
// templui component datepicker - version: v1.2.0 installed by templui v1.2.0
// templui component datepicker - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/date-picker
package datepicker
@ -47,8 +47,6 @@ type Props struct {
Placeholder string
Disabled bool
HasError bool
Required bool
Clearable bool
}
templ DatePicker(props ...Props) {
@ -73,99 +71,79 @@ templ DatePicker(props ...Props) {
p.Format = FormatLOCALE_MEDIUM
}
var contentID = p.ID + "-content"
var valuePtr *time.Time
var initialSelectedISO string
if !p.Value.IsZero() {
valuePtr = &p.Value
initialSelectedISO = p.Value.Format("2006-01-02")
}
var required = "false"
if p.Required {
required = "true"
}
}}
<div class="relative inline-block w-full">
<input
type="hidden"
name={ p.Name }
value={ initialSelectedISO }
if p.Form != "" {
form={ p.Form }
}
id={ p.ID + "-hidden" }
data-tui-datepicker-hidden-input
/>
@popover.Trigger(popover.TriggerProps{For: contentID}) {
@button.Button(button.Props{
ID: p.ID,
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(p.Attributes, templ.Attributes{
"data-tui-datepicker": "true",
"data-tui-datepicker-display-format": string(p.Format),
"data-tui-datepicker-locale-tag": string(p.LocaleTag),
"data-tui-datepicker-placeholder": p.Placeholder,
"data-tui-datepicker-required": required,
"aria-invalid": utils.If(p.HasError, "true"),
}),
}) {
if p.Placeholder != "" {
<span data-tui-datepicker-display class={ "text-left grow text-muted-foreground" }>
{ p.Placeholder }
<div class="relative inline-block w-full" data-tui-datepicker-root>
@popover.Root() {
<input
type="hidden"
name={ p.Name }
value={ initialSelectedISO }
if p.Form != "" {
form={ p.Form }
}
data-tui-datepicker-hidden-input
/>
@popover.Trigger() {
@button.Button(button.Props{
ID: p.ID,
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(p.Attributes, templ.Attributes{
"data-tui-datepicker": "true",
"data-tui-datepicker-display-format": string(p.Format),
"data-tui-datepicker-locale-tag": string(p.LocaleTag),
"data-tui-datepicker-placeholder": p.Placeholder,
"aria-invalid": utils.If(p.HasError, "true"),
}),
}) {
if p.Placeholder != "" {
<span data-tui-datepicker-display class={ "text-left grow text-muted-foreground" }>
{ p.Placeholder }
</span>
}
<span class="text-muted-foreground flex items-center ml-2">
@icon.Calendar(icon.Props{Class: "size-4"})
</span>
}
<span class="text-muted-foreground flex items-center ml-2">
@icon.Calendar(icon.Props{Size: 16})
</span>
}
}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: popover.PlacementBottomStart,
Class: "p-0",
}) {
@card.Card(card.Props{
Class: "border-0 shadow-none",
@popover.Content(popover.ContentProps{
Placement: popover.PlacementBottomStart,
Class: "p-0",
}) {
@card.Content(card.ContentProps{
Class: "p-3",
@card.Card(card.Props{
Class: "border-0 shadow-none",
}) {
@calendar.Calendar(calendar.Props{
ID: p.ID + "-calendar-instance", // Pass ID for calendar instance
LocaleTag: calendar.LocaleTag(p.LocaleTag), // Pass locale tag to calendar
StartOfWeek: p.StartOfWeek, // Pass start of week to calendar
Value: valuePtr, // Pass pointer to value
RenderHiddenInput: false, // Don't render hidden input inside popover
})
if p.Clearable {
@button.Button(button.Props{
ID: p.ID + "-clear-button",
Class: "mt-4 w-full",
Variant: button.VariantOutline,
Attributes: templ.Attributes{
"data-tui-datepicker-clear": "true",
},
}) {
Clear
}
@card.Content(card.ContentProps{
Class: "p-3",
}) {
@calendar.Calendar(calendar.Props{
LocaleTag: calendar.LocaleTag(p.LocaleTag),
StartOfWeek: p.StartOfWeek,
Value: valuePtr,
HideHiddenInput: true,
})
}
}
}
@ -173,6 +151,12 @@ templ DatePicker(props ...Props) {
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/datepicker.js") }></script>
@scriptOnce.Once() {
@calendar.Script()
@popover.Script()
@utils.ComponentScript("datepicker")
}
}

View file

@ -1,4 +1,4 @@
// templui component dialog - version: v1.2.0 installed by templui v1.2.0
// templui component dialog - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/dialog
package dialog
@ -11,8 +11,7 @@ import (
type contextKey string
const (
instanceKey contextKey = "dialogInstance"
openKey contextKey = "dialogOpen"
openKey contextKey = "dialogOpen"
)
type Props struct {
@ -28,23 +27,20 @@ type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string // Reference to a specific dialog ID (for external triggers)
For string // Dialog root ID for external triggers
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
HideCloseButton bool
Open bool // Initial open state for standalone usage (when no context)
DisableAutoFocus bool
Class string
Attributes templ.Attributes
HideCloseButton bool
}
type CloseProps struct {
ID string
Class string
Attributes templ.Attributes
For string // ID of the dialog to close (optional, defaults to closest dialog)
For string // Dialog root ID to close (optional, defaults to closest dialog)
}
type HeaderProps struct {
@ -76,25 +72,20 @@ templ Dialog(props ...Props) {
if len(props) > 0 {
{{ p = props[0] }}
}
{{ instanceID := p.ID }}
if instanceID == "" {
{{ instanceID = utils.RandomID() }}
}
{{ ctx = context.WithValue(ctx, instanceKey, instanceID) }}
{{ ctx = context.WithValue(ctx, openKey, p.Open) }}
<div
if p.ID != "" {
id={ p.ID }
}
data-tui-dialog
data-dialog-instance={ instanceID }
if p.DisableClickAway {
data-tui-dialog-disable-click-away="true"
}
if p.DisableESC {
data-tui-dialog-disable-esc="true"
}
class={ utils.TwMerge("", p.Class) }
data-tui-dialog-open={ utils.IfElse(p.Open, "true", "false") }
class={ utils.TwMerge("contents", p.Class) }
{ p.Attributes... }
>
{ children... }
@ -106,19 +97,14 @@ templ Trigger(props ...TriggerProps) {
if len(props) > 0 {
{{ p = props[0] }}
}
{{ instanceID := "" }}
// Explicit For prop takes priority over inherited context
if p.For != "" {
{{ instanceID = p.For }}
} else if val := ctx.Value(instanceKey); val != nil {
{{ instanceID = val.(string) }}
}
<span
if p.ID != "" {
id={ p.ID }
}
data-tui-dialog-trigger={ instanceID }
data-dialog-instance={ instanceID }
data-tui-dialog-trigger
if p.For != "" {
data-tui-dialog-target={ p.For }
}
data-tui-dialog-trigger-open="false"
class={ utils.TwMerge("contents", p.Class) }
{ p.Attributes... }
@ -132,114 +118,67 @@ templ Content(props ...ContentProps) {
if len(props) > 0 {
{{ p = props[0] }}
}
// Start with prop values as defaults
{{ instanceID := p.ID }}
{{ open := p.Open }}
// Override with context values if available
if val := ctx.Value(instanceKey); val != nil {
{{ instanceID = val.(string) }}
}
{{ open := false }}
if val := ctx.Value(openKey); val != nil {
{{ open = val.(bool) }}
}
// Apply defaults if still empty
if instanceID == "" {
{{ instanceID = utils.RandomID() }}
}
<!-- Overlay -->
<div
class={ utils.TwMerge(
"fixed inset-0 z-50 bg-black/50",
"transition-opacity duration-300",
"data-[tui-dialog-open=false]:opacity-0",
"data-[tui-dialog-open=true]:opacity-100",
"data-[tui-dialog-open=false]:pointer-events-none",
"data-[tui-dialog-open=true]:pointer-events-auto",
"data-[tui-dialog-hidden=true]:!hidden",
) }
data-tui-dialog-backdrop
data-dialog-instance={ instanceID }
if open {
data-tui-dialog-open="true"
} else {
data-tui-dialog-open="false"
data-tui-dialog-hidden="true"
}
></div>
<!-- Content -->
<div
<dialog
class={
utils.TwMerge(
// Base positioning
"fixed z-50 left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%]",
// Style
"bg-background rounded-lg border shadow-lg",
// Layout
"grid gap-4 p-6",
// Size
"w-full max-w-[calc(100%-2rem)] sm:max-w-lg",
// Transitions
"fixed left-[50%] top-[50%] z-50 m-0 w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-hidden border bg-background text-foreground p-0 shadow-lg outline-none sm:max-w-lg",
"[&:not([open]):not([data-tui-dialog-closing=true])]:hidden",
"rounded-lg",
"[&::backdrop]:transition-all [&::backdrop]:duration-200",
"data-[tui-dialog-open=false]:[&::backdrop]:bg-black/0",
"data-[tui-dialog-open=true]:[&::backdrop]:bg-black/50",
"transition-all duration-200",
// Scale animation
"data-[tui-dialog-open=false]:scale-95",
"data-[tui-dialog-open=true]:scale-100",
// Opacity
"data-[tui-dialog-open=false]:opacity-0",
"data-[tui-dialog-open=true]:opacity-100",
// Pointer events
"data-[tui-dialog-open=false]:pointer-events-none",
"data-[tui-dialog-open=true]:pointer-events-auto",
// Hidden state
"data-[tui-dialog-hidden=true]:!hidden",
p.Class,
),
}
data-tui-dialog-content
data-dialog-instance={ instanceID }
if p.DisableAutoFocus {
data-tui-dialog-disable-autofocus="true"
}
if open {
data-tui-dialog-open="true"
} else {
data-tui-dialog-open="false"
data-tui-dialog-hidden="true"
}
data-tui-dialog-open={ utils.IfElse(open, "true", "false") }
data-tui-dialog-initial-open={ utils.IfElse(open, "true", "false") }
{ p.Attributes... }
>
{ children... }
if !p.HideCloseButton {
<button
class={ utils.TwMerge(
// Positioning
"absolute top-4 right-4",
// Style
"rounded-xs opacity-70",
// Interactions
"transition-opacity hover:opacity-100",
// Focus states
"focus:outline-none focus:ring-2",
"focus:ring-ring focus:ring-offset-2",
"ring-offset-background",
// Hover/Data states
"data-[tui-dialog-open=true]:bg-accent",
"data-[tui-dialog-open=true]:text-muted-foreground",
// Disabled state
"disabled:pointer-events-none",
// Icon styles
"[&_svg]:pointer-events-none",
<div class="relative grid gap-4 p-6" data-tui-dialog-panel>
{ children... }
if !p.HideCloseButton {
<button
class={ utils.TwMerge(
// Positioning
"absolute top-4 right-4",
// Style
"rounded-xs opacity-70",
// Interactions
"transition-opacity hover:opacity-100",
// Focus states
"focus:outline-none focus:ring-2",
"focus:ring-ring focus:ring-offset-2",
"ring-offset-background",
// Hover/Data states
"data-[tui-dialog-open=true]:bg-accent",
"data-[tui-dialog-open=true]:text-muted-foreground",
// Disabled state
"disabled:pointer-events-none",
// Icon styles
"[&_svg]:pointer-events-none",
"[&_svg]:shrink-0",
"[&_svg:not([class*='size-'])]:size-4",
) }
data-tui-dialog-close={ instanceID }
aria-label="Close"
type="button"
>
@icon.X()
<span class="sr-only">Close</span>
</button>
}
</div>
data-tui-dialog-close
aria-label="Close"
type="button"
>
@icon.X()
<span class="sr-only">Close</span>
</button>
}
</div>
</dialog>
}
templ Close(props ...CloseProps) {
@ -251,10 +190,9 @@ templ Close(props ...CloseProps) {
if p.ID != "" {
id={ p.ID }
}
data-tui-dialog-close
if p.For != "" {
data-tui-dialog-close={ p.For }
} else {
data-tui-dialog-close
data-tui-dialog-target={ p.For }
}
class={ utils.TwMerge("contents cursor-pointer", p.Class) }
{ p.Attributes... }
@ -327,6 +265,10 @@ templ Description(props ...DescriptionProps) {
</p>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/dialog.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("dialog")
}
}

View file

@ -1,9 +1,8 @@
// templui component dropdown - version: v1.2.0 installed by templui v1.2.0
// templui component dropdown - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/dropdown
package dropdown
import (
"context"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/popover"
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
)
@ -25,25 +24,16 @@ const (
PlacementLeftEnd = popover.PlacementLeftEnd
)
type contextKey string
var (
contentIDKey contextKey = "contentID"
subContentIDKey contextKey = "subContentID"
)
type Props struct {
ID string
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
Placement Placement
@ -90,13 +80,11 @@ type SubProps struct {
}
type SubTriggerProps struct {
ID string
Class string
Attributes templ.Attributes
}
type SubContentProps struct {
ID string
Class string
Attributes templ.Attributes
}
@ -108,36 +96,25 @@ type PortalProps struct {
}
templ Dropdown(props ...Props) {
{{
var p Props
if len(props) > 0 {
p = props[0]
}
contentID := p.ID
if contentID == "" {
contentID = utils.RandomID()
}
ctx = context.WithValue(ctx, contentIDKey, contentID)
}}
{ children... }
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Root(popover.RootProps{
ID: p.ID,
}) {
{ children... }
}
}
templ Trigger(props ...TriggerProps) {
{{
var p TriggerProps
if len(props) > 0 {
p = props[0]
}
contentID, ok := ctx.Value(contentIDKey).(string)
if !ok {
contentID = "fallback-content-id"
}
}}
{{ var p TriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Trigger(popover.TriggerProps{
ID: p.ID,
Class: p.Class,
Attributes: p.Attributes,
For: contentID,
TriggerType: popover.TriggerTypeClick,
}) {
{ children... }
@ -149,10 +126,6 @@ templ Content(props ...ContentProps) {
if len(props) > 0 {
{{ p = props[0] }}
}
{{ contentID, ok := ctx.Value(contentIDKey).(string) }}
if !ok {
{{ contentID = "fallback-content-id" }} // Must match fallback in Trigger
}
{{
placement := p.Placement
if placement == "" {
@ -160,7 +133,6 @@ templ Content(props ...ContentProps) {
}
}}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: placement,
Class: utils.TwMerge(
"z-50 rounded-md bg-popover p-1 shadow-md focus:outline-none overflow-auto",
@ -175,6 +147,15 @@ templ Content(props ...ContentProps) {
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
@scriptOnce.Once() {
@popover.Script()
@utils.ComponentScript("dropdown")
}
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
@ -298,43 +279,25 @@ templ Shortcut(props ...ShortcutProps) {
}
templ Sub(props ...SubProps) {
{{
var p SubProps
if len(props) > 0 {
p = props[0]
}
subContentID := p.ID
if subContentID == "" {
subContentID = utils.RandomID()
}
ctx = context.WithValue(ctx, subContentIDKey, subContentID)
}}
<div
if p.ID != "" {
id={ p.ID }
}
data-tui-dropdown-submenu
class={ utils.TwMerge("relative", p.Class) }
{ p.Attributes... }
>
{{ var p SubProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Root(popover.RootProps{
ID: p.ID,
Class: p.Class,
Attributes: p.Attributes,
}) {
{ children... }
</div>
}
}
templ SubTrigger(props ...SubTriggerProps) {
{{
var p SubTriggerProps
if len(props) > 0 {
p = props[0]
}
subContentID, ok := ctx.Value(subContentIDKey).(string)
if !ok {
subContentID = "fallback-subcontent-id"
}
}}
{{ var p SubTriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Trigger(popover.TriggerProps{
ID: p.ID,
For: subContentID,
TriggerType: popover.TriggerTypeHover,
}) {
<button
@ -360,18 +323,11 @@ templ SubTrigger(props ...SubTriggerProps) {
}
templ SubContent(props ...SubContentProps) {
{{
var p SubContentProps
if len(props) > 0 {
p = props[0]
}
subContentID, ok := ctx.Value(subContentIDKey).(string)
if !ok {
subContentID = "fallback-subcontent-id"
}
}}
{{ var p SubContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Content(popover.ContentProps{
ID: subContentID,
Placement: popover.PlacementRightStart,
Offset: -4, // Adjust as needed
HoverDelay: 100, // ms
@ -385,7 +341,3 @@ templ SubContent(props ...SubContentProps) {
{ children... }
}
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/dropdown.min.js") }></script>
}

View file

@ -1,4 +1,4 @@
// templui component form - version: v1.2.0 installed by templui v1.2.0
// templui component form - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/form
package form

View file

@ -1,4 +1,4 @@
// templui component icon - version: v1.2.0 installed by templui v1.2.0
// templui component icon - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/icon
package icon
@ -20,12 +20,7 @@ var (
// Props defines the properties that can be set for an icon.
type Props struct {
Size int
Color string
Fill string
Stroke string
StrokeWidth string // Stroke Width of Icon, Usage: "2.5"
Class string
Class string
}
// Icon returns a function that generates a templ.Component for the specified icon name.
@ -36,10 +31,8 @@ func Icon(name string) func(...Props) templ.Component {
p = props[0]
}
// Create a unique key for the cache based on icon name and all relevant props.
// This ensures different stylings of the same icon are cached separately.
cacheKey := fmt.Sprintf("%s|s:%d|c:%s|f:%s|sk:%s|sw:%s|cl:%s",
name, p.Size, p.Color, p.Fill, p.Stroke, p.StrokeWidth, p.Class)
// Cache by icon name and class so repeated renders reuse the generated SVG.
cacheKey := fmt.Sprintf("%s|cl:%s", name, p.Class)
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) (err error) {
iconMutex.RLock()
@ -78,33 +71,10 @@ func generateSVG(name string, props Props) (string, error) {
return "", err // Error from getIconContent already includes icon name
}
size := props.Size
if size <= 0 {
size = 24 // Default size
}
fill := props.Fill
if fill == "" {
fill = "none" // Default fill
}
stroke := props.Stroke
if stroke == "" {
stroke = props.Color // Fallback to Color if Stroke is not set
}
if stroke == "" {
stroke = "currentColor" // Default stroke color
}
strokeWidth := props.StrokeWidth
if strokeWidth == "" {
strokeWidth = "2" // Default stroke width
}
// Construct the final SVG string.
// The data-lucide attribute helps identify these as Lucide icons if needed.
return fmt.Sprintf("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\" viewBox=\"0 0 24 24\" fill=\"%s\" stroke=\"%s\" stroke-width=\"%s\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"%s\" data-lucide=\"icon\">%s</svg>",
size, size, fill, stroke, strokeWidth, props.Class, content), nil
return fmt.Sprintf("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"%s\" data-lucide=\"icon\">%s</svg>",
props.Class, content), nil
}
// getIconContent retrieves the raw inner SVG content for a given icon name.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
// templui component input - version: v1.2.0 installed by templui v1.2.0
// templui component input - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/input
package input
@ -38,6 +38,7 @@ type Props struct {
Value string
Disabled bool
Readonly bool
Required bool
FileAccept string
HasError bool
NoTogglePassword bool
@ -75,6 +76,7 @@ templ Input(props ...Props) {
}
disabled?={ p.Disabled }
readonly?={ p.Readonly }
required?={ p.Required }
if p.HasError {
aria-invalid="true"
}
@ -111,20 +113,20 @@ templ Input(props ...Props) {
Attributes: templ.Attributes{"data-tui-input-toggle-password": p.ID},
}) {
<span class="icon-open block">
@icon.Eye(icon.Props{
Size: 18,
})
@icon.Eye(icon.Props{Class: "size-[18px]"})
</span>
<span class="icon-closed hidden">
@icon.EyeOff(icon.Props{
Size: 18,
})
@icon.EyeOff(icon.Props{Class: "size-[18px]"})
</span>
}
}
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/input.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("input")
}
}

View file

@ -1,8 +1,9 @@
// templui component inputotp - version: v1.2.0 installed by templui v1.2.0
// templui component inputotp - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/input-otp
package inputotp
import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
"strconv"
)
@ -176,6 +177,11 @@ templ Separator(props ...SeparatorProps) {
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/inputotp.min.js") }></script>
@scriptOnce.Once() {
@input.Script()
@utils.ComponentScript("inputotp")
}
}

View file

@ -1,4 +1,4 @@
// templui component label - version: v1.2.0 installed by templui v1.2.0
// templui component label - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/label
package label
@ -38,6 +38,10 @@ templ Label(props ...Props) {
</label>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/label.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("label")
}
}

View file

@ -1,4 +1,4 @@
// templui component pagination - version: v1.2.0 installed by templui v1.2.0
// templui component pagination - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/pagination
package pagination
@ -145,7 +145,7 @@ templ Previous(props ...PreviousProps) {
Class: utils.TwMerge("gap-1", p.Class),
Attributes: p.Attributes,
}) {
@icon.ChevronLeft(icon.Props{Size: 16})
@icon.ChevronLeft(icon.Props{Class: "size-4"})
if p.Label != "" {
<span>{ p.Label }</span>
}
@ -168,12 +168,12 @@ templ Next(props ...NextProps) {
if p.Label != "" {
<span>{ p.Label }</span>
}
@icon.ChevronRight(icon.Props{Size: 16})
@icon.ChevronRight(icon.Props{Class: "size-4"})
}
}
templ Ellipsis() {
@icon.Ellipsis(icon.Props{Size: 16})
@icon.Ellipsis(icon.Props{Class: "size-4"})
}
func CreatePagination(currentPage, totalPages, maxVisible int) struct {

View file

@ -1,4 +1,4 @@
// templui component popover - version: v1.2.0 installed by templui v1.2.0
// templui component popover - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/popover
package popover
@ -27,15 +27,21 @@ const (
type TriggerType string
const (
TriggerTypeHover TriggerType = "hover"
TriggerTypeClick TriggerType = "click"
TriggerTypeHover TriggerType = "hover"
TriggerTypeClick TriggerType = "click"
TriggerTypeManual TriggerType = "manual"
)
type RootProps struct {
ID string
Class string
Attributes templ.Attributes
}
type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string
TriggerType TriggerType
}
@ -50,10 +56,26 @@ type ContentProps struct {
ShowArrow bool
HoverDelay int
HoverOutDelay int
MatchWidth bool
Exclusive bool
}
templ Root(props ...RootProps) {
{{ var p RootProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
data-tui-popover-root
class={ utils.TwMerge("contents", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Trigger(props ...TriggerProps) {
{{ var p TriggerProps }}
if len(props) > 0 {
@ -66,11 +88,8 @@ templ Trigger(props ...TriggerProps) {
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("group cursor-pointer", p.Class) }
if p.For != "" {
data-tui-popover-trigger={ p.For }
}
data-tui-popover-open="false"
class={ utils.TwMerge("contents", p.Class) }
data-tui-popover-trigger
data-tui-popover-type={ string(p.TriggerType) }
{ p.Attributes... }
>
@ -94,8 +113,11 @@ templ Content(props ...ContentProps) {
}
}
<div
id={ p.ID }
data-tui-popover-id={ p.ID }
if p.ID != "" {
id={ p.ID }
}
popover="manual"
data-tui-popover-content
data-tui-popover-open="false"
data-tui-popover-placement={ string(p.Placement) }
data-tui-popover-offset={ strconv.Itoa(p.Offset) }
@ -105,11 +127,8 @@ templ Content(props ...ContentProps) {
data-tui-popover-hover-delay={ strconv.Itoa(p.HoverDelay) }
data-tui-popover-hover-out-delay={ strconv.Itoa(p.HoverOutDelay) }
data-tui-popover-exclusive={ strconv.FormatBool(p.Exclusive) }
if p.MatchWidth {
data-tui-popover-match-width="true"
}
class={ utils.TwMerge(
"bg-popover rounded-lg border text-popover-foreground text-sm shadow-lg pointer-events-auto absolute z-[9999] hidden top-0 left-0",
"bg-popover rounded-lg border text-popover-foreground text-sm shadow-lg pointer-events-auto fixed inset-auto top-0 left-0 m-0 max-w-[min(24rem,calc(100vw-2rem))] overflow-visible outline-none transition-opacity duration-150 ease-out data-[tui-popover-open=false]:opacity-0 data-[tui-popover-open=true]:opacity-100 motion-reduce:transition-none",
p.Class,
) }
{ p.Attributes... }
@ -120,16 +139,16 @@ templ Content(props ...ContentProps) {
if p.ShowArrow {
<div
data-tui-popover-arrow
class="absolute h-2.5 w-2.5 rotate-45 bg-popover border border-border
data-[tui-popover-placement^=top]:-bottom-[5px] data-[tui-popover-placement^=top]:border-t-transparent data-[tui-popover-placement^=top]:border-l-transparent
data-[tui-popover-placement^=bottom]:-top-[5px] data-[tui-popover-placement^=bottom]:border-b-transparent data-[tui-popover-placement^=bottom]:border-r-transparent
data-[tui-popover-placement^=left]:-right-[5px] data-[tui-popover-placement^=left]:border-l-transparent data-[tui-popover-placement^=left]:border-b-transparent
data-[tui-popover-placement^=right]:-left-[5px] data-[tui-popover-placement^=right]:border-r-transparent data-[tui-popover-placement^=right]:border-t-transparent"
class="absolute h-2.5 w-2.5 rotate-45 bg-popover border border-border"
></div>
}
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/popover.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("popover")
}
}

View file

@ -1,4 +1,4 @@
// templui component progress - version: v1.2.0 installed by templui v1.2.0
// templui component progress - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/progress
package progress
@ -122,6 +122,10 @@ func variantClasses(variant Variant) string {
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/progress.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("progress")
}
}

View file

@ -1,4 +1,4 @@
// templui component radio - version: v1.2.0 installed by templui v1.2.0
// templui component radio - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/radio
package radio

View file

@ -1,4 +1,4 @@
// templui component rating - version: v1.2.0 installed by templui v1.2.0
// templui component rating - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/rating
package rating
@ -166,7 +166,7 @@ func ratingIcon(style Style, filled bool, value float64) templ.Component {
}
iconProps := icon.Props{}
if filled {
iconProps.Fill = "currentColor"
iconProps.Class = "fill-current"
}
switch style {
case StyleHeart:
@ -188,6 +188,10 @@ func (p *Props) setDefaults() {
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/rating.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("rating")
}
}

View file

@ -1,10 +1,8 @@
// templui component selectbox - version: v1.2.0 installed by templui v1.2.0
// templui component selectbox - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/select-box
package selectbox
import (
"context"
"fmt"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/button"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/icon"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
@ -13,10 +11,6 @@ import (
"strconv"
)
type contextKey string
var contentIDKey contextKey = "contentID"
type Props struct {
ID string
Class string
@ -80,170 +74,18 @@ templ SelectBox(props ...Props) {
if len(props) > 0 {
p = props[0]
}
wrapperID := p.ID
if wrapperID == "" {
wrapperID = utils.RandomID()
}
contentID := fmt.Sprintf("%s-content", wrapperID)
ctx = context.WithValue(ctx, contentIDKey, contentID)
}}
<div
id={ wrapperID }
class={ utils.TwMerge("select-container w-full relative", p.Class) }
{ p.Attributes... }
>
{ children... }
</div>
}
templ Trigger(props ...TriggerProps) {
{{
var p TriggerProps
if len(props) > 0 {
p = props[0]
}
contentID, ok := ctx.Value(contentIDKey).(string)
if !ok {
contentID = "fallback-select-content-id"
}
if p.ShowPills {
p.Multiple = true
}
}}
@popover.Trigger(popover.TriggerProps{
For: contentID,
TriggerType: popover.TriggerTypeClick,
}) {
@button.Button(button.Props{
ID: p.ID,
Type: "button",
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Required class for JavaScript
"select-trigger",
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"data-tui-selectbox-content-id": contentID,
"data-tui-selectbox-multiple": strconv.FormatBool(p.Multiple),
"data-tui-selectbox-show-pills": strconv.FormatBool(p.ShowPills),
"data-tui-selectbox-selected-count-text": p.SelectedCountText,
"tabindex": "0",
"aria-invalid": utils.If(p.HasError, "true"),
},
),
}) {
<input
type="hidden"
if p.Name != "" {
name={ p.Name }
}
if p.Form != "" {
form={ p.Form }
}
data-tui-selectbox-hidden-input
{ p.Attributes... }
/>
{ children... }
<span class="pointer-events-none ml-1">
@icon.ChevronDown(icon.Props{
Size: 16,
Class: "text-muted-foreground",
})
</span>
}
}
}
templ Value(props ...ValueProps) {
{{ var p ValueProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("block truncate select-value text-muted-foreground", p.Class) }
if p.Placeholder != "" {
data-tui-selectbox-placeholder={ p.Placeholder }
}
class={ utils.TwMerge("select-container w-full relative", p.Class) }
{ p.Attributes... }
>
if p.Placeholder != "" {
{ p.Placeholder }
}
{ children... }
</span>
}
templ Content(props ...ContentProps) {
{{
var p ContentProps
if len(props) > 0 {
p = props[0]
}
contentID, ok := ctx.Value(contentIDKey).(string)
if !ok {
contentID = "fallback-select-content-id"
}
}}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: popover.PlacementBottomStart,
Offset: 4,
MatchWidth: true,
DisableESC: !p.NoSearch,
Class: utils.TwMerge(
"p-1 select-content z-50 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md",
"min-w-[var(--popover-trigger-width)] w-[var(--popover-trigger-width)]",
p.Class,
),
Attributes: utils.MergeAttributes(
templ.Attributes{
"role": "listbox",
"tabindex": "-1",
},
p.Attributes,
),
Exclusive: true,
}) {
if !p.NoSearch {
<div class="sticky top-0 bg-popover p-1">
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground z-10 pointer-events-none">
@icon.Search(icon.Props{Size: 16})
</span>
@input.Input(input.Props{
Type: input.TypeSearch,
Class: "pl-8",
Placeholder: utils.IfElse(p.SearchPlaceholder != "", p.SearchPlaceholder, "Search..."),
Attributes: templ.Attributes{
"data-tui-selectbox-search": "",
},
})
</div>
</div>
}
<div class="max-h-[300px] overflow-y-auto">
@popover.Root() {
{ children... }
</div>
}
}
</div>
}
templ Group(props ...GroupProps) {
@ -290,10 +132,10 @@ templ Item(props ...ItemProps) {
}
class={
utils.TwMerge(
"select-item relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-light outline-none",
"select-item group relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-light outline-none",
"hover:bg-accent hover:text-accent-foreground",
"focus:bg-accent focus:text-accent-foreground",
utils.If(p.Selected, "bg-accent text-accent-foreground"),
"focus-visible:bg-accent focus-visible:text-accent-foreground",
"data-[tui-selectbox-selected=true]:bg-accent data-[tui-selectbox-selected=true]:text-accent-foreground",
utils.If(p.Disabled, "pointer-events-none opacity-50"),
p.Class,
),
@ -311,16 +153,163 @@ templ Item(props ...ItemProps) {
<span
class={
utils.TwMerge(
"select-check absolute right-2 flex h-3.5 w-3.5 items-center justify-center",
utils.IfElse(p.Selected, "opacity-100", "opacity-0"),
"select-check absolute right-2 flex h-3.5 w-3.5 items-center justify-center opacity-0",
"group-data-[tui-selectbox-selected=true]:opacity-100",
),
}
>
@icon.Check(icon.Props{Size: 16})
@icon.Check(icon.Props{Class: "size-4"})
</span>
</div>
}
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/selectbox.min.js") }></script>
templ Trigger(props ...TriggerProps) {
{{
var p TriggerProps
if len(props) > 0 {
p = props[0]
}
if p.ShowPills {
p.Multiple = true
}
}}
@popover.Trigger(popover.TriggerProps{
TriggerType: popover.TriggerTypeClick,
}) {
@button.Button(button.Props{
ID: p.ID,
Type: "button",
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Required class for JavaScript
"select-trigger",
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(
templ.Attributes{
"data-tui-selectbox-multiple": strconv.FormatBool(p.Multiple),
"data-tui-selectbox-show-pills": strconv.FormatBool(p.ShowPills),
"data-tui-selectbox-selected-count-text": p.SelectedCountText,
"tabindex": "0",
"aria-invalid": utils.If(p.HasError, "true"),
},
),
}) {
<input
type="hidden"
if p.Name != "" {
name={ p.Name }
}
if p.Form != "" {
form={ p.Form }
}
data-tui-selectbox-hidden-input
{ p.Attributes... }
/>
{ children... }
<span
class="ml-1 hidden cursor-pointer text-muted-foreground hover:text-foreground"
data-tui-selectbox-clear-trigger
aria-label="Clear selection"
role="button"
>
@icon.CircleX(icon.Props{Class: "size-3.5"})
</span>
<span class="pointer-events-none ml-1" data-tui-selectbox-chevron>
@icon.ChevronDown(icon.Props{Class: "size-4 text-muted-foreground"})
</span>
}
}
}
templ Value(props ...ValueProps) {
{{ var p ValueProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("block truncate select-value text-muted-foreground", p.Class) }
if p.Placeholder != "" {
data-tui-selectbox-placeholder={ p.Placeholder }
}
{ p.Attributes... }
>
if p.Placeholder != "" {
{ p.Placeholder }
}
{ children... }
</span>
}
templ Content(props ...ContentProps) {
{{ var p ContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Content(popover.ContentProps{
Placement: popover.PlacementBottomStart,
Offset: 4,
DisableESC: !p.NoSearch,
Class: utils.TwMerge(
"p-1 select-content z-50 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md",
p.Class,
),
Attributes: utils.MergeAttributes(
templ.Attributes{
"role": "listbox",
"tabindex": "-1",
"data-tui-selectbox-content": "true",
},
p.Attributes,
),
Exclusive: true,
}) {
if !p.NoSearch {
<div class="sticky top-0 bg-popover p-1">
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground z-10 pointer-events-none">
@icon.Search(icon.Props{Class: "size-4"})
</span>
@input.Input(input.Props{
Type: input.TypeSearch,
Class: "pl-8",
Placeholder: utils.IfElse(p.SearchPlaceholder != "", p.SearchPlaceholder, "Search..."),
Attributes: templ.Attributes{
"data-tui-selectbox-search": "",
},
})
</div>
</div>
}
<div class="max-h-[300px] overflow-y-auto">
{ children... }
</div>
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
@scriptOnce.Once() {
@input.Script()
@popover.Script()
@utils.ComponentScript("selectbox")
}
}

View file

@ -1,4 +1,4 @@
// templui component separator - version: v1.2.0 installed by templui v1.2.0
// templui component separator - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/separator
package separator

View file

@ -1,4 +1,4 @@
// templui component sheet - version: v1.2.0 installed by templui v1.2.0
// templui component sheet - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/sheet
package sheet
@ -37,16 +37,14 @@ type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string // Reference to a specific sheet ID (for external triggers)
For string // Sheet root ID for external triggers
}
type ContentProps struct {
ID string
Class string
Attributes templ.Attributes
HideCloseButton bool
Side Side //
Open bool // Initial open state for standalone usage
Side Side
}
type HeaderProps struct {
@ -140,20 +138,16 @@ templ Content(props ...ContentProps) {
}
// Sheet content uses Dialog content with sheet-specific styles
@dialog.Content(dialog.ContentProps{
ID: p.ID,
Open: p.Open,
HideCloseButton: p.HideCloseButton,
Class: utils.TwMerge(
// First apply side-specific positioning and animations
getSideClasses(p.Side),
// Default gap matching shadcn (no padding in content)
"gap-4 !p-0", // Remove Dialog's p-6 padding
// Move panel layout overrides to the inner dialog panel
"[&_[data-tui-dialog-panel]]:gap-4 [&_[data-tui-dialog-panel]]:!p-0",
// Override Dialog styles
"!scale-100", // Reset Dialog's scale animation
"!rounded-none", // Remove dialog rounded corners
"!opacity-100", // Keep fully opaque - no fade, only slide
// Remove pointer-events control during animation
"!pointer-events-auto data-[tui-dialog-hidden=true]:!pointer-events-none",
// User-provided classes last
p.Class,
),
@ -256,26 +250,26 @@ func getSideClasses(side Side) string {
case SideRight:
return baseClasses +
// Positioning
"!inset-y-0 !right-0 !left-auto !top-auto " +
"!top-0 !bottom-0 !right-0 !left-auto " +
// Size
"h-full w-3/4 sm:max-w-sm " +
"!h-dvh !max-h-dvh w-3/4 sm:!max-w-sm " +
// Border
"border-l border-t-0 border-r-0 border-b-0 " +
// Reset Dialog transforms
"!translate-y-0 " +
"!translate-x-0 !translate-y-0 " +
// Slide animation
"data-[tui-dialog-open=false]:!translate-x-full " +
"data-[tui-dialog-open=true]:!translate-x-0"
case SideLeft:
return baseClasses +
// Positioning
"!inset-y-0 !left-0 !right-auto !top-auto " +
"!top-0 !bottom-0 !left-0 !right-auto " +
// Size
"h-full w-3/4 sm:max-w-sm " +
"!h-dvh !max-h-dvh w-3/4 sm:!max-w-sm " +
// Border
"border-r border-t-0 border-l-0 border-b-0 " +
// Reset Dialog transforms
"!translate-y-0 " +
"!translate-x-0 !translate-y-0 " +
// Slide animation
"data-[tui-dialog-open=false]:!-translate-x-full " +
"data-[tui-dialog-open=true]:!translate-x-0"
@ -308,11 +302,19 @@ func getSideClasses(side Side) string {
default:
return baseClasses +
// Default to right side
"!inset-y-0 !right-0 !left-auto !top-auto " +
"h-full w-3/4 " +
"!top-0 !bottom-0 !right-0 !left-auto " +
"!h-dvh !max-h-dvh w-3/4 sm:!max-w-sm " +
"border-l border-t-0 border-r-0 border-b-0 " +
"!translate-y-0 " +
"!translate-x-0 !translate-y-0 " +
"data-[tui-dialog-open=false]:!translate-x-full " +
"data-[tui-dialog-open=true]:!translate-x-0"
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
@scriptOnce.Once() {
@dialog.Script()
}
}

View file

@ -1,4 +1,4 @@
// templui component sidebar - version: v1.2.0 installed by templui v1.2.0
// templui component sidebar - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/sidebar
package sidebar
@ -47,6 +47,113 @@ type Props struct {
KeyboardShortcut string // default: "b"
}
templ Inset(props ...InsetProps) {
{{ var p InsetProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<main
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"relative flex w-full flex-1 flex-col bg-background",
// Add special styling when peer sidebar has variant="inset"
"md:peer-data-[tui-sidebar-variant=inset]:m-2",
"md:peer-data-[tui-sidebar-variant=inset]:ml-0",
"md:peer-data-[tui-sidebar-variant=inset]:rounded-xl",
"md:peer-data-[tui-sidebar-variant=inset]:shadow-sm",
// When sidebar is collapsed (offcanvas mode) and variant is inset, add left margin back
"md:peer-data-[tui-sidebar-variant=inset]:peer-data-[tui-sidebar-state=collapsed]:peer-data-[tui-sidebar-collapsible=offcanvas]:ml-2",
p.Class,
) }
data-tui-sidebar="inset"
{ p.Attributes... }
>
{ children... }
</main>
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("relative flex w-full min-w-0 flex-col p-2", p.Class) }
data-tui-sidebar="group"
{ p.Attributes... }
>
{ children... }
</div>
}
templ GroupLabel(props ...GroupLabelProps) {
{{ var p GroupLabelProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70",
"ring-sidebar-ring outline-none transition-[margin,opacity] duration-200 ease-linear",
"focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:-mt-8 group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:opacity-0",
p.Class,
) }
data-tui-sidebar="group-label"
{ p.Attributes... }
>
{ children... }
</div>
}
templ MenuBadge(props ...MenuBadgeProps) {
{{ var p MenuBadgeProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"ml-auto flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium",
"bg-sidebar-accent text-sidebar-accent-foreground",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:hidden",
p.Class,
) }
data-tui-sidebar="menu-badge"
{ p.Attributes... }
>
{ children... }
</span>
}
templ Separator(props ...SeparatorProps) {
{{ var p SeparatorProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<hr
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"mx-2 my-2 border-t border-sidebar-border",
p.Class,
) }
data-tui-sidebar="separator"
{ p.Attributes... }
/>
}
type TriggerProps struct {
ID string
Class string
@ -453,9 +560,7 @@ templ MenuButton(props ...MenuButtonProps) {
// When collapsed to icon mode - show with tooltip
<div class="group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:block hidden">
@tooltip.Tooltip() {
@tooltip.Trigger(tooltip.TriggerProps{
For: tooltipID,
}) {
@tooltip.Trigger(tooltip.TriggerProps{}) {
@menuButtonContent(p, "") {
{ children... }
}
@ -641,113 +746,12 @@ templ MenuSubButton(props ...MenuSubButtonProps) {
}
}
templ Inset(props ...InsetProps) {
{{ var p InsetProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<main
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"relative flex w-full flex-1 flex-col bg-background",
// Add special styling when peer sidebar has variant="inset"
"md:peer-data-[tui-sidebar-variant=inset]:m-2",
"md:peer-data-[tui-sidebar-variant=inset]:ml-0",
"md:peer-data-[tui-sidebar-variant=inset]:rounded-xl",
"md:peer-data-[tui-sidebar-variant=inset]:shadow-sm",
// When sidebar is collapsed (offcanvas mode) and variant is inset, add left margin back
"md:peer-data-[tui-sidebar-variant=inset]:peer-data-[tui-sidebar-state=collapsed]:peer-data-[tui-sidebar-collapsible=offcanvas]:ml-2",
p.Class,
) }
data-tui-sidebar="inset"
{ p.Attributes... }
>
{ children... }
</main>
}
templ Group(props ...GroupProps) {
{{ var p GroupProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("relative flex w-full min-w-0 flex-col p-2", p.Class) }
data-tui-sidebar="group"
{ p.Attributes... }
>
{ children... }
</div>
}
templ GroupLabel(props ...GroupLabelProps) {
{{ var p GroupLabelProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70",
"ring-sidebar-ring outline-none transition-[margin,opacity] duration-200 ease-linear",
"focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:-mt-8 group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:opacity-0",
p.Class,
) }
data-tui-sidebar="group-label"
{ p.Attributes... }
>
{ children... }
</div>
}
templ MenuBadge(props ...MenuBadgeProps) {
{{ var p MenuBadgeProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<span
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"ml-auto flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium",
"bg-sidebar-accent text-sidebar-accent-foreground",
"group-data-[tui-sidebar-state=collapsed]:group-data-[tui-sidebar-collapsible=icon]:hidden",
p.Class,
) }
data-tui-sidebar="menu-badge"
{ p.Attributes... }
>
{ children... }
</span>
}
templ Separator(props ...SeparatorProps) {
{{ var p SeparatorProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<hr
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge(
"mx-2 my-2 border-t border-sidebar-border",
p.Class,
) }
data-tui-sidebar="separator"
{ p.Attributes... }
/>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/sidebar.min.js") }></script>
@scriptOnce.Once() {
@sheet.Script()
@tooltip.Script()
@utils.ComponentScript("sidebar")
}
}

View file

@ -1,4 +1,4 @@
// templui component skeleton - version: v1.2.0 installed by templui v1.2.0
// templui component skeleton - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/skeleton
package skeleton

View file

@ -1,4 +1,4 @@
// templui component slider - version: v1.2.0 installed by templui v1.2.0
// templui component slider - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/slider
package slider
@ -116,6 +116,10 @@ templ Value(props ...ValueProps) {
</span>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/slider.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("slider")
}
}

View file

@ -1,4 +1,4 @@
// templui component switch - version: v1.2.0 installed by templui v1.2.0
// templui component switch - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/switch
package switchcomp

View file

@ -1,4 +1,4 @@
// templui component table - version: v1.2.0 installed by templui v1.2.0
// templui component table - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/table
package table

View file

@ -1,4 +1,4 @@
// templui component tabs - version: v1.2.0 installed by templui v1.2.0
// templui component tabs - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/tabs
package tabs
@ -158,6 +158,10 @@ func IDFromContext(ctx context.Context) string {
return ""
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/tabs.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("tabs")
}
}

View file

@ -1,10 +1,11 @@
// templui component tagsinput - version: v1.2.0 installed by templui v1.2.0
// templui component tagsinput - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/tags-input
package tagsinput
import (
"git.juancwu.dev/juancwu/budgit/internal/ui/components/badge"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/input"
"git.juancwu.dev/juancwu/budgit/internal/ui/components/popover"
"git.juancwu.dev/juancwu/budgit/internal/ui/utils"
)
@ -19,19 +20,26 @@ type Props struct {
Attributes templ.Attributes
Disabled bool
Readonly bool
Suggestions []string
}
templ TagsInput(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{
var p Props
if len(props) > 0 {
p = props[0]
}
if p.ID == "" {
p.ID = utils.RandomID()
}
suggestionsID := p.ID + "-suggestions"
}}
<div
id={ p.ID + "-container" }
class={
utils.TwMerge(
// Base styles
"flex items-center flex-wrap gap-2 p-2 rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
"flex flex-col gap-2 p-2 rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Focus styles
@ -48,9 +56,13 @@ templ TagsInput(props ...Props) {
data-tui-tagsinput
data-tui-tagsinput-name={ p.Name }
data-tui-tagsinput-form={ p.Form }
if len(p.Suggestions) > 0 {
data-tui-tagsinput-suggestions-id={ suggestionsID }
}
{ p.Attributes... }
>
<div class="flex items-center flex-wrap gap-2" data-tui-tagsinput-container>
<!-- Tags row (hidden when empty) -->
<div class="flex flex-wrap gap-2 empty:hidden" data-tui-tagsinput-chips>
for _, tag := range p.Value {
@badge.Badge(badge.Props{
Attributes: templ.Attributes{"data-tui-tagsinput-chip": ""},
@ -69,19 +81,65 @@ templ TagsInput(props ...Props) {
}
}
</div>
@input.Input(input.Props{
ID: p.ID,
Class: "border-0 shadow-none focus-visible:ring-0 h-auto py-0 px-0 bg-transparent rounded-none min-h-0 disabled:opacity-100 dark:bg-transparent",
Type: input.TypeText,
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Readonly: p.Readonly,
Attributes: utils.MergeAttributes(
templ.Attributes{"data-tui-tagsinput-text-input": ""},
p.Attributes,
),
})
<div data-tui-tagsinput-hidden-inputs>
<!-- Input row -->
if len(p.Suggestions) > 0 {
@popover.Root(popover.RootProps{
ID: suggestionsID,
}) {
@popover.Trigger(popover.TriggerProps{
TriggerType: popover.TriggerTypeManual,
}) {
<div class="relative w-full">
@input.Input(input.Props{
ID: p.ID,
Class: "border-0 shadow-none focus-visible:ring-0 h-auto py-0 px-0 bg-transparent rounded-none min-h-0 disabled:opacity-100 dark:bg-transparent w-full",
Type: input.TypeText,
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Readonly: p.Readonly,
Attributes: utils.MergeAttributes(
templ.Attributes{"data-tui-tagsinput-text-input": ""},
p.Attributes,
),
})
</div>
}
@popover.Content(popover.ContentProps{
Placement: popover.PlacementBottomStart,
DisableClickAway: true,
Class: "p-1 max-h-[200px] overflow-y-auto min-w-[var(--trigger-width,12rem)] w-[var(--trigger-width,12rem)]",
Attributes: templ.Attributes{
"data-tui-tagsinput-suggestions-content": "true",
},
}) {
for _, suggestion := range p.Suggestions {
<div
class="relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 px-2 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
data-tui-tagsinput-suggestion
data-tui-tagsinput-suggestion-value={ suggestion }
>
{ suggestion }
</div>
}
}
}
} else {
<div class="relative w-full">
@input.Input(input.Props{
ID: p.ID,
Class: "border-0 shadow-none focus-visible:ring-0 h-auto py-0 px-0 bg-transparent rounded-none min-h-0 disabled:opacity-100 dark:bg-transparent w-full",
Type: input.TypeText,
Placeholder: p.Placeholder,
Disabled: p.Disabled,
Readonly: p.Readonly,
Attributes: utils.MergeAttributes(
templ.Attributes{"data-tui-tagsinput-text-input": ""},
p.Attributes,
),
})
</div>
}
<div data-tui-tagsinput-hidden-inputs class="hidden">
for _, tag := range p.Value {
<input type="hidden" name={ p.Name } value={ tag }/>
}
@ -89,6 +147,11 @@ templ TagsInput(props ...Props) {
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/tagsinput.min.js") }></script>
@scriptOnce.Once() {
@popover.Script()
@utils.ComponentScript("tagsinput")
}
}

View file

@ -1,4 +1,4 @@
// templui component textarea - version: v1.2.0 installed by templui v1.2.0
// templui component textarea - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/textarea
package textarea
@ -80,6 +80,10 @@ templ Textarea(props ...Props) {
>{ p.Value }</textarea>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/textarea.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("textarea")
}
}

View file

@ -1,4 +1,4 @@
// templui component timepicker - version: v1.2.0 installed by templui v1.2.0
// templui component timepicker - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/time-picker
package timepicker
@ -56,7 +56,6 @@ templ TimePicker(props ...Props) {
p.Step = 1
}
var contentID = p.ID + "-content"
var valueString string
if p.Value != (time.Time{}) {
valueString = p.Value.Format("15:04")
@ -70,181 +69,185 @@ templ TimePicker(props ...Props) {
maxTimeString = p.MaxTime.Format("15:04")
}
}}
<div class="relative inline-block w-full">
<input
type="hidden"
name={ p.Name }
value={ valueString }
if p.Form != "" {
form={ p.Form }
}
id={ p.ID + "-hidden" }
data-tui-timepicker-hidden-input="true"
/>
@popover.Trigger(popover.TriggerProps{For: contentID}) {
@button.Button(button.Props{
ID: p.ID,
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(p.Attributes, templ.Attributes{
"data-tui-timepicker": "true",
"data-tui-timepicker-use12hours": fmt.Sprintf("%t", p.Use12Hours),
"data-tui-timepicker-am-label": p.AMLabel,
"data-tui-timepicker-pm-label": p.PMLabel,
"data-tui-timepicker-placeholder": p.Placeholder,
"data-tui-timepicker-step": fmt.Sprintf("%d", p.Step),
"data-tui-timepicker-min-time": minTimeString,
"data-tui-timepicker-max-time": maxTimeString,
"aria-invalid": utils.If(p.HasError, "true"),
}),
}) {
<span data-tui-timepicker-display class="text-left grow text-muted-foreground">
{ p.Placeholder }
</span>
<span class="text-muted-foreground flex items-center ml-2">
@icon.Clock(icon.Props{Size: 16})
</span>
}
}
@popover.Content(popover.ContentProps{
ID: contentID,
Placement: popover.PlacementBottomStart,
Class: "p-0 w-80",
}) {
@card.Card(card.Props{
Class: "border-0 shadow-none",
}) {
@card.Content(card.ContentProps{
Class: "p-4",
<div class="relative inline-block w-full" data-tui-timepicker-root>
@popover.Root() {
<input
type="hidden"
name={ p.Name }
value={ valueString }
if p.Form != "" {
form={ p.Form }
}
data-tui-timepicker-hidden-input="true"
/>
@popover.Trigger() {
@button.Button(button.Props{
ID: p.ID,
Variant: button.VariantOutline,
Class: utils.TwMerge(
// Base styles matching input
"w-full h-9 px-3 py-1 text-base md:text-sm",
"flex items-center justify-between",
"rounded-md border border-input bg-transparent shadow-xs transition-[color,box-shadow] outline-none",
// Dark mode background
"dark:bg-input/30",
// Selection styles
"selection:bg-primary selection:text-primary-foreground",
// Focus styles
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
// Error/Invalid styles
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive dark:aria-invalid:ring-destructive/40",
utils.If(p.HasError, "border-destructive ring-destructive/20 dark:ring-destructive/40"),
p.Class,
),
Disabled: p.Disabled,
Attributes: utils.MergeAttributes(p.Attributes, templ.Attributes{
"data-tui-timepicker": "true",
"data-tui-timepicker-use12hours": fmt.Sprintf("%t", p.Use12Hours),
"data-tui-timepicker-am-label": p.AMLabel,
"data-tui-timepicker-pm-label": p.PMLabel,
"data-tui-timepicker-placeholder": p.Placeholder,
"data-tui-timepicker-step": fmt.Sprintf("%d", p.Step),
"data-tui-timepicker-min-time": minTimeString,
"data-tui-timepicker-max-time": maxTimeString,
"aria-invalid": utils.If(p.HasError, "true"),
}),
}) {
<div
data-tui-timepicker-popup="true"
data-tui-timepicker-input-name={ p.Name }
data-tui-timepicker-parent-id={ p.ID }
if valueString != "" {
data-tui-timepicker-value={ valueString }
}
>
// Time selection grid
<div class="grid grid-cols-2 gap-3 mb-4">
// Hour selection
<div class="space-y-2">
<label class="text-sm font-medium">Hour</label>
<div class="max-h-32 overflow-y-auto border rounded-md bg-background">
<div data-tui-timepicker-hour-list="true" class="p-1 space-y-0.5">
if p.Use12Hours {
// 12-hour format: 12, 01-11
<button
type="button"
data-tui-timepicker-hour="0"
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
12
</button>
for hour := 1; hour <= 11; hour++ {
<span data-tui-timepicker-display class="text-left grow text-muted-foreground">
{ p.Placeholder }
</span>
<span class="text-muted-foreground flex items-center ml-2">
@icon.Clock(icon.Props{Class: "size-4"})
</span>
}
}
@popover.Content(popover.ContentProps{
Placement: popover.PlacementBottomStart,
Class: "p-0 w-80",
}) {
@card.Card(card.Props{
Class: "border-0 shadow-none",
}) {
@card.Content(card.ContentProps{
Class: "p-4",
}) {
<div
data-tui-timepicker-popup="true"
data-tui-timepicker-input-name={ p.Name }
if valueString != "" {
data-tui-timepicker-value={ valueString }
}
>
// Time selection grid
<div class="grid grid-cols-2 gap-3 mb-4">
// Hour selection
<div class="space-y-2">
<label class="text-sm font-medium">Hour</label>
<div class="max-h-32 overflow-y-auto border rounded-md bg-background">
<div data-tui-timepicker-hour-list="true" class="p-1 space-y-0.5">
if p.Use12Hours {
// 12-hour format: 12, 01-11
<button
type="button"
data-tui-timepicker-hour={ strconv.Itoa(hour) }
data-tui-timepicker-hour="0"
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", hour) }
12
</button>
for hour := 1; hour <= 11; hour++ {
<button
type="button"
data-tui-timepicker-hour={ strconv.Itoa(hour) }
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", hour) }
</button>
}
} else {
// 24-hour format: 00-23
for hour := 0; hour < 24; hour++ {
<button
type="button"
data-tui-timepicker-hour={ strconv.Itoa(hour) }
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", hour) }
</button>
}
}
} else {
// 24-hour format: 00-23
for hour := 0; hour < 24; hour++ {
</div>
</div>
</div>
// Minute selection
<div class="space-y-2">
<label class="text-sm font-medium">Minute</label>
<div class="max-h-32 overflow-y-auto border rounded-md bg-background">
<div data-tui-timepicker-minute-list="true" class="p-1 space-y-0.5">
for minute := 0; minute < 60; minute += p.Step {
<button
type="button"
data-tui-timepicker-hour={ strconv.Itoa(hour) }
data-tui-timepicker-minute={ strconv.Itoa(minute) }
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", hour) }
{ fmt.Sprintf("%02d", minute) }
</button>
}
}
</div>
</div>
</div>
</div>
// Minute selection
<div class="space-y-2">
<label class="text-sm font-medium">Minute</label>
<div class="max-h-32 overflow-y-auto border rounded-md bg-background">
<div data-tui-timepicker-minute-list="true" class="p-1 space-y-0.5">
for minute := 0; minute < 60; minute += p.Step {
<button
type="button"
data-tui-timepicker-minute={ strconv.Itoa(minute) }
data-tui-timepicker-selected="false"
class="w-full px-2 py-1 text-sm rounded transition-colors text-left hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-selected=true]:bg-primary data-[tui-timepicker-selected=true]:text-primary-foreground data-[tui-timepicker-selected=true]:hover:bg-primary/90"
>
{ fmt.Sprintf("%02d", minute) }
</button>
}
// AM/PM selector and action buttons
<div class="flex justify-between items-center">
if p.Use12Hours {
<div class="flex gap-1">
<button
type="button"
data-tui-timepicker-period="AM"
data-tui-timepicker-active="false"
class="px-3 py-1 text-sm rounded-md border transition-colors hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-active=true]:bg-primary data-[tui-timepicker-active=true]:text-primary-foreground data-[tui-timepicker-active=true]:hover:bg-primary/90"
>
{ p.AMLabel }
</button>
<button
type="button"
data-tui-timepicker-period="PM"
data-tui-timepicker-active="false"
class="px-3 py-1 text-sm rounded-md border transition-colors hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-active=true]:bg-primary data-[tui-timepicker-active=true]:text-primary-foreground data-[tui-timepicker-active=true]:hover:bg-primary/90"
>
{ p.PMLabel }
</button>
</div>
</div>
} else {
<div></div>
}
@button.Button(button.Props{
Type: "button",
Variant: button.VariantSecondary,
Size: button.SizeSm,
Attributes: templ.Attributes{
"data-tui-timepicker-done": "true",
},
}) {
Done
}
</div>
</div>
// AM/PM selector and action buttons
<div class="flex justify-between items-center">
if p.Use12Hours {
<div class="flex gap-1">
<button
type="button"
data-tui-timepicker-period="AM"
data-tui-timepicker-active="false"
class="px-3 py-1 text-sm rounded-md border transition-colors hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-active=true]:bg-primary data-[tui-timepicker-active=true]:text-primary-foreground data-[tui-timepicker-active=true]:hover:bg-primary/90"
>
{ p.AMLabel }
</button>
<button
type="button"
data-tui-timepicker-period="PM"
data-tui-timepicker-active="false"
class="px-3 py-1 text-sm rounded-md border transition-colors hover:bg-accent hover:text-accent-foreground data-[tui-timepicker-active=true]:bg-primary data-[tui-timepicker-active=true]:text-primary-foreground data-[tui-timepicker-active=true]:hover:bg-primary/90"
>
{ p.PMLabel }
</button>
</div>
} else {
<div></div>
}
@button.Button(button.Props{
Type: "button",
Variant: button.VariantSecondary,
Size: button.SizeSm,
Attributes: templ.Attributes{
"data-tui-timepicker-done": "true",
},
}) {
Done
}
</div>
</div>
}
}
}
}
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/timepicker.min.js") }></script>
@scriptOnce.Once() {
@popover.Script()
@utils.ComponentScript("timepicker")
}
}

View file

@ -1,4 +1,4 @@
// templui component toast - version: v1.2.0 installed by templui v1.2.0
// templui component toast - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/toast
package toast
@ -109,13 +109,13 @@ templ Toast(props ...Props) {
if p.Icon {
switch p.Variant {
case VariantSuccess:
@icon.CircleCheck(icon.Props{Size: 22, Class: "text-green-500 mr-3 flex-shrink-0"})
@icon.CircleCheck(icon.Props{Class: "size-[22px] text-green-500 mr-3 flex-shrink-0"})
case VariantError:
@icon.CircleX(icon.Props{Size: 22, Class: "text-red-500 mr-3 flex-shrink-0"})
@icon.CircleX(icon.Props{Class: "size-[22px] text-red-500 mr-3 flex-shrink-0"})
case VariantWarning:
@icon.TriangleAlert(icon.Props{Size: 22, Class: "text-yellow-500 mr-3 flex-shrink-0"})
@icon.TriangleAlert(icon.Props{Class: "size-[22px] text-yellow-500 mr-3 flex-shrink-0"})
case VariantInfo:
@icon.Info(icon.Props{Size: 22, Class: "text-blue-500 mr-3 flex-shrink-0"})
@icon.Info(icon.Props{Class: "size-[22px] text-blue-500 mr-3 flex-shrink-0"})
}
}
// Content
@ -138,16 +138,17 @@ templ Toast(props ...Props) {
"type": "button",
},
}) {
@icon.X(icon.Props{
Size: 18,
Class: "opacity-75 hover:opacity-100",
})
@icon.X(icon.Props{Class: "size-[18px] opacity-75 hover:opacity-100"})
}
}
</div>
</div>
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
<script defer nonce={ templ.GetNonce(ctx) } src={ utils.ScriptURL("/assets/js/toast.min.js") }></script>
@scriptOnce.Once() {
@utils.ComponentScript("toast")
}
}

View file

@ -1,4 +1,4 @@
// templui component tooltip - version: v1.2.0 installed by templui v1.2.0
// templui component tooltip - version: v1.9.5 installed by templui v1.9.5
// 📚 Documentation: https://templui.io/docs/components/tooltip
package tooltip
@ -16,7 +16,6 @@ const (
PositionLeft Position = "left"
)
// Map tooltip positions to popover positions
func mapTooltipPositionToPopover(position Position) popover.Placement {
switch position {
case PositionTop:
@ -42,7 +41,6 @@ type TriggerProps struct {
ID string
Class string
Attributes templ.Attributes
For string
}
type ContentProps struct {
@ -56,7 +54,17 @@ type ContentProps struct {
}
templ Tooltip(props ...Props) {
{ children... }
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
@popover.Root(popover.RootProps{
ID: p.ID,
Class: p.Class,
Attributes: p.Attributes,
}) {
{ children... }
}
}
templ Trigger(props ...TriggerProps) {
@ -69,7 +77,6 @@ templ Trigger(props ...TriggerProps) {
Class: p.Class,
Attributes: p.Attributes,
TriggerType: popover.TriggerTypeHover,
For: p.For,
}) {
{ children... }
}
@ -92,3 +99,11 @@ templ Content(props ...ContentProps) {
{ children... }
}
}
var scriptOnce = templ.NewOnceHandle()
templ Script() {
@scriptOnce.Once() {
@popover.Script()
}
}

View file

@ -156,7 +156,7 @@ templ AppSidebarDropdown(user *model.User) {
Href: "/app/settings",
}) {
<span class="flex items-center">
@icon.Settings(icon.Props{Size: 16, Class: "mr-2"})
@icon.Settings(icon.Props{Class: "mr-2"})
Settings
</span>
}
@ -168,7 +168,7 @@ templ AppSidebarDropdown(user *model.User) {
},
}) {
<span class="flex items-center">
@icon.LogOut(icon.Props{Size: 16, Class: "mr-2"})
@icon.LogOut(icon.Props{Class: "mr-2"})
Log out
</span>
}

View file

@ -1,13 +1,19 @@
// templui util templui.go - version: v0.101.0 installed by templui v0.101.0
// templui util templui.go - version: v1.9.5 installed by templui v1.9.5
package utils
import (
"context"
"crypto/rand"
"fmt"
"io"
"io/fs"
"net/http"
"path"
"strings"
"time"
"crypto/rand"
"github.com/a-h/templ"
"github.com/templui/templui/components"
twmerge "github.com/Oudwins/tailwind-merge-go"
)
@ -18,9 +24,9 @@ func TwMerge(classes ...string) string {
return twmerge.Merge(classes...)
}
// TwIf returns value if condition is true, otherwise an empty value of type T.
// If returns value if condition is true, otherwise the zero value of T.
// Example: true, "bg-red-500" → "bg-red-500"
func If[T comparable](condition bool, value T) T {
func If[T any](condition bool, value T) T {
var empty T
if condition {
return value
@ -28,7 +34,7 @@ func If[T comparable](condition bool, value T) T {
return empty
}
// TwIfElse returns trueValue if condition is true, otherwise falseValue.
// IfElse returns trueValue if condition is true, otherwise falseValue.
// Example: true, "bg-red-500", "bg-gray-300" → "bg-red-500"
func IfElse[T any](condition bool, trueValue T, falseValue T) T {
if condition {
@ -56,7 +62,7 @@ func RandomID() string {
}
// ScriptVersion is a timestamp generated at app start for cache busting.
// Used in Script() templates to append ?v=<timestamp> to script URLs.
// Used in component script tags to append ?v=<timestamp> to script URLs.
var ScriptVersion = fmt.Sprintf("%d", time.Now().Unix())
// ScriptURL generates cache-busted script URLs.
@ -72,3 +78,85 @@ var ScriptVersion = fmt.Sprintf("%d", time.Now().Unix())
var ScriptURL = func(path string) string {
return path + "?v=" + ScriptVersion
}
// componentScriptBasePath is the base public path for component JavaScript files.
// In the import workflow this stays "/templui/js". The CLI rewrites it to the user's local jsPublicPath.
var componentScriptBasePath = "/assets/js"
// UseUnminifiedScripts switches component script loading to the unminified files.
// Leave this false in normal use and set it to true during app startup for debugging.
var UseUnminifiedScripts = false
// ComponentScript renders a deferred script tag for a component JavaScript file.
// Example: ComponentScript("datepicker") → <script defer src="/templui/js/datepicker.min.js?..."></script>
func ComponentScript(component string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
nonce := templ.GetNonce(ctx)
fileName := component + ".min.js"
if UseUnminifiedScripts {
fileName = component + ".js"
}
src := ScriptURL(componentScriptBasePath + "/" + fileName)
if _, err := io.WriteString(w, `<script type="module"`); err != nil {
return err
}
if nonce != "" {
if _, err := io.WriteString(w, ` nonce="`); err != nil {
return err
}
if _, err := io.WriteString(w, templ.EscapeString(nonce)); err != nil {
return err
}
if _, err := io.WriteString(w, `"`); err != nil {
return err
}
}
if _, err := io.WriteString(w, ` src="`); err != nil {
return err
}
if _, err := io.WriteString(w, templ.EscapeString(src)); err != nil {
return err
}
if _, err := io.WriteString(w, `"></script>`); err != nil {
return err
}
return nil
})
}
// SetupScriptRoutes serves embedded component JavaScript files for the import workflow.
// Example: SetupScriptRoutes(mux, true) mounts /templui/js/*.js with no-store caching in development.
func SetupScriptRoutes(mux *http.ServeMux, isDevelopment bool) {
if mux == nil || componentScriptBasePath != "/templui/js" {
return
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
urlPath := strings.TrimPrefix(r.URL.Path, "/templui/js/")
if urlPath == r.URL.Path || urlPath == "" || strings.Contains(urlPath, "..") {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/javascript")
if isDevelopment {
w.Header().Set("Cache-Control", "no-store")
} else {
w.Header().Set("Cache-Control", "public, max-age=31536000")
}
fileName := path.Base(urlPath)
component := strings.TrimSuffix(fileName, ".min.js")
component = strings.TrimSuffix(component, ".js")
file, err := fs.ReadFile(components.TemplFiles, path.Join(component, fileName))
if err != nil {
http.NotFound(w, r)
return
}
_, _ = w.Write(file)
})
mux.Handle("GET /templui/js/", handler)
}