alerts, toast amd breadcrum

This commit is contained in:
JACS 2026-05-03 00:06:23 -05:00
parent aa651083cd
commit 1f95f86829
12 changed files with 525 additions and 1 deletions

View file

@ -69,6 +69,31 @@ func TablerNavbar(c *core.Context) *core.Response {
return c.Response.Template("tabler_default.html", data) return c.Response.Template("tabler_default.html", data)
} }
// TablerComponents renders a page with alerts, breadcrumbs, and toasts.
// Uses the composition pattern: embeds TablerPageData and adds Components.
func TablerComponents(c *core.Context) *core.Response {
type componentsPageData struct {
TablerPageData
Components FormtablerComponentsPage
}
data := componentsPageData{
TablerPageData: TablerPageData{
PageTitle: "UI Components",
PageDescription: "Alerts, breadcrumbs and toasts demo",
ShowTopbar: true,
Sidebar: false,
PageHeader: "Components",
PagePretitle: "UI Elements",
UserName: "Jane Doe",
UserRole: "Administrator",
NavbarMenu: SampleNavbarMenu(),
Content: template.HTML(""),
},
Components: SampleComponents(),
}
return c.Response.Template("tabler_components.html", data)
}
// TablerCards renders a page with card component demos. // TablerCards renders a page with card component demos.
// Uses the composition pattern: embeds TablerPageData and adds Cards. // Uses the composition pattern: embeds TablerPageData and adds Cards.
func TablerCards(c *core.Context) *core.Response { func TablerCards(c *core.Context) *core.Response {

View file

@ -44,6 +44,103 @@ func SampleNavbarMenu() TablerMenu {
} }
} }
// SampleComponents returns sample data for alerts, breadcrumbs, and toasts.
func SampleComponents() FormtablerComponentsPage {
return FormtablerComponentsPage{
Alerts: []FormtablerAlert{
{
Type: "success",
Title: "Success alert!",
ShowClose: true,
},
{
Type: "warning",
Title: "Warning alert with description",
Description: "This is a warning alert with additional description text to provide more context to the user.",
List: []string{"Item one is important", "Item two requires attention", "Item three is optional"},
},
{
Type: "danger",
Title: "Danger alert",
Action: "Undo",
Buttons: true,
},
{
Type: "info",
Title: "Info alert with link",
Link: "Learn more",
ShowClose: true,
},
{
Type: "success",
Title: "Important alert",
Important: true,
ShowClose: true,
},
{
Type: "warning",
Title: "Minor alert variant",
Minor: true,
},
},
Breadcrumbs: []FormtablerBreadcrumb{
{
Items: []FormtablerBreadcrumbItem{
{Title: "Home", Link: "/", HomeIcon: true},
{Title: "Library", Link: "/library"},
{Title: "Data"},
},
},
{
Separator: "arrows",
Items: []FormtablerBreadcrumbItem{
{Title: "Dashboard", Link: "/"},
{Title: "Components", Link: "/components"},
{Title: "Alerts"},
},
},
{
Separator: "dots",
Items: []FormtablerBreadcrumbItem{
{Title: "Home", Link: "/", HomeIcon: true},
{Title: "Settings", Link: "/settings"},
{Title: "Profile"},
},
},
},
Toasts: []FormtablerToast{
{
ID: "simple",
Show: true,
PersonName: "Paweł Kuna",
PersonSrc: "/static/avatars/000m.jpg",
Date: "2 mins ago",
Text: "Hello, world! This is a toast message.",
},
{
ID: "avatar-toast",
Show: true,
PersonName: "Jeffie Lewzey",
PersonSrc: "/static/avatars/052f.jpg",
Date: "5 mins ago",
Text: "Your report has been generated successfully.",
},
{
ID: "cookies",
Show: true,
Date: "just now",
Cookies: true,
},
{
ID: "no-header",
Show: true,
HideHeader: true,
Text: "This toast has no header — just a plain message body.",
},
},
}
}
// SampleCards returns 6 sample cards showing different variants. // SampleCards returns 6 sample cards showing different variants.
func SampleCards() []FormtablerCard { func SampleCards() []FormtablerCard {
defaultBody := &FormtablerCardBody{ defaultBody := &FormtablerCardBody{

View file

@ -369,6 +369,48 @@ type FormtablerCardBody struct {
ButtonLink string ButtonLink string
} }
// FormtablerAlert represents a single alert component.
type FormtablerAlert struct {
Type string // "success", "warning", "danger", "info"
Title string
Description string
List []string // if set, renders as <ul class="alert-list">
Important bool
Minor bool
ShowClose bool
Action string // "Action" button text
Link string // "Link" anchor text
Buttons bool // "Okay" / "Cancel" buttons
Avatar bool
Class string
}
// FormtablerBreadcrumbItem represents a single item in the breadcrumb trail.
type FormtablerBreadcrumbItem struct {
Title string
Link string // empty for the last (active) item
HomeIcon bool // render home icon instead of text for the first item
}
// FormtablerBreadcrumb represents a breadcrumb navigation component.
type FormtablerBreadcrumb struct {
Items []FormtablerBreadcrumbItem
Separator string // optional: "dots", "arrows", "bullets"
Class string
}
// FormtablerToast represents a toast notification component.
type FormtablerToast struct {
ID string
Show bool
HideHeader bool
PersonName string
PersonSrc string
Date string
Text string
Cookies bool // renders cookie consent variant
}
// FormtablerCardProgress represents a progress bar in the card. // FormtablerCardProgress represents a progress bar in the card.
type FormtablerCardProgress struct { type FormtablerCardProgress struct {
Percent int Percent int
@ -431,6 +473,14 @@ type FormtablerFormElementsPage struct {
ValidationStates FormtablerValidationStates ValidationStates FormtablerValidationStates
} }
// FormtablerComponentsPage is the page-specific struct for the combined demo
// showing alerts, toasts, and breadcrumbs.
type FormtablerComponentsPage struct {
Alerts []FormtablerAlert
Breadcrumbs []FormtablerBreadcrumb
Toasts []FormtablerToast
}
// TablerPageData holds the common data for all tabler pages. // TablerPageData holds the common data for all tabler pages.
// It should NOT contain component-specific fields like tables or forms. // It should NOT contain component-specific fields like tables or forms.
// Add those by creating a page-specific struct that embeds TablerPageData // Add those by creating a page-specific struct that embeds TablerPageData

View file

@ -69,8 +69,9 @@ func registerRoutes() {
controller.Get("/tablerdefault", controllers.TablerDefault) controller.Get("/tablerdefault", controllers.TablerDefault)
controller.Get("/tablerhome", controllers.TablerHome) controller.Get("/tablerhome", controllers.TablerHome)
controller.Get("/tablernavmenu", controllers.TablerNavbar) controller.Get("/tablernavmenu", controllers.TablerNavbar)
controller.Get("/tablertable", controllers.TablerTables) controller.Get("/tablertable", controllers.TablerTables)
controller.Get("/tablerformelements", controllers.TablerFormElements) controller.Get("/tablerformelements", controllers.TablerFormElements)
controller.Get("/tablercards", controllers.TablerCards) controller.Get("/tablercards", controllers.TablerCards)
controller.Get("/tablercomponents", controllers.TablerComponents)
} }

View file

@ -0,0 +1,40 @@
{{define "tabler_alert"}}
{{$a := .}}
{{$icon := $a.Type}}
{{if eq $a.Type "success"}}{{$icon = "check"}}{{else if eq $a.Type "warning"}}{{$icon = "alert-triangle"}}{{else if eq $a.Type "danger"}}{{$icon = "alert-circle"}}{{else if eq $a.Type "info"}}{{$icon = "info-circle"}}{{end}}
<div class="alert{{if $a.Important}} alert-important{{else if $a.Minor}} alert-minor{{end}} alert-{{$a.Type}}{{if $a.ShowClose}} alert-dismissible{{end}}{{if $a.Avatar}} alert-avatar{{end}}{{if $a.Class}} {{$a.Class}}{{end}}" role="alert">
<div class="alert-icon">
<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="icon alert-icon">
{{if eq $icon "check"}}<path d="M5 12l5 5l10 -10"></path>
{{else if eq $icon "alert-triangle"}}<path d="M12 9v4"></path><path d="M10.363 3.591l-8.106 13.534a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636 -2.871l-8.106 -13.534a1.914 1.914 0 0 0 -3.274 0z"></path><path d="M12 16h.01"></path>
{{else if eq $icon "alert-circle"}}<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 8v4"></path><path d="M12 16h.01"></path>
{{else if eq $icon "info-circle"}}<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path><path d="M12 16v-4"></path><path d="M12 8h.01"></path>
{{end}}
</svg>
</div>
{{if or $a.Description $a.List}}
<div>
<h4 class="alert-heading">{{defaultVal "This is a custom alert box!" $a.Title}}</h4>
<div class="alert-description">
{{$a.Description}}
{{if $a.List}}
<ul class="alert-list">
{{range $a.List}}<li>{{.}}</li>{{end}}
</ul>
{{end}}
</div>
</div>
{{else}}
{{defaultVal "This is a custom alert box!" $a.Title}}
{{if $a.Action}}<a href="#" class="alert-action">{{$a.Action}}</a>{{end}}
{{if $a.Link}}<a href="#" class="alert-link">{{$a.Link}}</a>{{end}}
{{end}}
{{if $a.Buttons}}
<div class="btn-list">
<a href="#" class="btn btn-{{$a.Type}}">Okay</a>
<a href="#" class="btn">Cancel</a>
</div>
{{end}}
{{if $a.ShowClose}}<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>{{end}}
</div>
{{end}}

View file

@ -0,0 +1,22 @@
{{define "tabler_breadcrumb"}}
{{$b := .}}
<nav aria-label="Breadcrumb">
<ol class="breadcrumb{{if $b.Class}} {{$b.Class}}{{end}}{{if $b.Separator}} breadcrumb-{{$b.Separator}}{{end}}">
{{range $i, $item := $b.Items}}
{{if $item.Link}}
<li class="breadcrumb-item">
{{if and (eq $i 0) $item.HomeIcon}}
<a href="{{$item.Link}}">
<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="icon"><path d="M5 12l-2 0l9 -9l9 9l-2 0"></path><path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7"></path><path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6"></path></svg>
</a>
{{else}}
<a href="{{$item.Link}}">{{$item.Title}}</a>
{{end}}
</li>
{{else}}
<li class="breadcrumb-item active" aria-current="page">{{$item.Title}}</li>
{{end}}
{{end}}
</ol>
</nav>
{{end}}

View file

@ -0,0 +1,36 @@
{{define "tabler_components_content"}}
{{$c := .}}
{{if hasField $c "Alerts"}}
<div class="card mb-3">
<div class="card-header"><h3 class="card-title">Alerts</h3></div>
<div class="card-body">
{{range $c.Alerts}}
{{template "tabler_alert" .}}
{{end}}
</div>
</div>
{{end}}
{{if hasField $c "Breadcrumbs"}}
<div class="card mb-3">
<div class="card-header"><h3 class="card-title">Breadcrumbs</h3></div>
<div class="card-body">
{{range $c.Breadcrumbs}}
{{template "tabler_breadcrumb" .}}
{{end}}
</div>
</div>
{{end}}
{{if hasField $c "Toasts"}}
<div class="card mb-3">
<div class="card-header"><h3 class="card-title">Toasts</h3></div>
<div class="card-body">
{{range $c.Toasts}}
{{template "tabler_toast" .}}
{{end}}
</div>
</div>
{{end}}
{{end}}

View file

@ -0,0 +1,23 @@
{{define "tabler_toast"}}
{{$t := .}}
<div class="toast{{if $t.Show}} show{{end}}" id="toast-{{defaultVal "simple" $t.ID}}" role="alert" aria-live="assertive" aria-atomic="true" data-bs-autohide="false">
{{if not $t.HideHeader}}
<div class="toast-header">
<span class="avatar avatar-xs me-2" style="background-image: url({{defaultVal "/public/static/avatars/000m.jpg" $t.PersonSrc}})"></span>
<strong class="me-auto">{{defaultVal "Jane Doe" $t.PersonName}}</strong>
<small>{{defaultVal "11 mins ago" $t.Date}}</small>
<button type="button" class="ms-2 btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
{{end}}
<div class="toast-body">
{{if $t.Cookies}}
🍪&nbsp;Our site uses cookies. By continuing to use our site, you agree to our Cookie Policy.
<div class="mt-2 pt-2 border-top">
<a href="#" class="btn btn-primary btn-sm">I understand</a>
</div>
{{else}}
{{defaultVal "Hello, world! This is a toast message." $t.Text}}
{{end}}
</div>
</div>
{{end}}

View file

@ -32,6 +32,8 @@
</div> </div>
{{end}} {{end}}
</div> </div>
{{else if hasField . "Components"}}
{{template "tabler_components_content" .Components}}
{{else if hasField . "Tables"}} {{else if hasField . "Tables"}}
{{range .Tables}} {{range .Tables}}
<div class="card{{if .CardClass}} {{.CardClass}}{{end}}"> <div class="card{{if .CardClass}} {{.CardClass}}{{end}}">

View file

@ -0,0 +1 @@
{{template "default_layout" .}}

View file

@ -0,0 +1 @@
{{template "homepage_layout" .}}

226
template-helpers.md Normal file
View file

@ -0,0 +1,226 @@
# Go Template Helpers
Custom `html/template` FuncMap extensions that bring Liquid-like expressiveness to Go server-side templates.
---
## String Helpers
### `capitalize`
Uppercases the first letter of a string and lowercases the rest.
```html
<h2>{{capitalize .Title}}</h2>
```
---
### `truncate`
Cuts a string to at most `n` characters and appends `…` if trimmed. Designed for pipeline use — pass `n` first.
```html
<p>{{.Excerpt | truncate 120}}</p>
```
---
### `prepend`
Adds a prefix to the beginning of a string. Designed for pipeline use — pass the prefix first.
```html
<a href="{{.Slug | prepend "/articles/"}}">Read more</a>
```
---
### `strAppend`
Adds a suffix to the end of a string. Designed for pipeline use — pass the suffix first.
```html
<a href="{{.Slug | strAppend ".html"}}">Read more</a>
```
---
### `split`
Divides a string into a slice of substrings by a separator. Designed for pipeline use — pass the separator first. Useful combined with `range` or `join`.
```html
{{range split "," .TagString}}
<span class="tag">{{.}}</span>
{{end}}
```
---
### `join`
Concatenates a string slice into a single string with a separator.
```html
<p class="tags">{{join .Tags " · "}}</p>
```
---
## Number Helpers
### `fmtNumber`
Formats an integer or float with thousands separators using the English locale.
```html
<span>{{fmtNumber .Views}} views</span>
<span>$ {{fmtNumber .Price}}</span>
```
---
## Date & Time Helpers
### `fmtDate`
Formats a `time.Time` value using a named layout or any custom Go layout string.
| Named layout | Output example |
|---|---|
| `"short"` | `02 Jan 2006` |
| `"long"` | `02 January 2006` |
| `"iso"` | `2006-01-02` |
| `"datetime"` | `02 Jan 2006 15:04` |
```html
<time>{{fmtDate .PublishedAt "short"}}</time>
<time>{{fmtDate .PublishedAt "02/01/2006"}}</time>
```
---
### `timeAgo`
Returns a human-readable relative time string from now (`"just now"`, `"3 hours ago"`, `"2 days ago"`, etc.).
```html
<span class="meta">Published {{timeAgo .PublishedAt}}</span>
```
---
## Collection Helpers
### `first`
Returns the first element of a slice, or `nil` if the slice is empty.
```html
{{with first .Articles}}
<h2>{{capitalize .Title}}</h2>
{{end}}
```
---
### `last`
Returns the last element of a slice, or `nil` if the slice is empty.
```html
{{with last .Articles}}
<p>Latest: {{capitalize .Title}}</p>
{{end}}
```
---
### `sliceOf`
Returns a sub-range of a slice from index `start` (inclusive) to `end` (exclusive).
```html
{{range sliceOf .Articles 0 3}}
<li>{{capitalize .Title}}</li>
{{end}}
```
---
### `contains`
Reports whether an item is present in a slice, or a substring exists within a string.
```html
{{if contains .Tags "go"}}
<span class="badge">Go</span>
{{end}}
{{if contains .Bio "engineer"}}
<p>Engineering post</p>
{{end}}
```
---
## Logic Helpers
### `defaultVal`
Returns a fallback value if the given value is nil or its zero value.
```html
<p>{{defaultVal "No subtitle" .Subtitle}}</p>
```
---
### `ternary`
Returns `trueVal` if the condition is true, `falseVal` otherwise. Pass the true value first, then the false value, then the condition.
```html
<span>{{ternary "Active" "Inactive" .IsActive}}</span>
```
---
### `coalesce`
Returns the first non-empty, non-nil value from a list of arguments.
```html
<h3>{{coalesce .Nickname .FullName "Anonymous"}}</h3>
```
---
## Struct Helpers
### `hasField`
Reports whether a struct has a field with the given name. Useful for rendering shared templates across different data types.
```html
{{if hasField . "Subtitle"}}
<p class="subtitle">{{.Subtitle}}</p>
{{end}}
```
---
## Pipeline Chaining
Helpers designed for pipeline use (`truncate`, `prepend`, `strAppend`, `split`) accept their configuration argument first and the input string last, so they compose naturally with `|`.
```html
{{/* chain multiple helpers */}}
<td>{{.Title | capitalize | truncate 40}}</td>
{{/* build a URL from a slug */}}
<a href="{{.Slug | prepend "/blog/" | strAppend ".html"}}">
{{.Title | capitalize}}
</a>
```