Add select component with single and multi-value support

This commit is contained in:
Arthur Belleville 2026-05-10 22:04:09 +02:00
parent 9a92f358e8
commit c148ff9af3
No known key found for this signature in database
21 changed files with 1470 additions and 12 deletions

View file

@ -8,6 +8,6 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link is-active">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./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>
<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="./selects.html" class="catalog-nav-link">Selects</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./spacing.html" class="catalog-nav-link">Spacing</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Badges</h1><p>Semantic status labels for todo, in-progress, success, and destructive states.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Status set</h2><p>The four semantic badge tones used across the app.</p></div><div class="catalog-example-preview"><div class="catalog-inline"><span class="ui-badge ui-badge-info">À faire</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-warning">En cours</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-success">Terminé</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-danger">Erreur</span></div></div><pre class="catalog-example-snippet"><code>@ui.Badge(ui.BadgeProps{Label: &#34;En cours&#34;, Variant: ui.BadgeVariantWarning})</code></pre></section></div></main>
</body>
</html>

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link is-active">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./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{
<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="./selects.html" class="catalog-nav-link">Selects</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./spacing.html" class="catalog-nav-link">Spacing</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Buttons</h1><p>Primary, secondary, ghost, and destructive actions built from shared templ primitives.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Default solid action</h2><p>Used for the main action in a page section or modal footer.</p></div><div class="catalog-example-preview"><button type="button" class="ui-button ui-button-solid ui-button-default ui-button-md">Nouveau projet</button></div><pre class="catalog-example-snippet"><code>@ui.Button(ui.ButtonProps{
Label: &#34;Nouveau projet&#34;,
Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD,

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./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{
<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="./selects.html" class="catalog-nav-link">Selects</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./spacing.html" class="catalog-nav-link">Spacing</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link is-active">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Cards</h1><p>Reusable bordered surfaces with optional header, body, and footer regions.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Surface card</h2><p>Generic elevated surface with optional header and footer.</p></div><div class="catalog-example-preview"><section class="ui-card"><div class="ui-card-header">Header</div><div class="ui-card-body">Body</div><div class="ui-card-footer">Footer</div></section></div><pre class="catalog-example-snippet"><code>@ui.Card(ui.CardProps{
Header: textComponent(&#34;Header&#34;),
Body: textComponent(&#34;Body&#34;),
Footer: textComponent(&#34;Footer&#34;),

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./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{
<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="./selects.html" class="catalog-nav-link">Selects</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./spacing.html" class="catalog-nav-link">Spacing</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link is-active">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Empty States</h1><p>Centered fallback messaging with optional icon and action.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Centered empty state</h2><p>Used when a list has no rows yet and the next action should stay obvious.</p></div><div class="catalog-example-preview"><section class="ui-empty-state"><div class="ui-empty-state-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="18" height="18" x="3" y="3" rx="2"></rect> <path d="M3 9h18"></path> <path d="M3 15h18"></path> <path d="M9 3v18"></path> <path d="M15 3v18"></path></svg></div><h3 class="ui-empty-state-title">Aucun projet trouvé</h3><p class="ui-empty-state-description">Créez votre premier projet</p><div class="ui-empty-state-action"><button type="button" class="ui-button ui-button-solid ui-button-default ui-button-md"><span class="ui-button-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"></path> <path d="M12 5v14"></path></svg></span> Nouveau projet</button></div></section></div><pre class="catalog-example-snippet"><code>@ui.EmptyState(ui.EmptyStateProps{
Title: &#34;Aucun projet trouvé&#34;,
Description: &#34;Créez votre premier projet&#34;,
Icon: ui.UIIcon(&#34;grid3x3&#34;),

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link is-active">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./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{
<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="./selects.html" class="catalog-nav-link">Selects</a><a href="./form-fields.html" class="catalog-nav-link is-active">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./spacing.html" class="catalog-nav-link">Spacing</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Form Fields</h1><p>Labeled controls with optional hint and error messaging.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Field with validation</h2><p>Wraps a control with label and inline error feedback.</p></div><div class="catalog-example-preview"><div class="ui-form-field"><label for="catalog-name" class="ui-form-label">Nom</label> <input id="catalog-name" type="text" name="name" value="" placeholder="Nom du projet" class="ui-input"><p class="ui-form-error">Le nom est requis</p></div></div><pre class="catalog-example-snippet"><code>@ui.FormField(ui.FormFieldProps{
Label: &#34;Nom&#34;,
For: &#34;catalog-name&#34;,
Field: ui.Input(ui.InputProps{

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link 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{
<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="./selects.html" class="catalog-nav-link">Selects</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./spacing.html" class="catalog-nav-link">Spacing</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Icon Buttons</h1><p>Compact icon-only actions for destructive and neutral controls.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Borderless destructive action</h2><p>Used for delete controls inside project cards and list rows.</p></div><div class="catalog-example-preview"><button type="button" class="borderless-icon-button ui-icon-button-ghost ui-icon-button-danger" aria-label="Supprimer le projet"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash2 w-4 h-4" aria-hidden="true"><path d="M3 6h18"></path> <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path> <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path> <line x1="10" x2="10" y1="11" y2="17"></line> <line x1="14" x2="14" y1="11" y2="17"></line></svg></button></div><pre class="catalog-example-snippet"><code>@ui.IconButton(ui.IconButtonProps{
Label: &#34;Supprimer le projet&#34;,
Icon: &#34;trash&#34;,
Variant: ui.IconButtonVariantDanger,

View file

@ -8,6 +8,6 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Component Catalog</h1><p>Static documentation generated from the same templ primitives used by the Go application.</p></header><div class="catalog-page-list"><a href="./tokens.html" class="catalog-page-link-card"><h2>Tokens</h2><p>Semantic colors and status roles used by the Go design system.</p><p class="catalog-page-link">/tokens.html</p></a><a href="./buttons.html" class="catalog-page-link-card"><h2>Buttons</h2><p>Primary, secondary, ghost, and destructive actions built from shared templ primitives.</p><p class="catalog-page-link">/buttons.html</p></a><a href="./badges.html" class="catalog-page-link-card"><h2>Badges</h2><p>Semantic status labels for todo, in-progress, success, and destructive states.</p><p class="catalog-page-link">/badges.html</p></a><a href="./icon-buttons.html" class="catalog-page-link-card"><h2>Icon Buttons</h2><p>Compact icon-only actions for destructive and neutral controls.</p><p class="catalog-page-link">/icon-buttons.html</p></a><a href="./inputs.html" class="catalog-page-link-card"><h2>Inputs</h2><p>Shared single-line and multiline text controls.</p><p class="catalog-page-link">/inputs.html</p></a><a href="./form-fields.html" class="catalog-page-link-card"><h2>Form Fields</h2><p>Labeled controls with optional hint and error messaging.</p><p class="catalog-page-link">/form-fields.html</p></a><a href="./modals.html" class="catalog-page-link-card"><h2>Modals</h2><p>Shared modal shell for focused create, edit, and confirm flows.</p><p class="catalog-page-link">/modals.html</p></a><a href="./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>
<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="./selects.html" class="catalog-page-link-card"><h2>Selects</h2><p>Single and multi-value select controls with a shared server-rendered shell.</p><p class="catalog-page-link">/selects.html</p></a><a href="./form-fields.html" class="catalog-page-link-card"><h2>Form Fields</h2><p>Labeled controls with optional hint and error messaging.</p><p class="catalog-page-link">/form-fields.html</p></a><a href="./modals.html" class="catalog-page-link-card"><h2>Modals</h2><p>Shared modal shell for focused create, edit, and confirm flows.</p><p class="catalog-page-link">/modals.html</p></a><a href="./spacing.html" class="catalog-page-link-card"><h2>Spacing</h2><p>Fixed horizontal and vertical spacer primitives for composing gaps between UI components.</p><p class="catalog-page-link">/spacing.html</p></a><a href="./tables.html" class="catalog-page-link-card"><h2>Tables</h2><p>Shared table shell for server-rendered list views.</p><p class="catalog-page-link">/tables.html</p></a><a href="./empty-states.html" class="catalog-page-link-card"><h2>Empty States</h2><p>Centered fallback messaging with optional icon and action.</p><p class="catalog-page-link">/empty-states.html</p></a><a href="./cards.html" class="catalog-page-link-card"><h2>Cards</h2><p>Reusable bordered surfaces with optional header, body, and footer regions.</p><p class="catalog-page-link">/cards.html</p></a></div></main>
</body>
</html>

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link is-active">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./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{
<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="./selects.html" class="catalog-nav-link">Selects</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./spacing.html" class="catalog-nav-link">Spacing</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Inputs</h1><p>Shared single-line and multiline text controls.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Text input</h2><p>Single-line input for names, titles, and short labels.</p></div><div class="catalog-example-preview"><input id="name" type="text" name="name" value="Projet Atlas" placeholder="Nom du projet" class="ui-input"></div><pre class="catalog-example-snippet"><code>@ui.Input(ui.InputProps{
Name: &#34;name&#34;,
Value: &#34;Projet Atlas&#34;,
Placeholder: &#34;Nom du projet&#34;,

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link is-active">Modals</a><a href="./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{
<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="./selects.html" class="catalog-nav-link">Selects</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link is-active">Modals</a><a href="./spacing.html" class="catalog-nav-link">Spacing</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Modals</h1><p>Shared modal shell for focused create, edit, and confirm flows.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Create modal</h2><p>Shared modal shell with a form body and action footer.</p></div><div class="catalog-example-preview"><div class="ui-modal-backdrop"><div class="ui-modal-panel"><div class="ui-modal-header"><h2>Créer un projet</h2></div><div class="ui-modal-body"><div class="ui-form-field"><label for="modal-name" class="ui-form-label">Nom du projet</label> <input id="modal-name" type="text" name="name" value="" placeholder="Nom du projet" class="ui-input"></div></div><div class="ui-modal-actions"><button type="button" class="ui-button ui-button-solid ui-button-neutral ui-button-md">Annuler</button><button type="submit" class="ui-button ui-button-solid ui-button-default ui-button-md">Créer le projet</button></div></div></div></div><pre class="catalog-example-snippet"><code>@ui.Modal(ui.ModalProps{
Title: &#34;Créer un projet&#34;,
Body: ui.FormField(...),
Actions: ui.Button(...),

View file

@ -0,0 +1,302 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Selects</title>
<link rel="stylesheet" href="../../go-backend/static/tailwind.css">
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./selects.html" class="catalog-nav-link is-active">Selects</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>Selects</h1><p>Single and multi-value select controls with a shared server-rendered shell.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Single select</h2><p>Single-choice dropdown with the shared input shell and custom chevron.</p></div><div class="catalog-example-preview"><div class="ui-select" data-ui-select-root data-ui-select-multiple="false" data-placeholder="Select a status" data-selected-label="In progress"><select id="status-native" name="status" class="ui-select-native" data-ui-select-native><option value="">Select a status</option> <option value="todo">To do</option><option value="in-progress" selected>In progress</option><option value="done">Done</option></select> <button id="status" type="button" class="ui-select-control" data-ui-select-trigger aria-haspopup="listbox" aria-expanded="false" aria-controls="status-menu"><span class="ui-select-value-wrapper" data-ui-select-label>In progress</span> <span class="ui-select-arrow-zone"><svg class="ui-select-arrow-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg></span></button><div id="status-menu" class="ui-select-menu" data-ui-select-menu role="listbox" hidden><button type="button" class="ui-select-option" data-ui-select-option data-value="todo" data-label="To do" role="option" aria-selected="false"><span class="ui-select-option-text">To do</span> <span class="ui-select-option-check" aria-hidden="true"></span></button><button type="button" class="ui-select-option is-selected" data-ui-select-option data-value="in-progress" data-label="In progress" role="option" aria-selected="true"><span class="ui-select-option-text">In progress</span> <span class="ui-select-option-check" aria-hidden="true"></span></button><button type="button" class="ui-select-option" data-ui-select-option data-value="done" data-label="Done" role="option" aria-selected="false"><span class="ui-select-option-text">Done</span> <span class="ui-select-option-check" aria-hidden="true"></span></button></div><script>
(function () {
if (!window.__uiSelectInitAll) {
window.__uiSelectSetOpen = function (root, open) {
var trigger = root.querySelector("[data-ui-select-trigger]");
var menu = root.querySelector("[data-ui-select-menu]");
if (!trigger || !menu) {
return;
}
root.classList.toggle("is-open", open);
trigger.setAttribute("aria-expanded", open ? "true" : "false");
menu.hidden = !open;
};
window.__uiSelectCloseAll = function (exceptRoot) {
document.querySelectorAll("[data-ui-select-root].is-open").forEach(function (root) {
if (root !== exceptRoot) {
window.__uiSelectSetOpen(root, false);
}
});
};
window.__uiSelectSync = function (root) {
var nativeSelect = root.querySelector("[data-ui-select-native]");
var outlet = root.querySelector("[data-ui-select-label]");
var placeholder = root.getAttribute("data-placeholder") || "";
var multiple = root.getAttribute("data-ui-select-multiple") === "true";
if (!nativeSelect || !outlet) {
return;
}
var labels = Array.from(nativeSelect.options).filter(function (option) {
return option.selected && option.value !== "";
}).map(function (option) {
return option.textContent;
});
root.setAttribute("data-selected-label", labels.join(", "));
outlet.innerHTML = "";
if (labels.length === 0) {
var placeholderNode = document.createElement("span");
placeholderNode.className = "ui-select-placeholder";
placeholderNode.textContent = placeholder;
outlet.appendChild(placeholderNode);
} else if (multiple) {
labels.forEach(function (label) {
var chip = document.createElement("span");
chip.className = "ui-select-chip";
chip.textContent = label;
outlet.appendChild(chip);
});
} else {
outlet.textContent = labels[0];
}
root.querySelectorAll("[data-ui-select-option]").forEach(function (optionButton) {
var selected = Array.from(nativeSelect.options).some(function (option) {
return option.value === optionButton.getAttribute("data-value") && option.selected;
});
optionButton.classList.toggle("is-selected", selected);
optionButton.setAttribute("aria-selected", selected ? "true" : "false");
});
};
window.__uiSelectToggleValue = function (root, optionButton) {
var nativeSelect = root.querySelector("[data-ui-select-native]");
var multiple = root.getAttribute("data-ui-select-multiple") === "true";
var value = optionButton.getAttribute("data-value");
if (!nativeSelect || value === null) {
return;
}
Array.from(nativeSelect.options).forEach(function (option) {
if (option.value !== value && !multiple) {
option.selected = false;
}
if (option.value === value) {
option.selected = multiple ? !option.selected : true;
}
});
window.__uiSelectSync(root);
nativeSelect.dispatchEvent(new Event("change", { bubbles: true }));
if (!multiple) {
window.__uiSelectSetOpen(root, false);
}
};
window.__uiSelectInitAll = function (scope) {
(scope || document).querySelectorAll("[data-ui-select-root]").forEach(function (root) {
window.__uiSelectSync(root);
});
};
document.addEventListener("click", function (event) {
var optionButton = event.target.closest("[data-ui-select-option]");
if (optionButton) {
var optionRoot = optionButton.closest("[data-ui-select-root]");
if (optionRoot) {
window.__uiSelectToggleValue(optionRoot, optionButton);
}
return;
}
var trigger = event.target.closest("[data-ui-select-trigger]");
if (trigger) {
var root = trigger.closest("[data-ui-select-root]");
var shouldOpen = root && !root.classList.contains("is-open");
window.__uiSelectCloseAll(root);
if (root) {
window.__uiSelectSetOpen(root, shouldOpen);
}
return;
}
if (!event.target.closest("[data-ui-select-root]")) {
window.__uiSelectCloseAll(null);
}
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
window.__uiSelectCloseAll(null);
}
});
document.addEventListener("htmx:afterSwap", function (event) {
window.__uiSelectInitAll(event.target);
});
}
window.__uiSelectInitAll(document);
})();
</script></div></div><pre class="catalog-example-snippet"><code>@ui.Select(ui.SelectProps{
Name: &#34;status&#34;,
Placeholder: &#34;Select a status&#34;,
Value: &#34;in-progress&#34;,
Options: []ui.SelectOption{
{Value: &#34;todo&#34;, Label: &#34;To do&#34;},
{Value: &#34;in-progress&#34;, Label: &#34;In progress&#34;},
{Value: &#34;done&#34;, Label: &#34;Done&#34;},
},
})</code></pre></section><section class="catalog-example"><div class="catalog-example-copy"><h2>Multiple select</h2><p>Multi-value selection with inline pills that stay form-compatible.</p></div><div class="catalog-example-preview"><div class="ui-select" data-ui-select-root data-ui-select-multiple="true" data-placeholder="Select multiple values" data-selected-label="Alice, Bob"><select id="assignee_ids-native" name="assignee_ids" class="ui-select-native" data-ui-select-native multiple><option value="alice" selected>Alice</option><option value="bob" selected>Bob</option><option value="charlie">Charlie</option></select> <button id="assignee_ids" type="button" class="ui-select-control" data-ui-select-trigger aria-haspopup="listbox" aria-expanded="false" aria-controls="assignee_ids-menu"><span class="ui-select-value-wrapper" data-ui-select-label><span class="ui-select-chip">Alice</span><span class="ui-select-chip">Bob</span></span> <span class="ui-select-arrow-zone"><svg class="ui-select-arrow-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg></span></button><div id="assignee_ids-menu" class="ui-select-menu" data-ui-select-menu role="listbox" aria-multiselectable="true" hidden><button type="button" class="ui-select-option is-selected" data-ui-select-option data-value="alice" data-label="Alice" role="option" aria-selected="true"><span class="ui-select-option-text">Alice</span> <span class="ui-select-option-check" aria-hidden="true"></span></button><button type="button" class="ui-select-option is-selected" data-ui-select-option data-value="bob" data-label="Bob" role="option" aria-selected="true"><span class="ui-select-option-text">Bob</span> <span class="ui-select-option-check" aria-hidden="true"></span></button><button type="button" class="ui-select-option" data-ui-select-option data-value="charlie" data-label="Charlie" role="option" aria-selected="false"><span class="ui-select-option-text">Charlie</span> <span class="ui-select-option-check" aria-hidden="true"></span></button></div><script>
(function () {
if (!window.__uiSelectInitAll) {
window.__uiSelectSetOpen = function (root, open) {
var trigger = root.querySelector("[data-ui-select-trigger]");
var menu = root.querySelector("[data-ui-select-menu]");
if (!trigger || !menu) {
return;
}
root.classList.toggle("is-open", open);
trigger.setAttribute("aria-expanded", open ? "true" : "false");
menu.hidden = !open;
};
window.__uiSelectCloseAll = function (exceptRoot) {
document.querySelectorAll("[data-ui-select-root].is-open").forEach(function (root) {
if (root !== exceptRoot) {
window.__uiSelectSetOpen(root, false);
}
});
};
window.__uiSelectSync = function (root) {
var nativeSelect = root.querySelector("[data-ui-select-native]");
var outlet = root.querySelector("[data-ui-select-label]");
var placeholder = root.getAttribute("data-placeholder") || "";
var multiple = root.getAttribute("data-ui-select-multiple") === "true";
if (!nativeSelect || !outlet) {
return;
}
var labels = Array.from(nativeSelect.options).filter(function (option) {
return option.selected && option.value !== "";
}).map(function (option) {
return option.textContent;
});
root.setAttribute("data-selected-label", labels.join(", "));
outlet.innerHTML = "";
if (labels.length === 0) {
var placeholderNode = document.createElement("span");
placeholderNode.className = "ui-select-placeholder";
placeholderNode.textContent = placeholder;
outlet.appendChild(placeholderNode);
} else if (multiple) {
labels.forEach(function (label) {
var chip = document.createElement("span");
chip.className = "ui-select-chip";
chip.textContent = label;
outlet.appendChild(chip);
});
} else {
outlet.textContent = labels[0];
}
root.querySelectorAll("[data-ui-select-option]").forEach(function (optionButton) {
var selected = Array.from(nativeSelect.options).some(function (option) {
return option.value === optionButton.getAttribute("data-value") && option.selected;
});
optionButton.classList.toggle("is-selected", selected);
optionButton.setAttribute("aria-selected", selected ? "true" : "false");
});
};
window.__uiSelectToggleValue = function (root, optionButton) {
var nativeSelect = root.querySelector("[data-ui-select-native]");
var multiple = root.getAttribute("data-ui-select-multiple") === "true";
var value = optionButton.getAttribute("data-value");
if (!nativeSelect || value === null) {
return;
}
Array.from(nativeSelect.options).forEach(function (option) {
if (option.value !== value && !multiple) {
option.selected = false;
}
if (option.value === value) {
option.selected = multiple ? !option.selected : true;
}
});
window.__uiSelectSync(root);
nativeSelect.dispatchEvent(new Event("change", { bubbles: true }));
if (!multiple) {
window.__uiSelectSetOpen(root, false);
}
};
window.__uiSelectInitAll = function (scope) {
(scope || document).querySelectorAll("[data-ui-select-root]").forEach(function (root) {
window.__uiSelectSync(root);
});
};
document.addEventListener("click", function (event) {
var optionButton = event.target.closest("[data-ui-select-option]");
if (optionButton) {
var optionRoot = optionButton.closest("[data-ui-select-root]");
if (optionRoot) {
window.__uiSelectToggleValue(optionRoot, optionButton);
}
return;
}
var trigger = event.target.closest("[data-ui-select-trigger]");
if (trigger) {
var root = trigger.closest("[data-ui-select-root]");
var shouldOpen = root && !root.classList.contains("is-open");
window.__uiSelectCloseAll(root);
if (root) {
window.__uiSelectSetOpen(root, shouldOpen);
}
return;
}
if (!event.target.closest("[data-ui-select-root]")) {
window.__uiSelectCloseAll(null);
}
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
window.__uiSelectCloseAll(null);
}
});
document.addEventListener("htmx:afterSwap", function (event) {
window.__uiSelectInitAll(event.target);
});
}
window.__uiSelectInitAll(document);
})();
</script></div></div><pre class="catalog-example-snippet"><code>@ui.Select(ui.SelectProps{
Name: &#34;assignee_ids&#34;,
Placeholder: &#34;Select multiple values&#34;,
Multiple: true,
Values: []string{&#34;alice&#34;, &#34;bob&#34;},
Options: []ui.SelectOption{
{Value: &#34;alice&#34;, Label: &#34;Alice&#34;},
{Value: &#34;bob&#34;, Label: &#34;Bob&#34;},
{Value: &#34;charlie&#34;, Label: &#34;Charlie&#34;},
},
})</code></pre></section></div></main>
</body>
</html>

View file

@ -8,6 +8,6 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">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 is-active">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>Spacing</h1><p>Fixed horizontal and vertical spacer primitives for composing gaps between UI components.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Horizontal spacing</h2><p>Use SpaceX to insert fixed horizontal gaps between inline or row-aligned components.</p></div><div class="catalog-example-preview"><div class="catalog-spacing-row"><button type="button" class="ui-button ui-button-solid ui-button-neutral ui-button-md">Précédent</button><span class="ui-space-x ui-space-x-lg" aria-hidden="true"></span><button type="button" class="ui-button ui-button-solid ui-button-default ui-button-md">Suivant</button></div></div><pre class="catalog-example-snippet"><code>@ui.SpaceX(ui.SpaceProps{Size: ui.SpacingStepLG})</code></pre></section><section class="catalog-example"><div class="catalog-example-copy"><h2>Vertical spacing</h2><p>Use SpaceY to insert fixed vertical gaps between stacked blocks.</p></div><div class="catalog-example-preview"><div class="catalog-spacing-column"><section class="ui-card"><div class="ui-card-body">Bloc 1</div></section><div class="ui-space-y ui-space-y-md" aria-hidden="true"></div><section class="ui-card"><div class="ui-card-body">Bloc 2</div></section></div></div><pre class="catalog-example-snippet"><code>@ui.SpaceY(ui.SpaceProps{Size: ui.SpacingStepMD})</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">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="./selects.html" class="catalog-nav-link">Selects</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 is-active">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>Spacing</h1><p>Fixed horizontal and vertical spacer primitives for composing gaps between UI components.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Horizontal spacing</h2><p>Use SpaceX to insert fixed horizontal gaps between inline or row-aligned components.</p></div><div class="catalog-example-preview"><div class="catalog-spacing-row"><button type="button" class="ui-button ui-button-solid ui-button-neutral ui-button-md">Précédent</button><span class="ui-space-x ui-space-x-lg" aria-hidden="true"></span><button type="button" class="ui-button ui-button-solid ui-button-default ui-button-md">Suivant</button></div></div><pre class="catalog-example-snippet"><code>@ui.SpaceX(ui.SpaceProps{Size: ui.SpacingStepLG})</code></pre></section><section class="catalog-example"><div class="catalog-example-copy"><h2>Vertical spacing</h2><p>Use SpaceY to insert fixed vertical gaps between stacked blocks.</p></div><div class="catalog-example-preview"><div class="catalog-spacing-column"><section class="ui-card"><div class="ui-card-body">Bloc 1</div></section><div class="ui-space-y ui-space-y-md" aria-hidden="true"></div><section class="ui-card"><div class="ui-card-body">Bloc 2</div></section></div></div><pre class="catalog-example-snippet"><code>@ui.SpaceY(ui.SpaceProps{Size: ui.SpacingStepMD})</code></pre></section></div></main>
</body>
</html>

View file

@ -8,7 +8,7 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link">Tokens</a><a href="./buttons.html" class="catalog-nav-link">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{
<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="./selects.html" class="catalog-nav-link">Selects</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./spacing.html" class="catalog-nav-link">Spacing</a><a href="./tables.html" class="catalog-nav-link is-active">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Tables</h1><p>Shared table shell for server-rendered list views.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>List shell</h2><p>Shared wrapper for server-rendered resource tables.</p></div><div class="catalog-example-preview"><div class="ui-table-shell"><table class="ui-table"><thead><tr><th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Projet</th><th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Statut</th></tr></thead> <tbody><tr><td class="px-6 py-4">Table View</td><td class="px-6 py-4">En cours</td></tr></tbody></table></div></div><pre class="catalog-example-snippet"><code>@ui.Table(ui.TableProps{
Head: TabloListHead(),
Body: TabloListBody(tablos),
})</code></pre></section></div></main>

View file

@ -8,6 +8,6 @@
<link rel="stylesheet" href="../../go-backend/static/styles.css">
</head>
<body>
<main class="catalog-page"><nav class="catalog-nav" aria-label="Catalog navigation"><a href="./index.html" class="catalog-home-link">Catalog</a><div class="catalog-nav-links"><a href="./tokens.html" class="catalog-nav-link is-active">Tokens</a><a href="./buttons.html" class="catalog-nav-link">Buttons</a><a href="./badges.html" class="catalog-nav-link">Badges</a><a href="./icon-buttons.html" class="catalog-nav-link">Icon Buttons</a><a href="./inputs.html" class="catalog-nav-link">Inputs</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./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>
<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="./selects.html" class="catalog-nav-link">Selects</a><a href="./form-fields.html" class="catalog-nav-link">Form Fields</a><a href="./modals.html" class="catalog-nav-link">Modals</a><a href="./spacing.html" class="catalog-nav-link">Spacing</a><a href="./tables.html" class="catalog-nav-link">Tables</a><a href="./empty-states.html" class="catalog-nav-link">Empty States</a><a href="./cards.html" class="catalog-nav-link">Cards</a></div></nav><header class="catalog-page-header"><p class="catalog-eyebrow">Design System</p><h1>Tokens</h1><p>Semantic colors and status roles used by the Go design system.</p></header><div class="catalog-example-list"><section class="catalog-example"><div class="catalog-example-copy"><h2>Status tones</h2><p>Shared semantic badges for info, warning, success, and danger states.</p></div><div class="catalog-example-preview"><div class="catalog-inline"><span class="ui-badge ui-badge-info">À faire</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-warning">En cours</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-success">Terminé</span></div><div class="catalog-inline"><span class="ui-badge ui-badge-danger">Erreur</span></div></div><pre class="catalog-example-snippet"><code>@ui.Badge(ui.BadgeProps{Label: &#34;En cours&#34;, Variant: ui.BadgeVariantWarning})</code></pre></section></div></main>
</body>
</html>

View file

@ -15,6 +15,7 @@ var sourceOrder = []string{
filepath.Join("internal", "web", "ui", "badge.css"),
filepath.Join("internal", "web", "ui", "icon-button.css"),
filepath.Join("internal", "web", "ui", "input.css"),
filepath.Join("internal", "web", "ui", "select.css"),
filepath.Join("internal", "web", "ui", "textarea.css"),
filepath.Join("internal", "web", "ui", "form-field.css"),
filepath.Join("internal", "web", "ui", "modal.css"),

View file

@ -193,6 +193,61 @@ func inputExamples() []Example {
}
}
func selectExamples() []Example {
return []Example{
{
Title: "Single select",
Description: "Single-choice dropdown with the shared input shell and custom chevron.",
Preview: ui.Select(ui.SelectProps{
Name: "status",
Placeholder: "Select a status",
Value: "in-progress",
Options: []ui.SelectOption{
{Value: "todo", Label: "To do"},
{Value: "in-progress", Label: "In progress"},
{Value: "done", Label: "Done"},
},
}),
Snippet: `@ui.Select(ui.SelectProps{
Name: "status",
Placeholder: "Select a status",
Value: "in-progress",
Options: []ui.SelectOption{
{Value: "todo", Label: "To do"},
{Value: "in-progress", Label: "In progress"},
{Value: "done", Label: "Done"},
},
})`,
},
{
Title: "Multiple select",
Description: "Multi-value selection with inline pills that stay form-compatible.",
Preview: ui.Select(ui.SelectProps{
Name: "assignee_ids",
Placeholder: "Select multiple values",
Multiple: true,
Values: []string{"alice", "bob"},
Options: []ui.SelectOption{
{Value: "alice", Label: "Alice"},
{Value: "bob", Label: "Bob"},
{Value: "charlie", Label: "Charlie"},
},
}),
Snippet: `@ui.Select(ui.SelectProps{
Name: "assignee_ids",
Placeholder: "Select multiple values",
Multiple: true,
Values: []string{"alice", "bob"},
Options: []ui.SelectOption{
{Value: "alice", Label: "Alice"},
{Value: "bob", Label: "Bob"},
{Value: "charlie", Label: "Charlie"},
},
})`,
},
}
}
func formFieldExamples() []Example {
return []Example{
{

View file

@ -48,6 +48,12 @@ func Pages() []Page {
Description: "Shared single-line and multiline text controls.",
Examples: inputExamples(),
},
{
Slug: "selects",
Title: "Selects",
Description: "Single and multi-value select controls with a shared server-rendered shell.",
Examples: selectExamples(),
},
{
Slug: "form-fields",
Title: "Form Fields",

View file

@ -0,0 +1,154 @@
.ui-select {
position: relative;
width: 100%;
}
.ui-select-native {
height: 0;
left: 0;
opacity: 0;
pointer-events: none;
position: absolute;
top: 0;
width: 0;
}
.ui-select-control {
align-items: center;
appearance: none;
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 0.75rem;
color: var(--color-text-primary);
cursor: pointer;
display: flex;
gap: 0.75rem;
justify-content: space-between;
min-height: 44px;
padding: 0.55rem 0.75rem 0.55rem 0.95rem;
text-align: left;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
width: 100%;
}
.ui-select-control:focus-visible {
border-color: var(--color-brand-focus);
box-shadow: 0 0 0 3px var(--color-focus-ring-strong);
outline: none;
}
.ui-select-control:disabled {
color: var(--color-text-faint);
cursor: not-allowed;
}
.ui-select-value-wrapper {
align-items: center;
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
gap: 0.35rem;
min-width: 0;
}
.ui-select-placeholder {
color: var(--color-text-faint);
}
.ui-select-chip {
background: var(--color-surface-muted);
border-radius: 999px;
color: var(--color-text-primary);
display: inline-flex;
font-size: 0.875rem;
line-height: 1;
padding: 0.35rem 0.6rem;
}
.ui-select-arrow-zone {
align-items: center;
color: var(--color-text-secondary);
display: inline-flex;
flex: 0 0 auto;
justify-content: center;
}
.ui-select-arrow-icon {
height: 1rem;
transition: transform 0.2s ease;
width: 1rem;
}
.ui-select.is-open .ui-select-arrow-icon {
transform: rotate(180deg);
}
.ui-select-menu {
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 0.9rem;
box-shadow: var(--shadow-surface-md);
display: grid;
gap: 0.25rem;
left: 0;
margin-top: 0.45rem;
max-height: 16rem;
overflow-y: auto;
padding: 0.4rem;
position: absolute;
right: 0;
top: 100%;
z-index: 30;
}
.ui-select-menu[hidden] {
display: none;
}
.ui-select-option {
align-items: center;
appearance: none;
background: transparent;
border: 0;
border-radius: 0.7rem;
color: var(--color-text-primary);
cursor: pointer;
display: flex;
font: inherit;
gap: 0.75rem;
justify-content: space-between;
padding: 0.7rem 0.8rem;
text-align: left;
width: 100%;
}
.ui-select-option:hover,
.ui-select-option:focus-visible {
background: var(--color-surface-muted);
outline: none;
}
.ui-select-option.is-selected {
background: var(--color-status-info-soft-bg);
color: var(--color-text-brand);
}
.ui-select-option:disabled {
color: var(--color-text-faint);
cursor: not-allowed;
}
.ui-select-option-text {
flex: 1 1 auto;
}
.ui-select-option-check {
color: currentColor;
opacity: 0;
}
.ui-select-option.is-selected .ui-select-option-check {
opacity: 1;
}

View file

@ -0,0 +1,251 @@
package ui
type SelectOption struct {
Value string
Label string
Disabled bool
}
type SelectProps struct {
ID string
Name string
Placeholder string
Value string
Values []string
Multiple bool
Options []SelectOption
Attrs templ.Attributes
}
templ Select(props SelectProps) {
<div
class="ui-select"
data-ui-select-root
data-ui-select-multiple={ selectBoolData(props.Multiple) }
data-placeholder={ selectPlaceholder(props) }
data-selected-label={ selectSelectedLabel(props) }
>
<select
id={ selectNativeID(props.ID, props.Name) }
name={ props.Name }
class="ui-select-native"
data-ui-select-native
if props.Multiple {
multiple
}
{ props.Attrs... }
>
if !props.Multiple && selectPlaceholder(props) != "" {
<option value="">{ selectPlaceholder(props) }</option>
}
for _, option := range props.Options {
<option
value={ option.Value }
if option.Disabled {
disabled
}
if selectOptionSelected(props, option.Value) {
selected
}
>{ option.Label }</option>
}
</select>
<button
id={ inputID(props.ID, props.Name) }
type="button"
class="ui-select-control"
data-ui-select-trigger
aria-haspopup="listbox"
aria-expanded="false"
aria-controls={ selectMenuID(props.ID, props.Name) }
if selectIsDisabled(props.Attrs) {
disabled
}
>
<span class="ui-select-value-wrapper" data-ui-select-label>
if props.Multiple {
if len(selectSelectedLabels(props)) == 0 {
<span class="ui-select-placeholder">{ selectPlaceholder(props) }</span>
} else {
for _, label := range selectSelectedLabels(props) {
<span class="ui-select-chip">{ label }</span>
}
}
} else if selectSelectedLabel(props) != "" {
{ selectSelectedLabel(props) }
} else {
<span class="ui-select-placeholder">{ selectPlaceholder(props) }</span>
}
</span>
<span class="ui-select-arrow-zone">
<svg class="ui-select-arrow-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m6 9 6 6 6-6"></path>
</svg>
</span>
</button>
<div
id={ selectMenuID(props.ID, props.Name) }
class="ui-select-menu"
data-ui-select-menu
role="listbox"
if props.Multiple {
aria-multiselectable="true"
}
hidden
>
for _, option := range props.Options {
<button
type="button"
class={ selectMenuOptionClass(selectOptionSelected(props, option.Value), option.Disabled) }
data-ui-select-option
data-value={ option.Value }
data-label={ option.Label }
role="option"
aria-selected={ selectBoolData(selectOptionSelected(props, option.Value)) }
if option.Disabled {
disabled
}
>
<span class="ui-select-option-text">{ option.Label }</span>
<span class="ui-select-option-check" aria-hidden="true">✓</span>
</button>
}
</div>
<script>
(function () {
if (!window.__uiSelectInitAll) {
window.__uiSelectSetOpen = function (root, open) {
var trigger = root.querySelector("[data-ui-select-trigger]");
var menu = root.querySelector("[data-ui-select-menu]");
if (!trigger || !menu) {
return;
}
root.classList.toggle("is-open", open);
trigger.setAttribute("aria-expanded", open ? "true" : "false");
menu.hidden = !open;
};
window.__uiSelectCloseAll = function (exceptRoot) {
document.querySelectorAll("[data-ui-select-root].is-open").forEach(function (root) {
if (root !== exceptRoot) {
window.__uiSelectSetOpen(root, false);
}
});
};
window.__uiSelectSync = function (root) {
var nativeSelect = root.querySelector("[data-ui-select-native]");
var outlet = root.querySelector("[data-ui-select-label]");
var placeholder = root.getAttribute("data-placeholder") || "";
var multiple = root.getAttribute("data-ui-select-multiple") === "true";
if (!nativeSelect || !outlet) {
return;
}
var labels = Array.from(nativeSelect.options).filter(function (option) {
return option.selected && option.value !== "";
}).map(function (option) {
return option.textContent;
});
root.setAttribute("data-selected-label", labels.join(", "));
outlet.innerHTML = "";
if (labels.length === 0) {
var placeholderNode = document.createElement("span");
placeholderNode.className = "ui-select-placeholder";
placeholderNode.textContent = placeholder;
outlet.appendChild(placeholderNode);
} else if (multiple) {
labels.forEach(function (label) {
var chip = document.createElement("span");
chip.className = "ui-select-chip";
chip.textContent = label;
outlet.appendChild(chip);
});
} else {
outlet.textContent = labels[0];
}
root.querySelectorAll("[data-ui-select-option]").forEach(function (optionButton) {
var selected = Array.from(nativeSelect.options).some(function (option) {
return option.value === optionButton.getAttribute("data-value") && option.selected;
});
optionButton.classList.toggle("is-selected", selected);
optionButton.setAttribute("aria-selected", selected ? "true" : "false");
});
};
window.__uiSelectToggleValue = function (root, optionButton) {
var nativeSelect = root.querySelector("[data-ui-select-native]");
var multiple = root.getAttribute("data-ui-select-multiple") === "true";
var value = optionButton.getAttribute("data-value");
if (!nativeSelect || value === null) {
return;
}
Array.from(nativeSelect.options).forEach(function (option) {
if (option.value !== value && !multiple) {
option.selected = false;
}
if (option.value === value) {
option.selected = multiple ? !option.selected : true;
}
});
window.__uiSelectSync(root);
nativeSelect.dispatchEvent(new Event("change", { bubbles: true }));
if (!multiple) {
window.__uiSelectSetOpen(root, false);
}
};
window.__uiSelectInitAll = function (scope) {
(scope || document).querySelectorAll("[data-ui-select-root]").forEach(function (root) {
window.__uiSelectSync(root);
});
};
document.addEventListener("click", function (event) {
var optionButton = event.target.closest("[data-ui-select-option]");
if (optionButton) {
var optionRoot = optionButton.closest("[data-ui-select-root]");
if (optionRoot) {
window.__uiSelectToggleValue(optionRoot, optionButton);
}
return;
}
var trigger = event.target.closest("[data-ui-select-trigger]");
if (trigger) {
var root = trigger.closest("[data-ui-select-root]");
var shouldOpen = root && !root.classList.contains("is-open");
window.__uiSelectCloseAll(root);
if (root) {
window.__uiSelectSetOpen(root, shouldOpen);
}
return;
}
if (!event.target.closest("[data-ui-select-root]")) {
window.__uiSelectCloseAll(null);
}
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
window.__uiSelectCloseAll(null);
}
});
document.addEventListener("htmx:afterSwap", function (event) {
window.__uiSelectInitAll(event.target);
});
}
window.__uiSelectInitAll(document);
})();
</script>
</div>
}

View file

@ -0,0 +1,104 @@
package ui
import (
"strings"
"github.com/a-h/templ"
)
func selectPlaceholder(props SelectProps) string {
if props.Placeholder != "" {
return props.Placeholder
}
if props.Multiple {
return "Select values"
}
return "Select an option"
}
func selectNativeID(id string, name string) string {
baseID := inputID(id, name)
if baseID == "" {
return "ui-select-native"
}
return baseID + "-native"
}
func selectMenuID(id string, name string) string {
baseID := inputID(id, name)
if baseID == "" {
return "ui-select-menu"
}
return baseID + "-menu"
}
func selectBoolData(value bool) string {
if value {
return "true"
}
return "false"
}
func selectSelectedValues(props SelectProps) []string {
if props.Multiple {
return props.Values
}
if props.Value == "" {
return nil
}
return []string{props.Value}
}
func selectOptionSelected(props SelectProps, value string) bool {
for _, selected := range selectSelectedValues(props) {
if selected == value {
return true
}
}
return false
}
func selectSelectedLabels(props SelectProps) []string {
var labels []string
for _, option := range props.Options {
if selectOptionSelected(props, option.Value) {
labels = append(labels, option.Label)
}
}
return labels
}
func selectSelectedLabel(props SelectProps) string {
return strings.Join(selectSelectedLabels(props), ", ")
}
func selectMenuOptionClass(selected bool, disabled bool) string {
className := "ui-select-option"
if selected {
className += " is-selected"
}
if disabled {
className += " is-disabled"
}
return className
}
func selectIsDisabled(attrs templ.Attributes) bool {
if attrs == nil {
return false
}
value, ok := attrs["disabled"]
if !ok {
return false
}
switch typed := value.(type) {
case bool:
return typed
case string:
return typed != "" && typed != "false"
default:
return true
}
}

File diff suppressed because one or more lines are too long

View file

@ -665,6 +665,162 @@ input {
outline: none;
}
/* Source: internal/web/ui/select.css */
.ui-select {
position: relative;
width: 100%;
}
.ui-select-native {
height: 0;
left: 0;
opacity: 0;
pointer-events: none;
position: absolute;
top: 0;
width: 0;
}
.ui-select-control {
align-items: center;
appearance: none;
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 0.75rem;
color: var(--color-text-primary);
cursor: pointer;
display: flex;
gap: 0.75rem;
justify-content: space-between;
min-height: 44px;
padding: 0.55rem 0.75rem 0.55rem 0.95rem;
text-align: left;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
width: 100%;
}
.ui-select-control:focus-visible {
border-color: var(--color-brand-focus);
box-shadow: 0 0 0 3px var(--color-focus-ring-strong);
outline: none;
}
.ui-select-control:disabled {
color: var(--color-text-faint);
cursor: not-allowed;
}
.ui-select-value-wrapper {
align-items: center;
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
gap: 0.35rem;
min-width: 0;
}
.ui-select-placeholder {
color: var(--color-text-faint);
}
.ui-select-chip {
background: var(--color-surface-muted);
border-radius: 999px;
color: var(--color-text-primary);
display: inline-flex;
font-size: 0.875rem;
line-height: 1;
padding: 0.35rem 0.6rem;
}
.ui-select-arrow-zone {
align-items: center;
color: var(--color-text-secondary);
display: inline-flex;
flex: 0 0 auto;
justify-content: center;
}
.ui-select-arrow-icon {
height: 1rem;
transition: transform 0.2s ease;
width: 1rem;
}
.ui-select.is-open .ui-select-arrow-icon {
transform: rotate(180deg);
}
.ui-select-menu {
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 0.9rem;
box-shadow: var(--shadow-surface-md);
display: grid;
gap: 0.25rem;
left: 0;
margin-top: 0.45rem;
max-height: 16rem;
overflow-y: auto;
padding: 0.4rem;
position: absolute;
right: 0;
top: 100%;
z-index: 30;
}
.ui-select-menu[hidden] {
display: none;
}
.ui-select-option {
align-items: center;
appearance: none;
background: transparent;
border: 0;
border-radius: 0.7rem;
color: var(--color-text-primary);
cursor: pointer;
display: flex;
font: inherit;
gap: 0.75rem;
justify-content: space-between;
padding: 0.7rem 0.8rem;
text-align: left;
width: 100%;
}
.ui-select-option:hover,
.ui-select-option:focus-visible {
background: var(--color-surface-muted);
outline: none;
}
.ui-select-option.is-selected {
background: var(--color-status-info-soft-bg);
color: var(--color-text-brand);
}
.ui-select-option:disabled {
color: var(--color-text-faint);
cursor: not-allowed;
}
.ui-select-option-text {
flex: 1 1 auto;
}
.ui-select-option-check {
color: currentColor;
opacity: 0;
}
.ui-select-option.is-selected .ui-select-option-check {
opacity: 1;
}
/* Source: internal/web/ui/textarea.css */
.ui-textarea {
appearance: none;