Add select component with single and multi-value support
This commit is contained in:
parent
9a92f358e8
commit
c148ff9af3
21 changed files with 1470 additions and 12 deletions
|
|
@ -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: "En cours", 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: "En cours", Variant: ui.BadgeVariantWarning})</code></pre></section></div></main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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: "Nouveau projet",
|
||||
Variant: ui.ButtonVariantDefault,
|
||||
Size: ui.SizeMD,
|
||||
|
|
|
|||
|
|
@ -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("Header"),
|
||||
Body: textComponent("Body"),
|
||||
Footer: textComponent("Footer"),
|
||||
|
|
|
|||
|
|
@ -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: "Aucun projet trouvé",
|
||||
Description: "Créez votre premier projet",
|
||||
Icon: ui.UIIcon("grid3x3"),
|
||||
|
|
|
|||
|
|
@ -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: "Nom",
|
||||
For: "catalog-name",
|
||||
Field: ui.Input(ui.InputProps{
|
||||
|
|
|
|||
|
|
@ -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: "Supprimer le projet",
|
||||
Icon: "trash",
|
||||
Variant: ui.IconButtonVariantDanger,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: "name",
|
||||
Value: "Projet Atlas",
|
||||
Placeholder: "Nom du projet",
|
||||
|
|
|
|||
|
|
@ -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: "Créer un projet",
|
||||
Body: ui.FormField(...),
|
||||
Actions: ui.Button(...),
|
||||
|
|
|
|||
302
docs/design-system/selects.html
Normal file
302
docs/design-system/selects.html
Normal 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: "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"},
|
||||
},
|
||||
})</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: "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"},
|
||||
},
|
||||
})</code></pre></section></div></main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: "En cours", 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: "En cours", Variant: ui.BadgeVariantWarning})</code></pre></section></div></main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
154
go-backend/internal/web/ui/select.css
Normal file
154
go-backend/internal/web/ui/select.css
Normal 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;
|
||||
}
|
||||
251
go-backend/internal/web/ui/select.templ
Normal file
251
go-backend/internal/web/ui/select.templ
Normal 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>
|
||||
}
|
||||
104
go-backend/internal/web/ui/select_helpers.go
Normal file
104
go-backend/internal/web/ui/select_helpers.go
Normal 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
|
||||
}
|
||||
}
|
||||
429
go-backend/internal/web/ui/select_templ.go
Normal file
429
go-backend/internal/web/ui/select_templ.go
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue