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:
go run ./cmd/server
ui-build:
cd ui-kit && npm i && npm run build
cd ui-kit && npm i && npm run build && npm run css:build
ui-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:
go test ./...

View File

@ -6,6 +6,8 @@ Boilerplate GoFiber MVC + HTMX + Svelte Custom Elements + GORM, con auth server-
```bash
cp .env.example .env
make css-build
make ui-build
make dev
```
@ -35,22 +37,26 @@ DB_PG_DSN=postgres://trustcontact:trustcontact@localhost:5432/trustcontact?sslmo
`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
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
make ui-dev
```
Output build in `web/static/ui`:
- `ui.esm.js`
- `ui.css`
- `/static/css/app.css?v={{.BuildHash}}`
- `/static/ui/ui.css?v={{.BuildHash}}`
- `/static/ui/ui.esm.js?v={{.BuildHash}}`
## Template Directories
@ -65,8 +71,10 @@ In `develop`, le email vengono salvate in `./data/emails`.
## Make Targets
- `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 css-build` -> build Tailwind CSS
- `make css-dev` -> watch Tailwind CSS
- `make test` -> `go test ./...`
- `make db-reset` -> reset DB sqlite locale (`./data/app.db` / `./data/app.sqlite3`)
- `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": {
"dev": "vite",
"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": {
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"svelte": "^5.0.0",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.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: {
entry: 'src/index.ts',
formats: ['es'],
fileName: () => 'ui.esm.js',
cssFileName: 'ui'
fileName: () => 'ui.esm.js'
},
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"}}
<h1>Admin Dashboard</h1>
<p class="muted">Area amministrazione.</p>
<div class="row">
<a href="/admin/users">Gestione utenti</a>
<a href="/users">Vista utenti (private)</a>
<div class="space-y-3">
<h1 class="text-2xl font-semibold">Admin Dashboard</h1>
<p class="muted">Area amministrazione.</p>
<div class="row">
<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>
{{end}}

View File

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

View File

@ -4,40 +4,28 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title>
<style>
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/css/app.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 type="module" src="/static/ui/ui.esm.js?v={{.BuildHash}}"></script>
</head>
<body>
<nav>
<a href="/" class="{{if eq .NavSection "public"}}active{{end}}">Public</a>
<a href="/private" class="{{if eq .NavSection "private"}}active{{end}}">Private</a>
<nav class="flex gap-3 bg-slate-900 px-4 py-3 text-white">
<a href="/" class="text-slate-200 hover:text-white {{if eq .NavSection "public"}}font-semibold text-white{{end}}">Public</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")}}
<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}}
{{if .CurrentUser}}
<form action="/logout" method="post" style="margin-left:auto;">
<button type="submit">Logout</button>
<form action="/logout" method="post" class="ml-auto">
<button type="submit" class="btn-primary">Logout</button>
</form>
{{end}}
</nav>
<div class="container">
<div class="mx-auto my-5 max-w-5xl px-4">
{{template "_flash.html" .}}
<div class="card">
<div class="rounded-xl bg-white p-5 shadow-sm">
{{template "content" .}}
</div>
</div>