tailwind c ss

This commit is contained in:
fabio 2026-02-22 18:31:19 +01:00
parent 81245535b3
commit 0cd6ce05cd
15 changed files with 4742 additions and 89 deletions

View File

@ -1,14 +1,20 @@
.PHONY: dev ui-build ui-dev test db-reset fmt .PHONY: dev ui-build ui-dev css-build css-dev test db-reset fmt
dev: dev:
go run ./cmd/server go run ./cmd/server
ui-build: ui-build:
cd ui-kit && npm i && npm run build cd ui-kit && npm i && npm run build && npm run css:build
ui-dev: ui-dev:
cd ui-kit && npm i && npm run dev cd ui-kit && npm i && npm run dev
css-build:
cd ui-kit && npm i && npm run css:build
css-dev:
cd ui-kit && npm i && npm run css:dev
test: test:
go test ./... go test ./...

View File

@ -6,6 +6,8 @@ Boilerplate GoFiber MVC + HTMX + Svelte Custom Elements + GORM, con auth server-
```bash ```bash
cp .env.example .env cp .env.example .env
make css-build
make ui-build
make dev make dev
``` ```
@ -35,22 +37,26 @@ DB_PG_DSN=postgres://trustcontact:trustcontact@localhost:5432/trustcontact?sslmo
`DB_POSTGRES_DSN` è comunque supportato. `DB_POSTGRES_DSN` è comunque supportato.
## UI Kit Build ## Tailwind + UI Kit
Tailwind (template server-rendered) compila in `web/static/css/app.css`.
UI kit (Svelte custom elements) compila in `web/static/ui`.
Comandi:
```bash ```bash
make ui-build make css-build # build tailwind
make css-dev # watch tailwind
make ui-build # build ui-kit + css tailwind
make ui-dev # vite dev server ui-kit
``` ```
Per sviluppo UI: Layout include:
```bash - `/static/css/app.css?v={{.BuildHash}}`
make ui-dev - `/static/ui/ui.css?v={{.BuildHash}}`
``` - `/static/ui/ui.esm.js?v={{.BuildHash}}`
Output build in `web/static/ui`:
- `ui.esm.js`
- `ui.css`
## Template Directories ## Template Directories
@ -65,8 +71,10 @@ In `develop`, le email vengono salvate in `./data/emails`.
## Make Targets ## Make Targets
- `make dev` -> `go run ./cmd/server` - `make dev` -> `go run ./cmd/server`
- `make ui-build` -> install + build ui-kit - `make ui-build` -> install + build ui-kit + build css tailwind
- `make ui-dev` -> watch UI con Vite - `make ui-dev` -> watch UI con Vite
- `make css-build` -> build Tailwind CSS
- `make css-dev` -> watch Tailwind CSS
- `make test` -> `go test ./...` - `make test` -> `go test ./...`
- `make db-reset` -> reset DB sqlite locale (`./data/app.db` / `./data/app.sqlite3`) - `make db-reset` -> reset DB sqlite locale (`./data/app.db` / `./data/app.sqlite3`)
- `make fmt` -> `gofmt` su `cmd/` e `internal/` - `make fmt` -> `gofmt` su `cmd/` e `internal/`

1309
ui-kit/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -6,11 +6,16 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"css:build": "tailwindcss -c tailwind.config.cjs -i ./styles/tailwind.css -o ../web/static/css/app.css --minify",
"css:dev": "tailwindcss -c tailwind.config.cjs -i ./styles/tailwind.css -o ../web/static/css/app.css --watch"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"svelte": "^5.0.0", "svelte": "^5.0.0",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.0", "typescript": "^5.6.0",
"vite": "^5.4.0" "vite": "^5.4.0"
} }

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View File

@ -0,0 +1,31 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply m-0 bg-slate-100 text-slate-800;
}
a {
@apply text-slate-800;
}
}
@layer components {
.row {
@apply flex flex-wrap gap-2;
}
.muted {
@apply text-sm text-slate-500;
}
.btn-primary {
@apply rounded-lg bg-slate-900 px-4 py-2 text-white hover:bg-slate-700;
}
.input-base {
@apply rounded-lg border border-slate-300 px-3 py-2;
}
}

View File

@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{svelte,ts,js}',
'../web/templates/**/*.html'
],
theme: {
extend: {}
},
plugins: []
};

View File

@ -16,8 +16,17 @@ export default defineConfig({
lib: { lib: {
entry: 'src/index.ts', entry: 'src/index.ts',
formats: ['es'], formats: ['es'],
fileName: () => 'ui.esm.js', fileName: () => 'ui.esm.js'
cssFileName: 'ui' },
rollupOptions: {
output: {
assetFileNames: (assetInfo) => {
if (assetInfo.name === 'style.css') {
return 'ui.css';
}
return '[name][extname]';
}
}
} }
} }
}); });

2
web/static/css/app.css Normal file
View File

@ -0,0 +1,2 @@
/* Generated by: cd ui-kit && npm run css:build */
/* Placeholder committed for first run. */

View File

1
web/static/ui/ui.css Normal file
View File

@ -0,0 +1 @@
:root{--ui-bg: #ffffff;--ui-fg: #111827;--ui-muted: #6b7280;--ui-border: #d1d5db;--ui-overlay: rgba(17, 24, 39, .56);--ui-panel: #ffffff;--ui-radius: 10px;--ui-shadow: 0 10px 35px rgba(15, 23, 42, .2);--ui-primary: #111827;--ui-primary-contrast: #ffffff}ui-modal,ui-drop-down,ui-data-table-shell{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color:var(--ui-fg)}

3269
web/static/ui/ui.esm.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,10 @@
{{define "content"}} {{define "content"}}
<h1>Admin Dashboard</h1> <div class="space-y-3">
<p class="muted">Area amministrazione.</p> <h1 class="text-2xl font-semibold">Admin Dashboard</h1>
<div class="row"> <p class="muted">Area amministrazione.</p>
<a href="/admin/users">Gestione utenti</a> <div class="row">
<a href="/users">Vista utenti (private)</a> <a href="/admin/users" class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50">Gestione utenti</a>
<a href="/users" class="rounded-lg border border-slate-300 px-4 py-2 hover:bg-slate-50">Vista utenti (private)</a>
</div>
</div> </div>
{{end}} {{end}}

View File

@ -1,53 +1,59 @@
{{define "content"}} {{define "content"}}
<h1>Admin - Users</h1> <div class="space-y-4">
<p class="muted">Elenco utenti server-rendered.</p> <div>
<h1 class="text-2xl font-semibold">Admin - Users</h1>
<p class="muted">Elenco utenti server-rendered.</p>
</div>
<form class="row" method="get" action="/admin/users"> <form class="row items-center" method="get" action="/admin/users">
<input type="text" name="q" placeholder="Cerca nome o email" value="{{.PageData.Q}}"> <input class="input-base" type="text" name="q" placeholder="Cerca nome o email" value="{{.PageData.Q}}">
<select name="sort" style="padding:10px;border:1px solid #d1d5db;border-radius:8px;"> <select class="input-base" name="sort">
<option value="id" {{if eq .PageData.Sort "id"}}selected{{end}}>ID</option> <option value="id" {{if eq .PageData.Sort "id"}}selected{{end}}>ID</option>
<option value="name" {{if eq .PageData.Sort "name"}}selected{{end}}>Name</option> <option value="name" {{if eq .PageData.Sort "name"}}selected{{end}}>Name</option>
<option value="email" {{if eq .PageData.Sort "email"}}selected{{end}}>Email</option> <option value="email" {{if eq .PageData.Sort "email"}}selected{{end}}>Email</option>
</select> </select>
<select name="dir" style="padding:10px;border:1px solid #d1d5db;border-radius:8px;"> <select class="input-base" name="dir">
<option value="asc" {{if eq .PageData.Dir "asc"}}selected{{end}}>ASC</option> <option value="asc" {{if eq .PageData.Dir "asc"}}selected{{end}}>ASC</option>
<option value="desc" {{if eq .PageData.Dir "desc"}}selected{{end}}>DESC</option> <option value="desc" {{if eq .PageData.Dir "desc"}}selected{{end}}>DESC</option>
</select> </select>
<input type="number" name="pageSize" min="1" max="100" value="{{.PageData.PageSize}}" style="max-width:120px;"> <input class="input-base w-28" type="number" name="pageSize" min="1" max="100" value="{{.PageData.PageSize}}">
<input type="hidden" name="page" value="1"> <input type="hidden" name="page" value="1">
<button type="submit">Filtra</button> <button class="btn-primary" type="submit">Filtra</button>
</form> </form>
<table style="width:100%;border-collapse:collapse;margin-top:16px;"> <div class="overflow-x-auto">
<thead> <table class="w-full border-collapse">
<tr> <thead>
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">ID</th> <tr class="border-b border-slate-200 text-left">
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Name</th> <th class="px-2 py-2">ID</th>
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Email</th> <th class="px-2 py-2">Name</th>
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Role</th> <th class="px-2 py-2">Email</th>
<th style="text-align:left;border-bottom:1px solid #e5e7eb;padding:8px;">Verified</th> <th class="px-2 py-2">Role</th>
</tr> <th class="px-2 py-2">Verified</th>
</thead> </tr>
<tbody> </thead>
{{range .PageData.Users}} <tbody>
<tr> {{range .PageData.Users}}
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{.ID}}</td> <tr class="border-b border-slate-100">
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{if .Name}}{{.Name}}{{else}}-{{end}}</td> <td class="px-2 py-2">{{.ID}}</td>
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{.Email}}</td> <td class="px-2 py-2">{{if .Name}}{{.Name}}{{else}}-{{end}}</td>
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{.Role}}</td> <td class="px-2 py-2">{{.Email}}</td>
<td style="border-bottom:1px solid #f1f5f9;padding:8px;">{{if .EmailVerified}}yes{{else}}no{{end}}</td> <td class="px-2 py-2">{{.Role}}</td>
</tr> <td class="px-2 py-2">{{if .EmailVerified}}yes{{else}}no{{end}}</td>
{{else}} </tr>
<tr><td colspan="5" style="padding:12px;">Nessun utente trovato.</td></tr> {{else}}
{{end}} <tr><td colspan="5" class="px-2 py-3">Nessun utente trovato.</td></tr>
</tbody> {{end}}
</table> </tbody>
</table>
</div>
<div class="row" style="margin-top:12px;align-items:center;justify-content:space-between;"> <div class="row items-center justify-between">
<div class="muted">Totale: {{.PageData.Total}} utenti. Pagina {{.PageData.Page}}{{if gt .PageData.TotalPages 0}} / {{.PageData.TotalPages}}{{end}}</div> <div class="muted">Totale: {{.PageData.Total}} utenti. Pagina {{.PageData.Page}}{{if gt .PageData.TotalPages 0}} / {{.PageData.TotalPages}}{{end}}</div>
<div class="row"> <div class="row">
<a {{if not .PageData.HasPrev}}style="pointer-events:none;opacity:.5;"{{end}} href="/admin/users?q={{.PageData.Q}}&sort={{.PageData.Sort}}&dir={{.PageData.Dir}}&page={{.PageData.PrevPage}}&pageSize={{.PageData.PageSize}}">Prev</a> <a class="rounded border border-slate-300 px-3 py-1.5 hover:bg-slate-50 {{if not .PageData.HasPrev}}pointer-events-none opacity-50{{end}}" href="/admin/users?q={{.PageData.Q}}&sort={{.PageData.Sort}}&dir={{.PageData.Dir}}&page={{.PageData.PrevPage}}&pageSize={{.PageData.PageSize}}">Prev</a>
<a {{if not .PageData.HasNext}}style="pointer-events:none;opacity:.5;"{{end}} href="/admin/users?q={{.PageData.Q}}&sort={{.PageData.Sort}}&dir={{.PageData.Dir}}&page={{.PageData.NextPage}}&pageSize={{.PageData.PageSize}}">Next</a> <a class="rounded border border-slate-300 px-3 py-1.5 hover:bg-slate-50 {{if not .PageData.HasNext}}pointer-events-none opacity-50{{end}}" href="/admin/users?q={{.PageData.Q}}&sort={{.PageData.Sort}}&dir={{.PageData.Dir}}&page={{.PageData.NextPage}}&pageSize={{.PageData.PageSize}}">Next</a>
</div>
</div> </div>
</div> </div>
{{end}} {{end}}

View File

@ -4,40 +4,28 @@
<meta charset="utf-8"> <meta charset="utf-8">
<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>
<style> <link rel="stylesheet" href="/static/css/app.css?v={{.BuildHash}}">
body { font-family: system-ui, sans-serif; margin: 0; background: #f5f7fb; color: #1f2937; }
nav { background: #111827; color: #fff; padding: 12px 16px; display: flex; gap: 12px; }
nav a { color: #e5e7eb; text-decoration: none; }
nav a.active { color: #fff; font-weight: 600; }
.container { max-width: 920px; margin: 20px auto; padding: 0 16px; }
.card { background: #fff; border-radius: 10px; padding: 20px; }
form { display: grid; gap: 10px; max-width: 420px; }
input { padding: 10px; border: 1px solid #d1d5db; border-radius: 8px; }
button { padding: 10px 14px; border: 0; border-radius: 8px; background: #111827; color: #fff; cursor: pointer; }
.muted { color: #6b7280; font-size: 0.95rem; }
.row { display: flex; gap: 10px; flex-wrap: wrap; }
</style>
<link rel="stylesheet" href="/static/ui/ui.css?v={{.BuildHash}}"> <link rel="stylesheet" href="/static/ui/ui.css?v={{.BuildHash}}">
<script src="https://unpkg.com/htmx.org@1.9.12"></script> <script src="https://unpkg.com/htmx.org@1.9.12"></script>
<script type="module" src="/static/ui/ui.esm.js?v={{.BuildHash}}"></script> <script type="module" src="/static/ui/ui.esm.js?v={{.BuildHash}}"></script>
</head> </head>
<body> <body>
<nav> <nav class="flex gap-3 bg-slate-900 px-4 py-3 text-white">
<a href="/" class="{{if eq .NavSection "public"}}active{{end}}">Public</a> <a href="/" class="text-slate-200 hover:text-white {{if eq .NavSection "public"}}font-semibold text-white{{end}}">Public</a>
<a href="/private" class="{{if eq .NavSection "private"}}active{{end}}">Private</a> <a href="/private" class="text-slate-200 hover:text-white {{if eq .NavSection "private"}}font-semibold text-white{{end}}">Private</a>
{{if and .CurrentUser (eq .CurrentUser.Role "admin")}} {{if and .CurrentUser (eq .CurrentUser.Role "admin")}}
<a href="/admin" class="{{if eq .NavSection "admin"}}active{{end}}">Admin</a> <a href="/admin" class="text-slate-200 hover:text-white {{if eq .NavSection "admin"}}font-semibold text-white{{end}}">Admin</a>
{{end}} {{end}}
{{if .CurrentUser}} {{if .CurrentUser}}
<form action="/logout" method="post" style="margin-left:auto;"> <form action="/logout" method="post" class="ml-auto">
<button type="submit">Logout</button> <button type="submit" class="btn-primary">Logout</button>
</form> </form>
{{end}} {{end}}
</nav> </nav>
<div class="container"> <div class="mx-auto my-5 max-w-5xl px-4">
{{template "_flash.html" .}} {{template "_flash.html" .}}
<div class="card"> <div class="rounded-xl bg-white p-5 shadow-sm">
{{template "content" .}} {{template "content" .}}
</div> </div>
</div> </div>