test web-component

This commit is contained in:
fabio 2026-03-06 21:41:02 +01:00
parent 2aa2fa38a4
commit 6f14a98704
30 changed files with 2012 additions and 173 deletions

View File

@ -1,4 +1,4 @@
.PHONY: tw-build tw-watch htmx-copy flags-copy assets server test db-reset fmt
.PHONY: tw-build tw-watch flags-copy assets server test db-reset fmt
tw-build:
npm run tw:build
@ -6,13 +6,10 @@ tw-build:
tw-watch:
npm run tw:watch
htmx-copy:
mkdir -p web/static/vendor && cp node_modules/htmx.org/dist/htmx.min.js web/static/vendor/htmx.min.js
flags-copy:
mkdir -p web/static/vendor/flags && cp assets/flags/*.svg web/static/vendor/flags/
assets: htmx-copy flags-copy tw-build
assets: flags-copy tw-build
server:
go run ./cmd/server

BIN
cmd/server/__debug_bin1684683884 Executable file

Binary file not shown.

View File

@ -35,7 +35,7 @@ TASK: Integra Flowbite (UI + JS behavior) e aggiungi Makefile per Tailwind CLI (
6) Layout
- Aggiornare /web/templates/layout.html per includere:
<link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}">
<script src="/static/vendor/htmx.min.js"></script>
<script src="/static/vendor/app.js"></script>
<script src="/static/vendor/flowbite.js"></script>
- Rimuovere dal layout riferimenti attivi al vecchio UI kit Svelte (ma non cancellare /ui-kit dal repo)
@ -68,4 +68,4 @@ Con markup Flowbite + data-attributes standard, senza JS custom.
Criteri:
- make assets genera CSS e copia flowbite.js
- modals/dropdowns Flowbite funzionano
- progetto compilabile e avviabile.
- progetto compilabile e avviabile.

View File

@ -30,7 +30,7 @@ Vincoli:
- In /web/templates/layout.html:
- includere theme.js nel <head> prima del CSS per evitare FOUC:
<script src="/static/vendor/theme.js"></script>
- poi link CSS e script htmx/flowbite come già presenti
- poi link CSS e script frontend come già presenti
- aggiungere classi base al body per dark:
- bg-white dark:bg-gray-900
- text-gray-900 dark:text-gray-100
@ -76,4 +76,4 @@ Non serve perfezione totale, ma assicurare leggibilità.
Esegui:
- make tw-build (o make tw-watch per verificare)
- Avvia server e verifica cambio tema su /login e /users.
Correggi eventuali classi mancanti.
Correggi eventuali classi mancanti.

View File

@ -19,7 +19,7 @@ Aggiornare /web/templates/layout.html:
- Dropdown utente con logout
- Include:
<link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}">
<script src="/static/vendor/htmx.min.js"></script>
<script src="/static/vendor/app.js"></script>
<script src="/static/vendor/flowbite.js"></script>
Struttura:
@ -60,9 +60,9 @@ Usare:
- Modal Flowbite per dettaglio utente
Assicurarsi che:
- hx-get
- hx-target
- hx-swap
- fetch/get
- target DOM update
- swap HTML manuale
rimangano funzionanti
-------------------------------------
@ -138,4 +138,4 @@ Usare nelle pagine private.
- Nessun errore JS in console
Scrivere codice pulito, leggibile, con commenti minimi.
Non eliminare logica Go template esistente.
Non eliminare logica Go template esistente.

View File

@ -2,16 +2,16 @@ Implementa modulo “users” sotto /web/templates/private/users.
Routes protette (RequireAuth):
- GET /users -> pagina con search + container tabella
- GET /users/table -> partial HTML tabella (htmx)
- GET /users/table -> partial HTML tabella (ajax)
- GET /users/:id/modal -> partial HTML contenuto modal
Requisiti tabella:
- query params: q, sort (id|name|email whitelist), dir (asc|desc), page, pageSize
- server-driven paging/sort/search usando GORM (Count + Limit/Offset + Order)
- _table.html deve includere:
- header th cliccabili con hx-get (toggle dir)
- pager prev/next con hx-get
- bottone “Apri” che hx-get sul modal e hx-target="#userModal" hx-swap="innerHTML"
- header th cliccabili con fetch GET (toggle dir)
- pager prev/next con fetch GET
- bottone “Apri” che aggiorna `#userModal` via fetch + `innerHTML`
- apri modal via JS minimal: setAttribute('open','') dopo swap (o onclick)
Crea template:
@ -19,4 +19,4 @@ Crea template:
- private/users/_table.html
- private/users/_modal.html
Integra <ui-modal id="userModal"> nella index privata.
Integra <ui-modal id="userModal"> nella index privata.

View File

@ -7,7 +7,7 @@ Requisiti:
- src/index.ts registra:
- ui-modal
- ui-drop-down
- ui-data-table-shell (driver htmx per aggiornare un target)
- ui-data-table-shell (driver JS per aggiornare un target)
Componenti:
1) UiModal.svelte:
@ -26,8 +26,8 @@ Componenti:
3) UiDataTableShell.svelte:
- attributi: endpoint, target, page-size
- input search -> usa htmx.ajax('GET', url, {target}) se disponibile
- input search -> usa fetch('GET', url) e aggiorna il target
- non renderizza tabella, solo toolbar
Aggiorna layout per includere /static/ui/ui.css e /static/ui/ui.esm.js con ?v={{.BuildHash}}.
Aggiorna README con comandi npm.
Aggiorna README con comandi npm.

View File

@ -73,7 +73,7 @@ func Load() (*Config, error) {
PostgresDSN: envFirstNonEmpty("DB_POSTGRES_DSN", "DB_PG_DSN"),
CORS: CORSConfig{
Origins: envListOrDefault("CORS_ORIGINS", []string{"http://localhost:3000"}),
Headers: envListOrDefault("CORS_HEADERS", []string{"Origin", "Content-Type", "Accept", "Authorization", "HX-Request"}),
Headers: envListOrDefault("CORS_HEADERS", []string{"Origin", "Content-Type", "Accept", "Authorization"}),
Methods: envListOrDefault("CORS_METHODS", []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
Credentials: envBoolOrDefault("CORS_CREDENTIALS", true),
},

View File

@ -1,7 +1,9 @@
package controllers
import (
"bytes"
"errors"
"html/template"
"strings"
httpmw "trustcontact/internal/http/middleware"
@ -261,3 +263,29 @@ func (ac *AuthController) UpdateTheme(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
}
func (ac *AuthController) UserDropdown(c *fiber.Ctx) error {
currentUser, ok := httpmw.CurrentUserFromContext(c)
if !ok {
return c.SendStatus(fiber.StatusUnauthorized)
}
files := []string{
"web/templates/partials/user_dropdown.html",
}
tmpl, err := template.ParseFiles(files...)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("cannot render dropdown")
}
viewData := map[string]any{
"CurrentUser": currentUser,
}
var out bytes.Buffer
if err := tmpl.ExecuteTemplate(&out, "user_dropdown", viewData); err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("cannot render dropdown")
}
c.Type("html", "utf-8")
return c.Send(out.Bytes())
}

View File

@ -28,6 +28,7 @@ func renderPublic(c *fiber.Ctx, page string, data map[string]any) error {
"web/templates/layout.html",
"web/templates/public/_navbar.html",
"web/templates/partials/language_dropdown.html",
"web/templates/partials/user_dropdown.html",
"web/templates/public/_flash.html",
filepath.Join("web/templates/public", page),
}
@ -66,6 +67,7 @@ func renderPrivate(c *fiber.Ctx, _ string, data map[string]any) error {
"web/templates/layout.html",
"web/templates/public/_navbar.html",
"web/templates/partials/language_dropdown.html",
"web/templates/partials/user_dropdown.html",
"web/templates/public/_flash.html",
"web/templates/private.html",
}

View File

@ -15,6 +15,7 @@ import (
func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg *config.Config) error {
app.Static("/static", "web/static")
app.Static("/web-components", "web_components/dist")
app.Use(httpmw.SessionStoreMiddleware(store))
app.Use(httpmw.CurrentUserMiddleware(store, database))
@ -53,6 +54,7 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
app.Get("/forbidden", authController.ShowForbidden)
app.Post("/preferences/lang", httpmw.RequireAuth(), authController.UpdateLanguage)
app.Post("/preferences/theme", httpmw.RequireAuth(), authController.UpdateTheme)
app.Get("/partials/user-dropdown", httpmw.RequireAuth(), authController.UserDropdown)
// Quasar admin SPA assets are emitted with absolute paths (/assets, /icons, /favicon.ico).
// Protect them with the same auth/admin middleware used by /admin.

9
package-lock.json generated
View File

@ -7,9 +7,6 @@
"": {
"name": "trustcontact-flowbite",
"version": "1.0.0",
"dependencies": {
"htmx.org": "^2.0.6"
},
"devDependencies": {
"@tailwindcss/cli": "^4.1.13",
"tailwindcss": "^4.1.13"
@ -681,12 +678,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/htmx.org": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.8.tgz",
"integrity": "sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==",
"license": "0BSD"
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",

View File

@ -9,8 +9,5 @@
"devDependencies": {
"@tailwindcss/cli": "^4.1.13",
"tailwindcss": "^4.1.13"
},
"dependencies": {
"htmx.org": "^2.0.6"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,12 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title>
<script>
window.__TC_IS_AUTHENTICATED = {{if .CurrentUser}}true{{else}}false{{end}};
window.__TC_SERVER_THEME = {{printf "%q" .UserTheme}};
window.__TC_IS_AUTHENTICATED = '{{if .CurrentUser}}true{{else}}false{{end}}';
window.__TC_SERVER_THEME = '{{printf "%q" .UserTheme}}';
</script>
<script src="/static/vendor/theme.js?v={{.BuildHash}}"></script>
<link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}">
<script src="/static/vendor/htmx.min.js"></script>
<link rel="stylesheet" href="/web-components/user-menu.css?v={{.BuildHash}}">
<script type="module" src="/web-components/user-menu.es.js?v={{.BuildHash}}"></script>
</head>
<body class="flex min-h-screen flex-col bg-white text-gray-900 antialiased dark:bg-gray-900 dark:text-gray-100">
@ -34,8 +35,8 @@
(function () {
var DEFAULT_LANG = 'it';
var STORAGE_KEY = 'tc_lang';
var SERVER_LANG = {{printf "%q" .UserLang}};
var IS_AUTHENTICATED = {{if .CurrentUser}}true{{else}}false{{end}};
var SERVER_LANG = '{{printf "%q" .UserLang}}';
var IS_AUTHENTICATED = '{{if .CurrentUser}}true{{else}}false{{end}}';
var dictionaries = {
it: {
'nav.open_main_menu': 'Apri menu principale', 'nav.open_user_menu': 'Apri menu utente', 'nav.dashboard': 'Dashboard', 'nav.users': 'Users', 'nav.admin': 'Admin', 'nav.login': 'Login', 'nav.signup': 'Signup', 'nav.logout': 'Logout',
@ -47,7 +48,7 @@
'verify.title': 'Verifica email', 'verify.p1': 'Controlla la casella di posta e apri il link di verifica ricevuto.', 'verify.p2': 'Se il link è scaduto, ripeti la registrazione o contatta supporto.', 'verify.go_login': 'Vai al login',
'private.title': 'Dashboard', 'private.back_prefix': 'Bentornato', 'private.generic': 'Benvenuto.', 'private.quick_links': 'Link rapidi',
'admin.dashboard.title': 'Admin Dashboard', 'admin.dashboard.area': 'Area amministrazione.', 'admin.users_count': 'Utenti', 'admin.current_role': 'Ruolo corrente', 'admin.navigation': 'Navigazione', 'admin.manage_users': 'Gestione utenti',
'users.title': 'Users', 'users.subtitle': 'Ricerca, ordinamento e paging server-side via HTMX.', 'users.new_user': 'Nuovo Utente', 'users.search': 'Search', 'users.search_placeholder': 'Cerca nome o email', 'users.page_size': 'Page size', 'users.search_button': 'Cerca', 'users.user_detail': 'Dettaglio utente', 'users.actions': 'Azioni', 'users.open': 'Apri', 'users.none': 'Nessun utente trovato.', 'users.total': 'Totale', 'users.users_label': 'utenti', 'users.page': 'Pagina', 'users.prev': 'Prev', 'users.next': 'Next', 'users.close': 'Chiudi',
'users.title': 'Users', 'users.subtitle': 'Ricerca, ordinamento e paging server-side.', 'users.new_user': 'Nuovo Utente', 'users.search': 'Search', 'users.search_placeholder': 'Cerca nome o email', 'users.page_size': 'Page size', 'users.search_button': 'Cerca', 'users.user_detail': 'Dettaglio utente', 'users.actions': 'Azioni', 'users.open': 'Apri', 'users.none': 'Nessun utente trovato.', 'users.total': 'Totale', 'users.users_label': 'utenti', 'users.page': 'Pagina', 'users.prev': 'Prev', 'users.next': 'Next', 'users.close': 'Chiudi',
'users.new_user_modal_title': 'Nuovo utente', 'users.new_user_modal_placeholder': 'Placeholder UI Flowbite. La creazione utente può essere collegata a una route backend quando disponibile.',
'table.id': 'ID', 'table.name': 'Name', 'table.email': 'Email', 'table.role': 'Role', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Verificato', 'user.created': 'Creato', 'user.yes': 'sì', 'user.no': 'no',
'audit.title': 'Audit Logs', 'audit.activity': 'Attività', 'audit.security': 'Sicurezza', 'audit.timestamp': 'Timestamp', 'audit.actor': 'Attore', 'audit.action': 'Azione', 'audit.placeholder': 'Placeholder log di sicurezza.'
@ -62,7 +63,7 @@
'verify.title': 'Verify email', 'verify.p1': 'Check your inbox and open the verification link.', 'verify.p2': 'If the link expired, sign up again or contact support.', 'verify.go_login': 'Go to login',
'private.title': 'Dashboard', 'private.back_prefix': 'Signed in as', 'private.generic': 'Signed in.', 'private.quick_links': 'Quick links',
'admin.dashboard.title': 'Admin Dashboard', 'admin.dashboard.area': 'Administration area.', 'admin.users_count': 'Users', 'admin.current_role': 'Current role', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Manage users',
'users.title': 'Users', 'users.subtitle': 'Search, sorting and server-side paging via HTMX.', 'users.new_user': 'New user', 'users.search': 'Search', 'users.search_placeholder': 'Search by name or email', 'users.page_size': 'Page size', 'users.search_button': 'Search', 'users.user_detail': 'User details', 'users.actions': 'Actions', 'users.open': 'Open', 'users.none': 'No users found.', 'users.total': 'Total', 'users.users_label': 'users', 'users.page': 'Page', 'users.prev': 'Prev', 'users.next': 'Next', 'users.close': 'Close',
'users.title': 'Users', 'users.subtitle': 'Search, sorting and server-side paging.', 'users.new_user': 'New user', 'users.search': 'Search', 'users.search_placeholder': 'Search by name or email', 'users.page_size': 'Page size', 'users.search_button': 'Search', 'users.user_detail': 'User details', 'users.actions': 'Actions', 'users.open': 'Open', 'users.none': 'No users found.', 'users.total': 'Total', 'users.users_label': 'users', 'users.page': 'Page', 'users.prev': 'Prev', 'users.next': 'Next', 'users.close': 'Close',
'users.new_user_modal_title': 'New user', 'users.new_user_modal_placeholder': 'Flowbite placeholder UI. Connect creation to backend route when available.',
'table.id': 'ID', 'table.name': 'Name', 'table.email': 'Email', 'table.role': 'Role', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Verified', 'user.created': 'Created', 'user.yes': 'yes', 'user.no': 'no',
'audit.title': 'Audit Logs', 'audit.activity': 'Activity', 'audit.security': 'Security', 'audit.timestamp': 'Timestamp', 'audit.actor': 'Actor', 'audit.action': 'Action', 'audit.placeholder': 'Security logs placeholder.'
@ -77,7 +78,7 @@
'verify.title': 'E-Mail verifizieren', 'verify.p1': 'Öffnen Sie die Verifizierungs-E-Mail in Ihrem Posteingang.', 'verify.p2': 'Wenn der Link abgelaufen ist, registrieren Sie sich erneut oder kontaktieren Sie den Support.', 'verify.go_login': 'Zum Login',
'private.title': 'Dashboard', 'private.back_prefix': 'Willkommen zurück', 'private.generic': 'Willkommen.', 'private.quick_links': 'Schnelllinks',
'admin.dashboard.title': 'Admin-Dashboard', 'admin.dashboard.area': 'Administrationsbereich.', 'admin.users_count': 'Benutzer', 'admin.current_role': 'Aktuelle Rolle', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Benutzer verwalten',
'users.title': 'Benutzer', 'users.subtitle': 'Suche, Sortierung und serverseitiges Paging via HTMX.', 'users.new_user': 'Neuer Benutzer', 'users.search': 'Suche', 'users.search_placeholder': 'Nach Name oder E-Mail suchen', 'users.page_size': 'Seitengröße', 'users.search_button': 'Suchen', 'users.user_detail': 'Benutzerdetails', 'users.actions': 'Aktionen', 'users.open': 'Öffnen', 'users.none': 'Keine Benutzer gefunden.', 'users.total': 'Gesamt', 'users.users_label': 'Benutzer', 'users.page': 'Seite', 'users.prev': 'Zurück', 'users.next': 'Weiter', 'users.close': 'Schließen',
'users.title': 'Benutzer', 'users.subtitle': 'Suche, Sortierung und serverseitiges Paging.', 'users.new_user': 'Neuer Benutzer', 'users.search': 'Suche', 'users.search_placeholder': 'Nach Name oder E-Mail suchen', 'users.page_size': 'Seitengröße', 'users.search_button': 'Suchen', 'users.user_detail': 'Benutzerdetails', 'users.actions': 'Aktionen', 'users.open': 'Öffnen', 'users.none': 'Keine Benutzer gefunden.', 'users.total': 'Gesamt', 'users.users_label': 'Benutzer', 'users.page': 'Seite', 'users.prev': 'Zurück', 'users.next': 'Weiter', 'users.close': 'Schließen',
'users.new_user_modal_title': 'Neuer Benutzer', 'users.new_user_modal_placeholder': 'Flowbite-Placeholder-UI. Bei Bedarf mit Backend-Route verbinden.',
'table.id': 'ID', 'table.name': 'Name', 'table.email': 'E-Mail', 'table.role': 'Rolle', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Verifiziert', 'user.created': 'Erstellt', 'user.yes': 'ja', 'user.no': 'nein',
'audit.title': 'Audit-Logs', 'audit.activity': 'Aktivität', 'audit.security': 'Sicherheit', 'audit.timestamp': 'Zeitstempel', 'audit.actor': 'Akteur', 'audit.action': 'Aktion', 'audit.placeholder': 'Platzhalter für Sicherheitsprotokolle.'
@ -91,7 +92,7 @@
'verify.title': 'Vérifier lemail', 'verify.p1': 'Vérifiez votre boîte mail et ouvrez le lien de vérification.', 'verify.p2': 'Si le lien a expiré, réinscrivez-vous ou contactez le support.', 'verify.go_login': 'Aller à la connexion',
'private.title': 'Tableau de bord', 'private.back_prefix': 'Bon retour', 'private.generic': 'Bienvenue.', 'private.quick_links': 'Liens rapides',
'admin.dashboard.title': 'Tableau de bord admin', 'admin.dashboard.area': 'Zone dadministration.', 'admin.users_count': 'Utilisateurs', 'admin.current_role': 'Rôle actuel', 'admin.navigation': 'Navigation', 'admin.manage_users': 'Gérer les utilisateurs',
'users.title': 'Utilisateurs', 'users.subtitle': 'Recherche, tri et pagination côté serveur via HTMX.', 'users.new_user': 'Nouvel utilisateur', 'users.search': 'Recherche', 'users.search_placeholder': 'Rechercher par nom ou email', 'users.page_size': 'Taille de page', 'users.search_button': 'Rechercher', 'users.user_detail': 'Détails utilisateur', 'users.actions': 'Actions', 'users.open': 'Ouvrir', 'users.none': 'Aucun utilisateur trouvé.', 'users.total': 'Total', 'users.users_label': 'utilisateurs', 'users.page': 'Page', 'users.prev': 'Préc.', 'users.next': 'Suiv.', 'users.close': 'Fermer',
'users.title': 'Utilisateurs', 'users.subtitle': 'Recherche, tri et pagination côté serveur.', 'users.new_user': 'Nouvel utilisateur', 'users.search': 'Recherche', 'users.search_placeholder': 'Rechercher par nom ou email', 'users.page_size': 'Taille de page', 'users.search_button': 'Rechercher', 'users.user_detail': 'Détails utilisateur', 'users.actions': 'Actions', 'users.open': 'Ouvrir', 'users.none': 'Aucun utilisateur trouvé.', 'users.total': 'Total', 'users.users_label': 'utilisateurs', 'users.page': 'Page', 'users.prev': 'Préc.', 'users.next': 'Suiv.', 'users.close': 'Fermer',
'users.new_user_modal_title': 'Nouvel utilisateur', 'users.new_user_modal_placeholder': 'UI Flowbite placeholder. Connecter à une route backend si nécessaire.',
'table.id': 'ID', 'table.name': 'Nom', 'table.email': 'Email', 'table.role': 'Rôle', 'user.role_admin': 'admin', 'user.role_user': 'user', 'user.verified': 'Vérifié', 'user.created': 'Créé', 'user.yes': 'oui', 'user.no': 'non',
'audit.title': 'Journaux daudit', 'audit.activity': 'Activité', 'audit.security': 'Sécurité', 'audit.timestamp': 'Horodatage', 'audit.actor': 'Acteur', 'audit.action': 'Action', 'audit.placeholder': 'Espace réservé des journaux de sécurité.'
@ -203,53 +204,34 @@
langSelect.addEventListener('change', function () {
var selectedLang = normalizeLang(langSelect.value);
localStorage.setItem(STORAGE_KEY, selectedLang);
if (langSelect.dataset.authenticated === '1') {
fetch('/preferences/lang', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'lang=' + encodeURIComponent(selectedLang)
}).catch(function () {
// Keep UI responsive even if persistence fails.
});
}
applyTranslations(document);
});
}
function initNavbarComponents(root) {
(root || document).querySelectorAll('[data-collapse-toggle]').forEach(function (button) {
if (button.dataset.tcBound === '1') return;
button.dataset.tcBound = '1';
var targetId = button.getAttribute('data-collapse-toggle');
var target = document.getElementById(targetId);
if (!target) return;
button.addEventListener('click', function () {
var isHidden = target.classList.contains('hidden');
target.classList.toggle('hidden', !isHidden);
button.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
});
});
var userButton = document.getElementById('user-menu-button');
var userDropdown = document.getElementById('user-dropdown');
if (userButton && userDropdown && userButton.dataset.tcBound !== '1') {
userButton.dataset.tcBound = '1';
userButton.addEventListener('click', function (event) {
event.preventDefault();
var isHidden = userDropdown.classList.contains('hidden');
userDropdown.classList.toggle('hidden', !isHidden);
userButton.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
});
}
if (!window.__tcNavbarDocBound) {
window.__tcNavbarDocBound = true;
document.addEventListener('click', function (event) {
var btn = document.getElementById('user-menu-button');
var menu = document.getElementById('user-dropdown');
if (!btn || !menu || menu.classList.contains('hidden')) return;
if (btn.contains(event.target) || menu.contains(event.target)) return;
menu.classList.add('hidden');
btn.setAttribute('aria-expanded', 'false');
if(menu.getAttribute('isopen') === 'false') return; // skip if menu is closed with css, to avoid issues with multiple navbars
menu.setAttribute('isopen', 'false');
menu.style.left = "-9999px";
});
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape') return;
var btn = document.getElementById('user-menu-button');
var menu = document.getElementById('user-dropdown');
if (!btn || !menu) return;
menu.classList.add('hidden');
btn.setAttribute('aria-expanded', 'false');
if(menu.getAttribute('isopen') === 'false') return; // skip if menu is closed with css, to avoid issues with multiple navbars
menu.setAttribute('isopen', 'false');
menu.style.left = "-9999px";
});
}
}
@ -259,13 +241,6 @@
if (typeof window.initThemeToggle === 'function') {
window.initThemeToggle();
}
document.body.addEventListener('htmx:afterSwap', function (evt) {
initNavbarComponents(evt.target || document);
applyTranslations(evt.target || document);
if (typeof window.initThemeToggle === 'function') {
window.initThemeToggle();
}
});
})();
</script>
</body>

View File

@ -1,7 +1,7 @@
{{define "language_dropdown"}}
<div class="relative flex items-center gap-2 text-sm">
<img id="lang-flag" class="rounded object-cover dark:outline" src="/static/vendor/flags/it.svg" alt="Italiano" style="width:32px;height:22px;">
<select id="lang-select" name="lang" class="inline-flex h-8 min-w-[95px] items-center rounded-lg bg-white px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:ring-gray-700" {{if .CurrentUser}}hx-post="/preferences/lang" hx-trigger="change" hx-swap="none"{{end}}>
<select id="lang-select" name="lang" data-authenticated="{{if .CurrentUser}}1{{else}}0{{end}}" class="inline-flex h-8 min-w-[95px] items-center rounded-lg bg-white px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:ring-gray-700">
<option value="it">Italiano</option>
<option value="en">English</option>
<option value="en_us">English USA</option>

View File

@ -0,0 +1,17 @@
{{define "user_dropdown"}}
<div id="user-dropdown" class="z-50 mt-2 w-56 list-none divide-y divide-gray-700 rounded-lg border border-gray-200 bg-white shadow-sm dark:border-2 dark:divide-gray-700 dark:border-gray-700 dark:bg-gray-800 " >
<div class="px-4 py-3">
<span class="block truncate text-sm text-gray-900 dark:text-gray-100">{{if .CurrentUser.Name}}{{.CurrentUser.Name}}{{else}}Utente{{end}}</span>
<span class="block truncate text-sm text-gray-500 dark:text-gray-400">{{.CurrentUser.Email}}</span>
</div >
<div class="py-2">
<a href="/private" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700">Private</a>
{{if eq .CurrentUser.Role "admin"}}
<a href="/admin" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700" data-i18n="nav.admin">Admin</a>
{{end}}
<form action="/logout" method="post" class="px-2">
<button type="submit" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-red-700 hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-900/40" data-i18n="nav.logout">Logout</button>
</form>
</div>
</div>
{{end}}

View File

@ -1,10 +1,17 @@
{{define "navbar"}}
<nav class="border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900">
<div class="mx-auto flex max-w-7xl flex-wrap items-center justify-between p-4">
<a href="/" class="flex items-center space-x-3 rtl:space-x-reverse">
<div class="flex">
{{if .CurrentUser}}
<!-- User menu > use web_components user-menu-->
<user-menu class="" target="user-dropdown" pos="bl" sr="Open user menu"></user-menu>
{{template "user_dropdown" .}}
{{end}}
<a href="/" class="px-4 flex items-start space-x-3 rtl:space-x-reverse">
<span class="self-center whitespace-nowrap text-xl font-semibold">Trustcontact</span>
</a>
</div>
<button data-collapse-toggle="navbar-main" type="button" class="inline-flex h-10 w-10 items-center justify-center rounded-lg p-2 text-sm text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-700 md:hidden" aria-controls="navbar-main" aria-expanded="false">
<span class="sr-only" data-i18n="nav.open_main_menu">Apri menu principale</span>
<svg class="h-5 w-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
@ -36,31 +43,5 @@
{{define "navbar_controls"}}
{{template "language_dropdown" .}}
{{if .CurrentUser}}
<div class="relative">
<button type="button" class="inline-flex h-8 items-center rounded-lg bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:ring-gray-700" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
<span class="sr-only" data-i18n="nav.open_user_menu">Apri menu utente</span>
<span class="inline-flex items-center justify-center">
<svg class="h-4 w-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
</svg>
</span>
</button>
<div class="z-50 my-4 hidden w-56 list-none divide-y divide-gray-100 rounded-lg bg-white text-base shadow-sm dark:divide-gray-700 dark:bg-gray-800" id="user-dropdown">
<div class="px-4 py-3">
<span class="block truncate text-sm text-gray-900 dark:text-gray-100">{{if .CurrentUser.Name}}{{.CurrentUser.Name}}{{else}}Utente{{end}}</span>
<span class="block truncate text-sm text-gray-500 dark:text-gray-400">{{.CurrentUser.Email}}</span>
</div>
<div class="py-2">
<a href="/private" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700">Private</a>
{{if eq .CurrentUser.Role "admin"}}
<a href="/admin" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700" data-i18n="nav.admin">Admin</a>
{{end}}
<form action="/logout" method="post" class="px-2">
<button type="submit" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-red-700 hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-900/40" data-i18n="nav.logout">Logout</button>
</form>
</div>
</div>
</div>
{{end}}
{{end}}

View File

@ -19,13 +19,13 @@ npm run build
```
Output in `dist/`:
- `omnimed-web-components.es.js`
- `omnimed-web-components.iife.js`
- `user-menu.es.js`
- `user-menu.iife.js`
## Uso nel browser
```html
<link rel="stylesheet" href="/path/omnimed-web-components.css" />
<script type="module" src="/path/omnimed-web-components.es.js"></script>
<link rel="stylesheet" href="/path/user-menu.css" />
<script type="module" src="/path/user-menu.es.js"></script>
<trustcontact-greeting name="Fabio"></trustcontact-greeting>
```

View File

@ -7,7 +7,24 @@
</head>
<body>
<h1>Web Components Playground</h1>
<trustcontact-greeting name="Fabio"></trustcontact-greeting>
<user-menu target="user-dropdown" pos="bl" sr="Open user menu"></user-menu>
<div id="user-dropdown" style="outline:auto;" class="z-50 my-4 w-56 list-none divide-y divide-gray-100 rounded-lg bg-white text-base shadow-sm dark:divide-gray-700 dark:bg-gray-800 ">
<div class="px-4 py-3">
<span class="block truncate text-sm text-gray-900 dark:text-gray-100">Admin User</span>
<span class="block truncate text-sm text-gray-500 dark:text-gray-400">admin@example.com</span>
</div>
<div class="py-2">
<a href="/private" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700">Private</a>
<a href="/admin" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-700" data-i18n="nav.admin">Admin</a>
<form action="/logout" method="post" class="px-2">
<button type="submit" class="block w-full rounded-lg px-2 py-2 text-left text-sm text-red-700 hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-900/40" data-i18n="nav.logout">Abmelden</button>
</form>
</div>
</div>
<script type="module" src="/src/playground.ts"></script>
</body>
</html>

View File

@ -1,18 +1,23 @@
{
"name": "omnimed-web-components",
"name": "user-menu",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"tw:build": "tailwindcss -i ./src/style.css -o ./dist/tailwind.css --minify",
"tw:watch": "tailwindcss -i ./src/style.css -o ./dist/tailwind.css --watch"
},
"dependencies": {
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"vite": "^6.0.7"
}

View File

@ -14,16 +14,29 @@ importers:
devDependencies:
'@vitejs/plugin-vue':
specifier: ^5.2.1
version: 5.2.4(vite@6.4.1)(vue@3.5.29(typescript@5.9.3))
version: 5.2.4(vite@6.4.1(jiti@1.21.7))(vue@3.5.29(typescript@5.9.3))
autoprefixer:
specifier: ^10.4.20
version: 10.4.27(postcss@8.5.6)
postcss:
specifier: ^8.4.47
version: 8.5.6
tailwindcss:
specifier: ^3.4.17
version: 3.4.19
typescript:
specifier: ^5.7.2
version: 5.9.3
vite:
specifier: ^6.0.7
version: 6.4.1
version: 6.4.1(jiti@1.21.7)
packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
@ -197,9 +210,31 @@ packages:
cpu: [x64]
os: [win32]
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
'@nodelib/fs.stat@2.0.5':
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
'@nodelib/fs.walk@1.2.8':
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@rollup/rollup-android-arm-eabi@4.59.0':
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
cpu: [arm]
@ -364,9 +399,73 @@ packages:
'@vue/shared@3.5.29':
resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
autoprefixer@10.4.27:
resolution: {integrity: sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
postcss: ^8.1.0
baseline-browser-mapping@2.10.0:
resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==}
engines: {node: '>=6.0.0'}
hasBin: true
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
browserslist@4.28.1:
resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
caniuse-lite@1.0.30001775:
resolution: {integrity: sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
electron-to-chromium@1.5.302:
resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==}
entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'}
@ -376,9 +475,20 @@ packages:
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@ -388,48 +498,244 @@ packages:
picomatch:
optional: true
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-core-module@2.16.1:
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
engines: {node: '>= 0.4'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
peerDependencies:
postcss: ^8.0.0
postcss-js@4.1.0:
resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==}
engines: {node: ^12 || ^14 || >= 16}
peerDependencies:
postcss: ^8.4.21
postcss-load-config@6.0.1:
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
engines: {node: '>= 18'}
peerDependencies:
jiti: '>=1.21.0'
postcss: '>=8.0.9'
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
jiti:
optional: true
postcss:
optional: true
tsx:
optional: true
yaml:
optional: true
postcss-nested@6.2.0:
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.2.14
postcss-selector-parser@6.1.2:
resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
resolve@1.22.11:
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
engines: {node: '>= 0.4'}
hasBin: true
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rollup@4.59.0:
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
sucrase@3.35.1:
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
tailwindcss@3.4.19:
resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==}
engines: {node: '>=14.0.0'}
hasBin: true
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vite@6.4.1:
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -480,6 +786,8 @@ packages:
snapshots:
'@alloc/quick-lru@5.2.0': {}
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.28.5': {}
@ -571,8 +879,32 @@ snapshots:
'@esbuild/win32-x64@0.25.12':
optional: true
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
'@nodelib/fs.stat@2.0.5': {}
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@rollup/rollup-android-arm-eabi@4.59.0':
optional: true
@ -650,9 +982,9 @@ snapshots:
'@types/estree@1.0.8': {}
'@vitejs/plugin-vue@5.2.4(vite@6.4.1)(vue@3.5.29(typescript@5.9.3))':
'@vitejs/plugin-vue@5.2.4(vite@6.4.1(jiti@1.21.7))(vue@3.5.29(typescript@5.9.3))':
dependencies:
vite: 6.4.1
vite: 6.4.1(jiti@1.21.7)
vue: 3.5.29(typescript@5.9.3)
'@vue/compiler-core@3.5.29':
@ -709,8 +1041,68 @@ snapshots:
'@vue/shared@3.5.29': {}
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
arg@5.0.2: {}
autoprefixer@10.4.27(postcss@8.5.6):
dependencies:
browserslist: 4.28.1
caniuse-lite: 1.0.30001775
fraction.js: 5.3.4
picocolors: 1.1.1
postcss: 8.5.6
postcss-value-parser: 4.2.0
baseline-browser-mapping@2.10.0: {}
binary-extensions@2.3.0: {}
braces@3.0.3:
dependencies:
fill-range: 7.1.1
browserslist@4.28.1:
dependencies:
baseline-browser-mapping: 2.10.0
caniuse-lite: 1.0.30001775
electron-to-chromium: 1.5.302
node-releases: 2.0.27
update-browserslist-db: 1.2.3(browserslist@4.28.1)
camelcase-css@2.0.1: {}
caniuse-lite@1.0.30001775: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
commander@4.1.1: {}
cssesc@3.0.0: {}
csstype@3.2.3: {}
didyoumean@1.2.2: {}
dlv@1.1.3: {}
electron-to-chromium@1.5.302: {}
entities@7.0.1: {}
esbuild@0.25.12:
@ -742,31 +1134,165 @@ snapshots:
'@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 0.25.12
escalade@3.2.0: {}
estree-walker@2.0.2: {}
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.8
fastq@1.20.1:
dependencies:
reusify: 1.1.0
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
fraction.js@5.3.4: {}
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
is-core-module@2.16.1:
dependencies:
hasown: 2.0.2
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
jiti@1.21.7: {}
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
merge2@1.4.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.11: {}
node-releases@2.0.27: {}
normalize-path@3.0.0: {}
object-assign@4.1.1: {}
object-hash@3.0.0: {}
path-parse@1.0.7: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@4.0.3: {}
pify@2.3.0: {}
pirates@4.0.7: {}
postcss-import@15.1.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.11
postcss-js@4.1.0(postcss@8.5.6):
dependencies:
camelcase-css: 2.0.1
postcss: 8.5.6
postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
jiti: 1.21.7
postcss: 8.5.6
postcss-nested@6.2.0(postcss@8.5.6):
dependencies:
postcss: 8.5.6
postcss-selector-parser: 6.1.2
postcss-selector-parser@6.1.2:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
queue-microtask@1.2.3: {}
read-cache@1.0.0:
dependencies:
pify: 2.3.0
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
resolve@1.22.11:
dependencies:
is-core-module: 2.16.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
reusify@1.1.0: {}
rollup@4.59.0:
dependencies:
'@types/estree': 1.0.8
@ -798,16 +1324,82 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.59.0
fsevents: 2.3.3
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
source-map-js@1.2.1: {}
sucrase@3.35.1:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
commander: 4.1.1
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.7
tinyglobby: 0.2.15
ts-interface-checker: 0.1.13
supports-preserve-symlinks-flag@1.0.0: {}
tailwindcss@3.4.19:
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
chokidar: 3.6.0
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.3.3
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.21.7
lilconfig: 3.1.3
micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.1.1
postcss: 8.5.6
postcss-import: 15.1.0(postcss@8.5.6)
postcss-js: 4.1.0(postcss@8.5.6)
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)
postcss-nested: 6.2.0(postcss@8.5.6)
postcss-selector-parser: 6.1.2
resolve: 1.22.11
sucrase: 3.35.1
transitivePeerDependencies:
- tsx
- yaml
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
ts-interface-checker@0.1.13: {}
typescript@5.9.3: {}
vite@6.4.1:
update-browserslist-db@1.2.3(browserslist@4.28.1):
dependencies:
browserslist: 4.28.1
escalade: 3.2.0
picocolors: 1.1.1
util-deprecate@1.0.2: {}
vite@6.4.1(jiti@1.21.7):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
@ -817,6 +1409,7 @@ snapshots:
tinyglobby: 0.2.15
optionalDependencies:
fsevents: 2.3.3
jiti: 1.21.7
vue@3.5.29(typescript@5.9.3):
dependencies:

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1,39 +0,0 @@
<template>
<section class="card">
<p class="title">Hello {{ safeName }}</p>
<p class="subtitle">This is a Vue 3 custom element.</p>
</section>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{ name?: string }>();
const safeName = computed(() => (props.name && props.name.trim()) || 'there');
</script>
<style scoped>
.card {
display: inline-flex;
flex-direction: column;
gap: 0.25rem;
border: 1px solid #d1d5db;
border-radius: 0.75rem;
padding: 0.75rem 1rem;
font-family: Inter, system-ui, -apple-system, sans-serif;
background: #ffffff;
color: #111827;
}
.title {
margin: 0;
font-weight: 700;
}
.subtitle {
margin: 0;
color: #4b5563;
font-size: 0.875rem;
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<button
id="user-menu-button"
@click="openModal"
type="button"
class="inline-flex h-8 items-center rounded-lg bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:bg-gray-800 dark:text-gray-100 dark:hover:bg-gray-700 dark:focus:ring-gray-700"
>
<span class="sr-only" data-i18n="nav.open_user_menu">{{ props.sr || 'Open user menu' }}</span>
<span style="pointer-events: none;" class="inline-flex items-center justify-center">
<svg class="h-4 w-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"></path>
</svg>
</span>
</button>
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue';
const props = defineProps<{ target?: string, pos?: string, sr?:string}>();
const allowedPositions = ['tr', 'tl', 'br', 'bl'];
const safePos = computed(() => {
if (props.pos && allowedPositions.includes(props.pos)) {
return props.pos;
}
return 'br';
});
onMounted(() => {
if (props.target) {
const elem = document.getElementById(props.target);
if (elem) {
elem.style.position = 'absolute';
elem.style.left = `-9999px`;
elem.setAttribute('isopen', 'false');
}
}
});
const openModal = (event: MouseEvent) => {
event.stopPropagation();
if (props.target) {
const elem = document.getElementById(props.target);
if (elem) {
if (elem.getAttribute('isopen') === 'false') {
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
const myRect = elem.getBoundingClientRect();
if(safePos.value.includes('b')) {
elem.style.top = `${rect.bottom - 4}px`;
}else {
elem.style.top = `${rect.top - myRect.height}px`;
}
if(safePos.value.includes('r')) {
elem.style.left = `${rect.right - myRect.width + 2}px`;
}else {
elem.style.left = `${rect.left}px`;
}
elem.setAttribute('isopen', 'true' );
}else {
elem.style.left = `-9999px`
elem.setAttribute('isopen', 'false');
}
}
}
};
</script>

View File

@ -1,11 +1,19 @@
import { defineCustomElement } from 'vue';
import GreetingElement from './components/Greeting.ce.vue';
import UserMenuElement from './components/UserMenu.ce.vue';
import './style.css';
const TAG_NAME = 'trustcontact-greeting';
const TAG_NAME = 'user-menu';
export function registerWebComponents(): void {
if (!customElements.get(TAG_NAME)) {
customElements.define(TAG_NAME, defineCustomElement(GreetingElement));
customElements.define(
TAG_NAME,
defineCustomElement(UserMenuElement, {
// Tailwind is generated as global CSS; without Shadow DOM
// utility classes apply correctly inside the custom element.
shadowRoot: false,
}),
);
}
}

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
darkMode: 'class',
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',

View File

@ -2,6 +2,9 @@ import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
plugins: [
vue({
customElement: true,
@ -10,9 +13,9 @@ export default defineConfig({
build: {
lib: {
entry: 'src/register.ts',
name: 'OmnimedWebComponents',
name: 'UserMenu',
formats: ['es', 'iife'],
fileName: (format) => `omnimed-web-components.${format}.js`,
fileName: (format) => `user-menu.${format}.js`,
},
},
});