chore: update templ and templui
This commit is contained in:
parent
b5d195baea
commit
61eaa268ab
89 changed files with 25776 additions and 8231 deletions
|
|
@ -25,6 +25,6 @@ templ ThemeSwitcher(props ...ThemeSwitcherProps) {
|
|||
"aria-label": "Toggle theme",
|
||||
},
|
||||
}) {
|
||||
@icon.Eclipse(icon.Props{Size: 20})
|
||||
@icon.Eclipse(icon.Props{Class: ""})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue