feat: add go-backend design system and tablos UI
This commit is contained in:
parent
bea78ffca7
commit
4ac33c77b9
74 changed files with 9314 additions and 625 deletions
13
docs/design-system/badges.html
Normal file
13
docs/design-system/badges.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Badges</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link is-active">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Badges</h1><p>Semantic status labels for todo, in-progress, success, and destructive states.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Status set</h2><p>The four semantic badge tones used across the app.</p></div><div class="catalog-example-preview"><div class="catalog-inline"><span class="ui-badge ui-badge-info">À faire</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-warning">En cours</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-success">Terminé</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-danger">Erreur</span></div></div><pre class="catalog-example-snippet"><code>@ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning})</code></pre></section></div></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
docs/design-system/buttons.html
Normal file
23
docs/design-system/buttons.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Buttons</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link is-active">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Buttons</h1><p>Primary, secondary, ghost, and destructive actions built from shared templ primitives.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Primary action</h2><p>Used for the main action in a page section or modal footer.</p></div><div class="catalog-example-preview"><button type="button" class="ui-button ui-button-primary ui-button-md">Nouveau projet</button></div><pre class="catalog-example-snippet"><code>@ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Nouveau projet",
|
||||||
|
Variant: ui.ButtonVariantPrimary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
})</code></pre></section><section class="catalog-example"><div class="catalog-example-copy"><h2>Danger action</h2><p>Used for irreversible actions after explicit confirmation.</p></div><div class="catalog-example-preview"><button type="submit" class="ui-button ui-button-danger ui-button-lg">Supprimer</button></div><pre class="catalog-example-snippet"><code>@ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Supprimer",
|
||||||
|
Variant: ui.ButtonVariantDanger,
|
||||||
|
Size: ui.SizeLG,
|
||||||
|
Type: "submit",
|
||||||
|
})</code></pre></section></div></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
docs/design-system/cards.html
Normal file
17
docs/design-system/cards.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Cards</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link is-active">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Cards</h1><p>Reusable bordered surfaces with optional header, body, and footer regions.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Surface card</h2><p>Generic elevated surface with optional header and footer.</p></div><div class="catalog-example-preview"><section class="ui-card"><div class="ui-card-header">Header</div><div class="ui-card-body">Body</div><div class="ui-card-footer">Footer</div></section></div><pre class="catalog-example-snippet"><code>@ui.Card(ui.CardProps{
|
||||||
|
Header: textComponent("Header"),
|
||||||
|
Body: textComponent("Body"),
|
||||||
|
Footer: textComponent("Footer"),
|
||||||
|
})</code></pre></section></div></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
docs/design-system/empty-states.html
Normal file
18
docs/design-system/empty-states.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Empty States</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link is-active">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Empty States</h1><p>Centered fallback messaging with optional icon and action.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Centered empty state</h2><p>Used when a list has no rows yet and the next action should stay obvious.</p></div><div class="catalog-example-preview"><section class="ui-empty-state"><div class="ui-empty-state-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2"></rect> <path d="M3 9h18"></path> <path d="M3 15h18"></path> <path d="M9 3v18"></path> <path d="M15 3v18"></path></svg></div><h3 class="ui-empty-state-title">Aucun projet trouvé</h3><p class="ui-empty-state-description">Créez votre premier projet</p><div class="ui-empty-state-action"><button type="button" class="ui-button ui-button-primary ui-button-md"><span class="ui-button-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path> <path d="M12 5v14"></path></svg></span> Nouveau projet</button></div></section></div><pre class="catalog-example-snippet"><code>@ui.EmptyState(ui.EmptyStateProps{
|
||||||
|
Title: "Aucun projet trouvé",
|
||||||
|
Description: "Créez votre premier projet",
|
||||||
|
Icon: ui.UIIcon("grid3x3"),
|
||||||
|
Action: ui.Button(...),
|
||||||
|
})</code></pre></section></div></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
docs/design-system/form-fields.html
Normal file
23
docs/design-system/form-fields.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Form Fields</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link is-active">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Form Fields</h1><p>Labeled controls with optional hint and error messaging.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Field with validation</h2><p>Wraps a control with label and inline error feedback.</p></div><div class="catalog-example-preview"><div class="ui-form-field"><label for="catalog-name" class="ui-form-label">Nom</label> <input id="catalog-name" type="text" name="name" value="" placeholder="Nom du projet" class="ui-input"><p class="ui-form-error">Le nom est requis</p></div></div><pre class="catalog-example-snippet"><code>@ui.FormField(ui.FormFieldProps{
|
||||||
|
Label: "Nom",
|
||||||
|
For: "catalog-name",
|
||||||
|
Field: ui.Input(ui.InputProps{
|
||||||
|
ID: "catalog-name",
|
||||||
|
Name: "name",
|
||||||
|
Placeholder: "Nom du projet",
|
||||||
|
Type: "text",
|
||||||
|
}),
|
||||||
|
Error: "Le nom est requis",
|
||||||
|
})</code></pre></section></div></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
docs/design-system/icon-buttons.html
Normal file
18
docs/design-system/icon-buttons.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Icon Buttons</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link is-active">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Icon Buttons</h1><p>Compact icon-only actions for destructive and neutral controls.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Borderless destructive action</h2><p>Used for delete controls inside project cards and list rows.</p></div><div class="catalog-example-preview"><button type="button" class="borderless-icon-button" aria-label="Supprimer le projet"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2 w-4 h-4" aria-hidden="true"><path d="M3 6h18"></path> <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path> <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path> <line x1="10" x2="10" y1="11" y2="17"></line> <line x1="14" x2="14" y1="11" y2="17"></line></svg></button></div><pre class="catalog-example-snippet"><code>@ui.IconButton(ui.IconButtonProps{
|
||||||
|
Label: "Supprimer le projet",
|
||||||
|
Icon: "trash",
|
||||||
|
Variant: ui.IconButtonVariantDangerGhost,
|
||||||
|
Type: "button",
|
||||||
|
})</code></pre></section></div></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
docs/design-system/index.html
Normal file
13
docs/design-system/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Design System</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="catalog-page"><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Component Catalog</h1><p>Static documentation generated from the same templ primitives used by the Go application.</p></header><div class="catalog-page-list"><a href="./tokens.html" class="catalog-page-link-card"><h2>Tokens</h2><p>Semantic colors and status roles used by the Go design system.</p><p class="catalog-page-link">/tokens.html</p></a><a href="./buttons.html" class="catalog-page-link-card"><h2>Buttons</h2><p>Primary, secondary, ghost, and destructive actions built from shared templ primitives.</p><p class="catalog-page-link">/buttons.html</p></a><a href="./badges.html" class="catalog-page-link-card"><h2>Badges</h2><p>Semantic status labels for todo, in-progress, success, and destructive states.</p><p class="catalog-page-link">/badges.html</p></a><a href="./icon-buttons.html" class="catalog-page-link-card"><h2>Icon Buttons</h2><p>Compact icon-only actions for destructive and neutral controls.</p><p class="catalog-page-link">/icon-buttons.html</p></a><a href="./inputs.html" class="catalog-page-link-card"><h2>Inputs</h2><p>Shared single-line and multiline text controls.</p><p class="catalog-page-link">/inputs.html</p></a><a href="./form-fields.html" class="catalog-page-link-card"><h2>Form Fields</h2><p>Labeled controls with optional hint and error messaging.</p><p class="catalog-page-link">/form-fields.html</p></a><a href="./modals.html" class="catalog-page-link-card"><h2>Modals</h2><p>Shared modal shell for focused create, edit, and confirm flows.</p><p class="catalog-page-link">/modals.html</p></a><a href="./tables.html" class="catalog-page-link-card"><h2>Tables</h2><p>Shared table shell for server-rendered list views.</p><p class="catalog-page-link">/tables.html</p></a><a href="./empty-states.html" class="catalog-page-link-card"><h2>Empty States</h2><p>Centered fallback messaging with optional icon and action.</p><p class="catalog-page-link">/empty-states.html</p></a><a href="./cards.html" class="catalog-page-link-card"><h2>Cards</h2><p>Reusable bordered surfaces with optional header, body, and footer regions.</p><p class="catalog-page-link">/cards.html</p></a></div></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
docs/design-system/inputs.html
Normal file
23
docs/design-system/inputs.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Inputs</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link is-active">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Inputs</h1><p>Shared single-line and multiline text controls.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Text input</h2><p>Single-line input for names, titles, and short labels.</p></div><div class="catalog-example-preview"><input id="name" type="text" name="name" value="Projet Atlas" placeholder="Nom du projet" class="ui-input"></div><pre class="catalog-example-snippet"><code>@ui.Input(ui.InputProps{
|
||||||
|
Name: "name",
|
||||||
|
Value: "Projet Atlas",
|
||||||
|
Placeholder: "Nom du projet",
|
||||||
|
Type: "text",
|
||||||
|
})</code></pre></section><section class="catalog-example"><div class="catalog-example-copy"><h2>Textarea</h2><p>Multiline field for longer project notes and descriptions.</p></div><div class="catalog-example-preview"><textarea id="description" name="description" placeholder="Description" rows="4" class="ui-textarea">Une description de projet plus détaillée.</textarea></div><pre class="catalog-example-snippet"><code>@ui.Textarea(ui.TextareaProps{
|
||||||
|
Name: "description",
|
||||||
|
Value: "Une description de projet plus détaillée.",
|
||||||
|
Placeholder: "Description",
|
||||||
|
Rows: 4,
|
||||||
|
})</code></pre></section></div></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
docs/design-system/modals.html
Normal file
17
docs/design-system/modals.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Modals</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link is-active">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Modals</h1><p>Shared modal shell for focused create, edit, and confirm flows.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Create modal</h2><p>Shared modal shell with a form body and action footer.</p></div><div class="catalog-example-preview"><div class="ui-modal-backdrop"><div class="ui-modal-panel"><div class="ui-modal-header"><h2>Créer un projet</h2></div><div class="ui-modal-body"><div class="ui-form-field"><label for="modal-name" class="ui-form-label">Nom du projet</label> <input id="modal-name" type="text" name="name" value="" placeholder="Nom du projet" class="ui-input"></div></div><div class="ui-modal-actions"><button type="button" class="ui-button ui-button-secondary ui-button-md">Annuler</button><button type="submit" class="ui-button ui-button-primary ui-button-md">Créer le projet</button></div></div></div></div><pre class="catalog-example-snippet"><code>@ui.Modal(ui.ModalProps{
|
||||||
|
Title: "Créer un projet",
|
||||||
|
Body: ui.FormField(...),
|
||||||
|
Actions: ui.Button(...),
|
||||||
|
})</code></pre></section></div></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
docs/design-system/tables.html
Normal file
16
docs/design-system/tables.html
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Tables</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link is-active">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Tables</h1><p>Shared table shell for server-rendered list views.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>List shell</h2><p>Shared wrapper for server-rendered resource tables.</p></div><div class="catalog-example-preview"><div class="ui-table-shell"><table class="ui-table"><thead><tr><th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Projet</th><th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Statut</th></tr></thead> <tbody><tr><td class="px-6 py-4">Table View</td><td class="px-6 py-4">En cours</td></tr></tbody></table></div></div><pre class="catalog-example-snippet"><code>@ui.Table(ui.TableProps{
|
||||||
|
Head: TabloListHead(),
|
||||||
|
Body: TabloListBody(tablos),
|
||||||
|
})</code></pre></section></div></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
docs/design-system/tokens.html
Normal file
13
docs/design-system/tokens.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Tokens</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link is-active">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Tokens</h1><p>Semantic colors and status roles used by the Go design system.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Status tones</h2><p>Shared semantic badges for info, warning, success, and danger states.</p></div><div class="catalog-example-preview"><div class="catalog-inline"><span class="ui-badge ui-badge-info">À faire</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-warning">En cours</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-success">Terminé</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-danger">Erreur</span></div></div><pre class="catalog-example-snippet"><code>@ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning})</code></pre></section></div></main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,624 @@
|
||||||
|
# Go Backend Tablos Index CRUD Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Build the first real `/tablos` vertical slice in the Go rewrite with server-rendered list, modal create, soft delete, and functional URL-backed grid/list, search, and status filters.
|
||||||
|
|
||||||
|
**Architecture:** Extend the current Go app from auth-only pages into a small app domain for `tablos`. Keep data access in Postgres via sqlc and the repository, keep request parsing and ownership checks in handlers, and keep HTMX fragment boundaries explicit in `templ` views so full-page and partial-page responses share the same rendering units.
|
||||||
|
|
||||||
|
**Tech Stack:** Go, chi, templ, HTMX, PostgreSQL, pgx, sqlc, Go standard `net/http` testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
**Existing files to modify**
|
||||||
|
|
||||||
|
- `go-backend/internal/db/schema.sql`
|
||||||
|
- Add the `public.tablos` table and any required index.
|
||||||
|
- `go-backend/internal/db/queries.sql`
|
||||||
|
- Add list/create/delete SQL queries for tablos.
|
||||||
|
- `go-backend/internal/db/repository.go`
|
||||||
|
- Extend the repository with `tablos` methods backed by sqlc. Take the time to split this file and create a new repository package that contains the tablos.go and users.go
|
||||||
|
- `go-backend/router.go`
|
||||||
|
- Register `POST /tablos` and `DELETE /tablos/{id}`.
|
||||||
|
- `go-backend/internal/web/handlers/auth.go`
|
||||||
|
- Update `GetTablosPage()` to use real data instead of placeholder content.
|
||||||
|
- `go-backend/internal/web/handlers/in_memory_auth_repository.go`
|
||||||
|
- Extend the in-memory test repository with tablo storage and filter/delete behavior.
|
||||||
|
- `go-backend/internal/web/views/pages.templ`
|
||||||
|
- Wire the `/tablos` page to real tablos content if needed by the final component split.
|
||||||
|
- Wire the `/` page to real tablos content
|
||||||
|
- `go-backend/internal/web/views/home.go`
|
||||||
|
- Replace hard-coded overview/tablo placeholder helpers with real tablos view models where needed.
|
||||||
|
- `go-backend/router_test.go`
|
||||||
|
- Add full-router tests for `/tablos` page load and HTMX fragment behavior.
|
||||||
|
|
||||||
|
**New files to create**
|
||||||
|
|
||||||
|
- `go-backend/internal/web/handlers/tablos.go`
|
||||||
|
- Parse query state, validate create input, validate ownership for delete, and render page/modal fragments.
|
||||||
|
- `go-backend/internal/web/handlers/tablos_test.go`
|
||||||
|
- Focused handler tests for `/tablos` filtering, create, and delete behavior.
|
||||||
|
- `go-backend/internal/web/views/tablos.templ`
|
||||||
|
- Main `/tablos` page content, grid/list variants, filter bar, and modal fragments.
|
||||||
|
- `go-backend/internal/web/views/tablos_view.go`
|
||||||
|
- View models, formatting helpers, status labels, safe defaults, and query-state helpers for the tablos page.
|
||||||
|
|
||||||
|
**Generated files expected to change**
|
||||||
|
|
||||||
|
- `go-backend/internal/db/sqlc/*.go`
|
||||||
|
- Regenerated by `just generate` after schema/query updates.
|
||||||
|
- `go-backend/internal/web/views/*_templ.go`
|
||||||
|
- Regenerated by `just generate` after `templ` changes.
|
||||||
|
|
||||||
|
**Test and verification commands**
|
||||||
|
|
||||||
|
- `cd go-backend && go test ./internal/web/handlers -run Tablos`
|
||||||
|
- `cd go-backend && go test ./...`
|
||||||
|
- `cd go-backend && just generate`
|
||||||
|
- `cd go-backend && just build`
|
||||||
|
|
||||||
|
## Chunk 1: Data Model And Repository
|
||||||
|
|
||||||
|
### Task 1: Add the failing repository tests for tablos storage behavior
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `go-backend/internal/web/handlers/in_memory_auth_repository.go`
|
||||||
|
- Create: `go-backend/internal/web/handlers/tablos_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests for create/list/delete semantics**
|
||||||
|
|
||||||
|
Add handler/repository-level tests that prove the repository contract before production code exists:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestInMemoryTablosListExcludesSoftDeletedRows(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
user, _ := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
|
||||||
|
|
||||||
|
first, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Name: "Visible",
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Name: "Hidden",
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.SoftDeleteTablo(context.Background(), first.ID, user.ID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tablos, err := repo.ListTablos(context.Background(), ListTablosInput{OwnerID: user.ID})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tablos) != 1 {
|
||||||
|
t.Fatalf("expected 1 visible tablo, got %d", len(tablos))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos'`
|
||||||
|
|
||||||
|
Expected: FAIL with missing tablo types or repository methods.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the minimal in-memory domain types and storage**
|
||||||
|
|
||||||
|
Add the minimum runtime types used by both handlers and repository implementations:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type TabloStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TabloStatusTodo TabloStatus = "todo"
|
||||||
|
TabloStatusInProgress TabloStatus = "in_progress"
|
||||||
|
TabloStatusDone TabloStatus = "done"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TabloRecord struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
OwnerID uuid.UUID
|
||||||
|
Name string
|
||||||
|
Status TabloStatus
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Extend the in-memory repo with:
|
||||||
|
|
||||||
|
- `tablos map[uuid.UUID]TabloRecord`
|
||||||
|
- `CreateTablo`
|
||||||
|
- `ListTablos`
|
||||||
|
- `SoftDeleteTablo`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the focused tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos'`
|
||||||
|
|
||||||
|
Expected: PASS for the new in-memory storage behavior tests.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/handlers/in_memory_auth_repository.go go-backend/internal/web/handlers/tablos_test.go
|
||||||
|
git commit -m "feat: add in-memory tablo repository support"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Add the SQL schema and query contract through tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `go-backend/internal/db/schema.sql`
|
||||||
|
- Modify: `go-backend/internal/db/queries.sql`
|
||||||
|
- Modify: `go-backend/internal/db/repository.go`
|
||||||
|
- Generated: `go-backend/internal/db/sqlc/*`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write a failing repository integration-shaped test for query semantics**
|
||||||
|
|
||||||
|
Add a focused repository test near the handler test seam if there is no separate db test package yet. The test should document the needed query contract:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestListTablosInputSupportsSearchAndStatus(t *testing.T) {
|
||||||
|
input := ListTablosInput{
|
||||||
|
OwnerID: ownerID,
|
||||||
|
Query: "hell",
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = input
|
||||||
|
// This test initially fails because the production repository contract does not exist yet.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The purpose is to drive the API shape before editing SQL.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos'`
|
||||||
|
|
||||||
|
Expected: FAIL due to missing repository input types or methods.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the SQL schema and queries**
|
||||||
|
|
||||||
|
Update `schema.sql` with:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS public.tablos (
|
||||||
|
id uuid PRIMARY KEY,
|
||||||
|
owner_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||||
|
name text NOT NULL,
|
||||||
|
status text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
deleted_at timestamptz NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS tablos_owner_created_idx
|
||||||
|
ON public.tablos (owner_id, created_at DESC)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add sqlc queries:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- name: CreateTablo :one
|
||||||
|
INSERT INTO public.tablos (
|
||||||
|
id, owner_id, name, status, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, now(), now()
|
||||||
|
)
|
||||||
|
RETURNING id, owner_id, name, status, created_at, updated_at, deleted_at;
|
||||||
|
|
||||||
|
-- name: ListTablos :many
|
||||||
|
SELECT id, owner_id, name, status, created_at, updated_at, deleted_at
|
||||||
|
FROM public.tablos
|
||||||
|
WHERE owner_id = sqlc.arg(owner_id)
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND (
|
||||||
|
sqlc.narg(status)::text IS NULL OR status = sqlc.narg(status)::text
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
sqlc.narg(query)::text IS NULL OR name ILIKE '%' || sqlc.narg(query)::text || '%'
|
||||||
|
)
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
|
||||||
|
-- name: SoftDeleteTablo :execrows
|
||||||
|
UPDATE public.tablos
|
||||||
|
SET deleted_at = now(), updated_at = now()
|
||||||
|
WHERE id = $1 AND owner_id = $2 AND deleted_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Regenerate sqlc and templ artifacts**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just generate`
|
||||||
|
|
||||||
|
Expected: PASS and regenerated `internal/db/sqlc` plus any `templ` outputs with no generation errors.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Implement the Postgres repository methods**
|
||||||
|
|
||||||
|
Extend `repository.go` with the real methods:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ListTablosInput struct {
|
||||||
|
OwnerID uuid.UUID
|
||||||
|
Query string
|
||||||
|
Status *TabloStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTabloInput struct {
|
||||||
|
OwnerID uuid.UUID
|
||||||
|
Name string
|
||||||
|
Status TabloStatus
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement:
|
||||||
|
|
||||||
|
- `CreateTablo(ctx, input)`
|
||||||
|
- `ListTablos(ctx, input)`
|
||||||
|
- `SoftDeleteTablo(ctx, tabloID, ownerID)`
|
||||||
|
|
||||||
|
Normalize empty search strings before passing them to sqlc. Return a domain-level not-found/unauthorized error when `SoftDeleteTablo` affects zero rows.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the focused tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos'`
|
||||||
|
|
||||||
|
Expected: PASS with the new contract compiling and the in-memory implementation still satisfying it.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/db/schema.sql go-backend/internal/db/queries.sql go-backend/internal/db/repository.go go-backend/internal/db/sqlc go-backend/internal/web/handlers/in_memory_auth_repository.go go-backend/internal/web/handlers/tablos_test.go
|
||||||
|
git commit -m "feat: add tablo persistence contract"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk 2: Handler Routing And URL State
|
||||||
|
|
||||||
|
### Task 3: Add failing handler tests for `/tablos` query state, create, and delete
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `go-backend/internal/web/handlers/tablos.go`
|
||||||
|
- Modify: `go-backend/internal/web/handlers/tablos_test.go`
|
||||||
|
- Modify: `go-backend/router.go`
|
||||||
|
- Modify: `go-backend/router_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing handler tests for GET/POST/DELETE**
|
||||||
|
|
||||||
|
Add tests that document the handler contract:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestGetTablosPageDefaultsToGridAndAllStatus(t *testing.T) {}
|
||||||
|
func TestGetTablosPageHonorsSearchAndStatus(t *testing.T) {}
|
||||||
|
func TestPostTablosCreatesTodoTablo(t *testing.T) {}
|
||||||
|
func TestPostTablosWithEmptyNameReturns422(t *testing.T) {}
|
||||||
|
func TestDeleteTabloSoftDeletesOwnedRow(t *testing.T) {}
|
||||||
|
func TestDeleteTabloRejectsDifferentOwner(t *testing.T) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also extend `router_test.go` with an end-to-end page assertion:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestTablosPageRendersRealProjectsHeading(t *testing.T) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run Tablos`
|
||||||
|
|
||||||
|
Expected: FAIL with missing handler methods and route wiring.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement query parsing and safe defaults**
|
||||||
|
|
||||||
|
Create `tablos.go` and add request parsing helpers:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type TablosQueryState struct {
|
||||||
|
View string
|
||||||
|
Query string
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTablosQueryState(r *http.Request) TablosQueryState {
|
||||||
|
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
||||||
|
view := r.URL.Query().Get("view")
|
||||||
|
status := r.URL.Query().Get("status")
|
||||||
|
|
||||||
|
if view != "list" {
|
||||||
|
view = "grid"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case "todo", "in_progress", "done":
|
||||||
|
default:
|
||||||
|
status = "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
return TablosQueryState{
|
||||||
|
View: view,
|
||||||
|
Query: q,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement the minimal handler methods**
|
||||||
|
|
||||||
|
Add handler methods:
|
||||||
|
|
||||||
|
- `GetTablosPage()`
|
||||||
|
- `PostTablos()`
|
||||||
|
- `DeleteTablo()`
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
- `GET /tablos` loads current user, parses query state, lists tablos, renders full page or HTMX fragment
|
||||||
|
- `POST /tablos` trims `name`, rejects empty names with `422`, creates with `status=todo`
|
||||||
|
- `DELETE /tablos/{id}` verifies ownership through the repository delete contract and preserves query state
|
||||||
|
|
||||||
|
- [ ] **Step 5: Wire the routes**
|
||||||
|
|
||||||
|
Update `router.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
mux.Get("/tablos", authHandler.GetTablosPage())
|
||||||
|
mux.Post("/tablos", authHandler.PostTablos())
|
||||||
|
mux.Delete("/tablos/{tabloID}", authHandler.DeleteTablo())
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Re-run focused tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run Tablos`
|
||||||
|
|
||||||
|
Expected: PASS for handler-specific tests.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/handlers/tablos.go go-backend/internal/web/handlers/tablos_test.go go-backend/router.go go-backend/router_test.go
|
||||||
|
git commit -m "feat: add tablo handlers and routes"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk 3: Templ Views, Modal, And Functional Controls
|
||||||
|
|
||||||
|
### Task 4: Add failing rendering tests for the `/tablos` page contract
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `go-backend/internal/web/views/tablos.templ`
|
||||||
|
- Create: `go-backend/internal/web/views/tablos_view.go`
|
||||||
|
- Modify: `go-backend/internal/web/views/pages.templ`
|
||||||
|
- Modify: `go-backend/internal/web/views/home.go`
|
||||||
|
- Modify: `go-backend/internal/web/views/icons.templ`
|
||||||
|
- Modify: `go-backend/router_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing page-render tests for the agreed UI contract**
|
||||||
|
|
||||||
|
Add assertions for:
|
||||||
|
|
||||||
|
- `Mes Projets`
|
||||||
|
- `Nouveau projet`
|
||||||
|
- `Vue en grille`
|
||||||
|
- `Vue en liste`
|
||||||
|
- `Rechercher...`
|
||||||
|
- `Tous`, `Pas commencé`, `En cours`, `Terminé`
|
||||||
|
- grid markup in default mode
|
||||||
|
- list markup in `?view=list`
|
||||||
|
- modal markup or modal target container
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if !strings.Contains(body, "Mes Projets") {
|
||||||
|
t.Fatalf("expected tablos page heading")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "Nouveau projet") {
|
||||||
|
t.Fatalf("expected create trigger")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the router tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./... -run 'TablosPage|TablosHTMX'`
|
||||||
|
|
||||||
|
Expected: FAIL because the placeholder page still renders.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add view models and formatting helpers**
|
||||||
|
|
||||||
|
Create `tablos_view.go` with focused helpers:
|
||||||
|
|
||||||
|
- `TabloCardView`
|
||||||
|
- `TablosPageViewModel`
|
||||||
|
- French status label mapping:
|
||||||
|
- `todo -> À faire`
|
||||||
|
- `in_progress -> En cours`
|
||||||
|
- `done -> Terminé`
|
||||||
|
- date formatting for French-style labels
|
||||||
|
- progress mapping:
|
||||||
|
- `todo -> 0`
|
||||||
|
- `in_progress -> 50`
|
||||||
|
- `done -> 100`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the new templ components**
|
||||||
|
|
||||||
|
Create `tablos.templ` with:
|
||||||
|
|
||||||
|
- `TablosMainContent(vm TablosPageViewModel)`
|
||||||
|
- `TablosToolbar(vm TablosPageViewModel)`
|
||||||
|
- `TablosGrid(vm TablosPageViewModel)`
|
||||||
|
- `TablosList(vm TablosPageViewModel)`
|
||||||
|
- `TabloCard(item TabloCardView, state TablosQueryState)`
|
||||||
|
- `CreateTabloModal(vm TablosPageViewModel)`
|
||||||
|
|
||||||
|
The top-level content should follow the user-provided HTML structure closely:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<main class="flex-1 overflow-auto">
|
||||||
|
<div class="px-4 pt-8 pb-6">
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use HTMX attributes for:
|
||||||
|
|
||||||
|
- view toggle `hx-get`
|
||||||
|
- search `hx-get` with trigger debounce
|
||||||
|
- status pill filtering `hx-get`
|
||||||
|
- modal form `hx-post="/tablos"`
|
||||||
|
- delete buttons `hx-delete="/tablos/{id}"`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add any missing icons or class helpers**
|
||||||
|
|
||||||
|
Extend `icons.templ` only if the provided `/tablos` structure needs icons not already present:
|
||||||
|
|
||||||
|
- `plus`
|
||||||
|
- `grid3x3`
|
||||||
|
- `list`
|
||||||
|
- `search`
|
||||||
|
- `filter`
|
||||||
|
|
||||||
|
Keep icons centralized rather than inlining large SVGs repeatedly.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Regenerate templ artifacts**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just generate`
|
||||||
|
|
||||||
|
Expected: PASS with new `*_templ.go` output and no templ generation errors.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Re-run the page tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./... -run 'TablosPage|TablosHTMX'`
|
||||||
|
|
||||||
|
Expected: PASS with the real `/tablos` page structure rendered.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/views/tablos.templ go-backend/internal/web/views/tablos_view.go go-backend/internal/web/views/pages.templ go-backend/internal/web/views/home.go go-backend/internal/web/views/icons.templ go-backend/internal/web/views/*_templ.go go-backend/router_test.go
|
||||||
|
git commit -m "feat: render real tablos page"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk 4: Modal Validation, Delete UX, And Full Verification
|
||||||
|
|
||||||
|
### Task 5: Tighten validation and modal error behavior with failing tests first
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `go-backend/internal/web/handlers/tablos.go`
|
||||||
|
- Modify: `go-backend/internal/web/handlers/tablos_test.go`
|
||||||
|
- Modify: `go-backend/internal/web/views/tablos.templ`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests for modal error and state preservation**
|
||||||
|
|
||||||
|
Add tests that verify:
|
||||||
|
|
||||||
|
- empty `name` returns `422`
|
||||||
|
- returned body still contains the modal and inline error
|
||||||
|
- create success preserves current `view`, `q`, and `status`
|
||||||
|
- delete success preserves current `view`, `q`, and `status`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if rec.Code != http.StatusUnprocessableEntity {
|
||||||
|
t.Fatalf("expected 422, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(rec.Body.String(), "Le nom du projet est requis") {
|
||||||
|
t.Fatalf("expected inline validation message")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'PostTablos|DeleteTablo'`
|
||||||
|
|
||||||
|
Expected: FAIL until modal error and state-preservation behavior is implemented.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement inline validation and state preservation**
|
||||||
|
|
||||||
|
Update `PostTablos()` and `DeleteTablo()` to:
|
||||||
|
|
||||||
|
- read current query state from request values
|
||||||
|
- reuse the same state when rendering success fragments
|
||||||
|
- return modal fragment with inline error text on `422`
|
||||||
|
- include confirm copy on delete buttons
|
||||||
|
|
||||||
|
Keep the logic minimal; do not add a client-side state store.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the focused tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'PostTablos|DeleteTablo'`
|
||||||
|
|
||||||
|
Expected: PASS for create/delete edge cases.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/handlers/tablos.go go-backend/internal/web/handlers/tablos_test.go go-backend/internal/web/views/tablos.templ
|
||||||
|
git commit -m "feat: finalize tablo modal and delete UX"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 6: Run full verification and record any gaps
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/superpowers/plans/2026-05-08-go-backend-tablos-index-crud.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run full project tests**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./...`
|
||||||
|
|
||||||
|
Expected: PASS across handlers, router, and repository compile paths.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run generation and build verification**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just generate`
|
||||||
|
|
||||||
|
Expected: PASS with no dirty generated output after generation.
|
||||||
|
|
||||||
|
Run: `cd go-backend && just build`
|
||||||
|
|
||||||
|
Expected: PASS with successful binary build.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Re-read the approved spec and verify acceptance criteria manually**
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
- `/tablos` uses the agreed layout direction
|
||||||
|
- create opens a modal
|
||||||
|
- list, search, status filters, and view toggle are functional
|
||||||
|
- delete is soft delete and owner-scoped
|
||||||
|
- no custom app JavaScript was introduced
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update the plan checklist and note any verification gaps**
|
||||||
|
|
||||||
|
If any command fails, record the exact failure under this plan before making completion claims.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/superpowers/plans/2026-05-08-go-backend-tablos-index-crud.md
|
||||||
|
git commit -m "docs: update tablo implementation plan status"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes For Execution
|
||||||
|
|
||||||
|
- Prefer creating `go-backend/internal/web/handlers/tablos.go` rather than continuing to grow `auth.go`; the current codebase is still small enough to benefit from a clearer split.
|
||||||
|
- Keep the first slice owner-scoped only. Do not pull in organization/shared-access behavior from the legacy app.
|
||||||
|
- Keep `status` constrained in Go only. Do not add a DB enum or check constraint for this milestone.
|
||||||
|
- Preserve HTMX fragment boundaries carefully so the modal can fail inline without forcing a full page reload.
|
||||||
|
- If CSS support for the exact class contract is incomplete, implement the semantic structure first, then add the necessary stylesheet updates in a follow-up execution pass inside the same task chunk.
|
||||||
|
|
||||||
|
Plan complete and saved to `docs/superpowers/plans/2026-05-08-go-backend-tablos-index-crud.md`. Ready to execute?
|
||||||
457
docs/superpowers/plans/2026-05-09-go-backend-design-system.md
Normal file
457
docs/superpowers/plans/2026-05-09-go-backend-design-system.md
Normal file
|
|
@ -0,0 +1,457 @@
|
||||||
|
# Go Backend Design System Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Build a repo-owned `templ` design system for `go-backend`, generate a static catalog outside the app, and migrate `/tablos` to the new shared primitives.
|
||||||
|
|
||||||
|
**Architecture:** Keep Tailwind as the styling/token foundation, but move reusable UI into `go-backend/internal/web/ui/` as `templ` primitives plus small Go helpers. Add a Go-driven static catalog generator that renders documentation and previews from the same component implementations, then migrate `/tablos` to consume those primitives instead of ad hoc view markup.
|
||||||
|
|
||||||
|
**Tech Stack:** Go, templ, HTMX, Tailwind CSS v4, Go `net/http` tests, standard library filesystem APIs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
**Existing files to modify**
|
||||||
|
|
||||||
|
- `go-backend/tailwind.input.css`
|
||||||
|
- Add design token definitions and any shared theme aliases needed by the primitives.
|
||||||
|
- `go-backend/static/styles.css`
|
||||||
|
- Keep only app-shell styles, semantic shared component classes, and any small non-Tailwind glue that the primitives require.
|
||||||
|
- `go-backend/internal/web/views/tablos.templ`
|
||||||
|
- Replace ad hoc button/badge/modal/table markup with `ui` primitives.
|
||||||
|
- `go-backend/internal/web/views/tablos_view.go`
|
||||||
|
- Adapt the tablos view models to feed the new shared component APIs.
|
||||||
|
- `go-backend/internal/web/views/dashboard_components.templ`
|
||||||
|
- Replace any reused button or badge markup that should be shared in the first migration pass.
|
||||||
|
- `go-backend/internal/web/views/home.go`
|
||||||
|
- Adjust any overview helper data needed to feed shared UI primitives.
|
||||||
|
- `go-backend/internal/web/handlers/tablos_test.go`
|
||||||
|
- Update assertions to check shared component contracts.
|
||||||
|
- `go-backend/router_test.go`
|
||||||
|
- Add or update full-page assertions proving shared primitives appear in rendered pages.
|
||||||
|
- `go-backend/justfile`
|
||||||
|
- Add a command for generating the design-system catalog.
|
||||||
|
|
||||||
|
**New files to create**
|
||||||
|
|
||||||
|
- `go-backend/internal/web/ui/variants.go`
|
||||||
|
- Shared enums/constants/helpers for variants and sizes.
|
||||||
|
- `go-backend/internal/web/ui/tokens.go`
|
||||||
|
- Token names, semantic aliases, and any helper mappings needed by components.
|
||||||
|
- `go-backend/internal/web/ui/button.templ`
|
||||||
|
- Shared button primitive.
|
||||||
|
- `go-backend/internal/web/ui/icon_button.templ`
|
||||||
|
- Shared icon-only button primitive, including destructive/borderless cases.
|
||||||
|
- `go-backend/internal/web/ui/badge.templ`
|
||||||
|
- Shared badge primitive for statuses.
|
||||||
|
- `go-backend/internal/web/ui/input.templ`
|
||||||
|
- Shared text input primitive.
|
||||||
|
- `go-backend/internal/web/ui/textarea.templ`
|
||||||
|
- Shared textarea primitive.
|
||||||
|
- `go-backend/internal/web/ui/form_field.templ`
|
||||||
|
- Shared labeled field wrapper with error state.
|
||||||
|
- `go-backend/internal/web/ui/card.templ`
|
||||||
|
- Shared card shell and subregions where justified.
|
||||||
|
- `go-backend/internal/web/ui/modal.templ`
|
||||||
|
- Shared modal shell and actions.
|
||||||
|
- `go-backend/internal/web/ui/table.templ`
|
||||||
|
- Shared table wrappers/helpers for headers and row actions.
|
||||||
|
- `go-backend/internal/web/ui/empty_state.templ`
|
||||||
|
- Shared empty-state primitive.
|
||||||
|
- `go-backend/internal/web/ui/ui_test.go`
|
||||||
|
- Rendering tests for core primitive contracts.
|
||||||
|
- `go-backend/internal/web/ui/catalog/pages.go`
|
||||||
|
- Catalog page registry and page metadata.
|
||||||
|
- `go-backend/internal/web/ui/catalog/examples.go`
|
||||||
|
- Rendered example fixtures used by the catalog.
|
||||||
|
- `go-backend/internal/web/ui/catalog/catalog.templ`
|
||||||
|
- Shared catalog page templates.
|
||||||
|
- `go-backend/internal/web/ui/catalog/catalog_test.go`
|
||||||
|
- Catalog rendering and registration tests.
|
||||||
|
- `go-backend/cmd/designsystem/main.go`
|
||||||
|
- Static catalog generator entrypoint.
|
||||||
|
- `go-backend/cmd/designsystem/main_test.go`
|
||||||
|
- Generator output tests.
|
||||||
|
|
||||||
|
**Generated files expected to change**
|
||||||
|
|
||||||
|
- `go-backend/internal/web/ui/*_templ.go`
|
||||||
|
- Generated from the new `templ` primitives.
|
||||||
|
- `go-backend/internal/web/ui/catalog/*_templ.go`
|
||||||
|
- Generated from catalog templates.
|
||||||
|
- `go-backend/internal/web/views/*_templ.go`
|
||||||
|
- Regenerated after migrating views to shared primitives.
|
||||||
|
- `docs/design-system/*.html`
|
||||||
|
- Generated static catalog output.
|
||||||
|
- `go-backend/static/tailwind.css`
|
||||||
|
- Regenerated compiled Tailwind output.
|
||||||
|
|
||||||
|
**Verification commands**
|
||||||
|
|
||||||
|
- `cd go-backend && go test ./internal/web/ui -v`
|
||||||
|
- `cd go-backend && go test ./cmd/designsystem -v`
|
||||||
|
- `cd go-backend && go test ./internal/web/handlers -run 'Tablos|HomePage'`
|
||||||
|
- `cd go-backend && just generate`
|
||||||
|
- `cd go-backend && go test ./...`
|
||||||
|
- `cd go-backend && just build`
|
||||||
|
|
||||||
|
## Chunk 1: UI Foundation
|
||||||
|
|
||||||
|
### Task 1: Introduce failing tests for shared variant and primitive contracts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `go-backend/internal/web/ui/ui_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing rendering tests for the first primitive contracts**
|
||||||
|
|
||||||
|
Add focused tests for:
|
||||||
|
|
||||||
|
- button variants and sizes
|
||||||
|
- icon button destructive variant
|
||||||
|
- badge variant rendering
|
||||||
|
- modal shell structure
|
||||||
|
|
||||||
|
Example starter shape:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestIconButtonRendersBorderlessDestructiveMarkup(t *testing.T) {
|
||||||
|
component := IconButton(IconButtonProps{
|
||||||
|
Label: "Supprimer le projet",
|
||||||
|
Icon: "trash",
|
||||||
|
Variant: IconButtonVariantDangerGhost,
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`class="borderless-icon-button"`,
|
||||||
|
`aria-label="Supprimer le projet"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused UI tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/ui -v`
|
||||||
|
|
||||||
|
Expected: FAIL because the `ui` package or primitives do not exist yet.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the minimal `ui` package structure**
|
||||||
|
|
||||||
|
Create:
|
||||||
|
|
||||||
|
- `variants.go`
|
||||||
|
- `tokens.go`
|
||||||
|
- empty `templ` primitive files for the first component set
|
||||||
|
|
||||||
|
Keep the first implementation minimal: just enough types and entrypoints to satisfy the tests.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the focused UI tests to verify the first primitives compile and pass**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just generate && go test ./internal/web/ui -v`
|
||||||
|
|
||||||
|
Expected: PASS for the initial primitive contract tests.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/ui go-backend/tailwind.input.css go-backend/static/styles.css
|
||||||
|
git commit -m "feat: add design system ui foundation"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 2: Add design tokens and shared semantic class rules
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `go-backend/tailwind.input.css`
|
||||||
|
- Modify: `go-backend/static/styles.css`
|
||||||
|
- Test: `go-backend/internal/web/ui/ui_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write a failing token-level test**
|
||||||
|
|
||||||
|
Add a test that renders a primitive and proves semantic classes/tokens are in use rather than page-local class strings.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused test to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/ui -run 'Token|Button|Badge' -v`
|
||||||
|
|
||||||
|
Expected: FAIL due to missing token-backed class behavior.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Define shared tokens and semantic CSS**
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
- semantic token aliases in `tailwind.input.css`
|
||||||
|
- shared borderless/destructive button styles
|
||||||
|
- any tiny semantic CSS hooks the primitives need
|
||||||
|
|
||||||
|
Do not add page-specific classes here.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the focused UI tests**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just generate && go test ./internal/web/ui -run 'Token|Button|Badge' -v`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/tailwind.input.css go-backend/static/styles.css go-backend/internal/web/ui
|
||||||
|
git commit -m "feat: add design system tokens"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk 2: Primitive Components
|
||||||
|
|
||||||
|
### Task 3: Build the first reusable primitives
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create/Modify: `go-backend/internal/web/ui/button.templ`
|
||||||
|
- Create/Modify: `go-backend/internal/web/ui/icon_button.templ`
|
||||||
|
- Create/Modify: `go-backend/internal/web/ui/badge.templ`
|
||||||
|
- Create/Modify: `go-backend/internal/web/ui/input.templ`
|
||||||
|
- Create/Modify: `go-backend/internal/web/ui/textarea.templ`
|
||||||
|
- Create/Modify: `go-backend/internal/web/ui/form_field.templ`
|
||||||
|
- Create/Modify: `go-backend/internal/web/ui/card.templ`
|
||||||
|
- Create/Modify: `go-backend/internal/web/ui/modal.templ`
|
||||||
|
- Create/Modify: `go-backend/internal/web/ui/table.templ`
|
||||||
|
- Create/Modify: `go-backend/internal/web/ui/empty_state.templ`
|
||||||
|
- Modify: `go-backend/internal/web/ui/ui_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Expand the test suite with one failing test per primitive**
|
||||||
|
|
||||||
|
Add one focused contract test per primitive. Examples:
|
||||||
|
|
||||||
|
- `Button` renders `primary`, `ghost`, and `danger`
|
||||||
|
- `IconButton` renders icon-only action buttons
|
||||||
|
- `Badge` renders status-like visual variants
|
||||||
|
- `FormField` renders label + error text
|
||||||
|
- `Modal` renders shell/body/actions
|
||||||
|
- `Table` renders shared header/body wrappers
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the primitive tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/ui -run 'Button|IconButton|Badge|Modal|Table|FormField' -v`
|
||||||
|
|
||||||
|
Expected: FAIL due to missing or incomplete primitive implementations.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the minimal primitives to satisfy the tests**
|
||||||
|
|
||||||
|
Keep APIs tight:
|
||||||
|
|
||||||
|
- semantic variants
|
||||||
|
- `sm`, `md`, `lg` sizes
|
||||||
|
- pass-through HTMX attrs only where needed
|
||||||
|
- no arbitrary class-first API
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the primitive tests**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just generate && go test ./internal/web/ui -run 'Button|IconButton|Badge|Modal|Table|FormField' -v`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/ui
|
||||||
|
git commit -m "feat: add initial design system primitives"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk 3: Static Catalog Generator
|
||||||
|
|
||||||
|
### Task 4: Build the catalog registry and page templates
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `go-backend/internal/web/ui/catalog/pages.go`
|
||||||
|
- Create: `go-backend/internal/web/ui/catalog/examples.go`
|
||||||
|
- Create: `go-backend/internal/web/ui/catalog/catalog.templ`
|
||||||
|
- Create: `go-backend/internal/web/ui/catalog/catalog_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing catalog tests**
|
||||||
|
|
||||||
|
Add tests for:
|
||||||
|
|
||||||
|
- component pages are registered
|
||||||
|
- a tokens page exists
|
||||||
|
- component examples render through the real primitives
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused catalog tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/ui/catalog -v`
|
||||||
|
|
||||||
|
Expected: FAIL because catalog registry/templates do not exist yet.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the catalog registry and page templates**
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
- page metadata
|
||||||
|
- component examples
|
||||||
|
- shared catalog layout
|
||||||
|
- snippet sections
|
||||||
|
|
||||||
|
Use the real primitives from `internal/web/ui/`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the focused catalog tests**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just generate && go test ./internal/web/ui/catalog -v`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/ui/catalog
|
||||||
|
git commit -m "feat: add design system catalog pages"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 5: Add the static generator command and generated output
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `go-backend/cmd/designsystem/main.go`
|
||||||
|
- Create: `go-backend/cmd/designsystem/main_test.go`
|
||||||
|
- Modify: `go-backend/justfile`
|
||||||
|
- Generated: `docs/design-system/*.html`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing generator tests**
|
||||||
|
|
||||||
|
Add tests that:
|
||||||
|
|
||||||
|
- create a temp output dir
|
||||||
|
- run the generator
|
||||||
|
- assert `index.html`, `tokens.html`, and at least one component page exist
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the generator tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./cmd/designsystem -v`
|
||||||
|
|
||||||
|
Expected: FAIL because the generator command does not exist yet.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the generator and `just` entrypoint**
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
- generator command
|
||||||
|
- output directory creation
|
||||||
|
- page rendering loop
|
||||||
|
- `just design-system` recipe
|
||||||
|
|
||||||
|
- [ ] **Step 4: Generate the static catalog and verify tests pass**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
- `cd go-backend && go test ./cmd/designsystem -v`
|
||||||
|
- `cd go-backend && just design-system`
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- tests PASS
|
||||||
|
- `docs/design-system/` contains generated HTML pages
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/cmd/designsystem go-backend/justfile docs/design-system
|
||||||
|
git commit -m "feat: add static design system generator"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk 4: Migrate `/tablos`
|
||||||
|
|
||||||
|
### Task 6: Replace ad hoc `/tablos` UI markup with shared primitives
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `go-backend/internal/web/views/tablos.templ`
|
||||||
|
- Modify: `go-backend/internal/web/views/tablos_view.go`
|
||||||
|
- Modify: `go-backend/internal/web/views/dashboard_components.templ`
|
||||||
|
- Modify: `go-backend/internal/web/views/home.go`
|
||||||
|
- Modify: `go-backend/internal/web/handlers/tablos_test.go`
|
||||||
|
- Modify: `go-backend/router_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing migration tests**
|
||||||
|
|
||||||
|
Add or update tests so they explicitly expect shared component contracts in `/tablos`, for example:
|
||||||
|
|
||||||
|
- shared `Button` class contract in toolbar or modal
|
||||||
|
- shared `IconButton` contract for delete actions
|
||||||
|
- shared `Badge` contract for statuses
|
||||||
|
- shared `Modal` shell contract
|
||||||
|
- shared `Table` wrapper contract
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused `/tablos` tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tablos|HomePage' -v`
|
||||||
|
|
||||||
|
Expected: FAIL because views still render ad hoc markup.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Migrate `/tablos` to `ui` primitives**
|
||||||
|
|
||||||
|
Replace local markup with shared components:
|
||||||
|
|
||||||
|
- primary actions -> `Button`
|
||||||
|
- delete actions -> `IconButton`
|
||||||
|
- statuses -> `Badge`
|
||||||
|
- create form shell -> `Modal` + `FormField` + `Input`
|
||||||
|
- list view shell -> `Table`
|
||||||
|
- empty state -> `EmptyState`
|
||||||
|
|
||||||
|
Keep the page-specific layout, filters, and HTMX flows intact.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run focused `/tablos` tests**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just generate && go test ./internal/web/handlers -run 'Tablos|HomePage' -v`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/views go-backend/internal/web/handlers/tablos_test.go go-backend/router_test.go
|
||||||
|
git commit -m "refactor: migrate tablos to design system primitives"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk 5: Final Verification And Docs Output
|
||||||
|
|
||||||
|
### Task 7: Regenerate docs, verify the full app, and lock in usage rules
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify if needed: `docs/design-system/*.html`
|
||||||
|
- Modify if needed: `go-backend/internal/web/ui/catalog/*`
|
||||||
|
- Modify if needed: `go-backend/internal/web/ui/ui_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Generate the final catalog output**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just design-system`
|
||||||
|
|
||||||
|
Expected: refreshed static catalog pages under `docs/design-system/`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the full verification suite**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
- `cd go-backend && just generate`
|
||||||
|
- `cd go-backend && go test ./...`
|
||||||
|
- `cd go-backend && just build`
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- all tests PASS
|
||||||
|
- app builds cleanly
|
||||||
|
|
||||||
|
- [ ] **Step 3: Do a final plan-vs-spec review pass**
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
- primitives exist
|
||||||
|
- catalog exists and is generated outside the app
|
||||||
|
- `/tablos` uses shared primitives
|
||||||
|
- no duplicate delete button implementation remains
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend docs/design-system
|
||||||
|
git commit -m "feat: add go-backend design system"
|
||||||
|
```
|
||||||
82
go-backend/cmd/designsystem/main.go
Normal file
82
go-backend/cmd/designsystem/main.go
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
|
||||||
|
"xtablo-backend/internal/web/ui/catalog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
outputDir := flag.String("output", filepath.Join("..", "docs", "design-system"), "output directory for generated catalog pages")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if err := GenerateSite(*outputDir); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "generate design system: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateSite(outputDir string) error {
|
||||||
|
if err := os.MkdirAll(outputDir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create output dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writePage(filepath.Join(outputDir, "index.html"), "Design System", catalog.CatalogIndex(catalog.Pages())); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, page := range catalog.Pages() {
|
||||||
|
target := filepath.Join(outputDir, page.Slug+".html")
|
||||||
|
if err := writePage(target, page.Title, catalog.CatalogPage(page)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePage(path string, title string, component templ.Component) error {
|
||||||
|
body, err := renderComponent(component)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("render %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := buildHTMLDocument(title, body)
|
||||||
|
if err := os.WriteFile(path, []byte(doc), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("write %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderComponent(component templ.Component) (template.HTML, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := component.Render(context.Background(), &buf); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return template.HTML(buf.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildHTMLDocument(title string, body template.HTML) string {
|
||||||
|
return fmt.Sprintf(`<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>%s</title>
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
|
||||||
|
<link rel="stylesheet" href="../../go-backend/static/styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
%s
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, template.HTMLEscapeString(title), body)
|
||||||
|
}
|
||||||
34
go-backend/cmd/designsystem/main_test.go
Normal file
34
go-backend/cmd/designsystem/main_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateSiteWritesExpectedPages(t *testing.T) {
|
||||||
|
outputDir := t.TempDir()
|
||||||
|
|
||||||
|
if err := GenerateSite(outputDir); err != nil {
|
||||||
|
t.Fatalf("generate site: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range []string{
|
||||||
|
"index.html",
|
||||||
|
"tokens.html",
|
||||||
|
"buttons.html",
|
||||||
|
"badges.html",
|
||||||
|
"icon-buttons.html",
|
||||||
|
"inputs.html",
|
||||||
|
"form-fields.html",
|
||||||
|
"modals.html",
|
||||||
|
"tables.html",
|
||||||
|
"empty-states.html",
|
||||||
|
"cards.html",
|
||||||
|
} {
|
||||||
|
path := filepath.Join(outputDir, name)
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
t.Fatalf("expected generated file %q: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -54,3 +54,41 @@ LIMIT 1;
|
||||||
-- name: DeleteSessionByToken :execrows
|
-- name: DeleteSessionByToken :execrows
|
||||||
DELETE FROM auth.sessions
|
DELETE FROM auth.sessions
|
||||||
WHERE session_token = $1;
|
WHERE session_token = $1;
|
||||||
|
|
||||||
|
-- name: CreateTablo :one
|
||||||
|
INSERT INTO public.tablos (
|
||||||
|
id,
|
||||||
|
owner_id,
|
||||||
|
name,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
now(),
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
RETURNING id, owner_id, name, status, created_at, updated_at, deleted_at;
|
||||||
|
|
||||||
|
-- name: ListTablos :many
|
||||||
|
SELECT id, owner_id, name, status, created_at, updated_at, deleted_at
|
||||||
|
FROM public.tablos
|
||||||
|
WHERE owner_id = sqlc.arg(owner_id)
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND (
|
||||||
|
sqlc.narg(status)::text IS NULL OR status = sqlc.narg(status)::text
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
sqlc.narg(query)::text IS NULL OR name ILIKE '%' || sqlc.narg(query)::text || '%'
|
||||||
|
)
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
|
||||||
|
-- name: SoftDeleteTablo :execrows
|
||||||
|
UPDATE public.tablos
|
||||||
|
SET deleted_at = now(), updated_at = now()
|
||||||
|
WHERE id = $1
|
||||||
|
AND owner_id = $2
|
||||||
|
AND deleted_at IS NULL;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
@ -14,6 +15,7 @@ import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
sqlcdb "xtablo-backend/internal/db/sqlc"
|
sqlcdb "xtablo-backend/internal/db/sqlc"
|
||||||
|
tablomodel "xtablo-backend/internal/tablos"
|
||||||
"xtablo-backend/internal/web/handlers"
|
"xtablo-backend/internal/web/handlers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -142,6 +144,83 @@ func (r *PostgresAuthRepository) DeleteSessionByToken(ctx context.Context, token
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *PostgresAuthRepository) CreateTablo(ctx context.Context, input tablomodel.CreateInput) (tablomodel.Record, error) {
|
||||||
|
row, err := r.queries.CreateTablo(ctx, sqlcdb.CreateTabloParams{
|
||||||
|
ID: uuid.New(),
|
||||||
|
OwnerID: input.OwnerID,
|
||||||
|
Name: strings.TrimSpace(input.Name),
|
||||||
|
Status: string(input.Status),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return tablomodel.Record{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapTabloRecord(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PostgresAuthRepository) ListTablos(ctx context.Context, input tablomodel.ListInput) ([]tablomodel.Record, error) {
|
||||||
|
params := sqlcdb.ListTablosParams{
|
||||||
|
OwnerID: input.OwnerID,
|
||||||
|
Query: nullableText(strings.TrimSpace(input.Query)),
|
||||||
|
Status: nullableStatus(input.Status),
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := r.queries.ListTablos(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tablos := make([]tablomodel.Record, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
tablos = append(tablos, mapTabloRecord(row))
|
||||||
|
}
|
||||||
|
return tablos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PostgresAuthRepository) SoftDeleteTablo(ctx context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error {
|
||||||
|
rows, err := r.queries.SoftDeleteTablo(ctx, sqlcdb.SoftDeleteTabloParams{
|
||||||
|
ID: tabloID,
|
||||||
|
OwnerID: ownerID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rows == 0 {
|
||||||
|
return tablomodel.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func pgtypeTimestamptz(value time.Time) pgtype.Timestamptz {
|
func pgtypeTimestamptz(value time.Time) pgtype.Timestamptz {
|
||||||
return pgtype.Timestamptz{Time: value, Valid: true}
|
return pgtype.Timestamptz{Time: value, Valid: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nullableText(value string) pgtype.Text {
|
||||||
|
if value == "" {
|
||||||
|
return pgtype.Text{}
|
||||||
|
}
|
||||||
|
return pgtype.Text{String: value, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nullableStatus(value *tablomodel.Status) pgtype.Text {
|
||||||
|
if value == nil {
|
||||||
|
return pgtype.Text{}
|
||||||
|
}
|
||||||
|
return nullableText(string(*value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapTabloRecord(row sqlcdb.Tablo) tablomodel.Record {
|
||||||
|
record := tablomodel.Record{
|
||||||
|
ID: row.ID,
|
||||||
|
OwnerID: row.OwnerID,
|
||||||
|
Name: row.Name,
|
||||||
|
Status: tablomodel.Status(row.Status),
|
||||||
|
CreatedAt: row.CreatedAt.Time,
|
||||||
|
UpdatedAt: row.UpdatedAt.Time,
|
||||||
|
}
|
||||||
|
if row.DeletedAt.Valid {
|
||||||
|
deletedAt := row.DeletedAt.Time
|
||||||
|
record.DeletedAt = &deletedAt
|
||||||
|
}
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,20 @@ CREATE TABLE IF NOT EXISTS auth.sessions (
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS auth_sessions_user_id_idx ON auth.sessions(user_id);
|
CREATE INDEX IF NOT EXISTS auth_sessions_user_id_idx ON auth.sessions(user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.tablos (
|
||||||
|
id uuid PRIMARY KEY,
|
||||||
|
owner_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||||
|
name text NOT NULL,
|
||||||
|
status text NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
deleted_at timestamptz NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS tablos_owner_created_idx
|
||||||
|
ON public.tablos (owner_id, created_at DESC)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger
|
CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
SECURITY DEFINER
|
SECURITY DEFINER
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,16 @@ type AuthUser struct {
|
||||||
UpdatedAt pgtype.Timestamptz `db:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `db:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Tablo struct {
|
||||||
|
ID uuid.UUID `db:"id"`
|
||||||
|
OwnerID uuid.UUID `db:"owner_id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
Status string `db:"status"`
|
||||||
|
CreatedAt pgtype.Timestamptz `db:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `db:"updated_at"`
|
||||||
|
DeletedAt pgtype.Timestamptz `db:"deleted_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uuid.UUID `db:"id"`
|
ID uuid.UUID `db:"id"`
|
||||||
Email string `db:"email"`
|
Email string `db:"email"`
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,13 @@ import (
|
||||||
type Querier interface {
|
type Querier interface {
|
||||||
CreateAuthUser(ctx context.Context, arg CreateAuthUserParams) (uuid.UUID, error)
|
CreateAuthUser(ctx context.Context, arg CreateAuthUserParams) (uuid.UUID, error)
|
||||||
CreateSession(ctx context.Context, arg CreateSessionParams) error
|
CreateSession(ctx context.Context, arg CreateSessionParams) error
|
||||||
|
CreateTablo(ctx context.Context, arg CreateTabloParams) (Tablo, error)
|
||||||
DeleteSessionByToken(ctx context.Context, sessionToken string) (int64, error)
|
DeleteSessionByToken(ctx context.Context, sessionToken string) (int64, error)
|
||||||
GetAuthUserByEmail(ctx context.Context, email string) (GetAuthUserByEmailRow, error)
|
GetAuthUserByEmail(ctx context.Context, email string) (GetAuthUserByEmailRow, error)
|
||||||
GetPublicUserByID(ctx context.Context, id uuid.UUID) (User, error)
|
GetPublicUserByID(ctx context.Context, id uuid.UUID) (User, error)
|
||||||
GetSessionByToken(ctx context.Context, sessionToken string) (AuthSession, error)
|
GetSessionByToken(ctx context.Context, sessionToken string) (AuthSession, error)
|
||||||
|
ListTablos(ctx context.Context, arg ListTablosParams) ([]Tablo, error)
|
||||||
|
SoftDeleteTablo(ctx context.Context, arg SoftDeleteTabloParams) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Querier = (*Queries)(nil)
|
var _ Querier = (*Queries)(nil)
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,52 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) er
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createTablo = `-- name: CreateTablo :one
|
||||||
|
INSERT INTO public.tablos (
|
||||||
|
id,
|
||||||
|
owner_id,
|
||||||
|
name,
|
||||||
|
status,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
now(),
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
RETURNING id, owner_id, name, status, created_at, updated_at, deleted_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateTabloParams struct {
|
||||||
|
ID uuid.UUID `db:"id"`
|
||||||
|
OwnerID uuid.UUID `db:"owner_id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
Status string `db:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateTablo(ctx context.Context, arg CreateTabloParams) (Tablo, error) {
|
||||||
|
row := q.db.QueryRow(ctx, createTablo,
|
||||||
|
arg.ID,
|
||||||
|
arg.OwnerID,
|
||||||
|
arg.Name,
|
||||||
|
arg.Status,
|
||||||
|
)
|
||||||
|
var i Tablo
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.OwnerID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Status,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.DeletedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const deleteSessionByToken = `-- name: DeleteSessionByToken :execrows
|
const deleteSessionByToken = `-- name: DeleteSessionByToken :execrows
|
||||||
DELETE FROM auth.sessions
|
DELETE FROM auth.sessions
|
||||||
WHERE session_token = $1
|
WHERE session_token = $1
|
||||||
|
|
@ -166,3 +212,72 @@ func (q *Queries) GetSessionByToken(ctx context.Context, sessionToken string) (A
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listTablos = `-- name: ListTablos :many
|
||||||
|
SELECT id, owner_id, name, status, created_at, updated_at, deleted_at
|
||||||
|
FROM public.tablos
|
||||||
|
WHERE owner_id = $1
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND (
|
||||||
|
$2::text IS NULL OR status = $2::text
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
$3::text IS NULL OR name ILIKE '%' || $3::text || '%'
|
||||||
|
)
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListTablosParams struct {
|
||||||
|
OwnerID uuid.UUID `db:"owner_id"`
|
||||||
|
Status pgtype.Text `db:"status"`
|
||||||
|
Query pgtype.Text `db:"query"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListTablos(ctx context.Context, arg ListTablosParams) ([]Tablo, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listTablos, arg.OwnerID, arg.Status, arg.Query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Tablo
|
||||||
|
for rows.Next() {
|
||||||
|
var i Tablo
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.OwnerID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Status,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.DeletedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const softDeleteTablo = `-- name: SoftDeleteTablo :execrows
|
||||||
|
UPDATE public.tablos
|
||||||
|
SET deleted_at = now(), updated_at = now()
|
||||||
|
WHERE id = $1
|
||||||
|
AND owner_id = $2
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
type SoftDeleteTabloParams struct {
|
||||||
|
ID uuid.UUID `db:"id"`
|
||||||
|
OwnerID uuid.UUID `db:"owner_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SoftDeleteTablo(ctx context.Context, arg SoftDeleteTabloParams) (int64, error) {
|
||||||
|
result, err := q.db.Exec(ctx, softDeleteTablo, arg.ID, arg.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.RowsAffected(), nil
|
||||||
|
}
|
||||||
|
|
|
||||||
40
go-backend/internal/tablos/model.go
Normal file
40
go-backend/internal/tablos/model.go
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
package tablos
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("tablo not found")
|
||||||
|
|
||||||
|
type Status string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusTodo Status = "todo"
|
||||||
|
StatusInProgress Status = "in_progress"
|
||||||
|
StatusDone Status = "done"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
OwnerID uuid.UUID
|
||||||
|
Name string
|
||||||
|
Status Status
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateInput struct {
|
||||||
|
OwnerID uuid.UUID
|
||||||
|
Name string
|
||||||
|
Status Status
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListInput struct {
|
||||||
|
OwnerID uuid.UUID
|
||||||
|
Query string
|
||||||
|
Status *Status
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,9 @@ type AuthRepository interface {
|
||||||
CreateSession(ctx context.Context, token string, userID uuid.UUID, expiresAt time.Time) error
|
CreateSession(ctx context.Context, token string, userID uuid.UUID, expiresAt time.Time) error
|
||||||
GetSessionByToken(ctx context.Context, token string) (Session, error)
|
GetSessionByToken(ctx context.Context, token string) (Session, error)
|
||||||
DeleteSessionByToken(ctx context.Context, token string) error
|
DeleteSessionByToken(ctx context.Context, token string) error
|
||||||
|
CreateTablo(ctx context.Context, input CreateTabloInput) (TabloRecord, error)
|
||||||
|
ListTablos(ctx context.Context, input ListTablosInput) ([]TabloRecord, error)
|
||||||
|
SoftDeleteTablo(ctx context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateAuthUserInput struct {
|
type CreateAuthUserInput struct {
|
||||||
|
|
@ -73,9 +76,43 @@ func NewAuthHandler(repo AuthRepository) *AuthHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) GetHome() http.HandlerFunc {
|
func (h *AuthHandler) GetHome() http.HandlerFunc {
|
||||||
return h.renderAppPage("/", func(user PublicUser) templ.Component {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
return views.OverviewMainContent(user.DisplayName, user.Email)
|
user, ok := h.authenticatedUser(r.Context(), r)
|
||||||
})
|
if !ok {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tablos, err := h.repo.ListTablos(context.Background(), ListTablosInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to load projects", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showAllProjects := r.URL.Query().Get("show_projects") == "all"
|
||||||
|
projects := views.OverviewProjectsFromTablos(tablos)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if isHXRequest(r) && targetsOverviewProjectsSection(r) {
|
||||||
|
if err := views.OverviewProjectsSection(projects, showAllProjects).Render(r.Context(), w); err != nil {
|
||||||
|
http.Error(w, "failed to render overview projects", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content := views.OverviewMainContent(user.DisplayName, user.Email, projects, showAllProjects)
|
||||||
|
var renderErr error
|
||||||
|
if isHXRequest(r) {
|
||||||
|
renderErr = views.DashboardContentSwap("/", content).Render(r.Context(), w)
|
||||||
|
} else {
|
||||||
|
renderErr = views.DashboardPage("/", content).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
if renderErr != nil {
|
||||||
|
http.Error(w, "failed to render app page", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) GetTasksPage() http.HandlerFunc {
|
func (h *AuthHandler) GetTasksPage() http.HandlerFunc {
|
||||||
|
|
@ -85,9 +122,9 @@ func (h *AuthHandler) GetTasksPage() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) GetTablosPage() http.HandlerFunc {
|
func (h *AuthHandler) GetTablosPage() http.HandlerFunc {
|
||||||
return h.renderAppPage("/tablos", func(user PublicUser) templ.Component {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
return views.TablosMainContent()
|
h.renderTablosPage(w, r)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *AuthHandler) GetPlanningPage() http.HandlerFunc {
|
func (h *AuthHandler) GetPlanningPage() http.HandlerFunc {
|
||||||
|
|
@ -397,3 +434,11 @@ func logStoreMutation(action string, email string, sessionID string, usersCount
|
||||||
func isHXRequest(r *http.Request) bool {
|
func isHXRequest(r *http.Request) bool {
|
||||||
return r.Header.Get("HX-Request") == "true"
|
return r.Header.Get("HX-Request") == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func targetsOverviewProjectsSection(r *http.Request) bool {
|
||||||
|
target := strings.TrimSpace(r.Header.Get("HX-Target"))
|
||||||
|
if target == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return target == "overview-projects-section" || strings.Contains(target, "#overview-projects-section")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ type InMemoryAuthRepository struct {
|
||||||
authUsers map[string]AuthUser
|
authUsers map[string]AuthUser
|
||||||
publicUsers map[uuid.UUID]PublicUser
|
publicUsers map[uuid.UUID]PublicUser
|
||||||
sessions map[string]Session
|
sessions map[string]Session
|
||||||
|
tablos map[uuid.UUID]TabloRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewInMemoryAuthRepository creates a testing-only auth repository.
|
// NewInMemoryAuthRepository creates a testing-only auth repository.
|
||||||
|
|
@ -24,6 +25,7 @@ func NewInMemoryAuthRepository() *InMemoryAuthRepository {
|
||||||
authUsers: map[string]AuthUser{},
|
authUsers: map[string]AuthUser{},
|
||||||
publicUsers: map[uuid.UUID]PublicUser{},
|
publicUsers: map[uuid.UUID]PublicUser{},
|
||||||
sessions: map[string]Session{},
|
sessions: map[string]Session{},
|
||||||
|
tablos: map[uuid.UUID]TabloRecord{},
|
||||||
}
|
}
|
||||||
|
|
||||||
demoHash, err := hashPassword("xtablo-demo")
|
demoHash, err := hashPassword("xtablo-demo")
|
||||||
|
|
|
||||||
369
go-backend/internal/web/handlers/tablos.go
Normal file
369
go-backend/internal/web/handlers/tablos.go
Normal file
|
|
@ -0,0 +1,369 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
tablomodel "xtablo-backend/internal/tablos"
|
||||||
|
"xtablo-backend/internal/web/views"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrTabloNotFound = tablomodel.ErrNotFound
|
||||||
|
|
||||||
|
type TabloStatus = tablomodel.Status
|
||||||
|
|
||||||
|
const (
|
||||||
|
TabloStatusTodo = tablomodel.StatusTodo
|
||||||
|
TabloStatusInProgress = tablomodel.StatusInProgress
|
||||||
|
TabloStatusDone = tablomodel.StatusDone
|
||||||
|
)
|
||||||
|
|
||||||
|
type TabloRecord = tablomodel.Record
|
||||||
|
type CreateTabloInput = tablomodel.CreateInput
|
||||||
|
type ListTablosInput = tablomodel.ListInput
|
||||||
|
|
||||||
|
type TablosPageState struct {
|
||||||
|
View string
|
||||||
|
Query string
|
||||||
|
Status string
|
||||||
|
ModalOpen bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeTabloQuery(query string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(query))
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTablosPageState(values interface {
|
||||||
|
Get(string) string
|
||||||
|
}) TablosPageState {
|
||||||
|
view := strings.TrimSpace(values.Get("view"))
|
||||||
|
if view != "list" {
|
||||||
|
view = "grid"
|
||||||
|
}
|
||||||
|
|
||||||
|
status := strings.TrimSpace(values.Get("status"))
|
||||||
|
switch status {
|
||||||
|
case "todo", "in_progress", "done":
|
||||||
|
default:
|
||||||
|
status = "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
return TablosPageState{
|
||||||
|
View: view,
|
||||||
|
Query: strings.TrimSpace(values.Get("q")),
|
||||||
|
Status: status,
|
||||||
|
ModalOpen: strings.TrimSpace(values.Get("modal")) == "create",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s TablosPageState) statusFilter() *TabloStatus {
|
||||||
|
switch s.Status {
|
||||||
|
case string(TabloStatusTodo):
|
||||||
|
status := TabloStatusTodo
|
||||||
|
return &status
|
||||||
|
case string(TabloStatusInProgress):
|
||||||
|
status := TabloStatusInProgress
|
||||||
|
return &status
|
||||||
|
case string(TabloStatusDone):
|
||||||
|
status := TabloStatusDone
|
||||||
|
return &status
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) PostTablos() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, ok := h.authenticatedUser(r.Context(), r)
|
||||||
|
if !ok {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "invalid form payload", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := parseTablosPageState(r.Form)
|
||||||
|
state.ModalOpen = true
|
||||||
|
|
||||||
|
name := strings.TrimSpace(r.FormValue("name"))
|
||||||
|
if name == "" {
|
||||||
|
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, nil, name, "Le nom du projet est requis"), http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := h.repo.CreateTablo(r.Context(), CreateTabloInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Name: name,
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
}); err != nil {
|
||||||
|
http.Error(w, "failed to create tablo", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.ModalOpen = false
|
||||||
|
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Query: state.Query,
|
||||||
|
Status: state.statusFilter(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", ""), http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) DeleteTablo() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, ok := h.authenticatedUser(r.Context(), r)
|
||||||
|
if !ok {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tabloID, err := uuid.Parse(r.PathValue("tabloID"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid tablo id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.repo.SoftDeleteTablo(r.Context(), tabloID, user.ID); err != nil {
|
||||||
|
if errors.Is(err, ErrTabloNotFound) {
|
||||||
|
http.Error(w, "tablo not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "failed to delete tablo", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := parseTablosPageState(r.URL.Query())
|
||||||
|
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Query: state.Query,
|
||||||
|
Status: state.statusFilter(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", ""), http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AuthHandler) renderTablosPage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, ok := h.authenticatedUser(r.Context(), r)
|
||||||
|
if !ok {
|
||||||
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state := parseTablosPageState(r.URL.Query())
|
||||||
|
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Query: state.Query,
|
||||||
|
Status: state.statusFilter(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", ""), http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tablosPageViewModel(user PublicUser, state TablosPageState, tablos []TabloRecord, formName string, errorMessage string) views.TablosPageViewModel {
|
||||||
|
return views.NewTablosPageViewModel(
|
||||||
|
user.DisplayName,
|
||||||
|
state.View,
|
||||||
|
state.Query,
|
||||||
|
state.Status,
|
||||||
|
state.ModalOpen,
|
||||||
|
formName,
|
||||||
|
errorMessage,
|
||||||
|
buildTabloCardViews(tablos, state),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTablosResponse(w http.ResponseWriter, r *http.Request, activePath string, vm views.TablosPageViewModel, statusCode int) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
content := views.TablosPageContent(vm)
|
||||||
|
if isHXRequest(r) {
|
||||||
|
err = views.DashboardContentSwapWithMainClass(activePath, "flex-1 overflow-auto", content).Render(r.Context(), w)
|
||||||
|
} else {
|
||||||
|
err = views.DashboardPageWithMainClass(activePath, "flex-1 overflow-auto", content).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to render tablos page", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *InMemoryAuthRepository) CreateTablo(_ context.Context, input CreateTabloInput) (TabloRecord, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
tablo := TabloRecord{
|
||||||
|
ID: uuid.New(),
|
||||||
|
OwnerID: input.OwnerID,
|
||||||
|
Name: strings.TrimSpace(input.Name),
|
||||||
|
Status: input.Status,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.tablos[tablo.ID] = tablo
|
||||||
|
return tablo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *InMemoryAuthRepository) ListTablos(_ context.Context, input ListTablosInput) ([]TabloRecord, error) {
|
||||||
|
r.mu.RLock()
|
||||||
|
defer r.mu.RUnlock()
|
||||||
|
|
||||||
|
query := normalizeTabloQuery(input.Query)
|
||||||
|
var tablos []TabloRecord
|
||||||
|
|
||||||
|
for _, tablo := range r.tablos {
|
||||||
|
if tablo.OwnerID != input.OwnerID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tablo.DeletedAt != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if input.Status != nil && tablo.Status != *input.Status {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if query != "" && !strings.Contains(strings.ToLower(tablo.Name), query) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tablos = append(tablos, tablo)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortTablosByCreatedAtDesc(tablos)
|
||||||
|
return tablos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *InMemoryAuthRepository) SoftDeleteTablo(_ context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
tablo, ok := r.tablos[tabloID]
|
||||||
|
if !ok || tablo.OwnerID != ownerID || tablo.DeletedAt != nil {
|
||||||
|
return ErrTabloNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
tablo.DeletedAt = &now
|
||||||
|
tablo.UpdatedAt = now
|
||||||
|
r.tablos[tabloID] = tablo
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortTablosByCreatedAtDesc(tablos []TabloRecord) {
|
||||||
|
for i := 0; i < len(tablos); i++ {
|
||||||
|
for j := i + 1; j < len(tablos); j++ {
|
||||||
|
if tablos[j].CreatedAt.After(tablos[i].CreatedAt) {
|
||||||
|
tablos[i], tablos[j] = tablos[j], tablos[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTabloCardViews(tablos []TabloRecord, state TablosPageState) []views.TabloCardView {
|
||||||
|
items := make([]views.TabloCardView, 0, len(tablos))
|
||||||
|
for _, tablo := range tablos {
|
||||||
|
statusLabel, statusClass, progress, statusTone := tabloStatusPresentation(tablo.Status)
|
||||||
|
iconKind, bgClass, fgClass, accent := tabloIconPresentation(tablo.Name)
|
||||||
|
|
||||||
|
items = append(items, views.TabloCardView{
|
||||||
|
ID: tablo.ID.String(),
|
||||||
|
Name: tablo.Name,
|
||||||
|
Status: string(tablo.Status),
|
||||||
|
StatusLabel: statusLabel,
|
||||||
|
StatusClass: statusClass,
|
||||||
|
StatusTone: statusTone,
|
||||||
|
Progress: progress,
|
||||||
|
CreatedAtLabel: formatFrenchDate(tablo.CreatedAt),
|
||||||
|
CardDateLabel: formatCardDate(tablo.CreatedAt),
|
||||||
|
ProgressLabel: fmt.Sprintf("%d%%", progress),
|
||||||
|
DeleteURL: "/tablos/" + tablo.ID.String(),
|
||||||
|
DeleteRequestURL: buildDeleteRequestURL("/tablos/"+tablo.ID.String(), state),
|
||||||
|
IconKind: iconKind,
|
||||||
|
IconBgClass: bgClass,
|
||||||
|
IconFgClass: fgClass,
|
||||||
|
Accent: accent,
|
||||||
|
Initial: projectInitial(tablo.Name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDeleteRequestURL(path string, state TablosPageState) string {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("view", state.View)
|
||||||
|
values.Set("status", state.Status)
|
||||||
|
if strings.TrimSpace(state.Query) != "" {
|
||||||
|
values.Set("q", strings.TrimSpace(state.Query))
|
||||||
|
}
|
||||||
|
encoded := values.Encode()
|
||||||
|
if encoded == "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return path + "?" + encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func tabloStatusPresentation(status TabloStatus) (string, string, int, string) {
|
||||||
|
switch status {
|
||||||
|
case TabloStatusInProgress:
|
||||||
|
return "En cours", "bg-[#FFF4E2] text-[#DB9729] border border-[#DB9729]", 50, "warning"
|
||||||
|
case TabloStatusDone:
|
||||||
|
return "Terminé", "bg-green-50 text-green-600 border border-green-200 dark:bg-green-950/30 dark:text-green-400 dark:border-green-800", 100, "success"
|
||||||
|
default:
|
||||||
|
return "À faire", "bg-blue-50 text-blue-600 border border-blue-200 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800", 0, "info"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tabloIconPresentation(name string) (string, string, string, string) {
|
||||||
|
switch len(strings.TrimSpace(name)) % 3 {
|
||||||
|
case 1:
|
||||||
|
return "gem", "bg-purple-500", "text-white", "purple"
|
||||||
|
case 2:
|
||||||
|
return "sparkles", "bg-cyan-500", "text-gray-700", "red"
|
||||||
|
default:
|
||||||
|
return "bolt", "bg-blue-500", "text-white", "blue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatFrenchDate(value time.Time) string {
|
||||||
|
months := []string{"janv.", "fevr.", "mars", "avr.", "mai", "juin", "juil.", "aout", "sept.", "oct.", "nov.", "dec."}
|
||||||
|
month := months[int(value.Month())-1]
|
||||||
|
return fmt.Sprintf("%02d %s %d", value.Day(), month, value.Year())
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatCardDate(value time.Time) string {
|
||||||
|
return value.Format("Jan 02, 2006")
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectInitial(name string) string {
|
||||||
|
trimmed := strings.TrimSpace(name)
|
||||||
|
if trimmed == "" {
|
||||||
|
return "P"
|
||||||
|
}
|
||||||
|
runes := []rune(trimmed)
|
||||||
|
return strings.ToUpper(string(runes[0]))
|
||||||
|
}
|
||||||
632
go-backend/internal/web/handlers/tablos_test.go
Normal file
632
go-backend/internal/web/handlers/tablos_test.go
Normal file
|
|
@ -0,0 +1,632 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInMemoryTablosListExcludesSoftDeletedRows(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected demo user, got error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedTablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Name: "Visible",
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create deleted tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keptTablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Name: "Kept",
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create kept tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.SoftDeleteTablo(context.Background(), deletedTablo.ID, user.ID); err != nil {
|
||||||
|
t.Fatalf("soft delete tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tablos, err := repo.ListTablos(context.Background(), ListTablosInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list tablos: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tablos) != 1 {
|
||||||
|
t.Fatalf("expected 1 visible tablo, got %d", len(tablos))
|
||||||
|
}
|
||||||
|
|
||||||
|
if tablos[0].ID != keptTablo.ID {
|
||||||
|
t.Fatalf("expected kept tablo %s, got %s", keptTablo.ID, tablos[0].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInMemoryTablosListFiltersBySearchAndStatus(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected demo user, got error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Name: "Hello Product",
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create todo tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedTablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Name: "Hello Delivery",
|
||||||
|
Status: TabloStatusInProgress,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create in progress tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tablos, err := repo.ListTablos(context.Background(), ListTablosInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
Query: "delivery",
|
||||||
|
Status: &[]TabloStatus{TabloStatusInProgress}[0],
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list filtered tablos: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tablos) != 1 {
|
||||||
|
t.Fatalf("expected 1 filtered tablo, got %d", len(tablos))
|
||||||
|
}
|
||||||
|
|
||||||
|
if tablos[0].ID != expectedTablo.ID {
|
||||||
|
t.Fatalf("expected tablo %s, got %s", expectedTablo.ID, tablos[0].ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInMemoryTablosSoftDeleteRejectsDifferentOwner(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
owner, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected demo user, got error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
otherUserID, err := repo.CreateAuthUser(context.Background(), CreateAuthUserInput{
|
||||||
|
Email: "other@xtablo.com",
|
||||||
|
EncryptedPassword: "hash",
|
||||||
|
DisplayName: "other",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create other user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: owner.ID,
|
||||||
|
Name: "Owned",
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create owned tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.SoftDeleteTablo(context.Background(), tablo.ID, otherUserID); err == nil {
|
||||||
|
t.Fatal("expected deleting another user's tablo to fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTablosPageDefaultsToGridAndAllStatus(t *testing.T) {
|
||||||
|
handler := newTestAuthHandler(t)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/tablos", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.GetTablosPage().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
"Mes Projets",
|
||||||
|
"Nouveau projet",
|
||||||
|
"Vue en grille",
|
||||||
|
"Rechercher...",
|
||||||
|
"Tous",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected body to contain %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTablosPageHonorsSearchAndStatus(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
handler := NewAuthHandler(repo)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
userID, ok := handler.currentUserID(req.Context(), req)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected user session")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: userID,
|
||||||
|
Name: "Alpha Draft",
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create todo tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: userID,
|
||||||
|
Name: "Beta Delivery",
|
||||||
|
Status: TabloStatusInProgress,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create filtered tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pageReq := httptest.NewRequest(http.MethodGet, "/tablos?q=delivery&status=in_progress", nil)
|
||||||
|
pageReq.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.GetTablosPage().ServeHTTP(rec, pageReq)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
if !strings.Contains(body, "Beta Delivery") {
|
||||||
|
t.Fatalf("expected filtered tablo to be visible, got %q", body)
|
||||||
|
}
|
||||||
|
if strings.Contains(body, "Alpha Draft") {
|
||||||
|
t.Fatalf("expected non-matching tablo to be filtered out, got %q", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTablosPageUsesSharedToolbarButtonAndStatusBadge(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
handler := NewAuthHandler(repo)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
userID, ok := handler.currentUserID(req.Context(), req)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected user session")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: userID,
|
||||||
|
Name: "Shared UI",
|
||||||
|
Status: TabloStatusInProgress,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pageReq := httptest.NewRequest(http.MethodGet, "/tablos", nil)
|
||||||
|
pageReq.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.GetTablosPage().ServeHTTP(rec, pageReq)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
`ui-button ui-button-primary ui-button-md`,
|
||||||
|
`ui-badge ui-badge-warning`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected shared primitive markup %q, got %q", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTablosPageModalUsesSharedFormPrimitives(t *testing.T) {
|
||||||
|
handler := newTestAuthHandler(t)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/tablos?modal=create", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.GetTablosPage().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
`ui-modal-panel`,
|
||||||
|
`ui-form-field`,
|
||||||
|
`class="ui-input"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected modal primitive markup %q, got %q", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTablosPageListViewRendersTableLayout(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
handler := NewAuthHandler(repo)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
userID, ok := handler.currentUserID(req.Context(), req)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected user session")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: userID,
|
||||||
|
Name: "Table View",
|
||||||
|
Status: TabloStatusInProgress,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pageReq := httptest.NewRequest(http.MethodGet, "/tablos?view=list", nil)
|
||||||
|
pageReq.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.GetTablosPage().ServeHTTP(rec, pageReq)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
`class="ui-table-shell"`,
|
||||||
|
`<table class="ui-table">`,
|
||||||
|
"Progression",
|
||||||
|
"Table View",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected list view to contain %q, got %q", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTablosPageEmptyStateUsesSharedPrimitive(t *testing.T) {
|
||||||
|
handler := newTestAuthHandler(t)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/tablos", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.GetTablosPage().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
`ui-empty-state`,
|
||||||
|
`Aucun projet trouvé`,
|
||||||
|
`Créez votre premier projet`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected empty state primitive markup %q, got %q", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTablosPageListViewUsesDirectTableIconMarkup(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
handler := NewAuthHandler(repo)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
userID, ok := handler.currentUserID(req.Context(), req)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected user session")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: userID,
|
||||||
|
Name: "Markup Check",
|
||||||
|
Status: TabloStatusInProgress,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pageReq := httptest.NewRequest(http.MethodGet, "/tablos?view=list", nil)
|
||||||
|
pageReq.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.GetTablosPage().ServeHTTP(rec, pageReq)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
`class="flex items-center gap-1.5 [&>svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0"><svg viewBox="0 0 24 24"`,
|
||||||
|
`class="borderless-icon-button"`,
|
||||||
|
`class="lucide lucide-trash2 w-4 h-4"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected list view markup to contain %q, got %q", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTablosPageGridUsesProjectDateRowMarkup(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
handler := NewAuthHandler(repo)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
userID, ok := handler.currentUserID(req.Context(), req)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected user session")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: userID,
|
||||||
|
Name: "Calendar Check",
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pageReq := httptest.NewRequest(http.MethodGet, "/tablos", nil)
|
||||||
|
pageReq.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.GetTablosPage().ServeHTTP(rec, pageReq)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(rec.Body.String(), `class="project-date-row"><svg viewBox="0 0 24 24"`) {
|
||||||
|
t.Fatalf("expected grid card calendar icon to render inside project-date-row markup, got %q", rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTablosPageGridUsesProjectCardMarkup(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
handler := NewAuthHandler(repo)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
userID, ok := handler.currentUserID(req.Context(), req)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected user session")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: userID,
|
||||||
|
Name: "Hello",
|
||||||
|
Status: TabloStatusInProgress,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pageReq := httptest.NewRequest(http.MethodGet, "/tablos", nil)
|
||||||
|
pageReq.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.GetTablosPage().ServeHTTP(rec, pageReq)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
`<article class="project-card">`,
|
||||||
|
`class="project-card-top"`,
|
||||||
|
`class="borderless-icon-button"`,
|
||||||
|
`class="project-card-title-row"`,
|
||||||
|
`class="project-avatar project-accent-`,
|
||||||
|
`class="project-date-row"`,
|
||||||
|
`class="project-progress-track"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected grid card markup to contain %q, got %q", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostTablosCreatesTodoTablo(t *testing.T) {
|
||||||
|
handler := newTestAuthHandler(t)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("name", "Roadmap")
|
||||||
|
form.Set("view", "grid")
|
||||||
|
form.Set("status", "all")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.PostTablos().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
if !strings.Contains(body, "Roadmap") {
|
||||||
|
t.Fatalf("expected created tablo in response, got %q", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "À faire") {
|
||||||
|
t.Fatalf("expected todo status label in response, got %q", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostTablosWithEmptyNameReturns422(t *testing.T) {
|
||||||
|
handler := newTestAuthHandler(t)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/tablos", strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.PostTablos().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusUnprocessableEntity {
|
||||||
|
t.Fatalf("expected status 422, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(rec.Body.String(), "Le nom du projet est requis") {
|
||||||
|
t.Fatalf("expected validation error, got %q", rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteTabloSoftDeletesOwnedRow(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
handler := NewAuthHandler(repo)
|
||||||
|
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
userID, ok := handler.currentUserID(req.Context(), req)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected user session")
|
||||||
|
}
|
||||||
|
|
||||||
|
tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: userID,
|
||||||
|
Name: "Disposable",
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteReq := httptest.NewRequest(http.MethodDelete, "/tablos/"+tablo.ID.String(), nil)
|
||||||
|
deleteReq.SetPathValue("tabloID", tablo.ID.String())
|
||||||
|
deleteReq.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.DeleteTablo().ServeHTTP(rec, deleteReq)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := repo.ListTablos(context.Background(), ListTablosInput{OwnerID: userID})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("list tablos: %v", err)
|
||||||
|
}
|
||||||
|
if len(list) != 0 {
|
||||||
|
t.Fatalf("expected deleted tablo to be hidden, got %d rows", len(list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteTabloRejectsDifferentOwner(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
handler := NewAuthHandler(repo)
|
||||||
|
|
||||||
|
ownerCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
|
||||||
|
ownerReq := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
ownerReq.AddCookie(ownerCookie)
|
||||||
|
ownerID, ok := handler.currentUserID(ownerReq.Context(), ownerReq)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected owner session")
|
||||||
|
}
|
||||||
|
|
||||||
|
tablo, err := repo.CreateTablo(context.Background(), CreateTabloInput{
|
||||||
|
OwnerID: ownerID,
|
||||||
|
Name: "Owned",
|
||||||
|
Status: TabloStatusTodo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create tablo: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordHash, err := hashPassword("xtablo-demo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("hash password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repo.CreateAuthUser(context.Background(), CreateAuthUserInput{
|
||||||
|
Email: "other@xtablo.com",
|
||||||
|
EncryptedPassword: passwordHash,
|
||||||
|
DisplayName: "other",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
otherCookie := loginTestUser(t, handler, "other@xtablo.com", "xtablo-demo")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/tablos/"+tablo.ID.String(), nil)
|
||||||
|
req.SetPathValue("tabloID", tablo.ID.String())
|
||||||
|
req.AddCookie(otherCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.DeleteTablo().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotFound {
|
||||||
|
t.Fatalf("expected status 404, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginTestUser(t *testing.T, handler *AuthHandler, email string, password string) *http.Cookie {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("email", email)
|
||||||
|
form.Set("password", password)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.PostLogin().ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
for _, cookie := range rec.Result().Cookies() {
|
||||||
|
if cookie.Name == "xtablo_session" {
|
||||||
|
return cookie
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatal("expected session cookie to be set")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
10
go-backend/internal/web/ui/badge.templ
Normal file
10
go-backend/internal/web/ui/badge.templ
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type BadgeProps struct {
|
||||||
|
Label string
|
||||||
|
Variant BadgeVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Badge(props BadgeProps) {
|
||||||
|
<span class={ badgeClass(props.Variant) }>{ props.Label }</span>
|
||||||
|
}
|
||||||
76
go-backend/internal/web/ui/badge_templ.go
Normal file
76
go-backend/internal/web/ui/badge_templ.go
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package ui
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type BadgeProps struct {
|
||||||
|
Label string
|
||||||
|
Variant BadgeVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
func Badge(props BadgeProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var2 = []any{badgeClass(props.Variant)}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<span class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/badge.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/badge.templ`, Line: 9, Col: 56}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
21
go-backend/internal/web/ui/button.templ
Normal file
21
go-backend/internal/web/ui/button.templ
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type ButtonProps struct {
|
||||||
|
Label string
|
||||||
|
Variant ButtonVariant
|
||||||
|
Size Size
|
||||||
|
Type string
|
||||||
|
Icon string
|
||||||
|
Attrs templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Button(props ButtonProps) {
|
||||||
|
<button type={ buttonType(props.Type) } class={ buttonClass(props.Variant, props.Size) } { props.Attrs... }>
|
||||||
|
if props.Icon != "" {
|
||||||
|
<span class="ui-button-icon">
|
||||||
|
@UIIcon(props.Icon)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
{ props.Label }
|
||||||
|
</button>
|
||||||
|
}
|
||||||
115
go-backend/internal/web/ui/button_templ.go
Normal file
115
go-backend/internal/web/ui/button_templ.go
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package ui
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type ButtonProps struct {
|
||||||
|
Label string
|
||||||
|
Variant ButtonVariant
|
||||||
|
Size Size
|
||||||
|
Type string
|
||||||
|
Icon string
|
||||||
|
Attrs templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
func Button(props ButtonProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var2 = []any{buttonClass(props.Variant, props.Size)}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<button type=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(buttonType(props.Type))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/button.templ`, Line: 13, Col: 38}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/button.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, props.Attrs)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, ">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if props.Icon != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span class=\"ui-button-icon\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = UIIcon(props.Icon).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/button.templ`, Line: 19, Col: 15}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</button>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
27
go-backend/internal/web/ui/card.templ
Normal file
27
go-backend/internal/web/ui/card.templ
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type CardProps struct {
|
||||||
|
Header templ.Component
|
||||||
|
Body templ.Component
|
||||||
|
Footer templ.Component
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Card(props CardProps) {
|
||||||
|
<section class="ui-card">
|
||||||
|
if props.Header != nil {
|
||||||
|
<div class="ui-card-header">
|
||||||
|
@props.Header
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if props.Body != nil {
|
||||||
|
<div class="ui-card-body">
|
||||||
|
@props.Body
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if props.Footer != nil {
|
||||||
|
<div class="ui-card-footer">
|
||||||
|
@props.Footer
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
92
go-backend/internal/web/ui/card_templ.go
Normal file
92
go-backend/internal/web/ui/card_templ.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package ui
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type CardProps struct {
|
||||||
|
Header templ.Component
|
||||||
|
Body templ.Component
|
||||||
|
Footer templ.Component
|
||||||
|
}
|
||||||
|
|
||||||
|
func Card(props CardProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"ui-card\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if props.Header != nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"ui-card-header\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = props.Header.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if props.Body != nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"ui-card-body\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = props.Body.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if props.Footer != nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"ui-card-footer\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = props.Footer.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</section>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
61
go-backend/internal/web/ui/catalog/catalog.templ
Normal file
61
go-backend/internal/web/ui/catalog/catalog.templ
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
package catalog
|
||||||
|
|
||||||
|
templ CatalogPage(page Page) {
|
||||||
|
<main class="catalog-page">
|
||||||
|
<nav class="catalog-nav" aria-label="Catalog navigation">
|
||||||
|
<a href="./index.html" class="catalog-home-link">Catalog</a>
|
||||||
|
<div class="catalog-nav-links">
|
||||||
|
for _, navPage := range Pages() {
|
||||||
|
<a
|
||||||
|
href={ "./" + navPage.Slug + ".html" }
|
||||||
|
class={ catalogNavLinkClass(navPage.Slug == page.Slug) }
|
||||||
|
>
|
||||||
|
{ navPage.Title }
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<header class="catalog-page-header">
|
||||||
|
<p class="catalog-eyebrow">Design System</p>
|
||||||
|
<h1>{ page.Title }</h1>
|
||||||
|
<p>{ page.Description }</p>
|
||||||
|
</header>
|
||||||
|
<div class="catalog-example-list">
|
||||||
|
for _, example := range page.Examples {
|
||||||
|
<section class="catalog-example">
|
||||||
|
<div class="catalog-example-copy">
|
||||||
|
<h2>{ example.Title }</h2>
|
||||||
|
if example.Description != "" {
|
||||||
|
<p>{ example.Description }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="catalog-example-preview">
|
||||||
|
@example.Preview
|
||||||
|
</div>
|
||||||
|
if example.Snippet != "" {
|
||||||
|
<pre class="catalog-example-snippet"><code>{ example.Snippet }</code></pre>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ CatalogIndex(pages []Page) {
|
||||||
|
<main class="catalog-page">
|
||||||
|
<header class="catalog-page-header">
|
||||||
|
<p class="catalog-eyebrow">Design System</p>
|
||||||
|
<h1>Component Catalog</h1>
|
||||||
|
<p>Static documentation generated from the same templ primitives used by the Go application.</p>
|
||||||
|
</header>
|
||||||
|
<div class="catalog-page-list">
|
||||||
|
for _, page := range pages {
|
||||||
|
<a href={ "./" + page.Slug + ".html" } class="catalog-page-link-card">
|
||||||
|
<h2>{ page.Title }</h2>
|
||||||
|
<p>{ page.Description }</p>
|
||||||
|
<p class="catalog-page-link">/{ page.Slug }.html</p>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
288
go-backend/internal/web/ui/catalog/catalog_templ.go
Normal file
288
go-backend/internal/web/ui/catalog/catalog_templ.go
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package catalog
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
func CatalogPage(page Page) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<main class=\"catalog-page\"><nav class=\"catalog-nav\" aria-label=\"Catalog navigation\"><a href=\"./index.html\" class=\"catalog-home-link\">Catalog</a><div class=\"catalog-nav-links\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, navPage := range Pages() {
|
||||||
|
var templ_7745c5c3_Var2 = []any{catalogNavLinkClass(navPage.Slug == page.Slug)}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs("./" + navPage.Slug + ".html")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 10, Col: 42}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(navPage.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 13, Col: 21}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></nav><header class=\"catalog-page-header\"><p class=\"catalog-eyebrow\">Design System</p><h1>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(page.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 20, Col: 19}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h1><p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(page.Description)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 21, Col: 24}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p></header><div class=\"catalog-example-list\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, example := range page.Examples {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<section class=\"catalog-example\"><div class=\"catalog-example-copy\"><h2>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(example.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 27, Col: 25}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</h2>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if example.Description != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(example.Description)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 29, Col: 31}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div><div class=\"catalog-example-preview\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = example.Preview.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if example.Snippet != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<pre class=\"catalog-example-snippet\"><code>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(example.Snippet)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 36, Col: 66}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</code></pre>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</section>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></main>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CatalogIndex(pages []Page) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var11 == nil {
|
||||||
|
templ_7745c5c3_Var11 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<main class=\"catalog-page\"><header class=\"catalog-page-header\"><p class=\"catalog-eyebrow\">Design System</p><h1>Component Catalog</h1><p>Static documentation generated from the same templ primitives used by the Go application.</p></header><div class=\"catalog-page-list\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, page := range pages {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs("./" + page.Slug + ".html")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 53, Col: 40}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"catalog-page-link-card\"><h2>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(page.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 54, Col: 21}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</h2><p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var14 string
|
||||||
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(page.Description)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 55, Col: 26}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</p><p class=\"catalog-page-link\">/")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(page.Slug)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/catalog/catalog.templ`, Line: 56, Col: 46}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, ".html</p></a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div></main>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
174
go-backend/internal/web/ui/catalog/catalog_test.go
Normal file
174
go-backend/internal/web/ui/catalog/catalog_test.go
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
package catalog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPagesIncludeTokensAndButtons(t *testing.T) {
|
||||||
|
pages := Pages()
|
||||||
|
|
||||||
|
if len(pages) == 0 {
|
||||||
|
t.Fatal("expected catalog pages")
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasTokens bool
|
||||||
|
var hasButtons bool
|
||||||
|
for _, page := range pages {
|
||||||
|
switch page.Slug {
|
||||||
|
case "tokens":
|
||||||
|
hasTokens = true
|
||||||
|
case "buttons":
|
||||||
|
hasButtons = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasTokens {
|
||||||
|
t.Fatal("expected tokens page")
|
||||||
|
}
|
||||||
|
if !hasButtons {
|
||||||
|
t.Fatal("expected buttons page")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPagesIncludePrimitiveCatalogCoverage(t *testing.T) {
|
||||||
|
pages := Pages()
|
||||||
|
|
||||||
|
for _, slug := range []string{
|
||||||
|
"badges",
|
||||||
|
"icon-buttons",
|
||||||
|
"inputs",
|
||||||
|
"form-fields",
|
||||||
|
"modals",
|
||||||
|
"tables",
|
||||||
|
"empty-states",
|
||||||
|
"cards",
|
||||||
|
} {
|
||||||
|
if _, ok := FindPage(slug); !ok {
|
||||||
|
t.Fatalf("expected catalog page %q", slug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pages) < 10 {
|
||||||
|
t.Fatalf("expected expanded primitive catalog, got %d pages", len(pages))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestButtonPageExamplesRenderRealPrimitives(t *testing.T) {
|
||||||
|
page, ok := FindPage("buttons")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected buttons page")
|
||||||
|
}
|
||||||
|
if len(page.Examples) == 0 {
|
||||||
|
t.Fatal("expected button examples")
|
||||||
|
}
|
||||||
|
|
||||||
|
html := renderToString(t, page.Examples[0].Preview)
|
||||||
|
for _, want := range []string{
|
||||||
|
`ui-button`,
|
||||||
|
`ui-button-primary`,
|
||||||
|
`Nouveau projet`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogPageRendersMetadataAndExamples(t *testing.T) {
|
||||||
|
page, ok := FindPage("tokens")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected tokens page")
|
||||||
|
}
|
||||||
|
|
||||||
|
html := renderToString(t, CatalogPage(page))
|
||||||
|
for _, want := range []string{
|
||||||
|
`Design System`,
|
||||||
|
page.Title,
|
||||||
|
`catalog-example`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogIndexLinksToPrimitivePages(t *testing.T) {
|
||||||
|
html := renderToString(t, CatalogIndex(Pages()))
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`href="./inputs.html"`,
|
||||||
|
`href="./buttons.html"`,
|
||||||
|
`class="catalog-page-link-card"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogPageRendersSharedNavigationWithActivePage(t *testing.T) {
|
||||||
|
page, ok := FindPage("inputs")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected inputs page")
|
||||||
|
}
|
||||||
|
|
||||||
|
html := renderToString(t, CatalogPage(page))
|
||||||
|
for _, want := range []string{
|
||||||
|
`href="./index.html"`,
|
||||||
|
`href="./buttons.html"`,
|
||||||
|
`href="./inputs.html"`,
|
||||||
|
`catalog-nav-link is-active`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrimitiveExamplesRenderRealMarkup(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
slug string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{slug: "badges", want: []string{`ui-badge`, `En cours`}},
|
||||||
|
{slug: "icon-buttons", want: []string{`borderless-icon-button`, `aria-label="Supprimer le projet"`}},
|
||||||
|
{slug: "inputs", want: []string{`class="ui-input"`, `placeholder="Nom du projet"`}},
|
||||||
|
{slug: "form-fields", want: []string{`ui-form-field`, `ui-form-label`}},
|
||||||
|
{slug: "modals", want: []string{`ui-modal-panel`, `Créer le projet`}},
|
||||||
|
{slug: "tables", want: []string{`class="ui-table"`, `Table View`}},
|
||||||
|
{slug: "empty-states", want: []string{`ui-empty-state`, `Aucun projet trouvé`}},
|
||||||
|
{slug: "cards", want: []string{`ui-card`, `Header`}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
page, ok := FindPage(tt.slug)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected page %q", tt.slug)
|
||||||
|
}
|
||||||
|
if len(page.Examples) == 0 {
|
||||||
|
t.Fatalf("expected examples for %q", tt.slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
html := renderToString(t, page.Examples[0].Preview)
|
||||||
|
for _, want := range tt.want {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("page %q expected %q in %q", tt.slug, want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderToString(t *testing.T, component templ.Component) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := component.Render(context.Background(), &buf); err != nil {
|
||||||
|
t.Fatalf("render component: %v", err)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
326
go-backend/internal/web/ui/catalog/examples.go
Normal file
326
go-backend/internal/web/ui/catalog/examples.go
Normal file
|
|
@ -0,0 +1,326 @@
|
||||||
|
package catalog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
|
||||||
|
"xtablo-backend/internal/web/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type anyComponent = templ.Component
|
||||||
|
|
||||||
|
func buttonExamples() []Example {
|
||||||
|
return []Example{
|
||||||
|
{
|
||||||
|
Title: "Primary action",
|
||||||
|
Description: "Used for the main action in a page section or modal footer.",
|
||||||
|
Preview: ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Nouveau projet",
|
||||||
|
Variant: ui.ButtonVariantPrimary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Nouveau projet",
|
||||||
|
Variant: ui.ButtonVariantPrimary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Danger action",
|
||||||
|
Description: "Used for irreversible actions after explicit confirmation.",
|
||||||
|
Preview: ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Supprimer",
|
||||||
|
Variant: ui.ButtonVariantDanger,
|
||||||
|
Size: ui.SizeLG,
|
||||||
|
Type: "submit",
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Supprimer",
|
||||||
|
Variant: ui.ButtonVariantDanger,
|
||||||
|
Size: ui.SizeLG,
|
||||||
|
Type: "submit",
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenExamples() []Example {
|
||||||
|
return []Example{
|
||||||
|
{
|
||||||
|
Title: "Status tones",
|
||||||
|
Description: "Shared semantic badges for info, warning, success, and danger states.",
|
||||||
|
Preview: componentFunc(func(ctx context.Context, w io.Writer) error {
|
||||||
|
for _, component := range []templ.Component{
|
||||||
|
ui.Badge(ui.BadgeProps{Label: "À faire", Variant: ui.BadgeVariantInfo}),
|
||||||
|
ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning}),
|
||||||
|
ui.Badge(ui.BadgeProps{Label: "Terminé", Variant: ui.BadgeVariantSuccess}),
|
||||||
|
ui.Badge(ui.BadgeProps{Label: "Erreur", Variant: ui.BadgeVariantDanger}),
|
||||||
|
} {
|
||||||
|
if _, err := io.WriteString(w, `<div class="catalog-inline">`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := component.Render(ctx, w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, `</div>`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning})`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func badgeExamples() []Example {
|
||||||
|
return []Example{
|
||||||
|
{
|
||||||
|
Title: "Status set",
|
||||||
|
Description: "The four semantic badge tones used across the app.",
|
||||||
|
Preview: componentFunc(func(ctx context.Context, w io.Writer) error {
|
||||||
|
return renderInlineComponents(ctx, w,
|
||||||
|
ui.Badge(ui.BadgeProps{Label: "À faire", Variant: ui.BadgeVariantInfo}),
|
||||||
|
ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning}),
|
||||||
|
ui.Badge(ui.BadgeProps{Label: "Terminé", Variant: ui.BadgeVariantSuccess}),
|
||||||
|
ui.Badge(ui.BadgeProps{Label: "Erreur", Variant: ui.BadgeVariantDanger}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.Badge(ui.BadgeProps{Label: "En cours", Variant: ui.BadgeVariantWarning})`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func iconButtonExamples() []Example {
|
||||||
|
return []Example{
|
||||||
|
{
|
||||||
|
Title: "Borderless destructive action",
|
||||||
|
Description: "Used for delete controls inside project cards and list rows.",
|
||||||
|
Preview: ui.IconButton(ui.IconButtonProps{
|
||||||
|
Label: "Supprimer le projet",
|
||||||
|
Icon: "trash",
|
||||||
|
Variant: ui.IconButtonVariantDangerGhost,
|
||||||
|
Type: "button",
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.IconButton(ui.IconButtonProps{
|
||||||
|
Label: "Supprimer le projet",
|
||||||
|
Icon: "trash",
|
||||||
|
Variant: ui.IconButtonVariantDangerGhost,
|
||||||
|
Type: "button",
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inputExamples() []Example {
|
||||||
|
return []Example{
|
||||||
|
{
|
||||||
|
Title: "Text input",
|
||||||
|
Description: "Single-line input for names, titles, and short labels.",
|
||||||
|
Preview: ui.Input(ui.InputProps{
|
||||||
|
Name: "name",
|
||||||
|
Value: "Projet Atlas",
|
||||||
|
Placeholder: "Nom du projet",
|
||||||
|
Type: "text",
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.Input(ui.InputProps{
|
||||||
|
Name: "name",
|
||||||
|
Value: "Projet Atlas",
|
||||||
|
Placeholder: "Nom du projet",
|
||||||
|
Type: "text",
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Textarea",
|
||||||
|
Description: "Multiline field for longer project notes and descriptions.",
|
||||||
|
Preview: ui.Textarea(ui.TextareaProps{
|
||||||
|
Name: "description",
|
||||||
|
Value: "Une description de projet plus détaillée.",
|
||||||
|
Placeholder: "Description",
|
||||||
|
Rows: 4,
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.Textarea(ui.TextareaProps{
|
||||||
|
Name: "description",
|
||||||
|
Value: "Une description de projet plus détaillée.",
|
||||||
|
Placeholder: "Description",
|
||||||
|
Rows: 4,
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formFieldExamples() []Example {
|
||||||
|
return []Example{
|
||||||
|
{
|
||||||
|
Title: "Field with validation",
|
||||||
|
Description: "Wraps a control with label and inline error feedback.",
|
||||||
|
Preview: ui.FormField(ui.FormFieldProps{
|
||||||
|
Label: "Nom",
|
||||||
|
For: "catalog-name",
|
||||||
|
Field: ui.Input(ui.InputProps{
|
||||||
|
ID: "catalog-name",
|
||||||
|
Name: "name",
|
||||||
|
Placeholder: "Nom du projet",
|
||||||
|
Type: "text",
|
||||||
|
}),
|
||||||
|
Error: "Le nom est requis",
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.FormField(ui.FormFieldProps{
|
||||||
|
Label: "Nom",
|
||||||
|
For: "catalog-name",
|
||||||
|
Field: ui.Input(ui.InputProps{
|
||||||
|
ID: "catalog-name",
|
||||||
|
Name: "name",
|
||||||
|
Placeholder: "Nom du projet",
|
||||||
|
Type: "text",
|
||||||
|
}),
|
||||||
|
Error: "Le nom est requis",
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func modalExamples() []Example {
|
||||||
|
return []Example{
|
||||||
|
{
|
||||||
|
Title: "Create modal",
|
||||||
|
Description: "Shared modal shell with a form body and action footer.",
|
||||||
|
Preview: ui.Modal(ui.ModalProps{
|
||||||
|
Title: "Créer un projet",
|
||||||
|
Body: ui.FormField(ui.FormFieldProps{
|
||||||
|
Label: "Nom du projet",
|
||||||
|
For: "modal-name",
|
||||||
|
Field: ui.Input(ui.InputProps{
|
||||||
|
ID: "modal-name",
|
||||||
|
Name: "name",
|
||||||
|
Placeholder: "Nom du projet",
|
||||||
|
Type: "text",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Actions: componentFunc(func(ctx context.Context, w io.Writer) error {
|
||||||
|
return renderComponents(ctx, w,
|
||||||
|
ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Annuler",
|
||||||
|
Variant: ui.ButtonVariantSecondary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
}),
|
||||||
|
ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Créer le projet",
|
||||||
|
Variant: ui.ButtonVariantPrimary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "submit",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.Modal(ui.ModalProps{
|
||||||
|
Title: "Créer un projet",
|
||||||
|
Body: ui.FormField(...),
|
||||||
|
Actions: ui.Button(...),
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableExamples() []Example {
|
||||||
|
return []Example{
|
||||||
|
{
|
||||||
|
Title: "List shell",
|
||||||
|
Description: "Shared wrapper for server-rendered resource tables.",
|
||||||
|
Preview: ui.Table(ui.TableProps{
|
||||||
|
Head: textComponent(`<tr><th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Projet</th><th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Statut</th></tr>`),
|
||||||
|
Body: textComponent(`<tr><td class="px-6 py-4">Table View</td><td class="px-6 py-4">En cours</td></tr>`),
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.Table(ui.TableProps{
|
||||||
|
Head: TabloListHead(),
|
||||||
|
Body: TabloListBody(tablos),
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func emptyStateExamples() []Example {
|
||||||
|
return []Example{
|
||||||
|
{
|
||||||
|
Title: "Centered empty state",
|
||||||
|
Description: "Used when a list has no rows yet and the next action should stay obvious.",
|
||||||
|
Preview: ui.EmptyState(ui.EmptyStateProps{
|
||||||
|
Title: "Aucun projet trouvé",
|
||||||
|
Description: "Créez votre premier projet",
|
||||||
|
Icon: ui.UIIcon("grid3x3"),
|
||||||
|
Action: ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Nouveau projet",
|
||||||
|
Variant: ui.ButtonVariantPrimary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
Icon: "plus",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.EmptyState(ui.EmptyStateProps{
|
||||||
|
Title: "Aucun projet trouvé",
|
||||||
|
Description: "Créez votre premier projet",
|
||||||
|
Icon: ui.UIIcon("grid3x3"),
|
||||||
|
Action: ui.Button(...),
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cardExamples() []Example {
|
||||||
|
return []Example{
|
||||||
|
{
|
||||||
|
Title: "Surface card",
|
||||||
|
Description: "Generic elevated surface with optional header and footer.",
|
||||||
|
Preview: ui.Card(ui.CardProps{
|
||||||
|
Header: textComponent("Header"),
|
||||||
|
Body: textComponent("Body"),
|
||||||
|
Footer: textComponent("Footer"),
|
||||||
|
}),
|
||||||
|
Snippet: `@ui.Card(ui.CardProps{
|
||||||
|
Header: textComponent("Header"),
|
||||||
|
Body: textComponent("Body"),
|
||||||
|
Footer: textComponent("Footer"),
|
||||||
|
})`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func componentFunc(fn func(context.Context, io.Writer) error) templ.Component {
|
||||||
|
return templ.ComponentFunc(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func textComponent(text string) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
|
||||||
|
_, err := io.WriteString(w, text)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderInlineComponents(ctx context.Context, w io.Writer, components ...templ.Component) error {
|
||||||
|
for _, component := range components {
|
||||||
|
if _, err := io.WriteString(w, `<div class="catalog-inline">`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := component.Render(ctx, w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.WriteString(w, `</div>`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderComponents(ctx context.Context, w io.Writer, components ...templ.Component) error {
|
||||||
|
for _, component := range components {
|
||||||
|
if err := component.Render(ctx, w); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
99
go-backend/internal/web/ui/catalog/pages.go
Normal file
99
go-backend/internal/web/ui/catalog/pages.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package catalog
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
|
type Example struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Preview anyComponent
|
||||||
|
Snippet string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Page struct {
|
||||||
|
Slug string
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Examples []Example
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pages() []Page {
|
||||||
|
return []Page{
|
||||||
|
{
|
||||||
|
Slug: "tokens",
|
||||||
|
Title: "Tokens",
|
||||||
|
Description: "Semantic colors and status roles used by the Go design system.",
|
||||||
|
Examples: tokenExamples(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: "buttons",
|
||||||
|
Title: "Buttons",
|
||||||
|
Description: "Primary, secondary, ghost, and destructive actions built from shared templ primitives.",
|
||||||
|
Examples: buttonExamples(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: "badges",
|
||||||
|
Title: "Badges",
|
||||||
|
Description: "Semantic status labels for todo, in-progress, success, and destructive states.",
|
||||||
|
Examples: badgeExamples(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: "icon-buttons",
|
||||||
|
Title: "Icon Buttons",
|
||||||
|
Description: "Compact icon-only actions for destructive and neutral controls.",
|
||||||
|
Examples: iconButtonExamples(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: "inputs",
|
||||||
|
Title: "Inputs",
|
||||||
|
Description: "Shared single-line and multiline text controls.",
|
||||||
|
Examples: inputExamples(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: "form-fields",
|
||||||
|
Title: "Form Fields",
|
||||||
|
Description: "Labeled controls with optional hint and error messaging.",
|
||||||
|
Examples: formFieldExamples(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: "modals",
|
||||||
|
Title: "Modals",
|
||||||
|
Description: "Shared modal shell for focused create, edit, and confirm flows.",
|
||||||
|
Examples: modalExamples(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: "tables",
|
||||||
|
Title: "Tables",
|
||||||
|
Description: "Shared table shell for server-rendered list views.",
|
||||||
|
Examples: tableExamples(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: "empty-states",
|
||||||
|
Title: "Empty States",
|
||||||
|
Description: "Centered fallback messaging with optional icon and action.",
|
||||||
|
Examples: emptyStateExamples(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Slug: "cards",
|
||||||
|
Title: "Cards",
|
||||||
|
Description: "Reusable bordered surfaces with optional header, body, and footer regions.",
|
||||||
|
Examples: cardExamples(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindPage(slug string) (Page, bool) {
|
||||||
|
index := slices.IndexFunc(Pages(), func(page Page) bool {
|
||||||
|
return page.Slug == slug
|
||||||
|
})
|
||||||
|
if index == -1 {
|
||||||
|
return Page{}, false
|
||||||
|
}
|
||||||
|
return Pages()[index], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func catalogNavLinkClass(active bool) string {
|
||||||
|
if active {
|
||||||
|
return "catalog-nav-link is-active"
|
||||||
|
}
|
||||||
|
return "catalog-nav-link"
|
||||||
|
}
|
||||||
27
go-backend/internal/web/ui/empty_state.templ
Normal file
27
go-backend/internal/web/ui/empty_state.templ
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type EmptyStateProps struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Icon templ.Component
|
||||||
|
Action templ.Component
|
||||||
|
}
|
||||||
|
|
||||||
|
templ EmptyState(props EmptyStateProps) {
|
||||||
|
<section class="ui-empty-state">
|
||||||
|
if props.Icon != nil {
|
||||||
|
<div class="ui-empty-state-icon">
|
||||||
|
@props.Icon
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<h3 class="ui-empty-state-title">{ props.Title }</h3>
|
||||||
|
if props.Description != "" {
|
||||||
|
<p class="ui-empty-state-description">{ props.Description }</p>
|
||||||
|
}
|
||||||
|
if props.Action != nil {
|
||||||
|
<div class="ui-empty-state-action">
|
||||||
|
@props.Action
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
115
go-backend/internal/web/ui/empty_state_templ.go
Normal file
115
go-backend/internal/web/ui/empty_state_templ.go
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package ui
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type EmptyStateProps struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
Icon templ.Component
|
||||||
|
Action templ.Component
|
||||||
|
}
|
||||||
|
|
||||||
|
func EmptyState(props EmptyStateProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<section class=\"ui-empty-state\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if props.Icon != nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"ui-empty-state-icon\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = props.Icon.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<h3 class=\"ui-empty-state-title\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(props.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/empty_state.templ`, Line: 17, Col: 48}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</h3>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if props.Description != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p class=\"ui-empty-state-description\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(props.Description)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/empty_state.templ`, Line: 19, Col: 60}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if props.Action != nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"ui-empty-state-action\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = props.Action.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</section>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
26
go-backend/internal/web/ui/form_field.templ
Normal file
26
go-backend/internal/web/ui/form_field.templ
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type FormFieldProps struct {
|
||||||
|
Label string
|
||||||
|
For string
|
||||||
|
Field templ.Component
|
||||||
|
Error string
|
||||||
|
Hint string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ FormField(props FormFieldProps) {
|
||||||
|
<div class="ui-form-field">
|
||||||
|
if props.Label != "" {
|
||||||
|
<label for={ props.For } class="ui-form-label">{ props.Label }</label>
|
||||||
|
}
|
||||||
|
if props.Field != nil {
|
||||||
|
@props.Field
|
||||||
|
}
|
||||||
|
if props.Hint != "" {
|
||||||
|
<p class="ui-form-hint">{ props.Hint }</p>
|
||||||
|
}
|
||||||
|
if props.Error != "" {
|
||||||
|
<p class="ui-form-error">{ props.Error }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
128
go-backend/internal/web/ui/form_field_templ.go
Normal file
128
go-backend/internal/web/ui/form_field_templ.go
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package ui
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type FormFieldProps struct {
|
||||||
|
Label string
|
||||||
|
For string
|
||||||
|
Field templ.Component
|
||||||
|
Error string
|
||||||
|
Hint string
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormField(props FormFieldProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"ui-form-field\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if props.Label != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<label for=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(props.For)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/form_field.templ`, Line: 14, Col: 25}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" class=\"ui-form-label\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/form_field.templ`, Line: 14, Col: 63}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</label> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if props.Field != nil {
|
||||||
|
templ_7745c5c3_Err = props.Field.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if props.Hint != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<p class=\"ui-form-hint\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.Hint)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/form_field.templ`, Line: 20, Col: 39}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if props.Error != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<p class=\"ui-form-error\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Error)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/form_field.templ`, Line: 23, Col: 41}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
31
go-backend/internal/web/ui/helpers.go
Normal file
31
go-backend/internal/web/ui/helpers.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
func buttonType(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return "button"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func inputType(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func inputID(id string, name string) string {
|
||||||
|
if id != "" {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func textareaRows(rows int) string {
|
||||||
|
if rows <= 0 {
|
||||||
|
rows = 4
|
||||||
|
}
|
||||||
|
return strconv.Itoa(rows)
|
||||||
|
}
|
||||||
68
go-backend/internal/web/ui/icon_button.templ
Normal file
68
go-backend/internal/web/ui/icon_button.templ
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type IconButtonProps struct {
|
||||||
|
Label string
|
||||||
|
Icon string
|
||||||
|
Variant IconButtonVariant
|
||||||
|
Type string
|
||||||
|
Attrs templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
templ IconButton(props IconButtonProps) {
|
||||||
|
<button type={ buttonType(props.Type) } class={ iconButtonClass(props.Variant) } aria-label={ props.Label } { props.Attrs... }>
|
||||||
|
@UIIcon(props.Icon)
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ UIIcon(kind string) {
|
||||||
|
switch kind {
|
||||||
|
case "plus":
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M5 12h14"></path>
|
||||||
|
<path d="M12 5v14"></path>
|
||||||
|
</svg>
|
||||||
|
case "grid3x3":
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2"></rect>
|
||||||
|
<path d="M3 9h18"></path>
|
||||||
|
<path d="M3 15h18"></path>
|
||||||
|
<path d="M9 3v18"></path>
|
||||||
|
<path d="M15 3v18"></path>
|
||||||
|
</svg>
|
||||||
|
case "list":
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M3 12h.01"></path>
|
||||||
|
<path d="M3 18h.01"></path>
|
||||||
|
<path d="M3 6h.01"></path>
|
||||||
|
<path d="M8 12h13"></path>
|
||||||
|
<path d="M8 18h13"></path>
|
||||||
|
<path d="M8 6h13"></path>
|
||||||
|
</svg>
|
||||||
|
case "filter":
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
|
||||||
|
</svg>
|
||||||
|
case "search":
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<path d="m21 21-4.3-4.3"></path>
|
||||||
|
</svg>
|
||||||
|
case "calendar":
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M8 2v4"></path>
|
||||||
|
<path d="M16 2v4"></path>
|
||||||
|
<rect width="18" height="18" x="3" y="4" rx="2"></rect>
|
||||||
|
<path d="M3 10h18"></path>
|
||||||
|
</svg>
|
||||||
|
case "trash":
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2 w-4 h-4" aria-hidden="true">
|
||||||
|
<path d="M3 6h18"></path>
|
||||||
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
|
||||||
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
|
||||||
|
<line x1="10" x2="10" y1="11" y2="17"></line>
|
||||||
|
<line x1="14" x2="14" y1="11" y2="17"></line>
|
||||||
|
</svg>
|
||||||
|
default:
|
||||||
|
<span aria-hidden="true">{ kind }</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
188
go-backend/internal/web/ui/icon_button_templ.go
Normal file
188
go-backend/internal/web/ui/icon_button_templ.go
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package ui
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type IconButtonProps struct {
|
||||||
|
Label string
|
||||||
|
Icon string
|
||||||
|
Variant IconButtonVariant
|
||||||
|
Type string
|
||||||
|
Attrs templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
func IconButton(props IconButtonProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var2 = []any{iconButtonClass(props.Variant)}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<button type=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(buttonType(props.Type))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/icon_button.templ`, Line: 12, Col: 38}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/icon_button.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" aria-label=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/icon_button.templ`, Line: 12, Col: 106}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, props.Attrs)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = UIIcon(props.Icon).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</button>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func UIIcon(kind string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var6 == nil {
|
||||||
|
templ_7745c5c3_Var6 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
switch kind {
|
||||||
|
case "plus":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M5 12h14\"></path> <path d=\"M12 5v14\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "grid3x3":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"></rect> <path d=\"M3 9h18\"></path> <path d=\"M3 15h18\"></path> <path d=\"M9 3v18\"></path> <path d=\"M15 3v18\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "list":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 12h.01\"></path> <path d=\"M3 18h.01\"></path> <path d=\"M3 6h.01\"></path> <path d=\"M8 12h13\"></path> <path d=\"M8 18h13\"></path> <path d=\"M8 6h13\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "filter":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"></polygon></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "search":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"></circle> <path d=\"m21 21-4.3-4.3\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "calendar":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M8 2v4\"></path> <path d=\"M16 2v4\"></path> <rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"></rect> <path d=\"M3 10h18\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "trash":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-trash2 w-4 h-4\" aria-hidden=\"true\"><path d=\"M3 6h18\"></path> <path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"></path> <path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"></path> <line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\"></line> <line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\"></line></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span aria-hidden=\"true\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(kind)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/icon_button.templ`, Line: 66, Col: 34}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
22
go-backend/internal/web/ui/input.templ
Normal file
22
go-backend/internal/web/ui/input.templ
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type InputProps struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
Placeholder string
|
||||||
|
Type string
|
||||||
|
Attrs templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Input(props InputProps) {
|
||||||
|
<input
|
||||||
|
id={ inputID(props.ID, props.Name) }
|
||||||
|
type={ inputType(props.Type) }
|
||||||
|
name={ props.Name }
|
||||||
|
value={ props.Value }
|
||||||
|
placeholder={ props.Placeholder }
|
||||||
|
class="ui-input"
|
||||||
|
{ props.Attrs... }
|
||||||
|
/>
|
||||||
|
}
|
||||||
122
go-backend/internal/web/ui/input_templ.go
Normal file
122
go-backend/internal/web/ui/input_templ.go
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package ui
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type InputProps struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
Placeholder string
|
||||||
|
Type string
|
||||||
|
Attrs templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
func Input(props InputProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<input id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(inputID(props.ID, props.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/input.templ`, Line: 14, Col: 36}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" type=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(inputType(props.Type))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/input.templ`, Line: 15, Col: 30}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" name=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/input.templ`, Line: 16, Col: 19}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Value)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/input.templ`, Line: 17, Col: 21}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" placeholder=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.Placeholder)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/input.templ`, Line: 18, Col: 33}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"ui-input\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, props.Attrs)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
27
go-backend/internal/web/ui/modal.templ
Normal file
27
go-backend/internal/web/ui/modal.templ
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type ModalProps struct {
|
||||||
|
Title string
|
||||||
|
Body templ.Component
|
||||||
|
Actions templ.Component
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Modal(props ModalProps) {
|
||||||
|
<div class="ui-modal-backdrop">
|
||||||
|
<div class="ui-modal-panel">
|
||||||
|
<div class="ui-modal-header">
|
||||||
|
<h2>{ props.Title }</h2>
|
||||||
|
</div>
|
||||||
|
if props.Body != nil {
|
||||||
|
<div class="ui-modal-body">
|
||||||
|
@props.Body
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
if props.Actions != nil {
|
||||||
|
<div class="ui-modal-actions">
|
||||||
|
@props.Actions
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
91
go-backend/internal/web/ui/modal_templ.go
Normal file
91
go-backend/internal/web/ui/modal_templ.go
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package ui
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type ModalProps struct {
|
||||||
|
Title string
|
||||||
|
Body templ.Component
|
||||||
|
Actions templ.Component
|
||||||
|
}
|
||||||
|
|
||||||
|
func Modal(props ModalProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"ui-modal-backdrop\"><div class=\"ui-modal-panel\"><div class=\"ui-modal-header\"><h2>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(props.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/modal.templ`, Line: 13, Col: 21}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h2></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if props.Body != nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"ui-modal-body\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = props.Body.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if props.Actions != nil {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"ui-modal-actions\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = props.Actions.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
23
go-backend/internal/web/ui/table.templ
Normal file
23
go-backend/internal/web/ui/table.templ
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type TableProps struct {
|
||||||
|
Head templ.Component
|
||||||
|
Body templ.Component
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Table(props TableProps) {
|
||||||
|
<div class="ui-table-shell">
|
||||||
|
<table class="ui-table">
|
||||||
|
<thead>
|
||||||
|
if props.Head != nil {
|
||||||
|
@props.Head
|
||||||
|
}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
if props.Body != nil {
|
||||||
|
@props.Body
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
65
go-backend/internal/web/ui/table_templ.go
Normal file
65
go-backend/internal/web/ui/table_templ.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package ui
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type TableProps struct {
|
||||||
|
Head templ.Component
|
||||||
|
Body templ.Component
|
||||||
|
}
|
||||||
|
|
||||||
|
func Table(props TableProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"ui-table-shell\"><table class=\"ui-table\"><thead>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if props.Head != nil {
|
||||||
|
templ_7745c5c3_Err = props.Head.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</thead> <tbody>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if props.Body != nil {
|
||||||
|
templ_7745c5c3_Err = props.Body.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</tbody></table></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
21
go-backend/internal/web/ui/textarea.templ
Normal file
21
go-backend/internal/web/ui/textarea.templ
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type TextareaProps struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
Placeholder string
|
||||||
|
Rows int
|
||||||
|
Attrs templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Textarea(props TextareaProps) {
|
||||||
|
<textarea
|
||||||
|
id={ inputID(props.ID, props.Name) }
|
||||||
|
name={ props.Name }
|
||||||
|
placeholder={ props.Placeholder }
|
||||||
|
rows={ textareaRows(props.Rows) }
|
||||||
|
class="ui-textarea"
|
||||||
|
{ props.Attrs... }
|
||||||
|
>{ props.Value }</textarea>
|
||||||
|
}
|
||||||
122
go-backend/internal/web/ui/textarea_templ.go
Normal file
122
go-backend/internal/web/ui/textarea_templ.go
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package ui
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
type TextareaProps struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
Placeholder string
|
||||||
|
Rows int
|
||||||
|
Attrs templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
func Textarea(props TextareaProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<textarea id=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(inputID(props.ID, props.Name))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/textarea.templ`, Line: 14, Col: 36}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" name=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(props.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/textarea.templ`, Line: 15, Col: 19}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" placeholder=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.Placeholder)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/textarea.templ`, Line: 16, Col: 33}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" rows=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(textareaRows(props.Rows))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/textarea.templ`, Line: 17, Col: 33}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"ui-textarea\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, props.Attrs)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, ">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.Value)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/textarea.templ`, Line: 20, Col: 15}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</textarea>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
8
go-backend/internal/web/ui/tokens.go
Normal file
8
go-backend/internal/web/ui/tokens.go
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
const (
|
||||||
|
TokenPrimary = "primary"
|
||||||
|
TokenDanger = "danger"
|
||||||
|
TokenWarning = "warning"
|
||||||
|
TokenInfo = "info"
|
||||||
|
)
|
||||||
313
go-backend/internal/web/ui/ui_test.go
Normal file
313
go-backend/internal/web/ui/ui_test.go
Normal file
|
|
@ -0,0 +1,313 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestButtonRendersPrimaryMediumMarkup(t *testing.T) {
|
||||||
|
component := Button(ButtonProps{
|
||||||
|
Label: "Nouveau projet",
|
||||||
|
Variant: ButtonVariantPrimary,
|
||||||
|
Size: SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`type="button"`,
|
||||||
|
`Nouveau projet`,
|
||||||
|
`ui-button`,
|
||||||
|
`ui-button-primary`,
|
||||||
|
`ui-button-md`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIconButtonRendersBorderlessDestructiveMarkup(t *testing.T) {
|
||||||
|
component := IconButton(IconButtonProps{
|
||||||
|
Label: "Supprimer le projet",
|
||||||
|
Icon: "trash",
|
||||||
|
Variant: IconButtonVariantDangerGhost,
|
||||||
|
Type: "button",
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`type="button"`,
|
||||||
|
`aria-label="Supprimer le projet"`,
|
||||||
|
`borderless-icon-button`,
|
||||||
|
`lucide-trash2`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBadgeRendersSemanticStatusVariant(t *testing.T) {
|
||||||
|
component := Badge(BadgeProps{
|
||||||
|
Label: "En cours",
|
||||||
|
Variant: BadgeVariantWarning,
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`ui-badge`,
|
||||||
|
`ui-badge-warning`,
|
||||||
|
`En cours`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModalRendersShellStructure(t *testing.T) {
|
||||||
|
component := Modal(ModalProps{
|
||||||
|
Title: "Nouveau projet",
|
||||||
|
Body: textComponent("Body copy"),
|
||||||
|
Actions: textComponent("Actions"),
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`ui-modal-backdrop`,
|
||||||
|
`ui-modal-panel`,
|
||||||
|
`Nouveau projet`,
|
||||||
|
`Body copy`,
|
||||||
|
`Actions`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestButtonUsesSharedTokenBackedClasses(t *testing.T) {
|
||||||
|
component := Button(ButtonProps{
|
||||||
|
Label: "Create",
|
||||||
|
Variant: ButtonVariantPrimary,
|
||||||
|
Size: SizeSM,
|
||||||
|
Type: "button",
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`ui-button`,
|
||||||
|
`ui-button-primary`,
|
||||||
|
`ui-button-sm`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSharedSemanticClassesExistInStylesheet(t *testing.T) {
|
||||||
|
cssPath := filepath.Join("..", "..", "..", "static", "styles.css")
|
||||||
|
body, err := os.ReadFile(cssPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read stylesheet: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
css := string(body)
|
||||||
|
for _, want := range []string{
|
||||||
|
`.ui-button-primary`,
|
||||||
|
`.ui-button-sm`,
|
||||||
|
`.ui-badge-warning`,
|
||||||
|
`.ui-modal-panel`,
|
||||||
|
`.borderless-icon-button`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(css, want) {
|
||||||
|
t.Fatalf("expected stylesheet to contain %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestButtonRendersDangerLargeMarkup(t *testing.T) {
|
||||||
|
component := Button(ButtonProps{
|
||||||
|
Label: "Supprimer",
|
||||||
|
Variant: ButtonVariantDanger,
|
||||||
|
Size: SizeLG,
|
||||||
|
Type: "submit",
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`type="submit"`,
|
||||||
|
`ui-button-danger`,
|
||||||
|
`ui-button-lg`,
|
||||||
|
`Supprimer`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputRendersSharedControlMarkup(t *testing.T) {
|
||||||
|
component := Input(InputProps{
|
||||||
|
Name: "name",
|
||||||
|
Value: "My project",
|
||||||
|
Placeholder: "Nom du projet",
|
||||||
|
Type: "text",
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`name="name"`,
|
||||||
|
`value="My project"`,
|
||||||
|
`placeholder="Nom du projet"`,
|
||||||
|
`class="ui-input"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTextareaRendersSharedControlMarkup(t *testing.T) {
|
||||||
|
component := Textarea(TextareaProps{
|
||||||
|
Name: "description",
|
||||||
|
Value: "Longer copy",
|
||||||
|
Placeholder: "Description",
|
||||||
|
Rows: 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`name="description"`,
|
||||||
|
`placeholder="Description"`,
|
||||||
|
`rows="4"`,
|
||||||
|
`class="ui-textarea"`,
|
||||||
|
`Longer copy`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormFieldRendersLabelAndError(t *testing.T) {
|
||||||
|
component := FormField(FormFieldProps{
|
||||||
|
Label: "Nom",
|
||||||
|
For: "tablo-name",
|
||||||
|
Field: Input(InputProps{Name: "name", Type: "text"}),
|
||||||
|
Error: "Le nom est requis",
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`ui-form-field`,
|
||||||
|
`for="tablo-name"`,
|
||||||
|
`Nom`,
|
||||||
|
`ui-form-error`,
|
||||||
|
`Le nom est requis`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCardRendersSharedRegions(t *testing.T) {
|
||||||
|
component := Card(CardProps{
|
||||||
|
Header: textComponent("Header"),
|
||||||
|
Body: textComponent("Body"),
|
||||||
|
Footer: textComponent("Footer"),
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`ui-card`,
|
||||||
|
`ui-card-header`,
|
||||||
|
`ui-card-body`,
|
||||||
|
`ui-card-footer`,
|
||||||
|
`Body`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTableRendersSharedShell(t *testing.T) {
|
||||||
|
component := Table(TableProps{
|
||||||
|
Head: textComponent("<tr><th>Projet</th></tr>"),
|
||||||
|
Body: textComponent("<tr><td>Hello</td></tr>"),
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`ui-table-shell`,
|
||||||
|
`class="ui-table"`,
|
||||||
|
`<thead>`,
|
||||||
|
`<tbody>`,
|
||||||
|
`Projet`,
|
||||||
|
`Hello`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmptyStateRendersTitleDescriptionAndAction(t *testing.T) {
|
||||||
|
component := EmptyState(EmptyStateProps{
|
||||||
|
Title: "Aucun projet",
|
||||||
|
Description: "Créez votre premier projet.",
|
||||||
|
Action: textComponent("Créer"),
|
||||||
|
})
|
||||||
|
|
||||||
|
html := renderToString(t, component)
|
||||||
|
|
||||||
|
for _, want := range []string{
|
||||||
|
`ui-empty-state`,
|
||||||
|
`Aucun projet`,
|
||||||
|
`Créez votre premier projet.`,
|
||||||
|
`Créer`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(html, want) {
|
||||||
|
t.Fatalf("expected %q in %q", want, html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderToString(t *testing.T, component templ.Component) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := component.Render(context.Background(), &buf); err != nil {
|
||||||
|
t.Fatalf("render component: %v", err)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func textComponent(text string) templ.Component {
|
||||||
|
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
|
||||||
|
_, err := w.Write([]byte(text))
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
78
go-backend/internal/web/ui/variants.go
Normal file
78
go-backend/internal/web/ui/variants.go
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type Size string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SizeSM Size = "sm"
|
||||||
|
SizeMD Size = "md"
|
||||||
|
SizeLG Size = "lg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ButtonVariant string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ButtonVariantPrimary ButtonVariant = "primary"
|
||||||
|
ButtonVariantSecondary ButtonVariant = "secondary"
|
||||||
|
ButtonVariantGhost ButtonVariant = "ghost"
|
||||||
|
ButtonVariantDanger ButtonVariant = "danger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IconButtonVariant string
|
||||||
|
|
||||||
|
const (
|
||||||
|
IconButtonVariantNeutral IconButtonVariant = "neutral"
|
||||||
|
IconButtonVariantDangerGhost IconButtonVariant = "danger-ghost"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BadgeVariant string
|
||||||
|
|
||||||
|
const (
|
||||||
|
BadgeVariantInfo BadgeVariant = "info"
|
||||||
|
BadgeVariantWarning BadgeVariant = "warning"
|
||||||
|
BadgeVariantSuccess BadgeVariant = "success"
|
||||||
|
BadgeVariantDanger BadgeVariant = "danger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buttonClass(variant ButtonVariant, size Size) string {
|
||||||
|
return "ui-button ui-button-" + string(normalizedButtonVariant(variant)) + " ui-button-" + string(normalizedSize(size))
|
||||||
|
}
|
||||||
|
|
||||||
|
func iconButtonClass(variant IconButtonVariant) string {
|
||||||
|
switch variant {
|
||||||
|
case IconButtonVariantDangerGhost:
|
||||||
|
return "borderless-icon-button"
|
||||||
|
default:
|
||||||
|
return "ui-icon-button"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func badgeClass(variant BadgeVariant) string {
|
||||||
|
return "ui-badge ui-badge-" + string(normalizedBadgeVariant(variant))
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedSize(size Size) Size {
|
||||||
|
switch size {
|
||||||
|
case SizeSM, SizeLG:
|
||||||
|
return size
|
||||||
|
default:
|
||||||
|
return SizeMD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedButtonVariant(variant ButtonVariant) ButtonVariant {
|
||||||
|
switch variant {
|
||||||
|
case ButtonVariantSecondary, ButtonVariantGhost, ButtonVariantDanger:
|
||||||
|
return variant
|
||||||
|
default:
|
||||||
|
return ButtonVariantPrimary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedBadgeVariant(variant BadgeVariant) BadgeVariant {
|
||||||
|
switch variant {
|
||||||
|
case BadgeVariantWarning, BadgeVariantSuccess, BadgeVariantDanger:
|
||||||
|
return variant
|
||||||
|
default:
|
||||||
|
return BadgeVariantInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
templ DashboardPage(activePath string, content templ.Component) {
|
templ DashboardPage(activePath string, content templ.Component) {
|
||||||
|
@DashboardPageWithMainClass(activePath, "dashboard-main flex-1 overflow-auto", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
templ DashboardPageWithMainClass(activePath string, mainClass string, content templ.Component) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -8,12 +12,13 @@ templ DashboardPage(activePath string, content templ.Component) {
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>XTablo</title>
|
<title>XTablo</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta2/dist/htmx.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta2/dist/htmx.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/tailwind.css"/>
|
||||||
<link rel="stylesheet" href="/static/styles.css"/>
|
<link rel="stylesheet" href="/static/styles.css"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="dashboard-shell">
|
<div class="dashboard-shell">
|
||||||
@DashboardSidebar(activePath)
|
@DashboardSidebar(activePath)
|
||||||
@DashboardMainContent(content)
|
@DashboardMainContentWithClass(mainClass, content)
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -24,13 +29,21 @@ templ DashboardNotFoundPage(displayName string, email string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
templ DashboardMainContent(content templ.Component) {
|
templ DashboardMainContent(content templ.Component) {
|
||||||
<main id="app-main-content" class="dashboard-main">
|
@DashboardMainContentWithClass("dashboard-main flex-1 overflow-auto", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
templ DashboardMainContentWithClass(mainClass string, content templ.Component) {
|
||||||
|
<main id="app-main-content" class={ mainClass }>
|
||||||
@content
|
@content
|
||||||
</main>
|
</main>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ DashboardContentSwap(activePath string, content templ.Component) {
|
templ DashboardContentSwap(activePath string, content templ.Component) {
|
||||||
@DashboardMainContent(content)
|
@DashboardContentSwapWithMainClass(activePath, "dashboard-main flex-1 overflow-auto", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
templ DashboardContentSwapWithMainClass(activePath string, mainClass string, content templ.Component) {
|
||||||
|
@DashboardMainContentWithClass(mainClass, content)
|
||||||
@DashboardNavOOB(activePath)
|
@DashboardNavOOB(activePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,8 +64,9 @@ templ DashboardSidebar(activePath string) {
|
||||||
<div class="sidebar-primary">
|
<div class="sidebar-primary">
|
||||||
<ul class="sidebar-list" role="list">
|
<ul class="sidebar-list" role="list">
|
||||||
for _, item := range sidebarPrimaryNavItems(activePath) {
|
for _, item := range sidebarPrimaryNavItems(activePath) {
|
||||||
<li>@SidebarNavItem(item)
|
<li>
|
||||||
</li>
|
@SidebarNavItem(item)
|
||||||
|
</li>
|
||||||
if item.DividerAfter {
|
if item.DividerAfter {
|
||||||
<li class="sidebar-divider"><hr role="separator"/></li>
|
<li class="sidebar-divider"><hr role="separator"/></li>
|
||||||
}
|
}
|
||||||
|
|
@ -63,15 +77,17 @@ templ DashboardSidebar(activePath string) {
|
||||||
<div class="sidebar-section-label">Projets</div>
|
<div class="sidebar-section-label">Projets</div>
|
||||||
<ul class="sidebar-project-list">
|
<ul class="sidebar-project-list">
|
||||||
for _, item := range sidebarProjectItems() {
|
for _, item := range sidebarProjectItems() {
|
||||||
<li>@SidebarProjectItem(item)
|
<li>
|
||||||
</li>
|
@SidebarProjectItem(item)
|
||||||
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<ul class="sidebar-list sidebar-footer-links" role="list">
|
<ul class="sidebar-list sidebar-footer-links" role="list">
|
||||||
for _, item := range sidebarFooterNavItems(activePath) {
|
for _, item := range sidebarFooterNavItems(activePath) {
|
||||||
<li>@SidebarNavItem(item)
|
<li>
|
||||||
</li>
|
@SidebarNavItem(item)
|
||||||
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -103,11 +119,11 @@ templ SidebarOrganization() {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ OverviewMainContent(displayName string, email string) {
|
templ OverviewMainContent(displayName string, email string, tablos []TabloCardView, showAllProjects bool) {
|
||||||
<div class="overview-page">
|
<div class="overview-page">
|
||||||
@OverviewHeader(displayName)
|
@OverviewHeader(displayName)
|
||||||
@OverviewActions(overviewQuickActions())
|
@OverviewActions(overviewQuickActions())
|
||||||
@OverviewProjects(overviewProjects())
|
@OverviewProjectsSection(tablos, showAllProjects)
|
||||||
@OverviewTasks(overviewTasks())
|
@OverviewTasks(overviewTasks())
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -190,25 +206,37 @@ templ OverviewActions(actions []quickAction) {
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ OverviewProjects(projects []dashboardProject) {
|
templ OverviewProjectsSection(projects []TabloCardView, showAllProjects bool) {
|
||||||
<section class="overview-section">
|
<section id="overview-projects-section" class="overview-section">
|
||||||
<div class="overview-section-heading">
|
<div class="overview-section-heading">
|
||||||
<h3>Mes Projets</h3>
|
<h3>Mes Projets</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-grid">
|
<div class="project-grid">
|
||||||
for _, project := range projects {
|
for _, project := range visibleOverviewProjects(projects, showAllProjects) {
|
||||||
@ProjectCard(project)
|
@TabloGridCard(project)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@SeeMoreProjects(hiddenOverviewProjectsCount(projects, showAllProjects))
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ SeeMoreProjects(hiddenCount int) {
|
||||||
|
if hiddenCount > 0 {
|
||||||
<div class="overview-more-row">
|
<div class="overview-more-row">
|
||||||
<button type="button" class="overview-more-button">
|
<button
|
||||||
Voir 11 de plus
|
type="button"
|
||||||
|
class="overview-more-button"
|
||||||
|
hx-get="/?show_projects=all"
|
||||||
|
hx-target="#overview-projects-section"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
Voir { hiddenCount } de plus
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<path d="m6 9 6 6 6-6"></path>
|
<path d="m6 9 6 6 6-6"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
templ OverviewTasks(tasks []dashboardTask) {
|
templ OverviewTasks(tasks []dashboardTask) {
|
||||||
|
|
@ -243,36 +271,6 @@ templ QuickActionCard(action quickAction) {
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ ProjectCard(project dashboardProject) {
|
|
||||||
<article class="project-card">
|
|
||||||
<div class="project-card-top">
|
|
||||||
<span class={ "project-status " + toneClass(project.StatusTone) }>{ project.Status }</span>
|
|
||||||
<button class="project-delete-button" type="button" aria-label="Supprimer le projet">
|
|
||||||
@ActionIcon("trash")
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="project-card-title-row">
|
|
||||||
<div class={ "project-avatar " + projectAccentClass(project.Accent) }>
|
|
||||||
<span>{ project.Initial }</span>
|
|
||||||
</div>
|
|
||||||
<h4>{ project.Title }</h4>
|
|
||||||
</div>
|
|
||||||
<div class="project-date-row">
|
|
||||||
@ActionIcon("calendar")
|
|
||||||
<span>{ project.Date }</span>
|
|
||||||
</div>
|
|
||||||
<div class="project-progress">
|
|
||||||
<div class="project-progress-label">
|
|
||||||
<span>Progression:</span>
|
|
||||||
<strong>{ progressPercentLabel(project.Progress) }</strong>
|
|
||||||
</div>
|
|
||||||
<div class="project-progress-track">
|
|
||||||
<div class={ "project-progress-bar " + projectAccentClass(project.Accent) } style={ progressInlineStyle(project.Progress) }></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
}
|
|
||||||
|
|
||||||
templ TaskRow(task dashboardTask) {
|
templ TaskRow(task dashboardTask) {
|
||||||
<div class={ taskRowClass(task.Completed) }>
|
<div class={ taskRowClass(task.Completed) }>
|
||||||
<button class={ taskCheckClass(task.Completed) } type="button" aria-label="Marquer la tâche">
|
<button class={ taskCheckClass(task.Completed) } type="button" aria-label="Marquer la tâche">
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -6,8 +6,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
|
tablomodel "xtablo-backend/internal/tablos"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const overviewProjectsPreviewLimit = 6
|
||||||
|
|
||||||
func sidebarNavItemClass(active bool) string {
|
func sidebarNavItemClass(active bool) string {
|
||||||
if active {
|
if active {
|
||||||
return "sidebar-nav-item is-active"
|
return "sidebar-nav-item is-active"
|
||||||
|
|
@ -52,16 +55,6 @@ type sidebarProjectItem struct {
|
||||||
Icon string
|
Icon string
|
||||||
}
|
}
|
||||||
|
|
||||||
type dashboardProject struct {
|
|
||||||
Title string
|
|
||||||
Status string
|
|
||||||
StatusTone string
|
|
||||||
Initial string
|
|
||||||
Accent string
|
|
||||||
Date string
|
|
||||||
Progress int
|
|
||||||
}
|
|
||||||
|
|
||||||
type dashboardTask struct {
|
type dashboardTask struct {
|
||||||
Title string
|
Title string
|
||||||
Project string
|
Project string
|
||||||
|
|
@ -101,17 +94,6 @@ func overviewQuickActions() []quickAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func overviewProjects() []dashboardProject {
|
|
||||||
return []dashboardProject{
|
|
||||||
{Title: "Hello", Status: "En cours", StatusTone: "warning", Initial: "H", Accent: "blue", Date: "Apr 15, 2026", Progress: 50},
|
|
||||||
{Title: "Jean Macon interet pour le produit de ta mere", Status: "En cours", StatusTone: "warning", Initial: "J", Accent: "purple", Date: "Nov 18, 2025", Progress: 50},
|
|
||||||
{Title: "bikip56648 / Arthur Belleville", Status: "En cours", StatusTone: "warning", Initial: "B", Accent: "blue", Date: "Nov 06, 2025", Progress: 50},
|
|
||||||
{Title: "lsdkfjsl / Arthur Belleville", Status: "À faire", StatusTone: "info", Initial: "L", Accent: "blue", Date: "Oct 26, 2025", Progress: 0},
|
|
||||||
{Title: "Hello / Arthur Belleville", Status: "À faire", StatusTone: "info", Initial: "H", Accent: "blue", Date: "Oct 26, 2025", Progress: 0},
|
|
||||||
{Title: "Wes Ocif / Arthur", Status: "À faire", StatusTone: "info", Initial: "W", Accent: "blue", Date: "Oct 20, 2025", Progress: 0},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func overviewTasks() []dashboardTask {
|
func overviewTasks() []dashboardTask {
|
||||||
return []dashboardTask{
|
return []dashboardTask{
|
||||||
{Title: "yo", Project: "Hello", ProjectKey: "H", ProjectHue: "blue", Date: "Apr 16, 2026", Status: "À faire", StatusTone: "info", Completed: false},
|
{Title: "yo", Project: "Hello", ProjectKey: "H", ProjectHue: "blue", Date: "Apr 16, 2026", Status: "À faire", StatusTone: "info", Completed: false},
|
||||||
|
|
@ -124,6 +106,41 @@ func overviewTasks() []dashboardTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func OverviewProjectsFromTablos(tablos []tablomodel.Record) []TabloCardView {
|
||||||
|
projects := make([]TabloCardView, 0, len(tablos))
|
||||||
|
for _, tablo := range tablos {
|
||||||
|
statusLabel, statusTone, progress := overviewProjectStatus(tablo.Status)
|
||||||
|
projects = append(projects, TabloCardView{
|
||||||
|
ID: tablo.ID.String(),
|
||||||
|
Name: tablo.Name,
|
||||||
|
Status: string(tablo.Status),
|
||||||
|
StatusLabel: statusLabel,
|
||||||
|
StatusTone: statusTone,
|
||||||
|
Initial: projectInitial(tablo.Name),
|
||||||
|
Accent: overviewProjectAccent(tablo.Name),
|
||||||
|
CardDateLabel: tablo.CreatedAt.Format("Jan 02, 2006"),
|
||||||
|
Progress: progress,
|
||||||
|
ProgressLabel: progressPercentLabel(progress),
|
||||||
|
DeleteRequestURL: "/tablos/" + tablo.ID.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return projects
|
||||||
|
}
|
||||||
|
|
||||||
|
func visibleOverviewProjects(projects []TabloCardView, showAll bool) []TabloCardView {
|
||||||
|
if showAll || len(projects) <= overviewProjectsPreviewLimit {
|
||||||
|
return projects
|
||||||
|
}
|
||||||
|
return projects[:overviewProjectsPreviewLimit]
|
||||||
|
}
|
||||||
|
|
||||||
|
func hiddenOverviewProjectsCount(projects []TabloCardView, showAll bool) int {
|
||||||
|
if showAll || len(projects) <= overviewProjectsPreviewLimit {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(projects) - overviewProjectsPreviewLimit
|
||||||
|
}
|
||||||
|
|
||||||
func sidebarPrimaryNavItems(activePath string) []sidebarNavItem {
|
func sidebarPrimaryNavItems(activePath string) []sidebarNavItem {
|
||||||
return []sidebarNavItem{
|
return []sidebarNavItem{
|
||||||
{Href: "/", Label: "Aperçu", Icon: "panels", Active: isActivePath(activePath, "/"), DividerAfter: true},
|
{Href: "/", Label: "Aperçu", Icon: "panels", Active: isActivePath(activePath, "/"), DividerAfter: true},
|
||||||
|
|
@ -193,3 +210,33 @@ func progressPercentLabel(progress int) string {
|
||||||
func progressInlineStyle(progress int) templ.SafeCSS {
|
func progressInlineStyle(progress int) templ.SafeCSS {
|
||||||
return templ.SanitizeCSS("width", templ.SafeCSSProperty(progressPercentLabel(progress)))
|
return templ.SanitizeCSS("width", templ.SafeCSSProperty(progressPercentLabel(progress)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func overviewProjectStatus(status tablomodel.Status) (string, string, int) {
|
||||||
|
switch status {
|
||||||
|
case tablomodel.StatusInProgress:
|
||||||
|
return "En cours", "warning", 50
|
||||||
|
case tablomodel.StatusDone:
|
||||||
|
return "Terminé", "success", 100
|
||||||
|
default:
|
||||||
|
return "À faire", "info", 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func overviewProjectAccent(name string) string {
|
||||||
|
switch len(strings.TrimSpace(name)) % 3 {
|
||||||
|
case 1:
|
||||||
|
return "purple"
|
||||||
|
case 2:
|
||||||
|
return "red"
|
||||||
|
default:
|
||||||
|
return "blue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func projectInitial(name string) string {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return "P"
|
||||||
|
}
|
||||||
|
return strings.ToUpper(name[:1])
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ package views
|
||||||
|
|
||||||
templ ActionIcon(kind string) {
|
templ ActionIcon(kind string) {
|
||||||
switch kind {
|
switch kind {
|
||||||
|
case "plus":
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M5 12h14"></path>
|
||||||
|
<path d="M12 5v14"></path>
|
||||||
|
</svg>
|
||||||
case "folder-plus":
|
case "folder-plus":
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<path d="M12 10v6"></path>
|
<path d="M12 10v6"></path>
|
||||||
|
|
@ -36,6 +41,32 @@ templ ActionIcon(kind string) {
|
||||||
<rect width="18" height="18" x="3" y="4" rx="2"></rect>
|
<rect width="18" height="18" x="3" y="4" rx="2"></rect>
|
||||||
<path d="M3 10h18"></path>
|
<path d="M3 10h18"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
case "grid3x3":
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2"></rect>
|
||||||
|
<path d="M3 9h18"></path>
|
||||||
|
<path d="M3 15h18"></path>
|
||||||
|
<path d="M9 3v18"></path>
|
||||||
|
<path d="M15 3v18"></path>
|
||||||
|
</svg>
|
||||||
|
case "list":
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M3 12h.01"></path>
|
||||||
|
<path d="M3 18h.01"></path>
|
||||||
|
<path d="M3 6h.01"></path>
|
||||||
|
<path d="M8 12h13"></path>
|
||||||
|
<path d="M8 18h13"></path>
|
||||||
|
<path d="M8 6h13"></path>
|
||||||
|
</svg>
|
||||||
|
case "search":
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<path d="m21 21-4.3-4.3"></path>
|
||||||
|
</svg>
|
||||||
|
case "filter":
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
|
||||||
|
</svg>
|
||||||
case "check-circle":
|
case "check-circle":
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
|
|
||||||
|
|
@ -30,33 +30,58 @@ func ActionIcon(kind string) templ.Component {
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
switch kind {
|
switch kind {
|
||||||
|
case "plus":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M5 12h14\"></path> <path d=\"M12 5v14\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
case "folder-plus":
|
case "folder-plus":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 10v6\"></path> <path d=\"M9 13h6\"></path> <path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M12 10v6\"></path> <path d=\"M9 13h6\"></path> <path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "circle-plus":
|
case "circle-plus":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle> <path d=\"M8 12h8\"></path> <path d=\"M12 8v8\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle> <path d=\"M8 12h8\"></path> <path d=\"M12 8v8\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "user-plus":
|
case "user-plus":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"></path> <circle cx=\"9\" cy=\"7\" r=\"4\"></circle> <line x1=\"19\" x2=\"19\" y1=\"8\" y2=\"14\"></line> <line x1=\"22\" x2=\"16\" y1=\"11\" y2=\"11\"></line></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"></path> <circle cx=\"9\" cy=\"7\" r=\"4\"></circle> <line x1=\"19\" x2=\"19\" y1=\"8\" y2=\"14\"></line> <line x1=\"22\" x2=\"16\" y1=\"11\" y2=\"11\"></line></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "trash":
|
case "trash":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 6h18\"></path> <path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"></path> <path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"></path> <line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\"></line> <line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\"></line></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 6h18\"></path> <path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"></path> <path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"></path> <line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\"></line> <line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\"></line></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "calendar":
|
case "calendar":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M8 2v4\"></path> <path d=\"M16 2v4\"></path> <rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"></rect> <path d=\"M3 10h18\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M8 2v4\"></path> <path d=\"M16 2v4\"></path> <rect width=\"18\" height=\"18\" x=\"3\" y=\"4\" rx=\"2\"></rect> <path d=\"M3 10h18\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "grid3x3":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"></rect> <path d=\"M3 9h18\"></path> <path d=\"M3 15h18\"></path> <path d=\"M9 3v18\"></path> <path d=\"M15 3v18\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "list":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M3 12h.01\"></path> <path d=\"M3 18h.01\"></path> <path d=\"M3 6h.01\"></path> <path d=\"M8 12h13\"></path> <path d=\"M8 18h13\"></path> <path d=\"M8 6h13\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "search":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"11\" cy=\"11\" r=\"8\"></circle> <path d=\"m21 21-4.3-4.3\"></path></svg>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
case "filter":
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\"></polygon></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "check-circle":
|
case "check-circle":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle> <path d=\"m9 12 2 2 4-4\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle> <path d=\"m9 12 2 2 4-4\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|
@ -93,47 +118,47 @@ func SidebarIcon(kind string) templ.Component {
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
switch kind {
|
switch kind {
|
||||||
case "panels":
|
case "panels":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"></rect> <path d=\"M3 9h18\"></path> <path d=\"M9 21V9\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"></rect> <path d=\"M3 9h18\"></path> <path d=\"M9 21V9\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "tasks":
|
case "tasks":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"3\" y=\"5\" width=\"6\" height=\"6\" rx=\"1\"></rect> <path d=\"m3 17 2 2 4-4\"></path> <path d=\"M13 6h8\"></path> <path d=\"M13 12h8\"></path> <path d=\"M13 18h8\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect x=\"3\" y=\"5\" width=\"6\" height=\"6\" rx=\"1\"></rect> <path d=\"m3 17 2 2 4-4\"></path> <path d=\"M13 6h8\"></path> <path d=\"M13 12h8\"></path> <path d=\"M13 18h8\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "layers":
|
case "layers":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z\"></path> <path d=\"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65\"></path> <path d=\"m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z\"></path> <path d=\"m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65\"></path> <path d=\"m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "planning":
|
case "planning":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"></rect> <path d=\"M8 7v7\"></path> <path d=\"M12 7v4\"></path> <path d=\"M16 7v9\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"></rect> <path d=\"M8 7v7\"></path> <path d=\"M12 7v4\"></path> <path d=\"M16 7v9\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "chat":
|
case "chat":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "files":
|
case "files":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "send":
|
case "send":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\"></path> <path d=\"m21.854 2.147-10.94 10.939\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z\"></path> <path d=\"m21.854 2.147-10.94 10.939\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
case "gem":
|
case "gem":
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M6 3h12l4 6-10 13L2 9Z\"></path> <path d=\"M11 3 8 9l4 13 4-13-3-6\"></path> <path d=\"M2 9h20\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M6 3h12l4 6-10 13L2 9Z\"></path> <path d=\"M11 3 8 9l4 13 4-13-3-6\"></path> <path d=\"M2 9h20\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z\"></path></svg>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z\"></path></svg>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package views
|
package views
|
||||||
|
|
||||||
templ AuthPage(content templ.Component) {
|
templ AuthPage(content templ.Component) {
|
||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="light">
|
<html lang="en" class="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
|
|
@ -15,6 +15,7 @@ templ AuthPage(content templ.Component) {
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default"/>
|
<meta name="apple-mobile-web-app-status-bar-style" content="default"/>
|
||||||
<title>XTablo</title>
|
<title>XTablo</title>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta2/dist/htmx.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta2/dist/htmx.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/tailwind.css"/>
|
||||||
<link rel="stylesheet" href="/static/styles.css"/>
|
<link rel="stylesheet" href="/static/styles.css"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -64,7 +65,3 @@ templ LoginPage() {
|
||||||
templ SignupPage() {
|
templ SignupPage() {
|
||||||
@AuthPage(SignupScreen())
|
@AuthPage(SignupScreen())
|
||||||
}
|
}
|
||||||
|
|
||||||
templ HomePage(displayName string, email string) {
|
|
||||||
@DashboardPage("/", OverviewMainContent(displayName, email))
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ func AuthPage(content templ.Component) templ.Component {
|
||||||
templ_7745c5c3_Var1 = templ.NopComponent
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"light\"><head><meta charset=\"UTF-8\"><link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/pwa-icons/favicon-32x32.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/pwa-icons/favicon-16x16.png\"><link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/pwa-icons/apple-touch-icon-180x180.png\"><link rel=\"manifest\" href=\"/manifest.webmanifest\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\"><meta name=\"theme-color\" content=\"#1e1b2e\"><meta name=\"apple-mobile-web-app-capable\" content=\"yes\"><meta name=\"apple-mobile-web-app-status-bar-style\" content=\"default\"><title>XTablo</title><script src=\"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta2/dist/htmx.min.js\"></script><link rel=\"stylesheet\" href=\"/static/styles.css\"></head><body><div id=\"root\"><section aria-label=\"Notifications alt+T\" tabindex=\"-1\" aria-live=\"polite\" aria-relevant=\"additions text\" aria-atomic=\"false\"></section><div class=\"app-shell\"><div class=\"login-screen\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\" class=\"light\"><head><meta charset=\"UTF-8\"><link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/pwa-icons/favicon-32x32.png\"><link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/pwa-icons/favicon-16x16.png\"><link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"/pwa-icons/apple-touch-icon-180x180.png\"><link rel=\"manifest\" href=\"/manifest.webmanifest\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\"><meta name=\"theme-color\" content=\"#1e1b2e\"><meta name=\"apple-mobile-web-app-capable\" content=\"yes\"><meta name=\"apple-mobile-web-app-status-bar-style\" content=\"default\"><title>XTablo</title><script src=\"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta2/dist/htmx.min.js\"></script><link rel=\"stylesheet\" href=\"/static/tailwind.css\"><link rel=\"stylesheet\" href=\"/static/styles.css\"></head><body><div id=\"root\"><section aria-label=\"Notifications alt+T\" tabindex=\"-1\" aria-live=\"polite\" aria-relevant=\"additions text\" aria-atomic=\"false\"></section><div class=\"app-shell\"><div class=\"login-screen\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|
@ -111,33 +111,4 @@ func SignupPage() templ.Component {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func HomePage(displayName string, email string) templ.Component {
|
|
||||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
|
||||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
|
||||||
return templ_7745c5c3_CtxErr
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
|
||||||
if !templ_7745c5c3_IsBuffer {
|
|
||||||
defer func() {
|
|
||||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err == nil {
|
|
||||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
ctx = templ.InitializeContext(ctx)
|
|
||||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
|
||||||
if templ_7745c5c3_Var4 == nil {
|
|
||||||
templ_7745c5c3_Var4 = templ.NopComponent
|
|
||||||
}
|
|
||||||
ctx = templ.ClearChildren(ctx)
|
|
||||||
templ_7745c5c3_Err = DashboardPage("/", OverviewMainContent(displayName, email)).Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|
|
||||||
289
go-backend/internal/web/views/tablos.templ
Normal file
289
go-backend/internal/web/views/tablos.templ
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import "xtablo-backend/internal/web/ui"
|
||||||
|
|
||||||
|
templ TablosPageContent(vm TablosPageViewModel) {
|
||||||
|
<div class="px-4 pt-8 pb-6">
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8 gap-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Mes Projets</h1>
|
||||||
|
@ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Nouveau projet",
|
||||||
|
Variant: ui.ButtonVariantPrimary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
Icon: "plus",
|
||||||
|
Attrs: templ.Attributes{
|
||||||
|
"hx-get": vm.CreateModalHref(),
|
||||||
|
"hx-target": "#app-main-content",
|
||||||
|
"hx-swap": "outerHTML",
|
||||||
|
"hx-push-url": "true",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-6 mb-6 border-b border-[#EAECF0] dark:border-gray-700">
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(vm.ViewHref("grid")) }
|
||||||
|
hx-get={ vm.ViewHref("grid") }
|
||||||
|
hx-target="#app-main-content"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-push-url="true"
|
||||||
|
class={ gridToggleClass(vm.IsGridView()) }
|
||||||
|
>
|
||||||
|
<span class="w-5 h-5">
|
||||||
|
@ActionIcon("grid3x3")
|
||||||
|
</span>
|
||||||
|
<span class="font-medium">Vue en grille</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(vm.ViewHref("list")) }
|
||||||
|
hx-get={ vm.ViewHref("list") }
|
||||||
|
hx-target="#app-main-content"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-push-url="true"
|
||||||
|
class={ listToggleClass(vm.IsGridView()) }
|
||||||
|
>
|
||||||
|
<span class="w-5 h-5">
|
||||||
|
@ActionIcon("list")
|
||||||
|
</span>
|
||||||
|
<span class="font-medium">Vue en liste</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col md:flex-row gap-4 mb-6">
|
||||||
|
<form
|
||||||
|
class="relative md:w-[350px]"
|
||||||
|
hx-get={ vm.SearchHref() }
|
||||||
|
hx-target="#app-main-content"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-push-url="true"
|
||||||
|
hx-trigger="input changed delay:300ms from:input[name='q']"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="view" value={ vm.View }/>
|
||||||
|
<input type="hidden" name="status" value={ vm.Status }/>
|
||||||
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5 pointer-events-none">
|
||||||
|
@ActionIcon("search")
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
name="q"
|
||||||
|
value={ vm.Query }
|
||||||
|
placeholder="Rechercher..."
|
||||||
|
class="w-full pl-10 pr-4 py-3 border border-[#EAECF0] dark:border-gray-700 rounded-[8px] focus:outline-none focus:ring-2 focus:ring-purple-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
@StatusPill(vm, "all", "Tous")
|
||||||
|
@StatusPill(vm, "todo", "Pas commencé")
|
||||||
|
@StatusPill(vm, "in_progress", "En cours")
|
||||||
|
@StatusPill(vm, "done", "Terminé")
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
if vm.HasTablos() {
|
||||||
|
if vm.IsGridView() {
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6">
|
||||||
|
for _, tablo := range vm.Tablos {
|
||||||
|
@TabloGridCard(tablo)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-xl border border-[#EAECF0] dark:border-gray-700 overflow-x-auto -mx-4 sm:mx-0">
|
||||||
|
@ui.Table(ui.TableProps{
|
||||||
|
Head: TabloListHead(),
|
||||||
|
Body: TabloListBody(vm.Tablos),
|
||||||
|
})
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@ui.EmptyState(ui.EmptyStateProps{
|
||||||
|
Title: "Aucun projet trouvé",
|
||||||
|
Description: "Créez votre premier projet",
|
||||||
|
Icon: ui.UIIcon("grid3x3"),
|
||||||
|
Action: ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Nouveau projet",
|
||||||
|
Variant: ui.ButtonVariantPrimary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
Icon: "plus",
|
||||||
|
Attrs: templ.Attributes{
|
||||||
|
"hx-get": vm.CreateModalHref(),
|
||||||
|
"hx-target": "#app-main-content",
|
||||||
|
"hx-swap": "outerHTML",
|
||||||
|
"hx-push-url": "true",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if vm.ModalOpen {
|
||||||
|
@CreateTabloModal(vm)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ StatusPill(vm TablosPageViewModel, status string, label string) {
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(vm.StatusHref(status)) }
|
||||||
|
hx-get={ vm.StatusHref(status) }
|
||||||
|
hx-target="#app-main-content"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-push-url="true"
|
||||||
|
class={ statusPillClass(vm.Status == status) }
|
||||||
|
>
|
||||||
|
if status == "all" {
|
||||||
|
<span class="w-4 h-4">
|
||||||
|
@ActionIcon("filter")
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
{ label }
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ BorderlessDeleteButton(deleteRequestURL string) {
|
||||||
|
@ui.IconButton(ui.IconButtonProps{
|
||||||
|
Label: "Supprimer le projet",
|
||||||
|
Icon: "trash",
|
||||||
|
Variant: ui.IconButtonVariantDangerGhost,
|
||||||
|
Type: "button",
|
||||||
|
Attrs: templ.Attributes{
|
||||||
|
"hx-delete": deleteRequestURL,
|
||||||
|
"hx-target": "#app-main-content",
|
||||||
|
"hx-swap": "outerHTML",
|
||||||
|
"hx-confirm": "Supprimer ce projet ?",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TabloGridCard(tablo TabloCardView) {
|
||||||
|
<article class="project-card">
|
||||||
|
<div class="project-card-top">
|
||||||
|
@ui.Badge(ui.BadgeProps{
|
||||||
|
Label: tablo.StatusLabel,
|
||||||
|
Variant: badgeVariantForTone(tablo.StatusTone),
|
||||||
|
})
|
||||||
|
@BorderlessDeleteButton(tablo.DeleteRequestURL)
|
||||||
|
</div>
|
||||||
|
<div class="project-card-title-row">
|
||||||
|
<div class={ "project-avatar " + projectAccentClass(tablo.Accent) }>
|
||||||
|
<span>{ tablo.Initial }</span>
|
||||||
|
</div>
|
||||||
|
<h4>{ tablo.Name }</h4>
|
||||||
|
</div>
|
||||||
|
<div class="project-date-row">
|
||||||
|
@ActionIcon("calendar")
|
||||||
|
<span>{ tablo.CardDateLabel }</span>
|
||||||
|
</div>
|
||||||
|
<div class="project-progress">
|
||||||
|
<div class="project-progress-label">
|
||||||
|
<span>Progression:</span>
|
||||||
|
<strong>{ tablo.ProgressLabel }</strong>
|
||||||
|
</div>
|
||||||
|
<div class="project-progress-track">
|
||||||
|
<div class={ "project-progress-bar " + projectAccentClass(tablo.Accent) } style={ progressInlineStyle(tablo.Progress) }></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TabloListRow(tablo TabloCardView) {
|
||||||
|
<tr class="border-t border-[#EAECF0] dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer">
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class={ "w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden [&>svg]:w-4 [&>svg]:h-4 " + tablo.IconBgClass + " " + tablo.IconFgClass }>
|
||||||
|
@ActionIcon(tablo.IconKind)
|
||||||
|
</div>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-gray-100 truncate">{ tablo.Name }</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
@ui.Badge(ui.BadgeProps{
|
||||||
|
Label: tablo.StatusLabel,
|
||||||
|
Variant: badgeVariantForTone(tablo.StatusTone),
|
||||||
|
})
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<div class="flex items-center gap-1.5 [&>svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0">
|
||||||
|
@ActionIcon("calendar")
|
||||||
|
{ tablo.CreatedAtLabel }
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 min-w-[80px]">
|
||||||
|
<div class="bg-green-500 h-2 rounded-full transition-all" style={ progressInlineStyle(tablo.Progress) }></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100 w-8 text-right">{ tablo.ProgressLabel }</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 text-right">
|
||||||
|
@BorderlessDeleteButton(tablo.DeleteRequestURL)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ CreateTabloModal(vm TablosPageViewModel) {
|
||||||
|
@ui.Modal(ui.ModalProps{
|
||||||
|
Title: "Nouveau projet",
|
||||||
|
Body: CreateTabloModalBody(vm),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TabloListHead() {
|
||||||
|
<tr class="bg-gray-50 dark:bg-gray-800/80 border-b border-[#EAECF0] dark:border-gray-700">
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">Projet</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">Statut</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">Créé le</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">Progression</th>
|
||||||
|
<th class="px-6 py-3 w-12"></th>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ TabloListBody(tablos []TabloCardView) {
|
||||||
|
for _, tablo := range tablos {
|
||||||
|
@TabloListRow(tablo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ CreateTabloModalBody(vm TablosPageViewModel) {
|
||||||
|
<form
|
||||||
|
hx-post="/tablos"
|
||||||
|
hx-target="#app-main-content"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="flex flex-col gap-4"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="view" value={ vm.View }/>
|
||||||
|
<input type="hidden" name="status" value={ vm.Status }/>
|
||||||
|
<input type="hidden" name="q" value={ vm.Query }/>
|
||||||
|
<input type="hidden" name="modal" value="create"/>
|
||||||
|
if vm.ErrorMessage != "" {
|
||||||
|
<div class="mb-1 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{ vm.ErrorMessage }</div>
|
||||||
|
}
|
||||||
|
@ui.FormField(ui.FormFieldProps{
|
||||||
|
Label: "Nom du projet",
|
||||||
|
For: "tablo-name",
|
||||||
|
Field: ui.Input(ui.InputProps{
|
||||||
|
ID: "tablo-name",
|
||||||
|
Name: "name",
|
||||||
|
Value: vm.FormName,
|
||||||
|
Placeholder: "Nom du projet",
|
||||||
|
Type: "text",
|
||||||
|
}),
|
||||||
|
Error: vm.ErrorMessage,
|
||||||
|
})
|
||||||
|
<div class="flex items-center justify-end gap-3">
|
||||||
|
<a
|
||||||
|
href={ templ.SafeURL(vm.CloseModalHref()) }
|
||||||
|
hx-get={ vm.CloseModalHref() }
|
||||||
|
hx-target="#app-main-content"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-push-url="true"
|
||||||
|
class="ui-button ui-button-secondary ui-button-md"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</a>
|
||||||
|
@ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Créer le projet",
|
||||||
|
Variant: ui.ButtonVariantPrimary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "submit",
|
||||||
|
})
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
992
go-backend/internal/web/views/tablos_templ.go
Normal file
992
go-backend/internal/web/views/tablos_templ.go
Normal file
|
|
@ -0,0 +1,992 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.1001
|
||||||
|
package views
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import "xtablo-backend/internal/web/ui"
|
||||||
|
|
||||||
|
func TablosPageContent(vm TablosPageViewModel) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"px-4 pt-8 pb-6\"><div class=\"flex flex-col md:flex-row md:items-center md:justify-between mb-8 gap-4\"><h1 class=\"text-2xl font-bold text-gray-900 dark:text-gray-100\">Mes Projets</h1>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Nouveau projet",
|
||||||
|
Variant: ui.ButtonVariantPrimary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
Icon: "plus",
|
||||||
|
Attrs: templ.Attributes{
|
||||||
|
"hx-get": vm.CreateModalHref(),
|
||||||
|
"hx-target": "#app-main-content",
|
||||||
|
"hx-swap": "outerHTML",
|
||||||
|
"hx-push-url": "true",
|
||||||
|
},
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div class=\"flex items-center gap-6 mb-6 border-b border-[#EAECF0] dark:border-gray-700\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 = []any{gridToggleClass(vm.IsGridView())}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(vm.ViewHref("grid")))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 25, Col: 45}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(vm.ViewHref("grid"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 26, Col: 32}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"><span class=\"w-5 h-5\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ActionIcon("grid3x3").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span> <span class=\"font-medium\">Vue en grille</span></a> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 = []any{listToggleClass(vm.IsGridView())}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(vm.ViewHref("list")))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 38, Col: 45}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(vm.ViewHref("list"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 39, Col: 32}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var6).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\"><span class=\"w-5 h-5\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ActionIcon("list").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span> <span class=\"font-medium\">Vue en liste</span></a></div><div class=\"flex flex-col md:flex-row gap-4 mb-6\"><form class=\"relative md:w-[350px]\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(vm.SearchHref())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 54, Col: 28}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" hx-trigger=\"input changed delay:300ms from:input[name='q']\"><input type=\"hidden\" name=\"view\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(vm.View)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 60, Col: 52}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"> <input type=\"hidden\" name=\"status\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var12 string
|
||||||
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(vm.Status)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 61, Col: 56}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"> <span class=\"absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5 pointer-events-none\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ActionIcon("search").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span> <input name=\"q\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var13 string
|
||||||
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(vm.Query)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 67, Col: 21}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" placeholder=\"Rechercher...\" class=\"w-full pl-10 pr-4 py-3 border border-[#EAECF0] dark:border-gray-700 rounded-[8px] focus:outline-none focus:ring-2 focus:ring-purple-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400\" type=\"text\"></form><div class=\"flex items-center gap-2 flex-wrap\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = StatusPill(vm, "all", "Tous").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = StatusPill(vm, "todo", "Pas commencé").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = StatusPill(vm, "in_progress", "En cours").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = StatusPill(vm, "done", "Terminé").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if vm.HasTablos() {
|
||||||
|
if vm.IsGridView() {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, tablo := range vm.Tablos {
|
||||||
|
templ_7745c5c3_Err = TabloGridCard(tablo).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"bg-white dark:bg-gray-800 rounded-xl border border-[#EAECF0] dark:border-gray-700 overflow-x-auto -mx-4 sm:mx-0\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ui.Table(ui.TableProps{
|
||||||
|
Head: TabloListHead(),
|
||||||
|
Body: TabloListBody(vm.Tablos),
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = ui.EmptyState(ui.EmptyStateProps{
|
||||||
|
Title: "Aucun projet trouvé",
|
||||||
|
Description: "Créez votre premier projet",
|
||||||
|
Icon: ui.UIIcon("grid3x3"),
|
||||||
|
Action: ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Nouveau projet",
|
||||||
|
Variant: ui.ButtonVariantPrimary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
Icon: "plus",
|
||||||
|
Attrs: templ.Attributes{
|
||||||
|
"hx-get": vm.CreateModalHref(),
|
||||||
|
"hx-target": "#app-main-content",
|
||||||
|
"hx-swap": "outerHTML",
|
||||||
|
"hx-push-url": "true",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if vm.ModalOpen {
|
||||||
|
templ_7745c5c3_Err = CreateTabloModal(vm).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func StatusPill(vm TablosPageViewModel, status string, label string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var14 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var14 == nil {
|
||||||
|
templ_7745c5c3_Var14 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
var templ_7745c5c3_Var15 = []any{statusPillClass(vm.Status == status)}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var16 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(vm.StatusHref(status)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 123, Col: 45}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 string
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vm.StatusHref(status))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 124, Col: 32}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var18 string
|
||||||
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if status == "all" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<span class=\"w-4 h-4\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ActionIcon("filter").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</span> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var19 string
|
||||||
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 135, Col: 9}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BorderlessDeleteButton(deleteRequestURL string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var20 == nil {
|
||||||
|
templ_7745c5c3_Var20 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = ui.IconButton(ui.IconButtonProps{
|
||||||
|
Label: "Supprimer le projet",
|
||||||
|
Icon: "trash",
|
||||||
|
Variant: ui.IconButtonVariantDangerGhost,
|
||||||
|
Type: "button",
|
||||||
|
Attrs: templ.Attributes{
|
||||||
|
"hx-delete": deleteRequestURL,
|
||||||
|
"hx-target": "#app-main-content",
|
||||||
|
"hx-swap": "outerHTML",
|
||||||
|
"hx-confirm": "Supprimer ce projet ?",
|
||||||
|
},
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TabloGridCard(tablo TabloCardView) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var21 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var21 == nil {
|
||||||
|
templ_7745c5c3_Var21 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<article class=\"project-card\"><div class=\"project-card-top\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ui.Badge(ui.BadgeProps{
|
||||||
|
Label: tablo.StatusLabel,
|
||||||
|
Variant: badgeVariantForTone(tablo.StatusTone),
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = BorderlessDeleteButton(tablo.DeleteRequestURL).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div><div class=\"project-card-title-row\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var22 = []any{"project-avatar " + projectAccentClass(tablo.Accent)}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var23 string
|
||||||
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var22).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\"><span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var24 string
|
||||||
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Initial)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 165, Col: 25}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span></div><h4>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var25 string
|
||||||
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 167, Col: 19}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</h4></div><div class=\"project-date-row\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ActionIcon("calendar").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<span>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var26 string
|
||||||
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CardDateLabel)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 171, Col: 30}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</span></div><div class=\"project-progress\"><div class=\"project-progress-label\"><span>Progression:</span> <strong>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var27 string
|
||||||
|
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 176, Col: 33}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</strong></div><div class=\"project-progress-track\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var28 = []any{"project-progress-bar " + projectAccentClass(tablo.Accent)}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var28...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<div class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var29 string
|
||||||
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var28).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" style=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var30 string
|
||||||
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(progressInlineStyle(tablo.Progress))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 179, Col: 121}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\"></div></div></div></article>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TabloListRow(tablo TabloCardView) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var31 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var31 == nil {
|
||||||
|
templ_7745c5c3_Var31 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<tr class=\"border-t border-[#EAECF0] dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer\"><td class=\"px-6 py-4\"><div class=\"flex items-center gap-3\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var32 = []any{"w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden [&>svg]:w-4 [&>svg]:h-4 " + tablo.IconBgClass + " " + tablo.IconFgClass}
|
||||||
|
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var32...)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var33 string
|
||||||
|
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var32).String())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 1, Col: 0}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ActionIcon(tablo.IconKind).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</div><span class=\"font-medium text-gray-900 dark:text-gray-100 truncate\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var34 string
|
||||||
|
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 192, Col: 84}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</span></div></td><td class=\"px-6 py-4 whitespace-nowrap\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ui.Badge(ui.BadgeProps{
|
||||||
|
Label: tablo.StatusLabel,
|
||||||
|
Variant: badgeVariantForTone(tablo.StatusTone),
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</td><td class=\"px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400\"><div class=\"flex items-center gap-1.5 [&>svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ActionIcon("calendar").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var35 string
|
||||||
|
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CreatedAtLabel)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 204, Col: 26}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</div></td><td class=\"px-6 py-4\"><div class=\"flex items-center gap-3\"><div class=\"flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 min-w-[80px]\"><div class=\"bg-green-500 h-2 rounded-full transition-all\" style=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var36 string
|
||||||
|
templ_7745c5c3_Var36, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(progressInlineStyle(tablo.Progress))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 210, Col: 106}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\"></div></div><span class=\"text-sm font-semibold text-gray-900 dark:text-gray-100 w-8 text-right\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var37 string
|
||||||
|
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 212, Col: 109}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span></div></td><td class=\"px-6 py-4 text-right\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = BorderlessDeleteButton(tablo.DeleteRequestURL).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</td></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateTabloModal(vm TablosPageViewModel) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var38 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var38 == nil {
|
||||||
|
templ_7745c5c3_Var38 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = ui.Modal(ui.ModalProps{
|
||||||
|
Title: "Nouveau projet",
|
||||||
|
Body: CreateTabloModalBody(vm),
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TabloListHead() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var39 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var39 == nil {
|
||||||
|
templ_7745c5c3_Var39 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<tr class=\"bg-gray-50 dark:bg-gray-800/80 border-b border-[#EAECF0] dark:border-gray-700\"><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Projet</th><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Statut</th><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Créé le</th><th class=\"px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider\">Progression</th><th class=\"px-6 py-3 w-12\"></th></tr>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TabloListBody(tablos []TabloCardView) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var40 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var40 == nil {
|
||||||
|
templ_7745c5c3_Var40 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
for _, tablo := range tablos {
|
||||||
|
templ_7745c5c3_Err = TabloListRow(tablo).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var41 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var41 == nil {
|
||||||
|
templ_7745c5c3_Var41 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<form hx-post=\"/tablos\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" class=\"flex flex-col gap-4\"><input type=\"hidden\" name=\"view\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var42 string
|
||||||
|
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(vm.View)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 251, Col: 50}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\"> <input type=\"hidden\" name=\"status\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var43 string
|
||||||
|
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(vm.Status)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 252, Col: 54}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\"> <input type=\"hidden\" name=\"q\" value=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var44 string
|
||||||
|
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(vm.Query)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 253, Col: 48}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\"> <input type=\"hidden\" name=\"modal\" value=\"create\"> ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if vm.ErrorMessage != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<div class=\"mb-1 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var45 string
|
||||||
|
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(vm.ErrorMessage)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 256, Col: 112}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ui.FormField(ui.FormFieldProps{
|
||||||
|
Label: "Nom du projet",
|
||||||
|
For: "tablo-name",
|
||||||
|
Field: ui.Input(ui.InputProps{
|
||||||
|
ID: "tablo-name",
|
||||||
|
Name: "name",
|
||||||
|
Value: vm.FormName,
|
||||||
|
Placeholder: "Nom du projet",
|
||||||
|
Type: "text",
|
||||||
|
}),
|
||||||
|
Error: vm.ErrorMessage,
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<div class=\"flex items-center justify-end gap-3\"><a href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var46 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(vm.CloseModalHref()))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 272, Col: 45}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" hx-get=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var47 string
|
||||||
|
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(vm.CloseModalHref())
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 273, Col: 32}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"ui-button ui-button-secondary ui-button-md\">Annuler</a>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Créer le projet",
|
||||||
|
Variant: ui.ButtonVariantPrimary,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "submit",
|
||||||
|
}).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</div></form>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
161
go-backend/internal/web/views/tablos_view.go
Normal file
161
go-backend/internal/web/views/tablos_view.go
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
package views
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"xtablo-backend/internal/web/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TabloCardView struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Status string
|
||||||
|
StatusLabel string
|
||||||
|
StatusClass string
|
||||||
|
StatusTone string
|
||||||
|
Progress int
|
||||||
|
CreatedAtLabel string
|
||||||
|
CardDateLabel string
|
||||||
|
ProgressLabel string
|
||||||
|
DeleteURL string
|
||||||
|
DeleteRequestURL string
|
||||||
|
IconKind string
|
||||||
|
IconBgClass string
|
||||||
|
IconFgClass string
|
||||||
|
Accent string
|
||||||
|
Initial string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TablosPageViewModel struct {
|
||||||
|
DisplayName string
|
||||||
|
View string
|
||||||
|
Query string
|
||||||
|
Status string
|
||||||
|
ModalOpen bool
|
||||||
|
FormName string
|
||||||
|
ErrorMessage string
|
||||||
|
Tablos []TabloCardView
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTablosPageViewModel(displayName string, view string, query string, status string, modalOpen bool, formName string, errorMessage string, tablos []TabloCardView) TablosPageViewModel {
|
||||||
|
return TablosPageViewModel{
|
||||||
|
DisplayName: displayName,
|
||||||
|
View: normalizedView(view),
|
||||||
|
Query: strings.TrimSpace(query),
|
||||||
|
Status: normalizedStatus(status),
|
||||||
|
ModalOpen: modalOpen,
|
||||||
|
FormName: strings.TrimSpace(formName),
|
||||||
|
ErrorMessage: strings.TrimSpace(errorMessage),
|
||||||
|
Tablos: tablos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm TablosPageViewModel) IsGridView() bool {
|
||||||
|
return vm.View != "list"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm TablosPageViewModel) HasTablos() bool {
|
||||||
|
return len(vm.Tablos) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm TablosPageViewModel) StatusHref(status string) string {
|
||||||
|
values := vm.baseValues()
|
||||||
|
values.Set("status", normalizedStatus(status))
|
||||||
|
return "/tablos?" + values.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm TablosPageViewModel) ViewHref(view string) string {
|
||||||
|
values := vm.baseValues()
|
||||||
|
values.Set("view", normalizedView(view))
|
||||||
|
return "/tablos?" + values.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm TablosPageViewModel) SearchHref() string {
|
||||||
|
return "/tablos"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm TablosPageViewModel) HiddenStateFields() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
"view": vm.View,
|
||||||
|
"status": vm.Status,
|
||||||
|
"q": vm.Query,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm TablosPageViewModel) SearchValues() string {
|
||||||
|
return fmt.Sprintf("view=%s&status=%s", vm.View, vm.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm TablosPageViewModel) CreateModalHref() string {
|
||||||
|
values := vm.baseValues()
|
||||||
|
values.Set("modal", "create")
|
||||||
|
return "/tablos?" + values.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm TablosPageViewModel) CloseModalHref() string {
|
||||||
|
values := vm.baseValues()
|
||||||
|
return "/tablos?" + values.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm TablosPageViewModel) HasSearch() bool {
|
||||||
|
return vm.Query != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedView(view string) string {
|
||||||
|
if view == "list" {
|
||||||
|
return "list"
|
||||||
|
}
|
||||||
|
return "grid"
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizedStatus(status string) string {
|
||||||
|
switch status {
|
||||||
|
case "todo", "in_progress", "done":
|
||||||
|
return status
|
||||||
|
default:
|
||||||
|
return "all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vm TablosPageViewModel) baseValues() url.Values {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("view", vm.View)
|
||||||
|
values.Set("status", vm.Status)
|
||||||
|
if vm.Query != "" {
|
||||||
|
values.Set("q", vm.Query)
|
||||||
|
}
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func gridToggleClass(active bool) string {
|
||||||
|
if active {
|
||||||
|
return "flex items-center gap-2 pb-3 border-b-2 transition-colors border-purple-600 text-purple-600 dark:border-purple-400 dark:text-purple-400 font-semibold"
|
||||||
|
}
|
||||||
|
return "flex items-center gap-2 pb-3 border-b-2 transition-colors border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
||||||
|
}
|
||||||
|
|
||||||
|
func listToggleClass(gridActive bool) string {
|
||||||
|
return gridToggleClass(!gridActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusPillClass(active bool) string {
|
||||||
|
if active {
|
||||||
|
return "flex items-center gap-1.5 px-4 py-2.5 border rounded-[8px] font-medium text-sm transition-colors border-purple-600 bg-purple-50 dark:bg-purple-950/30 text-purple-600 dark:text-purple-400"
|
||||||
|
}
|
||||||
|
return "flex items-center gap-1.5 px-4 py-2.5 border rounded-[8px] font-medium text-sm transition-colors border-[#EAECF0] dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300"
|
||||||
|
}
|
||||||
|
|
||||||
|
func badgeVariantForTone(tone string) ui.BadgeVariant {
|
||||||
|
switch tone {
|
||||||
|
case "warning":
|
||||||
|
return ui.BadgeVariantWarning
|
||||||
|
case "success":
|
||||||
|
return ui.BadgeVariantSuccess
|
||||||
|
case "danger":
|
||||||
|
return ui.BadgeVariantDanger
|
||||||
|
default:
|
||||||
|
return ui.BadgeVariantInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ set shell := ["bash", "-cu"]
|
||||||
|
|
||||||
database_url := "postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable"
|
database_url := "postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable"
|
||||||
compose_config_dir := ".podman-compose"
|
compose_config_dir := ".podman-compose"
|
||||||
|
tailwind_input := "tailwind.input.css"
|
||||||
|
tailwind_output := "static/tailwind.css"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@just --list
|
@just --list
|
||||||
|
|
@ -40,9 +42,17 @@ db-logs: machine-up compose-config
|
||||||
DOCKER_CONFIG="$PWD/{{compose_config_dir}}" podman compose logs -f postgres
|
DOCKER_CONFIG="$PWD/{{compose_config_dir}}" podman compose logs -f postgres
|
||||||
|
|
||||||
generate:
|
generate:
|
||||||
|
pnpm exec tailwindcss -i {{tailwind_input}} -o {{tailwind_output}} --cwd .
|
||||||
go run github.com/a-h/templ/cmd/templ@latest generate
|
go run github.com/a-h/templ/cmd/templ@latest generate
|
||||||
go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate
|
go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate
|
||||||
|
|
||||||
|
design-system:
|
||||||
|
just generate
|
||||||
|
go run ./cmd/designsystem
|
||||||
|
|
||||||
|
css-watch:
|
||||||
|
pnpm exec tailwindcss -i {{tailwind_input}} -o {{tailwind_output}} --cwd . --watch
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
gofmt -w .
|
gofmt -w .
|
||||||
|
|
||||||
|
|
@ -50,12 +60,15 @@ test:
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
pnpm exec tailwindcss -i {{tailwind_input}} -o {{tailwind_output}} --cwd .
|
||||||
go build ./...
|
go build ./...
|
||||||
|
|
||||||
check: generate test build
|
check: generate test build
|
||||||
|
|
||||||
dev: db-up
|
dev: db-up
|
||||||
|
pnpm exec tailwindcss -i {{tailwind_input}} -o {{tailwind_output}} --cwd .
|
||||||
DATABASE_URL='{{database_url}}' air -c .air.toml
|
DATABASE_URL='{{database_url}}' air -c .air.toml
|
||||||
|
|
||||||
run: db-up
|
run: db-up
|
||||||
|
pnpm exec tailwindcss -i {{tailwind_input}} -o {{tailwind_output}} --cwd .
|
||||||
DATABASE_URL='{{database_url}}' go run .
|
DATABASE_URL='{{database_url}}' go run .
|
||||||
|
|
|
||||||
10
go-backend/package.json
Normal file
10
go-backend/package.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"name": "@xtablo/go-backend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"packageManager": "pnpm@10.19.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/cli": "4.1.15",
|
||||||
|
"tailwindcss": "4.1.15"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,8 @@ func newRouterWithHandler(authHandler *handlers.AuthHandler) http.Handler {
|
||||||
mux.Get("/chat", authHandler.GetChatPage())
|
mux.Get("/chat", authHandler.GetChatPage())
|
||||||
mux.Get("/files", authHandler.GetFilesPage())
|
mux.Get("/files", authHandler.GetFilesPage())
|
||||||
mux.Get("/feedback", authHandler.GetFeedbackPage())
|
mux.Get("/feedback", authHandler.GetFeedbackPage())
|
||||||
|
mux.Post("/tablos", authHandler.PostTablos())
|
||||||
|
mux.Delete("/tablos/{tabloID}", authHandler.DeleteTablo())
|
||||||
mux.Get("/login", authHandler.GetLoginPage())
|
mux.Get("/login", authHandler.GetLoginPage())
|
||||||
mux.Get("/signup", authHandler.GetSignupPage())
|
mux.Get("/signup", authHandler.GetSignupPage())
|
||||||
mux.Post("/login", authHandler.PostLogin())
|
mux.Post("/login", authHandler.PostLogin())
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -56,6 +58,7 @@ func TestLoginPageRenders(t *testing.T) {
|
||||||
"Se connecter à Xtablo",
|
"Se connecter à Xtablo",
|
||||||
`hx-post="/login"`,
|
`hx-post="/login"`,
|
||||||
"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta2/dist/htmx.min.js",
|
"https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta2/dist/htmx.min.js",
|
||||||
|
`href="/static/tailwind.css"`,
|
||||||
`href="/pwa-icons/favicon-32x32.png"`,
|
`href="/pwa-icons/favicon-32x32.png"`,
|
||||||
`href="/pwa-icons/favicon-16x16.png"`,
|
`href="/pwa-icons/favicon-16x16.png"`,
|
||||||
`href="/pwa-icons/apple-touch-icon-180x180.png"`,
|
`href="/pwa-icons/apple-touch-icon-180x180.png"`,
|
||||||
|
|
@ -72,6 +75,30 @@ func TestLoginPageRenders(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTailwindStylesheetIsServed(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/static/tailwind.css", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router := newTestRouter()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
".text-2xl",
|
||||||
|
".grid-cols-1",
|
||||||
|
".whitespace-nowrap",
|
||||||
|
".justify-end",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected tailwind.css to contain %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSignupPageRenders(t *testing.T) {
|
func TestSignupPageRenders(t *testing.T) {
|
||||||
req := httptest.NewRequest(http.MethodGet, "/signup", nil)
|
req := httptest.NewRequest(http.MethodGet, "/signup", nil)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
|
|
@ -239,6 +266,175 @@ func TestTasksPageRendersFullDashboardPage(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHomePageProjectsUseSharedTabloGridCardWithDeleteAction(t *testing.T) {
|
||||||
|
repo := handlers.NewInMemoryAuthRepository()
|
||||||
|
authUser, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected demo user, got error: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := repo.CreateTablo(context.Background(), handlers.CreateTabloInput{
|
||||||
|
OwnerID: authUser.ID,
|
||||||
|
Name: "Hello",
|
||||||
|
Status: handlers.TabloStatusInProgress,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("expected tablo creation to succeed, got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("email", "demo@xtablo.com")
|
||||||
|
form.Set("password", "xtablo-demo")
|
||||||
|
|
||||||
|
loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
||||||
|
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
loginRec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router := newRouterWithHandler(handlers.NewAuthHandler(repo))
|
||||||
|
router.ServeHTTP(loginRec, loginReq)
|
||||||
|
|
||||||
|
sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
|
||||||
|
if sessionCookie == nil {
|
||||||
|
t.Fatalf("expected session cookie to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
`class="project-card"`,
|
||||||
|
`class="project-date-row"`,
|
||||||
|
`hx-delete="/tablos/`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected home page to contain %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHomePageProjectsCollapseAfterSixByDefault(t *testing.T) {
|
||||||
|
repo := handlers.NewInMemoryAuthRepository()
|
||||||
|
authUser, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected demo user, got error: %v", err)
|
||||||
|
}
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
if _, err := repo.CreateTablo(context.Background(), handlers.CreateTabloInput{
|
||||||
|
OwnerID: authUser.ID,
|
||||||
|
Name: "Project " + string(rune('A'+i)),
|
||||||
|
Status: handlers.TabloStatusTodo,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("expected tablo creation to succeed, got error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("email", "demo@xtablo.com")
|
||||||
|
form.Set("password", "xtablo-demo")
|
||||||
|
|
||||||
|
loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
||||||
|
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
loginRec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router := newRouterWithHandler(handlers.NewAuthHandler(repo))
|
||||||
|
router.ServeHTTP(loginRec, loginReq)
|
||||||
|
|
||||||
|
sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
|
||||||
|
if sessionCookie == nil {
|
||||||
|
t.Fatalf("expected session cookie to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
if count := strings.Count(body, `class="project-card"`); count != 6 {
|
||||||
|
t.Fatalf("expected 6 visible project cards by default, got %d", count)
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
`id="overview-projects-section"`,
|
||||||
|
`Voir 2 de plus`,
|
||||||
|
`hx-get="/?show_projects=all"`,
|
||||||
|
`hx-target="#overview-projects-section"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected home page to contain %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHomePageProjectsExpandViaHTMXSectionSwap(t *testing.T) {
|
||||||
|
repo := handlers.NewInMemoryAuthRepository()
|
||||||
|
authUser, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected demo user, got error: %v", err)
|
||||||
|
}
|
||||||
|
for i := 0; i < 8; i++ {
|
||||||
|
if _, err := repo.CreateTablo(context.Background(), handlers.CreateTabloInput{
|
||||||
|
OwnerID: authUser.ID,
|
||||||
|
Name: "Project " + string(rune('A'+i)),
|
||||||
|
Status: handlers.TabloStatusTodo,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("expected tablo creation to succeed, got error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("email", "demo@xtablo.com")
|
||||||
|
form.Set("password", "xtablo-demo")
|
||||||
|
|
||||||
|
loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
||||||
|
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
loginRec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router := newRouterWithHandler(handlers.NewAuthHandler(repo))
|
||||||
|
router.ServeHTTP(loginRec, loginReq)
|
||||||
|
|
||||||
|
sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
|
||||||
|
if sessionCookie == nil {
|
||||||
|
t.Fatalf("expected session cookie to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/?show_projects=all", nil)
|
||||||
|
req.Header.Set("HX-Request", "true")
|
||||||
|
req.Header.Set("HX-Target", "section#overview-projects-section")
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
if count := strings.Count(body, `class="project-card"`); count != 8 {
|
||||||
|
t.Fatalf("expected 8 visible project cards after expansion, got %d", count)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, `id="overview-projects-section"`) {
|
||||||
|
t.Fatalf("expected section swap root in response, got %q", body)
|
||||||
|
}
|
||||||
|
if strings.Contains(body, `id="app-main-content"`) {
|
||||||
|
t.Fatalf("expected projects section response, got main content swap %q", body)
|
||||||
|
}
|
||||||
|
if strings.Contains(body, `Voir 2 de plus`) {
|
||||||
|
t.Fatalf("expected see more button to disappear after expansion, got %q", body)
|
||||||
|
}
|
||||||
|
if strings.Contains(body, `class="sidebar-nav-shell"`) {
|
||||||
|
t.Fatalf("expected projects section swap to avoid rerendering the full dashboard shell")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTasksPageReturnsHTMXMainContentSwap(t *testing.T) {
|
func TestTasksPageReturnsHTMXMainContentSwap(t *testing.T) {
|
||||||
form := url.Values{}
|
form := url.Values{}
|
||||||
form.Set("email", "demo@xtablo.com")
|
form.Set("email", "demo@xtablo.com")
|
||||||
|
|
@ -283,6 +479,116 @@ func TestTasksPageReturnsHTMXMainContentSwap(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTablosPageRendersFullDashboardPage(t *testing.T) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("email", "demo@xtablo.com")
|
||||||
|
form.Set("password", "xtablo-demo")
|
||||||
|
|
||||||
|
loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
||||||
|
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
loginRec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router := newTestRouter()
|
||||||
|
router.ServeHTTP(loginRec, loginReq)
|
||||||
|
|
||||||
|
sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
|
||||||
|
if sessionCookie == nil {
|
||||||
|
t.Fatalf("expected session cookie to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/tablos", nil)
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
`class="sidebar-nav-shell"`,
|
||||||
|
`id="app-main-content" class="flex-1 overflow-auto"`,
|
||||||
|
"Mes Projets",
|
||||||
|
"Nouveau projet",
|
||||||
|
"Vue en grille",
|
||||||
|
"Rechercher...",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected tablos page to contain %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTablosPageReturnsHTMXMainContentSwap(t *testing.T) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("email", "demo@xtablo.com")
|
||||||
|
form.Set("password", "xtablo-demo")
|
||||||
|
|
||||||
|
loginReq := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
|
||||||
|
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
loginRec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router := newTestRouter()
|
||||||
|
router.ServeHTTP(loginRec, loginReq)
|
||||||
|
|
||||||
|
sessionCookie := findCookie(loginRec.Result().Cookies(), "xtablo_session")
|
||||||
|
if sessionCookie == nil {
|
||||||
|
t.Fatalf("expected session cookie to be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/tablos?view=list&status=all", nil)
|
||||||
|
req.Header.Set("HX-Request", "true")
|
||||||
|
req.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{
|
||||||
|
`id="app-main-content"`,
|
||||||
|
`hx-swap-oob="outerHTML"`,
|
||||||
|
`id="sidebar-nav-tablos"`,
|
||||||
|
"Mes Projets",
|
||||||
|
"Vue en liste",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected HTMX tablos response to contain %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(body, `class="sidebar-nav-shell"`) {
|
||||||
|
t.Fatalf("expected HTMX tablos response to avoid rerendering the full sidebar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTablosPageUtilityStylesExist(t *testing.T) {
|
||||||
|
content, err := os.ReadFile("static/tailwind.css")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read tailwind.css: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
css := string(content)
|
||||||
|
for _, want := range []string{
|
||||||
|
".flex-1",
|
||||||
|
".overflow-auto",
|
||||||
|
".text-2xl",
|
||||||
|
".bg-purple-600",
|
||||||
|
".grid-cols-1",
|
||||||
|
".rounded-xl",
|
||||||
|
".md\\:flex-row",
|
||||||
|
".sm\\:grid-cols-2",
|
||||||
|
".lg\\:grid-cols-3",
|
||||||
|
".xl\\:grid-cols-4",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(css, want) {
|
||||||
|
t.Fatalf("expected tailwind.css to contain utility %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSignupCreatesUserSessionAndRedirects(t *testing.T) {
|
func TestSignupCreatesUserSessionAndRedirects(t *testing.T) {
|
||||||
form := url.Values{}
|
form := url.Values{}
|
||||||
form.Set("email", "new@xtablo.com")
|
form.Set("email", "new@xtablo.com")
|
||||||
|
|
|
||||||
|
|
@ -1017,19 +1017,510 @@ input {
|
||||||
color: #16a34a;
|
color: #16a34a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-delete-button {
|
.ui-button {
|
||||||
|
align-items: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
font-weight: 600;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
min-height: 44px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-icon,
|
||||||
|
.ui-button-icon svg {
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button:focus-visible,
|
||||||
|
.ui-icon-button:focus-visible,
|
||||||
|
.borderless-icon-button:focus-visible {
|
||||||
|
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.2);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0.625rem 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-md {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.75rem 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-lg {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.9rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-primary {
|
||||||
|
background: var(--secondary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-primary:hover {
|
||||||
|
background: #6d28d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-secondary {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-secondary:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-ghost:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-danger {
|
||||||
|
background: #dc2626;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-button-danger:hover {
|
||||||
|
background: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-badge {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-badge-info {
|
||||||
|
background: #eff6ff;
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-badge-warning {
|
||||||
|
background: #fff4e2;
|
||||||
|
border-color: #db9729;
|
||||||
|
color: #db9729;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-badge-success {
|
||||||
|
background: #ecfdf3;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-badge-danger {
|
||||||
|
background: #fef2f2;
|
||||||
|
border-color: #fecaca;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input,
|
||||||
|
.ui-textarea {
|
||||||
|
appearance: none;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #eaecf0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: #111827;
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.4;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input {
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0.75rem 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-textarea {
|
||||||
|
min-height: 7rem;
|
||||||
|
padding: 0.85rem 0.95rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input::placeholder,
|
||||||
|
.ui-textarea::placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-input:focus,
|
||||||
|
.ui-textarea:focus {
|
||||||
|
border-color: #8b5cf6;
|
||||||
|
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-form-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-form-label {
|
||||||
|
color: #111827;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-form-hint {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-form-error {
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #eaecf0;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card-header,
|
||||||
|
.ui-card-body,
|
||||||
|
.ui-card-footer {
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card-header,
|
||||||
|
.ui-card-footer {
|
||||||
|
border-color: #eaecf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card-header {
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-card-footer {
|
||||||
|
border-top-style: solid;
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-table-shell {
|
||||||
|
overflow-x: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
min-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-empty-state {
|
||||||
|
align-items: center;
|
||||||
|
border: 1px dashed #d0d5dd;
|
||||||
|
border-radius: 1rem;
|
||||||
|
color: #6b7280;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3rem 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-empty-state-title {
|
||||||
|
color: #111827;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-empty-state-icon {
|
||||||
|
align-items: center;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #9ca3af;
|
||||||
|
display: inline-flex;
|
||||||
|
height: 4rem;
|
||||||
|
justify-content: center;
|
||||||
|
width: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-empty-state-icon svg {
|
||||||
|
height: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-empty-state-description {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 32rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-page {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 72rem;
|
||||||
|
padding: 3rem 1.5rem 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-nav {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-home-link,
|
||||||
|
.catalog-nav-link {
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #6b7280;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.55rem 0.9rem;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-home-link:hover,
|
||||||
|
.catalog-nav-link:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-nav-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-nav-link.is-active {
|
||||||
|
background: #ede9fe;
|
||||||
|
color: #6d28d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-page-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-page-header h1 {
|
||||||
|
color: #111827;
|
||||||
|
font-size: 2.25rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin: 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-page-header p {
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 42rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-eyebrow {
|
||||||
|
color: #7c3aed !important;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin-bottom: 0.75rem !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-example-list,
|
||||||
|
.catalog-page-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-example,
|
||||||
|
.catalog-page-link-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #eaecf0;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05);
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-page-link-card {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-example-copy h2,
|
||||||
|
.catalog-page-link-card h2 {
|
||||||
|
color: #111827;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-example-copy p,
|
||||||
|
.catalog-page-link-card p {
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-example-preview {
|
||||||
|
align-items: flex-start;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-inline {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-example-snippet {
|
||||||
|
background: #111827;
|
||||||
|
border-radius: 0.875rem;
|
||||||
|
color: #f9fafb;
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-example-snippet code {
|
||||||
|
font-family:
|
||||||
|
ui-monospace,
|
||||||
|
SFMono-Regular,
|
||||||
|
"SF Mono",
|
||||||
|
Menlo,
|
||||||
|
Monaco,
|
||||||
|
Consolas,
|
||||||
|
"Liberation Mono",
|
||||||
|
monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-page-link {
|
||||||
|
color: #7c3aed !important;
|
||||||
|
font-family:
|
||||||
|
ui-monospace,
|
||||||
|
SFMono-Regular,
|
||||||
|
"SF Mono",
|
||||||
|
Menlo,
|
||||||
|
Monaco,
|
||||||
|
Consolas,
|
||||||
|
"Liberation Mono",
|
||||||
|
monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-icon-button {
|
||||||
|
align-items: center;
|
||||||
|
appearance: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: #6b7280;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-icon-button:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal-backdrop {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(17, 24, 39, 0.52);
|
||||||
|
display: flex;
|
||||||
|
inset: 0;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal-panel {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #eaecf0;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: 0 24px 48px rgba(15, 23, 42, 0.18);
|
||||||
|
max-width: 32rem;
|
||||||
|
width: min(100%, 32rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal-header,
|
||||||
|
.ui-modal-body,
|
||||||
|
.ui-modal-actions {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal-header {
|
||||||
|
border-bottom: 1px solid #eaecf0;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
padding-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal-header h2 {
|
||||||
|
color: #111827;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal-body {
|
||||||
|
padding-bottom: 1.25rem;
|
||||||
|
padding-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-modal-actions {
|
||||||
|
border-top: 1px solid #eaecf0;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.borderless-icon-button {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
appearance: none;
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-card-top .borderless-icon-button {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-delete-button:hover {
|
.project-card-top .borderless-icon-button:hover {
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-delete-button svg,
|
.borderless-icon-button svg,
|
||||||
.project-date-row svg,
|
.project-date-row svg,
|
||||||
.overview-more-button svg,
|
.overview-more-button svg,
|
||||||
.tasks-add-button svg,
|
.tasks-add-button svg,
|
||||||
|
|
@ -1038,6 +1529,22 @@ input {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td.text-right .borderless-icon-button {
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.text-right .borderless-icon-button:hover {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
.project-card-title-row {
|
.project-card-title-row {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
833
go-backend/static/tailwind.css
Normal file
833
go-backend/static/tailwind.css
Normal file
|
|
@ -0,0 +1,833 @@
|
||||||
|
/*! tailwindcss v4.1.15 | MIT License | https://tailwindcss.com */
|
||||||
|
@layer properties;
|
||||||
|
:root, :host {
|
||||||
|
--color-red-50: oklch(97.1% 0.013 17.38);
|
||||||
|
--color-red-200: oklch(88.5% 0.062 18.334);
|
||||||
|
--color-red-700: oklch(50.5% 0.213 27.518);
|
||||||
|
--color-green-50: oklch(98.2% 0.018 155.826);
|
||||||
|
--color-green-200: oklch(92.5% 0.084 155.995);
|
||||||
|
--color-green-400: oklch(79.2% 0.209 151.711);
|
||||||
|
--color-green-500: oklch(72.3% 0.219 149.579);
|
||||||
|
--color-green-600: oklch(62.7% 0.194 149.214);
|
||||||
|
--color-green-800: oklch(44.8% 0.119 151.328);
|
||||||
|
--color-green-950: oklch(26.6% 0.065 152.934);
|
||||||
|
--color-cyan-500: oklch(71.5% 0.143 215.221);
|
||||||
|
--color-blue-50: oklch(97% 0.014 254.604);
|
||||||
|
--color-blue-200: oklch(88.2% 0.059 254.128);
|
||||||
|
--color-blue-400: oklch(70.7% 0.165 254.624);
|
||||||
|
--color-blue-500: oklch(62.3% 0.214 259.815);
|
||||||
|
--color-blue-600: oklch(54.6% 0.245 262.881);
|
||||||
|
--color-blue-800: oklch(42.4% 0.199 265.638);
|
||||||
|
--color-blue-950: oklch(28.2% 0.091 267.935);
|
||||||
|
--color-purple-50: oklch(97.7% 0.014 308.299);
|
||||||
|
--color-purple-400: oklch(71.4% 0.203 305.504);
|
||||||
|
--color-purple-500: oklch(62.7% 0.265 303.9);
|
||||||
|
--color-purple-600: oklch(55.8% 0.288 302.321);
|
||||||
|
--color-purple-950: oklch(29.1% 0.149 302.717);
|
||||||
|
--color-gray-50: oklch(98.5% 0.002 247.839);
|
||||||
|
--color-gray-100: oklch(96.7% 0.003 264.542);
|
||||||
|
--color-gray-200: oklch(92.8% 0.006 264.531);
|
||||||
|
--color-gray-300: oklch(87.2% 0.01 258.338);
|
||||||
|
--color-gray-400: oklch(70.7% 0.022 261.325);
|
||||||
|
--color-gray-500: oklch(55.1% 0.027 264.364);
|
||||||
|
--color-gray-600: oklch(44.6% 0.03 256.802);
|
||||||
|
--color-gray-700: oklch(37.3% 0.034 259.733);
|
||||||
|
--color-gray-800: oklch(27.8% 0.033 256.848);
|
||||||
|
--color-gray-900: oklch(21% 0.034 264.665);
|
||||||
|
--color-white: #fff;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
--text-xs: 0.75rem;
|
||||||
|
--text-xs--line-height: calc(1 / 0.75);
|
||||||
|
--text-sm: 0.875rem;
|
||||||
|
--text-sm--line-height: calc(1.25 / 0.875);
|
||||||
|
--text-2xl: 1.5rem;
|
||||||
|
--text-2xl--line-height: calc(2 / 1.5);
|
||||||
|
--font-weight-medium: 500;
|
||||||
|
--font-weight-semibold: 600;
|
||||||
|
--font-weight-bold: 700;
|
||||||
|
--tracking-wider: 0.05em;
|
||||||
|
--radius-lg: 0.5rem;
|
||||||
|
--radius-xl: 0.75rem;
|
||||||
|
--default-transition-duration: 150ms;
|
||||||
|
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
.pointer-events-none {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.visible {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.static {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
.top-1\/2 {
|
||||||
|
top: calc(1/2 * 100%);
|
||||||
|
}
|
||||||
|
.left-3 {
|
||||||
|
left: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.isolate {
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
.-mx-4 {
|
||||||
|
margin-inline: calc(var(--spacing) * -4);
|
||||||
|
}
|
||||||
|
.mb-1 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 1);
|
||||||
|
}
|
||||||
|
.mb-6 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
|
.mb-8 {
|
||||||
|
margin-bottom: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.inline {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.table {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
.size-10 {
|
||||||
|
width: calc(var(--spacing) * 10);
|
||||||
|
height: calc(var(--spacing) * 10);
|
||||||
|
}
|
||||||
|
.size-11 {
|
||||||
|
width: calc(var(--spacing) * 11);
|
||||||
|
height: calc(var(--spacing) * 11);
|
||||||
|
}
|
||||||
|
.size-12 {
|
||||||
|
width: calc(var(--spacing) * 12);
|
||||||
|
height: calc(var(--spacing) * 12);
|
||||||
|
}
|
||||||
|
.size-13 {
|
||||||
|
width: calc(var(--spacing) * 13);
|
||||||
|
height: calc(var(--spacing) * 13);
|
||||||
|
}
|
||||||
|
.size-14 {
|
||||||
|
width: calc(var(--spacing) * 14);
|
||||||
|
height: calc(var(--spacing) * 14);
|
||||||
|
}
|
||||||
|
.size-15 {
|
||||||
|
width: calc(var(--spacing) * 15);
|
||||||
|
height: calc(var(--spacing) * 15);
|
||||||
|
}
|
||||||
|
.size-16 {
|
||||||
|
width: calc(var(--spacing) * 16);
|
||||||
|
height: calc(var(--spacing) * 16);
|
||||||
|
}
|
||||||
|
.size-18 {
|
||||||
|
width: calc(var(--spacing) * 18);
|
||||||
|
height: calc(var(--spacing) * 18);
|
||||||
|
}
|
||||||
|
.size-20 {
|
||||||
|
width: calc(var(--spacing) * 20);
|
||||||
|
height: calc(var(--spacing) * 20);
|
||||||
|
}
|
||||||
|
.h-2 {
|
||||||
|
height: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.h-4 {
|
||||||
|
height: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.h-5 {
|
||||||
|
height: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
|
.h-8 {
|
||||||
|
height: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.w-4 {
|
||||||
|
width: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.w-5 {
|
||||||
|
width: calc(var(--spacing) * 5);
|
||||||
|
}
|
||||||
|
.w-8 {
|
||||||
|
width: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.w-12 {
|
||||||
|
width: calc(var(--spacing) * 12);
|
||||||
|
}
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.min-w-\[80px\] {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.shrink-0 {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.-translate-y-1\/2 {
|
||||||
|
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
}
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.grid-cols-1 {
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.flex-wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.justify-end {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.gap-1\.5 {
|
||||||
|
gap: calc(var(--spacing) * 1.5);
|
||||||
|
}
|
||||||
|
.gap-2 {
|
||||||
|
gap: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
|
.gap-3 {
|
||||||
|
gap: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.gap-4 {
|
||||||
|
gap: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.gap-6 {
|
||||||
|
gap: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.overflow-auto {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.overflow-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.overflow-x-auto {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.rounded-\[8px\] {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.rounded-full {
|
||||||
|
border-radius: calc(infinity * 1px);
|
||||||
|
}
|
||||||
|
.rounded-lg {
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
|
.rounded-xl {
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
}
|
||||||
|
.border {
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
.border-t {
|
||||||
|
border-top-style: var(--tw-border-style);
|
||||||
|
border-top-width: 1px;
|
||||||
|
}
|
||||||
|
.border-b {
|
||||||
|
border-bottom-style: var(--tw-border-style);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
.border-b-2 {
|
||||||
|
border-bottom-style: var(--tw-border-style);
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
}
|
||||||
|
.border-\[\#DB9729\] {
|
||||||
|
border-color: #DB9729;
|
||||||
|
}
|
||||||
|
.border-\[\#EAECF0\] {
|
||||||
|
border-color: #EAECF0;
|
||||||
|
}
|
||||||
|
.border-blue-200 {
|
||||||
|
border-color: var(--color-blue-200);
|
||||||
|
}
|
||||||
|
.border-green-200 {
|
||||||
|
border-color: var(--color-green-200);
|
||||||
|
}
|
||||||
|
.border-purple-600 {
|
||||||
|
border-color: var(--color-purple-600);
|
||||||
|
}
|
||||||
|
.border-red-200 {
|
||||||
|
border-color: var(--color-red-200);
|
||||||
|
}
|
||||||
|
.border-transparent {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.bg-\[\#FFF4E2\] {
|
||||||
|
background-color: #FFF4E2;
|
||||||
|
}
|
||||||
|
.bg-blue-50 {
|
||||||
|
background-color: var(--color-blue-50);
|
||||||
|
}
|
||||||
|
.bg-blue-500 {
|
||||||
|
background-color: var(--color-blue-500);
|
||||||
|
}
|
||||||
|
.bg-cyan-500 {
|
||||||
|
background-color: var(--color-cyan-500);
|
||||||
|
}
|
||||||
|
.bg-gray-50 {
|
||||||
|
background-color: var(--color-gray-50);
|
||||||
|
}
|
||||||
|
.bg-gray-200 {
|
||||||
|
background-color: var(--color-gray-200);
|
||||||
|
}
|
||||||
|
.bg-green-50 {
|
||||||
|
background-color: var(--color-green-50);
|
||||||
|
}
|
||||||
|
.bg-green-500 {
|
||||||
|
background-color: var(--color-green-500);
|
||||||
|
}
|
||||||
|
.bg-purple-50 {
|
||||||
|
background-color: var(--color-purple-50);
|
||||||
|
}
|
||||||
|
.bg-purple-500 {
|
||||||
|
background-color: var(--color-purple-500);
|
||||||
|
}
|
||||||
|
.bg-purple-600 {
|
||||||
|
background-color: var(--color-purple-600);
|
||||||
|
}
|
||||||
|
.bg-red-50 {
|
||||||
|
background-color: var(--color-red-50);
|
||||||
|
}
|
||||||
|
.bg-white {
|
||||||
|
background-color: var(--color-white);
|
||||||
|
}
|
||||||
|
.px-4 {
|
||||||
|
padding-inline: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.px-6 {
|
||||||
|
padding-inline: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
|
.py-2\.5 {
|
||||||
|
padding-block: calc(var(--spacing) * 2.5);
|
||||||
|
}
|
||||||
|
.py-3 {
|
||||||
|
padding-block: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.py-4 {
|
||||||
|
padding-block: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.pt-8 {
|
||||||
|
padding-top: calc(var(--spacing) * 8);
|
||||||
|
}
|
||||||
|
.pr-4 {
|
||||||
|
padding-right: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
.pb-3 {
|
||||||
|
padding-bottom: calc(var(--spacing) * 3);
|
||||||
|
}
|
||||||
|
.pb-6 {
|
||||||
|
padding-bottom: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
|
.pl-10 {
|
||||||
|
padding-left: calc(var(--spacing) * 10);
|
||||||
|
}
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.text-2xl {
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
line-height: var(--tw-leading, var(--text-2xl--line-height));
|
||||||
|
}
|
||||||
|
.text-sm {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: var(--tw-leading, var(--text-sm--line-height));
|
||||||
|
}
|
||||||
|
.text-xs {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
line-height: var(--tw-leading, var(--text-xs--line-height));
|
||||||
|
}
|
||||||
|
.font-bold {
|
||||||
|
--tw-font-weight: var(--font-weight-bold);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
.font-medium {
|
||||||
|
--tw-font-weight: var(--font-weight-medium);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
.font-semibold {
|
||||||
|
--tw-font-weight: var(--font-weight-semibold);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
}
|
||||||
|
.tracking-wider {
|
||||||
|
--tw-tracking: var(--tracking-wider);
|
||||||
|
letter-spacing: var(--tracking-wider);
|
||||||
|
}
|
||||||
|
.whitespace-nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.text-\[\#DB9729\] {
|
||||||
|
color: #DB9729;
|
||||||
|
}
|
||||||
|
.text-blue-600 {
|
||||||
|
color: var(--color-blue-600);
|
||||||
|
}
|
||||||
|
.text-gray-400 {
|
||||||
|
color: var(--color-gray-400);
|
||||||
|
}
|
||||||
|
.text-gray-500 {
|
||||||
|
color: var(--color-gray-500);
|
||||||
|
}
|
||||||
|
.text-gray-600 {
|
||||||
|
color: var(--color-gray-600);
|
||||||
|
}
|
||||||
|
.text-gray-700 {
|
||||||
|
color: var(--color-gray-700);
|
||||||
|
}
|
||||||
|
.text-gray-900 {
|
||||||
|
color: var(--color-gray-900);
|
||||||
|
}
|
||||||
|
.text-green-600 {
|
||||||
|
color: var(--color-green-600);
|
||||||
|
}
|
||||||
|
.text-purple-600 {
|
||||||
|
color: var(--color-purple-600);
|
||||||
|
}
|
||||||
|
.text-red-700 {
|
||||||
|
color: var(--color-red-700);
|
||||||
|
}
|
||||||
|
.text-white {
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
.uppercase {
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.placeholder-gray-400 {
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-gray-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.filter {
|
||||||
|
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||||
|
}
|
||||||
|
.transition-all {
|
||||||
|
transition-property: all;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
|
}
|
||||||
|
.transition-colors {
|
||||||
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
|
}
|
||||||
|
.hover\:bg-gray-50 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-gray-50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hover\:text-gray-700 {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
color: var(--color-gray-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:ring-2 {
|
||||||
|
&:focus {
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:ring-purple-500 {
|
||||||
|
&:focus {
|
||||||
|
--tw-ring-color: var(--color-purple-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.focus\:outline-none {
|
||||||
|
&:focus {
|
||||||
|
--tw-outline-style: none;
|
||||||
|
outline-style: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sm\:mx-0 {
|
||||||
|
@media (width >= 40rem) {
|
||||||
|
margin-inline: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sm\:grid-cols-2 {
|
||||||
|
@media (width >= 40rem) {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sm\:gap-6 {
|
||||||
|
@media (width >= 40rem) {
|
||||||
|
gap: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.md\:w-\[350px\] {
|
||||||
|
@media (width >= 48rem) {
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.md\:flex-row {
|
||||||
|
@media (width >= 48rem) {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.md\:items-center {
|
||||||
|
@media (width >= 48rem) {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.md\:justify-between {
|
||||||
|
@media (width >= 48rem) {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.lg\:grid-cols-3 {
|
||||||
|
@media (width >= 64rem) {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.xl\:grid-cols-4 {
|
||||||
|
@media (width >= 80rem) {
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:border-blue-800 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
border-color: var(--color-blue-800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:border-gray-700 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
border-color: var(--color-gray-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:border-green-800 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
border-color: var(--color-green-800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:border-purple-400 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
border-color: var(--color-purple-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:bg-blue-950\/30 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
background-color: color-mix(in srgb, oklch(28.2% 0.091 267.935) 30%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
& {
|
||||||
|
background-color: color-mix(in oklab, var(--color-blue-950) 30%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:bg-gray-700 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
background-color: var(--color-gray-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:bg-gray-800 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
background-color: var(--color-gray-800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:bg-gray-800\/80 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 80%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
& {
|
||||||
|
background-color: color-mix(in oklab, var(--color-gray-800) 80%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:bg-green-950\/30 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
background-color: color-mix(in srgb, oklch(26.6% 0.065 152.934) 30%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
& {
|
||||||
|
background-color: color-mix(in oklab, var(--color-green-950) 30%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:bg-purple-950\/30 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
background-color: color-mix(in srgb, oklch(29.1% 0.149 302.717) 30%, transparent);
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
& {
|
||||||
|
background-color: color-mix(in oklab, var(--color-purple-950) 30%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:text-blue-400 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
color: var(--color-blue-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:text-gray-100 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
color: var(--color-gray-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:text-gray-300 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
color: var(--color-gray-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:text-gray-400 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
color: var(--color-gray-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:text-green-400 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
color: var(--color-green-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:text-purple-400 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
color: var(--color-purple-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:hover\:bg-gray-800 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
background-color: var(--color-gray-800);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dark\:hover\:text-gray-200 {
|
||||||
|
&:is(.dark *) {
|
||||||
|
&:hover {
|
||||||
|
@media (hover: hover) {
|
||||||
|
color: var(--color-gray-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\]\:h-4 {
|
||||||
|
&>svg {
|
||||||
|
height: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\]\:w-4 {
|
||||||
|
&>svg {
|
||||||
|
width: calc(var(--spacing) * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.\[\&\>svg\]\:shrink-0 {
|
||||||
|
&>svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@property --tw-translate-x {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-translate-y {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-translate-z {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-border-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
|
@property --tw-font-weight {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-tracking {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-blur {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-brightness {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-contrast {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-grayscale {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-hue-rotate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-invert {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-opacity {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-saturate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-sepia {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-drop-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-drop-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-drop-shadow-alpha {
|
||||||
|
syntax: "<percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
|
@property --tw-drop-shadow-size {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-shadow-alpha {
|
||||||
|
syntax: "<percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow-alpha {
|
||||||
|
syntax: "<percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
|
@property --tw-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-ring-inset {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-width {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0px;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: #fff;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@layer properties {
|
||||||
|
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
|
||||||
|
*, ::before, ::after, ::backdrop {
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-translate-z: 0;
|
||||||
|
--tw-border-style: solid;
|
||||||
|
--tw-font-weight: initial;
|
||||||
|
--tw-tracking: initial;
|
||||||
|
--tw-blur: initial;
|
||||||
|
--tw-brightness: initial;
|
||||||
|
--tw-contrast: initial;
|
||||||
|
--tw-grayscale: initial;
|
||||||
|
--tw-hue-rotate: initial;
|
||||||
|
--tw-invert: initial;
|
||||||
|
--tw-opacity: initial;
|
||||||
|
--tw-saturate: initial;
|
||||||
|
--tw-sepia: initial;
|
||||||
|
--tw-drop-shadow: initial;
|
||||||
|
--tw-drop-shadow-color: initial;
|
||||||
|
--tw-drop-shadow-alpha: 100%;
|
||||||
|
--tw-drop-shadow-size: initial;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-color: initial;
|
||||||
|
--tw-shadow-alpha: 100%;
|
||||||
|
--tw-inset-shadow: 0 0 #0000;
|
||||||
|
--tw-inset-shadow-color: initial;
|
||||||
|
--tw-inset-shadow-alpha: 100%;
|
||||||
|
--tw-ring-color: initial;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-inset-ring-color: initial;
|
||||||
|
--tw-inset-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-inset: initial;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
go-backend/tailwind.input.css
Normal file
28
go-backend/tailwind.input.css
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
@import "tailwindcss/theme.css";
|
||||||
|
@import "tailwindcss/utilities.css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-muted: #f9fafb;
|
||||||
|
--color-text-strong: #111827;
|
||||||
|
--color-text-muted: #6b7280;
|
||||||
|
--color-border-subtle: #eaecf0;
|
||||||
|
--color-primary: #7c3aed;
|
||||||
|
--color-primary-strong: #6d28d9;
|
||||||
|
--color-danger: #dc2626;
|
||||||
|
--color-danger-strong: #b91c1c;
|
||||||
|
--color-warning-bg: #fff4e2;
|
||||||
|
--color-warning-fg: #db9729;
|
||||||
|
--color-warning-border: #db9729;
|
||||||
|
--color-info-bg: #eff6ff;
|
||||||
|
--color-info-fg: #2563eb;
|
||||||
|
--color-info-border: #bfdbfe;
|
||||||
|
--color-success-bg: #ecfdf3;
|
||||||
|
--color-success-fg: #16a34a;
|
||||||
|
--color-success-border: #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@source "./internal/web/views/**/*.templ";
|
||||||
|
@source "./internal/web/ui/**/*.templ";
|
||||||
|
|
@ -720,6 +720,15 @@ importers:
|
||||||
specifier: ^4.24.3
|
specifier: ^4.24.3
|
||||||
version: 4.44.0(@cloudflare/workers-types@4.20260411.1)
|
version: 4.44.0(@cloudflare/workers-types@4.20260411.1)
|
||||||
|
|
||||||
|
go-backend:
|
||||||
|
devDependencies:
|
||||||
|
'@tailwindcss/cli':
|
||||||
|
specifier: 4.1.15
|
||||||
|
version: 4.1.15
|
||||||
|
tailwindcss:
|
||||||
|
specifier: 4.1.15
|
||||||
|
version: 4.1.15
|
||||||
|
|
||||||
packages/auth-ui:
|
packages/auth-ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@xtablo/shared':
|
'@xtablo/shared':
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
packages:
|
packages:
|
||||||
- 'apps/*'
|
- 'apps/*'
|
||||||
|
- 'go-backend'
|
||||||
- 'packages/*'
|
- 'packages/*'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue