Skip to content

Commit 485d442

Browse files
authored
feat(themebuilder): config file (#4116)
1 parent 9300714 commit 485d442

13 files changed

Lines changed: 505 additions & 234 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Link, Paragraph, Switch } from '@digdir/designsystemet-react';
2+
import { CodeBlock } from '@internal/components';
3+
import { useState } from 'react';
4+
import { useTranslation } from 'react-i18next';
5+
import classes from '../token-modal.module.css';
6+
7+
export default function Cli({
8+
buildSnippet,
9+
cliSnippet,
10+
}: {
11+
buildSnippet: string;
12+
cliSnippet: {
13+
windows: string;
14+
unix: string;
15+
};
16+
}) {
17+
const { t } = useTranslation();
18+
19+
const [formatWin, setFormatWin] = useState(
20+
navigator.userAgent.includes('Windows'),
21+
);
22+
23+
return (
24+
<>
25+
<div className={classes.step}>
26+
<span>1</span>
27+
<Paragraph>
28+
{t('themeModal.cli.step-one')}{' '}
29+
<Link
30+
target='_blank'
31+
href='https://www.figma.com/community/plugin/1382044395533039221/designsystemet-beta'
32+
>
33+
{t('themeModal.figma-plugin')}
34+
</Link>{' '}
35+
{t('themeModal.in')}{' '}
36+
<Link
37+
target='_blank'
38+
href='https://www.figma.com/community/file/1322138390374166141'
39+
>
40+
{t('themeModal.core-ui-kit')}
41+
</Link>{' '}
42+
{t('themeModal.to-update')}{' '}
43+
<Link
44+
target='_blank'
45+
href='https://www.designsystemet.no/no/fundamentals/themebuilder/own-theme'
46+
>
47+
{t('themeModal.own-theme')}
48+
</Link>{' '}
49+
{t('themeModal.page')}
50+
</Paragraph>
51+
</div>
52+
<div className={classes.snippet}>
53+
<Switch
54+
style={{
55+
marginInlineStart: 'auto',
56+
width: 'fit-content',
57+
}}
58+
position='end'
59+
label={t('themeModal.format')}
60+
checked={formatWin}
61+
onChange={(e) => {
62+
setFormatWin(e.currentTarget.checked);
63+
}}
64+
/>
65+
<CodeBlock language='bash'>
66+
{formatWin ? cliSnippet.windows : cliSnippet.unix}
67+
</CodeBlock>
68+
</div>
69+
<div
70+
className={classes.step}
71+
style={{
72+
marginTop: 'var(--ds-size-4)',
73+
}}
74+
>
75+
<span>2</span>
76+
<Paragraph>{t('themeModal.cli.step-two')}</Paragraph>
77+
</div>
78+
<div className={classes.snippet}>
79+
<CodeBlock language='bash'>{buildSnippet}</CodeBlock>
80+
</div>
81+
</>
82+
);
83+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Paragraph } from '@digdir/designsystemet-react';
2+
import { CodeBlock } from '@internal/components';
3+
import { useTranslation } from 'react-i18next';
4+
import classes from '../token-modal.module.css';
5+
6+
export default function Config({
7+
configSnippet,
8+
buildSnippet,
9+
}: {
10+
configSnippet: string;
11+
buildSnippet: string;
12+
}) {
13+
const { t } = useTranslation();
14+
15+
return (
16+
<>
17+
<div className={classes.step}>
18+
<span>1</span>
19+
<Paragraph>{t('themeModal.config.step-one')}</Paragraph>
20+
</div>
21+
<div className={classes.snippet}>
22+
<CodeBlock language='json'>{configSnippet}</CodeBlock>
23+
</div>
24+
<div
25+
className={classes.step}
26+
style={{
27+
marginTop: 'var(--ds-size-4)',
28+
}}
29+
>
30+
<span>2</span>
31+
<Paragraph>{t('themeModal.config.step-two')}</Paragraph>
32+
</div>
33+
<div className={classes.snippet}>
34+
<CodeBlock language='bash'>{buildSnippet}</CodeBlock>
35+
</div>
36+
</>
37+
);
38+
}

apps/themebuilder/app/_components/token-modal/token-modal.module.css

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,18 +112,21 @@
112112
.step {
113113
display: flex;
114114
gap: var(--ds-size-4);
115+
& > span {
116+
background-color: var(--ds-color-accent-base-default);
117+
color: var(--ds-color-accent-base-contrast-default);
118+
aspect-ratio: 1;
119+
border-radius: var(--ds-border-radius-full);
120+
width: var(--ds-size-8);
121+
height: var(--ds-size-8);
122+
display: grid;
123+
place-items: center;
124+
font-size: var(--ds-body-sm-font-size);
125+
}
115126
}
116127

117-
.step span {
118-
background-color: var(--ds-color-accent-base-default);
119-
color: var(--ds-color-accent-base-contrast-default);
120-
aspect-ratio: 1;
121-
border-radius: var(--ds-border-radius-full);
122-
width: var(--ds-size-8);
123-
height: var(--ds-size-8);
124-
display: grid;
125-
place-items: center;
126-
font-size: var(--ds-body-sm-font-size);
128+
.tabpanel {
129+
padding: var(--ds-size-4) 0;
127130
}
128131

129132
@media (max-width: 1150px) {

apps/themebuilder/app/_components/token-modal/token-modal.tsx

Lines changed: 25 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,25 @@
1-
import type { Color, CssColor, ThemeInfo } from '@digdir/designsystemet/color';
21
import {
3-
type CreateTokensOptions,
4-
cliOptions,
5-
formatThemeCSS,
6-
} from '@digdir/designsystemet/tokens';
7-
import {
8-
Button,
92
Dialog,
103
Divider,
114
Heading,
125
Input,
136
Link,
147
Paragraph,
15-
Switch,
8+
Tabs,
169
} from '@digdir/designsystemet-react';
17-
import { CodeBlock } from '@internal/components';
1810
import { InformationSquareIcon, StarIcon } from '@navikt/aksel-icons';
19-
import { useRef, useState } from 'react';
11+
import { useRef } from 'react';
2012
import { useTranslation } from 'react-i18next';
21-
import { useLoaderData } from 'react-router';
22-
import { useThemebuilder } from '~/routes/themebuilder/_utils/use-themebuilder';
13+
import Cli from './steps/cli';
14+
import Config from './steps/config';
2315
import classes from './token-modal.module.css';
24-
25-
type ColorTheme = {
26-
name: string;
27-
colors: ThemeInfo;
28-
};
29-
30-
const colorCliOptions = cliOptions.theme.colors;
31-
32-
const getBaseDefault = (colorTheme: Color[]) =>
33-
colorTheme.find((color) => color.name === 'base-default');
34-
35-
const LOADING_CSS_MESSAGE = 'Generating CSS...';
36-
const FEAT_THEME_CSS = false; // TODO set to false before merging
16+
import { useTokenModal } from './use-token-modal';
3717

3818
export const TokenModal = () => {
3919
const { t } = useTranslation();
4020
const modalRef = useRef<HTMLDialogElement>(null);
41-
const { isProduction } = useLoaderData();
42-
43-
// Use separate selectors for better performance
44-
const { colors, baseBorderRadius } = useThemebuilder();
45-
46-
const [themeName, setThemeName] = useState('theme');
47-
const [themeCSS, setThemeCSS] = useState('');
48-
const [formatWin, setFormatWin] = useState(
49-
navigator.userAgent.includes('Windows'),
50-
);
51-
52-
const setCliColors = (colorTheme: ColorTheme[]) => {
53-
if (!colorTheme.length) return '';
54-
55-
return (
56-
colorTheme
57-
.map((theme) => {
58-
const baseColor = getBaseDefault(theme.colors.light);
59-
return `"${theme.name}:${baseColor?.hex}"`;
60-
})
61-
.join(' ') + ' '
62-
);
63-
};
64-
65-
const packageWithTag = `@digdir/designsystemet${isProduction ? '@latest' : '@next'}`;
66-
const buildSnippet = `npx ${packageWithTag} tokens build`;
67-
const seperator = formatWin ? ' ^\n' : ' \\\n';
68-
69-
const cliSnippet = [
70-
`npx ${packageWithTag} tokens create`,
71-
`--${colorCliOptions.main} ${setCliColors(colors.main).trimEnd()}`,
72-
`--${colorCliOptions.neutral} "${getBaseDefault(colors.neutral[0]?.colors.light)?.hex}"`,
73-
`${colors.support.length > 0 ? `--${colorCliOptions.support} ${setCliColors(colors.support).trimEnd()}` : ''}`,
74-
`--border-radius ${baseBorderRadius}`,
75-
`--theme "${themeName}"`,
76-
]
77-
.filter(Boolean)
78-
.join(seperator);
79-
80-
const theme: CreateTokensOptions = {
81-
name: themeName,
82-
colors: {
83-
main: colors.main.reduce(
84-
(acc, color) => {
85-
acc[color.name] = getBaseDefault(color.colors.light)?.hex || '#';
86-
return acc;
87-
},
88-
{} as Record<string, CssColor>,
89-
),
90-
support: colors.support.reduce(
91-
(acc, color) => {
92-
acc[color.name] = getBaseDefault(color.colors.light)?.hex || '#';
93-
return acc;
94-
},
95-
{} as Record<string, CssColor>,
96-
),
97-
neutral: getBaseDefault(colors.neutral[0]?.colors.light)?.hex || '#',
98-
},
99-
borderRadius: baseBorderRadius,
100-
typography: {
101-
fontFamily: 'Inter',
102-
},
103-
};
104-
105-
const onThemeButtonClick = () => {
106-
if (themeCSS !== LOADING_CSS_MESSAGE) {
107-
setThemeCSS(LOADING_CSS_MESSAGE);
108-
109-
formatThemeCSS(theme).then((css) => {
110-
setThemeCSS(css);
111-
});
112-
}
113-
};
21+
const { themeName, setThemeName, cliSnippet, buildSnippet, configSnippet } =
22+
useTokenModal();
11423

11524
return (
11625
<Dialog.TriggerContext>
@@ -143,7 +52,6 @@ export const TokenModal = () => {
14352
</Heading>
14453
<Paragraph>{t('themeModal.theme-name-description')}</Paragraph>
14554
<Input
146-
aria-label={t('themeModal.theme-name-label')}
14755
name='themeName'
14856
value={themeName}
14957
onChange={(e) => {
@@ -162,72 +70,24 @@ export const TokenModal = () => {
16270
<Dialog.Block>
16371
<div className={classes.content}>
16472
<div className={classes.rightSection}>
165-
{FEAT_THEME_CSS && (
166-
<div className={classes['snippet-themecss']}>
167-
<Button
168-
onClick={onThemeButtonClick}
169-
loading={themeCSS === LOADING_CSS_MESSAGE}
170-
>
171-
{themeCSS === LOADING_CSS_MESSAGE
172-
? t('themeModal.generating-css')
173-
: t('themeModal.generate-css')}
174-
</Button>
175-
{themeCSS && themeCSS !== LOADING_CSS_MESSAGE && (
176-
<CodeBlock language='css'>{themeCSS}</CodeBlock>
177-
)}
178-
</div>
179-
)}{' '}
180-
<div className={classes.step}>
181-
<span>1</span>
182-
<Paragraph>
183-
{t('themeModal.step-one')}{' '}
184-
<Link
185-
target='_blank'
186-
href='https://www.figma.com/community/plugin/1382044395533039221/designsystemet-beta'
187-
>
188-
{t('themeModal.figma-plugin')}
189-
</Link>{' '}
190-
{t('themeModal.in')}{' '}
191-
<Link
192-
target='_blank'
193-
href='https://www.figma.com/community/file/1322138390374166141'
194-
>
195-
{t('themeModal.core-ui-kit')}
196-
</Link>{' '}
197-
{t('themeModal.to-update')}{' '}
198-
<Link
199-
target='_blank'
200-
href='https://www.designsystemet.no/no/fundamentals/themebuilder/own-theme'
201-
>
202-
{t('themeModal.own-theme')}
203-
</Link>{' '}
204-
{t('themeModal.page')}
205-
</Paragraph>
206-
</div>
207-
<div className={classes.snippet}>
208-
<Switch
209-
style={{ marginInlineStart: 'auto', width: 'fit-content' }}
210-
position='end'
211-
label={t('themeModal.format')}
212-
checked={formatWin}
213-
onChange={(e) => {
214-
setFormatWin(e.currentTarget.checked);
215-
}}
216-
/>
217-
<CodeBlock language='bash'>{cliSnippet}</CodeBlock>
218-
</div>
219-
<div
220-
className={classes.step}
221-
style={{
222-
marginTop: 'var(--ds-size-4)',
223-
}}
224-
>
225-
<span>2</span>
226-
<Paragraph>{t('themeModal.step-two')}</Paragraph>
227-
</div>
228-
<div className={classes.snippet}>
229-
<CodeBlock language='bash'>{buildSnippet}</CodeBlock>
230-
</div>
73+
<Tabs defaultValue='config'>
74+
<Tabs.List>
75+
<Tabs.Tab value='config'>Config File</Tabs.Tab>
76+
<Tabs.Tab value='cli'>CLI</Tabs.Tab>
77+
</Tabs.List>
78+
<Tabs.Panel value='cli' className={classes.tabpanel}>
79+
<Cli
80+
cliSnippet={cliSnippet}
81+
buildSnippet={buildSnippet.cli}
82+
/>
83+
</Tabs.Panel>
84+
<Tabs.Panel value='config' className={classes.tabpanel}>
85+
<Config
86+
configSnippet={configSnippet}
87+
buildSnippet={buildSnippet.config}
88+
/>
89+
</Tabs.Panel>
90+
</Tabs>
23191
<Divider />
23292
<div className={classes.contact}>
23393
<div className={classes.contact__icon}>

0 commit comments

Comments
 (0)