1
0
mirror of https://github.com/axzilla/templui.git synced 2025-02-21 00:32:58 +00:00

feat(toast): WIP

This commit is contained in:
axzilla 2024-12-11 11:13:07 +07:00
parent d24b843cec
commit 1c969ad381
9 changed files with 569 additions and 0 deletions

View File

@ -633,6 +633,10 @@ body {
pointer-events: none;
}
.pointer-events-auto {
pointer-events: auto;
}
.visible {
visibility: visible;
}
@ -723,6 +727,10 @@ body {
top: 0.75rem;
}
.top-4 {
top: 1rem;
}
.top-full {
top: 100%;
}
@ -780,6 +788,10 @@ body {
margin-left: 0.75rem;
}
.ml-4 {
margin-left: 1rem;
}
.ml-auto {
margin-left: auto;
}
@ -792,6 +804,10 @@ body {
margin-right: 0.5rem;
}
.mr-3 {
margin-right: 0.75rem;
}
.mt-1 {
margin-top: 0.25rem;
}
@ -974,10 +990,22 @@ body {
width: 1.75rem;
}
.w-72 {
width: 18rem;
}
.w-8 {
width: 2rem;
}
.w-96 {
width: 24rem;
}
.w-\[30rem\] {
width: 30rem;
}
.w-full {
width: 100%;
}
@ -990,6 +1018,10 @@ body {
max-width: 48rem;
}
.max-w-4xl {
max-width: 56rem;
}
.max-w-7xl {
max-width: 80rem;
}
@ -1080,6 +1112,11 @@ body {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.translate-y-4 {
--tw-translate-y: 1rem;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.translate-y-full {
--tw-translate-y: 100%;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@ -1420,6 +1457,11 @@ body {
background-color: hsl(var(--destructive) / var(--tw-bg-opacity, 1));
}
.bg-gray-50 {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
}
.bg-gray-700 {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1));
@ -1454,6 +1496,11 @@ body {
background-color: hsl(var(--primary) / var(--tw-bg-opacity, 1));
}
.bg-primary-foreground {
--tw-bg-opacity: 1;
background-color: hsl(var(--primary-foreground) / var(--tw-bg-opacity, 1));
}
.bg-purple-500 {
--tw-bg-opacity: 1;
background-color: rgb(168 85 247 / var(--tw-bg-opacity, 1));
@ -1518,6 +1565,10 @@ body {
padding: 1.5rem;
}
.p-8 {
padding: 2rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
@ -1783,6 +1834,11 @@ body {
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
}
.text-yellow-500 {
--tw-text-opacity: 1;
color: rgb(234 179 8 / var(--tw-text-opacity, 1));
}
.underline {
text-decoration-line: underline;
}
@ -1807,6 +1863,10 @@ body {
opacity: 0.5;
}
.opacity-75 {
opacity: 0.75;
}
.opacity-80 {
opacity: 0.8;
}

View File

@ -3,14 +3,74 @@ package main
import (
"fmt"
"net/http"
"strconv"
"github.com/a-h/templ"
"github.com/axzilla/goilerplate/assets"
"github.com/axzilla/goilerplate/internals/config"
"github.com/axzilla/goilerplate/internals/middleware"
"github.com/axzilla/goilerplate/internals/ui/pages"
"github.com/axzilla/goilerplate/pkg/components"
)
func HandleToastDemo(w http.ResponseWriter, r *http.Request) {
duration, err := strconv.Atoi(r.FormValue("duration"))
if err != nil {
duration = 0
}
fmt.Println("duration", duration)
fmt.Println("r.FormValue(\"message\")", r.FormValue("message"))
fmt.Println("r.FormValue(\"type\")", r.FormValue("type"))
fmt.Println("r.FormValue(\"position\")", r.FormValue("position"))
fmt.Println("r.FormValue(\"theme\")", r.FormValue("theme"))
fmt.Println("r.FormValue(\"size\")", r.FormValue("size"))
fmt.Println("r.FormValue(\"dismissible\")", r.FormValue("dismissible"))
fmt.Println("r.FormValue(\"icon\")", r.FormValue("icon"))
cfg := components.ToastProps{
Message: r.FormValue("message"),
Type: r.FormValue("type"),
Position: r.FormValue("position"),
Duration: duration,
Size: r.FormValue("size"),
Dismissible: r.FormValue("dismissible") == "on",
Icon: r.FormValue("icon") == "on",
}
components.Toast(cfg).Render(r.Context(), w)
return
}
func HandleCreateUser(w http.ResponseWriter, r *http.Request) {
email := r.FormValue("email")
// Validierung
if email == "" {
// Error Toast
components.Toast(components.ToastProps{
Message: "Email ist erforderlich",
Type: "error",
Position: "top-left",
Duration: 1000,
Dismissible: true,
Size: "md",
Icon: true,
}).Render(r.Context(), w)
return
}
// Erfolg Toast
components.Toast(components.ToastProps{
Message: "Benutzer erstellt",
Type: "success",
Position: "bottom-right",
Duration: 3000,
Dismissible: false,
Size: "sm",
Icon: true,
}).Render(r.Context(), w)
}
func main() {
mux := http.NewServeMux()
config.LoadConfig()
@ -45,7 +105,11 @@ func main() {
mux.Handle("GET /docs/components/slider", templ.Handler(pages.Slider()))
mux.Handle("GET /docs/components/tabs", templ.Handler(pages.Tabs()))
mux.Handle("GET /docs/components/textarea", templ.Handler(pages.Textarea()))
mux.Handle("GET /docs/components/toast", templ.Handler(pages.Toast()))
mux.Handle("GET /docs/components/toggle", templ.Handler(pages.Toggle()))
// Showcase API
mux.Handle("POST /users", http.HandlerFunc(HandleCreateUser))
mux.Handle("POST /docs/toast/demo", http.HandlerFunc(HandleToastDemo))
fmt.Println("Server is running on http://localhost:8090")
http.ListenAndServe(":8090", wrappedMux)

View File

@ -113,6 +113,10 @@ var Sections = []Section{
Text: "Textarea",
Href: "/docs/components/textarea",
},
{
Text: "Toast",
Href: "/docs/components/toast",
},
{
Text: "Toggle",
Href: "/docs/components/toggle",

View File

@ -30,6 +30,8 @@ templ BaseLayout() {
<!-- Alpine.js -->
// <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
// HTMX
<script defer src="https://cdn.jsdelivr.net/npm/htmx.org@1.7.0/dist/htmx.min.js"></script>
// Theme Customizer
<script src="/assets/js/theme-customizer.js"></script>
<!-- Themes CSS -->

View File

@ -0,0 +1,27 @@
package pages
import (
"github.com/axzilla/goilerplate/internals/ui/layouts"
"github.com/axzilla/goilerplate/internals/ui/modules"
"github.com/axzilla/goilerplate/internals/ui/showcase"
)
templ Toast() {
@layouts.DocsLayout() {
@modules.PageWrapper(modules.PageWrapperProps{
Name: "Toast",
Description: templ.Raw("Flexible Toast component for notifications and feedback."),
}) {
// @modules.ExampleWrapper(modules.ExampleWrapperProps{
// ShowcaseFile: showcase.Toast(),
// PreviewCodeFile: "toast.templ",
// ComponentCodeFile: "toast.templ",
// })
@modules.ExampleWrapper(modules.ExampleWrapperProps{
ShowcaseFile: showcase.ToastAdvanced(),
PreviewCodeFile: "toast_advancded.templ",
ComponentCodeFile: "toast.templ",
})
}
}
}

View File

@ -0,0 +1,25 @@
package showcase
templ Toast() {
<div class="flex flex-wrap gap-2">
<div>
<form
hx-post="/users"
hx-target="#toast-container"
class="space-y-4"
>
<div>
<label for="email">Email</label>
<input
type="email"
name="email"
id="email"
class="w-full rounded-lg border"
/>
</div>
<button type="submit">Speichern</button>
</form>
<div id="toast-container"></div>
</div>
</div>
}

View File

@ -0,0 +1,139 @@
package showcase
import "github.com/axzilla/goilerplate/pkg/components"
templ ToastAdvanced() {
<div class="max-w-4xl mx-auto p-8">
<section class="mb-12">
@components.Card(components.CardProps{}) {
@components.CardContent() {
<form
class="flex flex-col gap-2"
hx-post="/docs/toast/demo"
hx-trigger="change, submit"
hx-target="#toast-containerx"
>
// Message
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Message"})
@components.Input(components.InputProps{
Value: "Test Notification",
Name: "message",
})
}
// Type
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Type"})
@components.Select(components.SelectProps{
Name: "type",
Options: []components.SelectOption{
{Value: "default", Label: "Default"},
{Value: "success", Label: "Success"},
{Value: "error", Label: "Error"},
{Value: "warning", Label: "Warning"},
{Value: "info", Label: "Info"},
},
})
}
// Position
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Position"})
@components.Select(components.SelectProps{
Name: "position",
Options: []components.SelectOption{
{Value: "top-right", Label: "Top Right"},
{Value: "top-left", Label: "Top Left"},
{Value: "top-center", Label: "Top Center"},
{Value: "bottom-right", Label: "Bottom Right"},
{Value: "bottom-left", Label: "Bottom Left"},
{Value: "bottom-center", Label: "Bottom Center"},
},
})
}
// Duration
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Duration (ms)"})
@components.Input(components.InputProps{
Type: "number",
Name: "duration",
Value: "3000",
})
}
// Size
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Size"})
@components.Select(components.SelectProps{
Name: "size",
Options: []components.SelectOption{
{Value: "sm", Label: "Small"},
{Value: "md", Label: "Medium"},
{Value: "lg", Label: "Large"},
},
})
}
// Options
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Options"})
@components.FormItemFlex(components.FormItemProps{}) {
@components.Toggle(components.ToggleProps{
Name: "dismissible",
Checked: true,
})
@components.Label(components.LabelProps{Text: "Dismissible"})
}
@components.FormItemFlex(components.FormItemProps{}) {
@components.Toggle(components.ToggleProps{
Name: "icon",
Checked: true,
})
@components.Label(components.LabelProps{Text: "Show Icon"})
}
}
@components.Button(components.ButtonProps{
Text: "Show Toast",
Type: "submit",
Class: "w-full",
})
</form>
}
}
</section>
<div id="toast-containerx"></div>
// Code Examples
<section>
<h2 class="text-xl font-semibold mb-4">Usage Examples</h2>
<div class="space-y-4">
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="font-medium mb-2">Basic Usage</h3>
<pre class="text-sm">
{ `// In your handler
components.Toast(types.ToastConfig{
Message: "Nachricht",
Type: "success",
Position: "top-right",
Duration: 3000,
}).Render(ctx, w)` }
</pre>
</div>
<div class="p-4 bg-gray-50 rounded-lg">
<h3 class="font-medium mb-2">With HTMX</h3>
<pre class="text-sm">
{ `// Template
<form hx-post="/save" hx-target="#toast-container">
...
</form>
<div id="toast-container"></div>
// Handler
if err != nil {
return components.Toast(types.ToastConfig{
Message: err.Error(),
Type: "error",
}).Render(ctx, w)
}` }
</pre>
</div>
</div>
</section>
</div>
}

View File

@ -0,0 +1,91 @@
package components
import "fmt"
import "github.com/axzilla/goilerplate/pkg/icons"
type ToastProps struct {
Message string // Die Nachricht
Type string // success, error, warning, info, default
Position string // top-right, top-left, top-center, bottom-right, bottom-left, bottom-center
Duration int // Millisekunden, 0 = permanent
Dismissible bool // Kann manuell geschlossen werden
Size string // sm, md, lg
Icon bool // Icon anzeigen/verstecken
HTML string // Custom HTML Content
}
templ Toast(cfg ToastProps) {
<div
x-data={ `{
show: true,
message: '` + cfg.Message + `',
type: '` + cfg.Type + `',
position: '` + cfg.Position + `',
duration: ` + fmt.Sprint(cfg.Duration) + `,
dismissible: ` + fmt.Sprint(cfg.Dismissible) + `,
size: '` + cfg.Size + `'
}` }
x-init="if(duration > 0) setTimeout(() => show = false, duration)"
x-show="show"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4"
@click="if(dismissible) show = false"
class="fixed pointer-events-auto"
:class="{
// Position
'top-4 right-4': position === 'top-right',
'top-4 left-4': position === 'top-left',
'top-4 left-1/2 -translate-x-1/2': position === 'top-center',
'bottom-4 right-4': position === 'bottom-right',
'bottom-4 left-4': position === 'bottom-left',
'bottom-4 left-1/2 -translate-x-1/2': position === 'bottom-center',
// Size
'w-72': size === 'sm',
'w-96': size === 'md',
'w-[30rem]': size === 'lg'
}"
>
if cfg.HTML != "" {
<div
x-html="message"
class="rounded-lg shadow-lg"
></div>
} else {
// Default Toast
<div
class="bg-primary-foreground rounded-lg shadow-sm border p-4 flex items-center justify-center"
>
if cfg.Icon {
// Icons für verschiedene Types
if cfg.Type == "success" {
@icons.CircleCheck(icons.IconProps{Size: "18", Class: "text-green-500 mr-3"})
}
if cfg.Type == "error" {
@icons.CircleX(icons.IconProps{Size: "18", Class: "text-red-500 mr-3"})
}
if cfg.Type == "warning" {
@icons.TriangleAlert(icons.IconProps{Size: "18", Class: "text-yellow-500 mr-3"})
}
if cfg.Type == "info" {
@icons.Info(icons.IconProps{Size: "18", Class: "text-blue-500 mr-3"})
}
}
<div class="flex-1" x-text="message"></div>
if cfg.Dismissible {
<button
@click.stop="show = false"
>
@icons.X(icons.IconProps{
Size: "18",
Class: "ml-4 flex-shrink-0 opacity-75 hover:opacity-100",
})
</button>
}
</div>
}
</div>
}

View File

@ -0,0 +1,157 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "fmt"
import "github.com/axzilla/goilerplate/pkg/icons"
type ToastProps struct {
Message string // Die Nachricht
Type string // success, error, warning, info, default
Position string // top-right, top-left, top-center, bottom-right, bottom-left, bottom-center
Duration int // Millisekunden, 0 = permanent
Dismissible bool // Kann manuell geschlossen werden
Size string // sm, md, lg
Icon bool // Icon anzeigen/verstecken
HTML string // Custom HTML Content
}
func Toast(cfg ToastProps) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div x-data=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(`{
show: true,
message: '` + cfg.Message + `',
type: '` + cfg.Type + `',
position: '` + cfg.Position + `',
duration: ` + fmt.Sprint(cfg.Duration) + `,
dismissible: ` + fmt.Sprint(cfg.Dismissible) + `,
size: '` + cfg.Size + `'
}`)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/components/toast.templ`, Line: 27, Col: 10}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" x-init=\"if(duration &gt; 0) setTimeout(() =&gt; show = false, duration)\" x-show=\"show\" x-transition:enter=\"transition ease-out duration-300\" x-transition:enter-start=\"opacity-0 translate-y-4\" x-transition:enter-end=\"opacity-100 translate-y-0\" x-transition:leave=\"transition ease-in duration-200\" x-transition:leave-start=\"opacity-100 translate-y-0\" x-transition:leave-end=\"opacity-0 translate-y-4\" @click=\"if(dismissible) show = false\" class=\"fixed pointer-events-auto\" :class=\"{\n // Position\n &#39;top-4 right-4&#39;: position === &#39;top-right&#39;,\n &#39;top-4 left-4&#39;: position === &#39;top-left&#39;,\n &#39;top-4 left-1/2 -translate-x-1/2&#39;: position === &#39;top-center&#39;,\n &#39;bottom-4 right-4&#39;: position === &#39;bottom-right&#39;,\n &#39;bottom-4 left-4&#39;: position === &#39;bottom-left&#39;,\n &#39;bottom-4 left-1/2 -translate-x-1/2&#39;: position === &#39;bottom-center&#39;,\n // Size\n &#39;w-72&#39;: size === &#39;sm&#39;,\n &#39;w-96&#39;: size === &#39;md&#39;,\n &#39;w-[30rem]&#39;: size === &#39;lg&#39;\n }\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cfg.HTML != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div x-html=\"message\" class=\"rounded-lg shadow-lg\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <div class=\"bg-primary-foreground rounded-lg shadow-sm border p-4 flex items-center justify-center\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cfg.Icon {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cfg.Type == "success" {
templ_7745c5c3_Err = icons.CircleCheck(icons.IconProps{Size: "18", Class: "text-green-500 mr-3"}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cfg.Type == "error" {
templ_7745c5c3_Err = icons.CircleX(icons.IconProps{Size: "18", Class: "text-red-500 mr-3"}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cfg.Type == "warning" {
templ_7745c5c3_Err = icons.TriangleAlert(icons.IconProps{Size: "18", Class: "text-yellow-500 mr-3"}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cfg.Type == "info" {
templ_7745c5c3_Err = icons.Info(icons.IconProps{Size: "18", Class: "text-blue-500 mr-3"}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"flex-1\" x-text=\"message\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if cfg.Dismissible {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button @click.stop=\"show = false\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = icons.X(icons.IconProps{
Size: "18",
Class: "ml-4 flex-shrink-0 opacity-75 hover:opacity-100",
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate