From e9f3639c7cc140bdd1866765d39b0a47ad2b878f Mon Sep 17 00:00:00 2001 From: "Janus C. H. Knudsen" Date: Fri, 16 Jan 2026 23:25:05 +0100 Subject: [PATCH] Enhance service detail UI with improved select and color controls Refactors select dropdown functionality to use custom implementation Adds color dot support for color selection Improves keyboard navigation and interaction for select dropdowns Modernizes UI components with more flexible and interactive controls --- .../ServiceDetailGeneral/Default.cshtml | 50 ++++++------ PlanTempus.Application/wwwroot/css/cash.css | 10 --- .../wwwroot/css/components.css | 50 ++++++------ .../wwwroot/css/controls.css | 48 ++++++++---- .../wwwroot/ts/modules/controls.ts | 76 ++++++++++++++++--- 5 files changed, 154 insertions(+), 80 deletions(-) diff --git a/PlanTempus.Application/Features/Services/Components/ServiceDetailGeneral/Default.cshtml b/PlanTempus.Application/Features/Services/Components/ServiceDetailGeneral/Default.cshtml index a176741..207805d 100644 --- a/PlanTempus.Application/Features/Services/Components/ServiceDetailGeneral/Default.cshtml +++ b/PlanTempus.Application/Features/Services/Components/ServiceDetailGeneral/Default.cshtml @@ -14,44 +14,45 @@ @Model.LabelCategory - -
+ Kombi-behandlinger Klip Farve Behandlinger Styling -
+
@Model.LabelCalendarColor - -
- Rød - Pink - Lilla - Mørk lilla - Indigo - Blå - Lyseblå - Cyan - Teal - Grøn - Lysegrøn - Lime - Gul - Amber - Orange - Mørk orange -
+ + Rød + Pink + Lilla + Mørk lilla + Indigo + Blå + Lyseblå + Cyan + Teal + Grøn + Lysegrøn + Lime + Gul + Amber + Orange + Mørk orange +
@@ -123,7 +124,10 @@ @Model.LabelImage - @Model.LabelUploadImage + + + @Model.LabelUploadImage + diff --git a/PlanTempus.Application/wwwroot/css/cash.css b/PlanTempus.Application/wwwroot/css/cash.css index f7b8ba9..e75a52f 100644 --- a/PlanTempus.Application/wwwroot/css/cash.css +++ b/PlanTempus.Application/wwwroot/css/cash.css @@ -655,19 +655,9 @@ swp-auto-id { =========================================== */ swp-note-field { textarea { - width: 100%; min-height: 80px; padding: var(--spacing-6); - font-size: var(--font-size-base); - font-family: var(--font-family); - border: 1px solid var(--color-border); border-radius: var(--radius-md); - resize: vertical; - - &:focus { - outline: none; - border-color: var(--color-teal); - } } } diff --git a/PlanTempus.Application/wwwroot/css/components.css b/PlanTempus.Application/wwwroot/css/components.css index 410495a..1aa6172 100644 --- a/PlanTempus.Application/wwwroot/css/components.css +++ b/PlanTempus.Application/wwwroot/css/components.css @@ -678,6 +678,34 @@ swp-user-email { /* =========================================== FORM INPUTS (shared base styling) =========================================== */ + +/* Global textarea styling */ +textarea { + width: 100%; + padding: var(--spacing-3) var(--spacing-4); + font-size: var(--font-size-base); + font-family: var(--font-family); + color: var(--color-text); + background: var(--color-background-alt); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + resize: vertical; + + &::placeholder { + color: var(--color-text-muted); + } + + &:hover { + background: var(--color-background); + } + + &:focus { + outline: none; + background: var(--color-surface); + border-color: var(--color-teal); + } +} + swp-form-group { display: flex; flex-direction: column; @@ -861,28 +889,6 @@ swp-form-input { } } - /* Textarea */ - textarea { - width: 100%; - padding: 10px 12px; - border: 1px solid var(--color-border); - border-radius: 6px; - font-size: 14px; - font-family: var(--font-family); - color: var(--color-text); - background: var(--color-surface); - resize: vertical; - - &::placeholder { - color: var(--color-text-muted); - } - - &:focus { - outline: none; - border-color: var(--color-teal); - } - } - /* Date range inputs */ swp-date-range { display: flex; diff --git a/PlanTempus.Application/wwwroot/css/controls.css b/PlanTempus.Application/wwwroot/css/controls.css index af3ee2e..b146e3a 100644 --- a/PlanTempus.Application/wwwroot/css/controls.css +++ b/PlanTempus.Application/wwwroot/css/controls.css @@ -183,11 +183,11 @@ swp-notification-intro { } /* =========================================== - SELECT DROPDOWN (Popover API) + SELECT DROPDOWN =========================================== */ swp-select { position: relative; - display: inline-block; + display: block; } swp-select button { @@ -202,8 +202,7 @@ swp-select button { border: 1px solid transparent; cursor: pointer; transition: all 150ms ease; - min-width: 160px; - anchor-name: --select-trigger; + width: 100%; &:hover { background: var(--color-background); @@ -227,35 +226,37 @@ swp-select button i { transition: transform 150ms ease; } -swp-select button[aria-expanded="true"] i { +swp-select.open button i { transform: rotate(180deg); } -swp-select [popover] { +swp-select-dropdown { + display: none; position: absolute; - position-anchor: --select-trigger; - top: anchor(bottom); - left: anchor(left); - margin: var(--spacing-1) 0 0 0; + top: 100%; + left: 0; + z-index: 100; + margin-top: var(--spacing-1); padding: var(--spacing-2); + min-width: 100%; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); - min-width: anchor-size(width); max-height: 280px; overflow-y: auto; -} - -swp-select [popover]:popover-open { - display: flex; flex-direction: column; gap: 2px; } +swp-select.open swp-select-dropdown { + display: flex; +} + swp-select-option { display: flex; align-items: center; + gap: var(--spacing-3); padding: var(--spacing-2) var(--spacing-3); border-radius: var(--radius-sm); cursor: pointer; @@ -272,4 +273,21 @@ swp-select-option { font-weight: var(--font-weight-medium); color: var(--color-teal); } + + &.highlighted { + background: var(--color-background-alt); + outline: 2px solid var(--color-teal); + outline-offset: -2px; + } +} + +/* =========================================== + COLOR DOT (for color pickers) + =========================================== */ +swp-color-dot { + width: 18px; + height: 12px; + border-radius: 3px; + flex-shrink: 0; + background: var(--b-primary); } diff --git a/PlanTempus.Application/wwwroot/ts/modules/controls.ts b/PlanTempus.Application/wwwroot/ts/modules/controls.ts index 03eadc3..010d007 100644 --- a/PlanTempus.Application/wwwroot/ts/modules/controls.ts +++ b/PlanTempus.Application/wwwroot/ts/modules/controls.ts @@ -3,7 +3,7 @@ * * Handles generic UI controls functionality: * - Toggle sliders (Ja/Nej switches) - * - Select dropdowns (Popover API) + * - Select dropdowns */ /** @@ -38,20 +38,20 @@ export class ControlsController { /** * Initialize all select dropdowns on the page - * Uses Popover API for dropdown behavior */ private initSelectDropdowns(): void { document.querySelectorAll('swp-select').forEach(select => { const trigger = select.querySelector('button'); - const popover = select.querySelector('[popover]') as HTMLElement | null; + const dropdown = select.querySelector('swp-select-dropdown'); const options = select.querySelectorAll('swp-select-option'); - if (!trigger || !popover) return; + if (!trigger || !dropdown) return; - // Update aria-expanded on toggle - popover.addEventListener('toggle', (e: Event) => { - const event = e as ToggleEvent; - trigger.setAttribute('aria-expanded', event.newState === 'open' ? 'true' : 'false'); + // Toggle dropdown on button click + trigger.addEventListener('click', (e) => { + e.stopPropagation(); + const isOpen = select.classList.toggle('open'); + trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); }); // Handle option selection @@ -70,11 +70,18 @@ export class ControlsController { valueEl.textContent = label; } + // Update color dot if present (for color pickers) + const triggerDot = trigger.querySelector('swp-color-dot'); + if (triggerDot && value) { + triggerDot.className = `is-${value}`; + } + // Update data-value on select element (select as HTMLElement).dataset.value = value; - // Close popover - popover.hidePopover(); + // Close dropdown + select.classList.remove('open'); + trigger.setAttribute('aria-expanded', 'false'); // Dispatch custom event select.dispatchEvent(new CustomEvent('change', { @@ -84,5 +91,54 @@ export class ControlsController { }); }); }); + + // Click outside to close any open dropdown + document.addEventListener('click', () => { + this.closeAllSelects(); + }); + + // Keyboard navigation + document.addEventListener('keydown', (e) => { + const openSelect = document.querySelector('swp-select.open'); + if (!openSelect) return; + + // Escape to close + if (e.key === 'Escape') { + this.closeAllSelects(); + return; + } + + // Letter key to jump to option + if (e.key.length === 1 && /[a-zA-ZæøåÆØÅ]/.test(e.key)) { + const options = openSelect.querySelectorAll('swp-select-option'); + const letter = e.key.toLowerCase(); + + for (const option of options) { + const text = option.textContent?.trim().toLowerCase() || ''; + if (text.startsWith(letter)) { + // Scroll into view and highlight + option.scrollIntoView({ block: 'nearest' }); + options.forEach(o => o.classList.remove('highlighted')); + option.classList.add('highlighted'); + break; + } + } + } + + // Enter to select highlighted option + if (e.key === 'Enter') { + const highlighted = openSelect.querySelector('swp-select-option.highlighted') as HTMLElement; + if (highlighted) { + highlighted.click(); + } + } + }); + } + + private closeAllSelects(): void { + document.querySelectorAll('swp-select.open').forEach(select => { + select.classList.remove('open'); + select.querySelector('button')?.setAttribute('aria-expanded', 'false'); + }); } }