Compare commits

..
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] 3b31bd3d75 Version Packages (#2066)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-21 11:59:33 +02:00
Halvor HauganandKen b1625ba388 ✨ Chat: ny small-variant, justert design, forbedret UU (#2048)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2023-06-21 09:40:50 +02:00
Ken 269d06c277 📝 ErrorSummary prop-dokumentasjon 2023-06-20 15:47:52 +02:00
Ken 0c56273f3b 🐛 Fikset rekkefølge på button-stories 2023-06-20 15:41:13 +02:00
Ken ab6a96aed7 ♻️ Oppdatert komponent demoer (#2063) 2023-06-20 11:46:45 +02:00
Ken 9ee8a72082 🔥 Fjernet død kode, lazy-loader sanity-moduler bare brukt for preview 2023-06-19 19:53:50 +02:00
Ken c2e5a92d8e 🎉 Oppdatert søk (#2062) 2023-06-19 18:28:24 +02:00
Ken 6059aa59aa 🐛 Oppdatert playwright-config for mindre flaky tester 2023-06-19 16:21:13 +02:00
Ken b877ada408 🐛 Fikset playwright-tester 2023-06-19 15:11:59 +02:00
Ken 4e339f183e 🔊 Oppdatert amplitude (#2061) 2023-06-19 14:09:08 +02:00
Ken 1d62bf26ff 🛠️ Diverse småjustering for Aksel (#2060) 2023-06-19 13:19:57 +02:00
135 changed files with 2270 additions and 2109 deletions

No files matched your search

+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
playwright:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.33.0-focal
image: mcr.microsoft.com/playwright:v1.35.1-jammy
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
name: Smoketest Aksel
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.33.0-focal
image: mcr.microsoft.com/playwright:v1.35.1-jammy
steps:
- uses: actions/setup-node@v3
with:
+2 -1
View File
@@ -150,4 +150,5 @@ playwright-report/
/test-results/
backup-*.tar.gz
sitemap-urls.json
*.tar.gz
*.tar.gz
searchindex.json
+2
View File
@@ -1,5 +1,7 @@
# @navikt/aksel-icons
## 4.2.0
## 4.1.7
## 4.1.6
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "4.1.7",
"version": "4.2.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
+12
View File
@@ -1,5 +1,17 @@
# @navikt/aksel-stylelint
## 4.2.0
### Minor Changes
- [#2048](https://github.com/navikt/aksel/pull/2048) [`b1625ba38`](https://github.com/navikt/aksel/commit/b1625ba3882b430d678d0623922d7fb56aa724b3) Thanks [@HalvorHaugan](https://github.com/HalvorHaugan)! - Oppdatert Chat
- :sparkles: Ny prop `size` som kan settes til "small" for en mer kompakt layout
- :sparkles: Ny prop `variant` som erstatter `backgroundColor` og `avatarBgColor`
- :sparkles: `avatar` er nå valgfritt
- :wheelchair: Forbedret UU
- :lipstick: Oppdatert utseende
## 4.1.7
## 4.1.6
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "4.1.7",
"version": "4.2.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
"repository": {
@@ -35,8 +35,8 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^4.1.7",
"@navikt/ds-tokens": "^4.1.7",
"@navikt/ds-css": "^4.2.0",
"@navikt/ds-tokens": "^4.2.0",
"@types/jest": "^29.0.0",
"concurrently": "7.2.1",
"copyfiles": "2.4.1",
+7
View File
@@ -1,5 +1,12 @@
# @navikt/aksel
## 4.2.0
### Patch Changes
- Updated dependencies [[`b1625ba38`](https://github.com/navikt/aksel/commit/b1625ba3882b430d678d0623922d7fb56aa724b3)]:
- @navikt/ds-css@4.2.0
## 4.1.7
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "4.1.7",
"version": "4.2.0",
"description": "Aksel command line interface. Handles css-imports, codemods and more",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
"dependencies": {
"@navikt/ds-css": "4.1.7",
"@navikt/ds-css": "4.2.0",
"axios": "1.3.6",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
+12
View File
@@ -1,5 +1,17 @@
# @navikt/ds-css
## 4.2.0
### Minor Changes
- [#2048](https://github.com/navikt/aksel/pull/2048) [`b1625ba38`](https://github.com/navikt/aksel/commit/b1625ba3882b430d678d0623922d7fb56aa724b3) Thanks [@HalvorHaugan](https://github.com/HalvorHaugan)! - Oppdatert Chat
- :sparkles: Ny prop `size` som kan settes til "small" for en mer kompakt layout
- :sparkles: Ny prop `variant` som erstatter `backgroundColor` og `avatarBgColor`
- :sparkles: `avatar` er nå valgfritt
- :wheelchair: Forbedret UU
- :lipstick: Oppdatert utseende
## 4.1.7
### Patch Changes
+30 -16
View File
@@ -1,14 +1,12 @@
.navds-chat {
display: flex;
align-items: flex-end;
gap: var(--a-spacing-4);
padding-right: var(--a-spacing-16);
gap: var(--a-spacing-3);
max-width: 40.75rem;
}
.navds-chat--right {
flex-direction: row-reverse;
padding-right: 0;
padding-left: var(--a-spacing-16);
}
.navds-chat__bubble-wrapper {
@@ -26,15 +24,19 @@
.navds-chat__avatar {
align-items: center;
background: var(--ac-chat-avatar-bg, var(--a-bg-subtle));
color: var(--ac-chat-avatar-color, var(--a-text-default));
box-shadow: var(--a-shadow-xsmall);
background-color: var(--ac-chat-avatar-bg, var(--a-surface-neutral-subtle));
border: 1px solid var(--ac-chat-border-color, var(--a-border-subtle));
border-radius: var(--a-border-radius-full);
color: var(--ac-chat-avatar-color, var(--a-text-default));
display: flex;
flex-shrink: 0;
justify-content: center;
overflow: hidden;
height: 3rem;
width: 3rem;
height: 2.5rem;
width: 2.5rem;
font-size: 1.06rem;
letter-spacing: 0.024rem;
}
.navds-chat__avatar svg {
@@ -44,11 +46,11 @@
}
.navds-chat__bubble {
padding: 1rem;
box-shadow: var(--a-shadow-small);
padding: var(--a-spacing-4);
box-shadow: var(--a-shadow-xsmall);
width: fit-content;
max-width: 37.5rem;
background-color: var(--ac-chat-bubble-bg, var(--a-bg-subtle));
background-color: var(--ac-chat-bubble-bg, var(--a-surface-neutral-subtle));
border: 1px solid var(--ac-chat-border-color, var(--a-border-subtle));
border-radius: var(--a-border-radius-xlarge);
border-bottom-left-radius: 2px;
display: flex;
@@ -61,11 +63,27 @@
border-bottom-right-radius: 2px;
}
.navds-chat--small .navds-chat__bubble {
padding: var(--a-spacing-3);
}
.navds-chat--info .navds-chat__bubble,
.navds-chat--info .navds-chat__avatar {
background-color: var(--a-surface-info-subtle);
}
.navds-chat--neutral .navds-chat__bubble,
.navds-chat--neutral .navds-chat__avatar {
background-color: var(--a-surface-default);
}
.navds-chat__top-text {
color: var(--ac-chat-top-text, var(--a-text-default));
display: flex;
gap: var(--a-spacing-2);
align-items: baseline;
font-weight: normal;
margin: 0;
}
.navds-chat--right .navds-chat__top-text {
@@ -87,7 +105,3 @@
.navds-chat--right .navds-chat__top-text--left {
align-self: flex-start;
}
.navds-chat__name {
font-weight: var(--a-font-weight-bold);
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "4.1.7",
"version": "4.2.0",
"description": "CSS for NAV Designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -27,7 +27,7 @@
"css:get-version": "node config/get-version.js"
},
"devDependencies": {
"@navikt/ds-tokens": "^4.1.7",
"@navikt/ds-tokens": "^4.2.0",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
+3 -2
View File
@@ -80,9 +80,10 @@
"--ac-button-tertiary-neutral-active-hover-bg": "--a-surface-neutral-active"
},
"chat": {
"--ac-chat-avatar-bg": "--a-bg-subtle",
"--ac-chat-border-color": "--a-border-subtle",
"--ac-chat-avatar-bg": "--a-surface-neutral-subtle",
"--ac-chat-avatar-color": "--a-text-default",
"--ac-chat-bubble-bg": "--a-bg-subtle",
"--ac-chat-bubble-bg": "--a-surface-neutral-subtle",
"--ac-chat-top-text": "--a-text-default"
},
"chips": {
+17
View File
@@ -1,5 +1,22 @@
# @navikt/ds-react
## 4.2.0
### Minor Changes
- [#2048](https://github.com/navikt/aksel/pull/2048) [`b1625ba38`](https://github.com/navikt/aksel/commit/b1625ba3882b430d678d0623922d7fb56aa724b3) Thanks [@HalvorHaugan](https://github.com/HalvorHaugan)! - Oppdatert Chat
- :sparkles: Ny prop `size` som kan settes til "small" for en mer kompakt layout
- :sparkles: Ny prop `variant` som erstatter `backgroundColor` og `avatarBgColor`
- :sparkles: `avatar` er nå valgfritt
- :wheelchair: Forbedret UU
- :lipstick: Oppdatert utseende
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.2.0
## 4.1.7
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "4.1.7",
"version": "4.2.0",
"description": "Aksel react-components for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -38,7 +38,7 @@
},
"dependencies": {
"@floating-ui/react": "0.24.1",
"@navikt/aksel-icons": "^4.1.7",
"@navikt/aksel-icons": "^4.2.0",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"clsx": "^1.2.1",
+9 -5
View File
@@ -17,6 +17,7 @@ export interface BubbleProps extends HTMLAttributes<HTMLDivElement> {
timestamp?: string;
/**
* Background color on bubble
* @deprecated Use `variant` on Chat instead
*/
backgroundColor?: string;
/**
@@ -46,17 +47,20 @@ const Bubble = forwardRef<HTMLDivElement, BubbleProps>(
{...rest}
>
{(timestamp || name) && (
<div
<h3
className={cl(
`navds-chat__top-text`,
toptextPosition && `navds-chat__top-text--${toptextPosition}`
)}
>
{name && <Detail className="navds-chat__name">{name}</Detail>}
{timestamp && (
<Detail className="navds-chat__timestamp">{timestamp}</Detail>
{name && <Detail as="span">{name}</Detail>}
{name && timestamp && (
<Detail as="span" aria-hidden>
&bull;
</Detail>
)}
</div>
{timestamp && <Detail as="span">{timestamp}</Detail>}
</h3>
)}
{children}
</div>
+70 -52
View File
@@ -1,7 +1,10 @@
import React, { forwardRef, HTMLAttributes } from "react";
import cl from "clsx";
import Bubble, { BubbleProps } from "./Bubble";
import { BodyLong, BodyShort } from "../typography";
import Bubble from "./Bubble";
import { BodyLong } from "../typography";
export const POSITIONS = ["left", "right"] as const;
export const SIZES = ["medium", "small"] as const;
export interface ChatProps extends HTMLAttributes<HTMLDivElement> {
/**
@@ -17,27 +20,40 @@ export interface ChatProps extends HTMLAttributes<HTMLDivElement> {
*/
timestamp?: string;
/**
* Avatar for messenger. Regular text for initials works to
* Avatar for messenger. Regular text for initials works too, but it will be hidden for screen readers.
*/
avatar: React.ReactNode;
avatar?: React.ReactNode;
/**
* Changes background color on avatar and bubbles.
* Avoid using the same background as the surface behind Chat.
* @default "subtle"
*/
variant?: "subtle" | "info" | "neutral";
/**
* Background color on bubbles
* @deprecated Use `variant` instead
*/
backgroundColor?: string;
/**
* Background color for avatar
* @deprecated Use `variant` instead
*/
avatarBgColor?: string;
/**
* Positions avatar and Bubbles
* Positions avatar and bubbles
* @default "left"
*/
position?: "left" | "right";
position?: (typeof POSITIONS)[number];
/**
* Hoizontal position of toptext
* @default Same as chat
* @default Same as position
*/
toptextPosition?: "left" | "right";
toptextPosition?: (typeof POSITIONS)[number];
/**
* Affects padding and font size in bubbles
* @default "medium"
*/
size?: (typeof SIZES)[number];
}
interface ChatComponent
@@ -47,24 +63,23 @@ interface ChatComponent
/**
* @see 🏷️ {@link BubbleProps}
*/
Bubble: React.ForwardRefExoticComponent<
BubbleProps & React.RefAttributes<HTMLDivElement>
>;
Bubble: typeof Bubble;
}
/**
* A component for displaying chat messages.
* A component for communicating dialogs between two parties.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/chat)
* @see 🏷️ {@link ChatProps}
*
* @example
* ```jsx
* <Chat>
* <Chat.Bubble avatar="A" name="Alice">Hello!</Chat.Bubble>
* <Chat avatar="A" name="Alice" timestamp="01.01.21 14:00">
* <Chat.Bubble>Hello!</Chat.Bubble>
* <Chat.Bubble>How can I help you?</Chat.Bubble>
* </Chat>
* <Chat>
* <Chat.Bubble avatar="B" name="Bob">Hi there!</Chat.Bubble>
* <Chat avatar="B" name="Bob" timestamp="01.01.21 14:01" position="right">
* <Chat.Bubble>Hi there!</Chat.Bubble>
* </Chat>
* ```
*/
@@ -77,51 +92,54 @@ export const Chat = forwardRef<HTMLDivElement, ChatProps>(
timestamp,
avatar,
position = "left",
variant = "subtle",
avatarBgColor,
backgroundColor,
toptextPosition,
size = "medium",
...rest
},
}: ChatProps,
ref
) => {
return (
<div
ref={ref}
className={cl(
"navds-chat",
className,
`navds-chat--${position} navds-chat--top-text-${
toptextPosition ?? position
}`
)}
{...rest}
>
<BodyShort
as="div"
) => (
<div
ref={ref}
className={cl(
"navds-chat",
className,
`navds-chat--${position}`,
`navds-chat--top-text-${toptextPosition ?? position}`,
`navds-chat--${size}`,
`navds-chat--${variant}`
)}
{...rest}
>
{avatar && (
<div
className="navds-chat__avatar"
aria-hidden
style={{ backgroundColor: avatarBgColor }}
>
{avatar}
</BodyShort>
<ol className="navds-chat__bubble-wrapper">
{React.Children.map(children, (child, i) => {
if (React.isValidElement(child)) {
return (
<BodyLong as="li">
{React.cloneElement(child, {
name: name && i === 0 ? name : undefined,
timestamp: timestamp && i === 0 ? timestamp : undefined,
backgroundColor,
...child.props,
})}
</BodyLong>
);
}
})}
</ol>
</div>
);
}
</div>
)}
<ol className="navds-chat__bubble-wrapper">
{React.Children.map(children, (child, i) => {
if (React.isValidElement(child)) {
return (
<BodyLong as="li" size={size}>
{React.cloneElement(child, {
name: name && i === 0 ? name : undefined,
timestamp: timestamp && i === 0 ? timestamp : undefined,
backgroundColor,
...child.props,
})}
</BodyLong>
);
}
})}
</ol>
</div>
)
) as ChatComponent;
Chat.Bubble = Bubble;
+150 -128
View File
@@ -1,6 +1,7 @@
import React from "react";
import { Chat } from "../index";
import { Meta } from "@storybook/react";
import { POSITIONS, SIZES } from "./Chat";
export default {
title: "ds-react/Chat",
@@ -16,19 +17,25 @@ export default {
type: "text",
},
},
position: {
control: { type: "radio" },
options: POSITIONS,
},
toptextPosition: {
control: { type: "radio" },
options: POSITIONS,
},
size: {
control: { type: "radio" },
options: SIZES,
},
},
} as Meta;
} satisfies Meta<typeof Chat>;
export const Default = {
render: (props) => {
return (
<Chat
avatar={props?.avatar ?? "ON"}
name={props?.name ?? "Ola Normann"}
timestamp={props?.timestamp ?? "01.01.21 14:00"}
avatarBgColor={props?.avatarBgColor}
backgroundColor={props?.backgroundColor}
>
<Chat {...props}>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
@@ -48,141 +55,156 @@ export const Default = {
},
};
export const Farger = () => {
return (
export const Small = () => (
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 14:00" size="small">
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
);
export const Colors = () => (
<Chat
avatar="NAV"
name="NAV"
timestamp="01.01.21 14:00"
avatarBgColor="var(--a-surface-alt-1-subtle)"
backgroundColor="var(--a-surface-warning-subtle)"
>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
<Chat.Bubble backgroundColor="lightYellow">
Tempor fugiat amet eu sint in in ullamco.
</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
);
export const Variants = () => (
<div className="colgap">
<Chat avatar="NAV" name="NAV" timestamp="01.01.21 14:00" variant="info">
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
</Chat>
<Chat
avatar="KN"
name="Kari Nordmann"
timestamp="01.01.21 14:03"
variant="neutral"
position="right"
>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
</Chat>
<Chat
avatar="ON"
name="Ola Nordmann"
timestamp="01.01.21 14:07"
variant="subtle"
position="right"
>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
</div>
);
export const Position = () => (
<div className="colgap">
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 14:00">
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
<Chat
avatar="KH"
name="Kari Høyli"
timestamp="01.01.21 14:32"
position="right"
variant="info"
>
<Chat.Bubble>
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble>
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit est
dolore incididunt. Occaecat cupidatat magna.
</Chat.Bubble>
</Chat>
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 15:00">
<Chat.Bubble>
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo ea
esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
labore sunt aute.
</Chat.Bubble>
</Chat>
</div>
);
export const ToptextPosition = () => (
<div className="colgap">
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 14:00"
avatarBgColor="var(--a-lightblue-200)"
backgroundColor="var(--a-lightblue-100)"
toptextPosition="right"
>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
);
};
export const Position = () => {
return (
<div className="colgap">
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 14:00"
backgroundColor="rgba(255, 249, 240, 1)"
avatarBgColor="rgba(255, 236, 204, 1)"
>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint
laboris labore nisi ut.
</Chat.Bubble>
</Chat>
<Chat
avatar="KH"
name="Kari Høyli"
timestamp="01.01.21 14:32"
position="right"
backgroundColor="rgba(230, 240, 255, 1)"
avatarBgColor="rgba(204, 225, 255, 1)"
>
<Chat.Bubble>
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble>
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit
est dolore incididunt. Occaecat cupidatat magna.
</Chat.Bubble>
</Chat>
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 15:00"
backgroundColor="rgba(255, 249, 240, 1)"
avatarBgColor="rgba(255, 236, 204, 1)"
>
<Chat.Bubble>
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo
ea esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
labore sunt aute.
</Chat.Bubble>
</Chat>
</div>
);
};
export const ToptextPosition = () => {
return (
<div className="colgap">
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 14:00"
backgroundColor="rgba(255, 249, 240, 1)"
avatarBgColor="rgba(255, 236, 204, 1)"
toptextPosition="right"
>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint
laboris labore nisi ut.
</Chat.Bubble>
</Chat>
<Chat
avatar="KH"
name="Kari Høyli"
timestamp="01.01.21 14:32"
position="right"
backgroundColor="rgba(230, 240, 255, 1)"
avatarBgColor="rgba(204, 225, 255, 1)"
toptextPosition="left"
>
<Chat.Bubble>
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble>
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit
est dolore incididunt. Occaecat cupidatat magna.
</Chat.Bubble>
</Chat>
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 15:00"
backgroundColor="rgba(255, 249, 240, 1)"
avatarBgColor="rgba(255, 236, 204, 1)"
>
<Chat.Bubble toptextPosition="right">
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo
ea esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
labore sunt aute.
</Chat.Bubble>
</Chat>
</div>
);
};
export const Avatar = () => {
return (
<Chat avatar={<Illustration />} timestamp="01.01.21 14:00">
<Chat
name="Kari Høyli"
timestamp="01.01.21 14:32"
position="right"
variant="info"
toptextPosition="left"
>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit est
dolore incididunt. Occaecat cupidatat magna.
</Chat.Bubble>
</Chat>
);
};
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 15:00">
<Chat.Bubble toptextPosition="right">
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo ea
esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
labore sunt aute.
</Chat.Bubble>
</Chat>
</div>
);
export const Avatar = () => (
<Chat avatar={<Illustration />} timestamp="01.01.21 14:00">
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
);
const Illustration = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 93">
@@ -29,6 +29,18 @@ interface ErrorSummaryComponent
extends React.ForwardRefExoticComponent<
ErrorSummaryProps & React.RefAttributes<HTMLDivElement>
> {
/**
* Link to error
*
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
*
* @example
* ```jsx
* <ErrorSummary.Item href="#1">
* Felt må fylles ut med alder
* </ErrorSummary.Item>
* ```
*/
Item: ErrorSummaryItemType;
}
@@ -8,6 +8,10 @@ export interface ErrorSummaryItemProps
* Link text
*/
children: React.ReactNode;
/**
* Link to errormessage
*/
href?: string;
}
export type ErrorSummaryItemType = OverridableComponent<
+2
View File
@@ -1,5 +1,7 @@
# @navikt/ds-tailwind
## 4.2.0
## 4.1.7
## 4.1.6
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "4.1.7",
"version": "4.2.0",
"description": "Tailwind config based on @navikt/ds-tokens",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -22,7 +22,7 @@
"directory": "@navikt/core/tailwind"
},
"devDependencies": {
"@navikt/ds-tokens": "^4.1.7",
"@navikt/ds-tokens": "^4.2.0",
"@types/jest": "^29.0.0",
"color": "4.2.3",
"jest": "^29.0.0",
+2
View File
@@ -1,5 +1,7 @@
# @navikt/ds-tokens
## 4.2.0
## 4.1.7
## 4.1.6
+8 -8
View File
@@ -238,15 +238,15 @@
"name": "--a-text-action-on-action-subtle",
"value": "rgba(0, 86, 180, 1)"
},
{ "name": "--a-text-on-warning", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-info", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-alt-2", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-inverted", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-neutral", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-action", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-success", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-danger", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-warning", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-info", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-alt-1", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-alt-2", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-alt-3", "value": "rgba(255, 255, 255, 1)" }
],
"semantic-surface-default": [
@@ -317,15 +317,15 @@
],
"semantic-surface-warning": [
{ "name": "--a-surface-warning-subtle", "value": "rgba(255, 236, 204, 1)" },
{
"name": "--a-surface-warning-subtle-hover",
"value": "rgba(255, 215, 153, 1)"
},
{
"name": "--a-surface-warning-moderate",
"value": "rgba(255, 215, 153, 1)"
},
{ "name": "--a-surface-warning", "value": "rgba(255, 145, 0, 1)" },
{
"name": "--a-surface-warning-subtle-hover",
"value": "rgba(255, 215, 153, 1)"
}
{ "name": "--a-surface-warning", "value": "rgba(255, 145, 0, 1)" }
],
"semantic-surface-info": [
{ "name": "--a-surface-info-subtle", "value": "rgba(216, 249, 255, 1)" },
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "4.1.7",
"version": "4.2.0",
"description": "Design-tokens for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
-8
View File
@@ -17,14 +17,6 @@ spec:
path: /api/isAlive
initialDelay: 10
resources:
limits:
cpu: 2000m
memory: 2048Mi
requests:
cpu: 500m
memory: 1024Mi
accessPolicy:
outbound:
external:
+1 -8
View File
@@ -21,13 +21,6 @@ spec:
path: /api/isAlive
initialDelay: 20
timeout: 1
resources:
limits:
cpu: 2000m
memory: 2048Mi
requests:
cpu: 500m
memory: 1024Mi
accessPolicy:
outbound:
external:
@@ -49,4 +42,4 @@ spec:
filesFrom:
- emptyDir:
medium: Memory
mountPath: /app/aksel.nav.no/website/.next/cache
mountPath: /app/aksel.nav.no/website/.next/cache
+5 -5
View File
@@ -15,11 +15,11 @@
"website"
],
"dependencies": {
"@navikt/aksel-icons": "^4.1.7",
"@navikt/ds-css": "^4.1.7",
"@navikt/ds-react": "^4.1.7",
"@navikt/ds-tailwind": "^4.1.7",
"@navikt/ds-tokens": "^4.1.7"
"@navikt/aksel-icons": "^4.2.0",
"@navikt/ds-css": "^4.2.0",
"@navikt/ds-react": "^4.2.0",
"@navikt/ds-tailwind": "^4.2.0",
"@navikt/ds-tokens": "^4.2.0"
},
"devDependencies": {
"prettier-plugin-tailwindcss": "^0.2.3"
@@ -101,6 +101,7 @@ const serializers = {
spacing
level="4"
size="small"
id={`h${node._key}`}
{...textProps}
/>
);
@@ -1,13 +0,0 @@
import "../dist/tw.css";
import { Provider } from "@navikt/ds-react";
import { IdContext } from "components/website-modules/utils/contexts";
export const NextDecorator = ({ children }: { children: React.ReactNode }) => (
<Provider>
<IdContext.Provider value={{ id: "storybook-testid" }}>
<div id="__next" className="aksel antialiased">
{children}
</div>
</IdContext.Provider>
</Provider>
);
+1 -3
View File
@@ -13,15 +13,13 @@ export { default as PropTable } from "./sanity-modules/props/PropTabell";
export { default as SideModul } from "./sanity-modules/SideModul";
export { default as Tabell } from "./sanity-modules/Tabell";
export { default as TastaturModul } from "./sanity-modules/TastaturTabell";
export { default as Tips } from "./sanity-modules/tips";
export { default as Tips } from "./sanity-modules/Tips";
export { default as TokenTable } from "./sanity-modules/TokenTable";
export * from "./sanity-modules/UuFeedback";
export { default as Video } from "./sanity-modules/VideoEmbed";
export * from "./website-modules/BreadCrumbs";
export { default as EditButton } from "./website-modules/EditButton";
export { default as Feedback } from "./website-modules/feedback";
export * from "./website-modules/H2";
export { default as PreviewBanner } from "./website-modules/PreviewBanner";
export * from "./website-modules/TilToppen";
export * from "./website-modules/TOC";
export * from "./website-modules/utils";
@@ -123,10 +123,10 @@ export const WithSidebar = ({
</Heading>
<div
className={cl({
" w-fit": variant !== "page",
"w-fit": variant !== "page",
})}
>
{intro && intro}
{intro}
</div>
</div>
{variant === "page" && pageProps.status?.bilde && (
@@ -160,10 +160,7 @@ export const WithSidebar = ({
</div>
<div className={cl("sm:px-6 md:px-10", { flex: withToc })}>
{withToc && (
<TableOfContentsv2
changedState={pageProps["content"]}
hideToc={false}
/>
<TableOfContentsv2 changedState={pageProps["content"]} />
)}
<div className="w-full">
{children}
@@ -2,8 +2,15 @@ import { BodyShort, Heading, Link } from "@navikt/ds-react";
import { FigmaIcon, GithubIcon, SlackIcon } from "components/assets";
import Logo from "components/assets/Logo";
import { logNav } from "components/website-modules/utils/amplitude";
import { EditButton } from "../..";
import FooterForm from "./FooterForm";
import dynamic from "next/dynamic";
export const EditButton = dynamic(
() => import("components/website-modules/EditButton"),
{
ssr: false,
}
);
const Footer = () => {
return (
@@ -1,10 +1,33 @@
import Logo from "components/assets/Logo";
import Link from "next/link";
import { logNav } from "../..";
import cl from "clsx";
import { useRouter } from "next/router";
import Logo from "components/assets/Logo";
import { Hamburger } from "components/layout/header/Hamburger";
import { GlobalSearch } from "./search";
import Link from "next/link";
import { useRouter } from "next/router";
import { logNav } from "../..";
import { Button } from "@navikt/ds-react";
import dynamic from "next/dynamic";
import { MagnifyingGlassIcon } from "@navikt/aksel-icons";
export const GlobalSearch = dynamic(() => import("./search"), {
loading: () => (
<Button
variant="primary"
className="hover:bg-deepblue-700 bg-deepblue-600 h-11"
aria-keyshortcuts="Control+b"
icon={
<MagnifyingGlassIcon
className="pointer-events-none -mt-[1px] shrink-0 text-2xl"
aria-label="Åpne meny"
aria-hidden
/>
}
iconPosition="left"
>
Søk
</Button>
),
ssr: false,
});
const LinkElement = ({ name, href, prefetch = undefined }) => {
const { asPath } = useRouter();
@@ -59,7 +82,7 @@ export const Header = ({
"mx-auto flex h-full max-w-screen-2xl items-center pr-4 lg:pr-6"
)}
>
<div className="flex h-11 items-center pr-4 pl-4 sm:pr-6 sm:pl-4">
<div className="flex h-11 items-center pl-4 pr-4 sm:pl-4 sm:pr-6">
<Link
href="/"
passHref
@@ -88,7 +111,9 @@ export const Header = ({
<LinkElement name="Bloggen" href="/produktbloggen" />
</ul>
</nav>
<GlobalSearch />
<div className="z-[1050] ml-auto mr-4 flex justify-center lg:ml-0 lg:mr-0">
<GlobalSearch />
</div>
<div className="mr-2 h-full lg:hidden">
<Hamburger />
</div>
@@ -1,3 +1,4 @@
/* stylelint-disable aksel/design-token-no-global-override */
.modalOverlay {
z-index: var(--a-z-index-modal);
@apply bg-surface-backdrop fixed inset-0 grid place-items-center lg:hidden;
@@ -5,7 +6,7 @@
.modalOverlaySearch {
z-index: 3000;
@apply bg-surface-backdrop fixed inset-0 grid place-items-center;
@apply bg-deepblue-900/20 fixed inset-0 grid place-items-center;
}
.akselTertiaryButton {
@@ -0,0 +1,36 @@
import { searchOptions } from "@/types";
import { Skeleton } from "@navikt/ds-react";
import { useContext } from "react";
import { SearchResultContext } from "./providers";
export const Error = () => {
const { isValidating, error } = useContext(SearchResultContext);
if (error) {
return (
<p aria-live="assertive" className="mx-auto w-fit px-6 py-24">
Noe gikk galt! Last siden på nytt eller ta kontakt med Aksel.
</p>
);
}
if (!isValidating) {
return null;
}
return (
<div className="grid w-full gap-2 p-1">
<Skeleton height="3rem" variant="rounded" />
<div className="flex flex-wrap gap-2 px-4 pb-4 md:px-10">
{Object.keys(searchOptions)
.slice(0, Object.keys(searchOptions).length - 1)
.map((x) => (
<Skeleton variant="rounded" height="1.75rem">
{searchOptions[x].display}
</Skeleton>
))}
</div>
</div>
);
};
@@ -0,0 +1,110 @@
import { searchOptions } from "@/types";
import { XMarkIcon } from "@navikt/aksel-icons";
import { Button, Chips, Search } from "@navikt/ds-react";
import { KBD } from "components/website-modules/KBD";
import { useContext, useRef } from "react";
import { useShortcut } from "./hooks";
import {
SearchContext,
SearchNavigationContext,
SearchResultContext,
} from "./providers";
export const SearchForm = () => {
const { open, setOpen, tags, setTags, query, setQuery, os } =
useContext(SearchContext);
const { update, results, reset, isValidating, error } =
useContext(SearchResultContext);
const { close } = useContext(SearchNavigationContext);
const inputRef = useRef(null);
useShortcut(open, setOpen, inputRef);
const handleSearchStart = (value: string) => {
setQuery(value);
value === "" ? reset() : update(value, tags);
};
const noHits = (key: string) => {
return !Object.hasOwn(results?.groupedHits ?? {}, key);
};
const noHitsAndQuery = (key: string) => {
return query.length > 0 && !tags.find((x) => x === key);
};
if (isValidating || error) {
return null;
}
const chipsToShow = Object.entries(searchOptions)
.filter((x) => !x[1].hidden)
.filter(
([key]) =>
!(noHitsAndQuery(key) && noHits(key) && results?.hits[key] === 0)
);
return (
<form
role="search"
onSubmit={(e) => e.preventDefault()}
className="bg-surface-default grid w-full gap-2"
>
<div className="flex flex-row-reverse items-center gap-1 p-1">
<Button
variant="tertiary-neutral"
icon={<XMarkIcon title="Lukk søk" />}
onClick={close}
type="button"
/>
<Search
label={
<span className="flex items-center gap-2">
<span>Søk i hele Aksel</span>
{os === "mac" ? <KBD>CMD + B</KBD> : <KBD>CTRL + B</KBD>}
</span>
}
aria-autocomplete="both"
variant="simple"
value={query}
onChange={(v) => handleSearchStart(v)}
onClear={() => {
setQuery("");
reset();
}}
ref={inputRef}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
autoFocus
id="aksel-search-input"
clearButton={false}
placeholder="Søk gjennom hele aksel"
className="border-none pr-0 focus:shadow-none"
/>
</div>
{chipsToShow.length !== 0 && (
<Chips className="px-4 pb-4 md:px-10">
{chipsToShow.map(([key, val]) => (
<Chips.Toggle
key={key}
variant="neutral"
selected={tags.includes(key as keyof typeof searchOptions)}
type="button"
onClick={() => {
const newTags = (tags as string[]).includes(key)
? tags.filter((y) => y !== key)
: [...tags, key];
setTags(newTags as Array<keyof typeof searchOptions>);
update(query, newTags);
}}
>
{val.display}
</Chips.Toggle>
))}
</Chips>
)}
</form>
);
};
@@ -1,109 +0,0 @@
import { Label } from "@navikt/ds-react";
import { GroupedHitsT, SearchHitT, searchOptions } from "@/types";
import { Hit, IconHit, IconPageHit } from "./Hit";
import React from "react";
export function Group({
groups,
query,
startIndex,
logSuccess,
}: {
groups: GroupedHitsT;
query: string;
startIndex: number;
logSuccess: (index: number, url: string) => void;
}) {
if (Object.keys(groups).length === 0) {
return null;
}
return (
<>
{Object.entries(groups)
.sort((a, b) => searchOptions[a[0]].index - searchOptions[b[0]].index)
.map(([key, val], index, arr) => {
const prev = arr.slice(0, index);
const total =
prev.reduce((prev, cur) => prev + cur[1].length, 0) + startIndex;
return (
<GroupComponent
logSuccess={logSuccess}
startIndex={total}
key={key}
heading={`${searchOptions[key].display} (${val.length})`}
hits={val}
query={query}
/>
);
})}
</>
);
}
export function GroupComponent({
heading,
hits,
query,
startIndex,
logSuccess,
}: {
heading: React.ReactNode;
hits: SearchHitT[];
query: string;
startIndex: number;
logSuccess: (index: number, url: string) => void;
}) {
return (
<div>
<div className="z-10 mt-4 rounded bg-teal-100 p-2">
<Label className="text-text-default" as="h3">
{heading}
</Label>
</div>
<ul className="mt-2">
{hits.map((x, xi) => {
switch (x.item._type) {
case "icon":
return (
<React.Fragment key={x.item.name + xi}>
<IconHit
key={x.item.name}
hit={x}
query={query}
index={startIndex + xi}
logSuccess={logSuccess}
/>
</React.Fragment>
);
case "icon_page":
return (
<React.Fragment key={x.item.heading + xi}>
<IconPageHit
key={x.item.heading}
hit={x}
query={query}
index={startIndex + xi}
logSuccess={logSuccess}
/>
</React.Fragment>
);
default:
return (
<React.Fragment key={x.item._id}>
<Hit
key={x.item._id}
hit={x}
query={query}
index={startIndex + xi}
logSuccess={logSuccess}
/>
</React.Fragment>
);
}
})}
</ul>
</div>
);
}
@@ -1,212 +1,145 @@
import { isNew } from "@/utils";
import { Tag } from "@navikt/ds-react";
import cl from "classnames";
import Image from "next/legacy/image";
import { StatusTag } from "components/website-modules/StatusTag";
import { SearchHitT } from "@/types";
import NextLink from "next/link";
import { urlFor } from "@/sanity/interface";
import { SearchHitT, searchOptions } from "@/types";
import { ChevronRightIcon } from "@navikt/aksel-icons";
import { Chips } from "@navikt/ds-react";
import cl from "clsx";
import { Tag } from "components/sanity-modules/frontpage-blocks/Tag";
import { StatusTag } from "components/website-modules/StatusTag";
import Image from "next/legacy/image";
import { default as Link, default as NextLink } from "next/link";
import { forwardRef, useContext } from "react";
import { SearchContext, SearchLoggingContext } from "./providers";
import { highlightMatches, highlightStr } from "./utils";
export function Hit({
hit,
query,
index,
logSuccess,
}: {
hit: SearchHitT;
query: string;
index: number;
logSuccess: (index: number, url: string) => void;
}) {
if (hit.item._type === "icon" || hit.item._type === "icon_page") {
return;
export const Hit = forwardRef<
HTMLLIElement,
{
hit: SearchHitT | Omit<SearchHitT, "score" | "anchor">;
index: number;
simple?: boolean;
tag?: Partial<keyof typeof searchOptions>;
}
>(({ hit, index, simple = false, tag }, ref) => {
const { query } = useContext(SearchContext);
const { logSuccess } = useContext(SearchLoggingContext);
const getHref = () => {
if ("anchor" in hit) {
return `/${hit.item.slug}${hit?.anchor ? `#h${hit.anchor}` : ""}`;
}
return `/${hit.item.slug}`;
};
return (
<li
ref={ref}
className={cl(
"focus-within:shadow-focus border-border-subtle group relative flex cursor-pointer scroll-mt-12 items-center justify-between gap-4 rounded border-b px-2 last-of-type:border-b-0 focus-within:z-10 hover:bg-gray-100"
"border-border-subtle relative flex scroll-my-10 items-center justify-between gap-3 border-b px-2 last-of-type:border-b-0"
)}
>
<div className="px-2 py-6">
<NextLink
href={`/${hit.item.slug}`}
passHref
onClick={() => logSuccess(index, `/${(hit.item as any).slug}`)}
className="flex items-center gap-2 text-xl font-semibold after:absolute after:inset-0 focus:outline-none"
>
<span className="group-hover:underline">
{highlightStr(hit.item.heading, query)}
</span>
<StatusTag status={hit?.item?.status?.tag} aria-hidden />
</NextLink>
<span className="font-regular text-text-subtle text-lg" aria-hidden>
{hit.highlight.shouldHightlight ? (
<div>{highlightStr(hit.highlight.description, query)}</div>
) : (
<div>{hit.highlight.description}</div>
)}
</span>
<span className="mt-4 flex gap-2 empty:mt-0">
{hit.item?.tema &&
hit.item?.tema.map((x) => (
<Tag variant="neutral-moderate" size="xsmall" key={x}>
{x}
</Tag>
))}
</span>
</div>
<div className="hidden aspect-square w-24 sm:block">
{hit.item?.status?.bilde && (
<Image
src={urlFor(hit.item.status.bilde).auto("format").url()}
decoding="sync"
width="96"
height="96"
layout="fixed"
objectFit="contain"
alt={hit.item?.heading + " thumbnail"}
aria-hidden
/>
)}
</div>
</li>
);
}
export function IconHit({
hit,
query,
index,
logSuccess,
}: {
hit: SearchHitT;
query: string;
index: number;
logSuccess: (index: number, url: string) => void;
}) {
if (hit.item._type !== "icon") {
return;
}
return (
<li
className={cl(
"focus-within:shadow-focus border-border-subtle group relative flex cursor-pointer scroll-mt-12 items-center justify-between gap-4 rounded border-b px-2 last-of-type:border-b-0 focus-within:z-10 hover:bg-gray-100"
)}
>
<div className="px-2 py-2">
<NextLink
href={`/ikoner/${hit.item.name}`}
passHref
onClick={() => logSuccess(index, `/${(hit.item as any).name}`)}
className="flex items-center gap-2 text-xl font-semibold after:absolute after:inset-0 focus:outline-none"
>
<span className="group-hover:underline">
{highlightStr(`${hit.item.name}Icon`, query)}
</span>
{isNew(hit.item?.created_at ?? "") && (
<Tag variant="info" size="small" aria-hidden>
Ny
</Tag>
)}
</NextLink>
<span className="font-regular text-text-subtle text-lg" aria-hidden>
<div>{highlightStr(hit.item.sub_category, query)}</div>
</span>
</div>
<div className="hidden aspect-square w-24 place-content-center sm:grid">
<Image
src={`https://raw.githubusercontent.com/navikt/aksel/main/%40navikt/aksel-icons/icons/${hit.item.name}.svg`}
decoding="sync"
width="24"
height="24"
layout="fixed"
objectFit="contain"
alt={hit.item.name + " thumbnail"}
aria-hidden
/>
</div>
</li>
);
}
export function IconPageHit({
hit,
query,
index,
logSuccess,
}: {
hit: SearchHitT;
query: string;
index: number;
logSuccess: (index: number, url: string) => void;
}) {
if (hit.item._type !== "icon_page") {
return;
}
return (
<li
className={cl(
"focus-within:shadow-focus border-border-subtle group relative flex cursor-pointer scroll-mt-12 items-center justify-between gap-4 rounded border-b px-2 last-of-type:border-b-0 focus-within:z-10 hover:bg-gray-100"
)}
>
<div className="px-2 py-2">
<NextLink
href="/ikoner"
passHref
onClick={() => logSuccess(index, `/ikoner`)}
className="flex items-center gap-2 text-xl font-semibold after:absolute after:inset-0 focus:outline-none"
>
<span className="group-hover:underline">
{highlightStr(`Ikoner`, query)}
</span>
</NextLink>
<span className="font-regular text-text-subtle text-lg" aria-hidden>
<div>{highlightStr(hit.item.description, query)}</div>
</span>
</div>
<div className="hidden aspect-square place-items-center sm:grid">
<Image
src="/images/thumbnail/ikoner/thumbnail.svg"
decoding="sync"
width="96"
height="96"
layout="fixed"
objectFit="contain"
alt=""
aria-hidden
/>
</div>
</li>
);
}
function splitStr(str: string, query: string) {
const regexStr = query.split(" ").join("|");
return str.split(new RegExp(`(${regexStr})`, "gi"));
}
function highlightStr(str: string, query: string) {
return (
<span>
{splitStr(str, query).map((part, i) => (
<div className="w-full truncate px-2 py-4">
<span
key={i}
className={cl({
"text-text-default bg-teal-200/80": query
.split(" ")
.map((x) => x.toLowerCase())
.includes(part.toLowerCase()),
"flex flex-col gap-1": simple,
"flex items-center gap-2": !simple,
})}
>
{part}
<NextLink
href={getHref()}
onClick={() => logSuccess(index, `/${(hit.item as any).slug}`)}
className="focus-visible:shadow-focus focus-visible:bg-border-focus focus-visible:text-text-on-action group scroll-my-32 text-xl font-semibold focus:outline-none"
>
<span className="group-hover:underline">
{highlightStr(hit.item.heading, query, tag)}
</span>
</NextLink>
{simple ? (
<Tag
type={hit.item._type}
size="xsmall"
text={hit.item.tema ? hit.item.tema[0] : undefined}
inline
aria-hidden
/>
) : (
<StatusTag status={hit?.item?.status?.tag} aria-hidden />
)}
</span>
))}
</span>
{!simple && <HeadingLinks index={index} hit={hit} />}
</div>
{!simple && (
<div className="hidden aspect-square w-24 sm:block">
{hit.item?.status?.bilde && (
<Image
src={urlFor(hit.item.status.bilde).auto("format").url()}
decoding="sync"
width="96"
height="96"
layout="fixed"
objectFit="contain"
alt={hit.item?.heading + " thumbnail"}
aria-hidden
/>
)}
</div>
)}
</li>
);
});
const highlightedHeadings = ["eksempler", "props", "tokens", "retningslinjer"];
function HeadingLinks({
hit,
index,
}: {
hit: SearchHitT | Omit<SearchHitT, "score" | "anchor">;
index: number;
}) {
const { logSuccess } = useContext(SearchLoggingContext);
const Description = () => {
if (hit.matches[0].key !== "heading") {
return highlightMatches(hit.matches[0]);
}
return (
<span className="font-regular text-text-subtle max-w-full text-lg">
{hit.description}
</span>
);
};
if (hit.item._type === "komponent_artikkel") {
return (
<>
<Description />
<Chips className="mt-3" size="small">
{hit.item.lvl2
.filter(
(x) =>
!!x.id && highlightedHeadings.includes(x.text.toLowerCase())
)
.map((x) => (
<Link
prefetch={false}
key={x.text}
href={`/${hit.item.slug}${`#h${x.id}`}`}
onClick={() =>
logSuccess(index, `/${(hit.item as any).slug}`, x.text)
}
className="min-h-6 bg-surface-neutral-subtle focus-visible:shadow-focus hover:bg-surface-neutral-subtle-hover ring-border-subtle flex items-center justify-center rounded-full px-2 ring-1 ring-inset focus:outline-none"
>
<span>{x.text}</span>
<ChevronRightIcon aria-hidden className="-mr-1" />
</Link>
))}
</Chips>
</>
);
}
return <Description />;
}
@@ -0,0 +1,132 @@
import { GroupedHitsT, SearchHitT, searchOptions } from "@/types";
import { Heading } from "@navikt/ds-react";
import { Tag } from "components/sanity-modules/frontpage-blocks/Tag";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Hit } from "./Hit";
import cl from "clsx";
export function CollectionMapper({
groups,
startIndex,
}: {
groups: GroupedHitsT;
startIndex: number;
}) {
if (Object.keys(groups).length === 0) {
return null;
}
return (
<>
{Object.entries(groups)
.sort((a, b) => searchOptions[a[0]].index - searchOptions[b[0]].index)
.map(
(
[key, val]: [key: keyof typeof searchOptions, val: SearchHitT[]],
index,
arr
) => {
const prev = arr.slice(0, index);
const total =
prev.reduce((prev, cur) => prev + cur[1].length, 0) + startIndex;
return (
<Collection
startIndex={total}
key={key}
heading={`${searchOptions[key].display} (${val.length})`}
tag={key}
hits={val}
/>
);
}
)}
</>
);
}
export function Collection({
heading,
hits,
startIndex,
simple = false,
tag,
}: {
heading?: React.ReactNode;
hits: SearchHitT[] | Omit<SearchHitT, "score" | "anchor">[];
simple?: boolean;
startIndex: number;
tag?: Partial<keyof typeof searchOptions>;
}) {
const [intersected, setIntersected] = useState(false);
const item = useRef(null);
const split = useMemo(() => {
if (hits.length <= (simple ? 20 : 5)) {
return { initial: hits, lazy: null };
}
return { initial: hits.slice(0, 4), lazy: hits.slice(4 + 1) };
}, [hits, simple]);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
entry.isIntersecting && setIntersected(entry.isIntersecting);
});
item.current && observer.observe(item.current);
return () => observer.disconnect();
}, []);
return (
<div>
{heading && tag && (
<h3
className={cl("sticky top-0 z-10 p-2 px-4 md:px-10", {
"bg-deepblue-50": ["komponent_artikkel", "ds_artikkel"].includes(
tag
),
"bg-violet-50": ["aksel_prinsipp", "aksel_standalone"].includes(
tag
),
"bg-teal-50": tag === "aksel_artikkel",
"bg-pink-50": tag === "aksel_blogg",
})}
>
<Tag hTag="span" type={tag} size="small" inline count={hits.length} />
</h3>
)}
{heading && !tag && (
<Heading
className="bg-surface-subtle sticky top-0 z-10 p-2 px-4 md:px-10"
size="small"
level="3"
>
{heading}
</Heading>
)}
<ul className="mt-2 px-0 md:px-6">
{split.initial.map((x, xi) => (
<Hit
key={xi}
hit={x}
index={startIndex + xi}
ref={xi === split.initial.length - 1 ? item : null}
simple={simple}
tag={tag}
/>
))}
{split.lazy &&
intersected &&
split.lazy.map((x, xi) => (
<Hit
key={xi}
hit={x}
index={startIndex + xi}
simple={simple}
tag={tag}
/>
))}
</ul>
</div>
);
}
@@ -0,0 +1,89 @@
import { searchOptions } from "@/types";
import { ChangeLogIconOutline } from "components/assets";
import { useContext } from "react";
import { Collection, CollectionMapper } from "./HitCollection";
import { SearchContext, SearchResultContext } from "./providers";
import { Heading, Label } from "@navikt/ds-react";
export const Results = () => {
const { tags, query } = useContext(SearchContext);
const { results, isValidating, error, mostResent } =
useContext(SearchResultContext);
if (isValidating || error) {
return null;
}
return (
<div className="flex h-full flex-col overflow-y-auto">
{!results && mostResent && (
<div id="aksel-search-results" aria-label="Nyeste artikler">
<Collection
startIndex={0}
heading={
<span className="flex items-center gap-2">
Nyeste artikler
<ChangeLogIconOutline className="shrink-0" aria-hidden />
</span>
}
simple
hits={mostResent}
/>
</div>
)}
{!results?.totalHits && query && (
<Heading
size="medium"
as="p"
className="mx-auto w-fit px-6 py-24"
aria-live="polite"
aria-atomic
>
<span className="text-text-subtle">{`Ingen treff på "`}</span>
<span className="break-all">{query}</span>
<span className="text-text-subtle">"</span>
{tags.length > 0 && (
<span className="text-text-subtle">{` i ${tags
.map((x) => searchOptions[x].display.toLowerCase())
.join(", ")}`}</span>
)}
</Heading>
)}
{results?.totalHits > 0 && (
<div id="aksel-search-results" aria-label="Søkeresultater">
<Label as="p" className="sr-only" aria-live="polite">
{`${results?.totalHits} treff på "${results.query}"${
tags.length > 0
? ` i ${tags
.map((x) => searchOptions[x].display.toLowerCase())
.join(", ")}`
: ""
}`}
</Label>
<div className="pb-4">
{results?.topResults.length > 0 && (
<Collection
startIndex={1}
heading={
<span className="flex items-center gap-2">
Beste treff
<ChangeLogIconOutline className="shrink-0" />
</span>
}
hits={results?.topResults}
/>
)}
<CollectionMapper
startIndex={
results.topResults.length > 0
? results.topResults.length + 1
: 1
}
groups={results.groupedHits}
/>
</div>
</div>
)}
</div>
);
};
@@ -0,0 +1,53 @@
import { MagnifyingGlassIcon } from "@navikt/aksel-icons";
import { Button } from "@navikt/ds-react";
import cl from "clsx";
import { useContext, useEffect } from "react";
import ReactModal from "react-modal";
import styles from "../header.module.css";
import { Error } from "./Error";
import { SearchForm } from "./Form";
import { Results } from "./Results";
import { SearchContext, SearchNavigationContext } from "./providers";
export const Search = () => {
const { open, setOpen } = useContext(SearchContext);
const { close } = useContext(SearchNavigationContext);
useEffect(() => {
ReactModal.setAppElement("#__next");
}, []);
return (
<>
<Button
variant="primary"
className="hover:bg-deepblue-700 bg-deepblue-600 h-11"
aria-keyshortcuts="Control+b"
icon={
<MagnifyingGlassIcon
className="pointer-events-none -mt-[1px] shrink-0 text-2xl"
aria-label="Åpne meny"
aria-hidden
/>
}
iconPosition="left"
onClick={() => setOpen(true)}
>
Søk
</Button>
<ReactModal
isOpen={open}
onRequestClose={close}
aria={{ modal: true }}
contentLabel="Søk"
className="bg-surface-default focus-visible:shadow-focus animate-searchModal shadow-xlarge absolute inset-0 mx-auto my-auto flex h-[90%] max-h-[52rem] w-full max-w-[90%] flex-col overflow-x-hidden rounded-lg focus:outline-none md:h-[80%] lg:max-w-4xl"
overlayClassName={cl(styles.modalOverlaySearch, "backdrop-blur-[2px]")}
htmlOpenClassName="overflow-hidden"
>
<Error />
<SearchForm />
<Results />
</ReactModal>
</>
);
};
@@ -0,0 +1,102 @@
import { SearchResultsT } from "@/types";
import { createSearchResult } from "../utils";
describe("createSearchResult", () => {
let result;
let rawResults;
beforeEach(() => {
result = [
{
item: { _type: "komponent_artikkel" },
score: 0.05,
matches: [],
},
{
item: { _type: "aksel_artikkel" },
score: 0.1,
matches: [],
},
{
item: { _type: "ds_artikkel" },
score: 0.15,
matches: [],
},
{
item: { _type: "aksel_blogg" },
score: 0.2,
matches: [],
},
{
item: { _type: "aksel_prinsipp" },
score: 0.25,
matches: [],
},
{
item: { _type: "aksel_standalone" },
score: 0.3,
matches: [],
},
];
rawResults = [
{ item: { _type: "komponent_artikkel" }, score: 0.05 },
{ item: { _type: "aksel_artikkel" }, score: 0.1 },
{ item: { _type: "ds_artikkel" }, score: 0.15 },
{ item: { _type: "aksel_blogg" }, score: 0.2 },
{ item: { _type: "aksel_prinsipp" }, score: 0.25 },
{ item: { _type: "aksel_standalone" }, score: 0.3 },
];
});
it("should group hits by type", () => {
const expected = {
komponent_artikkel: [
{ item: { _type: "komponent_artikkel" }, score: 0.05, matches: [] },
],
aksel_artikkel: [
{ item: { _type: "aksel_artikkel" }, score: 0.1, matches: [] },
],
ds_artikkel: [
{ item: { _type: "ds_artikkel" }, score: 0.15, matches: [] },
],
aksel_blogg: [
{ item: { _type: "aksel_blogg" }, score: 0.2, matches: [] },
],
aksel_prinsipp: [
{ item: { _type: "aksel_prinsipp" }, score: 0.25, matches: [] },
],
aksel_standalone: [
{ item: { _type: "aksel_standalone" }, score: 0.3, matches: [] },
],
};
const actual: SearchResultsT = createSearchResult(result, rawResults);
expect(actual.groupedHits).toEqual(expected);
});
it("should return top 3 results with score < 0.1", () => {
const expected = [
{ item: { _type: "komponent_artikkel" }, score: 0.05, matches: [] },
];
const actual: SearchResultsT = createSearchResult(result, rawResults);
expect(actual.topResults).toEqual(expected);
});
it("should return total number of hits", () => {
const expected = 6;
const actual: SearchResultsT = createSearchResult(result, rawResults);
expect(actual.totalHits).toEqual(expected);
});
it("should return number of hits by type", () => {
const expected = {
komponent_artikkel: 1,
aksel_artikkel: 1,
ds_artikkel: 1,
aksel_blogg: 1,
aksel_prinsipp: 1,
aksel_standalone: 1,
};
const actual: SearchResultsT = createSearchResult(result, rawResults);
expect(actual.hits).toEqual(expected);
});
});
@@ -0,0 +1,2 @@
export * from "./useSearch";
export * from "./useShortcut";
@@ -0,0 +1,56 @@
import { allArticleDocuments } from "@/sanity/config";
import { SearchResultsT } from "@/types";
import { useMemo, useState } from "react";
import useSWRImmutable from "swr/immutable";
import { createSearchResult, formatResults, fuseSearch } from "../utils";
import { debounce } from "@navikt/ds-react";
import { logSearch } from "@/utils";
export const useSearch = () => {
const [fuseResults, setFuseResults] = useState<SearchResultsT>(null);
const { data, error, isValidating } = useSWRImmutable(
`/searchindex.json`,
(query) => fetch(query).then((res) => res.json())
);
const updateResults = useMemo(
() =>
debounce((value: string, tags: string[]) => {
if (!value) {
return;
}
const rawResults = fuseSearch(data, value);
const formatedResults = formatResults(
rawResults.filter((x) =>
(tags.length > 0 ? tags : allArticleDocuments).includes(
x.item._type
)
)
);
setFuseResults({
...createSearchResult(formatedResults, rawResults),
query: value,
});
logSearch({
type: "standard",
searchedFromUrl: window.location.pathname,
query: value,
filter: tags,
});
}),
[data]
);
return {
results: fuseResults,
update: updateResults,
error,
isValidating,
reset: () => setFuseResults(null),
rawData: data,
};
};
@@ -0,0 +1,28 @@
import { MutableRefObject, useEffect } from "react";
export const useShortcut = (
open: boolean,
setOpen: (value: boolean) => void,
inputRef: MutableRefObject<HTMLInputElement>
) => {
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (
event.key === "b" &&
(event.metaKey || event.ctrlKey) &&
!event.shiftKey
) {
event.preventDefault();
if (open) {
inputRef.current?.focus();
} else {
setOpen(true);
}
}
};
document.addEventListener("keydown", listener);
return () => document.removeEventListener("keydown", listener);
}, [inputRef, open, setOpen]);
};
@@ -1,370 +1,21 @@
import { SearchLogT, SearchResultsT, searchOptions } from "@/types";
import { logSearch, useDebounce } from "@/utils";
import { MagnifyingGlassIcon } from "@navikt/aksel-icons";
import { Search } from "./Search";
import {
Button,
Checkbox,
CheckboxGroup,
Loader,
Search,
} from "@navikt/ds-react";
import { ChangeLogIconOutline } from "components/assets";
import { KBD } from "components/website-modules/KBD";
import { useRouter } from "next/router";
import { useCallback, useEffect, useRef, useState } from "react";
import ReactModal from "react-modal";
import styles from "../header.module.css";
import { Group, GroupComponent } from "./Group";
SearchProvider,
SearchResultProvider,
SearchLoggingProvider,
SearchNavigationProvider,
} from "./providers";
/**
* https://www.figma.com/file/71Sm1h6VV23lbBbQ3CJJ9t/Aksel-v2?node-id=1861%3A186079&t=ARKgZcA6B7ysmG3V-0
* TODO:
* - Oppdatere url-query basert på query + filter: ?search=abcd&filter=god_praksis
* - Oppdatere søkefelt og filter basert på url.
const GlobalSearch = () => (
<SearchProvider>
<SearchLoggingProvider>
<SearchResultProvider>
<SearchNavigationProvider>
<Search />
</SearchNavigationProvider>
</SearchResultProvider>
</SearchLoggingProvider>
</SearchProvider>
);
*/
export const GlobalSearch = () => {
const [results, setResults] = useState<SearchResultsT>(null);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [activeTags, setTags] = useState<Array<keyof typeof searchOptions>>([]);
const [os, setOs] = useState<"mac" | "windows">("windows");
const inputRef = useRef(null);
const session = useRef<{ retires: number; queries: string[] }>({
retires: 0,
queries: [],
});
const firstSearch = useRef<boolean>(true);
const prevSearch = useRef<string>("");
const router = useRouter();
const [query, setQuery] = useState("");
const debouncedSearchTerm = useDebounce(query);
useEffect(() => {
ReactModal.setAppElement("#__next");
navigator.userAgent?.indexOf("Mac OS X") !== -1
? setOs("mac")
: setOs("windows");
}, []);
const logSearchAttempt = useCallback(
(hits: number, type: SearchLogT["type"]) => {
const data: SearchLogT = {
type,
searchedFromUrl: router.asPath,
hits: hits ?? 0,
retries: session.current.retires,
retriedQueries: session.current.queries,
query: debouncedSearchTerm,
filter: activeTags,
};
logSearch(data);
},
[router.asPath, debouncedSearchTerm, activeTags]
);
const logSuccessSearchAttempt = useCallback(
(index: number, url: string) => {
const data: SearchLogT = {
type: "suksess",
searchedFromUrl: router.asPath,
hits: results?.totalHits ?? 0,
retries: session.current.retires,
retriedQueries: session.current.queries,
query: debouncedSearchTerm,
filter: activeTags,
index,
url,
accuracy: (100 - index / results?.totalHits).toFixed(0),
topResult: index <= results?.topResults?.length,
};
logSearch(data);
},
[
router.asPath,
results?.totalHits,
results?.topResults,
debouncedSearchTerm,
activeTags,
]
);
useEffect(() => {
if (debouncedSearchTerm && open) {
setLoading(true);
fetch(
`/api/search/v1?q=${encodeURIComponent(debouncedSearchTerm)}${
activeTags.length > 0 ? `&doc=${activeTags.join(",")}` : ""
}`
)
.then((x) => x.json())
.then((res) => {
setResults(res);
logSearchAttempt(res?.hits?.totalHits ?? 0, "standard");
setLoading(false);
})
.catch(() => {
setResults(null);
setLoading(false);
});
window.scrollTo(0, 0);
} else {
setLoading(false);
setResults(null);
}
}, [debouncedSearchTerm, activeTags, open, logSearchAttempt]);
useEffect(() => {
if (debouncedSearchTerm && open && !firstSearch.current) {
session.current = {
queries: [...session.current.queries, prevSearch.current],
retires: session.current.retires + 1,
};
prevSearch.current = debouncedSearchTerm;
} else if (debouncedSearchTerm && open) {
firstSearch.current = false;
prevSearch.current = debouncedSearchTerm;
}
}, [debouncedSearchTerm, activeTags, open]);
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (
event.key === "b" &&
(event.metaKey || event.ctrlKey) &&
!event.shiftKey
) {
event.preventDefault();
if (open) {
inputRef.current?.focus();
} else {
setOpen(true);
}
}
};
document.addEventListener("keydown", listener);
return () => document.removeEventListener("keydown", listener);
}, [open]);
const handleClose = useCallback(() => {
if (session.current.retires > 0 || prevSearch.current !== "") {
logSearchAttempt(results?.totalHits, "feilet");
}
setOpen(false);
setQuery("");
setTags([]);
session.current = { queries: [], retires: 0 };
firstSearch.current = true;
}, [logSearchAttempt, results?.totalHits]);
useEffect(() => {
router.events.on("beforeHistoryChange", handleClose);
router.events.on("hashChangeComplete", handleClose);
return () => {
router.events.off("beforeHistoryChange", handleClose);
router.events.off("hashChangeComplete", handleClose);
};
}, [handleClose, router.events]);
const handleQueryChange = (v: string) => {
setQuery(v);
v !== debouncedSearchTerm && setLoading(!!v);
};
const noHits = (key: string) => {
return !Object.hasOwn(results?.groupedHits ?? {}, key);
};
const noHitsAndQuery = (key: string) => {
return debouncedSearchTerm.length > 0 && !activeTags.find((x) => x === key);
};
return (
<div className="z-[1050] ml-auto mr-4 flex justify-center lg:ml-0 lg:mr-0">
<Button
variant="primary"
className="hover:bg-deepblue-700 bg-deepblue-600 focus-visible:shadow-focus-gap h-11 focus:shadow-none"
aria-keyshortcuts="Control+b"
icon={
<MagnifyingGlassIcon
className="pointer-events-none -mt-[1px] shrink-0 text-2xl"
aria-label="Åpne meny"
aria-hidden
/>
}
iconPosition="left"
onClick={() => setOpen(true)}
>
Søk
</Button>
<ReactModal
isOpen={open}
onRequestClose={() => handleClose()}
aria={{ modal: true }}
contentLabel="Søk"
className="bg-surface-default absolute inset-0 block w-screen overflow-x-auto px-4 md:px-6"
overlayClassName={styles.modalOverlaySearch}
>
<div className="search-grid-wrapper relative mx-auto max-w-4xl gap-4 gap-x-8 py-24">
{/* <button
className="focus-visible:shadow-focus hover:bg-surface-neutral-subtle-hover absolute right-4 top-8 flex items-center justify-center gap-2 rounded px-2 py-3 text-lg focus:outline-none"
>
<span>Lukk søk</span>
</button> */}
<Button
className="absolute right-4 top-8"
variant="tertiary-neutral"
icon={<KBD>ESC</KBD>}
onClick={() => handleClose()}
iconPosition="right"
>
Lukk søk
</Button>
<div className="search-grid-filter mt-8">
<CheckboxGroup legend="Filter" onChange={setTags}>
{Object.entries(searchOptions)
.filter((x) => !x[1].hidden)
.map(([key, val]) => (
<Checkbox
disabled={
noHitsAndQuery(key) &&
noHits(key) &&
results?.hits[key] === 0
}
key={key}
value={key}
className="whitespace-nowrap"
>
{`${val.display} ${
results?.hits[key] > 0 ? `(${results?.hits[key]})` : ""
}`}
</Checkbox>
))}
</CheckboxGroup>
</div>
<div className="search-grid-input w-full">
<form role="search" onSubmit={(e) => e.preventDefault()}>
<Search
label={
<span className="flex items-center gap-2">
<span>Søk i hele Aksel</span>
{os === "mac" ? <KBD>CMD + B</KBD> : <KBD>CTRL + B</KBD>}
</span>
}
variant="simple"
value={query}
hideLabel={false}
onChange={(v) => handleQueryChange(v)}
onClear={() => setQuery("")}
ref={inputRef}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
autoFocus
id="aksel-search-input"
clearButton={false}
/>
</form>
</div>
<div
className="search-grid-results mt-8 w-full max-w-3xl"
aria-busy={loading}
role="status"
>
{loading && (
<div className="flex w-full justify-center p-4">
<Loader size="xlarge" variant="neutral" />
</div>
)}
{results && query && !loading && (
<div id="aksel-search-results" aria-label="Søkeresultater">
<p className="text-xl font-semibold">
{`${results?.totalHits} treff på "${query}"${
activeTags.length > 0
? ` i ${activeTags
.map((x) => searchOptions[x].display.toLowerCase())
.join(", ")}`
: ""
}`}
</p>
<div className="mt-4 pb-16 md:block">
{results?.topResults.length > 0 && (
<GroupComponent
startIndex={1}
heading={
<span className="flex items-center gap-2">
Beste treff
<ChangeLogIconOutline className="shrink-0" />
</span>
}
logSuccess={logSuccessSearchAttempt}
hits={results?.topResults}
query={query}
/>
)}
<Group
startIndex={
results?.topResults.length > 0
? results?.topResults.length + 1
: 1
}
logSuccess={logSuccessSearchAttempt}
groups={results?.groupedHits}
query={debouncedSearchTerm}
/>
</div>
</div>
)}
</div>
</div>
<style jsx>{`
.search-grid-wrapper {
display: grid;
grid-template-columns: 12rem auto;
grid-template-areas:
"input input"
"results results";
}
.search-grid-wrapper .search-grid-filter {
display: none;
}
.search-grid-wrapper .search-grid-input {
grid-area: input;
}
.search-grid-wrapper .search-grid-results {
grid-area: results;
}
@media (min-width: 768px) {
.search-grid-wrapper {
display: grid;
grid-template-columns: 12rem auto;
grid-template-areas:
"empty input"
"filter results";
}
.search-grid-wrapper .search-grid-filter {
display: block;
grid-area: filter;
}
}
`}</style>
</ReactModal>
</div>
);
};
export default GlobalSearch;
@@ -0,0 +1,50 @@
import { logSearch } from "@/utils";
import { createContext, useCallback, useContext } from "react";
import { useSearch } from "../hooks";
import { SearchContext } from "./SearchProvider";
type SearchLoggingContextType = {
logSuccess: (index: number, url: string, tag?: string) => void;
};
export const SearchLoggingContext = createContext<SearchLoggingContextType>({
logSuccess: () => null,
});
export const SearchLoggingProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const context = useSearch();
const { query, tags } = useContext(SearchContext);
const logSuccess = useCallback(
(index: number, url: string, tag?: string) => {
const data = {
type: "suksess",
searchedFromUrl: window.location.pathname,
query,
filter: tags,
tag,
index,
url,
accuracy: (100 - index / context?.results?.totalHits).toFixed(0),
topResult: index <= context?.results?.topResults?.length,
};
logSearch(data);
},
[
query,
tags,
context?.results?.totalHits,
context?.results?.topResults?.length,
]
);
return (
<SearchLoggingContext.Provider value={{ logSuccess }}>
{children}
</SearchLoggingContext.Provider>
);
};
@@ -0,0 +1,52 @@
import { createContext, useCallback, useContext, useEffect } from "react";
import { SearchContext } from "./SearchProvider";
import { SearchResultContext } from "./SearchResultProvider";
import { useRouter } from "next/router";
type SearchNavigationProviderT = { close: () => void };
export const SearchNavigationContext = createContext<SearchNavigationProviderT>(
{
close: () => null,
}
);
export const SearchNavigationProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const router = useRouter();
const { setOpen, setQuery, setTags } = useContext(SearchContext);
const { reset } = useContext(SearchResultContext);
const close = useCallback(() => {
setOpen(false);
reset();
setQuery("");
setTags([]);
}, [reset, setOpen, setQuery, setTags]);
/* Add a small delay to get a precieved smoother navigation */
useEffect(() => {
let timeout: NodeJS.Timeout;
const handler = () => {
timeout && clearTimeout(timeout);
setTimeout(() => close(), 100);
};
router.events.on("beforeHistoryChange", handler);
router.events.on("hashChangeComplete", handler);
return () => {
router.events.off("beforeHistoryChange", handler);
router.events.off("hashChangeComplete", handler);
};
}, [close, router.events]);
return (
<SearchNavigationContext.Provider value={{ close }}>
{children}
</SearchNavigationContext.Provider>
);
};
@@ -0,0 +1,63 @@
import { searchOptions } from "@/types";
import {
Dispatch,
SetStateAction,
createContext,
useEffect,
useRef,
useState,
} from "react";
import { useShortcut } from "../hooks";
type SearchContextType = {
os: "mac" | "windows";
open: boolean;
setOpen: (v) => void;
tags: Array<keyof typeof searchOptions>;
setTags: Dispatch<SetStateAction<Array<keyof typeof searchOptions>>>;
query: string;
setQuery: (v: string) => void;
};
export const SearchContext = createContext<SearchContextType>({
os: "windows",
open: false,
setOpen: () => null,
tags: [],
setTags: () => null,
query: "",
setQuery: () => null,
});
export const SearchProvider = ({ children }: { children: React.ReactNode }) => {
const [open, setOpen] = useState(false);
const [tags, setTags] = useState<SearchContextType["tags"]>([]);
const [os, setOs] = useState<SearchContextType["os"]>("windows");
const inputRef = useRef(null);
const [query, setQuery] = useState("");
useShortcut(open, setOpen, inputRef);
useEffect(() => {
navigator.userAgent?.indexOf("Mac OS X") !== -1
? setOs("mac")
: setOs("windows");
}, []);
return (
<SearchContext.Provider
value={{
os,
open,
setOpen,
tags,
setTags,
query,
setQuery,
}}
>
{children}
</SearchContext.Provider>
);
};
@@ -0,0 +1,53 @@
import { allArticleDocuments } from "@/sanity/config";
import { SearchHitT, SearchResultsT } from "@/types";
import { createContext, useContext, useMemo } from "react";
import { useSearch } from "../hooks";
import { formatRawResults } from "../utils";
import { SearchContext } from "./SearchProvider";
type SearchResultContextType = {
results: SearchResultsT;
update: (value: string, tags: string[]) => void;
error: any;
isValidating: boolean;
reset: () => void;
mostResent: Omit<SearchHitT, "score" | "anchor" | "matches">[];
};
export const SearchResultContext = createContext<SearchResultContextType>({
results: null,
update: () => null,
error: false,
isValidating: false,
reset: () => null,
mostResent: [],
});
export const SearchResultProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const context = useSearch();
const { tags } = useContext(SearchContext);
const mostResent: Omit<SearchHitT, "score" | "anchor">[] = useMemo(() => {
if (!context.rawData) {
return null;
}
return formatRawResults(
context.rawData
.filter((x) =>
(tags.length > 0 ? tags : allArticleDocuments).includes(x._type)
)
.slice(0, 20)
);
}, [context.rawData, tags]);
return (
<SearchResultContext.Provider value={{ ...context, mostResent }}>
{children}
</SearchResultContext.Provider>
);
};
@@ -0,0 +1,4 @@
export * from "./SearchProvider";
export * from "./SearchResultProvider";
export * from "./SearchLoggingProvider";
export * from "./SearchNavigationProvider";
@@ -0,0 +1,36 @@
import { FuseItemT, SearchHitT } from "@/types";
import { omit } from "@navikt/ds-react";
import Fuse from "fuse.js";
export function formatResults(res: Fuse.FuseResult<FuseItemT>[]): SearchHitT[] {
return res.map((x) => {
return {
...x,
item: omit(x.item, ["intro", "ingress"]) as Omit<
FuseItemT,
"ingress" | "intro"
>,
anchor: resolveAnchor(x.matches[0], x.item),
description: x?.item.intro || x.item.ingress || "",
};
});
}
export function formatRawResults(
res: FuseItemT[]
): Omit<SearchHitT, "score" | "anchor">[] {
return res.map((x) => ({
item: omit(x, ["intro", "ingress"]) as Omit<FuseItemT, "ingress" | "intro">,
description: x?.intro || x.ingress || "",
}));
}
function resolveAnchor(match: Fuse.FuseResultMatch, item: FuseItemT) {
if (match.key.includes("lvl")) {
return item[match.key.split(".")[0]][match.refIndex].id;
}
if (match.key === "content.text") {
return item[match.key.split(".")[0]][match.refIndex].id;
}
return null;
}
@@ -0,0 +1,25 @@
import { FuseItemT } from "@/types";
import Fuse from "fuse.js";
export function fuseSearch(results: any[], query: string) {
/* https://fusejs.io/api/options.html */
const fuse = new Fuse<FuseItemT>(results, {
keys: [
{ name: "heading", weight: 100 },
{ name: "lvl2.text", weight: 50 },
{ name: "lvl3.text", weight: 40 },
{ name: "lvl4.text", weight: 30 },
{ name: "ingress", weight: 30 },
{ name: "intro", weight: 30 },
{ name: "tema", weight: 70 },
{ name: "content.text", weight: 20 },
],
includeScore: true,
shouldSort: true,
minMatchCharLength: 3,
includeMatches: true,
threshold: 0.3,
location: 120,
});
return fuse.search(query).filter((x) => x.score < 0.3);
}
@@ -0,0 +1,42 @@
import { FuseItemT, GroupedHitsT, SearchHitT, SearchResultsT } from "@/types";
import Fuse from "fuse.js";
export function createSearchResult(
result: SearchHitT[],
rawResults: Fuse.FuseResult<FuseItemT>[]
) {
const groupedHits: GroupedHitsT = result?.reduce((prev, cur) => {
if (cur.item._type in prev) {
return { ...prev, [cur.item._type]: [...prev[cur.item._type], cur] };
} else {
return { ...prev, [cur.item._type]: [cur] };
}
}, {});
const response: SearchResultsT = {
groupedHits,
topResults:
result?.length > 4 ? result.slice(0, 4).filter((x) => x.score < 0.1) : [],
totalHits: result?.length ?? 0,
hits: {
komponent_artikkel: rawResults.filter(
(x: any) => x.item._type === "komponent_artikkel"
).length,
aksel_artikkel: rawResults.filter(
(x: any) => x.item._type === "aksel_artikkel"
).length,
ds_artikkel: rawResults.filter((x: any) => x.item._type === "ds_artikkel")
.length,
aksel_blogg: rawResults.filter((x: any) => x.item._type === "aksel_blogg")
.length,
aksel_prinsipp: rawResults.filter(
(x: any) => x.item._type === "aksel_prinsipp"
).length,
aksel_standalone: rawResults.filter(
(x: any) => x.item._type === "aksel_prinsipp"
).length,
},
};
return response;
}
@@ -0,0 +1,93 @@
import React from "react";
import cl from "clsx";
import Fuse from "fuse.js";
import { searchOptions } from "@/types";
function splitStr(str: string, query: string) {
const regexStr = query.toLowerCase().split(" ").join("|");
return str.split(new RegExp(`(${regexStr})`, "gi"));
}
export function highlightStr(
str: string,
query: string,
tag: keyof typeof searchOptions
) {
if (!query) {
return str;
}
const getClass = (part: string) => {
if (
!query
.split(" ")
.map((x) => x.toLowerCase())
.includes(part.toLowerCase())
) {
return undefined;
}
return cl(
"text-text-default group-focus-visible:text-text-on-action group-focus-visible:bg-transparent",
{
"bg-gray-200": !tag,
"bg-teal-100": tag === "aksel_artikkel",
"bg-deepblue-100": ["komponent_artikkel", "ds_artikkel"].includes(tag),
"bg-violet-100": ["aksel_prinsipp", "aksel_standalone"].includes(tag),
"bg-pink-100": tag === "aksel_blogg",
}
);
};
return (
<>
{splitStr(str, query)
.filter((x) => !!x)
.map((part, i) => (
<span key={i} className={getClass(part)}>
{part}
</span>
))}
</>
);
}
export function highlightMatches(hitMatch: Fuse.FuseResultMatch) {
const text = hitMatch.value;
const result = [];
const matches = [...hitMatch.indices];
let index = 0;
matches.forEach((match, i) => {
const str = text.substring(match[0], match[1] - 1);
let preText = text.substring(index, match[0]);
if (i === 0) {
const preLength = preText.length;
preText = preText.substring(preText.length - 23);
if (preText.length < preLength) {
preText = `...${preText}`;
}
}
result.push({ text: preText, highlight: false });
result.push({ text: str, highlight: true });
index = match[1] - 1;
if (i === matches.length - 1) {
result.push({ text: text.substring(index), highlight: false });
}
});
return (
<span>
{result.map((part, i) => (
<span
key={i}
className={cl({
"text-text-default font-semibold": part.highlight,
})}
>
{part.text}
</span>
))}
</span>
);
}
@@ -0,0 +1,4 @@
export * from "./format-result";
export * from "./fuse-search";
export * from "./group-results";
export * from "./highlighter";
@@ -6,7 +6,7 @@ import cl from "clsx";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { useSection } from "./use-sections";
import { useSection } from "./useSection";
const NavItem = ({
link,
@@ -3,7 +3,7 @@ import { TokenTableT } from "@/types";
import { ChevronDownCircleIcon } from "@navikt/aksel-icons";
import core from "@navikt/ds-css/tokens.json";
import { BodyLong, CopyButton, Label, Link } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import { AkselTable, AkselTableRow } from "components/website-modules/Table";
import NextLink from "next/link";
import { useState } from "react";
@@ -1,7 +1,7 @@
import { FeedbackT } from "@/types";
import { IdContext } from "@/utils";
import { Button, Heading, Textarea } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import { useRouter } from "next/router";
import { useContext, useState } from "react";
@@ -127,7 +127,7 @@ const Card = ({
<Tag
type={article._type}
text={article.tema ? article.tema[0] : undefined}
size="small"
size="xsmall"
beta={article.status?.tag === "beta"}
/>
</div>
@@ -38,14 +38,13 @@ export const Highlight = ({
quality={100}
layout="fill"
aria-hidden
priority
className={cl(
"bg-deepblue-200 rounded-lg object-cover sm:object-contain",
{
"hue-rotate-[65deg]": article?.status?.tag === "beta",
}
)}
decoding="sync"
decoding="auto"
/>
) : article?.seo?.image ? (
<Image
@@ -54,11 +53,10 @@ export const Highlight = ({
layout="fill"
objectFit="cover"
aria-hidden
priority
className={cl("rounded-lg", {
"hue-rotate-[65deg]": article?.status?.tag === "beta",
})}
decoding="sync"
decoding="auto"
/>
) : (
<Image
@@ -66,9 +64,8 @@ export const Highlight = ({
layout="fill"
objectFit="contain"
aria-hidden
priority
className="rounded-lg"
decoding="sync"
decoding="auto"
/>
)}
</div>
@@ -6,22 +6,36 @@ export const Tag = ({
text,
size = "medium",
beta = false,
inline = false,
hTag,
count,
...rest
}: {
type: string;
text?: string;
size?: "medium" | "small";
size?: "medium" | "small" | "xsmall";
beta?: boolean;
inline?: boolean;
hTag?: string;
count?: number;
}) => {
const Comp = ({ ...props }: any) =>
size === "medium" ? (
<Heading {...props} as="div" size="medium" />
size === "medium" || size === "small" ? (
<Heading {...rest} {...props} as={hTag ?? "div"} size={size}>
{props.children}
{`${count ? ` (${count})` : ""}`}
</Heading>
) : (
<Label
{...rest}
{...props}
as="div"
as={hTag ?? "div"}
size="small"
className={cl(props?.className, "mb-2")}
/>
className={cl(props?.className, !inline && "mb-2")}
>
{props.children}
{`${count ? ` (${count})` : ""}`}
</Label>
);
switch (type) {
@@ -30,13 +44,17 @@ export const Tag = ({
<Comp className="text-teal-700">{text ? text : "God praksis"}</Comp>
);
case "aksel_blogg":
return <Comp className="text-pink-600">Blogg</Comp>;
return <Comp className="text-pink-700">Blogg</Comp>;
case "komponent_artikkel":
return (
<Comp className="text-deepblue-600 flex items-center gap-3">
Komponent
{beta && (
<DsTag variant="alt1" size="small">
<DsTag
variant="alt1"
size="small"
className="border-violet-300 bg-violet-50"
>
Beta
</DsTag>
)}
@@ -53,6 +71,18 @@ export const Tag = ({
)}
</Comp>
);
case "aksel_prinsipp":
return (
<Comp className="flex items-center gap-3 text-violet-800">
Prinsipper
</Comp>
);
case "aksel_standalone":
return (
<Comp className="flex items-center gap-3 text-violet-800">
Unike sider
</Comp>
);
default:
return null;
@@ -9,7 +9,7 @@ export const Grid = ({
stacked?: boolean;
}) => (
<div
className={cl("mt-8 grid", {
className={cl("mt-4 grid", {
"grid-cols-[repeat(auto-fit,_minmax(16rem,_1fr))] gap-6 xl:grid-cols-3":
!stacked,
"gap-8": stacked,
@@ -169,7 +169,7 @@ export const SemanticView = ({ cat }: { cat: string }) => {
}}
/>
<span
className="rounded-t-small h-2/3 w-3"
className="rounded-t-small h-2/4 w-3"
style={{
border: `1px solid var(${x.name})`,
backgroundColor: `var(${x.name.replace(
@@ -179,7 +179,7 @@ export const SemanticView = ({ cat }: { cat: string }) => {
}}
/>
<span
className="rounded-t-small h-1/3 w-3"
className="rounded-t-small h-2/3 w-3"
style={{
border: `1px solid var(${x.name})`,
backgroundColor: `var(${x.name.replace(
@@ -233,11 +233,11 @@ export const SemanticView = ({ cat }: { cat: string }) => {
style={{ backgroundColor: `var(${x.name})` }}
/>
<span
className="rounded-t-small h-2/3 w-3"
className="rounded-t-small h-2/4 w-3"
style={{ backgroundColor: `var(${x.name})` }}
/>
<span
className="rounded-t-small h-1/3 w-3"
className="rounded-t-small h-2/3 w-3"
style={{ backgroundColor: `var(${x.name})` }}
/>
</div>
@@ -1,7 +1,8 @@
import { Tooltip } from "@navikt/ds-react";
import { useContext } from "react";
import { IdContext, useCheckAuth } from "./utils";
import { IdContext } from "./utils";
import { PencilIcon } from "@navikt/aksel-icons";
import { useCheckAuth } from "components/website-modules/utils/useCheckAuth";
function EditButton() {
const idCtx = useContext(IdContext);
@@ -9,26 +10,24 @@ function EditButton() {
const validUser = useCheckAuth();
return validUser && idCtx?.id ? (
<>
<div className="overflow-hidden">
<Tooltip
content="Vises bare for innloggede redaktører"
placement="left"
delay={500}
<div className="overflow-hidden">
<Tooltip
content="Vises bare for innloggede redaktører"
placement="left"
delay={500}
>
<a
href={`https://aksel.nav.no/admin/prod/intent/edit/id=${idCtx?.id}`}
target="_blank"
rel="noreferrer"
className="editbutton bg-deepblue-800 hover:bg-deepblue-700 absolute right-0 top-0 flex -translate-y-[99%] items-center gap-2 overflow-hidden rounded-tl px-2 py-1 text-white transition-transform hover:translate-x-0"
aria-hidden
tabIndex={-1}
>
<a
href={`https://aksel.nav.no/admin/prod/intent/edit/id=${idCtx?.id}`}
target="_blank"
rel="noreferrer"
className="editbutton bg-deepblue-800 hover:bg-deepblue-700 absolute right-0 top-0 flex -translate-y-[99%] items-center gap-2 overflow-hidden rounded-tl px-2 py-1 text-white transition-transform hover:translate-x-0"
aria-hidden
tabIndex={-1}
>
<PencilIcon aria-hidden className="shrink-0 text-xl" /> Rediger side
</a>
</Tooltip>
</div>
</>
<PencilIcon aria-hidden className="shrink-0 text-xl" /> Rediger side
</a>
</Tooltip>
</div>
) : null;
}
@@ -29,6 +29,7 @@ export const IntroCards = ({
"shadow-small max-w-md": variant === "forside",
}
)}
prefetch={false}
onClick={(e) =>
logNav(
"intro-kort",
@@ -3,8 +3,9 @@ import cl from "clsx";
export const KBD = (props: React.HTMLAttributes<HTMLElement>) => (
<kbd
className={cl(
"mx-05 ring-border-subtle my-0 inline-block min-w-[2rem] rounded-md px-2 py-[1px] text-center font-mono text-sm text-gray-900 ring-1 ring-inset",
"from-surface-default via-surface-default to-surface-neutral-subtle bg-gradient-to-b"
"mx-05 ring-border-subtle my-0 inline-block min-w-[2rem] rounded-md px-2 py-[1px] text-center font-mono text-sm text-gray-900 ring-1 ring-inset",
"from-surface-default via-surface-default to-surface-neutral-subtle bg-gradient-to-b",
"group-active:text-text-on-inverted group-active:ring-grayalpha-400 group-active:from-gray-700 group-active:via-gray-700 group-active:to-gray-900"
)}
>
<span className="kbd">{props.children}</span>
@@ -1,8 +1,8 @@
/**
* https://github.com/navikt/detsombetyrnoe/blob/main/src/components/PreviewBanner.tsx#L17
*/
import { useCheckAuth } from "./utils";
import { BodyShort, Button } from "@navikt/ds-react";
import { useCheckAuth } from "components/website-modules/utils/useCheckAuth";
import Link from "next/link";
import { useRouter } from "next/router";
@@ -53,7 +53,7 @@ function PreviewBanner() {
return (
<a
href={`/api/exit-preview?slug=${asPath}`}
className="text-text-on-inverted focus-visible:shadow-focus-inverted fixed top-2 left-0 z-[9999] w-80 -translate-x-24 translate-y-6 -rotate-45 bg-gray-900 p-4 text-center font-semibold no-underline hover:bg-gray-700 focus:outline-none"
className="text-text-on-inverted focus-visible:shadow-focus-inverted fixed left-0 top-2 z-[9999] w-80 -translate-x-24 translate-y-6 -rotate-45 bg-gray-900 p-4 text-center font-semibold no-underline hover:bg-gray-700 focus:outline-none"
>
<div>EXIT PREVIEW</div>
</a>
@@ -1,5 +1,5 @@
import { removeEmojies } from "@/utils";
import { BodyShort, Link } from "@navikt/ds-react";
import { BodyShort, Link, useClientLayoutEffect } from "@navikt/ds-react";
import cl from "clsx";
import throttle from "lodash/throttle";
import * as React from "react";
@@ -7,11 +7,11 @@ import { useEffect, useState } from "react";
export function TableOfContentsv2({
changedState,
hideToc = true,
aksel = false,
}: {
changedState: any;
hideToc?: boolean;
aksel?: boolean;
}) {
const [toc, setToc] = useState<
@@ -21,7 +21,7 @@ export function TableOfContentsv2({
const [activeId, setActiveId] = useState(null);
const [activeSubId, setActiveSubId] = useState(null);
useEffect(() => {
useClientLayoutEffect(() => {
const time = setTimeout(() => {
const main = document.getElementsByTagName("main")?.[0];
const tags = main?.getElementsByTagName("h2");
@@ -126,24 +126,27 @@ export function TableOfContentsv2({
element && element?.scrollIntoView();
};
const renderToc = !(toc.length < 2) && !hideToc;
const renderToc = toc.length !== 0;
return (
<aside
className={cl(
"toc-ignore sticky top-20 z-[1] order-1 my-0 ml-6 mb-16 mr-auto h-full w-full max-w-[160px] flex-col items-start sm:-mr-6 md:-mr-10",
"toc-ignore sticky top-20 z-[1] order-1 my-0 mb-16 ml-6 mr-auto h-full w-full max-w-[160px] flex-col items-start sm:-mr-6 md:-mr-10",
"invisible hidden xl:flex",
{
hidden: !renderToc,
"hidden xl:flex": renderToc,
invisible: !renderToc,
"xl:visible": renderToc,
}
)}
>
<BodyShort
id="toc-heading"
className="text-deepblue-800 mb-2 font-semibold"
>
Innhold på siden
</BodyShort>
{toc.length > 0 && (
<BodyShort
id="toc-heading"
className="text-deepblue-800 animate-fadeIn mb-2 font-semibold "
>
Innhold på siden
</BodyShort>
)}
<nav
aria-labelledby="toc-heading"
@@ -157,9 +160,8 @@ export function TableOfContentsv2({
<BodyShort
as="li"
size="small"
className={cl("relative py-1", {
className={cl("animate-fadeIn relative py-1 ", {
"font-semibold": link.id === activeId,
"": link.id !== activeId,
})}
>
<Link
@@ -1,37 +0,0 @@
import { Heading } from "@navikt/ds-react";
export const ScrollTop = () => {
return (
<button
onClick={() => {
window.scrollTo(0, 0);
document.getElementById("hovedinnhold")?.focus?.();
}}
className="text-xlarge text-text-on-inverted focus-visible:shadow-focus-inverted group -mt-[10px] flex h-[48px] w-fit items-center justify-center gap-2 rounded p-2 font-semibold hover:underline focus:outline-none"
>
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="h-[19px] w-[19px] translate-y-1 rotate-90 transition-all group-hover:translate-y-0"
aria-hidden
>
<path
d="M8 17.5L2.12132 11.6213C0.949744 10.4497 0.949746 8.55025 2.12132 7.37868L8 1.5"
stroke="white"
strokeWidth="1.5"
/>
<path
d="M16 9.72266L8 9.72266"
stroke="white"
strokeWidth="1.5"
className="hidden group-hover:block"
/>
</svg>
<Heading as="span" size="small">
Til toppen
</Heading>
</button>
);
};
export default ScrollTop;
@@ -1,100 +0,0 @@
import { Link } from "@navikt/ds-react";
import NextLink from "next/link";
import { logNav } from "../";
const tools = [
/* {
title: "Identitet",
description: "Åpen for alle",
href: "https://identitet.nav.no/",
icon: (
<svg
className="h-7 w-7"
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
focusable="false"
aria-hidden="true"
>
<path
fill="currentColor"
d="M19.25 27.4 21.05 21.65 16.3 17.95H22.1L24 12L25.85 17.95H31.7L26.95 21.65L28.7 27.4L24 23.85ZM12.2 46V30.8Q9.95 28.45 8.975 25.65Q8 22.85 8 20Q8 13.2 12.6 8.6Q17.2 4 24 4Q30.8 4 35.4 8.6Q40 13.2 40 20Q40 22.85 39.025 25.65Q38.05 28.45 35.8 30.8V46L24 42.05ZM24 33Q29.45 33 33.225 29.225Q37 25.45 37 20Q37 14.55 33.225 10.775Q29.45 7 24 7Q18.55 7 14.775 10.775Q11 14.55 11 20Q11 25.45 14.775 29.225Q18.55 33 24 33ZM15.2 41.8 24 39.05 32.8 41.8V33.25Q30.8 34.7 28.5 35.35Q26.2 36 24 36Q21.8 36 19.5 35.35Q17.2 34.7 15.2 33.25ZM24 37.5Q24 37.5 24 37.5Q24 37.5 24 37.5Q24 37.5 24 37.5Q24 37.5 24 37.5Z"
/>
</svg>
),
}, */
{
title: "Security Playbook",
description: "Åpen for alle",
href: "https://sikkerhet.nav.no/",
icon: (
<svg
className="h-7 w-7"
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
focusable="false"
aria-hidden="true"
>
<path
fill="currentColor"
d="M24 44Q17 42.25 12.5 35.875Q8 29.5 8 21.9V10L24 4L40 10V21.9Q40 29.5 35.5 35.875Q31 42.25 24 44ZM24 40.9Q29.3 39.15 32.775 34.475Q36.25 29.8 36.85 24H24V7.25L11 12.1V21.9Q11 22.5 11.025 22.925Q11.05 23.35 11.15 24H24Z"
/>
</svg>
),
},
{
title: "Etterlevelse",
description: "Kun for ansatte",
href: "https://etterlevelse.intern.nav.no/",
icon: (
<svg
className="h-7 w-7"
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
focusable="false"
aria-hidden="true"
>
<path
fill="currentColor"
d="M17.3 45 13.5 38.5 5.95 36.95 6.8 29.6 2 24 6.8 18.45 5.95 11.1 13.5 9.55 17.3 3 24 6.1 30.7 3 34.55 9.55 42.05 11.1 41.2 18.45 46 24 41.2 29.6 42.05 36.95 34.55 38.5 30.7 45 24 41.9ZM24 24ZM21.85 30.65 33.2 19.4 30.95 17.35 21.85 26.35 17.1 21.4 14.8 23.65ZM18.65 41.05 24 38.8 29.5 41.05 32.85 36.05 38.7 34.55 38.1 28.6 42.15 24 38.1 19.3 38.7 13.35 32.85 11.95 29.4 6.95 24 9.2 18.5 6.95 15.15 11.95 9.3 13.35 9.9 19.3 5.85 24 9.9 28.6 9.3 34.65 15.15 36.05Z"
/>
</svg>
),
},
];
export const ToolCard = () => {
return (
<ul className="mx-auto flex w-full max-w-screen-lg flex-col items-center justify-center gap-16 rounded-2xl bg-[#99F6E4] px-2 py-12 md:flex-row md:justify-evenly md:gap-4">
{tools.map((t) => (
<li key={t.title} className="flex w-56 items-center gap-4 md:w-fit">
<div className="rotate-45 rounded-lg bg-[#5EEAD4] p-3">
<div className="-rotate-45">{t.icon}</div>
</div>
<div>
<NextLink href={t.href} passHref legacyBehavior>
<Link
className="text-text-default mb-1 font-semibold"
onClick={(e) =>
logNav(
"verktøy",
window.location.pathname,
e.currentTarget.getAttribute("href")
)
}
>
{t.title}
</Link>
</NextLink>
<div>{t.description}</div>
</div>
</li>
))}
</ul>
);
};
@@ -7,12 +7,13 @@ export const withDsExample = (
/**
* Static: Used for dynamic-height examples like ExpansionCard
*/
variant?: "full" | "inverted" | "static"
variant?: "full" | "inverted" | "static" | "subtle"
) => {
const DsHOC = (props: any) => (
<div
className={cl({
"bg-gray-900": variant === "inverted",
"bg-bg-subtle": variant === "subtle",
[styles.containerStatic]: variant === "static",
[styles.container]: variant !== "static",
})}
@@ -2,7 +2,7 @@ import { useMedia } from "@/utils";
import * as Icons from "@navikt/aksel-icons";
import meta from "@navikt/aksel-icons/metadata";
import { Heading, Modal, Search, ToggleGroup } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import Footer from "components/layout/footer/Footer";
import { Header } from "components/layout/header/Header";
import { SuggestionBlock } from "components/website-modules/suggestionblock";
@@ -1,6 +1,6 @@
import { LightBulbIcon } from "@navikt/aksel-icons";
import { BodyLong, BodyShort, Button, Heading } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import { ChangeLogIconOutline } from "components/assets";
const SuggestionBlockComponent = ({
@@ -1,8 +1,8 @@
import { SearchLogT } from "@/types";
import amplitude from "amplitude-js";
import getConfig from "next/config";
import { Router } from "next/router";
import { useCallback, useEffect, useMemo } from "react";
import { track, init } from "@amplitude/analytics-browser";
const { publicRuntimeConfig } = getConfig();
export enum AmplitudeEvents {
@@ -20,13 +20,14 @@ export enum AmplitudeEvents {
}
export const initAmplitude = () => {
if (amplitude && !(typeof window === "undefined")) {
amplitude.getInstance().init("1a9a84a5e557ac9635a250bc27d75030", "", {
apiEndpoint: "amplitude.nav.no/collect",
saveEvents: false,
includeUtm: true,
includeReferrer: true,
platform: window.location.toString(),
if (!(typeof window === "undefined")) {
init("1a9a84a5e557ac9635a250bc27d75030", undefined, {
useBatch: true,
serverUrl: "https://amplitude.nav.no/collect",
defaultTracking: {
attribution: false,
},
ingestionMetadata: { sourceName: window.location.toString() },
});
}
};
@@ -66,8 +67,8 @@ const isProduction = () => {
export function logAmplitudeEvent(eventName: string, data?: any): Promise<any> {
return new Promise(function (resolve: any) {
const eventData = data ? { ...data } : {};
if (amplitude && isProduction()) {
amplitude.getInstance().logEvent(eventName, eventData, resolve);
if (isProduction()) {
track(eventName, eventData, resolve);
}
});
}
@@ -1,28 +1,4 @@
import { _checkAuth } from "@sanity/preview-kit";
import { useEffect, useState } from "react";
import { clientConfig } from "@/sanity/config";
export const useDebounce = (value: any) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timeOut = setTimeout(() => setDebouncedValue(value), 300);
return () => clearTimeout(timeOut);
}, [value]);
return debouncedValue;
};
export const useCheckAuth = () => {
const [user, setUser] = useState<boolean>(true);
useEffect(() => {
_checkAuth(clientConfig.projectId, null).then(setUser);
}, []);
return user;
};
export const useMedia = (media: string): boolean => {
const [isActive, setIsActive] = useState(false);
@@ -0,0 +1,16 @@
import { clientConfig } from "@/sanity/config";
import { useState, useEffect } from "react";
export const useCheckAuth = () => {
const [user, setUser] = useState<boolean>(true);
useEffect(() => {
const handleAuth = async () => {
const _checkAuth = (await import("@sanity/preview-kit"))._checkAuth;
_checkAuth(clientConfig.projectId, null).then(setUser);
};
handleAuth();
}, []);
return user;
};
+4 -1
View File
@@ -6,11 +6,12 @@ test.describe("Axe a11y", () => {
for (const url of urls) {
test(`Check page ${url}`, async ({ page }) => {
await page.goto(`http://localhost:3000${url}`);
page.waitForLoadState("domcontentloaded");
await page.waitForLoadState("domcontentloaded");
const accessibilityScanResults = await new AxeBuilder({ page })
.disableRules(["definition-list", "scrollable-region-focusable"])
.exclude("iframe")
.exclude("#aksel-expansioncard")
.exclude("#toc-scroll")
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
@@ -21,4 +22,6 @@ test.describe("Axe a11y", () => {
Disabled rules:
- definition-list: 'div' as a direct child of 'dl' should be valid. Ignoring failed test.
- scrollable-region-focusable: Up for discussion. Should code-block be focusable fot easier access and allowing scroll with keyboard?
- aksel-expansioncard: Errors with duplicate label-text
- toc-scroll: Axe does not wait for fadein animation to finish before scanning. This causes false positives for low contrast.
*/
+28
View File
@@ -0,0 +1,28 @@
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test("test", async ({ page }) => {
await page.goto("http://localhost:3000/");
await page.waitForLoadState("networkidle");
await page.waitForTimeout(3000);
await page.getByRole("button", { name: "Søk" }).click();
const accessibilityScanResults = await new AxeBuilder({ page })
.disableRules(["definition-list", "scrollable-region-focusable"])
.exclude("iframe")
.exclude("#aksel-expansioncard")
.exclude("#toc-scroll")
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
await page.getByPlaceholder("Søk gjennom hele aksel").click();
await page.getByPlaceholder("Søk gjennom hele aksel").fill("button");
const SearchHitsScan = await new AxeBuilder({ page })
.disableRules(["definition-list", "scrollable-region-focusable"])
.exclude("iframe")
.exclude("#aksel-expansioncard")
.exclude("#toc-scroll")
.analyze();
expect(SearchHitsScan.violations).toEqual([]);
});
+1 -1
View File
@@ -5,7 +5,7 @@ test.describe("Smoketest all pages", () => {
for (const url of urls) {
test(`Check page ${url}`, async ({ page }) => {
await page.goto(`http://localhost:3000${url}`);
page.waitForLoadState("domcontentloaded");
await page.waitForLoadState("domcontentloaded");
expect(await page.locator(".vk-errorboundary").count()).toEqual(0);
expect(await page.locator(".should-not-be-found").count()).toEqual(0);
+1 -1
View File
@@ -5,7 +5,7 @@ test.describe("Check for errors in all pages", () => {
for (const url of urls) {
test(`Check page ${url}`, async ({ page }) => {
await page.goto(`http://localhost:3000${url}`);
page.waitForLoadState("domcontentloaded");
await page.waitForLoadState("domcontentloaded");
expect(await page.locator(".vk-errorboundary").count()).toEqual(0);
expect(await page.locator(".should-not-be-found").count()).toEqual(0);
+4 -3
View File
@@ -7,9 +7,10 @@
"scripts": {
"dev": "next | pino-pretty",
"build:old": "next build",
"build": "next build && yarn cp:assets",
"build": "next build && yarn update:searchindex && yarn cp:assets",
"start": "cd .next/standalone/aksel.nav.no/website && node server.js",
"cp:assets": "cross-env cp -R public .next/standalone/aksel.nav.no/website/ && cross-env cp -R .next/static .next/standalone/aksel.nav.no/website/.next/static",
"update:searchindex": "ts-node --project tsconfig.json ./scripts/create-index",
"e2e": "yarn build && NEXT_PUBLIC_TEST=true yarn start-server-and-test start http://localhost:3000 playwright",
"e2e:full": "yarn build && ts-node ./e2e/genUrls.ts && NEXT_PUBLIC_TEST=true FULL_TEST=true yarn start-server-and-test start http://localhost:3000 playwright",
"e2e:ci": "NEXT_PUBLIC_TEST=true yarn start-server-and-test start http://localhost:3000 playwright",
@@ -26,6 +27,7 @@
"watch:tw": "yarn tailwindcss -i ./styles/index.css -o ./dist/tw.css --watch"
},
"dependencies": {
"@amplitude/analytics-browser": "^2.0.0",
"@sanity/block-content-to-react": "^3.0.0",
"@sanity/client": "^6.1.3",
"@sanity/code-input": "4.1.1",
@@ -37,7 +39,6 @@
"@sanity/ui": "^1.3.0",
"@sanity/vision": "3.12.1",
"@svgr/webpack": "^5.5.0",
"amplitude-js": "^8.21.5",
"boring-avatars": "1.7.0",
"clsx": "^1.2.1",
"codesandbox": "^2.2.3",
@@ -70,7 +71,7 @@
"@babel/core": "^7.19.0",
"@next/bundle-analyzer": "^12.1.0",
"@next/eslint-plugin-next": "^12.2.0",
"@playwright/test": "1.33.0",
"@playwright/test": "1.35.1",
"@testing-library/jest-dom": "^5.16.0",
"@testing-library/react": "^13.3.0",
"@types/jest": "^29.0.0",
+15 -13
View File
@@ -1,5 +1,3 @@
import { Provider } from "@navikt/ds-react";
import PreviewBanner from "components/website-modules/PreviewBanner";
import {
initAmplitude,
usePageView,
@@ -9,10 +7,18 @@ import Head from "next/head";
import { Router } from "next/router";
import { useEffect } from "react";
import { IdContext } from "../components/website-modules/utils/contexts/id-context";
import dynamic from "next/dynamic";
import "../styles/index.css";
initAmplitude();
export const PreviewBanner = dynamic(
() => import("components/website-modules/PreviewBanner"),
{
ssr: false,
}
);
function App({
Component,
pageProps,
@@ -26,10 +32,8 @@ function App({
usePageView(router, pageProps);
useEffect(() => {
if (window.location.host === "design.nav.no") {
window.location.host === "design.nav.no" &&
window.location.replace(`http://aksel.nav.no`);
return;
}
}, []);
return (
@@ -38,7 +42,9 @@ function App({
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
<meta
property="og:url"
content={`https://aksel.nav.no${router.asPath.split("?")[0]}`}
content={`https://aksel.nav.no${
router.asPath.split("?")[0].split("#")[0]
}`}
key="ogurl"
/>
<link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml" />
@@ -52,13 +58,9 @@ function App({
</Head>
{pageProps?.preview && <PreviewBanner />}
<Provider>
<IdContext.Provider
value={{ id: pageProps?.id ?? pageProps?.page?._id }}
>
<Component {...pageProps} />
</IdContext.Provider>
</Provider>
<IdContext.Provider value={{ id: pageProps?.id ?? pageProps?.page?._id }}>
<Component {...pageProps} />
</IdContext.Provider>
</>
);
}
+6
View File
@@ -21,6 +21,12 @@ export default function Document() {
type="font/woff2"
crossOrigin="anonymous"
/>
<link
rel="preload"
href="/searchindex.json"
as="fetch"
crossOrigin="anonymous"
/>
</Head>
<body data-theme="light" className="aksel antialiased">
<Main />
@@ -1,303 +0,0 @@
import {
FuseHitsT,
FuseItemT,
GroupedHitsT,
IconPageItemT,
SearchHitT,
SearchResultsT,
} from "@/types";
import { omit } from "@navikt/ds-react";
import Fuse from "fuse.js";
import { NextApiRequest, NextApiResponse } from "next";
import { allArticleDocuments } from "../../../../sanity/config";
import IconMetadata from "@navikt/aksel-icons/metadata";
import { logger } from "logger";
const token = process.env.SANITY_PRIVATE_NO_DRAFTS;
const iconPageData: IconPageItemT = {
_type: "icon_page",
heading: "Ikoner",
description: "800+ open source ikoner laget av Aksel.",
keywords: [
"ikoner",
"ikon",
"icon",
"icons",
"Aksel icons",
"ikonpakke",
"svg",
],
};
export default async function initialSearch(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== "GET") {
return res.status(405).json({ message: "Method not allowed" });
}
let doc;
if (req.query?.doc) {
const queryDoc = Array.isArray(req.query.doc)
? req.query.doc.join("")
: req.query.doc;
doc = queryDoc.split(",");
} else {
doc = [...allArticleDocuments, "icon", "icon_page"];
}
const query = Array.isArray(req.query.q)
? req.query.q.join(" ")
: req.query.q;
const hits = [
...(await searchSanity()),
...Object.values(IconMetadata).map((icon) => ({ ...icon, _type: "icon" })),
iconPageData,
];
const result: FuseHitsT[] = getSearchResults(
hits.map((x) => ({
...x,
...(x?.content ? { content: x.content.replace(/\n|\r/g, " ") } : {}),
})),
query
) as unknown as FuseHitsT[];
const filteredResult: FuseHitsT[] = getSearchResults(
hits
.filter((x) => doc.includes(x._type))
.map((x) => ({
...x,
...(x?.content ? { content: x.content.replace(/\n|\r/g, " ") } : {}),
})),
query
) as unknown as FuseHitsT[];
const formatedResults = formatResults(filteredResult, query);
const groupedHits: GroupedHitsT = formatedResults?.reduce(
(prev, cur: SearchHitT) => {
if (cur.item._type in prev) {
return { ...prev, [cur.item._type]: [...prev[cur.item._type], cur] };
} else {
return { ...prev, [cur.item._type]: [cur] };
}
},
{}
);
const topResults = formatResults(
filteredResult.slice(0, 3).filter((x) => x.score < 0.1),
query
);
const response: SearchResultsT = {
groupedHits,
topResults: filteredResult?.length > 8 ? topResults : [],
totalHits: filteredResult?.length ?? 0,
hits: {
icon_page: result.filter((x: any) => x.item._type === "icon_page").length,
icon: result.filter((x: any) => x.item._type === "icon").length,
komponent_artikkel: result.filter(
(x: any) => x.item._type === "komponent_artikkel"
).length,
aksel_artikkel: result.filter(
(x: any) => x.item._type === "aksel_artikkel"
).length,
ds_artikkel: result.filter((x: any) => x.item._type === "ds_artikkel")
.length,
aksel_blogg: result.filter((x: any) => x.item._type === "aksel_blogg")
.length,
aksel_prinsipp: result.filter(
(x: any) => x.item._type === "aksel_prinsipp"
).length,
aksel_standalone: result.filter(
(x: any) => x.item._type === "aksel_prinsipp"
).length,
},
};
return res.status(200).json(response);
}
function findRelevantQuery(q: string) {
return q.split(" ")[0].toLowerCase();
}
function formatResults(res: FuseHitsT[], query: string): SearchHitT[] {
return res.map((x) => {
let hightlightDesc = !!x.matches[0].indices
.map((y) => x.matches[0].value.slice(y[0], y[1] + 1))
.filter((x) => x.toLowerCase().includes(findRelevantQuery(query)));
let description = "";
if (
x.matches[0].key === "heading" &&
x.item._type !== "icon" &&
x.item._type !== "icon_page"
) {
hightlightDesc = false;
description = x?.item.intro ?? x.item.ingress;
const clampDesc = description?.length > 120;
description &&= description.slice(0, 120);
clampDesc && (description += "...");
} else {
const value = x.matches[0].value;
const idx = value.toLowerCase().indexOf(findRelevantQuery(query));
const clampBefore = Math.max(idx - 20, 0) === 0;
const clampAfter = Math.min(idx + 20, value.length) === value.length;
const slice = value.slice(
Math.max(idx - 45, 0),
Math.min(idx + 45, value.length)
);
let str = "";
!clampBefore && (str += "...");
str += slice;
!clampAfter && (str += "...");
description = str;
if (x.matches[0].key === "codeExamples") {
description = `Kodedemo ${description}`;
}
}
return omit(
{
...x,
item: omit(x.item, ["content", "intro", "ingress"]) as Omit<
FuseItemT,
"content" | "ingress" | "intro"
>,
highlight: { shouldHightlight: hightlightDesc, description },
},
["matches"]
) as SearchHitT;
});
}
let data = null;
async function searchSanity() {
const sanityQueryHttp = `*[_type in [${allArticleDocuments.map(
(x) => `"${x}"`
)}] ]{
_id,
heading,
_createdAt,
_updatedAt,
publishedAt,
updateInfo,
"slug": slug.current,
"tema": tema[]->title,
ingress,
status,
_type,
"intro": pt::text(intro.body),
"content": content[]{...,
_type == "kode_eksempler" => {
...dir->{"eksempler": filer[].navn},
}},
}`;
data = await fetch(
`https://hnbe3yhs.apicdn.sanity.io/v2021-06-07/data/query/production?query=${encodeURIComponent(
sanityQueryHttp
)}`,
{
headers: {
"Content-type": "application/json",
Authorization: `Bearer ${token}`,
},
}
)
.then((r) => r.json())
.then((res) => {
return (
res?.result?.map((x) => ({
...x,
contentHeadings: getHeadings(x?.content),
content: toPlainText(x?.content),
codeExamples: getCodeExamples(x?.content),
})) ?? []
);
})
.catch((err) => {
logger.error(err);
return [];
});
return data;
}
function getSearchResults(results, query) {
/* https://fusejs.io/api/options.html */
const fuse = new Fuse<SearchHitT>(results, {
keys: [
{ name: "heading", weight: 100 },
{ name: "contentHeadings", weight: 70 },
{ name: "ingress", weight: 50 },
{ name: "intro", weight: 50 },
{ name: "content", weight: 10 },
{ name: "tema", weight: 20 },
{ name: "codeExamples", weight: 40 },
// Icons
{ name: "name", weight: 70 },
{ name: "category", weight: 40 },
{ name: "sub_category", weight: 30 },
{ name: "keywords", weight: 10 },
// Icon page
{ name: "description", weight: 50 },
],
includeScore: true,
shouldSort: true,
minMatchCharLength: 3,
includeMatches: true,
ignoreLocation: true,
threshold: 0.2,
distance: 6000,
});
return fuse.search(query).filter((x) => x.score < 0.3);
}
function getHeadings(blocks: any[]) {
if (!blocks || blocks.length === 0) {
return "";
}
return blocks.filter((x) => x.style === "h2").map((x) => x.children[0].text);
}
function toPlainText(blocks: any[]) {
if (!blocks || blocks.length === 0) {
return "";
}
return blocks
.map((block) => {
if (block._type !== "block" || !block.children) {
return "";
}
return block.children.map((child) => child.text).join("");
})
.join("\n\n");
}
function getCodeExamples(blocks: any[]): string[] {
if (!blocks || blocks.length === 0) {
return [];
}
const examples = [];
blocks
.filter((block) => block._type === "kode_eksempler")
.map((x) => examples.push(...x.eksempler));
return examples;
}
@@ -0,0 +1,30 @@
import { Alert } from "@navikt/ds-react";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return (
<div className="grid gap-4">
<Alert variant="info">
Hvis du er mellom 62 og 67 år når du søker, må du som hovedregel ha hatt
en pensjonsgivende inntekt som tilsvarer x G, året før du fikk nedsatt
arbeidsevnen.
</Alert>
<Alert variant="success">Søknad er sendt inn!</Alert>
<Alert variant="warning">
Du må være registrert hos NAV for å bruke aktivitetsplanen.
</Alert>
<Alert variant="error">Noe gikk galt! Prøv igjen om noen minutter.</Alert>
</div>
);
};
export default withDsExample(Example);
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 0,
};
@@ -1,23 +0,0 @@
import { Alert } from "@navikt/ds-react";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return (
<Alert variant="error">
Hvis du er mellom 62 og 67 år når du søker, må du som hovedregel ha hatt
en pensjonsgivende inntekt som tilsvarer x G, året før du fikk nedsatt
arbeidsevnen.
</Alert>
);
};
export default withDsExample(Example);
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 3,
};
@@ -3,11 +3,22 @@ import { withDsExample } from "components/website-modules/examples/withDsExample
const Example = () => {
return (
<Alert variant="info" fullWidth>
Hvis du er mellom 62 og 67 år når du søker, må du som hovedregel ha hatt
en pensjonsgivende inntekt som tilsvarer x G, året før du fikk nedsatt
arbeidsevnen.
</Alert>
<div className="grid gap-4">
<Alert fullWidth variant="info">
Hvis du er mellom 62 og 67 år når du søker, må du som hovedregel ha hatt
en pensjonsgivende inntekt som tilsvarer x G, året før du fikk nedsatt
arbeidsevnen.
</Alert>
<Alert fullWidth variant="success">
Søknad er sendt inn!
</Alert>
<Alert fullWidth variant="warning">
Du må være registrert hos NAV for å bruke aktivitetsplanen.
</Alert>
<Alert fullWidth variant="error">
Noe gikk galt! Prøv igjen om noen minutter.
</Alert>
</div>
);
};
@@ -19,5 +30,5 @@ export const Demo = {
};
export const args = {
index: 5,
index: 2,
};
@@ -1,23 +0,0 @@
import { Alert } from "@navikt/ds-react";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return (
<Alert variant="info">
Hvis du er mellom 62 og 67 år når du søker, må du som hovedregel ha hatt
en pensjonsgivende inntekt som tilsvarer x G, året før du fikk nedsatt
arbeidsevnen.
</Alert>
);
};
export default withDsExample(Example);
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 0,
};
@@ -3,11 +3,22 @@ import { withDsExample } from "components/website-modules/examples/withDsExample
const Example = () => {
return (
<Alert variant="info" inline>
Hvis du er mellom 62 og 67 år når du søker, må du som hovedregel ha hatt
en pensjonsgivende inntekt som tilsvarer x G, året før du fikk nedsatt
arbeidsevnen.
</Alert>
<div className="grid gap-4">
<Alert inline variant="info">
Hvis du er mellom 62 og 67 år når du søker, må du som hovedregel ha hatt
en pensjonsgivende inntekt som tilsvarer x G, året før du fikk nedsatt
arbeidsevnen.
</Alert>
<Alert inline variant="success">
Søknad er sendt inn!
</Alert>
<Alert inline variant="warning">
Du må være registrert hos NAV for å bruke aktivitetsplanen.
</Alert>
<Alert inline variant="error">
Noe gikk galt! Prøv igjen om noen minutter.
</Alert>
</div>
);
};
@@ -19,5 +30,5 @@ export const Demo = {
};
export const args = {
index: 6,
index: 3,
};
@@ -22,5 +22,5 @@ export const Demo = {
};
export const args = {
index: 7,
index: 4,
};
@@ -3,11 +3,22 @@ import { withDsExample } from "components/website-modules/examples/withDsExample
const Example = () => {
return (
<Alert variant="info" size="small">
Hvis du er mellom 62 og 67 år når du søker, må du som hovedregel ha hatt
en pensjonsgivende inntekt som tilsvarer x G, året før du fikk nedsatt
arbeidsevnen.
</Alert>
<div className="grid gap-4">
<Alert size="small" variant="info">
Hvis du er mellom 62 og 67 år når du søker, må du som hovedregel ha hatt
en pensjonsgivende inntekt som tilsvarer x G, året før du fikk nedsatt
arbeidsevnen.
</Alert>
<Alert size="small" variant="success">
Søknad er sendt inn!
</Alert>
<Alert size="small" variant="warning">
Du må være registrert hos NAV for å bruke aktivitetsplanen.
</Alert>
<Alert size="small" variant="error">
Noe gikk galt! Prøv igjen om noen minutter.
</Alert>
</div>
);
};
@@ -19,5 +30,5 @@ export const Demo = {
};
export const args = {
index: 4,
index: 1,
};
@@ -1,23 +0,0 @@
import { Alert } from "@navikt/ds-react";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return (
<Alert variant="success">
Hvis du er mellom 62 og 67 år når du søker, må du som hovedregel ha hatt
en pensjonsgivende inntekt som tilsvarer x G, året før du fikk nedsatt
arbeidsevnen.
</Alert>
);
};
export default withDsExample(Example);
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 1,
};
@@ -1,23 +0,0 @@
import { Alert } from "@navikt/ds-react";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return (
<Alert variant="warning">
Hvis du er mellom 62 og 67 år når du søker, må du som hovedregel ha hatt
en pensjonsgivende inntekt som tilsvarer x G, året før du fikk nedsatt
arbeidsevnen.
</Alert>
);
};
export default withDsExample(Example);
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 2,
};
@@ -1,17 +0,0 @@
import { Button } from "@navikt/ds-react";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return <Button variant="danger">Danger</Button>;
};
export default withDsExample(Example);
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 3,
};
@@ -2,7 +2,14 @@ import { Button } from "@navikt/ds-react";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return <Button>Primary</Button>;
return (
<div className="flex flex-wrap gap-2">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
<Button variant="danger">Danger</Button>
</div>
);
};
export default withDsExample(Example);
@@ -14,5 +21,4 @@ export const Demo = {
export const args = {
index: 0,
desc: "Brukes som primærhandling i løsningen. Som regel ønsker man å ha kun én primærknapp per side.",
};
@@ -2,7 +2,22 @@ import { Button } from "@navikt/ds-react";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return <Button disabled>Disabled</Button>;
return (
<div className="flex flex-wrap gap-2">
<Button disabled variant="primary">
Primary
</Button>
<Button disabled variant="secondary">
Secondary
</Button>
<Button disabled variant="tertiary">
Tertiary
</Button>
<Button disabled variant="danger">
Danger
</Button>
</div>
);
};
export default withDsExample(Example);
@@ -13,5 +28,5 @@ export const Demo = {
};
export const args = {
index: 7,
index: 20,
};
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff. Show more