Skip to content

Commit 810c151

Browse files
author
Nick Campanini
committed
Wire up CO visual walkthrough fixes: button colors, heading styles, logo sizing, dashboard alerts, font variable
1 parent 96bfc17 commit 810c151

10 files changed

Lines changed: 169 additions & 24 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { readFileSync } from 'node:fs'
2+
import { resolve } from 'node:path'
3+
import { describe, expect, it } from 'vitest'
4+
5+
import { primaryFont } from './fonts'
6+
7+
/**
8+
* Contract test: the SCSS font override must reference the same CSS variable
9+
* that the font generator produces. A mismatch causes a FOUT (flash of unstyled
10+
* text) because the SCSS !important override resolves to an undefined variable,
11+
* falling back to the browser's default serif font.
12+
*
13+
* This caught a real bug: generate-fonts.js was updated in DC-143 to produce
14+
* --font-primary, but _uswds-theme-custom-styles.scss still referenced
15+
* --font-urbanist.
16+
*/
17+
describe('font variable contract', () => {
18+
const scssPath = resolve(__dirname, 'sass/_uswds-theme-custom-styles.scss')
19+
const scssContent = readFileSync(scssPath, 'utf-8')
20+
21+
it('primaryFont exports a CSS variable name', () => {
22+
expect(primaryFont.variable).toBeDefined()
23+
expect(primaryFont.variable).toMatch(/^--font-/)
24+
})
25+
26+
it('SCSS font override references the same variable as primaryFont', () => {
27+
const cssVariable = primaryFont.variable
28+
expect(scssContent).toContain(`var(${cssVariable})`)
29+
})
30+
31+
it('SCSS font override does not reference stale font variable names', () => {
32+
// Guard against the specific bug: old variable names left behind after
33+
// the font generator is updated to produce a different variable.
34+
const fontVarPattern = /var\(--font-([^)]+)\)/g
35+
const referencedVars = [...scssContent.matchAll(fontVarPattern)].map((m) => `--font-${m[1]}`)
36+
37+
for (const varName of referencedVars) {
38+
expect(varName).toBe(primaryFont.variable)
39+
}
40+
})
41+
})

src/SEBT.Portal.Web/design/sass/_uswds-theme-custom-styles.scss

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,22 @@ References:
2323
Custom Font Override
2424
----------------------------------------
2525
Override USWDS default fonts with our custom font from Next.js font loader.
26-
The --font-urbanist CSS variable is set by Next.js in layout.tsx.
26+
The --font-primary CSS variable is set by Next.js in layout.tsx.
2727
This ensures all USWDS components use our design token font.
2828
----------------------------------------
2929
*/
3030

3131
// Override all USWDS font-family declarations to use our custom font
3232
// This is necessary because USWDS sets font-family on many utility classes
33-
// TODO: Register Urbanist as a custom USWDS typeface to remove !important hack
33+
// TODO: Register the design token font as a custom USWDS typeface to remove !important hack
3434
// See: https://designsystem.digital.gov/documentation/settings/#typography-settings
3535
body,
3636
.usa-prose,
3737
[class*='font-'],
3838
[class*='text-'],
3939
[class*='usa-'] {
4040
font-family:
41-
var(--font-urbanist),
41+
var(--font-primary),
4242
system-ui,
4343
-apple-system,
4444
BlinkMacSystemFont,

src/SEBT.Portal.Web/design/sass/components/_buttons.scss

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,44 @@ Typography: 16px, Bold (700), Line height 24px
6868
}
6969
}
7070
}
71+
72+
// Temporary CO override: DC uses secondary (gold) for buttons, but CO's secondary
73+
// is red — intended for error/validation states, not interactive controls.
74+
// CO mockups show buttons in the primary teal range.
75+
// TODO: Resolve with a proper per-state button token mapping so this override
76+
// is unnecessary. See design token discussion re: semantic color roles.
77+
html[data-state='co'] {
78+
.usa-button:not(.usa-language__link):not(.usa-button--unstyled):not(.usa-button--outline) {
79+
background-color: color('primary');
80+
border-color: color('primary');
81+
color: color('white');
82+
83+
&:hover {
84+
background-color: color('primary-dark');
85+
border-color: color('primary-dark');
86+
}
87+
88+
&:active {
89+
background-color: color('primary-darker');
90+
border-color: color('primary-darker');
91+
}
92+
}
93+
94+
.usa-button--outline:not(.usa-language__link) {
95+
background-color: color('white');
96+
border: 2px solid color('primary');
97+
color: color('primary');
98+
99+
&:hover {
100+
background-color: color('primary-lightest');
101+
border-color: color('primary-dark');
102+
color: color('primary-dark');
103+
}
104+
105+
&:active {
106+
background-color: color('primary-lighter');
107+
border-color: color('primary-darker');
108+
color: color('primary-darker');
109+
}
110+
}
111+
}

src/SEBT.Portal.Web/src/app/(authenticated)/profile/address/(flow)/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ export default function AddressFormPage() {
2929
}
3030

3131
return (
32-
<div className="grid-container maxw-tablet">
33-
<h1>{t('pageTitle', 'Tell us where to safely send your mail')}</h1>
32+
<div className="grid-container maxw-tablet padding-top-4 padding-bottom-4">
33+
<h1 className="font-sans-xl text-primary">
34+
{t('pageTitle', 'Tell us where to safely send your mail')}
35+
</h1>
3436
<p className="usa-hint">
3537
{t('requiredFieldsNote', 'Asterisks (*) indicate a required field')}
3638
</p>

src/SEBT.Portal.Web/src/app/(authenticated)/profile/address/(flow)/replacement-cards/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export default function ReplacementCardsPage() {
2424
}
2525

2626
return (
27-
<div className="grid-container maxw-tablet">
28-
<h1>
27+
<div className="grid-container maxw-tablet padding-top-4 padding-bottom-4">
28+
<h1 className="font-sans-xl text-primary">
2929
{t(
3030
'replacementCardsTitle',
3131
`Do you want to request replacement ${programName} cards to be sent to this address?`

src/SEBT.Portal.Web/src/app/(authenticated)/profile/address/(flow)/replacement-cards/select/page.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ export default function CardSelectionPage() {
88
const { t } = useTranslation('confirmInfo')
99

1010
return (
11-
<div className="grid-container maxw-tablet">
12-
<h1>{t('cardSelectionTitle', 'Which cards need to be replaced?')}</h1>
11+
<div className="grid-container maxw-tablet padding-top-4 padding-bottom-4">
12+
<h1 className="font-sans-xl text-primary">
13+
{t('cardSelectionTitle', 'Which cards need to be replaced?')}
14+
</h1>
1315
<CardSelection />
1416
</div>
1517
)

src/SEBT.Portal.Web/src/components/layout/Header.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,28 @@ import { useTranslation } from 'react-i18next'
77
import { LanguageSelector } from './LanguageSelector'
88
import type { HeaderProps } from './types'
99

10+
// Logo dimensions match each state's SVG viewBox so the image renders
11+
// at its natural aspect ratio. maxh-6 caps the height for states with
12+
// taller logos (DC), while wider logos (CO) spread horizontally.
13+
const logoDimensions: Record<string, { width: number; height: number }> = {
14+
dc: { width: 122, height: 52 },
15+
co: { width: 192, height: 28 }
16+
}
17+
1018
export function Header({ state = 'dc' }: HeaderProps) {
1119
const { t } = useTranslation('common')
20+
const defaultDimensions = { width: 122, height: 52 }
21+
// eslint-disable-next-line security/detect-object-injection -- state is typed StateCode
22+
const { width, height } = logoDimensions[state] ?? defaultDimensions
1223

1324
return (
1425
<header
1526
className="usa-header usa-header--basic bg-white shadow-2"
1627
role="banner"
1728
>
18-
<div className="display-flex flex-justify flex-align-center width-full padding-y-105 padding-left-1 padding-right-3">
29+
<div className="display-flex flex-justify flex-align-center width-full padding-y-105 padding-x-2">
1930
<div className="usa-navbar border-0">
20-
<div className="usa-logo">
31+
<div className="usa-logo margin-left-0">
2132
<Link
2233
href="/"
2334
title="Home"
@@ -27,9 +38,10 @@ export function Header({ state = 'dc' }: HeaderProps) {
2738
<Image
2839
src={`/images/states/${state}/logo.svg`}
2940
alt={t('logoAlt')}
30-
width={121}
31-
height={51}
41+
width={width}
42+
height={height}
3243
priority
44+
className="maxw-full height-auto maxh-6"
3345
/>
3446
</Link>
3547
</div>

src/SEBT.Portal.Web/src/components/layout/HelpSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ function COHelpSection({ state = 'co' }: HelpSectionProps) {
110110
href={links.footer.digitalAccessibility ?? '#'}
111111
target="_blank"
112112
rel="noopener noreferrer"
113-
className="usa-button usa-button--outline border-primary text-primary"
113+
className="usa-button usa-button--outline border-primary text-primary display-block text-center"
114114
>
115115
{/* TODO: Use t('digitalAccessibilityStatement') once the key is added to co.csv */}
116116
Digital accessibility statement

src/SEBT.Portal.Web/src/features/address/components/ReplacementCardPrompt/ReplacementCardPrompt.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function ReplacementCardPrompt({ address }: ReplacementCardPromptProps) {
8484
</Alert>
8585
)}
8686

87-
<p className="usa-hint">
87+
<p className="usa-hint margin-bottom-3">
8888
{t('requiredFieldsNote', 'Asterisks (*) indicate a required field')}
8989
</p>
9090

@@ -93,7 +93,7 @@ export function ReplacementCardPrompt({ address }: ReplacementCardPromptProps) {
9393
aria-label={t('selectOneLabel', 'Select one')}
9494
aria-describedby={error ? 'replacement-choice-error' : undefined}
9595
>
96-
<legend className="usa-legend">
96+
<legend className="usa-legend text-bold">
9797
{t('selectOneLabel', 'Select one')}
9898
<span className="text-secondary-dark"> *</span>
9999
</legend>
@@ -112,7 +112,7 @@ export function ReplacementCardPrompt({ address }: ReplacementCardPromptProps) {
112112

113113
<div className="usa-radio">
114114
<input
115-
className="usa-radio__input"
115+
className="usa-radio__input usa-radio__input--tile"
116116
type="radio"
117117
id="replacement-yes"
118118
name="replacementChoice"
@@ -124,7 +124,7 @@ export function ReplacementCardPrompt({ address }: ReplacementCardPromptProps) {
124124
}}
125125
/>
126126
<label
127-
className="usa-radio__label"
127+
className="usa-radio__label text-bold"
128128
htmlFor="replacement-yes"
129129
>
130130
{tCommon('yes', 'Yes')}
@@ -133,7 +133,7 @@ export function ReplacementCardPrompt({ address }: ReplacementCardPromptProps) {
133133

134134
<div className="usa-radio">
135135
<input
136-
className="usa-radio__input"
136+
className="usa-radio__input usa-radio__input--tile"
137137
type="radio"
138138
id="replacement-no"
139139
name="replacementChoice"
@@ -145,7 +145,7 @@ export function ReplacementCardPrompt({ address }: ReplacementCardPromptProps) {
145145
}}
146146
/>
147147
<label
148-
className="usa-radio__label"
148+
className="usa-radio__label text-bold"
149149
htmlFor="replacement-no"
150150
>
151151
{tCommon('no', 'No')}

src/SEBT.Portal.Web/src/features/household/components/DashboardAlerts/DashboardAlerts.tsx

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
55
import { useEffect, useState } from 'react'
66

77
/**
8-
* Displays success alerts on the dashboard triggered by URL search params.
8+
* Displays success and warning alerts on the dashboard triggered by URL search params.
99
* Captures alert state on first read, then cleans the params from the URL.
1010
* The alert persists because rendering is driven by captured state, not live params.
1111
* Extensible: add new param checks for future alert types (e.g., DC-153 card ordering).
@@ -19,10 +19,20 @@ export function DashboardAlerts() {
1919
// survives the URL cleanup that follows.
2020
const [alerts] = useState(() => ({
2121
addressUpdated: searchParams.get('addressUpdated') === 'true',
22-
cardsRequested: searchParams.get('cardsRequested') === 'true'
22+
cardsRequested: searchParams.get('cardsRequested') === 'true',
23+
addressUpdateFailed: searchParams.get('addressUpdateFailed') === 'true',
24+
contactUpdateFailed: searchParams.get('contactUpdateFailed') === 'true',
25+
// TODO: Determine trigger logic — possibly driven by household data (e.g., address
26+
// hasn't been confirmed in N months, or address on file doesn't match state records).
27+
addressVerification: searchParams.get('addressVerification') === 'true'
2328
}))
2429

25-
const hasAlerts = alerts.addressUpdated || alerts.cardsRequested
30+
const hasAlerts =
31+
alerts.addressUpdated ||
32+
alerts.cardsRequested ||
33+
alerts.addressUpdateFailed ||
34+
alerts.contactUpdateFailed ||
35+
alerts.addressVerification
2636

2737
useEffect(() => {
2838
if (hasAlerts) {
@@ -35,7 +45,7 @@ export function DashboardAlerts() {
3545
}
3646

3747
return (
38-
<div className="margin-bottom-3">
48+
<div className="margin-bottom-3 display-flex flex-column gap-2">
3949
{alerts.addressUpdated && !alerts.cardsRequested && (
4050
<Alert
4151
variant="success"
@@ -59,6 +69,43 @@ export function DashboardAlerts() {
5969
integration is pending — changes are not yet reflected in the benefits system.
6070
</Alert>
6171
)}
72+
73+
{/* Warning alerts per CO-05 mockup — yellow with dark yellow left border.
74+
TODO: Wire to actual error flows once state connector persistence is integrated.
75+
Currently triggered by URL params for visual verification. */}
76+
77+
{alerts.addressUpdateFailed && (
78+
<Alert
79+
variant="warning"
80+
// TODO: Use t('addressUpdateFailedHeading') once key is in CSV
81+
heading="There was an issue updating your mailing address."
82+
>
83+
{/* TODO: Use t('addressUpdateFailedBody') once key is in CSV */}
84+
Please try again later or contact the Summer EBT Help Desk for assistance.
85+
</Alert>
86+
)}
87+
88+
{alerts.contactUpdateFailed && (
89+
<Alert
90+
variant="warning"
91+
// TODO: Use t('contactUpdateFailedHeading') once key is in CSV
92+
heading="There was an issue updating your contact preferences."
93+
>
94+
{/* TODO: Use t('contactUpdateFailedBody') once key is in CSV */}
95+
Please try again later.
96+
</Alert>
97+
)}
98+
99+
{alerts.addressVerification && (
100+
<Alert
101+
variant="warning"
102+
// TODO: Use t('addressVerificationHeading') once key is in CSV
103+
heading="Is your address correct?"
104+
>
105+
{/* TODO: Use t('addressVerificationBody') once key is in CSV */}
106+
Please verify your mailing address is up to date so you can receive your Summer EBT cards.
107+
</Alert>
108+
)}
62109
</div>
63110
)
64111
}

0 commit comments

Comments
 (0)