Skip to content

Commit a5c7236

Browse files
authored
feat(themebuilder): add severity colors (#4164)
1 parent 52f2140 commit a5c7236

14 files changed

Lines changed: 370 additions & 23 deletions

File tree

apps/themebuilder/app/_components/color-contrasts/color-contrasts.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ const ColorContrastMapper = ({
103103
variant: string;
104104
}) => {
105105
const { t } = useTranslation();
106-
const { colorScheme, colors } = useThemebuilder();
106+
const { colorScheme, colors, severityColors, severityEnabled } =
107+
useThemebuilder();
107108
const [selectedColor, setSelectedColor] = useState('dominant');
108109

109110
const getMappedTheme = () => {
@@ -120,6 +121,16 @@ const ColorContrastMapper = ({
120121
}
121122
}
122123
}
124+
125+
// Check severity colors if enabled
126+
if (severityEnabled) {
127+
for (const color of severityColors) {
128+
if (color.name === selectedColor) {
129+
colorTheme = color.colors;
130+
break;
131+
}
132+
}
133+
}
123134
}
124135

125136
for (const [, value] of Object.entries(colorTheme[colorScheme])) {
@@ -163,6 +174,12 @@ const ColorContrastMapper = ({
163174
</Select.Option>
164175
)),
165176
)}
177+
{severityEnabled &&
178+
severityColors.map((color, index) => (
179+
<Select.Option key={`severity-${index}`} value={color.name}>
180+
{color.name}
181+
</Select.Option>
182+
))}
166183
</Select>
167184
</Field>
168185
<div className={classes.tableContainer}>

apps/themebuilder/app/_components/color-preview/color-preview.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,16 @@ const CardWrapper = ({ color, view, ...rest }: CardProps) =>
2626

2727
export const ColorPreview = () => {
2828
const { t } = useTranslation();
29-
const { colorScheme, colors } = useThemebuilder();
29+
const { colorScheme, colors, severityColors, severityEnabled } =
30+
useThemebuilder();
3031
const [view, setView] = useState<ViewType>(DEFAULT_VIEW);
3132

32-
const allColors = [...colors.main, ...colors.neutral, ...colors.support];
33+
const allColors = [
34+
...colors.main,
35+
...colors.neutral,
36+
...colors.support,
37+
...(severityEnabled ? severityColors : []),
38+
];
3339

3440
const prepVariables = (variables: Record<string, string>) => {
3541
const prepped: Record<string, string> = {};

apps/themebuilder/app/_components/colors/colors.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@ import { Scale } from '../scale/scale';
44
import classes from './colors.module.css';
55

66
export const Colors = () => {
7+
const { severityEnabled } = useThemebuilder();
8+
79
return (
810
<div className={classes.rows}>
911
<MainColors />
1012
<Divider />
1113
<NeutralColor />
1214
<Divider />
1315
<SupportColors />
16+
{severityEnabled && (
17+
<>
18+
<Divider />
19+
<SeverityColors />
20+
</>
21+
)}
1422
</div>
1523
);
1624
};
@@ -69,3 +77,18 @@ const SupportColors = () => {
6977
</div>
7078
);
7179
};
80+
81+
const SeverityColors = () => {
82+
const { severityColors } = useThemebuilder();
83+
84+
return (
85+
<div className={classes.rows}>
86+
{severityColors.map((color, index) => (
87+
<div key={index} className={classes.row}>
88+
<div className={classes.scaleLabel}>{color.name}</div>
89+
<Scale colorScale={color.colors} namespace={color.name} />
90+
</div>
91+
))}
92+
</div>
93+
);
94+
};

apps/themebuilder/app/_components/sidebar/color-page/color-page.tsx

Lines changed: 178 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import { Button, Heading } from '@digdir/designsystemet-react';
1+
import {
2+
Button,
3+
Heading,
4+
Paragraph,
5+
Switch,
6+
} from '@digdir/designsystemet-react';
27
import { PlusIcon } from '@navikt/aksel-icons';
38
import { useState } from 'react';
49
import { ColorService, useColor } from 'react-color-palette';
510
import { useTranslation } from 'react-i18next';
611
import { useSearchParams } from 'react-router';
7-
import type { ColorTheme } from '~/routes/themebuilder/_utils/use-themebuilder';
12+
import type {
13+
ColorTheme,
14+
SeverityColorTheme,
15+
} from '~/routes/themebuilder/_utils/use-themebuilder';
816
import {
917
QUERY_SEPARATOR,
1018
useThemebuilder,
@@ -14,8 +22,8 @@ import { ColorPane } from '../color-pane/color-pane';
1422
import classes from './color-page.module.css';
1523

1624
type ColorEditorState = {
17-
activePanel: 'add-color' | 'edit-color' | 'none';
18-
colorType: 'main' | 'neutral' | 'support';
25+
activePanel: 'add-color' | 'edit-color' | 'edit-severity' | 'none';
26+
colorType: 'main' | 'neutral' | 'support' | 'severity';
1927
index: number;
2028
name: string;
2129
initialName: string;
@@ -26,7 +34,7 @@ const DEFAULT_COLOR = '#0062ba';
2634

2735
export const ColorPage = () => {
2836
const { t } = useTranslation();
29-
const { colors } = useThemebuilder();
37+
const { colors, severityColors, severityEnabled } = useThemebuilder();
3038
const [query, setQuery] = useSearchParams();
3139

3240
const [editorState, setEditorState] = useState<ColorEditorState>({
@@ -63,11 +71,42 @@ export const ColorPage = () => {
6371
updatedParams.set(type, colorArray.join(QUERY_SEPARATOR));
6472
} else if (type === 'neutral') {
6573
updatedParams.set('neutral', hex);
74+
} else if (type === 'severity') {
75+
updateSeverityColorInParams(hex, name, updatedParams);
6676
}
6777

6878
setQuery(updatedParams, { replace: true, preventScrollReset: true });
6979
};
7080

81+
const updateSeverityColorInParams = (
82+
hex: string,
83+
name: string,
84+
params: URLSearchParams,
85+
) => {
86+
const severityParam = params.get('severity') || '';
87+
const severityArray = severityParam.split(QUERY_SEPARATOR).filter(Boolean);
88+
89+
// Find the severity color and check if it's the default
90+
const defaultColor = severityColors.find((sc) => sc.name === name);
91+
if (!defaultColor) return;
92+
93+
const isDefault = hex.toLowerCase() === defaultColor.hex.toLowerCase();
94+
95+
// Remove existing entry for this severity color
96+
const filtered = severityArray.filter((s) => !s.startsWith(`${name}:`));
97+
98+
// Only add if it's not the default value
99+
if (!isDefault) {
100+
filtered.push(`${name}:${hex}`);
101+
}
102+
103+
if (filtered.length > 0) {
104+
params.set('severity', filtered.join(QUERY_SEPARATOR));
105+
} else {
106+
params.delete('severity');
107+
}
108+
};
109+
71110
const openColorEditor = (
72111
colorTheme: ColorTheme,
73112
index: number,
@@ -105,6 +144,24 @@ export const ColorPage = () => {
105144
updateColorInParams(DEFAULT_COLOR, newColorName, type, colorCount);
106145
};
107146

147+
const openSeverityColorEditor = (
148+
severityColor: SeverityColorTheme,
149+
index: number,
150+
) => {
151+
const hexColor = severityColor.hex;
152+
153+
setColor(ColorService.convert('hex', hexColor));
154+
155+
setEditorState({
156+
activePanel: 'edit-severity',
157+
colorType: 'severity',
158+
index: index,
159+
name: severityColor.name,
160+
initialName: severityColor.name,
161+
initialHex: hexColor,
162+
});
163+
};
164+
108165
const closeEditor = () => {
109166
setEditorState((prev) => ({
110167
...prev,
@@ -130,6 +187,17 @@ export const ColorPage = () => {
130187
}
131188
};
132189

190+
const toggleSeverityColors = (enabled: boolean) => {
191+
const updatedParams = new URLSearchParams(query);
192+
if (enabled) {
193+
updatedParams.set('severity-enabled', 'true');
194+
} else {
195+
updatedParams.delete('severity-enabled');
196+
updatedParams.delete('severity'); // Also remove any severity color overrides
197+
}
198+
setQuery(updatedParams, { replace: true, preventScrollReset: true });
199+
};
200+
133201
return (
134202
<div>
135203
{editorState.activePanel === 'none' && (
@@ -194,7 +262,9 @@ export const ColorPage = () => {
194262
</Button>
195263
)}{' '}
196264
{colors.support.length >= 40 && (
197-
<div className={classes.error}>Maximum 4 support colours</div>
265+
<div className={classes.error}>
266+
{t('themeModal.max-X-colors', { count: 4 })}
267+
</div>
198268
)}
199269
</div>
200270
<div className={classes.colors}>
@@ -208,6 +278,50 @@ export const ColorPage = () => {
208278
))}
209279
</div>
210280
</div>
281+
282+
<div className={classes.separator}></div>
283+
<div className={classes.group}>
284+
<div className={classes.groupHeader}>
285+
<Heading data-size='2xs' id='severity-colors-heading'>
286+
{t('themeModal.severity-colors')}
287+
</Heading>
288+
<Switch
289+
name='severity-colors-switch'
290+
data-size='sm'
291+
checked={severityEnabled}
292+
onChange={(e) => toggleSeverityColors(e.target.checked)}
293+
aria-labelledby='severity-colors-heading'
294+
aria-describedby='severity-colors-description'
295+
/>
296+
</div>
297+
{!severityEnabled && (
298+
<Paragraph
299+
id='severity-colors-description'
300+
data-size='sm'
301+
style={{
302+
marginTop: '-4px',
303+
marginBottom: '8px',
304+
color: 'var(--ds-color-neutral-text-subtle)',
305+
}}
306+
>
307+
{t('themeModal.severity-colors-switch')}
308+
</Paragraph>
309+
)}
310+
{severityEnabled && (
311+
<div className={classes.colors}>
312+
{severityColors.map((severityColor, index) => (
313+
<ColorInput
314+
key={severityColor.name}
315+
color={severityColor.hex}
316+
name={severityColor.name}
317+
onClick={() =>
318+
openSeverityColorEditor(severityColor, index)
319+
}
320+
/>
321+
))}
322+
</div>
323+
)}
324+
</div>
211325
</>
212326
)}
213327

@@ -218,7 +332,12 @@ export const ColorPage = () => {
218332
closeEditor();
219333
}}
220334
onRemove={() => {
221-
removeColor(editorState.index, editorState.colorType);
335+
if (
336+
editorState.colorType === 'main' ||
337+
editorState.colorType === 'support'
338+
) {
339+
removeColor(editorState.index, editorState.colorType);
340+
}
222341
closeEditor();
223342
}}
224343
onCancel={() => {
@@ -253,7 +372,58 @@ export const ColorPage = () => {
253372
editorState.index,
254373
);
255374
}}
256-
colorType={editorState.colorType}
375+
colorType={
376+
editorState.colorType === 'severity'
377+
? 'neutral'
378+
: editorState.colorType
379+
}
380+
/>
381+
)}
382+
383+
{editorState.activePanel === 'edit-severity' && (
384+
<ColorPane
385+
onClose={() => {
386+
closeEditor();
387+
}}
388+
onRemove={() => {
389+
// Reset to default by removing from query params
390+
const updatedParams = new URLSearchParams(query);
391+
updateSeverityColorInParams(
392+
editorState.initialHex,
393+
editorState.name,
394+
updatedParams,
395+
);
396+
setQuery(updatedParams, {
397+
replace: true,
398+
preventScrollReset: true,
399+
});
400+
closeEditor();
401+
}}
402+
onCancel={() => {
403+
updateColorInParams(
404+
editorState.initialHex,
405+
editorState.initialName,
406+
editorState.colorType,
407+
editorState.index,
408+
);
409+
closeEditor();
410+
}}
411+
type='edit-color'
412+
color={color}
413+
name={editorState.name}
414+
setColor={(newColor) => {
415+
setColor(newColor);
416+
updateColorInParams(
417+
newColor.hex,
418+
editorState.name,
419+
editorState.colorType,
420+
editorState.index,
421+
);
422+
}}
423+
setName={() => {
424+
// Name changes not allowed for severity colors
425+
}}
426+
colorType='severity'
257427
/>
258428
)}
259429
</div>

0 commit comments

Comments
 (0)