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: tw-build:
npm run tw:build npm run tw:build
@ -6,13 +6,10 @@ tw-build:
tw-watch: tw-watch:
npm run 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: flags-copy:
mkdir -p web/static/vendor/flags && cp assets/flags/*.svg web/static/vendor/flags/ 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: server:
go run ./cmd/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 6) Layout
- Aggiornare /web/templates/layout.html per includere: - Aggiornare /web/templates/layout.html per includere:
<link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}"> <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> <script src="/static/vendor/flowbite.js"></script>
- Rimuovere dal layout riferimenti attivi al vecchio UI kit Svelte (ma non cancellare /ui-kit dal repo) - 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: Criteri:
- make assets genera CSS e copia flowbite.js - make assets genera CSS e copia flowbite.js
- modals/dropdowns Flowbite funzionano - modals/dropdowns Flowbite funzionano
- progetto compilabile e avviabile. - progetto compilabile e avviabile.

View File

@ -30,7 +30,7 @@ Vincoli:
- In /web/templates/layout.html: - In /web/templates/layout.html:
- includere theme.js nel <head> prima del CSS per evitare FOUC: - includere theme.js nel <head> prima del CSS per evitare FOUC:
<script src="/static/vendor/theme.js"></script> <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: - aggiungere classi base al body per dark:
- bg-white dark:bg-gray-900 - bg-white dark:bg-gray-900
- text-gray-900 dark:text-gray-100 - text-gray-900 dark:text-gray-100
@ -76,4 +76,4 @@ Non serve perfezione totale, ma assicurare leggibilità.
Esegui: Esegui:
- make tw-build (o make tw-watch per verificare) - make tw-build (o make tw-watch per verificare)
- Avvia server e verifica cambio tema su /login e /users. - 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 - Dropdown utente con logout
- Include: - Include:
<link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}"> <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> <script src="/static/vendor/flowbite.js"></script>
Struttura: Struttura:
@ -60,9 +60,9 @@ Usare:
- Modal Flowbite per dettaglio utente - Modal Flowbite per dettaglio utente
Assicurarsi che: Assicurarsi che:
- hx-get - fetch/get
- hx-target - target DOM update
- hx-swap - swap HTML manuale
rimangano funzionanti rimangano funzionanti
------------------------------------- -------------------------------------
@ -138,4 +138,4 @@ Usare nelle pagine private.
- Nessun errore JS in console - Nessun errore JS in console
Scrivere codice pulito, leggibile, con commenti minimi. 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): Routes protette (RequireAuth):
- GET /users -> pagina con search + container tabella - 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 - GET /users/:id/modal -> partial HTML contenuto modal
Requisiti tabella: Requisiti tabella:
- query params: q, sort (id|name|email whitelist), dir (asc|desc), page, pageSize - query params: q, sort (id|name|email whitelist), dir (asc|desc), page, pageSize
- server-driven paging/sort/search usando GORM (Count + Limit/Offset + Order) - server-driven paging/sort/search usando GORM (Count + Limit/Offset + Order)
- _table.html deve includere: - _table.html deve includere:
- header th cliccabili con hx-get (toggle dir) - header th cliccabili con fetch GET (toggle dir)
- pager prev/next con hx-get - pager prev/next con fetch GET
- bottone “Apri” che hx-get sul modal e hx-target="#userModal" hx-swap="innerHTML" - bottone “Apri” che aggiorna `#userModal` via fetch + `innerHTML`
- apri modal via JS minimal: setAttribute('open','') dopo swap (o onclick) - apri modal via JS minimal: setAttribute('open','') dopo swap (o onclick)
Crea template: Crea template:
@ -19,4 +19,4 @@ Crea template:
- private/users/_table.html - private/users/_table.html
- private/users/_modal.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: - src/index.ts registra:
- ui-modal - ui-modal
- ui-drop-down - ui-drop-down
- ui-data-table-shell (driver htmx per aggiornare un target) - ui-data-table-shell (driver JS per aggiornare un target)
Componenti: Componenti:
1) UiModal.svelte: 1) UiModal.svelte:
@ -26,8 +26,8 @@ Componenti:
3) UiDataTableShell.svelte: 3) UiDataTableShell.svelte:
- attributi: endpoint, target, page-size - 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 - non renderizza tabella, solo toolbar
Aggiorna layout per includere /static/ui/ui.css e /static/ui/ui.esm.js con ?v={{.BuildHash}}. 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"), PostgresDSN: envFirstNonEmpty("DB_POSTGRES_DSN", "DB_PG_DSN"),
CORS: CORSConfig{ CORS: CORSConfig{
Origins: envListOrDefault("CORS_ORIGINS", []string{"http://localhost:3000"}), 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"}), Methods: envListOrDefault("CORS_METHODS", []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
Credentials: envBoolOrDefault("CORS_CREDENTIALS", true), Credentials: envBoolOrDefault("CORS_CREDENTIALS", true),
}, },

View File

@ -1,7 +1,9 @@
package controllers package controllers
import ( import (
"bytes"
"errors" "errors"
"html/template"
"strings" "strings"
httpmw "trustcontact/internal/http/middleware" httpmw "trustcontact/internal/http/middleware"
@ -261,3 +263,29 @@ func (ac *AuthController) UpdateTheme(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent) 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/layout.html",
"web/templates/public/_navbar.html", "web/templates/public/_navbar.html",
"web/templates/partials/language_dropdown.html", "web/templates/partials/language_dropdown.html",
"web/templates/partials/user_dropdown.html",
"web/templates/public/_flash.html", "web/templates/public/_flash.html",
filepath.Join("web/templates/public", page), 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/layout.html",
"web/templates/public/_navbar.html", "web/templates/public/_navbar.html",
"web/templates/partials/language_dropdown.html", "web/templates/partials/language_dropdown.html",
"web/templates/partials/user_dropdown.html",
"web/templates/public/_flash.html", "web/templates/public/_flash.html",
"web/templates/private.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 { func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg *config.Config) error {
app.Static("/static", "web/static") app.Static("/static", "web/static")
app.Static("/web-components", "web_components/dist")
app.Use(httpmw.SessionStoreMiddleware(store)) app.Use(httpmw.SessionStoreMiddleware(store))
app.Use(httpmw.CurrentUserMiddleware(store, database)) 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.Get("/forbidden", authController.ShowForbidden)
app.Post("/preferences/lang", httpmw.RequireAuth(), authController.UpdateLanguage) app.Post("/preferences/lang", httpmw.RequireAuth(), authController.UpdateLanguage)
app.Post("/preferences/theme", httpmw.RequireAuth(), authController.UpdateTheme) 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). // Quasar admin SPA assets are emitted with absolute paths (/assets, /icons, /favicon.ico).
// Protect them with the same auth/admin middleware used by /admin. // 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", "name": "trustcontact-flowbite",
"version": "1.0.0", "version": "1.0.0",
"dependencies": {
"htmx.org": "^2.0.6"
},
"devDependencies": { "devDependencies": {
"@tailwindcss/cli": "^4.1.13", "@tailwindcss/cli": "^4.1.13",
"tailwindcss": "^4.1.13" "tailwindcss": "^4.1.13"
@ -681,12 +678,6 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",

View File

@ -9,8 +9,5 @@
"devDependencies": { "devDependencies": {
"@tailwindcss/cli": "^4.1.13", "@tailwindcss/cli": "^4.1.13",
"tailwindcss": "^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"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title> <title>{{.Title}}</title>
<script> <script>
window.__TC_IS_AUTHENTICATED = {{if .CurrentUser}}true{{else}}false{{end}}; window.__TC_IS_AUTHENTICATED = '{{if .CurrentUser}}true{{else}}false{{end}}';
window.__TC_SERVER_THEME = {{printf "%q" .UserTheme}}; window.__TC_SERVER_THEME = '{{printf "%q" .UserTheme}}';
</script> </script>
<script src="/static/vendor/theme.js?v={{.BuildHash}}"></script> <script src="/static/vendor/theme.js?v={{.BuildHash}}"></script>
<link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}"> <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> </head>
<body class="flex min-h-screen flex-col bg-white text-gray-900 antialiased dark:bg-gray-900 dark:text-gray-100"> <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 () { (function () {
var DEFAULT_LANG = 'it'; var DEFAULT_LANG = 'it';
var STORAGE_KEY = 'tc_lang'; var STORAGE_KEY = 'tc_lang';
var SERVER_LANG = {{printf "%q" .UserLang}}; var SERVER_LANG = '{{printf "%q" .UserLang}}';
var IS_AUTHENTICATED = {{if .CurrentUser}}true{{else}}false{{end}}; var IS_AUTHENTICATED = '{{if .CurrentUser}}true{{else}}false{{end}}';
var dictionaries = { var dictionaries = {
it: { 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', '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', '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', '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', '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.', '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', '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.' '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', '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', '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', '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.', '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', '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.' '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', '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', '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', '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.', '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', '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.' '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', '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', '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', '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.', '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', '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é.' '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 () { langSelect.addEventListener('change', function () {
var selectedLang = normalizeLang(langSelect.value); var selectedLang = normalizeLang(langSelect.value);
localStorage.setItem(STORAGE_KEY, selectedLang); 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); applyTranslations(document);
}); });
} }
function initNavbarComponents(root) { 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) { if (!window.__tcNavbarDocBound) {
window.__tcNavbarDocBound = true; window.__tcNavbarDocBound = true;
document.addEventListener('click', function (event) { document.addEventListener('click', function (event) {
var btn = document.getElementById('user-menu-button');
var menu = document.getElementById('user-dropdown'); var menu = document.getElementById('user-dropdown');
if (!btn || !menu || menu.classList.contains('hidden')) return; if(menu.getAttribute('isopen') === 'false') return; // skip if menu is closed with css, to avoid issues with multiple navbars
if (btn.contains(event.target) || menu.contains(event.target)) return; menu.setAttribute('isopen', 'false');
menu.classList.add('hidden'); menu.style.left = "-9999px";
btn.setAttribute('aria-expanded', 'false');
}); });
document.addEventListener('keydown', function (event) { document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape') return; if (event.key !== 'Escape') return;
var btn = document.getElementById('user-menu-button');
var menu = document.getElementById('user-dropdown'); var menu = document.getElementById('user-dropdown');
if (!btn || !menu) return; if(menu.getAttribute('isopen') === 'false') return; // skip if menu is closed with css, to avoid issues with multiple navbars
menu.classList.add('hidden'); menu.setAttribute('isopen', 'false');
btn.setAttribute('aria-expanded', 'false'); menu.style.left = "-9999px";
}); });
} }
} }
@ -259,13 +241,6 @@
if (typeof window.initThemeToggle === 'function') { if (typeof window.initThemeToggle === 'function') {
window.initThemeToggle(); 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> </script>
</body> </body>

View File

@ -1,7 +1,7 @@
{{define "language_dropdown"}} {{define "language_dropdown"}}
<div class="relative flex items-center gap-2 text-sm"> <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;"> <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="it">Italiano</option>
<option value="en">English</option> <option value="en">English</option>
<option value="en_us">English USA</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"}} {{define "navbar"}}
<nav class="border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900"> <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"> <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> <span class="self-center whitespace-nowrap text-xl font-semibold">Trustcontact</span>
</a> </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"> <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> <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"> <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"}} {{define "navbar_controls"}}
{{template "language_dropdown" .}} {{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}} {{end}}

View File

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

View File

@ -7,7 +7,24 @@
</head> </head>
<body> <body>
<h1>Web Components Playground</h1> <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> <script type="module" src="/src/playground.ts"></script>
</body> </body>
</html> </html>

View File

@ -1,18 +1,23 @@
{ {
"name": "omnimed-web-components", "name": "user-menu",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "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": { "dependencies": {
"vue": "^3.5.13" "vue": "^3.5.13"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^6.0.7" "vite": "^6.0.7"
} }

View File

@ -14,16 +14,29 @@ importers:
devDependencies: devDependencies:
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^5.2.1 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: typescript:
specifier: ^5.7.2 specifier: ^5.7.2
version: 5.9.3 version: 5.9.3
vite: vite:
specifier: ^6.0.7 specifier: ^6.0.7
version: 6.4.1 version: 6.4.1(jiti@1.21.7)
packages: packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@babel/helper-string-parser@7.27.1': '@babel/helper-string-parser@7.27.1':
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@ -197,9 +210,31 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] 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': '@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 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': '@rollup/rollup-android-arm-eabi@4.59.0':
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
cpu: [arm] cpu: [arm]
@ -364,9 +399,73 @@ packages:
'@vue/shared@3.5.29': '@vue/shared@3.5.29':
resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} 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: csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} 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: entities@7.0.1:
resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
@ -376,9 +475,20 @@ packages:
engines: {node: '>=18'} engines: {node: '>=18'}
hasBin: true hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
estree-walker@2.0.2: estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 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: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@ -388,48 +498,244 @@ packages:
picomatch: picomatch:
optional: true 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: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin] 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: magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 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: nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true 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: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.3: picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'} 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: postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14} 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: rollup@4.59.0:
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} 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: tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'} 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: typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true 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: vite@6.4.1:
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -480,6 +786,8 @@ packages:
snapshots: snapshots:
'@alloc/quick-lru@5.2.0': {}
'@babel/helper-string-parser@7.27.1': {} '@babel/helper-string-parser@7.27.1': {}
'@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-identifier@7.28.5': {}
@ -571,8 +879,32 @@ snapshots:
'@esbuild/win32-x64@0.25.12': '@esbuild/win32-x64@0.25.12':
optional: true 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/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': '@rollup/rollup-android-arm-eabi@4.59.0':
optional: true optional: true
@ -650,9 +982,9 @@ snapshots:
'@types/estree@1.0.8': {} '@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: dependencies:
vite: 6.4.1 vite: 6.4.1(jiti@1.21.7)
vue: 3.5.29(typescript@5.9.3) vue: 3.5.29(typescript@5.9.3)
'@vue/compiler-core@3.5.29': '@vue/compiler-core@3.5.29':
@ -709,8 +1041,68 @@ snapshots:
'@vue/shared@3.5.29': {} '@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: {} csstype@3.2.3: {}
didyoumean@1.2.2: {}
dlv@1.1.3: {}
electron-to-chromium@1.5.302: {}
entities@7.0.1: {} entities@7.0.1: {}
esbuild@0.25.12: esbuild@0.25.12:
@ -742,31 +1134,165 @@ snapshots:
'@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-ia32': 0.25.12
'@esbuild/win32-x64': 0.25.12 '@esbuild/win32-x64': 0.25.12
escalade@3.2.0: {}
estree-walker@2.0.2: {} 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): fdir@6.5.0(picomatch@4.0.3):
optionalDependencies: optionalDependencies:
picomatch: 4.0.3 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: fsevents@2.3.3:
optional: true 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: magic-string@0.30.21:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.5 '@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: {} 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: {} picocolors@1.1.1: {}
picomatch@2.3.1: {}
picomatch@4.0.3: {} 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: postcss@8.5.6:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.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: rollup@4.59.0:
dependencies: dependencies:
'@types/estree': 1.0.8 '@types/estree': 1.0.8
@ -798,16 +1324,82 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.59.0 '@rollup/rollup-win32-x64-msvc': 4.59.0
fsevents: 2.3.3 fsevents: 2.3.3
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
source-map-js@1.2.1: {} 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: tinyglobby@0.2.15:
dependencies: dependencies:
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
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: {} 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: dependencies:
esbuild: 0.25.12 esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
@ -817,6 +1409,7 @@ snapshots:
tinyglobby: 0.2.15 tinyglobby: 0.2.15
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 1.21.7
vue@3.5.29(typescript@5.9.3): vue@3.5.29(typescript@5.9.3):
dependencies: 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 { 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 { export function registerWebComponents(): void {
if (!customElements.get(TAG_NAME)) { 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} */ /** @type {import('tailwindcss').Config} */
export default { export default {
darkMode: 'class',
content: [ content: [
'./index.html', './index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}', './src/**/*.{vue,js,ts,jsx,tsx}',

View File

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