Adding front-end

This commit is contained in:
Ronan Berder
2025-04-28 11:37:29 +08:00
parent dee28f1ee5
commit f30b2152ea
15 changed files with 253 additions and 103 deletions
+93 -26
View File
@@ -69,6 +69,8 @@
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--animate-spin: spin 1s linear infinite;
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
@@ -531,10 +533,21 @@
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.translate-y-0 {
--tw-translate-y: calc(var(--spacing) * 0);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.translate-y-0\.5 {
--tw-translate-y: calc(var(--spacing) * 0.5);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.translate-y-2 {
--tw-translate-y: calc(var(--spacing) * 2);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.transform {
transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
}
.animate-pulse {
animation: var(--animate-pulse);
}
@@ -893,12 +906,18 @@
.underline-offset-4 {
text-underline-offset: 4px;
}
.opacity-0 {
opacity: 0%;
}
.opacity-50 {
opacity: 50%;
}
.opacity-60 {
opacity: 60%;
}
.opacity-100 {
opacity: 100%;
}
.shadow-none {
--tw-shadow: 0 0 #0000;
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
@@ -928,6 +947,11 @@
.filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.transition-\[margin-left\] {
transition-property: margin-left;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
@@ -957,10 +981,22 @@
--tw-duration: 200ms;
transition-duration: 200ms;
}
.duration-300 {
--tw-duration: 300ms;
transition-duration: 300ms;
}
.ease-in {
--tw-ease: var(--ease-in);
transition-timing-function: var(--ease-in);
}
.ease-in-out {
--tw-ease: var(--ease-in-out);
transition-timing-function: var(--ease-in-out);
}
.ease-out {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.outline-none {
--tw-outline-style: none;
outline-style: none;
@@ -1378,32 +1414,6 @@
}
}
}
.\[\&_a\:not\(\[class\]\)\]\:text-muted-foreground {
& a:not([class]) {
color: var(--color-muted-foreground);
}
}
.\[\&_a\:not\(\[class\]\)\]\:no-underline {
& a:not([class]) {
text-decoration-line: none;
}
}
.\[\&_a\:not\(\[class\]\)\]\:transition-colors {
& a:not([class]) {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
}
.\[\&_a\:not\(\[class\]\)\]\:hover\:text-foreground {
& a:not([class]) {
&:hover {
@media (hover: hover) {
color: var(--color-foreground);
}
}
}
}
.\[\&_img\]\:size-8 {
& img {
width: calc(var(--spacing) * 8);
@@ -1537,6 +1547,33 @@
}
}
}
.\[\&\>\[role\=img\]\]\:size-4 {
&>[role=img] {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
}
.\[\&\>\[role\=img\]\]\:shrink-0 {
&>[role=img] {
flex-shrink: 0;
}
}
.\[\&\>\[role\=img\]\>svg\]\:size-4 {
&>[role=img]>svg {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
}
.\[\&\>\[role\=img\]\>svg\]\:w-full {
&>[role=img]>svg {
width: 100%;
}
}
.\[\&\>\[role\=img\]\>svg\]\:shrink-0 {
&>[role=img]>svg {
flex-shrink: 0;
}
}
.\[\&\>h2\]\:mt-16 {
&>h2 {
margin-top: calc(var(--spacing) * 16);
@@ -3946,6 +3983,31 @@
inherits: false;
initial-value: 0;
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
initial-value: rotateX(0);
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
initial-value: rotateY(0);
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
initial-value: rotateZ(0);
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
initial-value: skewX(0);
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
initial-value: skewY(0);
}
@property --tw-space-y-reverse {
syntax: "*";
inherits: false;
@@ -4160,6 +4222,11 @@
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-translate-z: 0;
--tw-rotate-x: rotateX(0);
--tw-rotate-y: rotateY(0);
--tw-rotate-z: rotateZ(0);
--tw-skew-x: skewX(0);
--tw-skew-y: skewY(0);
--tw-space-y-reverse: 0;
--tw-space-x-reverse: 0;
--tw-border-style: solid;
-5
View File
@@ -15,7 +15,6 @@
@param open {boolean} [optional] [default=false] - Whether the dialog should be open initially.
@param close_button {boolean} [optional] [default=true] - Whether to include a close button.
@param close_on_overlay_click {boolean} [optional] [default=true] - Whether clicking the overlay closes the dialog.
@param include_html {boolean} [optional] [default=true] - Whether to render the HTML structure.
@param include_script {boolean} [optional] [default=true] - Whether to include the Alpine.js component script.
@param register_on {array} [optional] [default=["alpine:init"]] - Alpine.js events to register the component on.
#}
@@ -34,7 +33,6 @@
open=false,
close_button=true,
close_on_overlay_click=true,
include_html=true,
include_script=false,
register_on=["alpine:init"]
) %}
@@ -42,7 +40,6 @@
{% include "components/dialog-script.njk" %}
{% endif %}
{% if include_html %}
<div
id="{{ id }}"
x-data="dialog({{ 'true' if open else 'false' }}, {{ 'true' if close_on_overlay_click else 'false' }})"
@@ -55,7 +52,6 @@
{% if trigger %}
<button
type="button"
{% if id %}aria-controls="{{ id }}-dialog"{% endif %}
aria-expanded="false"
aria-controls="{{ id }}-dialog"
x-bind="$trigger"
@@ -120,5 +116,4 @@
</article>
</div>
</div>
{% endif %}
{% endmacro %}
+1 -4
View File
@@ -4,7 +4,6 @@
main_attrs={},
trigger_attrs={},
content_attrs={},
include_html=true,
include_script=false,
register_on=["alpine:init"]
) %}
@@ -12,7 +11,6 @@
{% include "components/dropdown-menu-script.njk" %}
{% endif %}
{% if include_html %}
<div
class="popover {{ main_attrs.class }}"
x-data="dropdownMenu"
@@ -53,5 +51,4 @@
</nav>
</div>
</div>
{% endif %}
{% endmacro %}
{% endmacro %}
-3
View File
@@ -4,7 +4,6 @@
main_attrs=None,
trigger_attrs=None,
content_attrs=None,
include_html=true,
include_script=false,
register_on=["alpine:init"]
) %}
@@ -12,7 +11,6 @@
{% include "components/popover-script.njk" %}
{% endif %}
{% if include_html %}
<div
class="popover {{ main_attrs.class }}"
x-data="popover"
@@ -51,5 +49,4 @@
{% if caller %}{{ caller() }}{% endif %}
</div>
</div>
{% endif %}
{% endmacro %}
@@ -23,7 +23,6 @@
const option = this.options.find(opt => opt.getAttribute('data-value') === initialValue);
this.selectOption(option, false);
this.focusedIndex = this.options.indexOf(option);
this.focusOption();
} else {
this.selectOption(this.options[0], false);
}
-3
View File
@@ -9,7 +9,6 @@
input_attrs={},
search_placeholder="Search entries...",
is_combobox=false,
include_html=true,
include_script=false,
register_on=["alpine:init"]
) %}
@@ -17,7 +16,6 @@
{% include "components/select-script.njk" %}
{% endif %}
{% if include_html %}
<div
class="popover {{ main_attrs.class }}"
x-data="select('{{ name }}', '{{ selected or '' }}')"
@@ -99,5 +97,4 @@
>
{% endif %}
</div>
{% endif %}
{% endmacro %}
-3
View File
@@ -4,7 +4,6 @@
main_attrs=None,
tablist_attrs=None,
default_tab_index=1,
include_html=true,
include_script=false,
register_on=["alpine:init"]
)
@@ -13,7 +12,6 @@
{% include "components/tabs-script.njk" %}
{% endif %}
{% if include_html %}
<div
class="tabs {{ main_attrs.class }}"
x-data="tabs({{ default_tab_index - 1 }})"
@@ -65,5 +63,4 @@
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endmacro %}
+26 -3
View File
@@ -4,13 +4,14 @@
const registerToastComponent = () => {
Alpine.store('toaster', { isPaused: false });
Alpine.data('toast', (category = '', timeoutDuration = null) => ({
Alpine.data('toast', (config={}) => ({
config: config,
open: false,
timeoutDuration: null,
timeoutId: null,
init() {
this.timeoutDuration = timeoutDuration || (category === 'error' ? 5000 : 3000);
this.timeoutDuration = config.duration || (config.category === 'error' ? 5000 : 3000);
this.timeoutId = setTimeout(() => { this.close() }, this.timeoutDuration);
this.open = true;
this.$watch('$store.toaster.isPaused', (isPaused) => {
@@ -37,12 +38,34 @@
this.$el.blur();
},
$toast: {
$toastBindings: {
['@mouseenter']() { this.$store.toaster.isPaused = true },
['@mouseleave']() { this.$store.toaster.isPaused = false },
['@keydown.escape.prevent']() { this.close() },
[':aria-hidden']() { return !this.open }
},
}));
Alpine.magic('toast', (el) => (config, toasterId='toaster') => {
const toaster = document.getElementById(toasterId);
const template = document.getElementById('toast-template');
if (!toaster) {
console.error(`Toaster container with id #${targetId} not found.`);
return;
}
if (!template) {
console.error('Toast template with id #toast-template not found.');
return;
}
const clone = template.content.firstElementChild.cloneNode(true);
clone.setAttribute('x-data', `toast(${JSON.stringify(config)})`);
clone.removeAttribute('id');
toaster.appendChild(clone);
});
}
{% for event in (register_on or ["alpine:init"]) %}
+99 -35
View File
@@ -3,7 +3,6 @@
toasts=[],
main_attrs=None,
is_fragment=false,
include_html=true,
include_script=false,
register_on=["alpine:init"]
) %}
@@ -11,12 +10,11 @@
{% include "components/toast-script.njk" %}
{% endif %}
{% if include_html %}
{% set iconMapping = {
'error': 'circle-x',
'success': 'circle-check',
'info': 'info',
'warning': 'circle-alert'
{% set icons = {
'success': '<svg aria-hidden="true" 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="lucide lucide-circle-check-icon lucide-circle-check"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>',
'error': '<svg aria-hidden="true" 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="lucide lucide-circle-x-icon lucide-circle-x"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',
'info': '<svg aria-hidden="true" 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="lucide lucide-info-icon lucide-info"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>',
'warning': '<svg aria-hidden="true" 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="lucide lucide-triangle-alert-icon lucide-triangle-alert"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'
} %}
<div
@@ -33,56 +31,51 @@
role="{{ 'alert' if toast.category == 'error' else 'status' }}"
aria-atomic="true"
aria-hidden="false"
:aria-hidden="!open"
{% if toast.category %}data-category="{{ toast.category }}"{% endif %}
x-data="toast('{{ toast.category }}', {{ toast.timeoutDuration }})"
x-bind="$toast"
x-data="toast('{{ toast.category }}', {{ toast.duration }})"
x-bind="$toastBindings"
>
<div class="toast-content">
<div class="flex items-center justify-between gap-x-3 p-4">
{% if toast.category in ["error", "success", "info", "warning"] %}
{% lucide iconMapping[toast.category], class="size-4 shrink-0" %}
{{ icons[toast.category] | safe }}
{% endif %}
<section class="flex-1 flex flex-col gap-0.5 items-start">
{% if toast.message is string %}
<h2 class="font-medium">{{ toast.message }}</h2>
{% elif toast.message is mapping %}
{% if toast.message.title %}
<h2 class="font-medium">{{ toast.message.title }}</h2>
{% endif %}
{% if toast.message.description %}
<p class="text-muted-foreground">{{ toast.message.description }}</p>
{% endif %}
{% if toast.title %}
<h2 class="font-medium">{{ toast.title }}</h2>
{% endif %}
{% if toast.description %}
<p class="text-muted-foreground">{{ toast.description }}</p>
{% endif %}
</section>
{% if toast.message.action or toast.message.cancel %}
{% if toast.action or toast.cancel %}
<footer class="flex flex-col gap-1 self-start">
{% if toast.message.action %}
{% if toast.message.action.click %}
{% if toast.action %}
{% if toast.action.click %}
<button
type="button"
class="btn h-6 text-xs px-2.5 rounded-sm"
@click="{{ toast.message.action.click }}"
>{{ toast.message.action.label }}</button>
{% elif toast.message.action.url %}
@click="{{ toast.action.click }}"
>{{ toast.action.label }}</button>
{% elif toast.action.url %}
<a
href="{{ toast.message.action.url }}"
href="{{ toast.action.url }}"
class="btn h-6 text-xs px-2.5 rounded-sm"
>{{ toast.message.action.label }}</a>
>{{ toast.action.label }}</a>
{% endif %}
{% endif %}
{% if toast.message.cancel %}
{% if toast.message.cancel.click %}
{% if toast.cancel %}
{% if toast.cancel.click %}
<button
type="button"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
@click="{{ toast.message.cancel.click }}"
>{{ toast.message.cancel.label }}</button>
{% elif toast.message.cancel.url %}
@click="{{ toast.cancel.click }}"
>{{ toast.cancel.label }}</button>
{% elif toast.cancel.url %}
<a
href="{{ toast.message.cancel.url }}"
href="{{ toast.cancel.url }}"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
>{{ toast.message.cancel.label }}</a>
>{{ toast.cancel.label }}</a>
{% endif %}
{% endif %}
</footer>
@@ -92,5 +85,76 @@
</div>
{% endfor %}
</div>
{% if not is_fragment %}
<template id="toast-template">
<div
class="toast"
role="status"
aria-atomic="true"
x-bind="$toastBindings"
>
<div class="toast-content">
<div class="flex items-center justify-between gap-x-3 p-4 [&>svg]:size-4 [&>svg]:shrink-0 [&>[role=img]]:size-4 [&>[role=img]]:shrink-0 [&>[role=img]>svg]:size-4">
<template x-if="config.icon">
<span aria-hidden="true" role="img" x-html="config.icon"></span>
</template>
<template x-if="!config.icon && config.category === 'success'">
{{ icons.success | safe }}
</template>
<template x-if="!config.icon && config.category === 'error'">
{{ icons.error | safe }}
</template>
<template x-if="!config.icon && config.category === 'info'">
{{ icons.info | safe }}
</template>
<template x-if="!config.icon && config.category === 'warning'">
{{ icons.warning | safe }}
</template>
<section class="flex-1 flex flex-col gap-0.5 items-start">
<template x-if="config.title">
<h2 class="font-medium" x-text="config.title"></h2>
</template>
<template x-if="config.description">
<p class="text-muted-foreground" x-text="config.description"></p>
</template>
</section>
<template x-if="config.action || config.cancel">
<footer class="flex flex-col gap-1 self-start">
<template x-if="config.action?.click">
<button
type="button"
class="btn h-6 text-xs px-2.5 rounded-sm"
@click="$evaluate(config.action.click)"
x-text="config.action.label"
></button>
</template>
<template x-if="config.action?.url">
<a
:href="config.action.url"
class="btn h-6 text-xs px-2.5 rounded-sm"
x-text="config.action.label"
></a>
</template>
<template x-if="config.cancel?.click">
<button
type="button"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
@click="$evaluate(config.cancel.click)"
x-text="config.cancel.label"
></button>
</template>
<template x-if="config.cancel?.url">
<a
:href="config.cancel.url"
class="btn-outline h-6 text-xs px-2.5 rounded-sm"
x-text="config.cancel.label"
></a>
</template>
</footer>
</div>
</div>
</div>
</template>
{% endif %}
{% endmacro %}
+1 -1
View File
@@ -31,7 +31,7 @@
<header class="flex flex-col gap-2 p-2">
<a href="/" class="btn-ghost p-2 h-12 w-full justify-start">
<div class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
{% lucide "paint-roller", { "class": "size-4" } %}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" class="h-4 w-4"><rect width="256" height="256" fill="none"></rect><line x1="208" y1="128" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line><line x1="192" y1="40" x2="40" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"></line></svg>
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">Basecoat</span>
+26 -4
View File
@@ -5,10 +5,32 @@ description: A succinct message that is displayed temporarily.
---
{% from "macros/code_preview.njk" import code_preview %}
{% set code %}<button class="btn-outline" hx-trigger="click" hx-get="/fragments/toast/success" hx-target="#toast-htmx">
Success
{% set code %}<button
class="btn-outline"
hx-trigger="click"
hx-get="/fragments/toast/success"
hx-target="#toast-htmx"
>
Toast from backend
</button>
<div id="toast-htmx"></div>{% endset %}
{{ code_preview("accordion", code, "max-w-md") }}
{{ code_preview("toast-htmx", code, "max-w-md") }}
<h2>Usage</h2>
{% from "macros/code_preview.njk" import code_preview %}
{% set code %}<button
class="btn-outline"
@click='
$toast({
category: "success",
title: "Great success",
description: "Tremendous success toast.",
action: {
label: "Go to Google",
url: "https://google.com"
}
})
'>
Toast from front-end
</button>
{% endset %}
{{ code_preview("toast-alpine", code, "max-w-md") }}
+2 -4
View File
@@ -1,9 +1,7 @@
{% from "../../_includes/components/toast.njk" import toaster %}
{% set toasts = [{
category: "error",
message: {
title: "Event has not been created",
cancel: { label: "Dismiss", click: "close()" }
}
title: "Event has not been created",
cancel: { label: "Dismiss", click: "close()" }
}] %}
{{ toaster(toasts=toasts, is_fragment=true) }}
+1 -3
View File
@@ -1,8 +1,6 @@
{% from "../../_includes/components/toast.njk" import toaster %}
{% set toasts = [{
category: "info",
message: {
title: "Be at the area 10 minutes before the event time"
}
title: "Be at the area 10 minutes before the event time"
}] %}
{{ toaster(toasts=toasts, is_fragment=true) }}
+3 -5
View File
@@ -1,9 +1,7 @@
{% from "../../_includes/components/toast.njk" import toaster %}
{% set toasts = [{
message: {
title: "Event has been created",
description: "Sunday, December 03, 2023 at 9:00 AM",
cancel: { label: "Undo", click: "close()" }
}
title: "Event has been created",
description: "Sunday, December 03, 2023 at 9:00 AM",
cancel: { label: "Undo", click: "close()" }
}] %}
{{ toaster(toasts=toasts, is_fragment=true) }}
+1 -3
View File
@@ -1,8 +1,6 @@
{% from "../../_includes/components/toast.njk" import toaster %}
{% set toasts = [{
category: "warning",
message: {
title: "Event start time cannot be earlier than 8am"
}
title: "Event start time cannot be earlier than 8am"
}] %}
{{ toaster(toasts=toasts, is_fragment=true) }}