Compare commits

..
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] f506dca7e1 Version Packages (#2485)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-17 12:53:01 +01:00
Torkjel Skjetne a44b2e2e7c Ikke rendre child som ikke er valid element i ErrorSummary (#2484)
Dette gjør det mulig å conditionally rendre barn i ErrorSummary. For eksempel kan man nå gjøre:

```
const shouldShow = false

return (
  <ErrorSummary>
    {shouldShow && <ErrorSummary.Item href="#id1">Jeg er en feilmelding, men vises ikke</ErrorSummary.Item>}
    <ErrorSummary.Item href="#id2">Jeg er en feilmelding som vises!</ErrorSummary.Item>
  </ErrorSummary>
)
```
2023-11-17 12:47:08 +01:00
Karl O d96d368e7b ♻ types for @navikt/ds-tailwind (#2486) 2023-11-17 12:46:32 +01:00
Halvor Haugan d3c0052c33 Textarea: Teller flyttet ut (#2483) 2023-11-17 11:46:07 +01:00
Ken f848bea71a [aksel.nav.no] Forside, Header og Footer bruker primitives (#2478)
* ♻️ Forside logikk cleanup

* 🎨 Page-wrapper

* ♻️ Bruker primitives på forside

* 🎨 Mer bruk av bleed

* 🎨 Footer bruker primitives

* 🎨 restrukturert header

* 🎨 Restrukturert Footer

* ♻️ Imports i footer

* ♻️ Header struktur

* ♻️ footer-struktur
2023-11-17 09:49:43 +01:00
Ken 1585106c55 [Aksel.nav.no] Fikser preview for Aksel-sider (#2481)
* WIP preview resolving

* 🐛 Fikset preview på maler med sidebar

* 🎨 Restrukturert preview-provider

* 🚚 Flyttet inn i parts

* 🎨 Bedre resolving av nested types

* 🎨 Better documentation

* 🎨 Bruker nye resolvers for preview

* 🐛 Henter nå nested data

* ✅ La til tester
2023-11-17 09:49:15 +01:00
Ken 5ed06e19fe 📝 Eksempler for Page-komponent (#2471)
* 📝 Eksempler for Page-komponent

* 🐛 Fikset import aliasing

* 📝 Refactor tekster

* 🐛 OverridableComponent fungerer nå med typeof

* 📝 changeset

* 🔥 Undo commit

* Rename belowfold.tsx to below-fold.tsx
2023-11-17 09:48:32 +01:00
61 changed files with 1295 additions and 635 deletions

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# @navikt/aksel-icons
## 5.10.3
## 5.10.2
## 5.10.1
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "5.10.2",
"version": "5.10.3",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
+2
View File
@@ -1,5 +1,7 @@
# @navikt/aksel-stylelint
## 5.10.3
## 5.10.2
## 5.10.1
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "5.10.2",
"version": "5.10.3",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
"repository": {
@@ -35,8 +35,8 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^5.10.2",
"@navikt/ds-tokens": "^5.10.2",
"@navikt/ds-css": "^5.10.3",
"@navikt/ds-tokens": "^5.10.3",
"@types/jest": "^29.0.0",
"concurrently": "7.2.1",
"copyfiles": "2.4.1",
+7
View File
@@ -1,5 +1,12 @@
# @navikt/aksel
## 5.10.3
### Patch Changes
- Updated dependencies [[`d3c0052c3`](https://github.com/navikt/aksel/commit/d3c0052c3300189798d04f17c2c2892f7c304277)]:
- @navikt/ds-css@5.10.3
## 5.10.2
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "5.10.2",
"version": "5.10.3",
"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": "5.10.2",
"@navikt/ds-css": "5.10.3",
"axios": "1.6.0",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
+6
View File
@@ -1,5 +1,11 @@
# @navikt/ds-css
## 5.10.3
### Patch Changes
- :bug: Textarea: Teller flyttet ut av tekstfeltet for å unngå overlapp og misforståelser ([#2483](https://github.com/navikt/aksel/pull/2483))
## 5.10.2
### Patch Changes
+1 -15
View File
@@ -37,10 +37,6 @@
color: var(--ac-textarea-text, var(--__ac-textarea-text, var(--a-text-default)));
}
.navds-textarea--counter {
padding-bottom: var(--a-spacing-8);
}
.navds-textarea__input::placeholder {
color: var(--ac-textarea-placeholder, var(--__ac-textarea-placeholder, var(--a-text-subtle)));
}
@@ -65,19 +61,9 @@
padding: var(--a-spacing-1-alt);
}
.navds-form-field--small .navds-textarea--counter.navds-textarea__input {
padding-bottom: var(--a-spacing-7);
}
.navds-textarea__counter {
pointer-events: none;
margin-top: var(--a-spacing-05);
color: var(--ac-textarea-counter-text, var(--__ac-textarea-counter-text, var(--a-text-subtle)));
font-style: italic;
position: absolute;
text-align: left;
left: 0.0625rem;
bottom: 0.0625rem;
padding: var(--a-spacing-1) var(--a-spacing-2);
}
.navds-textarea__counter--error {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "5.10.2",
"version": "5.10.3",
"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": "^5.10.2",
"@navikt/ds-tokens": "^5.10.3",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
+14
View File
@@ -1,5 +1,19 @@
# @navikt/ds-react
## 5.10.3
### Patch Changes
- :wheelchair: Textarea: Byttet fra `aria-live` til `role=status` på telleren for bedre semantikk ([#2483](https://github.com/navikt/aksel/pull/2483))
- :bug: Textarea: Teller flyttet ut av tekstfeltet for å unngå overlapp og misforståelser ([#2483](https://github.com/navikt/aksel/pull/2483))
- ErrorSummary: Gjør det mulig å rendre ErrorSummary.Item conditionally ([#2484](https://github.com/navikt/aksel/pull/2484))
- Updated dependencies []:
- @navikt/ds-tokens@5.10.3
- @navikt/aksel-icons@5.10.3
## 5.10.2
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "5.10.2",
"version": "5.10.3",
"description": "Aksel react-components for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -38,8 +38,8 @@
},
"dependencies": {
"@floating-ui/react": "0.25.4",
"@navikt/aksel-icons": "^5.10.2",
"@navikt/ds-tokens": "^5.10.2",
"@navikt/aksel-icons": "^5.10.3",
"@navikt/ds-tokens": "^5.10.3",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"clsx": "^1.2.1",
+7 -28
View File
@@ -1,10 +1,11 @@
import cl from "clsx";
import React, { forwardRef, useState } from "react";
import { BodyShort, ErrorMessage, Label } from "../typography";
import TextareaAutosize from "../util/TextareaAutoSize";
import { FormFieldProps, useFormField } from "./useFormField";
import { ReadOnlyIcon } from "./ReadOnlyIcon";
import { omit, useId } from "../util";
import TextareaAutosize from "../util/TextareaAutoSize";
import { ReadOnlyIcon } from "./ReadOnlyIcon";
import Counter from "./TextareaCounter";
import { FormFieldProps, useFormField } from "./useFormField";
/**
* TODO: Mulighet for lokalisering av sr-only/counter text
@@ -27,7 +28,6 @@ export interface TextareaProps
defaultValue?: string;
/**
* Maximum number of character rows to display.
* @bug Internal scrolling with `maxLength` scrolls over maxLength-text
*/
maxRows?: number;
/**
@@ -50,7 +50,7 @@ export interface TextareaProps
* i18n-translations for counter-text
*/
i18n?: {
/** @default Antall tegn igjen */
/** @default tegn igjen */
counterLeft?: string;
/** @default tegn for mye */
counterTooMuch?: string;
@@ -162,14 +162,11 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
className={cl(
"navds-textarea__input",
"navds-body-short",
`navds-body-short--${size ?? "medium"}`,
{
"navds-textarea--counter": hasMaxLength,
}
`navds-body-short--${size ?? "medium"}`
)}
{...(describedBy ? { "aria-describedby": describedBy } : {})}
/>
{hasMaxLength && (
{hasMaxLength && !readOnly && !inputProps.disabled && (
<>
<span id={maxLengthId} className="navds-sr-only">
{`Tekstområde med plass til ${maxLength} tegn.`}
@@ -198,22 +195,4 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
}
);
export const Counter = ({ maxLength, currentLength, size, i18n }) => {
const difference = maxLength - currentLength;
return (
<BodyShort
className={cl("navds-textarea__counter", {
"navds-textarea__counter--error": difference < 0,
})}
aria-live={difference < 20 ? "polite" : "off"}
size={size}
>
{difference < 0
? `${Math.abs(difference)} ${i18n?.counterTooMuch ?? "tegn for mye"}`
: `${difference} ${i18n?.counterLeft ?? "tegn igjen"}`}
</BodyShort>
);
};
export default Textarea;
@@ -0,0 +1,31 @@
import cl from "clsx";
import React from "react";
import { BodyShort } from "../typography";
import type { TextareaProps } from "./Textarea";
interface Props {
maxLength: number;
currentLength: number;
size: TextareaProps["size"];
i18n: TextareaProps["i18n"];
}
const TextareaCounter = ({ maxLength, currentLength, size, i18n }: Props) => {
const difference = maxLength - currentLength;
return (
<BodyShort
className={cl("navds-textarea__counter", {
"navds-textarea__counter--error": difference < 0,
})}
role={difference < 20 ? "status" : undefined}
size={size}
>
{difference < 0
? `${Math.abs(difference)} ${i18n?.counterTooMuch ?? "tegn for mye"}`
: `${difference} ${i18n?.counterLeft ?? "tegn igjen"}`}
</BodyShort>
);
};
export default TextareaCounter;
@@ -1,4 +1,4 @@
import React, { forwardRef, HTMLAttributes } from "react";
import React, { forwardRef, HTMLAttributes, isValidElement } from "react";
import cl from "clsx";
import { Heading, BodyShort } from "../../typography";
import ErrorSummaryItem, { ErrorSummaryItemType } from "./ErrorSummaryItem";
@@ -100,7 +100,10 @@ export const ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(
</Heading>
<BodyShort as="ul" size={size} className="navds-error-summary__list">
{React.Children.map(children, (child) => {
return <li key={child?.toString()}>{child}</li>;
if (!isValidElement(child)) {
return null;
}
return <li key={child.toString()}>{child}</li>;
})}
</BodyShort>
</section>
+10 -10
View File
@@ -1,15 +1,20 @@
export {
default as ConfirmationPanel,
type ConfirmationPanelProps,
} from "./ConfirmationPanel";
export { Fieldset, FieldsetContext, type FieldsetProps } from "./Fieldset";
export { default as Select, type SelectProps } from "./Select";
export { default as Switch, type SwitchProps } from "./Switch";
export { default as TextField, type TextFieldProps } from "./TextField";
export { default as Textarea, type TextareaProps } from "./Textarea";
export {
Checkbox,
CheckboxGroup,
type CheckboxGroupProps,
type CheckboxProps,
} from "./checkbox";
export {
type ConfirmationPanelProps,
default as ConfirmationPanel,
} from "./ConfirmationPanel";
export { Combobox as UNSAFE_Combobox, type ComboboxProps } from "./combobox";
export { ErrorSummary, type ErrorSummaryProps } from "./error-summary";
export { Fieldset, FieldsetContext, type FieldsetProps } from "./Fieldset";
export {
Radio,
RadioGroup,
@@ -17,8 +22,3 @@ export {
type RadioProps,
} from "./radio";
export { Search, type SearchClearEvent, type SearchProps } from "./search";
export { Combobox as UNSAFE_Combobox, type ComboboxProps } from "./combobox";
export { default as Select, type SelectProps } from "./Select";
export { default as Switch, type SwitchProps } from "./Switch";
export { Counter, default as Textarea, type TextareaProps } from "./Textarea";
export { default as TextField, type TextFieldProps } from "./TextField";
@@ -14,7 +14,7 @@ export const Default: StoryObj<typeof Textarea> = {
},
args: {
maxLength: 100,
maxLength: 0,
label: "Ipsum enim quis culpa",
resize: false,
},
@@ -28,6 +28,7 @@ export const Default: StoryObj<typeof Textarea> = {
error: { type: "string" },
hideLabel: { type: "boolean" },
disabled: { type: "boolean" },
readOnly: { type: "boolean" },
maxRows: { type: "number" },
minRows: { type: "number" },
},
@@ -66,6 +67,20 @@ export const Error = () => {
error="Consectetur labore velit eiusmod Lorem ut nostrud mollit labore ullamco laboris laboris in."
size="small"
/>
<Textarea
label="Ipsum enim quis culpa"
error="Consectetur labore velit eiusmod Lorem ut nostrud mollit labore ullamco laboris laboris in."
maxLength={20}
/>
<Textarea
label="Ipsum enim quis culpa"
error="Consectetur labore velit eiusmod Lorem ut nostrud mollit labore ullamco laboris laboris in."
value="Sed dignissim sollicitudin porta."
maxLength={20}
size="small"
/>
</div>
);
};
+2 -1
View File
@@ -1,2 +1,3 @@
src/tokens.ts
tailwind.config.js
tailwind.config.js
!tailwind.config.d.ts
+6
View File
@@ -1,5 +1,11 @@
# @navikt/ds-tailwind
## 5.10.3
### Patch Changes
- typescript types for tailwind preset ([#2486](https://github.com/navikt/aksel/pull/2486))
## 5.10.2
## 5.10.1
+6 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "5.10.2",
"version": "5.10.3",
"description": "Tailwind config based on @navikt/ds-tokens",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -8,9 +8,11 @@
"design-tokens"
],
"main": "tailwind.config.js",
"types": "tailwind.config.d.ts",
"license": "MIT",
"files": [
"tailwind.config.js"
"tailwind.config.js",
"tailwind.config.d.ts"
],
"scripts": {
"build": "ts-node ./src",
@@ -22,11 +24,12 @@
"directory": "@navikt/core/tailwind"
},
"devDependencies": {
"@navikt/ds-tokens": "^5.10.2",
"@navikt/ds-tokens": "^5.10.3",
"@types/jest": "^29.0.0",
"color": "4.2.3",
"jest": "^29.0.0",
"lodash": "^4.17.21",
"tailwindcss": "^3.3.3",
"ts-jest": "^29.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
+5
View File
@@ -0,0 +1,5 @@
import type { Config } from "tailwindcss";
declare const preset: Partial<Config>;
export default preset;
+2
View File
@@ -1,5 +1,7 @@
# @navikt/ds-tokens
## 5.10.3
## 5.10.2
## 5.10.1
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "5.10.2",
"version": "5.10.3",
"description": "Design-tokens for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
+17 -5
View File
@@ -4,17 +4,29 @@
### @navikt/ds-react
- OverridableComponent: Fungerer nå bedre ved bruk av typeof <komponent> ([#2474](https://github.com/navikt/aksel/pull/2474))
- :wheelchair: Textarea: Byttet fra `aria-live` til `role=status` på telleren for bedre semantikk ([#2483](https://github.com/navikt/aksel/pull/2483))
* Table: Håndterer nå sjekk for interaktive elementer i ExpandableRow bedre ([#2461](https://github.com/navikt/aksel/pull/2461))
- :bug: Textarea: Teller flyttet ut av tekstfeltet for å unngå overlapp og misforståelser ([#2483](https://github.com/navikt/aksel/pull/2483))
- Primitives: Ny komponent `Page` og `Page.Block` ([#2456](https://github.com/navikt/aksel/pull/2456))
- ErrorSummary: Gjør det mulig å rendre ErrorSummary.Item conditionally ([#2484](https://github.com/navikt/aksel/pull/2484))
* OverridableComponent: Fungerer nå bedre ved bruk av typeof <komponent> ([#2474](https://github.com/navikt/aksel/pull/2474))
- Table: Håndterer nå sjekk for interaktive elementer i ExpandableRow bedre ([#2461](https://github.com/navikt/aksel/pull/2461))
* Primitives: Ny komponent `Page` og `Page.Block` ([#2456](https://github.com/navikt/aksel/pull/2456))
### @navikt/ds-tailwind
- typescript types for tailwind preset ([#2486](https://github.com/navikt/aksel/pull/2486))
### @navikt/ds-css
- Primtives: CSS-import for primitives er nå mer spesifikk ([#2469](https://github.com/navikt/aksel/pull/2469))
- :bug: Textarea: Teller flyttet ut av tekstfeltet for å unngå overlapp og misforståelser ([#2483](https://github.com/navikt/aksel/pull/2483))
* Primitives: Ny komponent `Page` og `Page.Block` ([#2456](https://github.com/navikt/aksel/pull/2456))
* Primtives: CSS-import for primitives er nå mer spesifikk ([#2469](https://github.com/navikt/aksel/pull/2469))
- Primitives: Ny komponent `Page` og `Page.Block` ([#2456](https://github.com/navikt/aksel/pull/2456))
## 5.9
+5 -5
View File
@@ -16,11 +16,11 @@
"playroom"
],
"dependencies": {
"@navikt/aksel-icons": "^5.10.2",
"@navikt/ds-css": "^5.10.2",
"@navikt/ds-react": "^5.10.2",
"@navikt/ds-tailwind": "^5.10.2",
"@navikt/ds-tokens": "^5.10.2"
"@navikt/aksel-icons": "^5.10.3",
"@navikt/ds-css": "^5.10.3",
"@navikt/ds-react": "^5.10.3",
"@navikt/ds-tailwind": "^5.10.3",
"@navikt/ds-tokens": "^5.10.3"
},
"devDependencies": {
"prettier-plugin-tailwindcss": "^0.2.3"
+70 -1
View File
@@ -1,7 +1,7 @@
import { loadCsf } from "@storybook/csf-tools";
import type { StorybookConfig } from "@storybook/nextjs";
import { readFileSync } from "fs";
import { dirname, join } from "path";
import { dirname, join, resolve } from "path";
/**
* This function is used to resolve the absolute path of a package.
@@ -65,5 +65,74 @@ const config: StorybookConfig = {
docs: {
autodocs: "tag",
},
webpackFinal: async (sbConfig: any) => {
if (!sbConfig.resolve.alias) {
return sbConfig;
}
// Add path aliases
sbConfig.resolve.alias["@/sb-util"] = resolve(__dirname, "./utils.tsx");
sbConfig.resolve.alias["@/types"] = resolve(
__dirname,
"../components/types/index.ts"
);
sbConfig.resolve.alias["@/sanity-block"] = resolve(
__dirname,
"../components/website-modules/SanityBlockContent.tsx"
);
sbConfig.resolve.alias["@/hooks"] = resolve(
__dirname,
"../components/hooks/"
);
sbConfig.resolve.alias["@/logging"] = resolve(
__dirname,
"../components/logging/index.ts"
);
sbConfig.resolve.alias["@/utils"] = resolve(
__dirname,
"../components/utils/index.ts"
);
sbConfig.resolve.alias["@/utils"] = resolve(
__dirname,
"../components/utils/index.ts"
);
sbConfig.resolve.alias["@/layout"] = resolve(
__dirname,
"../components/layout/"
);
sbConfig.resolve.alias["@/assets"] = resolve(
__dirname,
"../components/assets/"
);
sbConfig.resolve.alias["@/web"] = resolve(
__dirname,
"../components/website-modules/"
);
sbConfig.resolve.alias["@/cms"] = resolve(
__dirname,
"../components/sanity-modules/"
);
sbConfig.resolve.alias["@/error-boundary"] = resolve(
__dirname,
"../components/website-modules/ErrorBoundary.tsx"
);
sbConfig.resolve.alias["@/sanity/config"] = resolve(
__dirname,
"../sanity/config.ts"
);
sbConfig.resolve.alias["@/sanity/client.server"] = resolve(
__dirname,
"../sanity/interface/client.server.js"
);
sbConfig.resolve.alias["@/sanity/interface"] = resolve(
__dirname,
"../sanity/interface/interface.ts"
);
sbConfig.resolve.alias["@/sanity/queries"] = resolve(
__dirname,
"../sanity/interface/queries.js"
);
return sbConfig;
},
};
export default config;
@@ -1,8 +1,8 @@
import { FigmaIcon, GithubIcon, SlackIcon } from "@/assets/Icons";
import AkselLogo from "@/assets/Logo";
import { amplitudeLogNavigation } from "@/logging";
import { BodyShort, Heading, Link } from "@navikt/ds-react";
import { Box, HGrid, Page, VStack } from "@navikt/ds-react";
import dynamic from "next/dynamic";
import LinkBlock from "./parts/LinkBlock";
export const EditButton = dynamic(() => import("@/web/EditButton"), {
ssr: false,
@@ -10,116 +10,83 @@ export const EditButton = dynamic(() => import("@/web/EditButton"), {
const Footer = () => {
return (
<footer
id="aksel-footer"
data-hj-suppress
<Box
as="footer"
data-theme="dark"
className="toc-ignore text-text-on-inverted bg-deepblue-800 relative flex w-full justify-center"
id="aksel-footer"
className="relative"
background="surface-alt-3-strong"
paddingBlock="12 16"
>
<div className="relative z-10 mx-auto grid w-full max-w-screen-2xl gap-12 px-4 pb-16 pt-12 md:grid-cols-2 md:px-6 lg:grid-cols-4 xl:grid-cols-4 xl:gap-6">
<LogoBlock />
<Snarveier />
<SideLenker />
<Kontakt />
</div>
<EditButton />
</footer>
<Page.Block width="2xl" gutters>
<HGrid columns={{ xs: 1, md: 2, lg: 4 }} gap={{ xs: "12", xl: "6" }}>
<VStack gap="4">
<AkselLogo className="fill-white" />
<div>
<p>&copy; {new Date().getFullYear()} NAV</p>
<p>Arbeids- og velferdsetaten</p>
</div>
</VStack>
<LinkBlock
heading="Snarveier"
links={[
{
url: "/god-praksis/artikler/skriv-for-aksel",
text: "Skriv for Aksel",
},
{
url: "/prinsipper/brukeropplevelse",
text: "Prinsipper for brukeropplevelse",
},
{ url: "https://sikkerhet.nav.no/", text: "Security Playbook" },
{
url: "https://etterlevelse.intern.nav.no/",
text: "Etterlevelse",
},
]}
/>
<LinkBlock
heading="Om nettstedet"
links={[
{
url: "/god-praksis/artikler/om-aksel",
text: "Hva er Aksel?",
},
{
url: "/side/personvernerklaering",
text: "Personvernerklæring",
},
{
url: "/side/tilgjengelighetserklaring-for-aksel",
text: "Tilgjengelighetserklæring",
},
]}
/>
<LinkBlock
heading="Finn oss"
links={[
{
url: "https://www.figma.com/@nav_aksel",
text: "Figma",
icon: <FigmaIcon />,
},
{
url: "https://github.com/navikt/aksel",
text: "Github",
icon: <GithubIcon />,
},
{
url: "https://nav-it.slack.com/archives/C7NE7A8UF",
text: "Slack",
icon: <SlackIcon />,
},
]}
/>
</HGrid>
</Page.Block>
</Box>
);
};
function LogoBlock() {
return (
<div>
<AkselLogo className="fill-white" />
<p className="mt-4">&copy; {new Date().getFullYear()} NAV</p>
<p>Arbeids- og velferdsetaten</p>
</div>
);
}
function Snarveier() {
return (
<div>
<Heading level="2" size="xsmall">
Snarveier
</Heading>
<BodyShort as="ul" className="mt-3 grid gap-3">
<FooterLink href="/god-praksis/artikler/skriv-for-aksel">
Skriv for Aksel
</FooterLink>
<FooterLink href="/prinsipper/brukeropplevelse">
Prinsipper for brukeropplevelse
</FooterLink>
{/* <FooterLink href="https://identitet.nav.no/">Identitet</FooterLink> */}
<FooterLink href="https://sikkerhet.nav.no/">
Security Playbook
</FooterLink>
<FooterLink href="https://etterlevelse.intern.nav.no/">
Etterlevelse
</FooterLink>
</BodyShort>
</div>
);
}
function Kontakt() {
return (
<div>
<Heading level="2" size="xsmall">
Finn oss
</Heading>
<BodyShort as="ul" className="mt-3 grid gap-3">
<FooterLink href="https://www.figma.com/@nav_aksel">
<FigmaIcon />
Figma
</FooterLink>
<FooterLink href="https://github.com/navikt/aksel">
<GithubIcon />
Github
</FooterLink>
<FooterLink href="https://nav-it.slack.com/archives/C7NE7A8UF">
<SlackIcon />
Slack
</FooterLink>
</BodyShort>
</div>
);
}
function SideLenker() {
return (
<div>
<Heading level="2" size="xsmall">
Om nettstedet
</Heading>
<BodyShort as="ul" className="mt-3 grid gap-3">
<FooterLink href="/god-praksis/artikler/om-aksel">
Hva er Aksel?
</FooterLink>
<FooterLink href="/side/personvernerklaering">
Personvernerklæring
</FooterLink>
<FooterLink href="/side/tilgjengelighetserklaring-for-aksel">
Tilgjengelighetserklæring
</FooterLink>
</BodyShort>
</div>
);
}
function FooterLink({ children, href }) {
return (
<li>
<Link
className="text-text-on-inverted focus:shadow-focus focus:text-text-default flex w-fit items-center gap-1 underline hover:no-underline focus:bg-blue-200 focus:shadow-blue-200"
href={href}
onClick={(e) =>
amplitudeLogNavigation("footer", e.currentTarget.getAttribute("href"))
}
>
{children}
</Link>
</li>
);
}
export default Footer;
@@ -0,0 +1,19 @@
import { amplitudeLogNavigation } from "@/logging";
import { Link } from "@navikt/ds-react";
function FooterLink({ children, href }) {
return (
<li>
<Link
className="text-text-on-inverted focus:shadow-focus focus:text-text-default flex w-fit items-center gap-1 underline hover:no-underline focus:bg-blue-200 focus:shadow-blue-200"
href={href}
onClick={(e) =>
amplitudeLogNavigation("footer", e.currentTarget.getAttribute("href"))
}
>
{children}
</Link>
</li>
);
}
export default FooterLink;
@@ -0,0 +1,27 @@
import { BodyShort, Heading } from "@navikt/ds-react";
import FooterLink from "./FooterLink";
type LinkBlockPropsT = {
heading: string;
links: { url: string; text: string; icon?: React.ReactNode }[];
};
function LinkBlock({ heading, links }: LinkBlockPropsT) {
return (
<div>
<Heading level="2" size="xsmall">
{heading}
</Heading>
<BodyShort as="ul" className="mt-3 grid gap-3">
{links.map((link) => (
<FooterLink key={link.url} href={link.url}>
{link.icon}
{link.text}
</FooterLink>
))}
</BodyShort>
</div>
);
}
export default LinkBlock;
@@ -1,102 +0,0 @@
import { MenuHamburgerIcon } from "@navikt/aksel-icons";
import { amplitudeLogNavigation } from "@/logging";
import { Modal } from "@navikt/ds-react";
import cl from "clsx";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
const LinkElement = ({ name, href, onClick }) => {
const { asPath } = useRouter();
return (
<li className="my-2 flex h-11 items-center">
<Link
href={href}
passHref
className={cl(
"hover:bg-surface-action-subtle-hover focus-visible:shadow-focus relative flex h-full w-full items-center rounded px-2 focus:outline-none",
{
"before:bg-surface-action-selected pl-4 font-semibold before:absolute before:left-0 before:h-6 before:w-1 before:rounded-full":
asPath.startsWith(href),
"": !asPath.startsWith(href),
}
)}
onClick={(e) => {
amplitudeLogNavigation(
"header",
e.currentTarget.getAttribute("href")
);
onClick();
}}
>
{name}
</Link>
</li>
);
};
export const Hamburger = () => {
const [open, setOpen] = useState(false);
return (
<>
<div className="mr-2 grid h-full place-content-center lg:hidden">
<button
onClick={() => setOpen(!open)}
aria-expanded={open}
className="focus-visible:shadow-focus hover:bg-surface-neutral-subtle-hover ml-2 flex aspect-square items-center justify-center rounded px-3 py-3 focus:outline-none"
>
<MenuHamburgerIcon
fontSize="2rem"
className="pointer-events-none"
aria-label="Åpne meny"
/>
</button>
</div>
<Modal
open={open}
onClose={() => setOpen(false)}
header={{ heading: "Meny" }}
width="small"
closeOnBackdropClick
>
<Modal.Body>
<nav aria-label="hovedmeny">
<ul>
<LinkElement
onClick={() => setOpen(false)}
name="God praksis"
href="/god-praksis"
/>
<LinkElement
onClick={() => setOpen(false)}
name="Grunnleggende"
href="/grunnleggende"
/>
<LinkElement
onClick={() => setOpen(false)}
name="Ikoner"
href="/ikoner"
/>
<LinkElement
onClick={() => setOpen(false)}
name="Komponenter"
href="/komponenter"
/>
<LinkElement
onClick={() => setOpen(false)}
name="Mønster & Maler"
href="/monster-maler"
/>
<LinkElement
onClick={() => setOpen(false)}
name="Blogg"
href="/produktbloggen"
/>
</ul>
</nav>
</Modal.Body>
</Modal>
</>
);
};
@@ -1,12 +1,12 @@
import AkselLogo from "@/assets/Logo";
import { amplitudeLogNavigation } from "@/logging";
import { MagnifyingGlassIcon } from "@navikt/aksel-icons";
import { Button } from "@navikt/ds-react";
import { Box, Button, HStack, Page, Show, Spacer } from "@navikt/ds-react";
import cl from "clsx";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useRouter } from "next/router";
import { Hamburger } from "./Hamburger";
import { Hamburger } from "./parts/Hamburger";
import HeaderLink from "./parts/HeaderLink";
export const GlobalSearch = dynamic(
() => import("../../website-modules/search/Search"),
@@ -32,92 +32,64 @@ export const GlobalSearch = dynamic(
}
);
const LinkElement = ({ name, href, prefetch = undefined }) => {
const { asPath } = useRouter();
return (
<li>
<Link
href={href}
prefetch={prefetch}
className={cl(
"text-deepblue-800 focus-visible:shadow-focus relative grid h-11 place-items-center rounded px-2 focus:outline-none",
{
"before:bg-deepblue-600 font-semibold before:absolute before:bottom-[1px] before:z-10 before:h-1 before:w-[calc(100%_-_16px)] before:rounded-full":
asPath.startsWith(href),
"hover:before:bg-border-subtle-hover before:absolute before:bottom-[1px] before:z-10 before:h-1 before:w-[calc(100%_-_16px)] before:rounded-full":
!asPath.startsWith(href),
}
)}
onClick={(e) =>
amplitudeLogNavigation("header", e.currentTarget.getAttribute("href"))
}
>
{name}
</Link>
</li>
);
};
const Header = ({
variant = "default",
}: {
variant?: "blogg" | "subtle" | "default" | "transparent";
}) => {
return (
<>
<header
className={cl("z-20", {
"bg-[#FEFCE9]": variant === "blogg",
"bg-surface-default": variant === "default",
"bg-surface-subtle": variant === "subtle",
"bg-surface-transparent": variant === "transparent",
})}
>
<a className="skiplink" href="#hovedinnhold">
Hopp til innhold
</a>
<header
className={cl("h-header z-20", {
"bg-[#FEFCE9]": variant === "blogg",
"bg-surface-default": variant === "default",
"bg-surface-subtle": variant === "subtle",
"bg-surface-transparent": variant === "transparent",
})}
>
<div
className={cl(
"mx-auto flex h-full max-w-screen-2xl items-center pr-4 lg:pr-6"
)}
>
<div className="flex h-11 items-center pl-4 pr-4 sm:pl-4 sm:pr-6">
<Link
href="/"
passHref
onClick={(e) =>
amplitudeLogNavigation(
"header",
e.currentTarget.getAttribute("href")
)
}
className="focus-visible:shadow-focus grid h-full place-items-center rounded px-2 focus:outline-none"
>
<AkselLogo className="text-deepblue-800" />
<span className="sr-only">Aksel</span>
</Link>
</div>
<nav
className="ml-auto hidden h-full pr-2 lg:block lg:pr-8"
aria-label="Hovedmeny"
<Page.Block width="2xl">
<div className="h-header flex items-center pr-4 lg:pr-6">
<Link
href="/"
passHref
onClick={(e) =>
amplitudeLogNavigation(
"header",
e.currentTarget.getAttribute("href")
)
}
className="focus-visible:shadow-focus mx-4 grid h-11 place-items-center rounded px-2 focus:outline-none sm:mr-6"
>
<ul className="hidden h-full items-center gap-2 md:flex">
<LinkElement name="God praksis" href="/god-praksis" />
<LinkElement name="Grunnleggende" href="/grunnleggende" />
<LinkElement name="Ikoner" href="/ikoner" prefetch={false} />
<LinkElement name="Komponenter" href="/komponenter" />
<LinkElement name="Mønster & Maler" href="/monster-maler" />
<LinkElement name="Bloggen" href="/produktbloggen" />
</ul>
</nav>
<div className="z-[1050] ml-auto mr-4 flex justify-center lg:ml-0 lg:mr-0">
<GlobalSearch />
</div>
<AkselLogo className="text-deepblue-800" />
<span className="sr-only">Aksel</span>
</Link>
<Hamburger />
<Spacer />
<Show above="lg" asChild>
<Box
as="nav"
paddingInline={{ xs: "0 2", lg: "0 8" }}
aria-label="Hovedmeny"
>
<HStack as="ul" gap="2" align="center">
<HeaderLink name="God praksis" href="/god-praksis" />
<HeaderLink name="Grunnleggende" href="/grunnleggende" />
<HeaderLink name="Ikoner" href="/ikoner" prefetch={false} />
<HeaderLink name="Komponenter" href="/komponenter" />
<HeaderLink name="Mønster & Maler" href="/monster-maler" />
<HeaderLink name="Bloggen" href="/produktbloggen" />
</HStack>
</Box>
</Show>
<HStack align="center" gap="2">
<GlobalSearch />
<Hamburger />
</HStack>
</div>
</header>
</>
</Page.Block>
</header>
);
};
@@ -0,0 +1,69 @@
import { MenuHamburgerIcon } from "@navikt/aksel-icons";
import { Button, Modal, Show } from "@navikt/ds-react";
import { useState } from "react";
import HamburgerLink from "./HamburgerLink";
export const Hamburger = () => {
const [open, setOpen] = useState(false);
return (
<Show below="lg">
<Button
variant="tertiary-neutral"
onClick={() => setOpen(!open)}
aria-expanded={open}
icon={
<MenuHamburgerIcon
fontSize="2rem"
className="pointer-events-none"
title="Åpne meny"
/>
}
/>
<Modal
open={open}
onClose={() => setOpen(false)}
header={{ heading: "Meny" }}
width="small"
closeOnBackdropClick
>
<Modal.Body>
<nav aria-label="hovedmeny">
<ul>
<HamburgerLink
onClick={() => setOpen(false)}
name="God praksis"
href="/god-praksis"
/>
<HamburgerLink
onClick={() => setOpen(false)}
name="Grunnleggende"
href="/grunnleggende"
/>
<HamburgerLink
onClick={() => setOpen(false)}
name="Ikoner"
href="/ikoner"
/>
<HamburgerLink
onClick={() => setOpen(false)}
name="Komponenter"
href="/komponenter"
/>
<HamburgerLink
onClick={() => setOpen(false)}
name="Mønster & Maler"
href="/monster-maler"
/>
<HamburgerLink
onClick={() => setOpen(false)}
name="Blogg"
href="/produktbloggen"
/>
</ul>
</nav>
</Modal.Body>
</Modal>
</Show>
);
};
@@ -0,0 +1,35 @@
import { amplitudeLogNavigation } from "@/logging";
import cl from "clsx";
import Link from "next/link";
import { useRouter } from "next/router";
function HamburgerLink({ name, href, onClick }) {
const { asPath } = useRouter();
return (
<li className="my-2 flex h-11 items-center">
<Link
href={href}
passHref
className={cl(
"hover:bg-surface-action-subtle-hover focus-visible:shadow-focus relative flex h-full w-full items-center rounded px-2 focus:outline-none",
{
"before:bg-surface-action-selected pl-4 font-semibold before:absolute before:left-0 before:h-6 before:w-1 before:rounded-full":
asPath.startsWith(href),
"": !asPath.startsWith(href),
}
)}
onClick={(e) => {
amplitudeLogNavigation(
"header",
e.currentTarget.getAttribute("href")
);
onClick();
}}
>
{name}
</Link>
</li>
);
}
export default HamburgerLink;
@@ -0,0 +1,32 @@
import { amplitudeLogNavigation } from "@/logging";
import cl from "clsx";
import Link from "next/link";
import { useRouter } from "next/router";
function HeaderLink({ name, href, prefetch = undefined }) {
const { asPath } = useRouter();
return (
<li>
<Link
href={href}
prefetch={prefetch}
className={cl(
"text-deepblue-800 focus-visible:shadow-focus relative grid h-11 place-items-center rounded px-2 focus:outline-none",
{
"before:bg-deepblue-600 font-semibold before:absolute before:bottom-[1px] before:z-10 before:h-1 before:w-[calc(100%_-_16px)] before:rounded-full":
asPath.startsWith(href),
"hover:before:bg-border-subtle-hover before:absolute before:bottom-[1px] before:z-10 before:h-1 before:w-[calc(100%_-_16px)] before:rounded-full":
!asPath.startsWith(href),
}
)}
onClick={(e) =>
amplitudeLogNavigation("header", e.currentTarget.getAttribute("href"))
}
>
{name}
</Link>
</li>
);
}
export default HeaderLink;
@@ -10,8 +10,9 @@ export const FrontpageBlock = ({ blocks }: FrontpageBlockProps) => {
if (!blocks || !blocks.length) {
return null;
}
return (
<div className="lg:px-18 px-2">
<>
{blocks.map((x) => {
switch (x._type) {
case "nytt_fra_aksel":
@@ -20,7 +21,7 @@ export const FrontpageBlock = ({ blocks }: FrontpageBlockProps) => {
return null;
}
})}
</div>
</>
);
};
@@ -1,31 +1,20 @@
import { useCheckAuth } from "@/hooks/useCheckAuth";
import { useSanityDocId } from "@/hooks/useSanityDocId";
import { PencilIcon } from "@navikt/aksel-icons";
import { Tooltip } from "@navikt/ds-react";
function EditButton() {
const idCtx = useSanityDocId();
const validUser = useCheckAuth();
return validUser && idCtx?.id ? (
<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}
>
<PencilIcon aria-hidden className="shrink-0 text-xl" /> Rediger side
</a>
</Tooltip>
</div>
<a
href={`https://aksel.nav.no/admin/prod/intent/edit/id=${idCtx?.id}`}
target="_blank"
rel="noreferrer"
className="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"
>
<PencilIcon aria-hidden className="shrink-0 text-xl" /> Rediger side
</a>
) : null;
}
@@ -1,13 +1,10 @@
import { useCheckAuth } from "@/hooks/useCheckAuth";
import { BodyShort, Button } from "@navikt/ds-react";
import Link from "next/link";
function PreviewBanner({
loading = true,
validUser,
}: {
loading: boolean;
validUser: boolean;
}) {
function PreviewBanner({ loading = true }: { loading: boolean }) {
const validUser = useCheckAuth();
if (!validUser) {
return (
<>
@@ -1,70 +0,0 @@
import { useCheckAuth } from "@/hooks/useCheckAuth";
import { getClient } from "@/sanity/client.server";
import { LiveQueryProvider, useLiveQuery } from "next-sanity/preview";
import dynamic from "next/dynamic";
import { ComponentType, useMemo } from "react";
const PreviewBanner = dynamic(() => import("@/web/PreviewBanner"), {
ssr: false,
});
function PreviewProvider({ children }: { children: React.ReactNode }) {
const client = useMemo(() => {
return getClient().withConfig({
useCdn: false,
ignoreBrowserTokenWarning: true,
perspective: "previewDrafts",
});
}, []);
return <LiveQueryProvider client={client}>{children}</LiveQueryProvider>;
}
function LiveQuery({
comp: Comp,
query,
params,
props,
validUser,
}: {
comp: ComponentType;
query: string;
props: any;
params?: any;
validUser: boolean;
}) {
const [data, loading] = useLiveQuery(props, query, params);
return (
<>
<PreviewBanner loading={loading} validUser={validUser} />
<Comp {...props} {...data} />
</>
);
}
const WithPreview = ({
comp,
query,
params,
props,
}: {
comp;
query: string;
props: any;
params?: any;
}) => {
const validUser = useCheckAuth();
return (
<PreviewProvider>
<LiveQuery
props={props}
query={query}
params={params}
comp={comp}
validUser={validUser}
/>
</PreviewProvider>
);
};
export default WithPreview;
@@ -0,0 +1,35 @@
import { getClient } from "@/sanity/client.server";
import { LiveQueryProvider } from "next-sanity/preview";
import { useMemo } from "react";
import LiveQuery from "./parts/LiveQuery";
import { PreviewProps } from "./parts/types";
const WithPreview = ({
comp,
query,
params,
props,
resolvers,
}: PreviewProps) => {
const client = useMemo(() => {
return getClient().withConfig({
useCdn: false,
ignoreBrowserTokenWarning: true,
perspective: "previewDrafts",
});
}, []);
return (
<LiveQueryProvider client={client}>
<LiveQuery
props={props}
query={query}
params={params}
comp={comp}
resolvers={resolvers}
/>
</LiveQueryProvider>
);
};
export default WithPreview;
@@ -0,0 +1,29 @@
import { useLiveQuery } from "next-sanity/preview";
import dynamic from "next/dynamic";
import { runResolvers } from "./resolvers";
import { PreviewProps } from "./types";
const PreviewBanner = dynamic(() => import("@/web/PreviewBanner"), {
ssr: false,
});
function LiveQuery({
comp: Comp,
query,
params,
props,
resolvers,
}: PreviewProps) {
const [data, loading] = useLiveQuery(props, query, params);
const _data = loading ? props : runResolvers({ resolvers, data });
return (
<>
<PreviewBanner loading={loading} />
<Comp {..._data} />
</>
);
}
export default LiveQuery;
@@ -0,0 +1,64 @@
import { runResolvers } from "../resolvers";
describe("runResolvers", () => {
test("should return original data when resolvers is null", () => {
const data = { key: "value" };
expect(runResolvers({ resolvers: null, data })).toEqual(data);
});
test("should return original data when resolvers is an empty array", () => {
const data = { key: "value" };
expect(runResolvers({ resolvers: [], data })).toEqual(data);
});
test("should run resolver callback with data from specified keys", () => {
const data = { key1: "value1", key2: "value2" };
const resolvers = [
{
key: "result",
dataKeys: ["key1", "key2"],
cb: ([value1, value2]) => value1 + value2,
},
];
expect(runResolvers({ resolvers, data })).toEqual({
...data,
result: "value1value2",
});
});
test("should run multiple resolver callbacks", () => {
const data = { key1: "value1", key2: "value2" };
const resolvers = [
{
key: "result1",
dataKeys: ["key1"],
cb: ([value1]) => value1 + "suffix",
},
{
key: "result2",
dataKeys: ["key2"],
cb: ([value2]) => "prefix" + value2,
},
];
expect(runResolvers({ resolvers, data })).toEqual({
...data,
result1: "value1suffix",
result2: "prefixvalue2",
});
});
test("should handle nested keys", () => {
const data = { key1: { key2: "value2" } };
const resolvers = [
{
key: "result",
dataKeys: ["key1.key2"],
cb: ([value2]) => "prefix" + value2,
},
];
expect(runResolvers({ resolvers, data })).toEqual({
...data,
result: "prefixvalue2",
});
});
});
@@ -0,0 +1,33 @@
import { ResolverT } from "./types";
/**
* `runResolvers` is responsible for executing a set of resolvers for streamed data from sanity.
* This allows running functions and parse data both at initial load `getStaticProps` and when streamed `useLiveQuery`
* @param {Array<Resolvers>}
* @returns any
*/
export function runResolvers({
resolvers,
data,
}: {
resolvers: ResolverT;
data: any;
}) {
if (!resolvers) {
return data;
}
return resolvers.reduce((acc, resolver) => {
const dataFromKeys = resolver.dataKeys.map((key) =>
getNestedProperty(acc, key)
);
acc[resolver.key] = resolver.cb(dataFromKeys);
return acc;
}, data);
}
function getNestedProperty(obj: any, path: string) {
return path
.split(".")
.reduce((acc, key) => (acc ? acc[key] : undefined), obj);
}
@@ -0,0 +1,37 @@
import { ComponentType } from "react";
export type ResolverT = {
key: string;
dataKeys: string[];
cb: (v: any[]) => any;
}[];
export type PreviewProps = {
/**
* Component to call with live data.
* This would normally be the page-component
*/
comp: ComponentType;
/**
* Sanity(groq)-query to run live data on
*/
query: string;
/**
* sanity(groq)-params
*/
params?: any;
/**
* Default/fallback-data if preview fails and for initial load
*/
props: any;
/**
* Array of functions we want to run on live data when fetched
* @example Supports getting data from nested objects like `page.content`
* @example
* Here we run `generateSidebar` function on `sidebar` key every time the data is reloaded
* resolvers={[
* { key: "sidebar", dataKeys: ["page.content", "page.intro"], cb: (v) => generateSidebar(v[0], "komponenter") },
* ]}
*/
resolvers?: ResolverT;
};
@@ -1,10 +1,10 @@
import { MagnifyingGlassIcon } from "@navikt/aksel-icons";
import { Button, Modal } from "@navikt/ds-react";
import { useContext } from "react";
import { SearchContext, SearchNavigationContext } from "../providers";
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);
@@ -0,0 +1,43 @@
import { withDsExample } from "@/web/examples/withDsExample";
import { Box, Page } from "@navikt/ds-react";
const Example = () => {
return (
<Page
background="bg-subtle"
footer={
<Box as="footer" background="surface-neutral-moderate" padding="8">
<Page.Block gutters>Footer</Page.Block>
</Box>
}
>
<Box as="header" background="surface-neutral-moderate" padding="8">
<Page.Block gutters>Header</Page.Block>
</Box>
<Box
as="main"
background="surface-alt-3-moderate"
padding="8"
paddingBlock="16"
>
<Page.Block gutters>Content</Page.Block>
</Box>
</Page>
);
};
export default withDsExample(Example, {
showBreakpoints: true,
variant: "full",
});
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 5,
title: "Bakgrunn",
desc: "Background-prop lar deg velge mellom `default` og `subtle` bakgrunn.",
};
@@ -0,0 +1,43 @@
import { withDsExample } from "@/web/examples/withDsExample";
import { Box, Page } from "@navikt/ds-react";
const Example = () => {
return (
<Page
footerPosition="belowFold"
footer={
<Box as="footer" background="surface-neutral-moderate" padding="8">
<Page.Block gutters>Footer</Page.Block>
</Box>
}
>
<Box as="header" background="surface-neutral-moderate" padding="8">
<Page.Block gutters>Header</Page.Block>
</Box>
<Box
as="main"
background="surface-alt-3-moderate"
padding="8"
paddingBlock="16"
>
<Page.Block gutters>Content</Page.Block>
</Box>
</Page>
);
};
export default withDsExample(Example, {
showBreakpoints: true,
variant: "full",
});
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 1,
title: "Footer belowFold",
desc: "Sikrer at footer aldri vil vises før man begynner å scrolle. Dette hjelper med å redusere layout-shifts ved navigering mellom sider.",
};
@@ -0,0 +1,43 @@
import { withDsExample } from "@/web/examples/withDsExample";
import { Box, Page } from "@navikt/ds-react";
const Example = () => {
return (
<Page
contentBlockPadding="end"
footer={
<Box as="footer" background="surface-neutral-moderate" padding="8">
<Page.Block gutters>Footer</Page.Block>
</Box>
}
>
<Box as="header" background="surface-neutral-moderate" padding="8">
<Page.Block gutters>Header</Page.Block>
</Box>
<Box
as="main"
background="surface-alt-3-moderate"
padding="8"
paddingBlock="16"
>
<Page.Block gutters>Content</Page.Block>
</Box>
</Page>
);
};
export default withDsExample(Example, {
showBreakpoints: true,
variant: "full",
});
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 3,
title: "Content Padding",
desc: "contentBlockPadding på Page sikrer at det alltid vil være minimumspadding mellom innhold og footer. Dette vil være en god fallback, men basert på layout vil du selv måtte legge på egen padding top/bottom",
};
@@ -0,0 +1,48 @@
import { withDsExample } from "@/web/examples/withDsExample";
import { Box, Page } from "@navikt/ds-react";
const Example = () => {
return (
<Page
footer={
<Box background="surface-neutral-moderate" padding="8" as="footer">
<Page.Block gutters width="lg">
Footer
</Page.Block>
</Box>
}
>
<Box background="surface-neutral-moderate" padding="8" as="header">
<Page.Block gutters width="lg">
Header
</Page.Block>
</Box>
<Box
background="surface-alt-3-moderate"
padding="8"
paddingBlock="16"
as="main"
>
<Page.Block gutters width="lg">
Content
</Page.Block>
</Box>
</Page>
);
};
export default withDsExample(Example, {
showBreakpoints: true,
variant: "full",
});
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 0,
title: "Standard",
desc: "Page gjør det enklere å bygge opp layout med riktig maksbredde og gutters.",
};
@@ -0,0 +1,42 @@
import { withDsExample } from "@/web/examples/withDsExample";
import { Box, Page } from "@navikt/ds-react";
const Example = () => {
return (
<Page
footer={
<Box as="footer" background="surface-neutral-moderate" padding="8">
<Page.Block gutters>Footer</Page.Block>
</Box>
}
>
<Box as="header" background="surface-neutral-moderate" padding="8">
<Page.Block gutters>Header</Page.Block>
</Box>
<Box
as="main"
background="surface-alt-3-moderate"
padding="8"
paddingBlock="16"
>
<Page.Block gutters>Content</Page.Block>
</Box>
</Page>
);
};
export default withDsExample(Example, {
showBreakpoints: true,
variant: "full",
});
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 2,
title: "Gutters",
desc: "Gutters-prop på Page.Block setter responsive gutters (padding-inline)",
};
@@ -0,0 +1,49 @@
import { withDsExample } from "@/web/examples/withDsExample";
import { Box, Page } from "@navikt/ds-react";
const Example = () => {
return (
<Page
background="bg-subtle"
footer={
<Box as="footer" background="surface-neutral-moderate" padding="8">
<Page.Block gutters width="2xl">
Footer
</Page.Block>
</Box>
}
>
<Box as="header" background="surface-neutral-moderate" padding="8">
<Page.Block gutters width="2xl">
Header
</Page.Block>
</Box>
<Box
as="main"
background="surface-alt-3-moderate"
padding="8"
paddingBlock="16"
>
<Page.Block gutters width="2xl">
Content
</Page.Block>
</Box>
</Page>
);
};
export default withDsExample(Example, {
showBreakpoints: true,
variant: "full",
});
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 6,
title: "Maksbredde",
desc: "Width-prop på Page.Block sentrerer innhold og legger på maksbredde. I de fleste løsninger bør 2xl (1440px) være standard.",
};
@@ -0,0 +1,17 @@
import { withDsExample } from "@/web/examples/withDsExample";
import { Textarea } from "@navikt/ds-react";
const Example = () => {
return <Textarea label="Har du noen tilbakemeldinger?" resize />;
};
export default withDsExample(Example);
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 6,
};
@@ -285,6 +285,17 @@ const Wrapper = (props: any) => {
params={{
slug: `god-praksis/artikler/${props?.slug}`,
}}
resolvers={[
{
key: "toc",
dataKeys: ["page.content"],
cb: (v) =>
generateTableOfContents({
content: v[0],
type: "aksel_artikkel",
}),
},
]}
/>
</Suspense>
);
@@ -150,6 +150,23 @@ const Wrapper = (props: any) => {
type: "ds_artikkel",
}}
props={props}
resolvers={[
{
key: "sidebar",
dataKeys: ["sidebar"],
cb: (v) => generateSidebar(v[0], "grunnleggende"),
},
{
key: "toc",
dataKeys: ["page.content", "page.intro"],
cb: (v) =>
generateTableOfContents({
content: v[0],
type: "ds_artikkel",
intro: !!v[1],
}),
},
]}
/>
</Suspense>
);
@@ -128,6 +128,13 @@ const Wrapper = (props: any) => {
params={{
type: "ds_artikkel",
}}
resolvers={[
{
key: "sidebar",
dataKeys: ["sidebar"],
cb: (v) => generateSidebar(v[0], "grunnleggende"),
},
]}
/>
</Suspense>
);
+161 -147
View File
@@ -18,10 +18,18 @@ import {
PauseFillIcon,
PlayFillIcon,
} from "@navikt/aksel-icons";
import { BodyLong, Heading } from "@navikt/ds-react";
import {
Bleed,
BodyLong,
Box,
Button,
Heading,
Page,
useClientLayoutEffect,
} from "@navikt/ds-react";
import cl from "clsx";
import { GetStaticProps } from "next/types";
import { Suspense, lazy, useEffect, useState } from "react";
import { Suspense, lazy, useState } from "react";
type PageProps = NextPageT<{
tema: Array<AkselTemaT>;
@@ -33,30 +41,6 @@ type PageProps = NextPageT<{
blocks?: BlocksT[];
}>;
export const getStaticProps: GetStaticProps = async ({
preview = false,
}: {
preview?: boolean;
}): Promise<PageProps> => {
const client = getClient();
const { page = null, tema = null, blocks = null } = await client.fetch(query);
return {
props: {
tema,
page,
blocks,
slug: "/",
preview,
id: page?._id ?? "",
title: "Forsiden",
},
revalidate: 600,
notFound: false,
};
};
const query = `*[_type == "aksel_forside"][0]{
"page": {
...,
@@ -115,26 +99,16 @@ const query = `*[_type == "aksel_forside"][0]{
}
}`;
const Forside = ({ page, tema, blocks }: PageProps["props"]) => {
const [pause, setPause] = useState(false);
const [reducedMotion, setReducedMotion] = useState(false);
export const getStaticProps: GetStaticProps = async ({
preview = false,
}: {
preview?: boolean;
}): Promise<PageProps> => {
const client = getClient();
useEffect(() => {
const disableAnimations =
navigator.userAgent.indexOf("Safari") !== -1 &&
navigator.userAgent.indexOf("Chrome") === -1;
const { page = null, tema = null, blocks = null } = await client.fetch(query);
setReducedMotion(userPrefersReducedMotion() || disableAnimations);
const data = localStorage.getItem("pause-animations");
if (disableAnimations) {
setPause(true);
setPause(true);
return;
}
setPause(JSON.parse(data) ?? false);
}, []);
const validatedTema = tema
const validateTema = tema
.filter(
(t) =>
t?.title &&
@@ -146,8 +120,49 @@ const Forside = ({ page, tema, blocks }: PageProps["props"]) => {
)
.sort((a, b) => a.title.localeCompare(b.title));
return {
props: {
tema: validateTema,
page,
blocks,
slug: "/",
preview,
id: page?._id ?? "",
title: "Forsiden",
},
revalidate: 600,
notFound: false,
};
};
const Forside = ({ page, tema, blocks }: PageProps["props"]) => {
const [pause, setPause] = useState(false);
const [reducedMotion, setReducedMotion] = useState(false);
useClientLayoutEffect(() => {
const disableAnimations =
navigator.userAgent.indexOf("Safari") !== -1 &&
navigator.userAgent.indexOf("Chrome") === -1;
setReducedMotion(userPrefersReducedMotion() || disableAnimations);
const data = localStorage.getItem("pause-animations");
if (disableAnimations) {
setPause(true);
return;
}
setPause(JSON.parse(data) ?? false);
}, []);
return (
<>
<Page
footer={<Footer />}
footerPosition="belowFold"
contentBlockPadding="none"
className={cl(
"header-animated-bg relative overflow-hidden bg-violet-200",
{ "animation-stop": pause }
)}
>
<SEO
title="Aksel"
description={
@@ -156,117 +171,116 @@ const Forside = ({ page, tema, blocks }: PageProps["props"]) => {
}
image={page?.seo?.image}
/>
<div
className={cl(
"header-animated-bg relative max-w-[100vw] overflow-hidden bg-violet-200",
{ "animation-stop": pause }
)}
>
<Header />
<main tabIndex={-1} id="hovedinnhold" className="focus:outline-none">
<div className="z-20 pb-28">
<div className="relative mx-auto mb-12 mt-20 grid w-full place-items-center px-4 text-center sm:mt-36 sm:max-w-[632px] sm:px-6">
<Heading
level="1"
size="xlarge"
className="text-deepblue-700 leading-[1.2] sm:text-[3.5rem]"
>
Aksel gjør det enklere å lage digitale produkter
</Heading>
<AkselCubeAnimated />
</div>
<IntroCards
links={[
{
title: "Komponenter",
desc: "Bibliotekene Core og Interne flater",
icon: ComponentIcon,
href: "/komponenter",
},
{
title: "Design Tokens",
desc: "Farger, spacing, shadows, etc.",
icon: PaletteIcon,
href: "/grunnleggende/styling/design-tokens",
},
{
title: "Ikoner",
desc: "Alle ikonene våre",
icon: CompassIcon,
href: "/ikoner",
},
]}
className="mx-auto mb-40 w-full max-w-md grid-cols-1 px-4 sm:mb-36 sm:px-6 md:max-w-screen-lg md:grid-cols-3"
variant="forside"
/>
<Header />
<main tabIndex={-1} id="hovedinnhold" className="focus:outline-none">
<div className="z-20 pb-28">
<div className="relative mx-auto mb-12 mt-20 grid w-full place-items-center px-4 text-center sm:mt-36 sm:max-w-[632px] sm:px-6">
<Heading
level="1"
size="xlarge"
className="text-deepblue-700 leading-[1.2] sm:text-[3.5rem]"
>
Aksel gjør det enklere å lage digitale produkter
</Heading>
<AkselCubeAnimated />
</div>
<div className="bg-surface-subtle min-h-96 relative pb-20">
<div className="mx-auto grid w-full max-w-screen-2xl px-4 sm:px-6">
{/* God praksis */}
<div className="bg-surface-default ring-border-subtle mx-auto w-full -translate-y-48 rounded-2xl px-4 py-12 ring-1 sm:-translate-y-32 sm:px-12 sm:py-20">
{!reducedMotion && (
<button
className="focus-visible:shadow-focus text-text-subtle hover:text-text-default absolute right-2 top-2 grid h-11 w-11 place-items-center rounded-xl text-2xl focus:outline-none focus-visible:ring-2"
onClick={() => {
setPause(!pause);
localStorage.setItem(
"pause-animations",
JSON.stringify(!pause)
);
}}
>
{pause ? (
<>
<PlayFillIcon aria-hidden />
<span className="sr-only">Start animasjon</span>
</>
<IntroCards
links={[
{
title: "Komponenter",
desc: "Bibliotekene Core og Interne flater",
icon: ComponentIcon,
href: "/komponenter",
},
{
title: "Design Tokens",
desc: "Farger, spacing, shadows, etc.",
icon: PaletteIcon,
href: "/grunnleggende/styling/design-tokens",
},
{
title: "Ikoner",
desc: "Alle ikonene våre",
icon: CompassIcon,
href: "/ikoner",
},
]}
className="mx-auto mb-40 w-full max-w-md grid-cols-1 px-4 sm:mb-36 sm:px-6 md:max-w-screen-lg md:grid-cols-3"
variant="forside"
/>
</div>
<Box background="surface-subtle" paddingBlock="0 32">
{/* God praksis */}
<Page.Block width="2xl" gutters>
<Box
background="surface-default"
borderWidth="1"
borderColor="border-subtle"
borderRadius="xlarge"
paddingBlock={{ xs: "12", sm: "20" }}
paddingInline={{ xs: "4", sm: "12" }}
className="-translate-y-48 sm:-translate-y-32"
>
{!reducedMotion && (
<Button
variant="tertiary-neutral"
size="small"
className="absolute right-2 top-2"
icon={
pause ? (
<PlayFillIcon title="Start animasjon" />
) : (
<>
<PauseFillIcon aria-hidden />
<span className="sr-only">Stopp animasjon</span>
</>
)}
</button>
)}
<div className="px-2 sm:px-6">
<Heading
level="2"
size="xlarge"
className="text-deepblue-700 mb-6"
>
God praksis
</Heading>
{page?.god_praksis_intro && (
<BodyLong size="large" className="max-w-3xl">
{page.god_praksis_intro}
</BodyLong>
)}
</div>
<ul className="mt-12 grid gap-x-8 md:grid-cols-2 xl:grid-cols-3">
{validatedTema.map((t) => (
<GodPraksisCardSimple key={t._id} node={t} />
))}
</ul>
</div>
<PauseFillIcon title="Stopp animasjon" />
)
}
onClick={() => {
setPause(!pause);
localStorage.setItem(
"pause-animations",
JSON.stringify(!pause)
);
}}
/>
)}
<Box paddingInline={{ xs: "2", sm: "6" }} paddingBlock="0 12">
<Heading
level="2"
size="xlarge"
className="text-deepblue-700"
spacing
>
God praksis
</Heading>
<BodyLong size="large" className="max-w-3xl">
{page?.god_praksis_intro ??
"Alle som jobber med produktutvikling i NAV sitter på kunnskap og erfaring som er nyttig for andre. Derfor deler vi god praksis med hverandre her."}
</BodyLong>
</Box>
<div className="-mt-24 sm:-mt-12">
<ul className="grid gap-x-8 md:grid-cols-2 xl:grid-cols-3">
{tema.map((t) => (
<GodPraksisCardSimple key={t._id} node={t} />
))}
</ul>
</Box>
<Bleed marginBlock={{ xs: "24", sm: "12" }} asChild>
<Box paddingInline={{ xs: "2", lg: "18" }}>
<FrontpageBlock blocks={blocks} />
</div>
</div>
</div>
</main>
<Footer />
</div>
</>
</Box>
</Bleed>
</Page.Block>
</Box>
</main>
</Page>
);
};
const WithPreview = lazy(() => import("@/preview"));
const Page = (props: PageProps["props"]) => {
const PagePreview = (props: PageProps["props"]) => {
if (props?.preview) {
return (
<Suspense fallback={<Forside {...props} />}>
@@ -278,4 +292,4 @@ const Page = (props: PageProps["props"]) => {
return <Forside {...props} />;
};
export default Page;
export default PagePreview;
@@ -340,6 +340,23 @@ const Wrapper = (props: any) => {
preview: "true",
}}
props={props}
resolvers={[
{
key: "sidebar",
dataKeys: ["sidebar"],
cb: (v) => generateSidebar(v[0], "komponenter"),
},
{
key: "toc",
dataKeys: ["page.content", "page.intro"],
cb: (v) =>
generateTableOfContents({
content: v[0],
type: "komponent_artikkel",
intro: !!v[1],
}),
},
]}
/>
</Suspense>
);
@@ -248,6 +248,13 @@ const Wrapper = (props: any) => {
params={{
type: "komponent_artikkel",
}}
resolvers={[
{
key: "sidebar",
dataKeys: ["sidebar"],
cb: (v) => generateSidebar(v[0], "komponenter"),
},
]}
/>
</Suspense>
);
@@ -190,6 +190,23 @@ const Wrapper = (props: any) => {
type: "templates_artikkel",
}}
props={props}
resolvers={[
{
key: "sidebar",
dataKeys: ["sidebar"],
cb: (v) => generateSidebar(v[0], "templates"),
},
{
key: "toc",
dataKeys: ["page.content", "page.intro"],
cb: (v) =>
generateTableOfContents({
content: v[0],
type: "templates_artikkel",
intro: !!v[1],
}),
},
]}
/>
</Suspense>
);
@@ -128,6 +128,13 @@ const Wrapper = (props: any) => {
params={{
type: "templates_artikkel",
}}
resolvers={[
{
key: "sidebar",
dataKeys: ["sidebar"],
cb: (v) => generateSidebar(v[0], "templates"),
},
]}
/>
</Suspense>
);
+1 -1
View File
@@ -21,7 +21,7 @@
"@/types": ["./components/types/index.ts"],
"@/sanity-block": ["./components/website-modules/SanityBlockContent.tsx"],
"@/hooks/*": ["./components/hooks/*"],
"@/preview": ["./components/website-modules/WithPreview.tsx"],
"@/preview": ["./components/website-modules/preview/WithPreview.tsx"],
"@/logging": ["./components/logging/index.ts"],
"@/utils": ["./components/utils/index.ts"],
"@/layout/*": ["./components/layout/*"],
+1 -1
View File
@@ -8,7 +8,7 @@
"@/types": ["./components/types/index.ts"],
"@/sanity-block": ["./components/website-modules/SanityBlockContent.tsx"],
"@/hooks/*": ["./components/hooks/*"],
"@/preview": ["./components/website-modules/WithPreview.tsx"],
"@/preview": ["./components/website-modules/preview/WithPreview.tsx"],
"@/logging": ["./components/logging/index.ts"],
"@/utils": ["./components/utils/index.ts"],
"@/layout/*": ["./components/layout/*"],
+18 -17
View File
@@ -3417,7 +3417,7 @@ __metadata:
languageName: node
linkType: hard
"@navikt/aksel-icons@^5.10.1, @navikt/aksel-icons@workspace:@navikt/aksel-icons":
"@navikt/aksel-icons@^5.10.2, @navikt/aksel-icons@workspace:@navikt/aksel-icons":
version: 0.0.0-use.local
resolution: "@navikt/aksel-icons@workspace:@navikt/aksel-icons"
dependencies:
@@ -3444,8 +3444,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@navikt/aksel-stylelint@workspace:@navikt/aksel-stylelint"
dependencies:
"@navikt/ds-css": ^5.10.1
"@navikt/ds-tokens": ^5.10.1
"@navikt/ds-css": ^5.10.2
"@navikt/ds-tokens": ^5.10.2
"@types/jest": ^29.0.0
concurrently: 7.2.1
copyfiles: 2.4.1
@@ -3463,7 +3463,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@navikt/aksel@workspace:@navikt/aksel"
dependencies:
"@navikt/ds-css": 5.10.1
"@navikt/ds-css": 5.10.2
"@types/inquirer": ^9.0.3
"@types/jest": ^29.0.0
axios: 1.6.0
@@ -3487,11 +3487,11 @@ __metadata:
languageName: unknown
linkType: soft
"@navikt/ds-css@*, @navikt/ds-css@5.10.1, @navikt/ds-css@^5.10.1, @navikt/ds-css@^5.9.2, @navikt/ds-css@workspace:@navikt/core/css":
"@navikt/ds-css@*, @navikt/ds-css@5.10.2, @navikt/ds-css@^5.10.2, @navikt/ds-css@^5.9.2, @navikt/ds-css@workspace:@navikt/core/css":
version: 0.0.0-use.local
resolution: "@navikt/ds-css@workspace:@navikt/core/css"
dependencies:
"@navikt/ds-tokens": ^5.10.1
"@navikt/ds-tokens": ^5.10.2
cssnano: 6.0.0
fast-glob: 3.2.11
lodash: 4.17.21
@@ -3504,13 +3504,13 @@ __metadata:
languageName: unknown
linkType: soft
"@navikt/ds-react@*, @navikt/ds-react@^5.10.1, @navikt/ds-react@^5.9.2, @navikt/ds-react@workspace:@navikt/core/react":
"@navikt/ds-react@*, @navikt/ds-react@^5.10.2, @navikt/ds-react@^5.9.2, @navikt/ds-react@workspace:@navikt/core/react":
version: 0.0.0-use.local
resolution: "@navikt/ds-react@workspace:@navikt/core/react"
dependencies:
"@floating-ui/react": 0.25.4
"@navikt/aksel-icons": ^5.10.1
"@navikt/ds-tokens": ^5.10.1
"@navikt/aksel-icons": ^5.10.2
"@navikt/ds-tokens": ^5.10.2
"@radix-ui/react-tabs": 1.0.0
"@radix-ui/react-toggle-group": 1.0.0
"@testing-library/dom": 8.13.0
@@ -3544,22 +3544,23 @@ __metadata:
languageName: unknown
linkType: soft
"@navikt/ds-tailwind@^5.10.1, @navikt/ds-tailwind@workspace:@navikt/core/tailwind":
"@navikt/ds-tailwind@^5.10.2, @navikt/ds-tailwind@workspace:@navikt/core/tailwind":
version: 0.0.0-use.local
resolution: "@navikt/ds-tailwind@workspace:@navikt/core/tailwind"
dependencies:
"@navikt/ds-tokens": ^5.10.1
"@navikt/ds-tokens": ^5.10.2
"@types/jest": ^29.0.0
color: 4.2.3
jest: ^29.0.0
lodash: ^4.17.21
tailwindcss: ^3.3.3
ts-jest: ^29.0.0
ts-node: ^10.9.1
typescript: ^5.1.6
languageName: unknown
linkType: soft
"@navikt/ds-tokens@^5.10.1, @navikt/ds-tokens@workspace:@navikt/core/tokens":
"@navikt/ds-tokens@^5.10.2, @navikt/ds-tokens@workspace:@navikt/core/tokens":
version: 0.0.0-use.local
resolution: "@navikt/ds-tokens@workspace:@navikt/core/tokens"
dependencies:
@@ -8743,11 +8744,11 @@ __metadata:
version: 0.0.0-use.local
resolution: "aksel.nav.no@workspace:aksel.nav.no"
dependencies:
"@navikt/aksel-icons": ^5.10.1
"@navikt/ds-css": ^5.10.1
"@navikt/ds-react": ^5.10.1
"@navikt/ds-tailwind": ^5.10.1
"@navikt/ds-tokens": ^5.10.1
"@navikt/aksel-icons": ^5.10.2
"@navikt/ds-css": ^5.10.2
"@navikt/ds-react": ^5.10.2
"@navikt/ds-tailwind": ^5.10.2
"@navikt/ds-tokens": ^5.10.2
prettier-plugin-tailwindcss: ^0.2.3
languageName: unknown
linkType: soft