Add spacing, update catalog, and make several improvements to the tablo:

add update
This commit is contained in:
Arthur Belleville 2026-05-10 10:37:47 +02:00
parent c89f526780
commit b84eff7887
No known key found for this signature in database
39 changed files with 1666 additions and 374 deletions

View file

@ -8,6 +8,6 @@
<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: &#34;En cours&#34;, Variant: ui.BadgeVariantWarning})</code></pre></section></div></main>
<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="./spacing.html" class="catalog-nav-link">Spacing</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: &#34;En cours&#34;, Variant: ui.BadgeVariantWarning})</code></pre></section></div></main>
</body>
</html>

View file

@ -8,7 +8,7 @@
<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>Default solid 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-solid ui-button-default ui-button-md">Nouveau projet</button></div><pre class="catalog-example-snippet"><code>@ui.Button(ui.ButtonProps{
<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="./spacing.html" class="catalog-nav-link">Spacing</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>Default solid 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-solid ui-button-default ui-button-md">Nouveau projet</button></div><pre class="catalog-example-snippet"><code>@ui.Button(ui.ButtonProps{
Label: &#34;Nouveau projet&#34;,
Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD,

View file

@ -8,7 +8,7 @@
<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{
<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="./spacing.html" class="catalog-nav-link">Spacing</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(&#34;Header&#34;),
Body: textComponent(&#34;Body&#34;),
Footer: textComponent(&#34;Footer&#34;),

View file

@ -8,7 +8,7 @@
<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-solid ui-button-default 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{
<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="./spacing.html" class="catalog-nav-link">Spacing</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-solid ui-button-default 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: &#34;Aucun projet trouvé&#34;,
Description: &#34;Créez votre premier projet&#34;,
Icon: ui.UIIcon(&#34;grid3x3&#34;),

View file

@ -8,7 +8,7 @@
<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{
<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="./spacing.html" class="catalog-nav-link">Spacing</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: &#34;Nom&#34;,
For: &#34;catalog-name&#34;,
Field: ui.Input(ui.InputProps{

View file

@ -8,10 +8,17 @@
<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{
<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="./spacing.html" class="catalog-nav-link">Spacing</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 ui-icon-button-ghost ui-icon-button-danger" 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: &#34;Supprimer le projet&#34;,
Icon: &#34;trash&#34;,
Variant: ui.IconButtonVariantDangerGhost,
Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: &#34;button&#34;,
})</code></pre></section><section class="catalog-example"><div class="catalog-example-copy"><h2>Borderless neutral action</h2><p>Used for lightweight edit or details actions inside cards and list rows.</p></div><div class="catalog-example-preview"><button type="button" class="borderless-icon-button ui-icon-button-ghost ui-icon-button-neutral" aria-label="Modifier le projet"><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 20h9"></path> <path d="M16.5 3.5a2.12 2.12 0 1 1 3 3L7 19l-4 1 1-4Z"></path></svg></button></div><pre class="catalog-example-snippet"><code>@ui.IconButton(ui.IconButtonProps{
Label: &#34;Modifier le projet&#34;,
Icon: &#34;pencil&#34;,
Variant: ui.IconButtonVariantNeutral,
Tone: ui.IconButtonToneGhost,
Type: &#34;button&#34;,
})</code></pre></section></div></main>
</body>

View file

@ -8,6 +8,6 @@
<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>
<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="./spacing.html" class="catalog-page-link-card"><h2>Spacing</h2><p>Fixed horizontal and vertical spacer primitives for composing gaps between UI components.</p><p class="catalog-page-link">/spacing.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>

View file

@ -8,7 +8,7 @@
<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{
<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="./spacing.html" class="catalog-nav-link">Spacing</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: &#34;name&#34;,
Value: &#34;Projet Atlas&#34;,
Placeholder: &#34;Nom du projet&#34;,

View file

@ -8,7 +8,7 @@
<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-solid ui-button-neutral ui-button-md">Annuler</button><button type="submit" class="ui-button ui-button-solid ui-button-default ui-button-md">Créer le projet</button></div></div></div></div><pre class="catalog-example-snippet"><code>@ui.Modal(ui.ModalProps{
<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="./spacing.html" class="catalog-nav-link">Spacing</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-solid ui-button-neutral ui-button-md">Annuler</button><button type="submit" class="ui-button ui-button-solid ui-button-default ui-button-md">Créer le projet</button></div></div></div></div><pre class="catalog-example-snippet"><code>@ui.Modal(ui.ModalProps{
Title: &#34;Créer un projet&#34;,
Body: ui.FormField(...),
Actions: ui.Button(...),

View file

@ -8,7 +8,7 @@
<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{
<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="./spacing.html" class="catalog-nav-link">Spacing</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>

View file

@ -8,6 +8,6 @@
<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: &#34;En cours&#34;, Variant: ui.BadgeVariantWarning})</code></pre></section></div></main>
<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="./spacing.html" class="catalog-nav-link">Spacing</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: &#34;En cours&#34;, Variant: ui.BadgeVariantWarning})</code></pre></section></div></main>
</body>
</html>

View file

@ -33,7 +33,6 @@ This intentionally does not start the broader `status` deprecation effort. `stat
- Task-derived status inference
- Reworking the current search or filter model
- Introducing custom JavaScript beyond the existing HTMX-driven pattern
- Adding color pickers, preset palettes, or browser-native advanced color UI
- Building a `/tablos/:id` detail edit page
**User Experience**
@ -46,9 +45,10 @@ On the Go backend `Mes Projets` page:
- the modal lets the user update:
- `name`
- `color`
- the edit modal includes a color picker control bound to the same `color` value
- `status` is not editable in the create or edit modal for this slice
The color is entered as a text value using a strict full hex format such as `#3B82F6`.
The stored color value remains a strict full hex string such as `#3B82F6`.
**Data Model**
@ -152,6 +152,8 @@ Modal behavior:
- create modal collects `name` and `color`
- edit modal collects `name` and `color`
- edit modal includes a native color picker control, prefilled from the current tablo color
- the picker and color text input stay in sync so submissions always send one canonical `#RRGGBB` value
- both modals render inline validation errors
- cancel closes the modal and preserves current page state
@ -224,6 +226,13 @@ A pragmatic shape is:
The exact struct layout can be chosen during implementation, but it should support both modal variants without duplicating page-state plumbing.
For the edit modal specifically, the view model should provide the current validated hex color so both:
- the text input
- the native color picker
can render from the same source of truth.
**Error Handling**
Create or update validation failure:
@ -254,10 +263,11 @@ Repository coverage:
Handler coverage:
- `GET /tablos` create modal includes `color` field
- `GET /tablos/{id}/edit` renders prefilled `name` and `color`
- `GET /tablos/{id}/edit` renders prefilled `name`, `color`, and color picker value
- `POST /tablos` rejects missing or invalid `color`
- `POST /tablos/{id}` rejects missing or invalid `color`
- `POST /tablos/{id}` updates visible name and color in returned HTML
- edit modal keeps color picker and submitted hex value aligned
- grid card markup contains edit action before delete
- list row markup contains edit action before delete
@ -266,6 +276,7 @@ HTML assertions should verify:
- the edit trigger exists with the expected icon/button semantics
- the edit trigger appears before delete in the rendered action area
- the modal contains both `Nom du projet` and `Couleur`
- the edit modal contains a color picker control in addition to the hex color input
- invalid hex values return a `422` response with inline feedback mentioning `#RRGGBB`
**Implementation Notes**
@ -289,6 +300,7 @@ The feature is complete when:
- clicking edit opens a modal for the selected tablo
- the modal allows changing the tablo `name`
- the modal allows changing the tablo `color`
- the edit modal includes a color picker for choosing the tablo color
- create also accepts `color`
- `color` only accepts full 6-digit hex values like `#3B82F6`
- successful edits update the rendered project card or list row

View file

@ -22,6 +22,7 @@ func TestGenerateSiteWritesExpectedPages(t *testing.T) {
"inputs.html",
"form-fields.html",
"modals.html",
"spacing.html",
"tables.html",
"empty-states.html",
"cards.html",

View file

@ -60,6 +60,7 @@ INSERT INTO public.tablos (
id,
owner_id,
name,
color,
status,
created_at,
updated_at
@ -68,13 +69,14 @@ INSERT INTO public.tablos (
$2,
$3,
$4,
$5,
now(),
now()
)
RETURNING id, owner_id, name, status, created_at, updated_at, deleted_at;
RETURNING id, owner_id, name, color, status, created_at, updated_at, deleted_at;
-- name: ListTablos :many
SELECT id, owner_id, name, status, created_at, updated_at, deleted_at
SELECT id, owner_id, name, color, status, created_at, updated_at, deleted_at
FROM public.tablos
WHERE owner_id = sqlc.arg(owner_id)
AND deleted_at IS NULL
@ -86,6 +88,15 @@ WHERE owner_id = sqlc.arg(owner_id)
)
ORDER BY created_at DESC;
-- name: UpdateTablo :execrows
UPDATE public.tablos
SET name = $3,
color = $4,
updated_at = now()
WHERE id = $1
AND owner_id = $2
AND deleted_at IS NULL;
-- name: SoftDeleteTablo :execrows
UPDATE public.tablos
SET deleted_at = now(), updated_at = now()

View file

@ -24,6 +24,8 @@ type PostgresAuthRepository struct {
queries *sqlcdb.Queries
}
const defaultTabloColor = "#3B82F6"
func NewPostgresAuthRepository(ctx context.Context, databaseURL string) (*PostgresAuthRepository, error) {
if databaseURL == "" {
return nil, errors.New("DATABASE_URL is required")
@ -149,6 +151,7 @@ func (r *PostgresAuthRepository) CreateTablo(ctx context.Context, input tablomod
ID: uuid.New(),
OwnerID: input.OwnerID,
Name: strings.TrimSpace(input.Name),
Color: storedTabloColor(input.Color),
Status: string(input.Status),
})
if err != nil {
@ -177,6 +180,22 @@ func (r *PostgresAuthRepository) ListTablos(ctx context.Context, input tablomode
return tablos, nil
}
func (r *PostgresAuthRepository) UpdateTablo(ctx context.Context, input tablomodel.UpdateInput) error {
rows, err := r.queries.UpdateTablo(ctx, sqlcdb.UpdateTabloParams{
ID: input.ID,
OwnerID: input.OwnerID,
Name: strings.TrimSpace(input.Name),
Color: storedTabloColor(input.Color),
})
if err != nil {
return err
}
if rows == 0 {
return tablomodel.ErrNotFound
}
return 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,
@ -202,6 +221,14 @@ func nullableText(value string) pgtype.Text {
return pgtype.Text{String: value, Valid: true}
}
func storedTabloColor(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return defaultTabloColor
}
return trimmed
}
func nullableStatus(value *tablomodel.Status) pgtype.Text {
if value == nil {
return pgtype.Text{}
@ -214,6 +241,7 @@ func mapTabloRecord(row sqlcdb.Tablo) tablomodel.Record {
ID: row.ID,
OwnerID: row.OwnerID,
Name: row.Name,
Color: storedTabloColor(row.Color),
Status: tablomodel.Status(row.Status),
CreatedAt: row.CreatedAt.Time,
UpdatedAt: row.UpdatedAt.Time,

View file

@ -32,6 +32,7 @@ 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,
color text NOT NULL,
status text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),

View file

@ -31,6 +31,7 @@ type Tablo struct {
ID uuid.UUID `db:"id"`
OwnerID uuid.UUID `db:"owner_id"`
Name string `db:"name"`
Color string `db:"color"`
Status string `db:"status"`
CreatedAt pgtype.Timestamptz `db:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at"`

View file

@ -20,6 +20,7 @@ type Querier interface {
GetSessionByToken(ctx context.Context, sessionToken string) (AuthSession, error)
ListTablos(ctx context.Context, arg ListTablosParams) ([]Tablo, error)
SoftDeleteTablo(ctx context.Context, arg SoftDeleteTabloParams) (int64, error)
UpdateTablo(ctx context.Context, arg UpdateTabloParams) (int64, error)
}
var _ Querier = (*Queries)(nil)

View file

@ -90,6 +90,7 @@ INSERT INTO public.tablos (
id,
owner_id,
name,
color,
status,
created_at,
updated_at
@ -98,16 +99,18 @@ INSERT INTO public.tablos (
$2,
$3,
$4,
$5,
now(),
now()
)
RETURNING id, owner_id, name, status, created_at, updated_at, deleted_at
RETURNING id, owner_id, name, color, 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"`
Color string `db:"color"`
Status string `db:"status"`
}
@ -116,6 +119,7 @@ func (q *Queries) CreateTablo(ctx context.Context, arg CreateTabloParams) (Tablo
arg.ID,
arg.OwnerID,
arg.Name,
arg.Color,
arg.Status,
)
var i Tablo
@ -123,6 +127,7 @@ func (q *Queries) CreateTablo(ctx context.Context, arg CreateTabloParams) (Tablo
&i.ID,
&i.OwnerID,
&i.Name,
&i.Color,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
@ -214,7 +219,7 @@ func (q *Queries) GetSessionByToken(ctx context.Context, sessionToken string) (A
}
const listTablos = `-- name: ListTablos :many
SELECT id, owner_id, name, status, created_at, updated_at, deleted_at
SELECT id, owner_id, name, color, status, created_at, updated_at, deleted_at
FROM public.tablos
WHERE owner_id = $1
AND deleted_at IS NULL
@ -246,6 +251,7 @@ func (q *Queries) ListTablos(ctx context.Context, arg ListTablosParams) ([]Tablo
&i.ID,
&i.OwnerID,
&i.Name,
&i.Color,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
@ -281,3 +287,33 @@ func (q *Queries) SoftDeleteTablo(ctx context.Context, arg SoftDeleteTabloParams
}
return result.RowsAffected(), nil
}
const updateTablo = `-- name: UpdateTablo :execrows
UPDATE public.tablos
SET name = $3,
color = $4,
updated_at = now()
WHERE id = $1
AND owner_id = $2
AND deleted_at IS NULL
`
type UpdateTabloParams struct {
ID uuid.UUID `db:"id"`
OwnerID uuid.UUID `db:"owner_id"`
Name string `db:"name"`
Color string `db:"color"`
}
func (q *Queries) UpdateTablo(ctx context.Context, arg UpdateTabloParams) (int64, error) {
result, err := q.db.Exec(ctx, updateTablo,
arg.ID,
arg.OwnerID,
arg.Name,
arg.Color,
)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}

View file

@ -21,6 +21,7 @@ type Record struct {
ID uuid.UUID
OwnerID uuid.UUID
Name string
Color string
Status Status
CreatedAt time.Time
UpdatedAt time.Time
@ -30,9 +31,17 @@ type Record struct {
type CreateInput struct {
OwnerID uuid.UUID
Name string
Color string
Status Status
}
type UpdateInput struct {
ID uuid.UUID
OwnerID uuid.UUID
Name string
Color string
}
type ListInput struct {
OwnerID uuid.UUID
Query string

View file

@ -33,6 +33,7 @@ type AuthRepository interface {
GetSessionByToken(ctx context.Context, token string) (Session, error)
DeleteSessionByToken(ctx context.Context, token string) error
CreateTablo(ctx context.Context, input CreateTabloInput) (TabloRecord, error)
UpdateTablo(ctx context.Context, input UpdateTabloInput) error
ListTablos(ctx context.Context, input ListTablosInput) ([]TabloRecord, error)
SoftDeleteTablo(ctx context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error
}

View file

@ -6,6 +6,7 @@ import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"time"
@ -16,6 +17,11 @@ import (
var ErrTabloNotFound = tablomodel.ErrNotFound
var tabloColorPattern = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`)
const defaultTabloColor = "#3B82F6"
const tabloColorValidationMessage = "La couleur du projet doit être un code hexadécimal au format #RRGGBB"
type TabloStatus = tablomodel.Status
const (
@ -26,13 +32,15 @@ const (
type TabloRecord = tablomodel.Record
type CreateTabloInput = tablomodel.CreateInput
type UpdateTabloInput = tablomodel.UpdateInput
type ListTablosInput = tablomodel.ListInput
type TablosPageState struct {
View string
Query string
Status string
ModalOpen bool
ModalKind string
EditingTabloID string
}
func normalizeTabloQuery(query string) string {
@ -58,7 +66,7 @@ func parseTablosPageState(values interface {
View: view,
Query: strings.TrimSpace(values.Get("q")),
Status: status,
ModalOpen: strings.TrimSpace(values.Get("modal")) == "create",
ModalKind: normalizedModalKind(strings.TrimSpace(values.Get("modal"))),
}
}
@ -78,6 +86,30 @@ func (s TablosPageState) statusFilter() *TabloStatus {
}
}
func normalizedModalKind(kind string) string {
switch kind {
case "create", "edit":
return kind
default:
return ""
}
}
func normalizeTabloColor(raw string) (string, bool) {
color := strings.TrimSpace(raw)
if !tabloColorPattern.MatchString(color) {
return "", false
}
return strings.ToUpper(color), true
}
func storedTabloColor(raw string) string {
if color, ok := normalizeTabloColor(raw); ok {
return color
}
return defaultTabloColor
}
func (h *AuthHandler) PostTablos() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, ok := h.authenticatedUser(r.Context(), r)
@ -92,35 +124,153 @@ func (h *AuthHandler) PostTablos() http.HandlerFunc {
}
state := parseTablosPageState(r.Form)
state.ModalOpen = true
state.ModalKind = "create"
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)
tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), "Le nom du projet est requis"), http.StatusUnprocessableEntity)
return
}
color, ok := normalizeTabloColor(r.FormValue("color"))
if !ok {
tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), tabloColorValidationMessage), http.StatusUnprocessableEntity)
return
}
if _, err := h.repo.CreateTablo(r.Context(), CreateTabloInput{
OwnerID: user.ID,
Name: name,
Color: color,
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(),
})
state.ModalKind = ""
tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", ""), http.StatusOK)
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
}
}
func (h *AuthHandler) GetEditTabloModal() 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
}
state := parseTablosPageState(r.URL.Query())
state.ModalKind = "edit"
state.EditingTabloID = tabloID.String()
tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
tablo, ok := findTabloByID(tablos, tabloID)
if !ok {
http.Error(w, "tablo not found", http.StatusNotFound)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, tablo.Name, tablo.Color, ""), http.StatusOK)
}
}
func (h *AuthHandler) PostTabloUpdate() 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 := r.ParseForm(); err != nil {
http.Error(w, "invalid form payload", http.StatusBadRequest)
return
}
state := parseTablosPageState(r.Form)
state.ModalKind = "edit"
state.EditingTabloID = tabloID.String()
name := strings.TrimSpace(r.FormValue("name"))
if name == "" {
tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), "Le nom du projet est requis"), http.StatusUnprocessableEntity)
return
}
color, colorOK := normalizeTabloColor(r.FormValue("color"))
if !colorOK {
tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), tabloColorValidationMessage), http.StatusUnprocessableEntity)
return
}
if err := h.repo.UpdateTablo(r.Context(), UpdateTabloInput{
ID: tabloID,
OwnerID: user.ID,
Name: name,
Color: color,
}); err != nil {
if errors.Is(err, ErrTabloNotFound) {
http.Error(w, "tablo not found", http.StatusNotFound)
return
}
http.Error(w, "failed to update tablo", http.StatusInternalServerError)
return
}
state.ModalKind = ""
state.EditingTabloID = ""
tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
}
}
@ -148,17 +298,13 @@ func (h *AuthHandler) DeleteTablo() http.HandlerFunc {
}
state := parseTablosPageState(r.URL.Query())
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{
OwnerID: user.ID,
Query: state.Query,
Status: state.statusFilter(),
})
tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", ""), http.StatusOK)
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
}
}
@ -170,32 +316,47 @@ func (h *AuthHandler) renderTablosPage(w http.ResponseWriter, r *http.Request) {
}
state := parseTablosPageState(r.URL.Query())
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{
OwnerID: user.ID,
Query: state.Query,
Status: state.statusFilter(),
})
tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return
}
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", ""), http.StatusOK)
renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK)
}
func tablosPageViewModel(user PublicUser, state TablosPageState, tablos []TabloRecord, formName string, errorMessage string) views.TablosPageViewModel {
func tablosPageViewModel(user PublicUser, state TablosPageState, tablos []TabloRecord, formName string, formColor string, errorMessage string) views.TablosPageViewModel {
return views.NewTablosPageViewModel(
user.DisplayName,
state.View,
state.Query,
state.Status,
state.ModalOpen,
state.ModalKind,
state.EditingTabloID,
formName,
formColor,
errorMessage,
buildTabloCardViews(tablos, state),
)
}
func listTablosForState(ctx context.Context, repo AuthRepository, ownerID uuid.UUID, state TablosPageState) ([]TabloRecord, error) {
return repo.ListTablos(ctx, ListTablosInput{
OwnerID: ownerID,
Query: state.Query,
Status: state.statusFilter(),
})
}
func findTabloByID(tablos []TabloRecord, targetID uuid.UUID) (TabloRecord, bool) {
for _, tablo := range tablos {
if tablo.ID == targetID {
return tablo, true
}
}
return TabloRecord{}, false
}
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)
@ -221,6 +382,7 @@ func (r *InMemoryAuthRepository) CreateTablo(_ context.Context, input CreateTabl
ID: uuid.New(),
OwnerID: input.OwnerID,
Name: strings.TrimSpace(input.Name),
Color: storedTabloColor(input.Color),
Status: input.Status,
CreatedAt: now,
UpdatedAt: now,
@ -258,6 +420,22 @@ func (r *InMemoryAuthRepository) ListTablos(_ context.Context, input ListTablosI
return tablos, nil
}
func (r *InMemoryAuthRepository) UpdateTablo(_ context.Context, input UpdateTabloInput) error {
r.mu.Lock()
defer r.mu.Unlock()
tablo, ok := r.tablos[input.ID]
if !ok || tablo.OwnerID != input.OwnerID || tablo.DeletedAt != nil {
return ErrTabloNotFound
}
tablo.Name = strings.TrimSpace(input.Name)
tablo.Color = storedTabloColor(input.Color)
tablo.UpdatedAt = time.Now().UTC()
r.tablos[input.ID] = tablo
return nil
}
func (r *InMemoryAuthRepository) SoftDeleteTablo(_ context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error {
r.mu.Lock()
defer r.mu.Unlock()
@ -293,6 +471,7 @@ func buildTabloCardViews(tablos []TabloRecord, state TablosPageState) []views.Ta
items = append(items, views.TabloCardView{
ID: tablo.ID.String(),
Name: tablo.Name,
Color: storedTabloColor(tablo.Color),
Status: string(tablo.Status),
StatusLabel: statusLabel,
StatusClass: statusClass,
@ -303,6 +482,7 @@ func buildTabloCardViews(tablos []TabloRecord, state TablosPageState) []views.Ta
ProgressLabel: fmt.Sprintf("%d%%", progress),
DeleteURL: "/tablos/" + tablo.ID.String(),
DeleteRequestURL: buildDeleteRequestURL("/tablos/"+tablo.ID.String(), state),
EditRequestURL: buildEditRequestURL("/tablos/"+tablo.ID.String()+"/edit", state),
IconKind: iconKind,
IconBgClass: bgClass,
IconFgClass: fgClass,
@ -314,6 +494,14 @@ func buildTabloCardViews(tablos []TabloRecord, state TablosPageState) []views.Ta
}
func buildDeleteRequestURL(path string, state TablosPageState) string {
return buildStatefulRequestURL(path, state)
}
func buildEditRequestURL(path string, state TablosPageState) string {
return buildStatefulRequestURL(path, state)
}
func buildStatefulRequestURL(path string, state TablosPageState) string {
values := url.Values{}
values.Set("view", state.View)
values.Set("status", state.Status)

View file

@ -127,6 +127,28 @@ func TestInMemoryTablosSoftDeleteRejectsDifferentOwner(t *testing.T) {
}
}
func TestInMemoryTablosCreatePersistsColor(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)
}
created, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: user.ID,
Name: "Roadmap",
Color: "#3B82F6",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatalf("create colored tablo: %v", err)
}
if created.Color != "#3B82F6" {
t.Fatalf("expected color to persist, got %q", created.Color)
}
}
func TestGetTablosPageDefaultsToGridAndAllStatus(t *testing.T) {
handler := newTestAuthHandler(t)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
@ -376,7 +398,7 @@ func TestGetTablosPageListViewUsesDirectTableIconMarkup(t *testing.T) {
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="borderless-icon-button ui-icon-button-ghost ui-icon-button-danger" aria-label="Supprimer le projet"`,
`class="lucide lucide-trash2 w-4 h-4"`,
} {
if !strings.Contains(body, want) {
@ -454,11 +476,12 @@ func TestGetTablosPageGridUsesProjectCardMarkup(t *testing.T) {
body := rec.Body.String()
for _, want := range []string{
`<article class="project-card">`,
`<article class="project-card" style="--project-color:`,
`class="project-card-top"`,
`class="borderless-icon-button"`,
`class="borderless-icon-button ui-icon-button-ghost ui-icon-button-neutral" aria-label="Modifier le projet"`,
`class="borderless-icon-button ui-icon-button-ghost ui-icon-button-danger" aria-label="Supprimer le projet"`,
`class="project-card-title-row"`,
`class="project-avatar project-accent-`,
`class="project-avatar"`,
`class="project-date-row"`,
`class="project-progress-track"`,
} {
@ -474,6 +497,7 @@ func TestPostTablosCreatesTodoTablo(t *testing.T) {
form := url.Values{}
form.Set("name", "Roadmap")
form.Set("color", "#3B82F6")
form.Set("view", "grid")
form.Set("status", "all")
@ -518,6 +542,152 @@ func TestPostTablosWithEmptyNameReturns422(t *testing.T) {
}
}
func TestPostTablosWithMissingColorReturns422(t *testing.T) {
handler := newTestAuthHandler(t)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
form := url.Values{}
form.Set("name", "Roadmap")
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(), "La couleur du projet doit être un code hexadécimal au format #RRGGBB") {
t.Fatalf("expected color validation error, got %q", rec.Body.String())
}
}
func TestPostTablosWithInvalidColorReturns422(t *testing.T) {
handler := newTestAuthHandler(t)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
form := url.Values{}
form.Set("name", "Roadmap")
form.Set("color", "#0af")
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(), "La couleur du projet doit être un code hexadécimal au format #RRGGBB") {
t.Fatalf("expected invalid color validation error, got %q", rec.Body.String())
}
}
func TestGetTablosEditModalPrefillsNameAndColorPicker(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: "Palette",
Color: "#3B82F6",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatalf("create tablo: %v", err)
}
editReq := httptest.NewRequest(http.MethodGet, "/tablos/"+tablo.ID.String()+"/edit?view=grid&status=all", nil)
editReq.SetPathValue("tabloID", tablo.ID.String())
editReq.AddCookie(sessionCookie)
rec := httptest.NewRecorder()
handler.GetEditTabloModal().ServeHTTP(rec, editReq)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
body := rec.Body.String()
for _, want := range []string{
"Palette",
`value="#3B82F6"`,
`type="color"`,
} {
if !strings.Contains(body, want) {
t.Fatalf("expected edit modal to contain %q, got %q", want, body)
}
}
}
func TestPostTablosUpdateRejectsDifferentOwner(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",
Color: "#3B82F6",
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")
form := url.Values{}
form.Set("name", "Updated")
form.Set("color", "#EF4444")
updateReq := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String(), strings.NewReader(form.Encode()))
updateReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
updateReq.SetPathValue("tabloID", tablo.ID.String())
updateReq.AddCookie(otherCookie)
rec := httptest.NewRecorder()
handler.PostTabloUpdate().ServeHTTP(rec, updateReq)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected status 404, got %d", rec.Code)
}
}
func TestDeleteTabloSoftDeletesOwnedRow(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)

View file

@ -36,14 +36,13 @@ func TestPagesIncludeTokensAndButtons(t *testing.T) {
}
func TestPagesIncludePrimitiveCatalogCoverage(t *testing.T) {
pages := Pages()
for _, slug := range []string{
"badges",
"icon-buttons",
"inputs",
"form-fields",
"modals",
"spacing",
"tables",
"empty-states",
"cards",
@ -52,10 +51,6 @@ func TestPagesIncludePrimitiveCatalogCoverage(t *testing.T) {
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) {
@ -164,6 +159,23 @@ func TestPrimitiveExamplesRenderRealMarkup(t *testing.T) {
}
}
func TestSpacingPageRendersSpacingPrimitives(t *testing.T) {
page, ok := FindPage("spacing")
if !ok {
t.Fatal("expected spacing page")
}
html := renderToString(t, CatalogPage(page))
for _, want := range []string{
`ui-space-x`,
`ui-space-y`,
} {
if !strings.Contains(html, want) {
t.Fatalf("spacing page expected %q in %q", want, html)
}
}
}
func renderToString(t *testing.T, component templ.Component) string {
t.Helper()

View file

@ -123,13 +123,33 @@ func iconButtonExamples() []Example {
Preview: ui.IconButton(ui.IconButtonProps{
Label: "Supprimer le projet",
Icon: "trash",
Variant: ui.IconButtonVariantDangerGhost,
Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button",
}),
Snippet: `@ui.IconButton(ui.IconButtonProps{
Label: "Supprimer le projet",
Icon: "trash",
Variant: ui.IconButtonVariantDangerGhost,
Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button",
})`,
},
{
Title: "Borderless neutral action",
Description: "Used for lightweight edit or details actions inside cards and list rows.",
Preview: ui.IconButton(ui.IconButtonProps{
Label: "Modifier le projet",
Icon: "pencil",
Variant: ui.IconButtonVariantNeutral,
Tone: ui.IconButtonToneGhost,
Type: "button",
}),
Snippet: `@ui.IconButton(ui.IconButtonProps{
Label: "Modifier le projet",
Icon: "pencil",
Variant: ui.IconButtonVariantNeutral,
Tone: ui.IconButtonToneGhost,
Type: "button",
})`,
},
@ -247,6 +267,67 @@ func modalExamples() []Example {
}
}
func spacingExamples() []Example {
return []Example{
{
Title: "Horizontal spacing",
Description: "Use SpaceX to insert fixed horizontal gaps between inline or row-aligned components.",
Preview: componentFunc(func(ctx context.Context, w io.Writer) error {
if _, err := io.WriteString(w, `<div class="catalog-spacing-row">`); err != nil {
return err
}
for _, component := range []templ.Component{
ui.Button(ui.ButtonProps{
Label: "Précédent",
Variant: ui.ButtonVariantNeutral,
Size: ui.SizeMD,
Type: "button",
}),
ui.SpaceX(ui.SpaceProps{Size: ui.SpacingStepLG}),
ui.Button(ui.ButtonProps{
Label: "Suivant",
Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD,
Type: "button",
}),
} {
if err := component.Render(ctx, w); err != nil {
return err
}
}
_, err := io.WriteString(w, `</div>`)
return err
}),
Snippet: `@ui.SpaceX(ui.SpaceProps{Size: ui.SpacingStepLG})`,
},
{
Title: "Vertical spacing",
Description: "Use SpaceY to insert fixed vertical gaps between stacked blocks.",
Preview: componentFunc(func(ctx context.Context, w io.Writer) error {
if _, err := io.WriteString(w, `<div class="catalog-spacing-column">`); err != nil {
return err
}
for _, component := range []templ.Component{
ui.Card(ui.CardProps{
Body: textComponent("Bloc 1"),
}),
ui.SpaceY(ui.SpaceProps{Size: ui.SpacingStepMD}),
ui.Card(ui.CardProps{
Body: textComponent("Bloc 2"),
}),
} {
if err := component.Render(ctx, w); err != nil {
return err
}
}
_, err := io.WriteString(w, `</div>`)
return err
}),
Snippet: `@ui.SpaceY(ui.SpaceProps{Size: ui.SpacingStepMD})`,
},
}
}
func tableExamples() []Example {
return []Example{
{

View file

@ -60,6 +60,12 @@ func Pages() []Page {
Description: "Shared modal shell for focused create, edit, and confirm flows.",
Examples: modalExamples(),
},
{
Slug: "spacing",
Title: "Spacing",
Description: "Fixed horizontal and vertical spacer primitives for composing gaps between UI components.",
Examples: spacingExamples(),
},
{
Slug: "tables",
Title: "Tables",

View file

@ -4,12 +4,13 @@ type IconButtonProps struct {
Label string
Icon string
Variant IconButtonVariant
Tone IconButtonTone
Type string
Attrs templ.Attributes
}
templ IconButton(props IconButtonProps) {
<button type={ buttonType(props.Type) } class={ iconButtonClass(props.Variant) } aria-label={ props.Label } { props.Attrs... }>
<button type={ buttonType(props.Type) } class={ iconButtonClass(props.Variant, props.Tone) } aria-label={ props.Label } { props.Attrs... }>
@UIIcon(props.Icon)
</button>
}
@ -54,6 +55,11 @@ templ UIIcon(kind string) {
<rect width="18" height="18" x="3" y="4" rx="2"></rect>
<path d="M3 10h18"></path>
</svg>
case "pencil":
<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 20h9"></path>
<path d="M16.5 3.5a2.12 2.12 0 1 1 3 3L7 19l-4 1 1-4Z"></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>

View file

@ -12,6 +12,7 @@ type IconButtonProps struct {
Label string
Icon string
Variant IconButtonVariant
Tone IconButtonTone
Type string
Attrs templ.Attributes
}
@ -37,7 +38,7 @@ func IconButton(props IconButtonProps) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{iconButtonClass(props.Variant)}
var templ_7745c5c3_Var2 = []any{iconButtonClass(props.Variant, props.Tone)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
@ -49,7 +50,7 @@ func IconButton(props IconButtonProps) templ.Component {
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}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/icon_button.templ`, Line: 13, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@ -75,7 +76,7 @@ func IconButton(props IconButtonProps) templ.Component {
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}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/icon_button.templ`, Line: 13, Col: 118}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@ -157,26 +158,31 @@ func UIIcon(kind string) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
case "pencil":
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=\"M12 20h9\"></path> <path d=\"M16.5 3.5a2.12 2.12 0 1 1 3 3L7 19l-4 1 1-4Z\"></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>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<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\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<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}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/ui/icon_button.templ`, Line: 72, 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>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View file

@ -40,7 +40,8 @@ func TestIconButtonRendersBorderlessDestructiveMarkup(t *testing.T) {
component := IconButton(IconButtonProps{
Label: "Supprimer le projet",
Icon: "trash",
Variant: IconButtonVariantDangerGhost,
Variant: IconButtonVariantDanger,
Tone: IconButtonToneGhost,
Type: "button",
})
@ -50,6 +51,8 @@ func TestIconButtonRendersBorderlessDestructiveMarkup(t *testing.T) {
`type="button"`,
`aria-label="Supprimer le projet"`,
`borderless-icon-button`,
`ui-icon-button-ghost`,
`ui-icon-button-danger`,
`lucide-trash2`,
} {
if !strings.Contains(html, want) {
@ -58,6 +61,31 @@ func TestIconButtonRendersBorderlessDestructiveMarkup(t *testing.T) {
}
}
func TestIconButtonRendersBorderlessNeutralMarkup(t *testing.T) {
component := IconButton(IconButtonProps{
Label: "Modifier le projet",
Icon: "pencil",
Variant: IconButtonVariantNeutral,
Tone: IconButtonToneGhost,
Type: "button",
})
html := renderToString(t, component)
for _, want := range []string{
`type="button"`,
`aria-label="Modifier le projet"`,
`borderless-icon-button`,
`ui-icon-button-ghost`,
`ui-icon-button-neutral`,
`M12 20h9`,
} {
if !strings.Contains(html, want) {
t.Fatalf("expected %q in %q", want, html)
}
}
}
func TestBadgeRendersSemanticStatusVariant(t *testing.T) {
component := Badge(BadgeProps{
Label: "En cours",
@ -99,6 +127,38 @@ func TestModalRendersShellStructure(t *testing.T) {
}
}
func TestSpaceXRendersDefaultMediumMarkup(t *testing.T) {
component := SpaceX(SpaceProps{})
html := renderToString(t, component)
for _, want := range []string{
`aria-hidden="true"`,
`ui-space-x`,
`ui-space-x-md`,
} {
if !strings.Contains(html, want) {
t.Fatalf("expected %q in %q", want, html)
}
}
}
func TestSpaceYRendersExplicitExtraLargeMarkup(t *testing.T) {
component := SpaceY(SpaceProps{Size: SpacingStepXL})
html := renderToString(t, component)
for _, want := range []string{
`aria-hidden="true"`,
`ui-space-y`,
`ui-space-y-xl`,
} {
if !strings.Contains(html, want) {
t.Fatalf("expected %q in %q", want, html)
}
}
}
func TestButtonUsesSharedTokenBackedClasses(t *testing.T) {
component := Button(ButtonProps{
Label: "Create",
@ -134,7 +194,12 @@ func TestSharedSemanticClassesExistInStylesheet(t *testing.T) {
`.ui-button-sm`,
`.ui-badge-warning`,
`.ui-modal-panel`,
`.ui-space-x-md`,
`.ui-space-y-md`,
`.borderless-icon-button`,
`.ui-icon-button-solid.ui-icon-button-neutral`,
`.ui-icon-button-ghost.ui-icon-button-neutral`,
`.ui-icon-button-ghost.ui-icon-button-danger`,
`.ui-button-soft.ui-button-danger`,
} {
if !strings.Contains(css, want) {

View file

@ -29,7 +29,26 @@ type IconButtonVariant string
const (
IconButtonVariantNeutral IconButtonVariant = "neutral"
IconButtonVariantDangerGhost IconButtonVariant = "danger-ghost"
IconButtonVariantWarning IconButtonVariant = "warning"
IconButtonVariantSuccess IconButtonVariant = "success"
IconButtonVariantDanger IconButtonVariant = "danger"
)
type IconButtonTone string
const (
IconButtonToneSolid IconButtonTone = "solid"
IconButtonToneGhost IconButtonTone = "ghost"
)
type SpacingStep string
const (
SpacingStepXS SpacingStep = "xs"
SpacingStepSM SpacingStep = "sm"
SpacingStepMD SpacingStep = "md"
SpacingStepLG SpacingStep = "lg"
SpacingStepXL SpacingStep = "xl"
)
type BadgeVariant string
@ -45,12 +64,14 @@ func buttonClass(variant ButtonVariant, tone ButtonTone, size Size) string {
return "ui-button ui-button-" + string(normalizedButtonTone(tone)) + " ui-button-" + string(normalizedButtonVariant(variant)) + " ui-button-" + string(normalizedSize(size))
}
func iconButtonClass(variant IconButtonVariant) string {
switch variant {
case IconButtonVariantDangerGhost:
return "borderless-icon-button"
func iconButtonClass(variant IconButtonVariant, tone IconButtonTone) string {
normalizedVariant := normalizedIconButtonVariant(variant)
switch normalizedIconButtonTone(tone) {
case IconButtonToneGhost:
return "borderless-icon-button ui-icon-button-ghost ui-icon-button-" + string(normalizedVariant)
default:
return "ui-icon-button"
return "ui-icon-button ui-icon-button-solid ui-icon-button-" + string(normalizedVariant)
}
}
@ -58,6 +79,14 @@ func badgeClass(variant BadgeVariant) string {
return "ui-badge ui-badge-" + string(normalizedBadgeVariant(variant))
}
func spaceXClass(step SpacingStep) string {
return "ui-space-x ui-space-x-" + string(normalizedSpacingStep(step))
}
func spaceYClass(step SpacingStep) string {
return "ui-space-y ui-space-y-" + string(normalizedSpacingStep(step))
}
func normalizedSize(size Size) Size {
switch size {
case SizeSM, SizeLG:
@ -85,6 +114,33 @@ func normalizedButtonTone(tone ButtonTone) ButtonTone {
}
}
func normalizedIconButtonVariant(variant IconButtonVariant) IconButtonVariant {
switch variant {
case IconButtonVariantWarning, IconButtonVariantSuccess, IconButtonVariantDanger:
return variant
default:
return IconButtonVariantNeutral
}
}
func normalizedIconButtonTone(tone IconButtonTone) IconButtonTone {
switch tone {
case IconButtonToneGhost:
return tone
default:
return IconButtonToneSolid
}
}
func normalizedSpacingStep(step SpacingStep) SpacingStep {
switch step {
case SpacingStepXS, SpacingStepSM, SpacingStepLG, SpacingStepXL:
return step
default:
return SpacingStepMD
}
}
func normalizedBadgeVariant(variant BadgeVariant) BadgeVariant {
switch variant {
case BadgeVariantWarning, BadgeVariantSuccess, BadgeVariantDanger:

View file

@ -1,6 +1,7 @@
package views
import "strconv"
import "xtablo-backend/internal/web/ui"
templ DashboardPage(activePath string, content templ.Component) {
@DashboardPageWithMainClass(activePath, "dashboard-main flex-1 overflow-auto", content)
@ -193,7 +194,13 @@ templ OverviewHeader(displayName string) {
<div class="overview-header-actions">
<span class="overview-badge">Founder</span>
<form action="/logout" method="post" class="overview-logout-form">
<button type="submit" class="overview-logout-button">Se déconnecter</button>
@ui.Button(ui.ButtonProps{
Label: "Se déconnecter",
Variant: ui.ButtonVariantDanger,
Tone: ui.ButtonToneSoft,
Size: ui.SizeMD,
Type: "submit",
})
</form>
</div>
</div>

View file

@ -9,6 +9,7 @@ import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "strconv"
import "xtablo-backend/internal/web/ui"
func DashboardPage(activePath string, content templ.Component) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
@ -669,7 +670,7 @@ func AppSectionMainContent(title string, description string) templ.Component {
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 161, Col: 14}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 162, Col: 14}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
@ -682,7 +683,7 @@ func AppSectionMainContent(title string, description string) templ.Component {
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 162, Col: 19}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 163, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
@ -724,7 +725,7 @@ func NotFoundContent(displayName string) templ.Component {
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardGreetingName(displayName))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 182, Col: 48}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 183, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
@ -766,7 +767,7 @@ func OverviewHeader(displayName string) templ.Component {
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardTodayLabel())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 190, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 191, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
@ -779,13 +780,27 @@ func OverviewHeader(displayName string) templ.Component {
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardGreetingName(displayName))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 192, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 193, Col: 84}
}
_, 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, 28, "</span>!</h2><div class=\"overview-header-actions\"><span class=\"overview-badge\">Founder</span><form action=\"/logout\" method=\"post\" class=\"overview-logout-form\"><button type=\"submit\" class=\"overview-logout-button\">Se déconnecter</button></form></div></div></header>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</span>!</h2><div class=\"overview-header-actions\"><span class=\"overview-badge\">Founder</span><form action=\"/logout\" method=\"post\" class=\"overview-logout-form\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = ui.Button(ui.ButtonProps{
Label: "Se déconnecter",
Variant: ui.ButtonVariantDanger,
Tone: ui.ButtonToneSoft,
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, 29, "</form></div></div></header>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -814,7 +829,7 @@ func OverviewActions(actions []quickAction) templ.Component {
templ_7745c5c3_Var28 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<section class=\"overview-actions\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<section class=\"overview-actions\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -824,7 +839,7 @@ func OverviewActions(actions []quickAction) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</section>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -853,7 +868,7 @@ func OverviewProjectsSection(projects []TabloCardView) templ.Component {
templ_7745c5c3_Var29 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<section id=\"overview-projects-section\" class=\"overview-section\"><div class=\"overview-section-heading\"><h3>Mes Projets</h3></div><div class=\"project-grid\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<section id=\"overview-projects-section\" class=\"overview-section\"><div class=\"overview-section-heading\"><h3>Mes Projets</h3></div><div class=\"project-grid\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -872,7 +887,7 @@ func OverviewProjectsSection(projects []TabloCardView) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -880,7 +895,7 @@ func OverviewProjectsSection(projects []TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</section>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -914,33 +929,33 @@ func SeeMoreProjects(hiddenCount int) templ.Component {
}
ctx = templ.ClearChildren(ctx)
if hiddenCount > 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div class=\"overview-more-row\"><button type=\"button\" class=\"overview-more-button\" data-overview-see-more=\"true\" data-overview-expanded=\"false\" data-overview-hidden-count=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<div class=\"overview-more-row\"><button type=\"button\" class=\"overview-more-button\" data-overview-see-more=\"true\" data-overview-expanded=\"false\" data-overview-hidden-count=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(hiddenCount))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 240, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 247, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" data-overview-hide-label=\"Masquer\" data-overview-chevron-down=\"m6 9 6 6 6-6\" data-overview-chevron-up=\"m6 15 6-6 6 6\"><span data-overview-see-more-label=\"true\">Voir ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" data-overview-hide-label=\"Masquer\" data-overview-chevron-down=\"m6 9 6 6 6-6\" data-overview-chevron-up=\"m6 15 6-6 6 6\"><span data-overview-see-more-label=\"true\">Voir ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(hiddenCount)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 245, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 252, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " de plus</span> <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path data-overview-see-more-chevron=\"true\" d=\"m6 9 6 6 6-6\"></path></svg></button></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, " de plus</span> <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path data-overview-see-more-chevron=\"true\" d=\"m6 9 6 6 6-6\"></path></svg></button></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -970,7 +985,7 @@ func OverviewProjectsScript() templ.Component {
templ_7745c5c3_Var33 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<script>\n\t\t(function () {\n\t\t\tif (window.xtabloOverviewProjectsInitialized) return;\n\t\t\tdocument.addEventListener(\"click\", function (event) {\n\t\t\t\tvar button = event.target.closest(\"[data-overview-see-more]\");\n\t\t\t\tif (!button) return;\n\t\t\t\tvar section = button.closest(\"#overview-projects-section\");\n\t\t\t\tif (!section) return;\n\t\t\t\tvar expanded = button.getAttribute(\"data-overview-expanded\") === \"true\";\n\t\t\t\tvar label = button.querySelector(\"[data-overview-see-more-label]\");\n\t\t\t\tvar chevron = button.querySelector(\"[data-overview-see-more-chevron]\");\n\t\t\t\tvar hiddenCount = button.getAttribute(\"data-overview-hidden-count\");\n\t\t\t\tvar hideLabel = button.getAttribute(\"data-overview-hide-label\") || \"Masquer\";\n\t\t\t\tvar downChevron = button.getAttribute(\"data-overview-chevron-down\") || \"m6 9 6 6 6-6\";\n\t\t\t\tvar upChevron = button.getAttribute(\"data-overview-chevron-up\") || \"m6 15 6-6 6 6\";\n\t\t\t\tsection.querySelectorAll(\"[data-overview-project-hidden=\\\"true\\\"]\").forEach(function (project) {\n\t\t\t\t\tproject.hidden = expanded;\n\t\t\t\t\tif (expanded) {\n\t\t\t\t\t\tproject.setAttribute(\"hidden\", \"\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tproject.removeAttribute(\"hidden\");\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tbutton.setAttribute(\"data-overview-expanded\", expanded ? \"false\" : \"true\");\n\t\t\t\tif (label) {\n\t\t\t\t\tlabel.textContent = expanded ? \"Voir \" + hiddenCount + \" de plus\" : hideLabel;\n\t\t\t\t}\n\t\t\t\tif (chevron) {\n\t\t\t\t\tchevron.setAttribute(\"d\", expanded ? downChevron : upChevron);\n\t\t\t\t}\n\t\t\t});\n\t\t\twindow.xtabloOverviewProjectsInitialized = true;\n\t\t})();\n\t</script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<script>\n\t\t(function () {\n\t\t\tif (window.xtabloOverviewProjectsInitialized) return;\n\t\t\tdocument.addEventListener(\"click\", function (event) {\n\t\t\t\tvar button = event.target.closest(\"[data-overview-see-more]\");\n\t\t\t\tif (!button) return;\n\t\t\t\tvar section = button.closest(\"#overview-projects-section\");\n\t\t\t\tif (!section) return;\n\t\t\t\tvar expanded = button.getAttribute(\"data-overview-expanded\") === \"true\";\n\t\t\t\tvar label = button.querySelector(\"[data-overview-see-more-label]\");\n\t\t\t\tvar chevron = button.querySelector(\"[data-overview-see-more-chevron]\");\n\t\t\t\tvar hiddenCount = button.getAttribute(\"data-overview-hidden-count\");\n\t\t\t\tvar hideLabel = button.getAttribute(\"data-overview-hide-label\") || \"Masquer\";\n\t\t\t\tvar downChevron = button.getAttribute(\"data-overview-chevron-down\") || \"m6 9 6 6 6-6\";\n\t\t\t\tvar upChevron = button.getAttribute(\"data-overview-chevron-up\") || \"m6 15 6-6 6 6\";\n\t\t\t\tsection.querySelectorAll(\"[data-overview-project-hidden=\\\"true\\\"]\").forEach(function (project) {\n\t\t\t\t\tproject.hidden = expanded;\n\t\t\t\t\tif (expanded) {\n\t\t\t\t\t\tproject.setAttribute(\"hidden\", \"\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tproject.removeAttribute(\"hidden\");\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tbutton.setAttribute(\"data-overview-expanded\", expanded ? \"false\" : \"true\");\n\t\t\t\tif (label) {\n\t\t\t\t\tlabel.textContent = expanded ? \"Voir \" + hiddenCount + \" de plus\" : hideLabel;\n\t\t\t\t}\n\t\t\t\tif (chevron) {\n\t\t\t\t\tchevron.setAttribute(\"d\", expanded ? downChevron : upChevron);\n\t\t\t\t}\n\t\t\t});\n\t\t\twindow.xtabloOverviewProjectsInitialized = true;\n\t\t})();\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -999,7 +1014,7 @@ func OverviewTasks(tasks []dashboardTask) templ.Component {
templ_7745c5c3_Var34 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<section class=\"overview-section tasks-section\"><div class=\"tasks-section-header\"><h3>Mes Tâches</h3><button type=\"button\" class=\"tasks-add-button\"><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>Ajouter</span></button></div><div class=\"task-list\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<section class=\"overview-section tasks-section\"><div class=\"tasks-section-header\"><h3>Mes Tâches</h3><button type=\"button\" class=\"tasks-add-button\"><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>Ajouter</span></button></div><div class=\"task-list\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1009,7 +1024,7 @@ func OverviewTasks(tasks []dashboardTask) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</div></section>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1038,7 +1053,7 @@ func QuickActionCard(action quickAction) templ.Component {
templ_7745c5c3_Var35 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<button class=\"quick-action-card\" type=\"button\"><div class=\"quick-action-icon\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<button class=\"quick-action-card\" type=\"button\"><div class=\"quick-action-icon\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1046,33 +1061,33 @@ func QuickActionCard(action quickAction) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</div><div class=\"quick-action-copy\"><div class=\"quick-action-title\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div><div class=\"quick-action-copy\"><div class=\"quick-action-title\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(action.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 317, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 324, Col: 49}
}
_, 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, 42, "</div><p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(action.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 318, Col: 26}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 325, Col: 26}
}
_, 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, 43, "</p></div></button>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</p></div></button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1106,7 +1121,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1119,7 +1134,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1128,7 +1143,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<button class=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<button class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1141,7 +1156,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\" type=\"button\" aria-label=\"Marquer la tâche\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" type=\"button\" aria-label=\"Marquer la tâche\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1151,20 +1166,20 @@ func TaskRow(task dashboardTask) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</button><div class=\"task-body\"><p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</button><div class=\"task-body\"><p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(task.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 331, Col: 18}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 338, Col: 18}
}
_, 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, 49, "</p><div class=\"task-meta\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</p><div class=\"task-meta\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1173,7 +1188,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<div class=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1186,46 +1201,46 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\"><span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\"><span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var46 string
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(task.ProjectKey)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 334, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 341, Col: 28}
}
_, 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, 52, "</span></div><span class=\"task-project-name\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span></div><span class=\"task-project-name\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var47 string
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(task.Project)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 336, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 343, Col: 50}
}
_, 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, 53, "</span> <span class=\"task-date\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</span> <span class=\"task-date\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var48 string
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(task.Date)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 337, Col: 39}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 344, Col: 39}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</span></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1234,7 +1249,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<span class=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<span class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1247,20 +1262,20 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var51 string
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(task.Status)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 340, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 347, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</span></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1294,20 +1309,20 @@ func SidebarNavItem(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<div id=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var54 string
templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(sidebarNavItemID(item.Href))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 345, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 352, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\" class=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1320,33 +1335,33 @@ func SidebarNavItem(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\"><a class=\"sidebar-nav-link\" href=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\"><a class=\"sidebar-nav-link\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var56 templ.SafeURL
templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(item.Href))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 346, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 353, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" hx-get=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var57 string
templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(item.Href)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 346, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 353, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
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\"><div class=\"sidebar-nav-link-inner\"><span class=\"sidebar-nav-icon\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\"><div class=\"sidebar-nav-link-inner\"><span class=\"sidebar-nav-icon\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1354,20 +1369,20 @@ func SidebarNavItem(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</span><div class=\"sidebar-nav-label\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</span><div class=\"sidebar-nav-label\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var58 string
templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 351, Col: 47}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 358, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</div></div></a></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "</div></div></a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1401,20 +1416,20 @@ func SidebarNavItemOOB(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<div id=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var61 string
templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(sidebarNavItemID(item.Href))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 358, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 365, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\" class=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1427,33 +1442,33 @@ func SidebarNavItemOOB(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\" hx-swap-oob=\"outerHTML\"><a class=\"sidebar-nav-link\" href=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\" hx-swap-oob=\"outerHTML\"><a class=\"sidebar-nav-link\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var63 templ.SafeURL
templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(item.Href))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 359, Col: 61}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 366, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\" hx-get=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var64 string
templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(item.Href)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 359, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 366, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\"><div class=\"sidebar-nav-link-inner\"><span class=\"sidebar-nav-icon\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\"><div class=\"sidebar-nav-link-inner\"><span class=\"sidebar-nav-icon\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1461,20 +1476,20 @@ func SidebarNavItemOOB(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "</span><div class=\"sidebar-nav-label\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</span><div class=\"sidebar-nav-label\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var65 string
templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 364, Col: 47}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 371, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</div></div></a></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "</div></div></a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1503,20 +1518,20 @@ func SidebarProjectItem(item sidebarProjectItem) templ.Component {
templ_7745c5c3_Var66 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<a class=\"sidebar-project-link\" href=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "<a class=\"sidebar-project-link\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var67 templ.SafeURL
templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(item.Href))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 371, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 378, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\"><span class=\"sidebar-project-icon\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\"><span class=\"sidebar-project-icon\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1524,20 +1539,20 @@ func SidebarProjectItem(item sidebarProjectItem) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</span> <span class=\"sidebar-project-label\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</span> <span class=\"sidebar-project-label\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var68 string
templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 375, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 382, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</span></a>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "</span></a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View file

@ -113,6 +113,7 @@ func OverviewProjectsFromTablos(tablos []tablomodel.Record) []TabloCardView {
projects = append(projects, TabloCardView{
ID: tablo.ID.String(),
Name: tablo.Name,
Color: strings.TrimSpace(tablo.Color),
Status: string(tablo.Status),
StatusLabel: statusLabel,
StatusTone: statusTone,
@ -122,6 +123,7 @@ func OverviewProjectsFromTablos(tablos []tablomodel.Record) []TabloCardView {
Progress: progress,
ProgressLabel: progressPercentLabel(progress),
DeleteRequestURL: "/tablos/" + tablo.ID.String(),
EditRequestURL: "/tablos/" + tablo.ID.String() + "/edit",
})
}
return projects

View file

@ -112,8 +112,12 @@ templ TablosPageContent(vm TablosPageViewModel) {
}),
})
}
if vm.ModalOpen {
if vm.HasModal() {
if vm.IsCreateModal() {
@CreateTabloModal(vm)
} else if vm.IsEditModal() {
@EditTabloModal(vm)
}
}
</div>
}
@ -140,7 +144,8 @@ templ BorderlessDeleteButton(deleteRequestURL string) {
@ui.IconButton(ui.IconButtonProps{
Label: "Supprimer le projet",
Icon: "trash",
Variant: ui.IconButtonVariantDangerGhost,
Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-delete": deleteRequestURL,
@ -151,21 +156,40 @@ templ BorderlessDeleteButton(deleteRequestURL string) {
})
}
templ EditTabloButton(editRequestURL string) {
@ui.IconButton(ui.IconButtonProps{
Label: "Modifier le projet",
Icon: "pencil",
Variant: ui.IconButtonVariantNeutral,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-get": editRequestURL,
"hx-target": "#app-main-content",
"hx-swap": "outerHTML",
"hx-push-url": "true",
},
})
}
templ TabloGridCard(tablo TabloCardView) {
@TabloGridCardWithAttrs(tablo, nil)
}
templ TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) {
<article class="project-card" { attrs... }>
<article class="project-card" style={ projectColorVariableStyle(tablo.Color) } { attrs... }>
<div class="project-card-top">
@ui.Badge(ui.BadgeProps{
Label: tablo.StatusLabel,
Variant: badgeVariantForTone(tablo.StatusTone),
})
<div class="flex items-center gap-2">
@EditTabloButton(tablo.EditRequestURL)
@BorderlessDeleteButton(tablo.DeleteRequestURL)
</div>
</div>
<div class="project-card-title-row">
<div class={ "project-avatar " + projectAccentClass(tablo.Accent) }>
<div class="project-avatar">
<span>{ tablo.Initial }</span>
</div>
<h4>{ tablo.Name }</h4>
@ -180,17 +204,17 @@ templ TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) {
<strong>{ tablo.ProgressLabel }</strong>
</div>
<div class="project-progress-track">
<div class={ "project-progress-bar " + projectAccentClass(tablo.Accent) } style={ progressInlineStyle(tablo.Progress) }></div>
<div class="project-progress-bar" 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">
<tr class="border-t border-[#EAECF0] dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer" style={ projectColorVariableStyle(tablo.Color) }>
<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 }>
<div class="project-list-icon w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden [&>svg]:w-4 [&>svg]:h-4">
@ActionIcon(tablo.IconKind)
</div>
<span class="font-medium text-gray-900 dark:text-gray-100 truncate">{ tablo.Name }</span>
@ -211,13 +235,16 @@ templ TabloListRow(tablo TabloCardView) {
<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 class="project-progress-bar 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">
<div class="flex items-center justify-end gap-1">
@EditTabloButton(tablo.EditRequestURL)
@BorderlessDeleteButton(tablo.DeleteRequestURL)
</div>
</td>
</tr>
}
@ -269,7 +296,21 @@ templ CreateTabloModalBody(vm TablosPageViewModel) {
Placeholder: "Nom du projet",
Type: "text",
}),
Error: vm.ErrorMessage,
})
@ui.FormField(ui.FormFieldProps{
Label: "Couleur",
For: "tablo-color",
Field: ui.Input(ui.InputProps{
ID: "tablo-color",
Name: "color",
Value: vm.FormColor,
Placeholder: "#3B82F6",
Type: "text",
Attrs: templ.Attributes{
"pattern": "^#[0-9A-Fa-f]{6}$",
"autocomplete": "off",
},
}),
})
<div class="flex items-center justify-end gap-3">
<a
@ -291,3 +332,85 @@ templ CreateTabloModalBody(vm TablosPageViewModel) {
</div>
</form>
}
templ EditTabloModal(vm TablosPageViewModel) {
@ui.Modal(ui.ModalProps{
Title: "Modifier le projet",
Body: EditTabloModalBody(vm),
})
}
templ EditTabloColorField(vm TablosPageViewModel) {
<div class="flex items-center gap-3">
@ui.Input(ui.InputProps{
ID: "edit-tablo-color",
Name: "color",
Value: vm.FormColor,
Placeholder: "#3B82F6",
Type: "text",
Attrs: templ.Attributes{
"pattern": "^#[0-9A-Fa-f]{6}$",
"autocomplete": "off",
"oninput": "document.getElementById('edit-tablo-color-picker').value=this.value",
},
})
<input
id="edit-tablo-color-picker"
type="color"
value={ vm.FormColor }
class="ui-input tablo-color-picker"
oninput="document.getElementById('edit-tablo-color').value=this.value"
/>
</div>
}
templ EditTabloModalBody(vm TablosPageViewModel) {
<form
hx-post={ vm.EditSubmitHref() }
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 }/>
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: "edit-tablo-name",
Field: ui.Input(ui.InputProps{
ID: "edit-tablo-name",
Name: "name",
Value: vm.FormName,
Placeholder: "Nom du projet",
Type: "text",
}),
})
@ui.FormField(ui.FormFieldProps{
Label: "Couleur",
For: "edit-tablo-color",
Field: EditTabloColorField(vm),
Hint: "Utilisez le champ hex ou le sélecteur pour choisir une couleur au format #RRGGBB.",
})
<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-solid ui-button-neutral ui-button-md"
>
Annuler
</a>
@ui.Button(ui.ButtonProps{
Label: "Enregistrer",
Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD,
Type: "submit",
})
</div>
</form>
}

View file

@ -303,11 +303,18 @@ func TablosPageContent(vm TablosPageViewModel) templ.Component {
return templ_7745c5c3_Err
}
}
if vm.ModalOpen {
if vm.HasModal() {
if vm.IsCreateModal() {
templ_7745c5c3_Err = CreateTabloModal(vm).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if vm.IsEditModal() {
templ_7745c5c3_Err = EditTabloModal(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 {
@ -350,7 +357,7 @@ func StatusPill(vm TablosPageViewModel, status string, label string) templ.Compo
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}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 127, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@ -363,7 +370,7 @@ func StatusPill(vm TablosPageViewModel, status string, label string) templ.Compo
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}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 128, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
@ -403,7 +410,7 @@ func StatusPill(vm TablosPageViewModel, status string, label string) templ.Compo
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}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 139, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
@ -441,7 +448,8 @@ func BorderlessDeleteButton(deleteRequestURL string) templ.Component {
templ_7745c5c3_Err = ui.IconButton(ui.IconButtonProps{
Label: "Supprimer le projet",
Icon: "trash",
Variant: ui.IconButtonVariantDangerGhost,
Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-delete": deleteRequestURL,
@ -457,6 +465,47 @@ func BorderlessDeleteButton(deleteRequestURL string) templ.Component {
})
}
func EditTabloButton(editRequestURL 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_Var21 := templ.GetChildren(ctx)
if templ_7745c5c3_Var21 == nil {
templ_7745c5c3_Var21 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = ui.IconButton(ui.IconButtonProps{
Label: "Modifier le projet",
Icon: "pencil",
Variant: ui.IconButtonVariantNeutral,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-get": editRequestURL,
"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
}
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
@ -473,9 +522,9 @@ func TabloGridCard(tablo TabloCardView) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var21 := templ.GetChildren(ctx)
if templ_7745c5c3_Var21 == nil {
templ_7745c5c3_Var21 = templ.NopComponent
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
if templ_7745c5c3_Var22 == nil {
templ_7745c5c3_Var22 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = TabloGridCardWithAttrs(tablo, nil).Render(ctx, templ_7745c5c3_Buffer)
@ -502,12 +551,25 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
if templ_7745c5c3_Var22 == nil {
templ_7745c5c3_Var22 = templ.NopComponent
templ_7745c5c3_Var23 := templ.GetChildren(ctx)
if templ_7745c5c3_Var23 == nil {
templ_7745c5c3_Var23 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<article class=\"project-card\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<article class=\"project-card\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(projectColorVariableStyle(tablo.Color))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 180, Col: 77}
}
_, 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, 32, "\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -515,7 +577,7 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "><div class=\"project-card-top\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "><div class=\"project-card-top\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -526,40 +588,26 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div class=\"flex items-center gap-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = EditTabloButton(tablo.EditRequestURL).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, 33, "</div><div class=\"project-card-title-row\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 = []any{"project-avatar " + projectAccentClass(tablo.Accent)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var23).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_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"><span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div></div><div class=\"project-card-title-row\"><div class=\"project-avatar\"><span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, 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: 169, Col: 25}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 193, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
@ -572,7 +620,7 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, 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: 171, Col: 19}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 195, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
@ -593,7 +641,7 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, 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: 175, Col: 30}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 199, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
@ -606,48 +654,26 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, 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: 180, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 204, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</strong></div><div class=\"project-progress-track\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</strong></div><div class=\"project-progress-track\"><div class=\"project-progress-bar\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 = []any{"project-progress-bar " + projectAccentClass(tablo.Accent)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...)
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, 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: 207, Col: 81}
}
_, 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, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var29).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_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, 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: 183, Col: 121}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\"></div></div></div></article>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"></div></div></div></article>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -671,34 +697,25 @@ func TabloListRow(tablo TabloCardView) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var32 := templ.GetChildren(ctx)
if templ_7745c5c3_Var32 == nil {
templ_7745c5c3_Var32 = templ.NopComponent
templ_7745c5c3_Var30 := templ.GetChildren(ctx)
if templ_7745c5c3_Var30 == nil {
templ_7745c5c3_Var30 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<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\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<tr class=\"border-t border-[#EAECF0] dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var33 = []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_Var33...)
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(projectColorVariableStyle(tablo.Color))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 214, Col: 179}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var33).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_Var34))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\"><td class=\"px-6 py-4\"><div class=\"flex items-center gap-3\"><div class=\"project-list-icon w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden [&>svg]:w-4 [&>svg]:h-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -706,20 +723,20 @@ func TabloListRow(tablo TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</div><span class=\"font-medium text-gray-900 dark:text-gray-100 truncate\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</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_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name)
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, 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: 196, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 220, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</span></div></td><td class=\"px-6 py-4 whitespace-nowrap\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</span></div></td><td class=\"px-6 py-4 whitespace-nowrap\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -730,7 +747,7 @@ func TabloListRow(tablo TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</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\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</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
}
@ -738,42 +755,46 @@ func TabloListRow(tablo TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CreatedAtLabel)
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, 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: 208, Col: 26}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 232, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
_, 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, 50, "</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=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</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=\"project-progress-bar h-2 rounded-full transition-all\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(progressInlineStyle(tablo.Progress))
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, 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: 214, Col: 106}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 238, Col: 114}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
_, 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, 51, "\"></div></div><span class=\"text-sm font-semibold text-gray-900 dark:text-gray-100 w-8 text-right\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\"></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_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel)
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, 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: 216, Col: 109}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 240, Col: 109}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
_, 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, 52, "</span></div></td><td class=\"px-6 py-4 text-right\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</span></div></td><td class=\"px-6 py-4 text-right\"><div class=\"flex items-center justify-end gap-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = EditTabloButton(tablo.EditRequestURL).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -781,7 +802,7 @@ func TabloListRow(tablo TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -805,9 +826,9 @@ func CreateTabloModal(vm TablosPageViewModel) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var39 := templ.GetChildren(ctx)
if templ_7745c5c3_Var39 == nil {
templ_7745c5c3_Var39 = templ.NopComponent
templ_7745c5c3_Var36 := templ.GetChildren(ctx)
if templ_7745c5c3_Var36 == nil {
templ_7745c5c3_Var36 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = ui.Modal(ui.ModalProps{
@ -837,12 +858,12 @@ func TabloListHead() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var40 := templ.GetChildren(ctx)
if templ_7745c5c3_Var40 == nil {
templ_7745c5c3_Var40 = templ.NopComponent
templ_7745c5c3_Var37 := templ.GetChildren(ctx)
if templ_7745c5c3_Var37 == nil {
templ_7745c5c3_Var37 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<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_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<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
}
@ -866,9 +887,9 @@ func TabloListBody(tablos []TabloCardView) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var41 := templ.GetChildren(ctx)
if templ_7745c5c3_Var41 == nil {
templ_7745c5c3_Var41 = templ.NopComponent
templ_7745c5c3_Var38 := templ.GetChildren(ctx)
if templ_7745c5c3_Var38 == nil {
templ_7745c5c3_Var38 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
for _, tablo := range tablos {
@ -897,69 +918,69 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var42 := templ.GetChildren(ctx)
if templ_7745c5c3_Var42 == nil {
templ_7745c5c3_Var42 = templ.NopComponent
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, 55, "<form hx-post=\"/tablos\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" class=\"flex flex-col gap-4\"><input type=\"hidden\" name=\"view\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<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_Var40 string
templ_7745c5c3_Var40, 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: 282, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\"> <input type=\"hidden\" name=\"status\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var41 string
templ_7745c5c3_Var41, 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: 283, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\"> <input type=\"hidden\" name=\"q\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var42 string
templ_7745c5c3_Var42, 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: 284, Col: 48}
}
_, 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=\"modal\" value=\"create\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if vm.ErrorMessage != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<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_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(vm.View)
templ_7745c5c3_Var43, 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: 255, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 287, Col: 112}
}
_, 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=\"status\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var44 string
templ_7745c5c3_Var44, 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: 256, Col: 54}
}
_, 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=\"q\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var45 string
templ_7745c5c3_Var45, 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: 257, Col: 48}
}
_, 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, 58, "\"> <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, 59, "<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_Var46 string
templ_7745c5c3_Var46, 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: 260, Col: 112}
}
_, 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, 60, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -974,38 +995,55 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
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, 61, "<div class=\"flex items-center justify-end gap-3\"><a href=\"")
templ_7745c5c3_Err = ui.FormField(ui.FormFieldProps{
Label: "Couleur",
For: "tablo-color",
Field: ui.Input(ui.InputProps{
ID: "tablo-color",
Name: "color",
Value: vm.FormColor,
Placeholder: "#3B82F6",
Type: "text",
Attrs: templ.Attributes{
"pattern": "^#[0-9A-Fa-f]{6}$",
"autocomplete": "off",
},
}),
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var47 templ.SafeURL
templ_7745c5c3_Var47, 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: 276, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<div class=\"flex items-center justify-end gap-3\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" hx-get=\"")
var templ_7745c5c3_Var44 templ.SafeURL
templ_7745c5c3_Var44, 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: 317, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var48 string
templ_7745c5c3_Var48, 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: 277, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"ui-button ui-button-solid ui-button-neutral ui-button-md\">Annuler</a>")
var templ_7745c5c3_Var45 string
templ_7745c5c3_Var45, 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: 318, Col: 32}
}
_, 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, 60, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"ui-button ui-button-solid ui-button-neutral ui-button-md\">Annuler</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -1018,7 +1056,266 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</div></form>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func EditTabloModal(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_Var46 := templ.GetChildren(ctx)
if templ_7745c5c3_Var46 == nil {
templ_7745c5c3_Var46 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = ui.Modal(ui.ModalProps{
Title: "Modifier le projet",
Body: EditTabloModalBody(vm),
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func EditTabloColorField(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_Var47 := templ.GetChildren(ctx)
if templ_7745c5c3_Var47 == nil {
templ_7745c5c3_Var47 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<div class=\"flex items-center gap-3\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = ui.Input(ui.InputProps{
ID: "edit-tablo-color",
Name: "color",
Value: vm.FormColor,
Placeholder: "#3B82F6",
Type: "text",
Attrs: templ.Attributes{
"pattern": "^#[0-9A-Fa-f]{6}$",
"autocomplete": "off",
"oninput": "document.getElementById('edit-tablo-color-picker').value=this.value",
},
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<input id=\"edit-tablo-color-picker\" type=\"color\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var48 string
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(vm.FormColor)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 360, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\" class=\"ui-input tablo-color-picker\" oninput=\"document.getElementById('edit-tablo-color').value=this.value\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func EditTabloModalBody(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_Var49 := templ.GetChildren(ctx)
if templ_7745c5c3_Var49 == nil {
templ_7745c5c3_Var49 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<form hx-post=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var50 string
templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(vm.EditSubmitHref())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 369, Col: 31}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\" 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_Var51 string
templ_7745c5c3_Var51, 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: 374, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\"> <input type=\"hidden\" name=\"status\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var52 string
templ_7745c5c3_Var52, 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: 375, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\"> <input type=\"hidden\" name=\"q\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var53 string
templ_7745c5c3_Var53, 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: 376, Col: 48}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if vm.ErrorMessage != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<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_Var54 string
templ_7745c5c3_Var54, 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: 378, Col: 112}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = ui.FormField(ui.FormFieldProps{
Label: "Nom du projet",
For: "edit-tablo-name",
Field: ui.Input(ui.InputProps{
ID: "edit-tablo-name",
Name: "name",
Value: vm.FormName,
Placeholder: "Nom du projet",
Type: "text",
}),
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = ui.FormField(ui.FormFieldProps{
Label: "Couleur",
For: "edit-tablo-color",
Field: EditTabloColorField(vm),
Hint: "Utilisez le champ hex ou le sélecteur pour choisir une couleur au format #RRGGBB.",
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "<div class=\"flex items-center justify-end gap-3\"><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var55 templ.SafeURL
templ_7745c5c3_Var55, 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: 399, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var56 string
templ_7745c5c3_Var56, 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: 400, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" hx-target=\"#app-main-content\" hx-swap=\"outerHTML\" hx-push-url=\"true\" class=\"ui-button ui-button-solid ui-button-neutral ui-button-md\">Annuler</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = ui.Button(ui.ButtonProps{
Label: "Enregistrer",
Variant: ui.ButtonVariantDefault,
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, 75, "</div></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View file

@ -5,12 +5,14 @@ import (
"net/url"
"strings"
"github.com/a-h/templ"
"xtablo-backend/internal/web/ui"
)
type TabloCardView struct {
ID string
Name string
Color string
Status string
StatusLabel string
StatusClass string
@ -21,6 +23,7 @@ type TabloCardView struct {
ProgressLabel string
DeleteURL string
DeleteRequestURL string
EditRequestURL string
IconKind string
IconBgClass string
IconFgClass string
@ -33,20 +36,24 @@ type TablosPageViewModel struct {
View string
Query string
Status string
ModalOpen bool
ModalKind string
EditingTabloID string
FormName string
FormColor string
ErrorMessage string
Tablos []TabloCardView
}
func NewTablosPageViewModel(displayName string, view string, query string, status string, modalOpen bool, formName string, errorMessage string, tablos []TabloCardView) TablosPageViewModel {
func NewTablosPageViewModel(displayName string, view string, query string, status string, modalKind string, editingTabloID string, formName string, formColor string, errorMessage string, tablos []TabloCardView) TablosPageViewModel {
return TablosPageViewModel{
DisplayName: displayName,
View: normalizedView(view),
Query: strings.TrimSpace(query),
Status: normalizedStatus(status),
ModalOpen: modalOpen,
ModalKind: normalizedModalKind(modalKind),
EditingTabloID: strings.TrimSpace(editingTabloID),
FormName: strings.TrimSpace(formName),
FormColor: normalizedFormColor(modalKind, formColor),
ErrorMessage: strings.TrimSpace(errorMessage),
Tablos: tablos,
}
@ -60,6 +67,18 @@ func (vm TablosPageViewModel) HasTablos() bool {
return len(vm.Tablos) > 0
}
func (vm TablosPageViewModel) HasModal() bool {
return vm.ModalKind != ""
}
func (vm TablosPageViewModel) IsCreateModal() bool {
return vm.ModalKind == "create"
}
func (vm TablosPageViewModel) IsEditModal() bool {
return vm.ModalKind == "edit"
}
func (vm TablosPageViewModel) StatusHref(status string) string {
values := vm.baseValues()
values.Set("status", normalizedStatus(status))
@ -94,11 +113,20 @@ func (vm TablosPageViewModel) CreateModalHref() string {
return "/tablos?" + values.Encode()
}
func (vm TablosPageViewModel) EditModalHref(tabloID string) string {
values := vm.baseValues()
return "/tablos/" + strings.TrimSpace(tabloID) + "/edit?" + values.Encode()
}
func (vm TablosPageViewModel) CloseModalHref() string {
values := vm.baseValues()
return "/tablos?" + values.Encode()
}
func (vm TablosPageViewModel) EditSubmitHref() string {
return "/tablos/" + vm.EditingTabloID
}
func (vm TablosPageViewModel) HasSearch() bool {
return vm.Query != ""
}
@ -119,6 +147,26 @@ func normalizedStatus(status string) string {
}
}
func normalizedModalKind(kind string) string {
switch kind {
case "create", "edit":
return kind
default:
return ""
}
}
func normalizedFormColor(modalKind string, color string) string {
trimmed := strings.TrimSpace(color)
if trimmed != "" {
return trimmed
}
if normalizedModalKind(modalKind) == "create" {
return "#3B82F6"
}
return ""
}
func (vm TablosPageViewModel) baseValues() url.Values {
values := url.Values{}
values.Set("view", vm.View)
@ -159,3 +207,11 @@ func badgeVariantForTone(tone string) ui.BadgeVariant {
return ui.BadgeVariantInfo
}
}
func projectColorVariableStyle(color string) templ.SafeCSS {
return templ.SanitizeCSS("--project-color", templ.SafeCSSProperty(strings.TrimSpace(color)))
}
func backgroundColorStyle(color string) templ.SafeCSS {
return templ.SanitizeCSS("background-color", templ.SafeCSSProperty(strings.TrimSpace(color)))
}

View file

@ -38,6 +38,8 @@ func newRouterWithHandler(authHandler *handlers.AuthHandler) http.Handler {
mux.Get("/files", authHandler.GetFilesPage())
mux.Get("/feedback", authHandler.GetFeedbackPage())
mux.Post("/tablos", authHandler.PostTablos())
mux.Get("/tablos/{tabloID}/edit", authHandler.GetEditTabloModal())
mux.Post("/tablos/{tabloID}", authHandler.PostTabloUpdate())
mux.Delete("/tablos/{tabloID}", authHandler.DeleteTablo())
mux.Get("/login", authHandler.GetLoginPage())
mux.Get("/signup", authHandler.GetSignupPage())

View file

@ -983,7 +983,7 @@ input {
}
.project-card-top {
align-items: flex-start;
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
@ -1468,6 +1468,19 @@ input {
display: inline-flex;
}
.catalog-spacing-row {
align-items: center;
display: flex;
gap: 0;
}
.catalog-spacing-column {
display: flex;
flex-direction: column;
gap: 0;
width: 100%;
}
.catalog-example-snippet {
background: #111827;
border-radius: 0.875rem;
@ -1511,7 +1524,6 @@ input {
background: transparent;
border: 0;
border-radius: 0.5rem;
color: #6b7280;
cursor: pointer;
display: inline-flex;
justify-content: center;
@ -1523,11 +1535,64 @@ input {
color 0.2s ease;
}
.ui-icon-button:hover {
.ui-icon-button-solid.ui-icon-button-neutral {
color: #6b7280;
}
.ui-icon-button-solid.ui-icon-button-neutral:hover {
background: #f9fafb;
color: #111827;
}
.ui-space-x {
display: inline-block;
flex-shrink: 0;
}
.ui-space-y {
display: block;
}
.ui-space-x-xs {
width: 0.25rem;
}
.ui-space-x-sm {
width: 0.5rem;
}
.ui-space-x-md {
width: 0.75rem;
}
.ui-space-x-lg {
width: 1rem;
}
.ui-space-x-xl {
width: 1.5rem;
}
.ui-space-y-xs {
height: 0.25rem;
}
.ui-space-y-sm {
height: 0.5rem;
}
.ui-space-y-md {
height: 0.75rem;
}
.ui-space-y-lg {
height: 1rem;
}
.ui-space-y-xl {
height: 1.5rem;
}
.ui-modal-backdrop {
align-items: center;
background: rgba(17, 24, 39, 0.52);
@ -1587,16 +1652,24 @@ input {
border: 0;
box-shadow: none;
appearance: none;
color: #9ca3af;
cursor: pointer;
outline: none;
}
.ui-icon-button-ghost.ui-icon-button-neutral,
.ui-icon-button-ghost.ui-icon-button-danger {
color: #9ca3af;
}
.project-card-top .borderless-icon-button {
padding: 0;
}
.project-card-top .borderless-icon-button:hover {
.project-card-top .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-neutral:hover {
color: #111827;
}
.project-card-top .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-danger:hover {
color: #ef4444;
}
@ -1621,7 +1694,11 @@ td.text-right .borderless-icon-button {
transition: color 0.2s;
}
td.text-right .borderless-icon-button:hover {
td.text-right .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-neutral:hover {
color: #111827;
}
td.text-right .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-danger:hover {
color: #ef4444;
}
@ -1634,6 +1711,7 @@ td.text-right .borderless-icon-button:hover {
.project-avatar {
align-items: center;
background: var(--project-color, #3b82f6);
border-radius: 0.85rem;
color: #fff;
display: inline-flex;
@ -1645,6 +1723,11 @@ td.text-right .borderless-icon-button:hover {
width: 3rem;
}
.project-list-icon {
background: var(--project-color, #3b82f6);
color: #fff;
}
.project-accent-blue {
background: #3b82f6;
}
@ -1697,10 +1780,17 @@ td.text-right .borderless-icon-button:hover {
}
.project-progress-bar {
background: var(--project-color, #3b82f6);
border-radius: 999px;
height: 100%;
}
.tablo-color-picker {
max-width: 5rem;
min-height: 44px;
padding: 0.4rem;
}
.overview-more-row {
display: flex;
justify-content: center;

View file

@ -7,7 +7,6 @@
--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);
@ -60,6 +59,9 @@
.absolute {
position: absolute;
}
.fixed {
position: fixed;
}
.relative {
position: relative;
}
@ -199,6 +201,9 @@
.justify-end {
justify-content: flex-end;
}
.gap-1 {
gap: calc(var(--spacing) * 1);
}
.gap-1\.5 {
gap: calc(var(--spacing) * 1.5);
}
@ -298,9 +303,6 @@
.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);
}