1
0
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:
“axzilla” 2024-10-07 16:09:01 +02:00
parent a319540f30
commit 13aab664a3
7 changed files with 326 additions and 27 deletions

View File

@ -3,6 +3,7 @@
## 2024-10-07
- Added: Input component
- Added: Modal component
- Added: Alert component
- Added: Accordion component
- Added: Datepicker component

View File

@ -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));

View File

@ -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)

View File

@ -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",

View 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>
}

View 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>
}
}

View 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>
}