mirror of
https://github.com/hunvreus/basecoat.git
synced 2026-11-12 13:18:22 +00:00
Adding front-end
This commit is contained in:
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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"]) %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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") }}
|
||||
@@ -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,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) }}
|
||||
@@ -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,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) }}
|
||||
Reference in New Issue
Block a user