Skip to content

Commit bc8c724

Browse files
committed
refactor(frontend): replace JS hover style mutation with CSS :hover (WI-1318)
Roughly 130 onmouseenter/onmouseleave pairs mutated inline styles to paint hover states (backgrounds, text colors, borders, cssText overwrites), bypassing the cascade, needing manual resets, and occasionally clobbering other inline styles. Convert them to CSS :hover rules: the existing .hover-bg utility where a row has no base background, scoped component rules with semantic classes elsewhere, and class-driven :hover:not(.active) selectors for active-state nav items. Corrections that fall out of moving to the cascade: - data table rows keep their selected background while hovered - the unassigned row in customer organisation navigation hovers like every other nav item instead of only mid-drag The shared navItemStyle.js helpers become dead code and are removed; step status buttons drive their selected styling from classes instead of getStatusButtonStyle().
1 parent eb37ed0 commit bc8c724

54 files changed

Lines changed: 1017 additions & 584 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/src/lib/components/AIModalShell.svelte

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,7 @@
3939
</div>
4040
<button
4141
onclick={close}
42-
class="p-1.5 rounded transition-colors"
43-
style="color: var(--ds-text-subtle);"
44-
onmouseenter={(e) => { e.currentTarget.style.color = 'var(--ds-text)'; e.currentTarget.style.backgroundColor = 'var(--ds-background-neutral-hovered)'; }}
45-
onmouseleave={(e) => { e.currentTarget.style.color = 'var(--ds-text-subtle)'; e.currentTarget.style.backgroundColor = 'transparent'; }}
42+
class="modal-close p-1.5 rounded transition-colors"
4643
aria-label="Close"
4744
>
4845
<X class="w-5 h-5" />
@@ -72,14 +69,33 @@
7269
<div class="px-6 py-3 border-t flex justify-end" style="border-color: var(--ds-border);">
7370
<button
7471
onclick={close}
75-
class="px-4 py-2 text-sm font-medium rounded-md transition-colors"
76-
style="color: var(--ds-text); background-color: var(--ds-surface); border: 1px solid var(--ds-border);"
77-
onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--ds-background-neutral-hovered)'}
78-
onmouseleave={(e) => e.currentTarget.style.backgroundColor = 'var(--ds-surface)'}
72+
class="secondary-btn px-4 py-2 text-sm font-medium rounded-md transition-colors"
7973
>
8074
Close
8175
</button>
8276
</div>
8377
{/if}
8478
</div>
8579
</ModalBackdrop>
80+
81+
<style>
82+
.modal-close {
83+
color: var(--ds-text-subtle);
84+
}
85+
86+
.modal-close:hover {
87+
color: var(--ds-text);
88+
background-color: var(--ds-background-neutral-hovered);
89+
}
90+
91+
/* Secondary modal action: neutral surface that lifts on hover. */
92+
.secondary-btn {
93+
color: var(--ds-text);
94+
background-color: var(--ds-surface);
95+
border: 1px solid var(--ds-border);
96+
}
97+
98+
.secondary-btn:hover {
99+
background-color: var(--ds-background-neutral-hovered);
100+
}
101+
</style>

frontend/src/lib/components/DataTable.svelte

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,10 @@
195195
<tbody class={tbodyClass} style="--tw-divide-opacity: 1; border-color: var(--ds-border);">
196196
{#each displayData as item (item[keyField])}
197197
<tr
198-
class="{trClass} {onRowClick ? 'cursor-pointer' : ''}"
199-
style="border-color: var(--ds-border); {item[keyField] === selectedItemId ? 'background-color: var(--ds-surface-selected);' : ''}"
198+
class="data-row {trClass} {onRowClick ? 'cursor-pointer' : ''}"
199+
class:selected={item[keyField] === selectedItemId}
200+
style="border-color: var(--ds-border);"
200201
onclick={(e) => handleRowClick(item, e)}
201-
onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--ds-surface-raised-hovered)'}
202-
onmouseleave={(e) => e.currentTarget.style.backgroundColor = item[keyField] === selectedItemId ? 'var(--ds-surface-selected)' : ''}
203202
{...rowAttrs ? rowAttrs(item) : {}}
204203
>
205204
{#each columns as column, colIndex}
@@ -270,3 +269,17 @@
270269
{/if}
271270
{/if}
272271
</div>
272+
273+
<style>
274+
.data-row {
275+
border-color: var(--ds-border);
276+
}
277+
278+
.data-row:hover:not(.selected) {
279+
background-color: var(--ds-surface-raised-hovered);
280+
}
281+
282+
.data-row.selected {
283+
background-color: var(--ds-surface-selected);
284+
}
285+
</style>

frontend/src/lib/components/Tabs.svelte

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,11 @@
2525
href={tab.href}
2626
data-testid={tab.testid}
2727
class="tab-trigger flex flex-none items-center gap-2 px-4 py-3 text-sm font-medium transition-all relative border-b-2 no-underline whitespace-nowrap {tab.className || ''}"
28-
style="color: {activeTab === tab.id ? 'var(--ds-interactive)' : 'var(--ds-text-subtle)'}; border-bottom-color: {activeTab === tab.id ? 'var(--ds-interactive)' : 'transparent'}; {activeTab === tab.id ? 'margin-bottom: -1px;' : ''}"
28+
class:active={activeTab === tab.id}
2929
onclick={(e) => {
3030
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
3131
switchTab(tab.id);
3232
}}
33-
onmouseenter={(e) => { if (activeTab !== tab.id) /** @type {HTMLElement} */ (e.currentTarget).style.color = 'var(--ds-text)'; }}
34-
onmouseleave={(e) => { if (activeTab !== tab.id) /** @type {HTMLElement} */ (e.currentTarget).style.color = 'var(--ds-text-subtle)'; }}
3533
>
3634
{#if tab.icon}
3735
<tab.icon class="w-4 h-4 flex-shrink-0" />
@@ -46,10 +44,8 @@
4644
type="button"
4745
data-testid={tab.testid}
4846
class="tab-trigger flex flex-none items-center gap-2 px-4 py-3 text-sm font-medium transition-all relative border-b-2 whitespace-nowrap {tab.className || ''}"
49-
style="color: {activeTab === tab.id ? 'var(--ds-interactive)' : 'var(--ds-text-subtle)'}; border-bottom-color: {activeTab === tab.id ? 'var(--ds-interactive)' : 'transparent'}; {activeTab === tab.id ? 'margin-bottom: -1px;' : ''}"
47+
class:active={activeTab === tab.id}
5048
onclick={() => switchTab(tab.id)}
51-
onmouseenter={(e) => { if (activeTab !== tab.id) /** @type {HTMLElement} */ (e.currentTarget).style.color = 'var(--ds-text)'; }}
52-
onmouseleave={(e) => { if (activeTab !== tab.id) /** @type {HTMLElement} */ (e.currentTarget).style.color = 'var(--ds-text-subtle)'; }}
5349
>
5450
{#if tab.icon}
5551
<tab.icon class="w-4 h-4 flex-shrink-0" />
@@ -75,4 +71,19 @@
7571
overscroll-behavior-inline: contain;
7672
scrollbar-width: thin;
7773
}
74+
75+
.tab-trigger {
76+
color: var(--ds-text-subtle);
77+
border-bottom-color: transparent;
78+
}
79+
80+
.tab-trigger:hover:not(.active) {
81+
color: var(--ds-text);
82+
}
83+
84+
.tab-trigger.active {
85+
color: var(--ds-interactive);
86+
border-bottom-color: var(--ds-interactive);
87+
margin-bottom: -1px;
88+
}
7889
</style>

frontend/src/lib/dialogs/AIConfirmModal.svelte

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,7 @@
126126
<div class="px-6 py-3 border-t flex justify-end gap-2" style="border-color: var(--ds-border);">
127127
<button
128128
onclick={close}
129-
class="px-4 py-2 text-sm font-medium rounded-md transition-colors"
130-
style="color: var(--ds-text); background-color: var(--ds-surface); border: 1px solid var(--ds-border);"
131-
onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--ds-background-neutral-hovered)'}
132-
onmouseleave={(e) => e.currentTarget.style.backgroundColor = 'var(--ds-surface)'}
129+
class="secondary-btn px-4 py-2 text-sm font-medium rounded-md transition-colors"
133130
>
134131
Cancel
135132
</button>
@@ -147,14 +144,24 @@
147144
<div class="px-6 py-3 border-t flex justify-end" style="border-color: var(--ds-border);">
148145
<button
149146
onclick={close}
150-
class="px-4 py-2 text-sm font-medium rounded-md transition-colors"
151-
style="color: var(--ds-text); background-color: var(--ds-surface); border: 1px solid var(--ds-border);"
152-
onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--ds-background-neutral-hovered)'}
153-
onmouseleave={(e) => e.currentTarget.style.backgroundColor = 'var(--ds-surface)'}
147+
class="secondary-btn px-4 py-2 text-sm font-medium rounded-md transition-colors"
154148
>
155149
Close
156150
</button>
157151
</div>
158152
{/if}
159153
{/snippet}
160154
</AIModalShell>
155+
156+
<style>
157+
/* Secondary modal action: neutral surface that lifts on hover. */
158+
.secondary-btn {
159+
color: var(--ds-text);
160+
background-color: var(--ds-surface);
161+
border: 1px solid var(--ds-border);
162+
}
163+
164+
.secondary-btn:hover {
165+
background-color: var(--ds-background-neutral-hovered);
166+
}
167+
</style>

frontend/src/lib/dialogs/RequestTypeFieldsBuilder.svelte

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -973,10 +973,7 @@
973973
{#each filteredAvailableFields as field (field.identifier)}
974974
<button
975975
onclick={() => pickField(field)}
976-
class="w-full flex items-center justify-between gap-2 px-3 py-2 text-left transition-colors"
977-
style="color: var(--ds-text);"
978-
onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--ds-background-neutral-hovered)'}
979-
onmouseleave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
976+
class="field-option w-full flex items-center justify-between gap-2 px-3 py-2 text-left transition-colors"
980977
>
981978
<span class="text-sm font-medium truncate">{field.name}</span>
982979
<span
@@ -1096,3 +1093,13 @@
10961093
</div>
10971094
</PortalModal>
10981095
{/if}
1096+
1097+
<style>
1098+
.field-option {
1099+
color: var(--ds-text);
1100+
}
1101+
1102+
.field-option:hover {
1103+
background-color: var(--ds-background-neutral-hovered);
1104+
}
1105+
</style>

frontend/src/lib/editors/FieldLayoutEditor.svelte

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -305,10 +305,7 @@
305305
<!-- svelte-ignore a11y_no_static_element_interactions -->
306306
<div
307307
data-available-field-editor={JSON.stringify({ ...field, identifier })}
308-
class="group flex items-center gap-3 px-3 py-2 rounded border transition-all duration-200 cursor-grab hover:border-ds-border-focused active:cursor-grabbing"
309-
style="border-color: var(--ds-border); background-color: var(--ds-background-input); user-select: none; -webkit-user-select: none;"
310-
onmouseenter={(e) => e.currentTarget.style.background = 'var(--ds-background-neutral-hovered)'}
311-
onmouseleave={(e) => e.currentTarget.style.background = 'var(--ds-background-input)'}
308+
class="available-field-row group flex items-center gap-3 px-3 py-2 rounded border transition-all duration-200 cursor-grab hover:border-ds-border-focused active:cursor-grabbing"
312309
>
313310
<!-- Drag Handle -->
314311
<div class="flex-shrink-0">
@@ -413,4 +410,15 @@
413410
:global(.remove-field-btn:hover) {
414411
background: color-mix(in srgb, currentColor 10%, transparent);
415412
}
413+
414+
.available-field-row {
415+
border-color: var(--ds-border);
416+
background-color: var(--ds-background-input);
417+
user-select: none;
418+
-webkit-user-select: none;
419+
}
420+
421+
.available-field-row:hover {
422+
background-color: var(--ds-background-neutral-hovered);
423+
}
416424
</style>

frontend/src/lib/features/assets/AssetBrowser.svelte

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -562,10 +562,8 @@
562562
{:else if directAsset}
563563
<!-- Back button -->
564564
<button
565-
class="inline-flex items-center gap-1.5 mb-6 text-sm font-medium rounded-lg px-3 py-1.5 transition-colors"
566-
style="color: var(--ds-text-subtle); background: transparent;"
567-
onmouseenter={(e) => e.currentTarget.style.background = 'var(--ds-background-neutral-hovered)'}
568-
onmouseleave={(e) => e.currentTarget.style.background = 'transparent'}
565+
class="hover-bg inline-flex items-center gap-1.5 mb-6 text-sm font-medium rounded-lg px-3 py-1.5 transition-colors"
566+
style="color: var(--ds-text-subtle);"
569567
onclick={() => navigate('/assets')}
570568
>
571569
<IconArrowLeft class="w-4 h-4" />
@@ -577,10 +575,7 @@
577575
<h1 class="text-2xl font-semibold" style="color: var(--ds-text);">{directAsset.title}</h1>
578576
<div class="flex items-center gap-2">
579577
<button
580-
class="p-2 rounded-lg transition-colors"
581-
style="background: transparent;"
582-
onmouseenter={(e) => e.currentTarget.style.background = 'var(--ds-background-neutral-hovered)'}
583-
onmouseleave={(e) => e.currentTarget.style.background = 'transparent'}
578+
class="hover-bg p-2 rounded-lg transition-colors"
584579
onclick={() => { showRelationshipGraph = true; }}
585580
title="Relationship Graph"
586581
>
@@ -589,20 +584,15 @@
589584
{#if canEdit}
590585
<button
591586
data-testid="asset-edit"
592-
class="p-2 rounded-lg transition-colors"
593-
style="background: transparent;"
594-
onmouseenter={(e) => e.currentTarget.style.background = 'var(--ds-background-neutral-hovered)'}
595-
onmouseleave={(e) => e.currentTarget.style.background = 'transparent'}
587+
class="hover-bg p-2 rounded-lg transition-colors"
596588
onclick={() => showEditAssetForm(directAsset)}
597589
title={t('common.edit')}
598590
>
599591
<IconEdit class="w-4 h-4" style="color: var(--ds-icon);" />
600592
</button>
601593
<button
602-
class="p-2 rounded-lg transition-colors"
603-
style="background: transparent; color: var(--ds-text-danger);"
604-
onmouseenter={(e) => e.currentTarget.style.background = 'var(--ds-background-neutral-hovered)'}
605-
onmouseleave={(e) => e.currentTarget.style.background = 'transparent'}
594+
class="hover-bg p-2 rounded-lg transition-colors"
595+
style="color: var(--ds-text-danger);"
606596
onclick={() => deleteAsset(directAsset.id)}
607597
title={t('common.delete')}
608598
>
@@ -647,10 +637,9 @@
647637
<!-- Category tree -->
648638
<div class="flex-1 overflow-auto p-4">
649639
<button
650-
class="w-full text-left px-3 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2"
651-
style={selectedCategoryId === null ? 'background: var(--ds-surface-selected); color: var(--ds-text);' : 'color: var(--ds-text-subtle);'}
652-
onmouseenter={(e) => { if (selectedCategoryId !== null) e.currentTarget.style.cssText = 'background: var(--ds-background-neutral-hovered); color: var(--ds-text);'; }}
653-
onmouseleave={(e) => { if (selectedCategoryId !== null) e.currentTarget.style.cssText = 'color: var(--ds-text-subtle);'; }}
640+
class="nav-link w-full text-left px-3 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2"
641+
642+
class:active={selectedCategoryId === null}
654643
onclick={() => selectCategory(null)}
655644
>
656645
<IconPackage class="w-4 h-4" />
@@ -664,20 +653,16 @@
664653
<div
665654
role="button"
666655
tabindex="0"
667-
class="w-full text-left px-3 py-1.5 rounded-lg text-sm font-medium transition-all flex items-center gap-1 cursor-pointer"
668-
style={selectedCategoryId === category.id ? 'background: var(--ds-surface-selected); color: var(--ds-text);' : 'color: var(--ds-text-subtle);'}
669-
onmouseenter={(e) => { if (selectedCategoryId !== category.id) e.currentTarget.style.cssText = 'background: var(--ds-background-neutral-hovered); color: var(--ds-text);'; }}
670-
onmouseleave={(e) => { if (selectedCategoryId !== category.id) e.currentTarget.style.cssText = 'color: var(--ds-text-subtle);'; }}
656+
class="nav-link w-full text-left px-3 py-1.5 rounded-lg text-sm font-medium transition-all flex items-center gap-1 cursor-pointer"
657+
658+
class:active={selectedCategoryId === category.id}
671659
onclick={() => selectCategory(category.id)}
672660
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectCategory(category.id); } }}
673661
>
674662
{#if category.has_children}
675663
<button
676664
type="button"
677-
class="p-0.5 rounded"
678-
style="background: transparent;"
679-
onmouseenter={(e) => e.currentTarget.style.background = 'var(--ds-background-neutral-hovered)'}
680-
onmouseleave={(e) => e.currentTarget.style.background = 'transparent'}
665+
class="hover-bg p-0.5 rounded"
681666
onclick={(e) => { e.stopPropagation(); toggleCategory(category.id); }}
682667
>
683668
{#if expandedCategories.has(category.id)}
@@ -899,10 +884,7 @@
899884
<h2 class="font-semibold truncate" style="color: var(--ds-text);">{selectedAsset.title}</h2>
900885
<div class="flex items-center gap-1">
901886
<button
902-
class="p-1 rounded"
903-
style="background: transparent;"
904-
onmouseenter={(e) => e.currentTarget.style.background = 'var(--ds-background-neutral-hovered)'}
905-
onmouseleave={(e) => e.currentTarget.style.background = 'transparent'}
887+
class="hover-bg p-1 rounded"
906888
onclick={() => { showRelationshipGraph = true; }}
907889
title="Relationship Graph"
908890
>
@@ -911,21 +893,15 @@
911893
{#if canEdit}
912894
<button
913895
data-testid="asset-edit"
914-
class="p-1 rounded"
915-
style="background: transparent;"
916-
onmouseenter={(e) => e.currentTarget.style.background = 'var(--ds-background-neutral-hovered)'}
917-
onmouseleave={(e) => e.currentTarget.style.background = 'transparent'}
896+
class="hover-bg p-1 rounded"
918897
onclick={() => showEditAssetForm(selectedAsset)}
919898
title={t('common.edit')}
920899
>
921900
<IconEdit class="w-4 h-4" style="color: var(--ds-icon);" />
922901
</button>
923902
{/if}
924903
<button
925-
class="p-1 rounded"
926-
style="background: transparent;"
927-
onmouseenter={(e) => e.currentTarget.style.background = 'var(--ds-background-neutral-hovered)'}
928-
onmouseleave={(e) => e.currentTarget.style.background = 'transparent'}
904+
class="hover-bg p-1 rounded"
929905
onclick={() => { selectedAsset = null; updateQueryParams({ asset: null }); }}
930906
>
931907
<IconChevronRight class="w-4 h-4" style="color: var(--ds-icon);" />
@@ -1044,3 +1020,19 @@
10441020
{#if selectedAsset}
10451021
<AssetRelationshipGraph bind:isOpen={showRelationshipGraph} assetId={selectedAsset.id} />
10461022
{/if}
1023+
1024+
<style>
1025+
.nav-link {
1026+
color: var(--ds-text-subtle);
1027+
}
1028+
1029+
.nav-link:hover:not(.active) {
1030+
background: var(--ds-background-neutral-hovered);
1031+
color: var(--ds-text);
1032+
}
1033+
1034+
.nav-link.active {
1035+
background: var(--ds-surface-selected);
1036+
color: var(--ds-text);
1037+
}
1038+
</style>

frontend/src/lib/features/assets/AssetManager.svelte

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -767,10 +767,7 @@
767767
{#if category.has_children}
768768
<button
769769
onclick={() => toggleCategory(category.id)}
770-
class="p-1 rounded"
771-
style="background: transparent;"
772-
onmouseenter={(e) => e.currentTarget.style.background = 'var(--ds-background-neutral-hovered)'}
773-
onmouseleave={(e) => e.currentTarget.style.background = 'transparent'}
770+
class="hover-bg p-1 rounded"
774771
>
775772
{#if expandedCategories.has(category.id)}
776773
<IconChevronDown class="w-4 h-4" style="color: var(--ds-icon);" />

0 commit comments

Comments
 (0)