mirror of
https://github.com/axzilla/templui.git
synced 2025-02-21 00:32:58 +00:00
feat: add modal component
This commit is contained in:
parent
a319540f30
commit
13aab664a3
@ -3,6 +3,7 @@
|
||||
## 2024-10-07
|
||||
|
||||
- Added: Input component
|
||||
- Added: Modal component
|
||||
- Added: Alert component
|
||||
- Added: Accordion component
|
||||
- Added: Datepicker component
|
||||
|
@ -628,6 +628,18 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.static {
|
||||
position: static;
|
||||
}
|
||||
@ -1135,8 +1147,14 @@ body {
|
||||
border-color: hsl(var(--border) / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-destructive\/50 {
|
||||
border-color: hsl(var(--destructive) / 0.5);
|
||||
.border-destructive {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: hsl(var(--destructive) / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-input {
|
||||
@ -1152,11 +1170,6 @@ body {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.border-destructive {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: hsl(var(--destructive) / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.bg-background {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: hsl(var(--background) / var(--tw-bg-opacity));
|
||||
@ -1166,6 +1179,11 @@ body {
|
||||
background-color: hsl(var(--background) / 0.8);
|
||||
}
|
||||
|
||||
.bg-black {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-card {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: hsl(var(--card) / var(--tw-bg-opacity));
|
||||
@ -1211,6 +1229,10 @@ body {
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-opacity-50 {
|
||||
--tw-bg-opacity: 0.5;
|
||||
}
|
||||
|
||||
.bg-opacity-75 {
|
||||
--tw-bg-opacity: 0.75;
|
||||
}
|
||||
@ -1348,6 +1370,10 @@ body {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.pt-5 {
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@ -1420,6 +1446,10 @@ body {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.leading-6 {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.leading-none {
|
||||
line-height: 1;
|
||||
}
|
||||
@ -1547,6 +1577,12 @@ body {
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-xl {
|
||||
--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.outline {
|
||||
outline-style: solid;
|
||||
}
|
||||
@ -1581,6 +1617,12 @@ body {
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-opacity {
|
||||
transition-property: opacity;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-transform {
|
||||
transition-property: transform;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@ -1718,6 +1760,11 @@ body {
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.focus\:border-primary:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: hsl(var(--primary) / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.focus\:outline-none:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
@ -1738,6 +1785,11 @@ body {
|
||||
--tw-ring-color: rgb(99 102 241 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-primary:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: hsl(var(--primary) / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus\:ring-ring:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: hsl(var(--ring) / var(--tw-ring-opacity));
|
||||
@ -1801,16 +1853,6 @@ body {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.dark\:border-destructive:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: hsl(var(--destructive) / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:border-border:is(.dark *) {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: hsl(var(--border) / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:from-gray-900:is(.dark *) {
|
||||
--tw-gradient-from: #111827 var(--tw-gradient-from-position);
|
||||
--tw-gradient-to: rgb(17 24 39 / 0) var(--tw-gradient-to-position);
|
||||
@ -1841,11 +1883,6 @@ body {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:text-destructive-foreground:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: hsl(var(--destructive-foreground) / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.dark\:file\:text-foreground:is(.dark *)::file-selector-button {
|
||||
--tw-text-opacity: 1;
|
||||
color: hsl(var(--foreground) / var(--tw-text-opacity));
|
||||
@ -1857,6 +1894,11 @@ body {
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:my-8 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.sm\:mb-10 {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
@ -1869,6 +1911,10 @@ body {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.sm\:flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sm\:h-1\/2 {
|
||||
height: 50%;
|
||||
}
|
||||
@ -1885,11 +1931,23 @@ body {
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.sm\:flex-row-reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.sm\:p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.sm\:px-6 {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.sm\:pb-4 {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.sm\:text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
@ -1974,11 +2032,6 @@ body {
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.\[\&\>svg\]\:text-destructive>svg {
|
||||
--tw-text-opacity: 1;
|
||||
color: hsl(var(--destructive) / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.\[\&\[aria-expanded\=true\]\>svg\]\:rotate-180[aria-expanded=true]>svg {
|
||||
--tw-rotate: 180deg;
|
||||
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));
|
||||
|
@ -29,6 +29,7 @@ func main() {
|
||||
mux.Handle("GET /docs/components/accordion", templ.Handler(pages.Accordion()))
|
||||
mux.Handle("GET /docs/components/datepicker", templ.Handler(pages.Datepicker()))
|
||||
mux.Handle("GET /docs/components/alert", templ.Handler(pages.Alert()))
|
||||
mux.Handle("GET /docs/components/modal", templ.Handler(pages.Modal()))
|
||||
|
||||
fmt.Println("Server is running on http://localhost:8090")
|
||||
http.ListenAndServe(":8090", mux)
|
||||
|
@ -53,6 +53,10 @@ var Sections = []Section{
|
||||
Text: "Input",
|
||||
Href: "/docs/components/input",
|
||||
},
|
||||
{
|
||||
Text: "Modal",
|
||||
Href: "/docs/components/modal",
|
||||
},
|
||||
{
|
||||
Text: "Sheet",
|
||||
Href: "/docs/components/sheet",
|
||||
|
163
internals/ui/components/modal.templ
Normal file
163
internals/ui/components/modal.templ
Normal file
@ -0,0 +1,163 @@
|
||||
package components
|
||||
|
||||
// ModalProps defines the properties for the Modal component.
|
||||
type ModalProps struct {
|
||||
// ID is a unique identifier for the modal.
|
||||
// It's used to control opening and closing.
|
||||
// This should be unique across your application.
|
||||
ID string
|
||||
|
||||
// Class specifies additional CSS classes to apply to the modal container.
|
||||
Class string
|
||||
}
|
||||
|
||||
// Modal renders a modal dialog component.
|
||||
// It uses Alpine.js for state management and animations.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// @components.ModalTrigger("default-modal") {
|
||||
// @components.Button(components.ButtonProps{Text: "Open Modal"})
|
||||
// }
|
||||
//
|
||||
// @components.Modal(components.ModalProps{ID: "default-modal", Class: "max-w-md"}) {
|
||||
// @components.ModalHeader() {
|
||||
// Are you absolutely sure?
|
||||
// }
|
||||
// @components.ModalBody() {
|
||||
// This action cannot be undone. This will permanently delete your account and remove your data from our servers.
|
||||
// }
|
||||
// @components.ModalFooter() {
|
||||
// <div class="flex gap-2">
|
||||
// @components.ModalClose("default-modal") {
|
||||
// @components.Button(components.ButtonProps{
|
||||
// Text: "Cancel",
|
||||
// })
|
||||
// }
|
||||
// @components.ModalClose("default-modal") {
|
||||
// @components.Button(components.ButtonProps{
|
||||
// Text: "Continue",
|
||||
// Variant: components.Secondary,
|
||||
// })
|
||||
// }
|
||||
// </div>
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// The Modal component should be used in conjunction with ModalTrigger to open it.
|
||||
templ Modal(props ModalProps) {
|
||||
<div
|
||||
x-data="{ open: false }"
|
||||
x-on:open-modal.window="if ($event.detail.id === $el.dataset.modalId) open = true"
|
||||
x-on:close-modal.window="if ($event.detail.id === $el.dataset.modalId) open = false"
|
||||
data-modal-id={ props.ID }
|
||||
x-show="open"
|
||||
x-transition:enter="transition ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity" aria-hidden="true"></div>
|
||||
<div
|
||||
class={
|
||||
"relative bg-background rounded-lg border text-left overflow-hidden shadow-xl transform transition-all sm:my-8 w-full",
|
||||
props.Class,
|
||||
}
|
||||
@click.away="open = false"
|
||||
>
|
||||
{ children... }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
// ModalTrigger renders an element that opens the modal when clicked.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// @components.ModalTrigger("example-modal") {
|
||||
// @components.Button(components.ButtonProps{Text: "Open Modal"})
|
||||
// }
|
||||
//
|
||||
// The 'id' parameter should match the ID of the Modal you want to open.
|
||||
templ ModalTrigger(id string) {
|
||||
<span
|
||||
data-modal-id={ id }
|
||||
@click="$dispatch('open-modal', { id: $el.dataset.modalId })"
|
||||
>
|
||||
{ children... }
|
||||
</span>
|
||||
}
|
||||
|
||||
// ModalClose renders an element that closes the modal when clicked.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// @components.ModalClose("example-modal") {
|
||||
// @components.Button(components.ButtonProps{
|
||||
// Text: "Close",
|
||||
// Variant: components.Secondary,
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// The 'id' parameter should match the ID of the Modal you want to close.
|
||||
templ ModalClose(id string) {
|
||||
<span
|
||||
data-modal-id={ id }
|
||||
@click="$dispatch('close-modal', { id: $el.dataset.modalId })"
|
||||
>
|
||||
{ children... }
|
||||
</span>
|
||||
}
|
||||
|
||||
// ModalHeader renders the header section of the modal.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// @components.ModalHeader() {
|
||||
// Modal Title
|
||||
// @components.ModalClose("example-modal")
|
||||
// }
|
||||
templ ModalHeader() {
|
||||
<div class="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<h3 class="text-lg leading-6 font-medium text-foreground" id="modal-title">
|
||||
{ children... }
|
||||
</h3>
|
||||
</div>
|
||||
}
|
||||
|
||||
// ModalBody renders the main content area of the modal.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// @components.ModalBody() {
|
||||
// <p>This is the modal content.</p>
|
||||
// }
|
||||
templ ModalBody() {
|
||||
<div class="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
||||
|
||||
// ModalFooter renders the footer section of the modal, typically containing action buttons.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// @components.ModalFooter() {
|
||||
// @components.ModalClose("example-modal") {
|
||||
// @components.Button(components.ButtonProps{
|
||||
// Text: "Close",
|
||||
// Variant: components.Secondary,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
templ ModalFooter() {
|
||||
<div class="px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
{ children... }
|
||||
</div>
|
||||
}
|
39
internals/ui/pages/modal.templ
Normal file
39
internals/ui/pages/modal.templ
Normal file
@ -0,0 +1,39 @@
|
||||
package pages
|
||||
|
||||
import (
|
||||
"github.com/axzilla/goilerplate/internals/ui/components"
|
||||
"github.com/axzilla/goilerplate/internals/ui/layouts"
|
||||
"github.com/axzilla/goilerplate/internals/ui/showcase"
|
||||
)
|
||||
|
||||
templ Modal() {
|
||||
@layouts.DocsLayout() {
|
||||
<div>
|
||||
<div class="mb-16">
|
||||
<h1 class="text-3xl font-bold mb-2">Modal</h1>
|
||||
<p class="mb-4 text-muted-foreground">A modal dialog component for displaying content that requires user interaction.</p>
|
||||
</div>
|
||||
@components.Tabs(components.TabsProps{
|
||||
Tabs: []components.Tab{
|
||||
{
|
||||
ID: "preview",
|
||||
Title: "Preview",
|
||||
Content: showcase.ModalShowcase(),
|
||||
},
|
||||
{
|
||||
ID: "code",
|
||||
Title: "Code",
|
||||
Content: CodeSnippetFromEmbedded("modal.templ", "go", showcase.TemplFiles),
|
||||
},
|
||||
{
|
||||
ID: "component",
|
||||
Title: "Component",
|
||||
Content: CodeSnippetFromEmbedded("modal.templ", "go", components.TemplFiles),
|
||||
},
|
||||
},
|
||||
TabsContainerClass: "md:w-1/2",
|
||||
ContentContainerClass: "w-full",
|
||||
})
|
||||
</div>
|
||||
}
|
||||
}
|
38
internals/ui/showcase/modal.templ
Normal file
38
internals/ui/showcase/modal.templ
Normal file
@ -0,0 +1,38 @@
|
||||
package showcase
|
||||
|
||||
import (
|
||||
"github.com/axzilla/goilerplate/internals/ui/components"
|
||||
)
|
||||
|
||||
templ ModalShowcase() {
|
||||
<div class="flex justify-center items-center border rounded-md py-16 px-4">
|
||||
<div>
|
||||
@components.ModalTrigger("default-modal") {
|
||||
@components.Button(components.ButtonProps{Text: "Open Modal"})
|
||||
}
|
||||
@components.Modal(components.ModalProps{ID: "default-modal", Class: "max-w-md"}) {
|
||||
@components.ModalHeader() {
|
||||
Are you absolutely sure?
|
||||
}
|
||||
@components.ModalBody() {
|
||||
This action cannot be undone. This will permanently delete your account and remove your data from our servers.
|
||||
}
|
||||
@components.ModalFooter() {
|
||||
<div class="flex gap-2">
|
||||
@components.ModalClose("default-modal") {
|
||||
@components.Button(components.ButtonProps{
|
||||
Text: "Cancel",
|
||||
})
|
||||
}
|
||||
@components.ModalClose("default-modal") {
|
||||
@components.Button(components.ButtonProps{
|
||||
Text: "Continue",
|
||||
Variant: components.Secondary,
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user