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"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <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> </body>
</html> </html>

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <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;, Label: &#34;Nouveau projet&#34;,
Variant: ui.ButtonVariantDefault, Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD, Size: ui.SizeMD,

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <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;), Header: textComponent(&#34;Header&#34;),
Body: textComponent(&#34;Body&#34;), Body: textComponent(&#34;Body&#34;),
Footer: textComponent(&#34;Footer&#34;), Footer: textComponent(&#34;Footer&#34;),

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <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;, Title: &#34;Aucun projet trouvé&#34;,
Description: &#34;Créez votre premier projet&#34;, Description: &#34;Créez votre premier projet&#34;,
Icon: ui.UIIcon(&#34;grid3x3&#34;), Icon: ui.UIIcon(&#34;grid3x3&#34;),

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <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;, Label: &#34;Nom&#34;,
For: &#34;catalog-name&#34;, For: &#34;catalog-name&#34;,
Field: ui.Input(ui.InputProps{ Field: ui.Input(ui.InputProps{

View file

@ -8,10 +8,17 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <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;, Label: &#34;Supprimer le projet&#34;,
Icon: &#34;trash&#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;, Type: &#34;button&#34;,
})</code></pre></section></div></main> })</code></pre></section></div></main>
</body> </body>

View file

@ -8,6 +8,6 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <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> </body>
</html> </html>

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <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;, Name: &#34;name&#34;,
Value: &#34;Projet Atlas&#34;, Value: &#34;Projet Atlas&#34;,
Placeholder: &#34;Nom du projet&#34;, Placeholder: &#34;Nom du projet&#34;,

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <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;, Title: &#34;Créer un projet&#34;,
Body: ui.FormField(...), Body: ui.FormField(...),
Actions: ui.Button(...), Actions: ui.Button(...),

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <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(), Head: TabloListHead(),
Body: TabloListBody(tablos), Body: TabloListBody(tablos),
})</code></pre></section></div></main> })</code></pre></section></div></main>

View file

@ -8,6 +8,6 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css"> <link rel="stylesheet" href="../../go-backend/static/styles.css">
</head> </head>
<body> <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> </body>
</html> </html>

View file

@ -33,7 +33,6 @@ This intentionally does not start the broader `status` deprecation effort. `stat
- Task-derived status inference - Task-derived status inference
- Reworking the current search or filter model - Reworking the current search or filter model
- Introducing custom JavaScript beyond the existing HTMX-driven pattern - 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 - Building a `/tablos/:id` detail edit page
**User Experience** **User Experience**
@ -46,9 +45,10 @@ On the Go backend `Mes Projets` page:
- the modal lets the user update: - the modal lets the user update:
- `name` - `name`
- `color` - `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 - `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** **Data Model**
@ -152,6 +152,8 @@ Modal behavior:
- create modal collects `name` and `color` - create modal collects `name` and `color`
- edit 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 - both modals render inline validation errors
- cancel closes the modal and preserves current page state - 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. 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** **Error Handling**
Create or update validation failure: Create or update validation failure:
@ -254,10 +263,11 @@ Repository coverage:
Handler coverage: Handler coverage:
- `GET /tablos` create modal includes `color` field - `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` rejects missing or invalid `color`
- `POST /tablos/{id}` rejects missing or invalid `color` - `POST /tablos/{id}` rejects missing or invalid `color`
- `POST /tablos/{id}` updates visible name and color in returned HTML - `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 - grid card markup contains edit action before delete
- list row 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 exists with the expected icon/button semantics
- the edit trigger appears before delete in the rendered action area - the edit trigger appears before delete in the rendered action area
- the modal contains both `Nom du projet` and `Couleur` - 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` - invalid hex values return a `422` response with inline feedback mentioning `#RRGGBB`
**Implementation Notes** **Implementation Notes**
@ -289,6 +300,7 @@ The feature is complete when:
- clicking edit opens a modal for the selected tablo - clicking edit opens a modal for the selected tablo
- the modal allows changing the tablo `name` - the modal allows changing the tablo `name`
- the modal allows changing the tablo `color` - the modal allows changing the tablo `color`
- the edit modal includes a color picker for choosing the tablo color
- create also accepts `color` - create also accepts `color`
- `color` only accepts full 6-digit hex values like `#3B82F6` - `color` only accepts full 6-digit hex values like `#3B82F6`
- successful edits update the rendered project card or list row - successful edits update the rendered project card or list row

View file

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

View file

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

View file

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

View file

@ -32,6 +32,7 @@ CREATE TABLE IF NOT EXISTS public.tablos (
id uuid PRIMARY KEY, id uuid PRIMARY KEY,
owner_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, owner_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
name text NOT NULL, name text NOT NULL,
color text NOT NULL,
status text NOT NULL, status text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(), created_at timestamptz NOT NULL DEFAULT now(),
updated_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"` ID uuid.UUID `db:"id"`
OwnerID uuid.UUID `db:"owner_id"` OwnerID uuid.UUID `db:"owner_id"`
Name string `db:"name"` Name string `db:"name"`
Color string `db:"color"`
Status string `db:"status"` Status string `db:"status"`
CreatedAt pgtype.Timestamptz `db:"created_at"` CreatedAt pgtype.Timestamptz `db:"created_at"`
UpdatedAt pgtype.Timestamptz `db:"updated_at"` UpdatedAt pgtype.Timestamptz `db:"updated_at"`

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"regexp"
"strings" "strings"
"time" "time"
@ -16,6 +17,11 @@ import (
var ErrTabloNotFound = tablomodel.ErrNotFound 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 type TabloStatus = tablomodel.Status
const ( const (
@ -26,13 +32,15 @@ const (
type TabloRecord = tablomodel.Record type TabloRecord = tablomodel.Record
type CreateTabloInput = tablomodel.CreateInput type CreateTabloInput = tablomodel.CreateInput
type UpdateTabloInput = tablomodel.UpdateInput
type ListTablosInput = tablomodel.ListInput type ListTablosInput = tablomodel.ListInput
type TablosPageState struct { type TablosPageState struct {
View string View string
Query string Query string
Status string Status string
ModalOpen bool ModalKind string
EditingTabloID string
} }
func normalizeTabloQuery(query string) string { func normalizeTabloQuery(query string) string {
@ -58,7 +66,7 @@ func parseTablosPageState(values interface {
View: view, View: view,
Query: strings.TrimSpace(values.Get("q")), Query: strings.TrimSpace(values.Get("q")),
Status: status, 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 { func (h *AuthHandler) PostTablos() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
user, ok := h.authenticatedUser(r.Context(), r) user, ok := h.authenticatedUser(r.Context(), r)
@ -92,35 +124,153 @@ func (h *AuthHandler) PostTablos() http.HandlerFunc {
} }
state := parseTablosPageState(r.Form) state := parseTablosPageState(r.Form)
state.ModalOpen = true state.ModalKind = "create"
name := strings.TrimSpace(r.FormValue("name")) name := strings.TrimSpace(r.FormValue("name"))
if 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 return
} }
if _, err := h.repo.CreateTablo(r.Context(), CreateTabloInput{ if _, err := h.repo.CreateTablo(r.Context(), CreateTabloInput{
OwnerID: user.ID, OwnerID: user.ID,
Name: name, Name: name,
Color: color,
Status: TabloStatusTodo, Status: TabloStatusTodo,
}); err != nil { }); err != nil {
http.Error(w, "failed to create tablo", http.StatusInternalServerError) http.Error(w, "failed to create tablo", http.StatusInternalServerError)
return return
} }
state.ModalOpen = false state.ModalKind = ""
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{ tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
OwnerID: user.ID,
Query: state.Query,
Status: state.statusFilter(),
})
if err != nil { if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError) http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return 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()) state := parseTablosPageState(r.URL.Query())
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{ tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
OwnerID: user.ID,
Query: state.Query,
Status: state.statusFilter(),
})
if err != nil { if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError) http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return 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()) state := parseTablosPageState(r.URL.Query())
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{ tablos, err := listTablosForState(r.Context(), h.repo, user.ID, state)
OwnerID: user.ID,
Query: state.Query,
Status: state.statusFilter(),
})
if err != nil { if err != nil {
http.Error(w, "failed to list tablos", http.StatusInternalServerError) http.Error(w, "failed to list tablos", http.StatusInternalServerError)
return 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( return views.NewTablosPageViewModel(
user.DisplayName, user.DisplayName,
state.View, state.View,
state.Query, state.Query,
state.Status, state.Status,
state.ModalOpen, state.ModalKind,
state.EditingTabloID,
formName, formName,
formColor,
errorMessage, errorMessage,
buildTabloCardViews(tablos, state), 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) { 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.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
@ -221,6 +382,7 @@ func (r *InMemoryAuthRepository) CreateTablo(_ context.Context, input CreateTabl
ID: uuid.New(), ID: uuid.New(),
OwnerID: input.OwnerID, OwnerID: input.OwnerID,
Name: strings.TrimSpace(input.Name), Name: strings.TrimSpace(input.Name),
Color: storedTabloColor(input.Color),
Status: input.Status, Status: input.Status,
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
@ -258,6 +420,22 @@ func (r *InMemoryAuthRepository) ListTablos(_ context.Context, input ListTablosI
return tablos, nil 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 { func (r *InMemoryAuthRepository) SoftDeleteTablo(_ context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@ -293,6 +471,7 @@ func buildTabloCardViews(tablos []TabloRecord, state TablosPageState) []views.Ta
items = append(items, views.TabloCardView{ items = append(items, views.TabloCardView{
ID: tablo.ID.String(), ID: tablo.ID.String(),
Name: tablo.Name, Name: tablo.Name,
Color: storedTabloColor(tablo.Color),
Status: string(tablo.Status), Status: string(tablo.Status),
StatusLabel: statusLabel, StatusLabel: statusLabel,
StatusClass: statusClass, StatusClass: statusClass,
@ -303,6 +482,7 @@ func buildTabloCardViews(tablos []TabloRecord, state TablosPageState) []views.Ta
ProgressLabel: fmt.Sprintf("%d%%", progress), ProgressLabel: fmt.Sprintf("%d%%", progress),
DeleteURL: "/tablos/" + tablo.ID.String(), DeleteURL: "/tablos/" + tablo.ID.String(),
DeleteRequestURL: buildDeleteRequestURL("/tablos/"+tablo.ID.String(), state), DeleteRequestURL: buildDeleteRequestURL("/tablos/"+tablo.ID.String(), state),
EditRequestURL: buildEditRequestURL("/tablos/"+tablo.ID.String()+"/edit", state),
IconKind: iconKind, IconKind: iconKind,
IconBgClass: bgClass, IconBgClass: bgClass,
IconFgClass: fgClass, IconFgClass: fgClass,
@ -314,6 +494,14 @@ func buildTabloCardViews(tablos []TabloRecord, state TablosPageState) []views.Ta
} }
func buildDeleteRequestURL(path string, state TablosPageState) string { 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 := url.Values{}
values.Set("view", state.View) values.Set("view", state.View)
values.Set("status", state.Status) 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) { func TestGetTablosPageDefaultsToGridAndAllStatus(t *testing.T) {
handler := newTestAuthHandler(t) handler := newTestAuthHandler(t)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo") sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
@ -376,7 +398,7 @@ func TestGetTablosPageListViewUsesDirectTableIconMarkup(t *testing.T) {
body := rec.Body.String() body := rec.Body.String()
for _, want := range []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="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"`, `class="lucide lucide-trash2 w-4 h-4"`,
} { } {
if !strings.Contains(body, want) { if !strings.Contains(body, want) {
@ -454,11 +476,12 @@ func TestGetTablosPageGridUsesProjectCardMarkup(t *testing.T) {
body := rec.Body.String() body := rec.Body.String()
for _, want := range []string{ for _, want := range []string{
`<article class="project-card">`, `<article class="project-card" style="--project-color:`,
`class="project-card-top"`, `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-card-title-row"`,
`class="project-avatar project-accent-`, `class="project-avatar"`,
`class="project-date-row"`, `class="project-date-row"`,
`class="project-progress-track"`, `class="project-progress-track"`,
} { } {
@ -474,6 +497,7 @@ func TestPostTablosCreatesTodoTablo(t *testing.T) {
form := url.Values{} form := url.Values{}
form.Set("name", "Roadmap") form.Set("name", "Roadmap")
form.Set("color", "#3B82F6")
form.Set("view", "grid") form.Set("view", "grid")
form.Set("status", "all") 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) { func TestDeleteTabloSoftDeletesOwnedRow(t *testing.T) {
repo := NewInMemoryAuthRepository() repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo) handler := NewAuthHandler(repo)

View file

@ -36,14 +36,13 @@ func TestPagesIncludeTokensAndButtons(t *testing.T) {
} }
func TestPagesIncludePrimitiveCatalogCoverage(t *testing.T) { func TestPagesIncludePrimitiveCatalogCoverage(t *testing.T) {
pages := Pages()
for _, slug := range []string{ for _, slug := range []string{
"badges", "badges",
"icon-buttons", "icon-buttons",
"inputs", "inputs",
"form-fields", "form-fields",
"modals", "modals",
"spacing",
"tables", "tables",
"empty-states", "empty-states",
"cards", "cards",
@ -52,10 +51,6 @@ func TestPagesIncludePrimitiveCatalogCoverage(t *testing.T) {
t.Fatalf("expected catalog page %q", slug) 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) { 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 { func renderToString(t *testing.T, component templ.Component) string {
t.Helper() t.Helper()

View file

@ -123,13 +123,33 @@ func iconButtonExamples() []Example {
Preview: ui.IconButton(ui.IconButtonProps{ Preview: ui.IconButton(ui.IconButtonProps{
Label: "Supprimer le projet", Label: "Supprimer le projet",
Icon: "trash", Icon: "trash",
Variant: ui.IconButtonVariantDangerGhost, Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button", Type: "button",
}), }),
Snippet: `@ui.IconButton(ui.IconButtonProps{ Snippet: `@ui.IconButton(ui.IconButtonProps{
Label: "Supprimer le projet", Label: "Supprimer le projet",
Icon: "trash", 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", 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 { func tableExamples() []Example {
return []Example{ return []Example{
{ {

View file

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

View file

@ -4,12 +4,13 @@ type IconButtonProps struct {
Label string Label string
Icon string Icon string
Variant IconButtonVariant Variant IconButtonVariant
Tone IconButtonTone
Type string Type string
Attrs templ.Attributes Attrs templ.Attributes
} }
templ IconButton(props IconButtonProps) { 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) @UIIcon(props.Icon)
</button> </button>
} }
@ -54,6 +55,11 @@ templ UIIcon(kind string) {
<rect width="18" height="18" x="3" y="4" rx="2"></rect> <rect width="18" height="18" x="3" y="4" rx="2"></rect>
<path d="M3 10h18"></path> <path d="M3 10h18"></path>
</svg> </svg>
case "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": 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"> <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="M3 6h18"></path>

View file

@ -12,6 +12,7 @@ type IconButtonProps struct {
Label string Label string
Icon string Icon string
Variant IconButtonVariant Variant IconButtonVariant
Tone IconButtonTone
Type string Type string
Attrs templ.Attributes Attrs templ.Attributes
} }
@ -37,7 +38,7 @@ func IconButton(props IconButtonProps) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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...) templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
@ -49,7 +50,7 @@ func IconButton(props IconButtonProps) templ.Component {
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(buttonType(props.Type)) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(buttonType(props.Type))
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -75,7 +76,7 @@ func IconButton(props IconButtonProps) templ.Component {
var templ_7745c5c3_Var5 string var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label) templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -157,26 +158,31 @@ func UIIcon(kind string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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": 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
default: 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(kind) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(kind)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View file

@ -40,7 +40,8 @@ func TestIconButtonRendersBorderlessDestructiveMarkup(t *testing.T) {
component := IconButton(IconButtonProps{ component := IconButton(IconButtonProps{
Label: "Supprimer le projet", Label: "Supprimer le projet",
Icon: "trash", Icon: "trash",
Variant: IconButtonVariantDangerGhost, Variant: IconButtonVariantDanger,
Tone: IconButtonToneGhost,
Type: "button", Type: "button",
}) })
@ -50,6 +51,8 @@ func TestIconButtonRendersBorderlessDestructiveMarkup(t *testing.T) {
`type="button"`, `type="button"`,
`aria-label="Supprimer le projet"`, `aria-label="Supprimer le projet"`,
`borderless-icon-button`, `borderless-icon-button`,
`ui-icon-button-ghost`,
`ui-icon-button-danger`,
`lucide-trash2`, `lucide-trash2`,
} { } {
if !strings.Contains(html, want) { 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) { func TestBadgeRendersSemanticStatusVariant(t *testing.T) {
component := Badge(BadgeProps{ component := Badge(BadgeProps{
Label: "En cours", Label: "En cours",
@ -79,8 +107,8 @@ func TestBadgeRendersSemanticStatusVariant(t *testing.T) {
func TestModalRendersShellStructure(t *testing.T) { func TestModalRendersShellStructure(t *testing.T) {
component := Modal(ModalProps{ component := Modal(ModalProps{
Title: "Nouveau projet", Title: "Nouveau projet",
Body: textComponent("Body copy"), Body: textComponent("Body copy"),
Actions: textComponent("Actions"), Actions: textComponent("Actions"),
}) })
@ -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) { func TestButtonUsesSharedTokenBackedClasses(t *testing.T) {
component := Button(ButtonProps{ component := Button(ButtonProps{
Label: "Create", Label: "Create",
@ -134,7 +194,12 @@ func TestSharedSemanticClassesExistInStylesheet(t *testing.T) {
`.ui-button-sm`, `.ui-button-sm`,
`.ui-badge-warning`, `.ui-badge-warning`,
`.ui-modal-panel`, `.ui-modal-panel`,
`.ui-space-x-md`,
`.ui-space-y-md`,
`.borderless-icon-button`, `.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`, `.ui-button-soft.ui-button-danger`,
} { } {
if !strings.Contains(css, want) { if !strings.Contains(css, want) {

View file

@ -28,8 +28,27 @@ const (
type IconButtonVariant string type IconButtonVariant string
const ( const (
IconButtonVariantNeutral IconButtonVariant = "neutral" 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 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)) return "ui-button ui-button-" + string(normalizedButtonTone(tone)) + " ui-button-" + string(normalizedButtonVariant(variant)) + " ui-button-" + string(normalizedSize(size))
} }
func iconButtonClass(variant IconButtonVariant) string { func iconButtonClass(variant IconButtonVariant, tone IconButtonTone) string {
switch variant { normalizedVariant := normalizedIconButtonVariant(variant)
case IconButtonVariantDangerGhost:
return "borderless-icon-button" switch normalizedIconButtonTone(tone) {
case IconButtonToneGhost:
return "borderless-icon-button ui-icon-button-ghost ui-icon-button-" + string(normalizedVariant)
default: 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)) 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 { func normalizedSize(size Size) Size {
switch size { switch size {
case SizeSM, SizeLG: 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 { func normalizedBadgeVariant(variant BadgeVariant) BadgeVariant {
switch variant { switch variant {
case BadgeVariantWarning, BadgeVariantSuccess, BadgeVariantDanger: case BadgeVariantWarning, BadgeVariantSuccess, BadgeVariantDanger:

View file

@ -1,6 +1,7 @@
package views package views
import "strconv" import "strconv"
import "xtablo-backend/internal/web/ui"
templ DashboardPage(activePath string, content templ.Component) { templ DashboardPage(activePath string, content templ.Component) {
@DashboardPageWithMainClass(activePath, "dashboard-main flex-1 overflow-auto", content) @DashboardPageWithMainClass(activePath, "dashboard-main flex-1 overflow-auto", content)
@ -193,7 +194,13 @@ templ OverviewHeader(displayName string) {
<div class="overview-header-actions"> <div class="overview-header-actions">
<span class="overview-badge">Founder</span> <span class="overview-badge">Founder</span>
<form action="/logout" method="post" class="overview-logout-form"> <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> </form>
</div> </div>
</div> </div>
@ -220,7 +227,7 @@ templ OverviewProjectsSection(projects []TabloCardView) {
for _, project := range hiddenOverviewProjects(projects) { for _, project := range hiddenOverviewProjects(projects) {
@TabloGridCardWithAttrs(project, templ.Attributes{ @TabloGridCardWithAttrs(project, templ.Attributes{
"data-overview-project-hidden": "true", "data-overview-project-hidden": "true",
"hidden": true, "hidden": true,
}) })
} }
</div> </div>

View file

@ -9,6 +9,7 @@ import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime" import templruntime "github.com/a-h/templ/runtime"
import "strconv" import "strconv"
import "xtablo-backend/internal/web/ui"
func DashboardPage(activePath string, content templ.Component) templ.Component { func DashboardPage(activePath string, content templ.Component) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(title) templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -682,7 +683,7 @@ func AppSectionMainContent(title string, description string) templ.Component {
var templ_7745c5c3_Var22 string var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(description) templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(description)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -724,7 +725,7 @@ func NotFoundContent(displayName string) templ.Component {
var templ_7745c5c3_Var24 string var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardGreetingName(displayName)) templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardGreetingName(displayName))
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -766,7 +767,7 @@ func OverviewHeader(displayName string) templ.Component {
var templ_7745c5c3_Var26 string var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardTodayLabel()) templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardTodayLabel())
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -779,13 +780,27 @@ func OverviewHeader(displayName string) templ.Component {
var templ_7745c5c3_Var27 string var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardGreetingName(displayName)) templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardGreetingName(displayName))
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -814,7 +829,7 @@ func OverviewActions(actions []quickAction) templ.Component {
templ_7745c5c3_Var28 = templ.NopComponent templ_7745c5c3_Var28 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -824,7 +839,7 @@ func OverviewActions(actions []quickAction) templ.Component {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -853,7 +868,7 @@ func OverviewProjectsSection(projects []TabloCardView) templ.Component {
templ_7745c5c3_Var29 = templ.NopComponent templ_7745c5c3_Var29 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -872,7 +887,7 @@ func OverviewProjectsSection(projects []TabloCardView) templ.Component {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -880,7 +895,7 @@ func OverviewProjectsSection(projects []TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -914,33 +929,33 @@ func SeeMoreProjects(hiddenCount int) templ.Component {
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
if hiddenCount > 0 { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var31 string var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(hiddenCount)) templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(hiddenCount))
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var32 string var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(hiddenCount) templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(hiddenCount)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -970,7 +985,7 @@ func OverviewProjectsScript() templ.Component {
templ_7745c5c3_Var33 = templ.NopComponent templ_7745c5c3_Var33 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -999,7 +1014,7 @@ func OverviewTasks(tasks []dashboardTask) templ.Component {
templ_7745c5c3_Var34 = templ.NopComponent templ_7745c5c3_Var34 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1009,7 +1024,7 @@ func OverviewTasks(tasks []dashboardTask) templ.Component {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1038,7 +1053,7 @@ func QuickActionCard(action quickAction) templ.Component {
templ_7745c5c3_Var35 = templ.NopComponent templ_7745c5c3_Var35 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1046,33 +1061,33 @@ func QuickActionCard(action quickAction) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var36 string var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(action.Title) templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(action.Title)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var37 string var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(action.Description) templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(action.Description)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1106,7 +1121,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1119,7 +1134,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1128,7 +1143,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1141,7 +1156,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1151,20 +1166,20 @@ func TaskRow(task dashboardTask) templ.Component {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var43 string var templ_7745c5c3_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(task.Title) templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(task.Title)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1173,7 +1188,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1186,46 +1201,46 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var46 string var templ_7745c5c3_Var46 string
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(task.ProjectKey) templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(task.ProjectKey)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var47 string var templ_7745c5c3_Var47 string
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(task.Project) templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(task.Project)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var48 string var templ_7745c5c3_Var48 string
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(task.Date) templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(task.Date)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1234,7 +1249,7 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1247,20 +1262,20 @@ func TaskRow(task dashboardTask) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var51 string var templ_7745c5c3_Var51 string
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(task.Status) templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(task.Status)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1294,20 +1309,20 @@ func SidebarNavItem(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var54 string var templ_7745c5c3_Var54 string
templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(sidebarNavItemID(item.Href)) templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(sidebarNavItemID(item.Href))
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1320,33 +1335,33 @@ func SidebarNavItem(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var56 templ.SafeURL var templ_7745c5c3_Var56 templ.SafeURL
templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(item.Href)) templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(item.Href))
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var57 string var templ_7745c5c3_Var57 string
templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(item.Href) templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(item.Href)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1354,20 +1369,20 @@ func SidebarNavItem(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var58 string var templ_7745c5c3_Var58 string
templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label) templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1401,20 +1416,20 @@ func SidebarNavItemOOB(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var61 string var templ_7745c5c3_Var61 string
templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(sidebarNavItemID(item.Href)) templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(sidebarNavItemID(item.Href))
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1427,33 +1442,33 @@ func SidebarNavItemOOB(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var63 templ.SafeURL var templ_7745c5c3_Var63 templ.SafeURL
templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(item.Href)) templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(item.Href))
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var64 string var templ_7745c5c3_Var64 string
templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(item.Href) templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(item.Href)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1461,20 +1476,20 @@ func SidebarNavItemOOB(item sidebarNavItem) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var65 string var templ_7745c5c3_Var65 string
templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label) templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var65))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1503,20 +1518,20 @@ func SidebarProjectItem(item sidebarProjectItem) templ.Component {
templ_7745c5c3_Var66 = templ.NopComponent templ_7745c5c3_Var66 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var67 templ.SafeURL var templ_7745c5c3_Var67 templ.SafeURL
templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(item.Href)) templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(item.Href))
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1524,20 +1539,20 @@ func SidebarProjectItem(item sidebarProjectItem) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var68 string var templ_7745c5c3_Var68 string
templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label) templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(item.Label)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View file

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

View file

@ -112,8 +112,12 @@ templ TablosPageContent(vm TablosPageViewModel) {
}), }),
}) })
} }
if vm.ModalOpen { if vm.HasModal() {
@CreateTabloModal(vm) if vm.IsCreateModal() {
@CreateTabloModal(vm)
} else if vm.IsEditModal() {
@EditTabloModal(vm)
}
} }
</div> </div>
} }
@ -140,7 +144,8 @@ templ BorderlessDeleteButton(deleteRequestURL string) {
@ui.IconButton(ui.IconButtonProps{ @ui.IconButton(ui.IconButtonProps{
Label: "Supprimer le projet", Label: "Supprimer le projet",
Icon: "trash", Icon: "trash",
Variant: ui.IconButtonVariantDangerGhost, Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button", Type: "button",
Attrs: templ.Attributes{ Attrs: templ.Attributes{
"hx-delete": deleteRequestURL, "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) { templ TabloGridCard(tablo TabloCardView) {
@TabloGridCardWithAttrs(tablo, nil) @TabloGridCardWithAttrs(tablo, nil)
} }
templ TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) { 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"> <div class="project-card-top">
@ui.Badge(ui.BadgeProps{ @ui.Badge(ui.BadgeProps{
Label: tablo.StatusLabel, Label: tablo.StatusLabel,
Variant: badgeVariantForTone(tablo.StatusTone), Variant: badgeVariantForTone(tablo.StatusTone),
}) })
@BorderlessDeleteButton(tablo.DeleteRequestURL) <div class="flex items-center gap-2">
@EditTabloButton(tablo.EditRequestURL)
@BorderlessDeleteButton(tablo.DeleteRequestURL)
</div>
</div> </div>
<div class="project-card-title-row"> <div class="project-card-title-row">
<div class={ "project-avatar " + projectAccentClass(tablo.Accent) }> <div class="project-avatar">
<span>{ tablo.Initial }</span> <span>{ tablo.Initial }</span>
</div> </div>
<h4>{ tablo.Name }</h4> <h4>{ tablo.Name }</h4>
@ -180,17 +204,17 @@ templ TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) {
<strong>{ tablo.ProgressLabel }</strong> <strong>{ tablo.ProgressLabel }</strong>
</div> </div>
<div class="project-progress-track"> <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>
</div> </div>
</article> </article>
} }
templ TabloListRow(tablo TabloCardView) { 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"> <td class="px-6 py-4">
<div class="flex items-center gap-3"> <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) @ActionIcon(tablo.IconKind)
</div> </div>
<span class="font-medium text-gray-900 dark:text-gray-100 truncate">{ tablo.Name }</span> <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"> <td class="px-6 py-4">
<div class="flex items-center gap-3"> <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="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> </div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100 w-8 text-right">{ tablo.ProgressLabel }</span> <span class="text-sm font-semibold text-gray-900 dark:text-gray-100 w-8 text-right">{ tablo.ProgressLabel }</span>
</div> </div>
</td> </td>
<td class="px-6 py-4 text-right"> <td class="px-6 py-4 text-right">
@BorderlessDeleteButton(tablo.DeleteRequestURL) <div class="flex items-center justify-end gap-1">
@EditTabloButton(tablo.EditRequestURL)
@BorderlessDeleteButton(tablo.DeleteRequestURL)
</div>
</td> </td>
</tr> </tr>
} }
@ -269,7 +296,21 @@ templ CreateTabloModalBody(vm TablosPageViewModel) {
Placeholder: "Nom du projet", Placeholder: "Nom du projet",
Type: "text", 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"> <div class="flex items-center justify-end gap-3">
<a <a
@ -291,3 +332,85 @@ templ CreateTabloModalBody(vm TablosPageViewModel) {
</div> </div>
</form> </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,10 +303,17 @@ func TablosPageContent(vm TablosPageViewModel) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
if vm.ModalOpen { if vm.HasModal() {
templ_7745c5c3_Err = CreateTabloModal(vm).Render(ctx, templ_7745c5c3_Buffer) if vm.IsCreateModal() {
if templ_7745c5c3_Err != nil { templ_7745c5c3_Err = CreateTabloModal(vm).Render(ctx, templ_7745c5c3_Buffer)
return templ_7745c5c3_Err 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>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</div>")
@ -350,7 +357,7 @@ func StatusPill(vm TablosPageViewModel, status string, label string) templ.Compo
var templ_7745c5c3_Var16 templ.SafeURL var templ_7745c5c3_Var16 templ.SafeURL
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(vm.StatusHref(status))) templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(vm.StatusHref(status)))
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -363,7 +370,7 @@ func StatusPill(vm TablosPageViewModel, status string, label string) templ.Compo
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vm.StatusHref(status)) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vm.StatusHref(status))
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -403,7 +410,7 @@ func StatusPill(vm TablosPageViewModel, status string, label string) templ.Compo
var templ_7745c5c3_Var19 string var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(label) templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(label)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -441,7 +448,8 @@ func BorderlessDeleteButton(deleteRequestURL string) templ.Component {
templ_7745c5c3_Err = ui.IconButton(ui.IconButtonProps{ templ_7745c5c3_Err = ui.IconButton(ui.IconButtonProps{
Label: "Supprimer le projet", Label: "Supprimer le projet",
Icon: "trash", Icon: "trash",
Variant: ui.IconButtonVariantDangerGhost, Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button", Type: "button",
Attrs: templ.Attributes{ Attrs: templ.Attributes{
"hx-delete": deleteRequestURL, "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 { func TabloGridCard(tablo TabloCardView) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 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) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var21 := templ.GetChildren(ctx) templ_7745c5c3_Var22 := templ.GetChildren(ctx)
if templ_7745c5c3_Var21 == nil { if templ_7745c5c3_Var22 == nil {
templ_7745c5c3_Var21 = templ.NopComponent templ_7745c5c3_Var22 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = TabloGridCardWithAttrs(tablo, nil).Render(ctx, templ_7745c5c3_Buffer) 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) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var22 := templ.GetChildren(ctx) templ_7745c5c3_Var23 := templ.GetChildren(ctx)
if templ_7745c5c3_Var22 == nil { if templ_7745c5c3_Var23 == nil {
templ_7745c5c3_Var22 = templ.NopComponent templ_7745c5c3_Var23 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -515,7 +577,7 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -526,40 +588,26 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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) templ_7745c5c3_Err = BorderlessDeleteButton(tablo.DeleteRequestURL).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div><div class=\"project-card-title-row\">") 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_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>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var25 string var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Initial) templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Initial)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -572,7 +620,7 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
var templ_7745c5c3_Var26 string var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name) templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -593,7 +641,7 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
var templ_7745c5c3_Var27 string var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CardDateLabel) templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CardDateLabel)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -606,48 +654,26 @@ func TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) templ.C
var templ_7745c5c3_Var28 string var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel) templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel)
if templ_7745c5c3_Err != nil { 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)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var29 = []any{"project-progress-bar " + projectAccentClass(tablo.Accent)} var templ_7745c5c3_Var29 string
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<div class=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"></div></div></div></article>")
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>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -671,34 +697,25 @@ func TabloListRow(tablo TabloCardView) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var32 := templ.GetChildren(ctx) templ_7745c5c3_Var30 := templ.GetChildren(ctx)
if templ_7745c5c3_Var32 == nil { if templ_7745c5c3_Var30 == nil {
templ_7745c5c3_Var32 = templ.NopComponent templ_7745c5c3_Var30 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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} var templ_7745c5c3_Var31 string
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var33...) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div class=\"") 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
}
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, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -706,20 +723,20 @@ func TabloListRow(tablo TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var35 string var templ_7745c5c3_Var32 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name) templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.Name)
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -730,7 +747,7 @@ func TabloListRow(tablo TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -738,42 +755,46 @@ func TabloListRow(tablo TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var36 string var templ_7745c5c3_Var33 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CreatedAtLabel) templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.CreatedAtLabel)
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var37 string var templ_7745c5c3_Var34 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(progressInlineStyle(tablo.Progress)) templ_7745c5c3_Var34, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(progressInlineStyle(tablo.Progress))
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var38 string var templ_7745c5c3_Var35 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel) templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(tablo.ProgressLabel)
if templ_7745c5c3_Err != nil { 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -781,7 +802,7 @@ func TabloListRow(tablo TabloCardView) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -805,9 +826,9 @@ func CreateTabloModal(vm TablosPageViewModel) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var39 := templ.GetChildren(ctx) templ_7745c5c3_Var36 := templ.GetChildren(ctx)
if templ_7745c5c3_Var39 == nil { if templ_7745c5c3_Var36 == nil {
templ_7745c5c3_Var39 = templ.NopComponent templ_7745c5c3_Var36 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = ui.Modal(ui.ModalProps{ templ_7745c5c3_Err = ui.Modal(ui.ModalProps{
@ -837,12 +858,12 @@ func TabloListHead() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var40 := templ.GetChildren(ctx) templ_7745c5c3_Var37 := templ.GetChildren(ctx)
if templ_7745c5c3_Var40 == nil { if templ_7745c5c3_Var37 == nil {
templ_7745c5c3_Var40 = templ.NopComponent templ_7745c5c3_Var37 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -866,9 +887,9 @@ func TabloListBody(tablos []TabloCardView) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var41 := templ.GetChildren(ctx) templ_7745c5c3_Var38 := templ.GetChildren(ctx)
if templ_7745c5c3_Var41 == nil { if templ_7745c5c3_Var38 == nil {
templ_7745c5c3_Var41 = templ.NopComponent templ_7745c5c3_Var38 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
for _, tablo := range tablos { for _, tablo := range tablos {
@ -897,69 +918,69 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var42 := templ.GetChildren(ctx) templ_7745c5c3_Var39 := templ.GetChildren(ctx)
if templ_7745c5c3_Var42 == nil { if templ_7745c5c3_Var39 == nil {
templ_7745c5c3_Var42 = templ.NopComponent templ_7745c5c3_Var39 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var43 string var templ_7745c5c3_Var40 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(vm.View) templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(vm.View)
if templ_7745c5c3_Err != nil { 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: 282, Col: 50}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\"> <input type=\"hidden\" name=\"status\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\"> <input type=\"hidden\" name=\"status\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var44 string var templ_7745c5c3_Var41 string
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(vm.Status) templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(vm.Status)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 256, Col: 54} 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_Var44)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\"> <input type=\"hidden\" name=\"q\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\"> <input type=\"hidden\" name=\"q\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var45 string var templ_7745c5c3_Var42 string
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(vm.Query) templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(vm.Query)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 257, Col: 48} 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_Var45)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\"> <input type=\"hidden\" name=\"modal\" value=\"create\"> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\"> <input type=\"hidden\" name=\"modal\" value=\"create\"> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if vm.ErrorMessage != "" { 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\">") 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var46 string var templ_7745c5c3_Var43 string
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(vm.ErrorMessage) templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(vm.ErrorMessage)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/tablos.templ`, Line: 260, Col: 112} 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_Var46)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -974,38 +995,55 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
Placeholder: "Nom du projet", Placeholder: "Nom du projet",
Type: "text", Type: "text",
}), }),
Error: vm.ErrorMessage,
}).Render(ctx, templ_7745c5c3_Buffer) }).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var47 templ.SafeURL templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<div class=\"flex items-center justify-end gap-3\"><a href=\"")
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))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var48 string templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\" hx-get=\"")
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))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -1018,7 +1056,266 @@ func CreateTabloModalBody(vm TablosPageViewModel) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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 { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View file

@ -5,50 +5,57 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/a-h/templ"
"xtablo-backend/internal/web/ui" "xtablo-backend/internal/web/ui"
) )
type TabloCardView struct { type TabloCardView struct {
ID string ID string
Name string Name string
Status string Color string
StatusLabel string Status string
StatusClass string StatusLabel string
StatusTone string StatusClass string
Progress int StatusTone string
CreatedAtLabel string Progress int
CardDateLabel string CreatedAtLabel string
ProgressLabel string CardDateLabel string
DeleteURL string ProgressLabel string
DeleteURL string
DeleteRequestURL string DeleteRequestURL string
IconKind string EditRequestURL string
IconBgClass string IconKind string
IconFgClass string IconBgClass string
Accent string IconFgClass string
Initial string Accent string
Initial string
} }
type TablosPageViewModel struct { type TablosPageViewModel struct {
DisplayName string DisplayName string
View string View string
Query string Query string
Status string Status string
ModalOpen bool ModalKind string
FormName string EditingTabloID string
ErrorMessage string FormName string
Tablos []TabloCardView 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{ return TablosPageViewModel{
DisplayName: displayName, DisplayName: displayName,
View: normalizedView(view), View: normalizedView(view),
Query: strings.TrimSpace(query), Query: strings.TrimSpace(query),
Status: normalizedStatus(status), Status: normalizedStatus(status),
ModalOpen: modalOpen, ModalKind: normalizedModalKind(modalKind),
FormName: strings.TrimSpace(formName), EditingTabloID: strings.TrimSpace(editingTabloID),
ErrorMessage: strings.TrimSpace(errorMessage), FormName: strings.TrimSpace(formName),
Tablos: tablos, FormColor: normalizedFormColor(modalKind, formColor),
ErrorMessage: strings.TrimSpace(errorMessage),
Tablos: tablos,
} }
} }
@ -60,6 +67,18 @@ func (vm TablosPageViewModel) HasTablos() bool {
return len(vm.Tablos) > 0 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 { func (vm TablosPageViewModel) StatusHref(status string) string {
values := vm.baseValues() values := vm.baseValues()
values.Set("status", normalizedStatus(status)) values.Set("status", normalizedStatus(status))
@ -94,11 +113,20 @@ func (vm TablosPageViewModel) CreateModalHref() string {
return "/tablos?" + values.Encode() 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 { func (vm TablosPageViewModel) CloseModalHref() string {
values := vm.baseValues() values := vm.baseValues()
return "/tablos?" + values.Encode() return "/tablos?" + values.Encode()
} }
func (vm TablosPageViewModel) EditSubmitHref() string {
return "/tablos/" + vm.EditingTabloID
}
func (vm TablosPageViewModel) HasSearch() bool { func (vm TablosPageViewModel) HasSearch() bool {
return vm.Query != "" 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 { func (vm TablosPageViewModel) baseValues() url.Values {
values := url.Values{} values := url.Values{}
values.Set("view", vm.View) values.Set("view", vm.View)
@ -159,3 +207,11 @@ func badgeVariantForTone(tone string) ui.BadgeVariant {
return ui.BadgeVariantInfo 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("/files", authHandler.GetFilesPage())
mux.Get("/feedback", authHandler.GetFeedbackPage()) mux.Get("/feedback", authHandler.GetFeedbackPage())
mux.Post("/tablos", authHandler.PostTablos()) 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.Delete("/tablos/{tabloID}", authHandler.DeleteTablo())
mux.Get("/login", authHandler.GetLoginPage()) mux.Get("/login", authHandler.GetLoginPage())
mux.Get("/signup", authHandler.GetSignupPage()) mux.Get("/signup", authHandler.GetSignupPage())

View file

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

View file

@ -7,7 +7,6 @@
--color-green-50: oklch(98.2% 0.018 155.826); --color-green-50: oklch(98.2% 0.018 155.826);
--color-green-200: oklch(92.5% 0.084 155.995); --color-green-200: oklch(92.5% 0.084 155.995);
--color-green-400: oklch(79.2% 0.209 151.711); --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-600: oklch(62.7% 0.194 149.214);
--color-green-800: oklch(44.8% 0.119 151.328); --color-green-800: oklch(44.8% 0.119 151.328);
--color-green-950: oklch(26.6% 0.065 152.934); --color-green-950: oklch(26.6% 0.065 152.934);
@ -60,6 +59,9 @@
.absolute { .absolute {
position: absolute; position: absolute;
} }
.fixed {
position: fixed;
}
.relative { .relative {
position: relative; position: relative;
} }
@ -199,6 +201,9 @@
.justify-end { .justify-end {
justify-content: flex-end; justify-content: flex-end;
} }
.gap-1 {
gap: calc(var(--spacing) * 1);
}
.gap-1\.5 { .gap-1\.5 {
gap: calc(var(--spacing) * 1.5); gap: calc(var(--spacing) * 1.5);
} }
@ -298,9 +303,6 @@
.bg-green-50 { .bg-green-50 {
background-color: var(--color-green-50); background-color: var(--color-green-50);
} }
.bg-green-500 {
background-color: var(--color-green-500);
}
.bg-purple-50 { .bg-purple-50 {
background-color: var(--color-purple-50); background-color: var(--color-purple-50);
} }