prompt 9
This commit is contained in:
parent
70e34465de
commit
e069100c53
|
|
@ -26,6 +26,9 @@ tmp/
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# JS deps
|
||||||
|
ui-kit/node_modules/
|
||||||
|
|
||||||
# Dev data and local state
|
# Dev data and local state
|
||||||
/data/
|
/data/
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
|
|
||||||
41
README.md
41
README.md
|
|
@ -14,6 +14,22 @@ Boilerplate riusabile per:
|
||||||
|
|
||||||
In ambiente `develop`, le email vengono salvate in `./data/emails` (sink locale).
|
In ambiente `develop`, le email vengono salvate in `./data/emails` (sink locale).
|
||||||
|
|
||||||
|
## UI Kit (Vite + Svelte CE)
|
||||||
|
|
||||||
|
Comandi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui-kit
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
La build scrive direttamente in `web/static/ui`:
|
||||||
|
|
||||||
|
- `ui.esm.js`
|
||||||
|
- `ui.css`
|
||||||
|
|
||||||
## Struttura iniziale
|
## Struttura iniziale
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
@ -32,6 +48,7 @@ In ambiente `develop`, le email vengono salvate in `./data/emails` (sink locale)
|
||||||
│ ├── models/
|
│ ├── models/
|
||||||
│ ├── repo/
|
│ ├── repo/
|
||||||
│ └── services/
|
│ └── services/
|
||||||
|
├── ui-kit/
|
||||||
├── web/
|
├── web/
|
||||||
│ ├── emails/
|
│ ├── emails/
|
||||||
│ │ └── templates/
|
│ │ └── templates/
|
||||||
|
|
@ -43,29 +60,5 @@ In ambiente `develop`, le email vengono salvate in `./data/emails` (sink locale)
|
||||||
│ ├── admin/
|
│ ├── admin/
|
||||||
│ ├── private/
|
│ ├── private/
|
||||||
│ └── public/
|
│ └── public/
|
||||||
├── ui-kit/
|
|
||||||
└── data/ # solo sviluppo locale
|
└── data/ # solo sviluppo locale
|
||||||
```
|
```
|
||||||
|
|
||||||
## TODO Checklist
|
|
||||||
|
|
||||||
- [ ] Definire bootstrap server in `cmd/server` (entrypoint + lifecycle).
|
|
||||||
- [ ] Configurare loader config (`env`, `flags`) in `internal/config`.
|
|
||||||
- [ ] Impostare `internal/db` con supporto SQLite (dev) e Postgres (prod).
|
|
||||||
- [ ] Definire modelli base GORM in `internal/models` (User, Role, Session, ecc.).
|
|
||||||
- [ ] Implementare repository layer in `internal/repo`.
|
|
||||||
- [ ] Implementare service layer in `internal/services`.
|
|
||||||
- [ ] Implementare controller layer in `internal/controllers`.
|
|
||||||
- [ ] Configurare router HTTP in `internal/http` (gruppi public/private/admin).
|
|
||||||
- [ ] Aggiungere middleware comuni in `internal/middleware` (logging, recovery, auth, cors).
|
|
||||||
- [ ] Implementare auth in `internal/auth` (login/logout/session o token).
|
|
||||||
- [ ] Implementare RBAC con ruolo `admin`.
|
|
||||||
- [ ] Configurare mailer + email sink in `internal/mailer`.
|
|
||||||
- [ ] Definire template rendering per `web/templates/public`, `web/templates/private`, `web/templates/admin`.
|
|
||||||
- [ ] Preparare template email in `web/emails/templates`.
|
|
||||||
- [ ] Definire static assets pipeline e convenzioni in `web/static`.
|
|
||||||
- [ ] Impostare `ui-kit` con Svelte Custom Elements e output in `web/static/ui`.
|
|
||||||
- [ ] Definire integrazione HTMX lato template/partials.
|
|
||||||
- [ ] Aggiungere migrazioni DB iniziali e seed minimo.
|
|
||||||
- [ ] Aggiungere test base (unit + integrazione) per router/auth/repo.
|
|
||||||
- [ ] Aggiungere script Makefile/task runner per setup e run locale.
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
Crea /ui-kit come progetto Vite + Svelte per custom elements.
|
||||||
|
|
||||||
|
Requisiti:
|
||||||
|
- build deve scrivere direttamente in ../web/static/ui:
|
||||||
|
- ui.esm.js
|
||||||
|
- ui.css (tokens+base)
|
||||||
|
- src/index.ts registra:
|
||||||
|
- ui-modal
|
||||||
|
- ui-drop-down
|
||||||
|
- ui-data-table-shell (driver htmx per aggiornare un target)
|
||||||
|
|
||||||
|
Componenti:
|
||||||
|
1) UiModal.svelte:
|
||||||
|
- <svelte:options customElement="ui-modal" />
|
||||||
|
- attributi: title, open (boolean presence)
|
||||||
|
- close on ESC, backdrop click
|
||||||
|
- focus trap minimale
|
||||||
|
- emette evento "ui:close" (bubbles+composed)
|
||||||
|
- slot contenuto (HTMX swappa dentro al tag)
|
||||||
|
|
||||||
|
2) UiDropDown.svelte:
|
||||||
|
- usa <option> del light DOM
|
||||||
|
- espone value/name/placeholder/disabled
|
||||||
|
- integra con form MVC (hidden input name=...)
|
||||||
|
- emette change + ui:change
|
||||||
|
|
||||||
|
3) UiDataTableShell.svelte:
|
||||||
|
- attributi: endpoint, target, page-size
|
||||||
|
- input search -> usa htmx.ajax('GET', url, {target}) se disponibile
|
||||||
|
- non renderizza tabella, solo toolbar
|
||||||
|
|
||||||
|
Aggiorna layout per includere /static/ui/ui.css e /static/ui/ui.esm.js con ?v={{.BuildHash}}.
|
||||||
|
Aggiorna README con comandi npm.
|
||||||
|
|
@ -16,6 +16,10 @@ func RegisterRoutes(app *fiber.App, store *session.Store, database *gorm.DB, cfg
|
||||||
app.Use(httpmw.SessionStoreMiddleware(store))
|
app.Use(httpmw.SessionStoreMiddleware(store))
|
||||||
app.Use(httpmw.CurrentUserMiddleware(store, database))
|
app.Use(httpmw.CurrentUserMiddleware(store, database))
|
||||||
app.Use(httpmw.ConsumeFlash())
|
app.Use(httpmw.ConsumeFlash())
|
||||||
|
app.Use(func(c *fiber.Ctx) error {
|
||||||
|
httpmw.SetTemplateData(c, "BuildHash", cfg.BuildHash)
|
||||||
|
return c.Next()
|
||||||
|
})
|
||||||
|
|
||||||
authService, err := services.NewAuthService(database, cfg)
|
authService, err := services.NewAuthService(database, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>UI Kit Dev</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>UI Kit Dev</h1>
|
||||||
|
<ui-data-table-shell endpoint="/users/table" target="#target" page-size="10"></ui-data-table-shell>
|
||||||
|
<div id="target"></div>
|
||||||
|
|
||||||
|
<script type="module" src="/src/index.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "trustcontact-ui-kit",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||||
|
"svelte": "^5.0.0",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"vite": "^5.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
:root {
|
||||||
|
--ui-bg: #ffffff;
|
||||||
|
--ui-fg: #111827;
|
||||||
|
--ui-muted: #6b7280;
|
||||||
|
--ui-border: #d1d5db;
|
||||||
|
--ui-overlay: rgba(17, 24, 39, 0.56);
|
||||||
|
--ui-panel: #ffffff;
|
||||||
|
--ui-radius: 10px;
|
||||||
|
--ui-shadow: 0 10px 35px rgba(15, 23, 42, 0.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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
<svelte:options customElement="ui-data-table-shell" />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export let endpoint = '';
|
||||||
|
export let target = '';
|
||||||
|
export let pageSize = 10;
|
||||||
|
|
||||||
|
let query = '';
|
||||||
|
|
||||||
|
function submit(page = 1) {
|
||||||
|
if (!endpoint || !target) return;
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (query.trim()) params.set('q', query.trim());
|
||||||
|
params.set('page', String(page));
|
||||||
|
params.set('pageSize', String(pageSize));
|
||||||
|
|
||||||
|
const url = `${endpoint}${endpoint.includes('?') ? '&' : '?'}${params.toString()}`;
|
||||||
|
|
||||||
|
const htmxApi = (window as any).htmx;
|
||||||
|
if (htmxApi && typeof htmxApi.ajax === 'function') {
|
||||||
|
htmxApi.ajax('GET', url, { target });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetEl = document.querySelector(target);
|
||||||
|
if (!targetEl) return;
|
||||||
|
|
||||||
|
fetch(url)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((html) => {
|
||||||
|
targetEl.innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// no-op fallback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="toolbar" on:submit|preventDefault={() => submit(1)}>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder="Search..."
|
||||||
|
bind:value={query}
|
||||||
|
on:keydown={(e) => e.key === 'Enter' && submit(1)}
|
||||||
|
/>
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='search'] {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 180px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--ui-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--ui-primary);
|
||||||
|
color: var(--ui-primary-contrast);
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
<svelte:options customElement="ui-drop-down" />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let value = '';
|
||||||
|
export let name = '';
|
||||||
|
export let placeholder = 'Select...';
|
||||||
|
export let disabled = false;
|
||||||
|
|
||||||
|
let rootEl: HTMLElement;
|
||||||
|
let host: HTMLElement;
|
||||||
|
let hiddenInput: HTMLInputElement;
|
||||||
|
let open = false;
|
||||||
|
|
||||||
|
type OptionItem = { value: string; label: string };
|
||||||
|
let options: OptionItem[] = [];
|
||||||
|
|
||||||
|
function loadOptions() {
|
||||||
|
options = Array.from(host.querySelectorAll('option')).map((opt) => ({
|
||||||
|
value: opt.value,
|
||||||
|
label: opt.textContent?.trim() || opt.value
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
const selected = host.querySelector('option[selected]') as HTMLOptionElement | null;
|
||||||
|
if (selected) value = selected.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedLabel() {
|
||||||
|
if (!value) return placeholder;
|
||||||
|
return options.find((o) => o.value === value)?.label || placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectOption(nextValue: string) {
|
||||||
|
value = nextValue;
|
||||||
|
open = false;
|
||||||
|
syncHiddenInput();
|
||||||
|
|
||||||
|
host.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
host.dispatchEvent(
|
||||||
|
new CustomEvent('ui:change', {
|
||||||
|
detail: { value },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncHiddenInput() {
|
||||||
|
if (!hiddenInput) return;
|
||||||
|
hiddenInput.name = name;
|
||||||
|
hiddenInput.value = value;
|
||||||
|
hiddenInput.disabled = disabled || !name;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
host = (rootEl.getRootNode() as ShadowRoot).host as HTMLElement;
|
||||||
|
if (!host) return;
|
||||||
|
|
||||||
|
hiddenInput = host.querySelector('input[data-ui-dropdown-hidden="true"]') as HTMLInputElement;
|
||||||
|
if (!hiddenInput) {
|
||||||
|
hiddenInput = document.createElement('input');
|
||||||
|
hiddenInput.type = 'hidden';
|
||||||
|
hiddenInput.setAttribute('data-ui-dropdown-hidden', 'true');
|
||||||
|
host.appendChild(hiddenInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOptions();
|
||||||
|
syncHiddenInput();
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
loadOptions();
|
||||||
|
syncHiddenInput();
|
||||||
|
});
|
||||||
|
observer.observe(host, { childList: true, subtree: true, attributes: true });
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dropdown" bind:this={rootEl}>
|
||||||
|
<button type="button" class="trigger" disabled={disabled} on:click={() => (open = !open)}>
|
||||||
|
<span>{selectedLabel()}</span>
|
||||||
|
<span aria-hidden="true">▾</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div class="menu" role="listbox">
|
||||||
|
{#if options.length === 0}
|
||||||
|
<div class="empty">No options</div>
|
||||||
|
{:else}
|
||||||
|
{#each options as opt}
|
||||||
|
<button type="button" class="item" on:click={() => selectOption(opt.value)}>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--ui-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--ui-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 30;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 6px;
|
||||||
|
border: 1px solid var(--ui-border);
|
||||||
|
background: var(--ui-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--ui-shadow);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: var(--ui-muted);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
<svelte:options customElement="ui-modal" />
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let title = '';
|
||||||
|
export let open = false;
|
||||||
|
|
||||||
|
let rootEl: HTMLElement;
|
||||||
|
let panelEl: HTMLElement;
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
open = false;
|
||||||
|
rootEl?.removeAttribute('open');
|
||||||
|
rootEl?.dispatchEvent(
|
||||||
|
new CustomEvent('ui:close', { bubbles: true, composed: true })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
closeModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
trapFocus(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trapFocus(event: KeyboardEvent) {
|
||||||
|
const focusables = panelEl?.querySelectorAll<HTMLElement>(
|
||||||
|
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
|
||||||
|
);
|
||||||
|
if (!focusables || focusables.length === 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
panelEl?.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = focusables[0];
|
||||||
|
const last = focusables[focusables.length - 1];
|
||||||
|
const active = document.activeElement as HTMLElement | null;
|
||||||
|
|
||||||
|
if (event.shiftKey && active === first) {
|
||||||
|
event.preventDefault();
|
||||||
|
last.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.shiftKey && active === last) {
|
||||||
|
event.preventDefault();
|
||||||
|
first.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBackdropClick(event: MouseEvent) {
|
||||||
|
if (event.target === event.currentTarget) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const host = (rootEl.getRootNode() as ShadowRoot).host as HTMLElement;
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
open = host.hasAttribute('open');
|
||||||
|
if (open) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const autofocus = panelEl?.querySelector<HTMLElement>('[autofocus]');
|
||||||
|
(autofocus || panelEl)?.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(host, { attributes: true, attributeFilter: ['open'] });
|
||||||
|
return () => observer.disconnect();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div class="overlay" on:click={onBackdropClick} on:keydown={onKeyDown} role="presentation">
|
||||||
|
<div class="panel" role="dialog" aria-modal="true" tabindex="-1" bind:this={panelEl}>
|
||||||
|
<header class="header">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
<button type="button" class="close" on:click={closeModal} aria-label="Close">×</button>
|
||||||
|
</header>
|
||||||
|
<section class="body">
|
||||||
|
<slot />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div bind:this={rootEl} hidden></div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--ui-overlay);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
width: min(640px, calc(100vw - 32px));
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--ui-panel);
|
||||||
|
border-radius: var(--ui-radius);
|
||||||
|
box-shadow: var(--ui-shadow);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid var(--ui-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
padding: 12px 16px 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import './base.css';
|
||||||
|
import './components/UiModal.svelte';
|
||||||
|
import './components/UiDropDown.svelte';
|
||||||
|
import './components/UiDataTableShell.svelte';
|
||||||
|
|
||||||
|
export {};
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
svelte({
|
||||||
|
compilerOptions: {
|
||||||
|
customElement: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
outDir: '../web/static/ui',
|
||||||
|
emptyOutDir: true,
|
||||||
|
cssCodeSplit: false,
|
||||||
|
lib: {
|
||||||
|
entry: 'src/index.ts',
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: () => 'ui.esm.js',
|
||||||
|
cssFileName: 'ui'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -17,7 +17,9 @@
|
||||||
.muted { color: #6b7280; font-size: 0.95rem; }
|
.muted { color: #6b7280; font-size: 0.95rem; }
|
||||||
.row { display: flex; gap: 10px; flex-wrap: wrap; }
|
.row { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||||
</style>
|
</style>
|
||||||
|
<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>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue