diff --git a/apps/_components/src/CodeSnippet/CodeSnippet.tsx b/apps/_components/src/CodeSnippet/CodeSnippet.tsx index bf00ef571e..0b7ae248ec 100644 --- a/apps/_components/src/CodeSnippet/CodeSnippet.tsx +++ b/apps/_components/src/CodeSnippet/CodeSnippet.tsx @@ -27,7 +27,15 @@ const plugins = [ ]; type CodeSnippetProps = { - language?: 'css' | 'html' | 'ts' | 'markdown' | 'json' | 'shell' | 'tsx'; + language?: + | 'css' + | 'html' + | 'ts' + | 'markdown' + | 'json' + | 'shell' + | 'tsx' + | 'bash'; children: string; } & React.HTMLAttributes; diff --git a/apps/_components/src/Container/Container.module.css b/apps/_components/src/Container/Container.module.css index 4ef709ae9d..503558b48c 100644 --- a/apps/_components/src/Container/Container.module.css +++ b/apps/_components/src/Container/Container.module.css @@ -1,5 +1,5 @@ .container { - max-width: var(--grid-max-width, 1620px); + max-width: var(--grid-max-width, 1450px); margin: 0 auto; width: 100%; padding-left: var(--grid-padding, 16px); diff --git a/apps/_components/src/Header/Header.module.css b/apps/_components/src/Header/Header.module.css index efa649d6e6..f5bcb71e80 100644 --- a/apps/_components/src/Header/Header.module.css +++ b/apps/_components/src/Header/Header.module.css @@ -1,5 +1,5 @@ .header { - height: 106px; + height: 100px; display: flex; align-items: center; position: relative; @@ -12,6 +12,10 @@ } } +.header.transparentHeader { + background-color: transparent; +} + .container { display: flex; align-items: center; @@ -49,10 +53,10 @@ } .tag { - background-color: var(--ds-global-purple-5); + background-color: var(--ds-global-purple-4); border-radius: 4px; - padding: 7px 10px; - font-size: 18px; + padding: 4px 10px; + font-size: 16px; font-weight: 500; } diff --git a/apps/_components/src/Header/Header.tsx b/apps/_components/src/Header/Header.tsx index ae67f0da43..85b429bbaa 100644 --- a/apps/_components/src/Header/Header.tsx +++ b/apps/_components/src/Header/Header.tsx @@ -26,6 +26,7 @@ type HeaderProps = { betaTag?: boolean; skipLink?: boolean; themeSwitcher?: boolean; + transparentBackground?: boolean; }; /** @@ -63,6 +64,7 @@ const Header = ({ betaTag, skipLink = true, themeSwitcher = false, + transparentBackground = false, }: HeaderProps) => { const [open, setOpen] = useState(false); const [isHamburger, setIsHamburger] = useState(false); @@ -107,7 +109,11 @@ const Header = ({ Hopp til hovedinnhold ) : null}
diff --git a/apps/theme/app/globals.css b/apps/theme/app/globals.css index b5b9d14202..cf2706bcd8 100644 --- a/apps/theme/app/globals.css +++ b/apps/theme/app/globals.css @@ -31,6 +31,54 @@ code { border-radius: 2px; } +[data-theme='two'] { + --ds-color-accent-base-default: #740c7e; + --ds-color-accent-text-subtle: #93429b; + --ds-color-base-default: #740c7e; + --ds-color-accent-surface-hover: #6c0b75 !important; +} + +[data-color-scheme='dark'][data-theme='two'] { + --ds-color-accent-base-default: #c79dcb; + --ds-color-accent-text-subtle: #c294c6; + --ds-color-base-default: #c79dcb; + --ds-color-accent-surface-hover: #6c0b75 !important; +} + +.panelContainer { + display: flex; +} + +.panelLeft { + display: flex; + flex-direction: column; + gap: 12px; + width: 420px; + padding: 28px; + border-right: 1px solid var(--ds-color-neutral-border-subtle); + position: relative; +} + +.panelRight { + display: grid; + grid-template-columns: repeat(1, 1fr); + grid-auto-rows: min-content; + width: 100%; + gap: 12px; + padding: 28px; + background-color: var(--ds-color-neutral-background-subtle); + border-radius: 0 8px 8px 0; +} + +.panelBottom { + left: 0; + right: 0; + bottom: 0; + padding: 28px; + position: absolute; + border-top: 1px solid var(--ds-color-neutral-border-subtle); +} + .content { - min-height: 100vh; + min-height: 100svh; } diff --git a/apps/theme/app/layout.tsx b/apps/theme/app/layout.tsx index 57cefa9803..8c6f3ca838 100644 --- a/apps/theme/app/layout.tsx +++ b/apps/theme/app/layout.tsx @@ -1,10 +1,14 @@ import type { Metadata } from 'next'; -import './globals.css'; + import '@digdir/designsystemet-css'; import '@digdir/designsystemet-theme'; +import 'react-color-palette/css'; +import './globals.css'; import { EnvelopeClosedIcon } from '@navikt/aksel-icons'; import { Figma, Footer, Github, Header, Slack } from '@repo/components'; +import { ThemeWrapper } from '../components/ThemeWrapper/ThemeWrapper'; + export const metadata: Metadata = { title: 'Temabygger - Designsystemet', description: 'Bygg ditt eget tema med designsystemet', @@ -67,9 +71,11 @@ export default function RootLayout({ return ( -
-
{children}
-
+ +
+
{children}
+
+ ); diff --git a/apps/theme/app/page.module.css b/apps/theme/app/page.module.css index 9dcdae95bd..251eeb94fd 100644 --- a/apps/theme/app/page.module.css +++ b/apps/theme/app/page.module.css @@ -1,134 +1,45 @@ .main { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - margin-top: 4px; - margin-bottom: 100px; - font-size: 18px; -} - -.container { - max-width: 1600px; - margin: 0 auto; - width: 100%; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 25%, rgba(30, 152, 245, 0.2) 55%); + margin-top: -106px; + padding-top: 106px; } -.colors { - display: flex; -} - -.top { - display: flex; +[data-color-scheme='dark'] .main { + background: linear-gradient(180deg, rgba(0, 28, 54, 1) 20%, rgba(30, 152, 245, 0.2) 45%); } -.swatchContainer { - border: 1px solid #c0c0c0; - height: 260px; - width: 495px; - padding: 20px; - margin-right: 32px; - border-radius: 4px; - gap: 20px; - display: flex; - flex-direction: column; -} - -.tom { - display: flex; - align-items: center; - gap: 16px; - font-size: 16px; -} - -.swatch { - height: 30px; - width: 30px; - border-radius: 50%; -} - -.swatch:hover { - cursor: pointer; -} - -.swatches { - display: flex; - align-items: center; - gap: 10px; +div[data-color-scheme='light'][data-theme='two'] .main { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 20.33%, rgba(116, 12, 126, 0.2) 57.04%); } -.box { - display: flex; - align-items: end; - justify-content: center; - gap: 32px; - padding: 12px 80px 40px; - border-radius: 8px; +div[data-color-scheme='dark'][data-theme='two'] .main { + background: linear-gradient(180deg, #131c27 20.33%, #280f36 57.04%); } -.title { +.header { + width: 600px; + margin: 0 auto; text-align: center; - margin-bottom: 36px; - font-size: 32px; - font-weight: 500; - margin-top: 0; -} - -.test2 { - display: flex; - align-items: center; - justify-content: center; + margin-top: var(--ds-spacing-2); } -.test { - margin-bottom: 16px; - font-size: 21px; - border-radius: 2px; - font-weight: 400; - color: rgb(104, 104, 104); - letter-spacing: 0.5px; +.heading { + margin-top: var(--ds-spacing-1); } -.tokens { - margin-top: 40px; +.desc { + margin-top: var(--ds-spacing-5); } -.tokens h3 { - font-size: 18px; - font-weight: 500; - margin-bottom: 8px; -} - -.tokens h2 { - font-size: 22px; - margin-bottom: 0; -} - -.token { - font-size: 16px; -} - -.token div { - margin-bottom: 6px; -} - -.brandRow { - margin-top: 24px; -} - -.contrastSection { +.btnGroup { display: flex; + gap: 20px; align-items: center; - gap: 2px; - height: 52px; - width: 85px; - font-size: 16px; -} - -.contrastSection svg { - color: #c90000; + justify-content: center; + margin-top: var(--ds-spacing-7); } -.contrastSectionSuccess svg { - color: #090; +.headerText { + color: var(--ds-color-accent-base-default); + font-weight: 600; } diff --git a/apps/theme/app/page.tsx b/apps/theme/app/page.tsx index a172098320..2502a717c4 100644 --- a/apps/theme/app/page.tsx +++ b/apps/theme/app/page.tsx @@ -1,9 +1,8 @@ 'use client'; import type { CssColor } from '@adobe/leonardo-contrast-colors'; -import { Heading } from '@digdir/designsystemet-react'; +import { Button, Heading, Paragraph } from '@digdir/designsystemet-react'; import type { - ColorError, ColorInfo, ColorMode, ContrastMode, @@ -12,85 +11,30 @@ import type { import { areColorsContrasting, canTextBeUsedOnColors, - generateColorTheme, - generateThemeForColor, isHexColor, } from '@digdir/designsystemet/color'; +import { BookIcon, PaletteIcon } from '@navikt/aksel-icons'; import { ColorModal, Container } from '@repo/components'; +import NextLink from 'next/link'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; -import { useEffect, useRef, useState } from 'react'; - -import { Previews, Scales, ThemeToolbar } from '../components'; +import { useEffect, useRef } from 'react'; import { Settings } from '../settings'; import { useThemeStore } from '../store'; + import type { ThemeColors } from '../types'; -import { mapTokens } from '../utils/tokenMapping'; +import { Previews } from '../components'; import classes from './page.module.css'; export default function Home() { - const accentTheme = useThemeStore((state) => state.accentTheme); - const neutralTheme = useThemeStore((state) => state.neutralTheme); - const brandOneTheme = useThemeStore((state) => state.brandOneTheme); - const brandTwoTheme = useThemeStore((state) => state.brandTwoTheme); - const brandThreeTheme = useThemeStore((state) => state.brandThreeTheme); - const borderRadius = useThemeStore((state) => state.borderRadius); const selectedColor = useThemeStore((state) => state.selectedColor); - const setAccentTheme = useThemeStore((state) => state.setAccentTheme); - const setNeutralTheme = useThemeStore((state) => state.setNeutralTheme); - const setBrandOneTheme = useThemeStore((state) => state.setBrandOneTheme); - const setBrandTwoTheme = useThemeStore((state) => state.setBrandTwoTheme); - const setBrandThreeTheme = useThemeStore((state) => state.setBrandThreeTheme); - const setBorderRadius = useThemeStore((state) => state.setBorderRadius); - - const [accentError, setAccentError] = useState('none'); - const [neutralError, setNeutralError] = useState('none'); - const [brandOneError, setBrandOneError] = useState('none'); - const [brandTwoError, setBrandTwoError] = useState('none'); - const [brandThreeError, setBrandThreeError] = useState('none'); - const router = useRouter(); const searchParams = useSearchParams(); const pathname = usePathname(); const params = new URLSearchParams(searchParams); const colorModalRef = useRef(null); - - const themeMode = (params.get('theme') as ColorMode) || 'light'; - const contrastMode = (params.get('contrastMode') as ContrastMode) || 'aa'; - - useEffect(() => { - mapTokens(); - - // Get colors from query params or use default colors - const queryAccent = getQueryColor('accent', accentTheme.color); - const queryNeutral = getQueryColor('neutral', neutralTheme.color); - const queryBrand1 = getQueryColor('brand1', brandOneTheme.color); - const queryBrand2 = getQueryColor('brand2', brandTwoTheme.color); - const queryBrand3 = getQueryColor('brand3', brandThreeTheme.color); - - // Generate color scales - const colors = generateColorTheme({ - colors: { - main: { - accent: queryAccent, - }, - support: { - brand1: queryBrand1, - brand2: queryBrand2, - brand3: queryBrand3, - }, - neutral: queryNeutral, - }, - contrastMode, - }); - - // Update colors and themes - updateColor('accent', queryAccent, colors.main.accent); - updateColor('neutral', queryNeutral, colors.neutral); - updateColor('brand1', queryBrand1, colors.support.brand1); - updateColor('brand2', queryBrand2, colors.support.brand2); - updateColor('brand3', queryBrand3, colors.support.brand3); - }, [contrastMode]); + const addColor = useThemeStore((state) => state.addColor); + const resetColors = useThemeStore((state) => state.resetColors); useEffect(() => { // Open modal on selected color change @@ -135,40 +79,6 @@ export default function Home() { return returnColor; }; - /** - * - * Update all the states for a color - * - * @param type The type of color to update - * @param color The color to update - * @param theme The theme to update - */ - const updateColor = ( - type: ThemeColors, - color: CssColor, - theme: ThemeInfo, - ) => { - const colorErrorSetterMap = { - accent: setAccentError, - neutral: setNeutralError, - brand1: setBrandOneError, - brand2: setBrandTwoError, - brand3: setBrandThreeError, - }; - - const themeColorSetterMap = { - accent: setAccentTheme, - neutral: setNeutralTheme, - brand1: setBrandOneTheme, - brand2: setBrandTwoTheme, - brand3: setBrandThreeTheme, - }; - - themeColorSetterMap[type](theme, color); - colorErrorSetterMap[type](getColorError(theme.light)); - colorQuerySetter(type, color); - }; - /** * Set the color in the query params * @@ -213,25 +123,10 @@ export default function Home() { router.replace(`${pathname}?${params.toString()}`, { scroll: false }); }; - const updateBoderRadius = (radius: string) => { - const previewElem = document.getElementById('preview'); - if (previewElem) { - previewElem.style.setProperty('--ds-border-radius-base', radius); - } - - setBorderRadius(radius); - setQueryParams({ borderRadius: radius }); - }; - - const updateTheme = (theme: ColorMode) => { - setQueryParams({ theme }); - }; - /* get theme from query on initial load */ useEffect(() => { const borderRadius = params.get('borderRadius') as string; if (typeof borderRadius === 'string') { - updateBoderRadius(borderRadius); } }, []); @@ -264,39 +159,37 @@ export default function Home() {
-
-
Temabygger
+
+ Designsystemet sin Temabygger + + Sett i gang med å bygge ditt + eget tema + + + Far compensation times than my client our too it a now, hero's + been rationale perfecting which towards absolutely fellow at on + variety + +
+ + +
- - Sett opp temaet ditt - - - { - const theme = generateThemeForColor(color, contrastMode); - updateColor(colorType, color, theme); - }} - onContrastModeChanged={(mode) => { - setQueryParams({ contrastMode: mode }); - }} - onBorderRadiusChanged={updateBoderRadius} - borderRadius={borderRadius} - /> - - - +
diff --git a/apps/theme/app/result/page.module.css b/apps/theme/app/result/page.module.css new file mode 100644 index 0000000000..1dd6f9d534 --- /dev/null +++ b/apps/theme/app/result/page.module.css @@ -0,0 +1,44 @@ +.header { + background: linear-gradient(90deg, rgba(240, 86, 90, 0.9) 0.5%, rgba(229, 170, 32, 0.9) 99.96%); + height: 250px; + display: flex; + align-items: center; + justify-content: center; + font-size: 50px; +} + +.snippet code { + background-color: transparent; + height: 290px; + display: block; +} + +.snippet { + border-radius: 8px; + overflow: hidden; +} + +.content { + display: grid; + grid-template-columns: repeat(16, 1fr); + grid-template-rows: masonry; + grid-auto-flow: dense; + margin-top: 32px; + gap: 24px; + margin-bottom: 64px; +} + +.small { + grid-column: span 6; +} + +.big { + grid-column: span 10; +} + +.card { + background-color: var(--ds-color-neutral-background-default); + padding: 24px; + border: 1px solid var(--ds-color-neutral-border-subtle); + border-radius: 8px; +} diff --git a/apps/theme/app/result/page.tsx b/apps/theme/app/result/page.tsx new file mode 100644 index 0000000000..f8119f1874 --- /dev/null +++ b/apps/theme/app/result/page.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { colorCliOptions } from '@digdir/designsystemet/tokens'; +import { CodeSnippet, Container } from '@repo/components'; +import cl from 'clsx/lite'; +import { type ColorTheme, useThemeStore } from '../../store'; +import classes from './page.module.css'; + +export default function Result() { + const themeName = useThemeStore((state) => state.themeName); + const colors = useThemeStore((state) => state.colors); + + const setCliColors = (colorTheme: ColorTheme[]) => { + let str = ''; + for (const color of colorTheme) { + str += `"${color.name}:${color.colors.light[8].hex}" `; + } + return str; + }; + + const cliSnippet = `npx @digdir/designsystemet@next tokens create \\ + --${colorCliOptions.main} ${setCliColors(colors.main)} \\ + --${colorCliOptions.neutral} "${colors.neutral[0]?.colors.light[8].hex}" \\ + --${colorCliOptions.support} ${setCliColors(colors.support)} \\ + --theme "${themeName}" \\ + --write + `; + + return ( +
+
{themeName}
+ +
+
+
+ {cliSnippet} +
+
+
f
+
f
+
f
+
+
+
+ ); +} diff --git a/apps/theme/app/themebuilder/_utils/useThemeParams.tsx b/apps/theme/app/themebuilder/_utils/useThemeParams.tsx new file mode 100644 index 0000000000..695b115179 --- /dev/null +++ b/apps/theme/app/themebuilder/_utils/useThemeParams.tsx @@ -0,0 +1,101 @@ +import type { CssColor } from '@adobe/leonardo-contrast-colors'; +import { + type ColorMode, + generateThemeForColor, +} from '@digdir/designsystemet/color'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useEffect } from 'react'; +import { useThemeStore } from '../../../store'; + +export const useThemeParams = () => { + const query = useSearchParams(); + const pathname = usePathname(); + const router = useRouter(); + + const colors = useThemeStore((state) => state.colors); + const themeName = useThemeStore((state) => state.themeName); + const appearance = useThemeStore((state) => state.appearance); + + /* Check if we have params in URL */ + useEffect(() => { + if (query.get('name')) { + useThemeStore.setState({ + themeName: query.get('name') as string, + }); + } + + if (query.get('appearance')) { + useThemeStore.setState({ + appearance: query.get('appearance') as ColorMode, + }); + } + + const newColors = { + ...colors, + }; + + if (query.get('main')) { + const mainColors = createColorsFromQuery(query.get('main') as string); + + if (mainColors) newColors.main = mainColors; + } + + if (query.get('neutral')) { + const neutralColor = query.get('neutral') as string; + + if (neutralColor) + newColors.neutral = [ + { + name: 'neutral', + colors: generateThemeForColor(neutralColor as CssColor), + }, + ]; + } + + if (query.get('support')) { + const supportColors = createColorsFromQuery( + query.get('support') as string, + ); + + if (supportColors) newColors.support = supportColors; + } + + useThemeStore.setState({ + colors: newColors, + }); + }, []); + + /* When name, appearance or colors change, update query */ + useEffect(() => { + const params = new URLSearchParams(query.toString()); + + const mainColorString = colors.main + .map((color) => `${color.name}:${color.colors.light[8].hex}`) + .join(' '); + + const neutralColorString = colors.neutral[0] + ? colors.neutral[0].colors.light[8].hex + : ''; + + const supportColorString = colors.support + .map((color) => `${color.name}:${color.colors.light[8].hex}`) + .join(' '); + + params.set('appearance', appearance); + params.set('name', themeName); + params.set('main', mainColorString); + params.set('neutral', neutralColorString); + params.set('support', supportColorString); + + router.push(pathname + '?' + params.toString()); + }, [colors, themeName, appearance]); + + return null; +}; + +function createColorsFromQuery(colors: string) { + return colors.split(' ').map((color) => { + const [name, hex] = color.split(':'); + return { name, colors: generateThemeForColor(hex as CssColor) }; + }); +} diff --git a/apps/theme/app/themebuilder/page.module.css b/apps/theme/app/themebuilder/page.module.css new file mode 100644 index 0000000000..ba05dd04b5 --- /dev/null +++ b/apps/theme/app/themebuilder/page.module.css @@ -0,0 +1,108 @@ +.page { + position: relative; +} + +.header { + background-color: rgb(226, 226, 226); + padding: 40px; + height: 200px; + display: flex; + gap: 32px; + flex-direction: column; + justify-content: center; +} + +.content { + padding: 40px; + width: calc(100% - 550px); + margin-bottom: 40px; + flex-grow: 500; +} + +.colorsContainer { + overflow: auto; +} + +.sideBarContainer { + width: 420px; +} + +.container { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 24px; +} + +.panel { + background-color: var(--ds-color-neutral-background-default); + border: 1px solid var(--ds-color-neutral-border-subtle); + border-radius: 8px; + margin-top: 28px; + width: 100%; + max-width: 1390px; +} + +.backLink { + color: var(--ds-color-accent-text-default); + margin-left: -4px; + display: inline-flex; + width: auto; + align-self: flex-start; +} + +.hide { + display: none; +} + +.show { + display: block; +} + +.panelToggleGroup { + display: flex; + gap: 12px; +} + +.panelToggleBtn { + border-radius: 4px; + padding: 12px 14px; + border: 1px solid var(--ds-color-neutral-border-subtle); + cursor: pointer; + font-size: 16px; + background-color: var(--ds-color-neutral-background-default); + display: flex; + align-items: center; +} + +.active, +.panelToggleBtn:hover { + border-color: var(--ds-color-neutral-border-default); + outline: 1px solid var(--ds-color-neutral-border-default); +} + +.top { + display: flex; + align-items: flex-end; + justify-content: space-between; + margin-top: 0px; + margin-bottom: 32px; +} + +.group { + display: flex; + gap: 16px; + width: 300px; + align-items: center; +} + +.label { + font-size: 16px; + font-weight: 500; +} + +@media screen and (max-width: 1500px) { + .content { + width: 100%; + } +} diff --git a/apps/theme/app/themebuilder/page.tsx b/apps/theme/app/themebuilder/page.tsx new file mode 100644 index 0000000000..a9a058cfd6 --- /dev/null +++ b/apps/theme/app/themebuilder/page.tsx @@ -0,0 +1,150 @@ +'use client'; + +import type { ColorMode } from '@digdir/designsystemet'; +import { Heading, Link, Tabs } from '@digdir/designsystemet-react'; +import { ChevronLeftIcon } from '@navikt/aksel-icons'; +import NextLink from 'next/link'; +import { useRouter } from 'next/navigation'; +import { + BorderRadius, + ColorContrasts, + ColorPreview, + ColorTokens, + Colors, + Sidebar, +} from '../../components'; +import { Toggle } from '../../components/Toggle/Toggle'; +import { useThemeStore } from '../../store'; +import { useThemeParams } from './_utils/useThemeParams'; +import classes from './page.module.css'; + +export default function Home() { + const colors = useThemeStore((state) => state.colors); + const appearance = useThemeStore((state) => state.appearance); + const setActivePage = useThemeStore((state) => state.setActivePage); + const router = useRouter(); + const activePage = useThemeStore((state) => state.activePage); + const setAppearance = useThemeStore((state) => state.setAppearance); + + /* For theme params */ + useThemeParams(); + + type TestProps = 'light' | 'dark'; + + const setHeaderColor = () => { + let themeMode: TestProps = 'light'; + if (colors.main.length === 0) { + return '#D9D9D9'; + } + if (appearance === 'dark') { + themeMode = 'dark'; + } + const str = colors.main[0].colors[themeMode][3].hex; + if (colors.main.length > 1) { + return ( + 'linear-gradient(90deg, ' + + colors.main[0].colors[themeMode][3].hex + + ' 0%, ' + + colors.main[1].colors[themeMode][3].hex + + ' 60%)' + ); + } + return str; + }; + + return ( +
+
+ + + + Gå tilbake til forsiden + + + Temabygger +
+
+
+
+ { + if (e === 'intro') { + router.push('/welcome'); + } else { + setActivePage(e as 'colors' | 'radius' | 'intro'); + } + }} + > + + Navn på tema + Farger + Border radius + + + +
+
+
Visning
+ { + const val = value; + setAppearance(val as ColorMode); + }} + /> +
+
+
+ + {(activePage === 'intro' || activePage === 'colors') && ( + <> + Farger +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + )} + + {activePage === 'radius' && ( + <> + + Border radius + +
+ +
+ + )} +
+
+ +
+
+
+ ); +} diff --git a/apps/theme/app/welcome/page.module.css b/apps/theme/app/welcome/page.module.css new file mode 100644 index 0000000000..7097e2a76d --- /dev/null +++ b/apps/theme/app/welcome/page.module.css @@ -0,0 +1,30 @@ +.page { + height: calc(100vh - 100px); + display: flex; + align-items: center; + justify-content: center; +} + +.card { + width: 550px; + border: 1px solid var(--ds-color-neutral-border-subtle); + padding: 48px; + border-radius: 16px; + margin-top: -50px; +} + +.heading { + margin-bottom: 16px; +} + +.paragraph { + margin-bottom: 16px; +} + +.btn { + margin-top: 40px; +} + +.textfield { + margin-top: 24px; +} diff --git a/apps/theme/app/welcome/page.tsx b/apps/theme/app/welcome/page.tsx new file mode 100644 index 0000000000..bd62fb2276 --- /dev/null +++ b/apps/theme/app/welcome/page.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { + Button, + Heading, + Paragraph, + Textfield, +} from '@digdir/designsystemet-react'; +import NextLink from 'next/link'; +import { useThemeStore } from '../../store'; +import classes from './page.module.css'; + +export default function Welcome() { + const themeName = useThemeStore((state) => state.themeName); + const setThemeName = useThemeStore((state) => state.setThemeName); + + return ( +
+
+ + Velkommen til Temabyggeren + + + Todo: General intro the the theme builder and set expectations + + + Todo: Say what the name does and how it effects the user + + + { + const value = e.currentTarget.value + .replace(/\s+/g, '-') + .replace(/[^A-Z0-9-]+/gi, '') + .toLowerCase(); + + setThemeName(value); + }} + /> + + +
+
+ ); +} diff --git a/apps/theme/components/BorderRadius/BorderRadius.module.css b/apps/theme/components/BorderRadius/BorderRadius.module.css new file mode 100644 index 0000000000..aa8a6de0cb --- /dev/null +++ b/apps/theme/components/BorderRadius/BorderRadius.module.css @@ -0,0 +1,137 @@ +.items { + display: flex; + flex-direction: column; + gap: 20px; + margin-top: 16px; +} + +.itemsContainer { + margin-left: -28px; + width: calc(100% + 56px); + margin-top: 12px; + padding: 24px 28px; + border-top: 1px solid var(--ds-color-neutral-border-subtle); + border-bottom: 1px solid var(--ds-color-neutral-border-subtle); +} + +.item { + display: flex; + font-size: 16px; +} + +.itemName { + font-weight: 500; +} + +.itemName { + width: 85px; +} + +.outer { + background-color: var(--ds-color-neutral-background-default) !important; +} + +.inner { + border: 1px solid var(--ds-color-neutral-border-subtle); + border-radius: 16px; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 28px; + padding: 28px; + background-color: var(--ds-color-neutral-background-subtle); +} + +.card { + border-radius: 8px; + background-color: var(--ds-color-neutral-background-default); + border: 1px solid var(--ds-color-neutral-border-subtle); + padding: 24px; + gap: 20px; + display: flex; + flex-direction: column; +} + +.tags { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.chips { + display: flex; + gap: 12px; + margin-top: 4px; +} + +.btn { + width: 100%; + border-radius: 4px; + margin-top: 4px; +} + +.users { + display: flex; + flex-direction: column; +} + +.img { + width: 100%; + border-radius: 8px; + height: 200px; +} + +.imgText { + display: flex; + gap: 8px; + flex-direction: column; +} + +.imgTitle { + margin-bottom: 0; + margin-top: 6px; +} + +.imgDesc { + color: var(--ds-color-neutral-text-subtle); + font-size: 16px; +} + +.user { + display: flex; + align-items: center; + border-bottom: 1px solid var(--ds-color-neutral-border-subtle); + gap: 16px; + padding-bottom: 16px; + margin-bottom: 16px; +} + +.user:last-child { + border-bottom: none; + padding-bottom: 0; + margin-bottom: 0; +} + +.userImg { + height: 44px; + width: 44px; + border-radius: 4px; +} + +.userRole { + font-size: 15px; + font-weight: 500; + color: var(--ds-color-neutral-text-subtle); + margin-bottom: -1px; +} + +.userName { + font-size: 16px; +} + +.userBtn { + margin-left: auto; +} + +.test { + border-radius: 100px; +} diff --git a/apps/theme/components/BorderRadius/BorderRadius.tsx b/apps/theme/components/BorderRadius/BorderRadius.tsx new file mode 100644 index 0000000000..eeac2523a3 --- /dev/null +++ b/apps/theme/components/BorderRadius/BorderRadius.tsx @@ -0,0 +1,243 @@ +import { + Button, + Heading, + Link, + Paragraph, + Tag, + Textfield, +} from '@digdir/designsystemet-react'; + +import cl from 'clsx/lite'; +import { useEffect } from 'react'; +import { useThemeStore } from '../../store'; +import classes from './BorderRadius.module.css'; +import { SettingsCard } from './SettingsCard/SettingsCard'; + +export const BorderRadius = () => { + const borderRadius = useThemeStore((state) => state.borderRadius); + + const setToken = (token: string, color: string) => { + const previewElement = document.getElementById('test'); + if (previewElement) { + previewElement.style.setProperty(token, color); + } + }; + + useEffect(() => { + setToken('--ds-border-radius-md', borderRadiusMap[borderRadius][1]); + }, [borderRadius]); + + const borderRadiusMap = { + none: ['0px', '0px', '0px', '0px', '0px', '999px'], + small: ['2px', '4px', '8px', '12px', '4px', '999px'], + medium: ['2px', '8px', '12px', '16px', '8px', '999px'], + large: ['2px', '8px', '16px', '20px', '12px', '999px'], + full: ['2px', '8px', '16px', '24px', '999px', '999px'], + }; + + const items = [ + { + name: 'sm', + value: '2px', + }, + { + name: 'md', + value: '4px', + }, + { + name: 'lg', + value: '8px', + }, + { + name: 'xl', + value: '16px', + }, + { + name: 'default', + value: '4px', + }, + { + name: 'full', + value: '999px', + }, + ]; + + return ( +
+
+ + Border radius er delt inn i 6 forskjellige tokens som endrer på seg + når border radius gruppene blir justert i sidebaren. + + + + Small, Medium, Large og xLarge er ment å brukes som tokens på ulike + overflatestørrelser for å beholde proporsjonene når ting blir større + eller mindre. + + + + Full tokenet vil alltid gi runde kanter. + +
+ Tokens +
+ {items.map((item, index) => ( +
+
{item.name}:
+
+ {borderRadiusMap[borderRadius][index]} +
+
+ ))} +
+
+
+
+
+
+ Logg inn i portalen + + + + Glemt passord? + + + +
+
+ +
+
+ + Sport + + + Nyheter + + + Innenriks + +
+ + Reiste alene til storbyen + + + Mona kvist ville finne drømmen i New York City + +
+
+
+ +
+
+ Folk du kanskje kjenner +
+
+ +
+
Designer
+
Ola Normann
+
+ +
+
+ +
+
Frontend
+
Kari Slotsveen
+
+ +
+
+ +
+
Backend
+
Marcus Viken
+
+ +
+
+
+
+
+
+ ); +}; diff --git a/apps/theme/components/BorderRadius/SettingsCard/SettingsCard.module.css b/apps/theme/components/BorderRadius/SettingsCard/SettingsCard.module.css new file mode 100644 index 0000000000..04ff5784ec --- /dev/null +++ b/apps/theme/components/BorderRadius/SettingsCard/SettingsCard.module.css @@ -0,0 +1,22 @@ +.panelDesc { + color: var(--ds-color-neutral-text-subtle); + margin-top: 8px; + font-size: 16px; +} + +.toggleGroup { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 16px; +} + +.radioGroup { + margin-top: 16px; +} + +.toggleHeading { + font-size: 16px; + font-weight: 500; + margin-bottom: 4px; +} diff --git a/apps/theme/components/BorderRadius/SettingsCard/SettingsCard.tsx b/apps/theme/components/BorderRadius/SettingsCard/SettingsCard.tsx new file mode 100644 index 0000000000..35a4c145c1 --- /dev/null +++ b/apps/theme/components/BorderRadius/SettingsCard/SettingsCard.tsx @@ -0,0 +1,56 @@ +import { + Fieldset, + Heading, + Paragraph, + Radio, + Switch, + ValidationMessage, +} from '@digdir/designsystemet-react'; +import classes from './SettingsCard.module.css'; +export const SettingsCard = () => { + return ( +
+ Innstillinger + + Her kan du administrere brukerene{' '} + + +
+
+ Visning + + Her kan du administrere + +
+ +
+ +
+ Visnigsmodus + + + +
+
+ ); +}; diff --git a/apps/theme/components/BorderRadiusInput/BorderRadiusInput.module.css b/apps/theme/components/BorderRadiusInput/BorderRadiusInput.module.css new file mode 100644 index 0000000000..b4ea1496df --- /dev/null +++ b/apps/theme/components/BorderRadiusInput/BorderRadiusInput.module.css @@ -0,0 +1,45 @@ +.items { + display: flex; + gap: 16px; + + flex-wrap: wrap; +} + +.item { + width: calc(50% - 8px); +} + +.active .inner { + border-color: var(--ds-color-accent-border-default); + background-color: var(--ds-color-accent-surface-default); +} + +.active .box { + outline: 1px solid var(--ds-color-neutral-border-default); + border-color: var(--ds-color-neutral-border-default); +} + +.box { + border: 1px solid var(--ds-color-neutral-border-subtle); + border-radius: 4px; + height: 52px; + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 12px; + overflow: hidden; + font-size: 16px; + cursor: pointer; +} + +.box:hover { + background-color: var(--ds-color-neutral-background-subtle); +} + +.inner { + background-color: var(--ds-color-neutral-background-subtle); + border: 5px solid var(--ds-color-neutral-border-default); + height: 33px; + width: 72px; + margin-right: -34px; +} diff --git a/apps/theme/components/BorderRadiusInput/BorderRadiusInput.tsx b/apps/theme/components/BorderRadiusInput/BorderRadiusInput.tsx new file mode 100644 index 0000000000..55bc151306 --- /dev/null +++ b/apps/theme/components/BorderRadiusInput/BorderRadiusInput.tsx @@ -0,0 +1,49 @@ +import cl from 'clsx/lite'; +import { useState } from 'react'; +import { useThemeStore } from '../../store'; +import classes from './BorderRadiusInput.module.css'; + +export const BorderRadiusInput = () => { + const [active, setActive] = useState(0); + const setBorderRadius = useThemeStore((state) => state.setBorderRadius); + const borderRadius = useThemeStore((state) => state.borderRadius); + const items = [ + { name: 'Ingen', type: 'none', value: '0px' }, + { name: 'Small', type: 'small', value: '6px' }, + { name: 'Medium', type: 'medium', value: '10px' }, + { name: 'Large', type: 'large', value: '13px' }, + { name: 'Full', type: 'full', value: '9999px' }, + ]; + + return ( +
+
+ {items.map((item, index) => ( +
setActive(index)} + > +
{ + setBorderRadius( + item.type as 'none' | 'small' | 'medium' | 'large' | 'full', + ); + }} + > +
{item.name}
+
+
+
+ ))} +
+
+ ); +}; diff --git a/apps/theme/components/Color/Color.module.css b/apps/theme/components/Color/Color.module.css index bfdd8e98f0..7a298f22c6 100644 --- a/apps/theme/components/Color/Color.module.css +++ b/apps/theme/components/Color/Color.module.css @@ -1,12 +1,12 @@ .box { - height: 54px; - width: 102px; + height: 48px; + width: 74px; display: flex; align-items: center; justify-content: center; color: black; font-weight: 400; - border: 1px solid #d6d6d6; + border: 1px solid var(--ds-color-neutral-border-subtle); border-radius: 4px; cursor: pointer; } diff --git a/apps/theme/components/Color/Color.tsx b/apps/theme/components/Color/Color.tsx index 3d4269bb13..f6cb7016d5 100644 --- a/apps/theme/components/Color/Color.tsx +++ b/apps/theme/components/Color/Color.tsx @@ -1,23 +1,19 @@ import { omit } from '@digdir/designsystemet-react'; -import type { ColorInfo } from '@digdir/designsystemet/color'; import { SunIcon } from '@navikt/aksel-icons'; import cl from 'clsx/lite'; import { forwardRef } from 'react'; - import { useThemeStore } from '../../store'; -import type { ThemeColors } from '../../types'; import classes from './Color.module.css'; type ColorProps = { colorNumber: number; - color: ColorInfo; + color: string; contrast?: string; lightness?: string; featured?: boolean; - hex?: string; showColorMeta?: boolean; - type: ThemeColors; + name: string; } & Omit, 'color'>; const Color = forwardRef( @@ -27,9 +23,8 @@ const Color = forwardRef( contrast, featured, lightness, - hex, + showColorMeta = true, - type, ...rest }, ref, @@ -39,26 +34,15 @@ const Color = forwardRef( <> {showColorMeta && ( <> -
{hex}
{contrast} diff --git a/apps/theme/components/ColorContrasts/ColorContrasts.module.css b/apps/theme/components/ColorContrasts/ColorContrasts.module.css new file mode 100644 index 0000000000..f7a87a1b7f --- /dev/null +++ b/apps/theme/components/ColorContrasts/ColorContrasts.module.css @@ -0,0 +1,147 @@ +.color { + height: 36px; + display: block; + flex: 1 1; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2); + border-radius: 2px; +} + +.colors { + display: flex; + gap: 12px; +} + +.table { + background-color: #e2e2e2; + margin-bottom: 16px; + margin-top: 12px; +} + +.cell { + padding: 14px; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; +} + +.tagGroup { + display: inline-flex; + flex-direction: column; + gap: 9px; + align-items: flex-start; + border-top: 1px solid var(--ds-color-neutral-border-subtle); + margin-left: -28px; + width: calc(100% + 58px); + padding: 22px 28px; +} + +.tagGroups { + margin-top: 12px; +} + +.tagGroup:last-child { + border-bottom: 1px solid var(--ds-color-neutral-border-subtle); +} + +.tag { + background-color: #d23e3e; + color: white; + border-radius: 400px; + font-size: 13px; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + padding: 3px 9px; +} + +.AA { + background-color: green; +} + +.AAA { + background-color: #407be9; +} + +.AA18 { + background-color: #fbb025; + color: black; +} + +.meta { + display: flex; + align-items: center; + gap: 12px; +} + +.tagContainer { + flex: 1 1; + align-items: center; + justify-content: center; + display: flex; +} + +.contrast { + font-size: 16px; + font-weight: 500; + text-align: center; + flex: 1 1; +} + +.table, +.td, +.th { + border: 1px solid var(--ds-color-neutral-border-subtle); + border-collapse: collapse; + background-color: var(--ds-color-neutral-background-subtle); +} + +.th { + height: 90px; + width: 140px; + padding: 16px; +} + +.td { + height: 90px; + width: 140px; + vertical-align: top; +} + +.header { + font-size: 16px; + width: 30px; + text-align: left; + display: flex; + flex-direction: column; + gap: 2px; + font-weight: 500; +} + +.headerHex { + color: var(--ds-color-neutral-text-subtle); + font-weight: 400; +} + +.contrastChart { + background-color: transparent; +} + +.subHeading { + margin-top: 8px; + margin-bottom: -4px; +} + +.fieldGroup { + margin-top: 4px; + margin-bottom: 4px; +} + +.select { + width: 25%; +} + +.desc { + width: 90%; +} diff --git a/apps/theme/components/ColorContrasts/ColorContrasts.tsx b/apps/theme/components/ColorContrasts/ColorContrasts.tsx new file mode 100644 index 0000000000..7359bf08c3 --- /dev/null +++ b/apps/theme/components/ColorContrasts/ColorContrasts.tsx @@ -0,0 +1,274 @@ +import { + type ColorInfo, + generateThemeForColor, + getColorNameFromNumber, + getContrastFromHex, +} from '@digdir/designsystemet'; +import { + Field, + Heading, + Paragraph, + Select, +} from '@digdir/designsystemet-react'; +import cl from 'clsx/lite'; +import { useEffect, useState } from 'react'; +import { useThemeStore } from '../../store'; +import classes from './ColorContrasts.module.css'; + +export const ColorContrasts = () => { + const theme = generateThemeForColor('#0062BA'); + const indexOne = [1, 2, 3, 4, 5]; + const indexTwo = [6, 7, 8, 12, 13]; + const [reducedLight, setReducedLight] = useState({ + themeRange1: theme.light.filter((color) => indexOne.includes(color.number)), + themeRange2: theme.light.filter((color) => indexTwo.includes(color.number)), + }); + const colors = useThemeStore((state) => state.colors); + const [selectedColor, setSelectedColor] = useState('dominant'); + const [selectedBaseColor, setSelectedBaseColor] = useState('dominant'); + + const indexBaseOne = [0, 1, 2, 3, 14, 15]; + const indexBaseTwo = [9, 10, 11]; + const [reducedBaseLight, setReducedBaseLight] = useState({ + themeRange1: theme.light.filter((color) => + indexBaseOne.includes(color.number), + ), + themeRange2: theme.light.filter((color) => + indexBaseTwo.includes(color.number), + ), + }); + + useEffect(() => { + const newTheme = + (['main', 'neutral', 'support'] as Array) + .flatMap((group) => colors[group]) + .find((color) => color.name === selectedColor)?.colors || theme; + + setReducedLight({ + themeRange1: newTheme.light.filter((color) => + indexOne.includes(color.number), + ), + themeRange2: newTheme.light.filter((color) => + indexTwo.includes(color.number), + ), + }); + }, [selectedColor, colors]); + + useEffect(() => { + const newTheme = + (['main', 'neutral', 'support'] as Array) + .flatMap((group) => colors[group]) + .find((color) => color.name === selectedBaseColor)?.colors || theme; + + setReducedBaseLight({ + themeRange1: newTheme.light.filter((color) => + indexBaseOne.includes(color.number), + ), + themeRange2: newTheme.light.filter((color) => + indexBaseTwo.includes(color.number), + ), + }); + }, [selectedBaseColor, colors]); + + const ThCell = ({ color }: { color: ColorInfo }) => { + return ( + +
+ {getColorNameFromNumber(color.number)} +
{color.hex}
+
+ + ); + }; + + const Tag = ({ + color1, + color2, + }: { color1: ColorInfo; color2: ColorInfo }) => { + const contrast = getContrastFromHex(color1.hex, color2.hex); + let type = 'AAA'; + + if (contrast < 3) { + type = 'FAIL'; + } else if (contrast < 4.5) { + type = 'AA18'; + } else if (contrast < 7) { + type = 'AA'; + } + + return
{type}
; + }; + + const TdCell = ({ + color1, + color2, + }: { color1: ColorInfo; color2: ColorInfo }) => { + return ( +
+
+
+
+
+
+
+ +
+
+ {Math.floor(getContrastFromHex(color1.hex, color2.hex) * 10) / 10} + :1 +
+
+
+ ); + }; + + return ( +
+
+ Kontraster mellom farger + + Her vises kontrastene mellom de ulike trinnene i fargeskalaene, samt + om fargene oppfyller WCAG-kravene. + + +
+
+
AAA
+ + Tekst og bakgrunn må ha en kontrast på minst 7:1 for å oppfylle + WCAG AAA-kravet. + +
+
+
AA
+ + Tekst og bakgrunn må ha en kontrast på minst 4.5:1 for å oppfylle + WCAG AA-kravet. + +
+
+
AA18
+ + Tekst og bakgrunn må ha en kontrast på minst 3:1 og en + skriftstørrelse på 18 px eller større for å oppfylle WCAG + AA-kravet. + +
+
+
FAIL
+ + Oppfyller ingen kontrastkrav i WCAG og bør kun brukes til + dekorative formål. + +
+
+
+
+ + Text og Border mot Background og Surface + + + Når du bytter mellom fargeskalaene, vil du se at kontrastene mellom + fargene i seksjonen nedenfor er nesten identiske. Dette gjør at du kun + trenger å vurdere kontrastene for én fargeskala for å forstå hvordan + alle fungerer. Siden kontrastene er konsistente, kan du også kombinere + ulike farger på tvers av skalaene. + + + + + + + + + {reducedLight.themeRange2.map((color2, index) => ( + + + {reducedLight.themeRange1.map((color1, index) => ( + + ))} + + ))} + +
+ {reducedLight.themeRange1.map((color, index) => ( + + ))} +
+ +
+ Base fargene + + Fargene som blir valgt i verktøyet får tokenet Base Default i hver + fargeskala. Dette betyr at det er viktig å velge en farge som har over + 3:1 kontrast mot overflatefarger om den skal brukes som en viktig, + meningsbærende farge. Verktøyet lager også to kontrastfarger som trygt + kan brukes oppå base fargene. Disse kontrastfargene blir enten lyse + eller mørke avhengig av base fargen. + + + + + + + + + {reducedBaseLight.themeRange2.map((color2, index) => ( + + + {reducedBaseLight.themeRange1.map((color1, index) => ( + + ))} + + ))} + +
+ {reducedBaseLight.themeRange1.map((color, index) => ( + + ))} +
+ +
+
+
+ ); +}; diff --git a/apps/theme/components/ColorInput/ColorInput.module.css b/apps/theme/components/ColorInput/ColorInput.module.css new file mode 100644 index 0000000000..9fd3eeeb0c --- /dev/null +++ b/apps/theme/components/ColorInput/ColorInput.module.css @@ -0,0 +1,47 @@ +.component { + display: flex; + flex-direction: column; + gap: 8px; + flex: 0 calc(50% - 8px); +} + +.box { + border: 1px solid var(--ds-color-neutral-border-subtle); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px; + background-color: transparent; + cursor: pointer; + transition: 0.1s all; +} + +.box:hover { + background-color: var(--ds-color-neutral-background-subtle); +} + +.leftContent { + display: flex; + align-items: center; + gap: 12px; +} + +.color { + height: 24px; + width: 24px; + background-color: red; + border-radius: 4px; +} + +.name { + font-size: 16px; +} + +.hex { + font-size: 16px; +} + +.icon { + color: var(--ds-color-neutral-text-subtle); +} diff --git a/apps/theme/components/ColorInput/ColorInput.tsx b/apps/theme/components/ColorInput/ColorInput.tsx new file mode 100644 index 0000000000..2ac3ba4a40 --- /dev/null +++ b/apps/theme/components/ColorInput/ColorInput.tsx @@ -0,0 +1,31 @@ +import { PencilIcon } from '@navikt/aksel-icons'; + +import classes from './ColorInput.module.css'; + +type ColorInputProps = { + color: string; + name: string; + onClick: (e: React.MouseEvent) => void; +}; + +export const ColorInput = ({ name, color, onClick }: ColorInputProps) => { + return ( +
+
{name}
+ +
+ ); +}; diff --git a/apps/theme/components/Scales/Scales.module.css b/apps/theme/components/ColorPreview/Card2.module.css similarity index 50% rename from apps/theme/components/Scales/Scales.module.css rename to apps/theme/components/ColorPreview/Card2.module.css index dd29bb8fb9..7b37c2c660 100644 --- a/apps/theme/components/Scales/Scales.module.css +++ b/apps/theme/components/ColorPreview/Card2.module.css @@ -1,21 +1,22 @@ -.rows { +.card { + flex-direction: row !important; + gap: 24px; + justify-content: space-between; +} + +.text { display: flex; - flex-direction: column; align-items: center; gap: 8px; + justify-content: center; } -.row { - display: flex; - align-items: end; - justify-content: space-between; - gap: 24px; +.title { + width: 140px; } -.scaleLabel { - height: 54px; +.checkGroup { display: flex; align-items: center; - font-size: 16px; - color: #505050; + gap: 24px; } diff --git a/apps/theme/components/ColorPreview/ColorPreview.module.css b/apps/theme/components/ColorPreview/ColorPreview.module.css new file mode 100644 index 0000000000..1f7671673a --- /dev/null +++ b/apps/theme/components/ColorPreview/ColorPreview.module.css @@ -0,0 +1,43 @@ +.grid { + grid-template-columns: repeat(3, 1fr) !important; +} + +.card { + background-color: var(--ds-color-neutral-background-default); + display: flex; + flex-direction: column; + border-radius: 8px; + height: auto; + padding: 18px; + border: 1px solid var(--ds-color-accent-border-subtle); + background-color: var(--ds-color-accent-surface-default); +} + +.title { + margin-bottom: 8px; + color: var(--ds-color-accent-text-default); + width: 200px; +} + +.desc { + color: var(--ds-color-accent-text-subtle); +} + +.checkGroup { + margin-top: 16px; + margin-bottom: 24px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.btnGroup { + display: flex; + gap: 16px; +} + +.label { + font-weight: 500; + margin-bottom: 8px; + font-size: 16px; +} diff --git a/apps/theme/components/ColorPreview/ColorPreview.tsx b/apps/theme/components/ColorPreview/ColorPreview.tsx new file mode 100644 index 0000000000..0323d8ae30 --- /dev/null +++ b/apps/theme/components/ColorPreview/ColorPreview.tsx @@ -0,0 +1,187 @@ +import { + type ColorInfo, + type ColorNumber, + getColorNameFromNumber, +} from '@digdir/designsystemet'; +import { + Button, + Checkbox, + Heading, + Paragraph, + Switch, + ToggleGroup, +} from '@digdir/designsystemet-react'; +import cl from 'clsx/lite'; +import { useEffect, useState } from 'react'; +import { useThemeStore } from '../../store'; +import listClasses from './Card2.module.css'; +import classes from './ColorPreview.module.css'; + +type ViewType = 'list' | 'grid'; + +export const ColorPreview = () => { + const colors = useThemeStore((state) => state.colors); + const [view, setView] = useState('grid'); + const appearance = useThemeStore((state) => state.appearance); + + type CardProps = { + color: { + name: string; + colors: { + light: ColorInfo[]; + dark: ColorInfo[]; + }; + }; + }; + + const setStyle = (colors: { + light: ColorInfo[]; + dark: ColorInfo[]; + }) => { + const style = {} as Record; + + let lightColors = colors.light; + + if (appearance === 'dark') { + lightColors = colors.dark; + } + + for (let i = 0; i < lightColors.length; i++) { + const number = (i + 1) as ColorNumber; + style[ + `--ds-color-accent-${getColorNameFromNumber(number) + .replace(/\s+/g, '-') + .toLowerCase()}` + ] = lightColors[i].hex; + style[ + `--ds-color-${getColorNameFromNumber(number) + .replace(/\s+/g, '-') + .toLowerCase()}` + ] = lightColors[i].hex; + } + + return style; + }; + + const CardWrapper = ({ color }: CardProps) => { + if (view === 'list') { + return ; + } + return ; + }; + + const HorizontalCard = ({ color }: CardProps) => { + useEffect(() => {}, []); + + const [valueOne, setValueOne] = useState(true); + return ( +
+ + {color.name} + + + Livet er for kort til å være grått. Fyll deg selv og dine dager med + farger. + +
+ setValueOne(!valueOne)} + checked={valueOne} + /> + +
+
+ + +
+
+ ); + }; + const VerticalCard = ({ color }: CardProps) => { + const [isChecked, setIsChecked] = useState(true); + const [isSwitch, setIsSwitch] = useState(true); + return ( +
+
+ + {color.name} + + + Farger gjør livet mer fargerikt + +
+
+ setIsChecked(!isChecked)} + /> + setIsSwitch(!isSwitch)} + > + Switch + +
+
+ + +
+
+ ); + }; + return ( +
+
+ Se fargene dine i bruk + + Hver farge som blir valgt med verktøyet får sitt eget kort i seksjonen + til høyre slik at du kan se hvordan fargene harmonerer sammen. + + + Merk at kontrastfargen inne i knappen endrer seg fra hvit til svart, + avhengig av om den valgte fargen er lys eller mørk for å oppnå best + mulig kontrast. + +
+
Visning:
+ setView(value as ViewType)} + > + Grid + Liste + +
+
+
+ {colors.main.map((color, index) => ( + + ))} + {colors.neutral.map((color, index) => ( + + ))} + {colors.support.map((color, index) => ( + + ))} +
+
+ ); +}; diff --git a/apps/theme/components/ColorTokens/ColorTokens.module.css b/apps/theme/components/ColorTokens/ColorTokens.module.css new file mode 100644 index 0000000000..53aefd43a1 --- /dev/null +++ b/apps/theme/components/ColorTokens/ColorTokens.module.css @@ -0,0 +1,10 @@ +.img { + width: 80%; + height: auto; +} + +.right { + display: flex !important; + align-items: center !important; + justify-content: center !important; +} diff --git a/apps/theme/components/ColorTokens/ColorTokens.tsx b/apps/theme/components/ColorTokens/ColorTokens.tsx new file mode 100644 index 0000000000..0fdd45a1d0 --- /dev/null +++ b/apps/theme/components/ColorTokens/ColorTokens.tsx @@ -0,0 +1,19 @@ +import { Heading, Paragraph } from '@digdir/designsystemet-react'; +import cl from 'clsx/lite'; +import classes from './ColorTokens.module.css'; +export const ColorTokens = () => { + return ( +
+
+ Se fargetokens + + Her ser du hvilke tokens som er brukt for å lage kortene i seksjonen + over. + +
+
+ +
+
+ ); +}; diff --git a/apps/theme/components/Colors/Colors.module.css b/apps/theme/components/Colors/Colors.module.css new file mode 100644 index 0000000000..b9997d019f --- /dev/null +++ b/apps/theme/components/Colors/Colors.module.css @@ -0,0 +1,35 @@ +.rows { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + margin: 8px 0; + width: 100%; + max-width: 1390px; +} + +.row { + display: flex; + align-items: end; + justify-content: space-between; + gap: 24px; + width: 100%; +} + +.scaleLabel { + height: 48px; + display: flex; + align-items: center; + font-size: 16px; + color: var(--ds-color-neutral-text-default); + flex-shrink: 0; + min-width: 115px; +} + +.separator { + margin-top: 6px; + margin-bottom: 7px; + height: 1px; + width: 100%; + background-color: var(--ds-color-neutral-border-subtle); +} diff --git a/apps/theme/components/Colors/Colors.tsx b/apps/theme/components/Colors/Colors.tsx new file mode 100644 index 0000000000..7ec179a55a --- /dev/null +++ b/apps/theme/components/Colors/Colors.tsx @@ -0,0 +1,50 @@ +import type { ColorMode } from '@digdir/designsystemet/color'; + +import { useThemeStore } from '../../store'; +import { Scale } from '../Scale/Scale'; +import classes from './Colors.module.css'; + +type ScalesProps = { + themeMode: ColorMode; +}; + +export const Colors = ({ themeMode }: ScalesProps) => { + const colors = useThemeStore((state) => state.colors); + return ( +
+ {colors.main.map((color, index) => ( +
+
{color.name}
+ +
+ ))} +
+ {colors.neutral.map((color, index) => ( +
+
{color.name}
+ +
+ ))} +
+ {colors.support.map((color, index) => ( +
+
{color.name}
+ +
+ ))} +
+ ); +}; diff --git a/apps/theme/components/ContrastChart/ContrastChart.tsx b/apps/theme/components/ContrastChart/ContrastChart.tsx index bf62f573ff..d399b15fb4 100644 --- a/apps/theme/components/ContrastChart/ContrastChart.tsx +++ b/apps/theme/components/ContrastChart/ContrastChart.tsx @@ -3,7 +3,7 @@ import { generateThemeForColor, getColorNameFromNumber, getContrastFromHex, -} from '@/packages/cli/dist/src'; +} from '@digdir/designsystemet'; import cl from 'clsx/lite'; import classes from './ContrastChart.module.css'; diff --git a/apps/theme/components/Group/Group.module.css b/apps/theme/components/Group/Group.module.css index 3487a9a76c..0db991e88a 100644 --- a/apps/theme/components/Group/Group.module.css +++ b/apps/theme/components/Group/Group.module.css @@ -1,6 +1,6 @@ .colors { display: flex; - gap: 8px; + gap: 4px; border-radius: 0; } @@ -11,6 +11,7 @@ text-align: center; width: 100%; position: relative; + font-size: 16px; } .featured { @@ -29,9 +30,13 @@ } .names div { - font-size: 16px; + font-size: 14px; width: 100%; text-align: center; + align-items: center; + justify-content: center; + display: flex; + height: 45px; } @media (max-width: 1632px) { diff --git a/apps/theme/components/Group/Group.tsx b/apps/theme/components/Group/Group.tsx index f5bda2cb63..d9d9f2b308 100644 --- a/apps/theme/components/Group/Group.tsx +++ b/apps/theme/components/Group/Group.tsx @@ -1,19 +1,19 @@ import { RovingFocusItem } from '@digdir/designsystemet-react'; -import type { ColorInfo } from '@digdir/designsystemet/color'; +import type { ThemeInfo } from '@digdir/designsystemet/color'; import cl from 'clsx/lite'; -import type { ThemeColors } from '../../types'; import { Color } from '../Color/Color'; +import { useThemeStore } from '../../store'; import classes from './Group.module.css'; type GroupProps = { header: string; - colors: ColorInfo[]; + colors: number[]; + colorScale: ThemeInfo; showColorMeta?: boolean; names?: string[]; featured?: boolean; - type: ThemeColors; }; export const Group = ({ @@ -21,16 +21,13 @@ export const Group = ({ colors, showColorMeta, names, - type, + colorScale, featured = false, }: GroupProps) => { + const appearance = useThemeStore((state) => state.appearance); return (
- {header && ( -
- {header} -
- )} + {header &&
{header}
} {header && names && (
{names.map((name, index) => ( @@ -39,17 +36,20 @@ export const Group = ({
)} -
+
{colors.map((item, index) => ( - + ))} diff --git a/apps/theme/components/Previews/Previews.module.css b/apps/theme/components/Previews/Previews.module.css index 684476e82f..20a9e76db1 100644 --- a/apps/theme/components/Previews/Previews.module.css +++ b/apps/theme/components/Previews/Previews.module.css @@ -2,7 +2,7 @@ display: flex; align-items: center; justify-content: space-between; - margin: 48px 0 24px; + margin: 32px 0 24px; } .menu { @@ -15,7 +15,7 @@ display: flex; align-items: center; font-size: 16px; - padding: 9px 20px; + padding: 9px 14px; border-radius: 500px; border: none; background-color: transparent; @@ -28,7 +28,7 @@ } .menuItemActive { - background-color: var(--ds-color-accent-surface-default); + background-color: var(--ds-color-accent-surface-hover); } .menuItem[aria-disabled] { @@ -72,13 +72,10 @@ .preview { --background: var(--neutral-2); --foreground: var(--neutral-1); - width: 100%; border-radius: 12px; display: flex; gap: 32px; - box-shadow: var(--ds-shadow-md); - padding: var(--ds-spacing-4); } .preview[data-color-scheme='dark'], diff --git a/apps/theme/components/Previews/Previews.tsx b/apps/theme/components/Previews/Previews.tsx index f4b6bb7f44..e4f2fe6cf8 100644 --- a/apps/theme/components/Previews/Previews.tsx +++ b/apps/theme/components/Previews/Previews.tsx @@ -1,25 +1,22 @@ +'use client'; + import type { ColorMode } from '@digdir/designsystemet/color'; -import { Showcase } from '@repo/components'; import cl from 'clsx/lite'; -import { useState } from 'react'; -import { Dashboard } from './Dashboard/Dashboard'; -import { Landing } from './Landing/Landing'; -import classes from './Previews.module.css'; +import { useThemeStore } from '../../store'; -type previewModeType = - | 'dashboard' - | 'landing' - | 'forms' - | 'auth' - | 'components'; +import classes from './Previews.module.css'; +import { Theme1 } from './Theme1/Theme1'; type PreviewsProps = { themeMode: ColorMode; onThemeModeChange: (themeMode: ColorMode) => void; }; -export const Previews = ({ themeMode, onThemeModeChange }: PreviewsProps) => { - const [previewMode, setPreviewMode] = useState('components'); +export const Previews = () => { + const theme = useThemeStore((state) => state.themePreview); + const appearance = useThemeStore((state) => state.appearance); + const setTheme = useThemeStore((state) => state.setThemePreview); + const setAppearance = useThemeStore((state) => state.setAppearance); return ( <> @@ -29,104 +26,67 @@ export const Previews = ({ themeMode, onThemeModeChange }: PreviewsProps) => { className={cl( classes.menuItem, 'ds-focus', - previewMode === 'components' && classes.menuItemActive, + theme === 'one' && + appearance === 'light' && + classes.menuItemActive, )} - onClick={() => setPreviewMode('components')} + onClick={() => { + setTheme('one'); + setAppearance('light'); + }} > - Komponenter + Tema 1 lys - -
-
- - -
-
- {previewMode === 'components' && } - {previewMode === 'dashboard' && } - {previewMode === 'landing' && } +
+
); diff --git a/apps/theme/components/Previews/Theme1/Theme1.module.css b/apps/theme/components/Previews/Theme1/Theme1.module.css new file mode 100644 index 0000000000..408efd2f9f --- /dev/null +++ b/apps/theme/components/Previews/Theme1/Theme1.module.css @@ -0,0 +1,219 @@ +.components { + display: grid; + grid-template-columns: repeat(16, 1fr); + grid-template-rows: masonry; + grid-auto-flow: dense; + gap: 24px; + width: 100%; + margin-bottom: 88px; +} + +.panel { + background-color: var(--foreground); + border-radius: 16px; + padding: 24px; + background-color: var(--ds-color-neutral-background-default); + box-shadow: var(--ds-shadow-xs); +} + +[data-color-scheme='dark'] .panel { + background-color: var(--ds-color-neutral-background-subtle); +} + +[data-color-scheme='dark'].darkCard { + background-color: var(--ds-color-accent-surface-hover); +} + +.test { + grid-column: span 4; +} + +.wide { + grid-column: span 8; +} + +.test2 { + grid-column: span 5; +} + +.test3 { + grid-column: span 7; +} + +.userField { + margin-bottom: 20px; +} + +.tabs button { + min-height: 40px; +} + +.table { + width: 100%; + margin-top: 16px; +} + +.table thead button { + padding-top: 8px; + padding-bottom: 8px; +} + +.tableCell { + display: flex; + align-items: center; + gap: 12px; + padding: 13px 12px; +} + +.tableImg { + height: 36px; + width: 36px; + border-radius: 4px; +} + +.tableAction { + display: flex; + align-items: center; + gap: 12px; +} + +.tableSelect { + width: 160px; +} + +.tableHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 16px; + gap: 100px; +} + +.otherInfo { + margin-top: 16px; +} + +.panelHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + margin-top: -8px; +} + +.panelHeader button { + margin-right: -8px; +} + +.panelDesc { + color: var(--ds-color-neutral-text-subtle); + margin-top: 8px; + font-size: 16px; +} + +.toggleHeading { + font-size: 16px; + font-weight: 500; + margin-bottom: 4px; +} + +.toggleDesc { + font-size: 15px; + color: var(--ds-color-neutral-text-subtle); +} + +.toggleGroup { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 16px; +} + +.radioGroup { + margin-top: 16px; +} + +.pagination { + margin-left: -8px; +} + +.img { + width: 100%; + border-radius: 4px; + margin-top: 16px; +} + +.tagGroup { + display: flex; + gap: 12px; + margin-top: 12px; +} + +.tag { + font-size: 14px; +} + +.cardTitle { + font-size: 16px; + font-weight: 500; + margin-top: 12px; +} + +.cardDesc { + font-size: 16px; + color: var(--ds-color-neutral-text-subtle); + margin-top: 8px; + margin-bottom: 8px; +} + +.darkCard { + position: relative; + overflow: hidden; + display: flex; + justify-content: space-between; + flex-direction: column; +} + +.darkCard:before { + content: ''; + height: 170px; + width: 170px; + border-radius: 50%; + position: absolute; + bottom: -85px; + right: -85px; + z-index: 2; + background-color: var(--ds-color-accent-border-default); +} + +.darkCard:after { + content: ''; + height: 250px; + width: 250px; + border-radius: 50%; + position: absolute; + bottom: -125px; + right: -125px; + z-index: 1; + background-color: var(--ds-color-accent-surface-active); +} + +.darkCardTitle { + margin-bottom: 12px; + color: var(--ds-color-accent-text-default); +} + +.darkCardDesc { + color: var(--ds-color-accent-text-subtle); + font-size: 16px; + padding-right: 10%; + margin-bottom: 12px; +} + +.darkCardDesc:last-child { + margin-bottom: 0; +} + +.darkCardBtn { + align-self: flex-start; +} diff --git a/apps/theme/components/Previews/Theme1/Theme1.tsx b/apps/theme/components/Previews/Theme1/Theme1.tsx new file mode 100644 index 0000000000..d315d5c3e4 --- /dev/null +++ b/apps/theme/components/Previews/Theme1/Theme1.tsx @@ -0,0 +1,301 @@ +import { + Button, + Checkbox, + Fieldset, + Heading, + Link, + Paragraph, + Radio, + Search, + Select, + Switch, + Table, + Tag, + Textfield, + ValidationMessage, + usePagination, +} from '@digdir/designsystemet-react'; +import cl from 'clsx/lite'; +import { useState } from 'react'; + +import classes from './Theme1.module.css'; + +export const Theme1 = () => { + const [currentPage, setCurrentPage] = useState(3); + const pagination = usePagination({ + currentPage, + setCurrentPage, + totalPages: 5, + showPages: 5, + }); + + return ( +
+
+
+ + Min profil + + +
+ + + + + +
+ Annen informasjon + + + + +
+
+ +
+ + Alle brukere + +
+
+ + +
+ + + + +
+ + + + + Navn + + Epost + + Telefon + + + + + + + + Lise Nordmann + + lise@nordmann.no + 12345678 + + + + + Ola Nordmann + + ola@nordmann.no + 87654321 + + + + + Espen Slinde + + espen@nordmann.no + 43215678 + + +
+
+ +
+ + Innstillinger + + + Her kan du administrere brukerene{' '} + + +
+
+ Visning + + Her kan du administrere + +
+ +
+ +
+
+ Visning + + Her kan du administrere + +
+ +
+ +
+ Visnigsmodus + + + + +
+
+ +
+ + Reiste til storbyen + + +
+ + Musikk + + + Arikitektur + + + By + +
+ + In train outlines three due nor + + + Seven theoretically cannot retired thin over and is rewritten an I + were + + + Les mer om talentet + +
+
+
+ + Meld deg på nyhetsbrevet! + + + phase to leave an examples are up pane, completely tag by duties + were but pointing address even at the rolled a ourselves, was + starting parts place aslo will do a lot if the + + + Place copy the found with he an the an as a and that a pane, weary + phase to leave an examples are up pane. + +
+ + + + +
+
+ + Folk du kanskje kjenner + +
+
+ ); +}; diff --git a/apps/theme/components/Scale/Scale.module.css b/apps/theme/components/Scale/Scale.module.css index 2043b0a77b..59f8d2dec6 100644 --- a/apps/theme/components/Scale/Scale.module.css +++ b/apps/theme/components/Scale/Scale.module.css @@ -1,7 +1,6 @@ .test { display: flex; - gap: 24px; - flex-wrap: wrap; + gap: 16px; justify-content: center; } diff --git a/apps/theme/components/Scale/Scale.tsx b/apps/theme/components/Scale/Scale.tsx index d12bbddf4e..39ed1ca347 100644 --- a/apps/theme/components/Scale/Scale.tsx +++ b/apps/theme/components/Scale/Scale.tsx @@ -1,44 +1,15 @@ import { RovingFocusRoot } from '@digdir/designsystemet-react'; -import type { ColorInfo } from '@digdir/designsystemet/color'; -import { useEffect, useState } from 'react'; - -import type { ThemeColors, modeType } from '../../types'; +import type { ThemeInfo } from '@digdir/designsystemet/color'; +import type { modeType } from '../../types'; import { Group } from '../Group/Group'; import classes from './Scale.module.css'; type ScaleProps = { - colorScale: ColorInfo[]; + colorScale: ThemeInfo; showHeader?: boolean; showColorMeta?: boolean; themeMode: modeType; - type: ThemeColors; -}; - -const setTokens = (lightColors: ColorInfo[], type: string) => { - const previewElement = document.getElementById('preview'); - if (previewElement) { - for (let i = 0; i < lightColors.length; i++) { - previewElement.style.setProperty( - '--' + type + '-' + (i + 1), - lightColors[i].hex, - ); - } - } -}; - -const generateDefaultColors = () => { - const arr: ColorInfo[] = []; - - for (let i = 0; i < 14; i++) { - arr.push({ - hex: '#ffffff', - number: 1, - name: 'Default', - }); - } - - return arr; }; export const Scale = ({ @@ -46,55 +17,52 @@ export const Scale = ({ showHeader, showColorMeta, themeMode, - type, }: ScaleProps) => { - const [colors, setColors] = useState(generateDefaultColors()); - - useEffect(() => { - if (colorScale.length > 0) { - setColors(colorScale); - } - setTokens(colorScale, type); - }, [colorScale, themeMode, type]); return (
diff --git a/apps/theme/components/Scales/Scales.tsx b/apps/theme/components/Scales/Scales.tsx deleted file mode 100644 index 147e12a937..0000000000 --- a/apps/theme/components/Scales/Scales.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import type { ColorMode } from '@digdir/designsystemet/color'; -import cl from 'clsx/lite'; - -import { useThemeStore } from '../../store'; -import { Scale } from '../Scale/Scale'; - -import classes from './Scales.module.css'; - -type ScalesProps = { - themeMode: ColorMode; -}; - -export const Scales = ({ themeMode }: ScalesProps) => { - const accentTheme = useThemeStore((state) => state.accentTheme); - const neutralTheme = useThemeStore((state) => state.neutralTheme); - const brandOneTheme = useThemeStore((state) => state.brandOneTheme); - const brandTwoTheme = useThemeStore((state) => state.brandTwoTheme); - const brandThreeTheme = useThemeStore((state) => state.brandThreeTheme); - return ( -
-
-
Accent
- -
-
-
Neutral
- -
- -
-
Brand 1
- -
-
-
Brand 2
- -
- -
-
Brand 3
- -
-
- ); -}; diff --git a/apps/theme/components/Sidebar/ColorPage/ColorPage.module.css b/apps/theme/components/Sidebar/ColorPage/ColorPage.module.css new file mode 100644 index 0000000000..ef9a55481c --- /dev/null +++ b/apps/theme/components/Sidebar/ColorPage/ColorPage.module.css @@ -0,0 +1,62 @@ +.group { + margin-bottom: 24px; + margin-top: -8px; +} + +.group:last-child { + margin-bottom: 0; +} + +.groupHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + height: 44px; +} + +.AddBtn { + margin-right: -8px; +} + +.colors { + display: flex; + flex-wrap: wrap; + gap: 16px; +} + +.desc { + margin-top: 12px; + margin-bottom: 20px; +} + +.btnGroup { + display: flex; + gap: 12px; + width: 100%; +} + +.btn { + width: 100%; +} + +.bottom { + position: absolute; + bottom: 0; + right: 0; + left: 0; + padding: 24px; + border-top: 1px solid var(--ds-color-neutral-border-subtle); + display: flex; + align-items: center; + gap: 16px; + background-color: var(--ds-color-info-background-default); + border-radius: 0 0 8px 8px; +} + +.separator { + margin-bottom: 20px; + height: 1px; + width: 100%; + background-color: var(--ds-color-neutral-border-subtle); +} diff --git a/apps/theme/components/Sidebar/ColorPage/ColorPage.tsx b/apps/theme/components/Sidebar/ColorPage/ColorPage.tsx new file mode 100644 index 0000000000..970d79ceb8 --- /dev/null +++ b/apps/theme/components/Sidebar/ColorPage/ColorPage.tsx @@ -0,0 +1,190 @@ +import type { CssColor } from '@adobe/leonardo-contrast-colors'; +import { generateThemeForColor } from '@digdir/designsystemet'; +import { Button, Heading, Paragraph } from '@digdir/designsystemet-react'; +import { PlusIcon } from '@navikt/aksel-icons'; +import { useState } from 'react'; +import { ColorService, useColor } from 'react-color-palette'; +import { type ColorTheme, useThemeStore } from '../../../store'; +import { ColorInput } from '../../ColorInput/ColorInput'; +import { TokenModal } from '../../TokenModal/TokenModal'; +import { ColorPane } from '../ColorPane/ColorPane'; +import classes from './ColorPage.module.css'; + +type ColorPageProps = { + onPrevClick?: () => void; + onNextClick: () => void; +}; + +export const ColorPage = ({ onPrevClick, onNextClick }: ColorPageProps) => { + type Pages = 'addColor' | 'editColor' | 'none'; + type ColorType = 'main' | 'neutral' | 'support'; + + const removeColor = useThemeStore((state) => state.removeColor); + const addColor = useThemeStore((state) => state.addColor); + const updateColor = useThemeStore((state) => state.updateColor); + const colors = useThemeStore((state) => state.colors); + const [activePanel, setActivePanel] = useState('none'); + const [color, setColor] = useColor('#0062ba'); + const [name, setName] = useState(''); + const [index, setIndex] = useState(0); + const [colorType, setColorType] = useState('main'); + const [open, setOpen] = useState(false); + + const addNewColor = (color: string, name: string) => { + const theme = generateThemeForColor(color as CssColor, 'aa'); + addColor({ name: name, colors: theme }, colorType); + }; + + const updateExistingColor = (color: string, name: string) => { + const theme = generateThemeForColor(color as CssColor, 'aa'); + updateColor({ name: name, colors: theme }, index, colorType); + }; + + const setupEditState = ( + color: ColorTheme, + index: number, + type: ColorType, + ) => { + setActivePanel('editColor'); + setColor(ColorService.convert('hex', color.colors.light[8].hex)); + setName(color.name); + setIndex(index); + setColorType(type); + }; + + return ( +
+ + Velg fargene dine + + + Logbook a sitting success parents' girl in it however, greater, full + with he that pleasures up attention to hardly to power definitely hardly + + {/* MAIN COLORS */} +
+
+ Hovedfarger + {colors.main.length < 40 && ( + + )} + {colors.main.length >= 40 && ( +
Maks 4 hovedfarger
+ )} +
+
+ {colors.main.map((color, index) => ( + setupEditState(color, index, 'main')} + /> + ))} +
+
+
+
+
+ {colors.neutral.map((color, index) => ( + setupEditState(color, index, 'neutral')} + /> + ))} +
+
+ + {/* SUPPORT COLORS */} +
+
+ Støttefarger + {colors.support.length < 40 && ( + + )} + {colors.support.length >= 40 && ( +
Maks 4 støttefarger
+ )} +
+
+ {colors.support.map((color, index) => ( + setupEditState(color, index, 'support')} + /> + ))} +
+
+ +
+
+ + {/* */} +
+
+ + { + setColor(ColorService.convert('hex', '#0062ba')); + setName(''); + setActivePanel('none'); + }} + onPrimaryClicked={(color, name) => { + if (name === '') { + return; + } + if (activePanel === 'addColor') { + addNewColor(color, name); + } else { + updateExistingColor(color, name); + } + setColor(ColorService.convert('hex', '#0062ba')); + setName(''); + setActivePanel('none'); + }} + onRemove={() => { + removeColor(index, colorType); + setName(''); + setActivePanel('none'); + }} + type={activePanel} + color={color} + name={name} + setColor={setColor} + setName={setName} + colorType={colorType} + /> +
+ ); +}; diff --git a/apps/theme/components/Sidebar/ColorPane/ColorPane.module.css b/apps/theme/components/Sidebar/ColorPane/ColorPane.module.css new file mode 100644 index 0000000000..d7bb3e4427 --- /dev/null +++ b/apps/theme/components/Sidebar/ColorPane/ColorPane.module.css @@ -0,0 +1,75 @@ +.colorPage { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: var(--ds-color-neutral-background-default); + padding: 24px; + border-radius: 8px; + display: none; + z-index: 5; +} + +.title { + margin-top: 12px; + margin-bottom: 16px; +} + +.name { + margin-bottom: 24px; +} + +.label { + font-size: 16px; + font-weight: 500; + margin-bottom: 12px; +} + +.topBtnGroup { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: -4px; +} + +.btnGroup { + display: flex; + margin-top: 24px; + gap: 16px; +} + +.removeBtn { + margin-right: -8px; + margin-top: -4px; +} + +.colorPreviewContainer { + border: 1px solid var(--ds-color-neutral-border-subtle); + margin-bottom: 16px; + border-radius: 8px; +} + +.colorPreview { + height: 120px; + width: 100%; + border-radius: 8px; +} + +.back { + margin-left: -16px; + margin-top: -4px; +} + +.show { + display: block; +} + +.hide { + display: none; +} + +.desc { + color: var(--ds-color-neutral-text-subtle); + margin-bottom: 16px; +} diff --git a/apps/theme/components/Sidebar/ColorPane/ColorPane.tsx b/apps/theme/components/Sidebar/ColorPane/ColorPane.tsx new file mode 100644 index 0000000000..6b4a380f18 --- /dev/null +++ b/apps/theme/components/Sidebar/ColorPane/ColorPane.tsx @@ -0,0 +1,132 @@ +import { + Button, + Heading, + Paragraph, + Textfield, +} from '@digdir/designsystemet-react'; +import { ChevronLeftIcon, TrashIcon } from '@navikt/aksel-icons'; +import { ColorPicker, type IColor } from 'react-color-palette'; + +import cl from 'clsx/lite'; +import classes from './ColorPane.module.css'; + +type ColorPaneProps = { + onClose: () => void; + onPrimaryClicked: (color: string, name: string) => void; + show?: boolean; + type: 'addColor' | 'editColor' | 'none'; + color: IColor; + setColor: (color: IColor) => void; + name: string; + setName: (name: string) => void; + onRemove: () => void; + colorType: 'main' | 'neutral' | 'support'; +}; + +export const ColorPane = ({ + onClose, + onPrimaryClicked, + show = false, + type, + color, + setColor, + name, + setName, + onRemove, + colorType, +}: ColorPaneProps) => { + const getHeading = () => { + const t = colorType === 'main' ? 'hovedfarge' : 'støttefarge'; + return type === 'addColor' ? 'Legg til ' + t : 'Rediger farge'; + }; + + return ( +
+
+ + +
+ + {getHeading()} + + {colorType === 'neutral' && ( + + Neutral fargen kan ikke fjernes eller endres navn på. + + )} + {colorType !== 'neutral' && ( + { + const value = e.currentTarget.value + .replace(/\s+/g, '-') + .replace(/[^A-Z0-9-]+/gi, '') + .toLowerCase(); + setName(value); + }} + /> + )} +
Farge
+
+
+
+ +
+ + + +
+
+ ); +}; diff --git a/apps/theme/components/Sidebar/FinishPage/FinishPage.module.css b/apps/theme/components/Sidebar/FinishPage/FinishPage.module.css new file mode 100644 index 0000000000..e4a9e7876e --- /dev/null +++ b/apps/theme/components/Sidebar/FinishPage/FinishPage.module.css @@ -0,0 +1,28 @@ +.desc { + margin-top: 12px; + margin-bottom: 12px; +} + +.btnGroup { + display: flex; + gap: 12px; + width: 100%; +} + +.btn { + width: 100%; +} + +.bottom { + position: absolute; + bottom: 0; + right: 0; + left: 0; + padding: 24px; + border-top: 1px solid var(--ds-color-neutral-border-subtle); + display: flex; + align-items: center; + gap: 16px; + background-color: var(--ds-color-info-background-default); + border-radius: 0 0 8px 8px; +} diff --git a/apps/theme/components/Sidebar/FinishPage/FinishPage.tsx b/apps/theme/components/Sidebar/FinishPage/FinishPage.tsx new file mode 100644 index 0000000000..aa9fd023a1 --- /dev/null +++ b/apps/theme/components/Sidebar/FinishPage/FinishPage.tsx @@ -0,0 +1,47 @@ +import { Button, Heading, Paragraph } from '@digdir/designsystemet-react'; +import { ChevronLeftIcon } from '@navikt/aksel-icons'; +import { useState } from 'react'; +import { TokenModal } from '../../TokenModal/TokenModal'; +import classes from './FinishPage.module.css'; + +type FinishPageProps = { + onPrevClick: () => void; +}; + +export const FinishPage = ({ onPrevClick }: FinishPageProps) => { + const [modalOpen, setModalOpen] = useState(false); + return ( +
+ Ta i bruk tema + + Logbook a sitting success parents' girl in it however, greater, full + with he that pleasures up attention to hardly to power definitely hardly + + + + Logbook a sitting success parents' girl in it however, greater, full + with he that pleasures up attention to hardly to power definitely hardly + + + + Logbook a sitting success parents' girl in it however, greater, full + with he that pleasures up attention to hardly to power definitely hardly + + +
+
+ + +
+
+
+ ); +}; diff --git a/apps/theme/components/Sidebar/IntroPage/IntroPage.module.css b/apps/theme/components/Sidebar/IntroPage/IntroPage.module.css new file mode 100644 index 0000000000..6d8efcb6d9 --- /dev/null +++ b/apps/theme/components/Sidebar/IntroPage/IntroPage.module.css @@ -0,0 +1,22 @@ +.desc { + margin-top: 12px; + margin-bottom: 12px; +} + +.btn { + width: 100%; +} + +.bottom { + position: absolute; + bottom: 0; + right: 0; + left: 0; + padding: 24px; + border-top: 1px solid var(--ds-color-neutral-border-subtle); + display: flex; + align-items: center; + gap: 16px; + background-color: var(--ds-color-info-background-default); + border-radius: 0 0 8px 8px; +} diff --git a/apps/theme/components/Sidebar/IntroPage/IntroPage.tsx b/apps/theme/components/Sidebar/IntroPage/IntroPage.tsx new file mode 100644 index 0000000000..a91af5d5aa --- /dev/null +++ b/apps/theme/components/Sidebar/IntroPage/IntroPage.tsx @@ -0,0 +1,60 @@ +import { + Button, + Heading, + Paragraph, + Textfield, +} from '@digdir/designsystemet-react'; +import { ChevronRightIcon } from '@navikt/aksel-icons'; +import { useThemeStore } from '../../../store'; +import classes from './IntroPage.module.css'; + +type IntroPageProps = { + onNextClick: () => void; +}; + +export const IntroPage = ({ onNextClick }: IntroPageProps) => { + const themeName = useThemeStore((state) => state.themeName); + const setThemeName = useThemeStore((state) => state.setThemeName); + return ( +
+ Konfigurer temaet ditt + + Logbook a sitting success parents' girl in it however, greater, full + with he that pleasures up attention to hardly to power definitely hardly + + + + Logbook a sitting success parents' girl in it however, greater, full + with he that pleasures up attention to hardly to power definitely hardly + + + + Start først med å gi temaet ditt et navn. + + + { + const value = e.currentTarget.value + .replace(/\s+/g, '-') + .replace(/[^A-Z0-9-]+/gi, '') + .toLowerCase(); + + setThemeName(value); + }} + /> +
+ +
+
+ ); +}; diff --git a/apps/theme/components/Sidebar/RadiusPage/RadiusPage.module.css b/apps/theme/components/Sidebar/RadiusPage/RadiusPage.module.css new file mode 100644 index 0000000000..009c34c8c0 --- /dev/null +++ b/apps/theme/components/Sidebar/RadiusPage/RadiusPage.module.css @@ -0,0 +1,36 @@ +.desc { + margin-top: 12px; + margin-bottom: 24px; +} + +.separator { + margin-top: 24px; + margin-bottom: 24px; + height: 1px; + width: 100%; + background-color: var(--ds-color-neutral-border-subtle); +} + +.btnGroup { + display: flex; + gap: 16px; + width: 100%; +} + +.btn { + width: 100%; +} + +.bottom { + position: absolute; + bottom: 0; + right: 0; + left: 0; + padding: 24px; + border-top: 1px solid var(--ds-color-neutral-border-subtle); + display: flex; + align-items: center; + gap: 16px; + background-color: var(--ds-color-info-background-default); + border-radius: 0 0 8px 8px; +} diff --git a/apps/theme/components/Sidebar/RadiusPage/RadiusPage.tsx b/apps/theme/components/Sidebar/RadiusPage/RadiusPage.tsx new file mode 100644 index 0000000000..a8d5cc1aa2 --- /dev/null +++ b/apps/theme/components/Sidebar/RadiusPage/RadiusPage.tsx @@ -0,0 +1,41 @@ +import { Heading, Paragraph } from '@digdir/designsystemet-react'; +import { BorderRadiusInput } from '../../BorderRadiusInput/BorderRadiusInput'; +import { TokenModal } from '../../TokenModal/TokenModal'; +import classes from './RadiusPage.module.css'; + +type RadiusPageProps = { + onPrevClick: () => void; + onNextClick?: () => void; +}; + +export const RadiusPage = ({ onPrevClick, onNextClick }: RadiusPageProps) => { + return ( +
+ Velg border radius + + Logbook a sitting success parents' girl in it however, greater, full + with he that pleasures up attention to hardly to power definitely hardly + + + {/* BORDER RADIUS */} + + +
+
+ + {/* */} +
+
+
+ ); +}; diff --git a/apps/theme/components/Sidebar/Sidebar.module.css b/apps/theme/components/Sidebar/Sidebar.module.css new file mode 100644 index 0000000000..170c34cbf5 --- /dev/null +++ b/apps/theme/components/Sidebar/Sidebar.module.css @@ -0,0 +1,121 @@ +.sidebar { + position: relative; + z-index: 100; + right: 20px; + top: 20px; + background-color: var(--ds-color-neutral-background-default); + border: 1px solid var(--ds-color-neutral-border-subtle); + box-shadow: 0px 0px 60px 10px rgba(0, 0, 0, 0.1); + width: 420px; + flex-basis: 410px; + flex-grow: 1; + border-radius: 8px; + margin-top: -170px; + min-height: 835px; +} + +.showSidebar { + display: block !important; + top: 12px; + margin-top: 0; + right: 12px; + bottom: 12px; + position: fixed; +} + +.scrollContainer { + overflow: auto; + height: calc(100% - 90px); + padding: 24px; +} + +.sticky { + position: fixed; + margin-top: 0; +} + +.title { + margin-bottom: 20px; +} + +.group { + margin-bottom: 24px; + margin-top: -8px; +} + +.group:last-child { + margin-bottom: 0; +} + +.groupHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + height: 42px; +} + +.AddBtn { + margin-right: -8px; +} + +.error { + font-weight: 400; + font-size: 16px; + color: var(--ds-color-neutral-text-subtle); +} + +.label { + font-size: 16px; + margin-bottom: 8px; +} + +.themeMode { + margin-bottom: 24px; + margin-top: 24px; +} + +.sizes { + display: flex; + gap: 8px; + margin-top: -4px; +} + +.bottom { + position: absolute; + bottom: 0; + right: 0; + left: 0; + padding: 24px; + border-top: 1px solid var(--ds-color-neutral-border-subtle); + display: flex; + align-items: center; + gap: 16px; + background-color: var(--ds-color-info-background-default); + border-radius: 0 0 8px 8px; +} + +.toggle { + position: fixed; + top: 120px; + right: 20px; + height: 52px; + width: 52px; + border-radius: 50%; + border: 1px solid var(--ds-color-neutral-border-subtle); + display: none; + z-index: 5; + background-color: var(--ds-color-neutral-background-default); +} + +@media screen and (max-width: 1500px) { + .sidebar { + display: none; + } + .toggle { + display: block; + } + .toggleOpen { + right: 440px; + } +} diff --git a/apps/theme/components/Sidebar/Sidebar.tsx b/apps/theme/components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000000..b5ac4abe8e --- /dev/null +++ b/apps/theme/components/Sidebar/Sidebar.tsx @@ -0,0 +1,104 @@ +import cl from 'clsx/lite'; +import { useEffect, useState } from 'react'; + +import { useThemeStore } from '../../store'; + +import { CogIcon } from '@navikt/aksel-icons'; +import { ColorPage } from './ColorPage/ColorPage'; +import { RadiusPage } from './RadiusPage/RadiusPage'; +import classes from './Sidebar.module.css'; + +export const Sidebar = () => { + const setAppearance = useThemeStore((state) => state.setAppearance); + const [isSticky, setSticky] = useState(false); + const [size, setSize] = useState('sm'); + const [showSidebar, setShowSidebar] = useState(false); + const activePage = useThemeStore((state) => state.activePage); + const setActivePage = useThemeStore((state) => state.setActivePage); + + useEffect(() => { + const handleScroll = () => { + setSticky(window.scrollY > 135); + }; + + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + return ( +
+ +
+
+ {activePage === 'colors' && ( + setActivePage('radius')} /> + )} + {activePage === 'radius' && ( + setActivePage('colors')} /> + )} + + {/* APPEARANCE */} + {/*
+
+
Visning
+ { + const val = value; + setAppearance(val as ColorMode); + }} + /> +
+
*/} + + {/* SIZES */} + {/*
+
+ Komponentstørrelser +
+
+ setSize(size)} + name='xSmall' + size='14px' + /> + setSize(size)} + name='Small' + size='16px' + /> + setSize(size)} + name='Medium' + size='18px' + /> + setSize(size)} + name='Large' + size='21px' + /> +
+
*/} +
+
+
+ ); +}; diff --git a/apps/theme/components/SizeInput/SizeInput.module.css b/apps/theme/components/SizeInput/SizeInput.module.css new file mode 100644 index 0000000000..cb2add142d --- /dev/null +++ b/apps/theme/components/SizeInput/SizeInput.module.css @@ -0,0 +1,32 @@ +.component { + display: flex; + flex-direction: column; + flex: 1 1; +} + +.label { + font-size: 16px; + margin-bottom: 8px; +} + +.box { + width: 100%; + text-align: left; + background-color: transparent; + border: 1px solid var(--ds-color-neutral-border-subtle); + border-radius: 4px; + padding: 10px; + display: flex; + align-items: center; + justify-content: space-between; + font-size: 15px; +} + +.box:hover { + cursor: pointer; + background-color: var(--ds-color-neutral-background-subtle); +} + +.icon { + color: var(--ds-color-neutral-text-subtle); +} diff --git a/apps/theme/components/SizeInput/SizeInput.tsx b/apps/theme/components/SizeInput/SizeInput.tsx new file mode 100644 index 0000000000..3082a6ba92 --- /dev/null +++ b/apps/theme/components/SizeInput/SizeInput.tsx @@ -0,0 +1,24 @@ +import { PencilIcon } from '@navikt/aksel-icons'; +import classes from './SizeInput.module.css'; + +type SizeInputProps = { + onChange: (size: string) => void; + name: string; + size: string; +}; + +export const SizeInput = ({ onChange, size, name }: SizeInputProps) => { + return ( +
+
{name}
+ +
+ ); +}; diff --git a/apps/theme/components/ThemeToggle/ThemeToggle.module.css b/apps/theme/components/ThemeToggle/ThemeToggle.module.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/theme/components/ThemeToggle/ThemeToggle.tsx b/apps/theme/components/ThemeToggle/ThemeToggle.tsx new file mode 100644 index 0000000000..de1c16cce1 --- /dev/null +++ b/apps/theme/components/ThemeToggle/ThemeToggle.tsx @@ -0,0 +1,53 @@ +import cl from 'clsx/lite'; +import { useState } from 'react'; +import { useThemeStore } from '../../store'; +import classes from './ThemeToggle.module.css'; + +export const ThemeToggle = () => { + const [active, setActive] = useState(0); + const setBorderRadius = useThemeStore((state) => state.setBorderRadius); + const items = [ + { name: 'Ingen', type: 'sm', value: '0px' }, + { name: 'Small', type: 'sm', value: '6px' }, + { name: 'Medium', type: 'sm', value: '10px' }, + { name: 'Large', type: 'sm', value: '13px' }, + { name: 'Full', type: 'sm', value: '9999px' }, + ]; + + return ( +
+
+ {items.map((item, index) => ( +
setActive(index)} + > +
{ + if (item.name === 'Ingen') { + setBorderRadius('none'); + } else { + setBorderRadius( + item.name.toLowerCase() as + | 'small' + | 'medium' + | 'large' + | 'full', + ); + } + }} + > +
{item.name}
+
+
+
+ ))} +
+
+ ); +}; diff --git a/apps/theme/components/ThemeToolbar/ThemeToolbar.module.css b/apps/theme/components/ThemeToolbar/ThemeToolbar.module.css deleted file mode 100644 index 31f2ad9b25..0000000000 --- a/apps/theme/components/ThemeToolbar/ThemeToolbar.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.pickers { - display: flex; - align-items: flex-end; - justify-content: center; - gap: var(--ds-spacing-6); - padding: 12px 0; - flex-wrap: wrap; -} - -.pickersContainer { - height: 140px; -} - -.contrastSelect { - width: 150px; -} - -.select { - display: flex; - flex-direction: column; - gap: var(--ds-spacing-2); -} - -.shareBtn { - margin-left: 24px; -} diff --git a/apps/theme/components/ThemeToolbar/ThemeToolbar.tsx b/apps/theme/components/ThemeToolbar/ThemeToolbar.tsx deleted file mode 100644 index e40f2fdfff..0000000000 --- a/apps/theme/components/ThemeToolbar/ThemeToolbar.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import type { CssColor } from '@adobe/leonardo-contrast-colors'; -import { Button, Tooltip } from '@digdir/designsystemet-react'; -import type { ColorError, ContrastMode } from '@digdir/designsystemet/color'; -import cl from 'clsx/lite'; -import { useState } from 'react'; - -import { useThemeStore } from '../../store'; -import type { ThemeColors } from '../../types'; -import { ColorPicker } from '../ColorPicker/ColorPicker'; -import { TokenModal } from '../TokenModal/TokenModal'; - -import classes from './ThemeToolbar.module.css'; - -type ThemeToolbarProps = { - accentError: ColorError; - neutralError: ColorError; - brand1Error: ColorError; - brand2Error: ColorError; - brand3Error: ColorError; - borderRadius: string; - onColorChanged: (type: ThemeColors, color: CssColor) => void; - onContrastModeChanged: (mode: 'aa' | 'aaa') => void; - onBorderRadiusChanged: (radius: string) => void; - contrastMode: ContrastMode; -}; - -export const borderRadii = { - '0 rem': '0', - '0.25 rem': '0.25rem', - '0.5 rem': '0.5rem', - '0.75 rem': '0.75rem', - '1 rem': '1rem', - full: '99999px', -}; - -export const ThemeToolbar = ({ - accentError, - neutralError, - brand1Error, - brand2Error, - brand3Error, - borderRadius, - onColorChanged, - onContrastModeChanged, - onBorderRadiusChanged, - contrastMode, -}: ThemeToolbarProps) => { - const accentTheme = useThemeStore((state) => state.accentTheme); - const neutralTheme = useThemeStore((state) => state.neutralTheme); - const brandOneTheme = useThemeStore((state) => state.brandOneTheme); - const brandTwoTheme = useThemeStore((state) => state.brandTwoTheme); - const brandThreeTheme = useThemeStore((state) => state.brandThreeTheme); - - const [toolTipText, setToolTipText] = useState('Kopier nettadresse'); - const onButtonClick = () => { - setToolTipText('Kopiert!'); - navigator.clipboard.writeText(window.location.href).catch((reason) => { - throw Error(String(reason)); - }); - }; - - return ( -
-
- { - onColorChanged('accent', color); - }} - /> - { - onColorChanged('neutral', color); - }} - /> - { - onColorChanged('brand1', color); - }} - /> - { - onColorChanged('brand2', color); - }} - /> - { - onColorChanged('brand3', color); - }} - /> - - {/*
- - -
*/} - - {/* -
- - -
- */} - - - - -
-
- ); -}; diff --git a/apps/theme/components/ThemeWrapper/ThemeWrapper.tsx b/apps/theme/components/ThemeWrapper/ThemeWrapper.tsx new file mode 100644 index 0000000000..b38c1ad5b1 --- /dev/null +++ b/apps/theme/components/ThemeWrapper/ThemeWrapper.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { useThemeStore } from '../../store'; + +type ThemeWrapperProps = { + children: React.ReactNode; +}; + +export const ThemeWrapper = ({ children }: ThemeWrapperProps) => { + const appearance = useThemeStore((state) => state.appearance); + const theme = useThemeStore((state) => state.themePreview); + + return ( +
+ {children} +
+ ); +}; diff --git a/apps/theme/components/Toggle/Toggle.module.css b/apps/theme/components/Toggle/Toggle.module.css new file mode 100644 index 0000000000..e666da3a73 --- /dev/null +++ b/apps/theme/components/Toggle/Toggle.module.css @@ -0,0 +1,84 @@ +.toggle { + display: flex; + gap: 12px; + width: 100%; +} + +.box { + border: 1px solid var(--ds-color-neutral-border-subtle); + border-radius: 4px; + height: 42px; + display: flex; + align-items: center; + padding-left: 10px; + overflow: hidden; +} + +.box:hover { + cursor: pointer; + background-color: var(--ds-color-neutral-background-subtle); +} + +.box:hover .inner:before { + background-color: var(--ds-color-neutral-background-subtle); +} + +.item { + text-align: center; + font-size: 16px; + display: flex; + flex: 1 1; + flex-direction: column; + gap: 4px; +} + +.active .box { + outline: 1px solid var(--ds-color-neutral-border-default); + border-color: var(--ds-color-neutral-border-default); +} + +.inner { + height: 24px; + width: 48px; + border: 4px solid var(--ds-color-neutral-border-default); + position: relative; + background-color: var(--ds-color-neutral-surface-default); + margin-left: -18px; +} + +.inner:before { + content: ''; + display: block; + height: 24px; + width: 24px; + background-color: var(--ds-color-neutral-background-default); + position: absolute; + left: -4px; + top: -4px; +} + +.appearance { + display: flex; + align-items: center; + gap: 8px; +} + +.icon { + height: 20px; + width: 20px; + border: 2px solid var(--ds-color-neutral-border-strong); + border-radius: 50%; + overflow: hidden; +} + +.dark:before { + content: ''; + display: block; + height: 20px; + width: 9px; + background-color: var(--ds-color-neutral-border-strong); +} + +.contrast { + background-color: var(--ds-color-neutral-border-strong); +} diff --git a/apps/theme/components/Toggle/Toggle.tsx b/apps/theme/components/Toggle/Toggle.tsx new file mode 100644 index 0000000000..4ff06ce5a8 --- /dev/null +++ b/apps/theme/components/Toggle/Toggle.tsx @@ -0,0 +1,48 @@ +import { MoonIcon, SunIcon } from '@navikt/aksel-icons'; +import cl from 'clsx/lite'; +import { useState } from 'react'; +import classes from './Toggle.module.css'; + +type ToggleProps = { + type: 'radius' | 'appearance'; + items: { name: string; type: string; value: string }[]; + showLabel?: boolean; + onChange?: (value: string) => void; +}; + +export const Toggle = ({ + type, + items, + showLabel = false, + onChange, +}: ToggleProps) => { + const [active, setActive] = useState(0); + + return ( +
+ {items.map((item, index) => ( +
setActive(index)} + > +
onChange?.(item.value)} + > +
+ {item.value === 'light' && ( + + )} + {item.value === 'dark' && ( + + )} + {item.name} +
+
+ {showLabel &&
{item.name}
} +
+ ))} +
+ ); +}; diff --git a/apps/theme/components/TokenModal/TokenModal.module.css b/apps/theme/components/TokenModal/TokenModal.module.css index d493b29862..4fea22d911 100644 --- a/apps/theme/components/TokenModal/TokenModal.module.css +++ b/apps/theme/components/TokenModal/TokenModal.module.css @@ -1,7 +1,6 @@ .content { display: flex; flex-wrap: wrap; - margin-top: var(--ds-spacing-6); } .leftSection, @@ -16,13 +15,13 @@ } .rightSection { - padding: var(--ds-spacing-4) 0 var(--ds-spacing-4) var(--ds-spacing-8); + display: flex; + flex-direction: column; + gap: var(--ds-spacing-4); } .infoBoxes { - border-top: 1px solid var(--ds-color-neutral-border-subtle); - margin-top: var(--ds-spacing-8); - padding-top: var(--ds-spacing-8); + padding-top: var(--ds-spacing-6); display: flex; flex-direction: column; gap: var(--ds-spacing-8); @@ -60,7 +59,6 @@ } .contact { - margin-top: var(--ds-spacing-6); gap: 8px; display: flex; } @@ -73,7 +71,6 @@ .snippet code { background-color: transparent; - height: 290px; display: block; } @@ -85,7 +82,7 @@ .headerText { margin-right: auto; - font-size: var(--ds-spacing-6); + font-size: var(--ds-spacing-5); } .emblem { @@ -93,6 +90,27 @@ margin-right: var(--ds-spacing-2); } +.trigger { + width: 100%; +} + +.step { + display: flex; + gap: var(--ds-spacing-4); +} + +.step span { + background-color: var(--ds-color-accent-base-default); + color: var(--ds-color-accent-contrast-default); + aspect-ratio: 1; + border-radius: var(--ds-border-radius-full); + width: var(--ds-sizing-8); + height: var(--ds-sizing-8); + display: grid; + place-items: center; + font-size: var(--ds-body-sm-font-size); +} + @media (max-width: 1150px) { .content { flex-direction: column; diff --git a/apps/theme/components/TokenModal/TokenModal.tsx b/apps/theme/components/TokenModal/TokenModal.tsx index a9395b09e0..e71e8e01d7 100644 --- a/apps/theme/components/TokenModal/TokenModal.tsx +++ b/apps/theme/components/TokenModal/TokenModal.tsx @@ -1,76 +1,42 @@ 'use client'; -import type { CssColor } from '@adobe/leonardo-contrast-colors'; import { + Divider, Heading, Link, Modal, Paragraph, - Textfield, } from '@digdir/designsystemet-react'; -import { colorCliOptions, createTokens } from '@digdir/designsystemet/tokens'; -import { CodeIcon, InformationSquareIcon } from '@navikt/aksel-icons'; +import { colorCliOptions } from '@digdir/designsystemet/tokens'; +import { InformationSquareIcon, StarIcon } from '@navikt/aksel-icons'; import { CodeSnippet } from '@repo/components'; -import { useEffect, useRef, useState } from 'react'; +import { useRef } from 'react'; import cl from 'clsx/lite'; +import { type ColorTheme, useThemeStore } from '../../store'; import classes from './TokenModal.module.css'; -type TokenModalProps = { - accentColor: CssColor; - neutralColor: CssColor; - brand1Color: CssColor; - brand2Color: CssColor; - brand3Color: CssColor; - borderRadius: string; -}; - -const toFigmaSnippet = (obj: unknown) => - JSON.stringify(obj, null, 2).replaceAll('$', ''); - -export const TokenModal = ({ - accentColor, - neutralColor, - brand1Color, - brand2Color, - brand3Color, - borderRadius, -}: TokenModalProps) => { +export const TokenModal = () => { const modalRef = useRef(null); - const [lightThemeSnippet, setLightThemeSnippet] = useState(''); - const [darkThemeSnippet, setDarkThemeSnippet] = useState(''); - const [themeName, setThemeName] = useState('theme'); + const colors = useThemeStore((state) => state.colors); + const themeName = useThemeStore((state) => state.themeName); + + const setCliColors = (colorTheme: ColorTheme[]) => { + let str = ''; + for (const color of colorTheme) { + str += `"${color.name}:${color.colors.light[8].hex}" `; + } + return str; + }; const cliSnippet = `npx @digdir/designsystemet@next tokens create \\ - --${colorCliOptions.main} "accent:${accentColor}" \\ - --${colorCliOptions.neutral} "${neutralColor}" \\ - --${colorCliOptions.support} "brand1:${brand1Color}" "brand2:${brand2Color}" "brand3:${brand3Color}" \\ + --${colorCliOptions.main} ${setCliColors(colors.main)} \\ + --${colorCliOptions.neutral} "${colors.neutral[0]?.colors.light[8].hex}" \\ + --${colorCliOptions.support} ${setCliColors(colors.support)} \\ --theme "${themeName}" \\ - --write - `; - - useEffect(() => { - const tokens = createTokens({ - colors: { - main: { - accent: accentColor, - }, - neutral: neutralColor, - support: { - brand1: brand1Color, - brand2: brand2Color, - brand3: brand3Color, - }, - }, - typography: { fontFamily: 'Inter' }, - themeName: 'theme', - }); - - setLightThemeSnippet(toFigmaSnippet(tokens.colors.light.theme)); - setDarkThemeSnippet(toFigmaSnippet(tokens.colors.dark.theme)); - }, []); + --write`; type InfoBoxType = { title: string; @@ -105,113 +71,111 @@ export const TokenModal = ({ return ( { return modalRef.current?.showModal(); }} > + Ta i bruk tema - - - Ta i bruk tema - - -
-
- { - const value = e.currentTarget.value - .replace(/\s+/g, '-') - .replace(/[^A-Z0-9-]+/gi, '') - .toLowerCase(); + + + + Ta i bruk tema + + - setThemeName(value); - }} - /> -
-
-
-
-
- {cliSnippet} -
-
-
-
-
- Noe som ikke fungerer? - - Send oss en melding på{' '} + +
+
+
+ 1 + + Kopier kodesnutten og kjør den på maskinen din for å generere + design tokens (json-filer), eller lim den inn i Designsystemet + sin{' '} - Slack + Figma plugin {' '} - eller lag et{' '} + i{' '} - Github issue - - . + Core UI Kit + {' '} + for å oppdatere et tema direkte i Figma. Les mer om disse + alternativene på{' '} + + eget tema + {' '} + siden.
+
+ {cliSnippet} +
+
+ 2 + + Kjør kodesnutten for å generere CSS variabler til kode. + +
+
+ + npx @digdir/designsystemet@next tokens build + +
+ + + +
+
+
+
+ Noe som ikke fungerer? + + Send oss en melding på{' '} + + Slack + {' '} + eller lag et{' '} + + Github issue + + . + +
+
-
+ ); diff --git a/apps/theme/components/index.ts b/apps/theme/components/index.ts index 14e1084fc4..e183254caa 100644 --- a/apps/theme/components/index.ts +++ b/apps/theme/components/index.ts @@ -3,6 +3,15 @@ export { ColorPicker } from './ColorPicker/ColorPicker'; export { Group } from './Group/Group'; export { Previews } from './Previews/Previews'; export { Scale } from './Scale/Scale'; -export { Scales } from './Scales/Scales'; -export { ThemeToolbar } from './ThemeToolbar/ThemeToolbar'; +export { Colors } from './Colors/Colors'; export { TokenModal } from './TokenModal/TokenModal'; +export { Sidebar } from './Sidebar/Sidebar'; +export { ColorInput } from './ColorInput/ColorInput'; +export { ColorPreview } from './ColorPreview/ColorPreview'; +export { ColorPane } from './Sidebar/ColorPane/ColorPane'; +export { ColorTokens } from './ColorTokens/ColorTokens'; +export { ColorContrasts } from './ColorContrasts/ColorContrasts'; +export { BorderRadius } from './BorderRadius/BorderRadius'; +export { Theme1 } from './Previews/Theme1/Theme1'; +export { BorderRadiusInput } from './BorderRadiusInput/BorderRadiusInput'; +export { ThemeToggle } from './ThemeToggle/ThemeToggle'; diff --git a/apps/theme/package.json b/apps/theme/package.json index e381914752..c7f60fa550 100644 --- a/apps/theme/package.json +++ b/apps/theme/package.json @@ -22,6 +22,7 @@ "next": "^14.2.5", "react": "^18.3.1", "react-color": "^2.19.3", + "react-color-palette": "^7.3.0", "react-dom": "^18.3.1", "recharts": "^2.12.7", "zustand": "^4.5.4" diff --git a/apps/theme/public/img/avatars/female2.png b/apps/theme/public/img/avatars/female2.png new file mode 100644 index 0000000000..d6b57ab6a8 Binary files /dev/null and b/apps/theme/public/img/avatars/female2.png differ diff --git a/apps/theme/public/img/avatars/male3.png b/apps/theme/public/img/avatars/male3.png new file mode 100644 index 0000000000..c3ae6cc06e Binary files /dev/null and b/apps/theme/public/img/avatars/male3.png differ diff --git a/apps/theme/public/img/city.png b/apps/theme/public/img/city.png new file mode 100644 index 0000000000..08468513b7 Binary files /dev/null and b/apps/theme/public/img/city.png differ diff --git a/apps/theme/public/img/color-tokens.png b/apps/theme/public/img/color-tokens.png new file mode 100644 index 0000000000..8e23a04ee0 Binary files /dev/null and b/apps/theme/public/img/color-tokens.png differ diff --git a/apps/theme/store.ts b/apps/theme/store.ts index b5dbbbab47..ee4dbca325 100644 --- a/apps/theme/store.ts +++ b/apps/theme/store.ts @@ -1,82 +1,101 @@ -import type { CssColor } from '@adobe/leonardo-contrast-colors'; -import type { ColorInfo, ThemeInfo } from '@digdir/designsystemet/color'; +import { + type ColorInfo, + type ColorMode, + type ThemeInfo, + generateThemeForColor, +} from '@digdir/designsystemet/color'; import { create } from 'zustand'; import { subscribeWithSelector } from 'zustand/middleware'; -import { Settings } from './settings'; -import type { ThemeColors } from './types'; - -type StoreThemeType = { - theme: ThemeInfo; - color: CssColor; +export type ColorTheme = { + name: string; + colors: ThemeInfo; }; -type ColorStore = { - accentTheme: StoreThemeType; - neutralTheme: StoreThemeType; - brandOneTheme: StoreThemeType; - brandTwoTheme: StoreThemeType; - brandThreeTheme: StoreThemeType; - setAccentTheme: (theme: ThemeInfo, color: CssColor) => void; - setNeutralTheme: (theme: ThemeInfo, color: CssColor) => void; - setBrandOneTheme: (theme: ThemeInfo, color: CssColor) => void; - setBrandTwoTheme: (theme: ThemeInfo, color: CssColor) => void; - setBrandThreeTheme: (theme: ThemeInfo, color: CssColor) => void; - selectedColor: { color: ColorInfo; type: ThemeColors }; - setSelectedColor: (color: ColorInfo, type: ThemeColors) => void; - borderRadius: string; - setBorderRadius: (radius: string) => void; -}; +type BorderRadiusGroup = 'none' | 'small' | 'medium' | 'large' | 'full'; +type PageType = 'intro' | 'colors' | 'radius' | 'finish'; -const defaultTheme = () => { - return { - light: [], - dark: [], - contrast: [], +type ColorStore = { + activePage: PageType; + setActivePage: (page: PageType) => void; + colors: { + main: ColorTheme[]; + neutral: ColorTheme[]; + support: ColorTheme[]; }; + themeName: string; + setThemeName: (name: string) => void; + addColor: ( + newColor: ColorTheme, + type: 'main' | 'neutral' | 'support', + ) => void; + updateColor: ( + updatedColor: ColorTheme, + index: number, + type: 'main' | 'neutral' | 'support', + ) => void; + resetColors: () => void; + removeColor: (index: number, type: 'main' | 'neutral' | 'support') => void; + selectedColor: { color: ColorInfo; name: string }; + setSelectedColor: (color: ColorInfo, name: string) => void; + borderRadius: BorderRadiusGroup; + setBorderRadius: (radius: BorderRadiusGroup) => void; + appearance: ColorMode; + setAppearance: (appearance: ColorMode) => void; + themePreview: 'one' | 'two' | 'three'; + setThemePreview: (theme: 'one' | 'two' | 'three') => void; }; export const useThemeStore = create( subscribeWithSelector((set) => ({ - accentTheme: { - theme: defaultTheme(), - color: Settings.accentBaseColor, - }, - neutralTheme: { - theme: defaultTheme(), - color: Settings.neutralBaseColor, - }, - brandOneTheme: { - theme: defaultTheme(), - color: Settings.brand1BaseColor, - }, - brandTwoTheme: { - theme: defaultTheme(), - color: Settings.brand2BaseColor, - }, - brandThreeTheme: { - theme: defaultTheme(), - color: Settings.brand3BaseColor, - }, + activePage: 'colors', + setActivePage: (page) => set({ activePage: page }), selectedColor: { color: { hex: '#ffffff', number: 1, name: 'Default', }, - type: 'accent', + name: 'Default', + }, + borderRadius: 'small', + appearance: 'light', + themePreview: 'one', + colors: { + main: [{ name: 'accent', colors: generateThemeForColor('#0062BA') }], + neutral: [{ name: 'neutral', colors: generateThemeForColor('#1E2B3C') }], + support: [ + { name: 'brand1', colors: generateThemeForColor('#F45F63') }, + { name: 'brand2', colors: generateThemeForColor('#E5AA20') }, + { name: 'brand3', colors: generateThemeForColor('#1E98F5') }, + ], + }, + themeName: 'theme', + setThemeName: (name) => set({ themeName: name }), + addColor: (newColor, type) => + set((state) => { + const updatedColors = state.colors[type].concat(newColor); + return { colors: { ...state.colors, [type]: updatedColors } }; + }), + updateColor: (updatedColor, index, type) => + set((state) => { + const updatedColors = state.colors[type].map((color, i) => + i === index ? updatedColor : color, + ); + return { colors: { ...state.colors, [type]: updatedColors } }; + }), + resetColors: () => { + set({ colors: { main: [], neutral: [], support: [] } }); }, - borderRadius: '0.25rem', + removeColor: (index, type) => + set((state) => { + const updatedColors = state.colors[type].filter((_, i) => i !== index); + return { colors: { ...state.colors, [type]: updatedColors } }; + }), + setAppearance: (appearance) => set({ appearance: appearance }), + setThemePreview: (themePreview) => set({ themePreview: themePreview }), setBorderRadius: (radius) => set({ borderRadius: radius }), - setSelectedColor: (color, type) => - set({ selectedColor: { color: color, type: type } }), - setAccentTheme: (theme, color) => set({ accentTheme: { theme, color } }), - setNeutralTheme: (theme, color) => set({ neutralTheme: { theme, color } }), - setBrandOneTheme: (theme, color) => - set({ brandOneTheme: { theme, color } }), - setBrandTwoTheme: (theme, color) => - set({ brandTwoTheme: { theme, color } }), - setBrandThreeTheme: (theme, color) => - set({ brandThreeTheme: { theme, color } }), + setSelectedColor: (color, name) => + set({ selectedColor: { color: color, name: name } }), })), ); diff --git a/apps/theme/tokenMapping.ts b/apps/theme/tokenMapping.ts new file mode 100644 index 0000000000..39fd21b3d7 --- /dev/null +++ b/apps/theme/tokenMapping.ts @@ -0,0 +1,46 @@ +export const mapTokens = () => { + // ds-color + setToken( + '--ds-color-background-default', + 'var(--ds-color-accent-background-default)', + ); + setToken( + '--ds-color-background-subtle', + 'var(--ds-color-accent-background-subtle)', + ); + setToken( + '--ds-color-surface-default', + 'var(--ds-color-accent-surface-default)', + ); + setToken('--ds-color-surface-hover', 'var(--ds-color-accent-surface-hover)'); + setToken( + '--ds-color-surface-active', + 'var(--ds-color-accent-surface-active)', + ); + setToken('--ds-color-border-subtle', 'var(--ds-color-accent-border-subtle)'); + setToken( + '--ds-color-border-default', + 'var(--ds-color-accent-border-default)', + ); + setToken('--ds-color-border-strong', 'var(--ds-color-accent-border-strong)'); + setToken('--ds-color-base-default', 'var(--ds-color-accent-base-default)'); + setToken('--ds-color-base-hover', 'var(--ds-color-accent-base-hover)'); + setToken('--ds-color-base-active', 'var(--ds-color-accent-base-active)'); + setToken('--ds-color-text-subtle', 'var(--ds-color-accent-text-subtle)'); + setToken('--ds-color-text-default', 'var(--ds-color-accent-text-default)'); + setToken( + '--ds-color-contrast-default', + 'var(--ds-color-accent-contrast-default)', + ); + setToken( + '--ds-color-contrast-subtle', + 'var(--ds-color-accent-contrast-subtle)', + ); +}; + +const setToken = (token: string, color: string) => { + const previewElement = document.getElementById('preview'); + if (previewElement) { + previewElement.style.setProperty(token, color); + } +}; diff --git a/yarn.lock b/yarn.lock index 52f9ecf490..56dd71e09a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14906,6 +14906,15 @@ __metadata: languageName: node linkType: hard +"react-color-palette@npm:^7.3.0": + version: 7.3.0 + resolution: "react-color-palette@npm:7.3.0" + peerDependencies: + react: ">=16.8" + checksum: 10/49161f76c8c8658b7fa20022c81757cafb816b553d6e43affb4cd67ef4df2025c505ab4a55fd5598a11c223d00327fc1fef2f4bdf221e40864a49d2b75938d7b + languageName: node + linkType: hard + "react-color@npm:^2.19.3": version: 2.19.3 resolution: "react-color@npm:2.19.3" @@ -16940,6 +16949,7 @@ __metadata: next: "npm:^14.2.5" react: "npm:^18.3.1" react-color: "npm:^2.19.3" + react-color-palette: "npm:^7.3.0" react-dom: "npm:^18.3.1" recharts: "npm:^2.12.7" zustand: "npm:^4.5.4"