Compare commits

..
44 Commits
Author SHA1 Message Date
Halvor Haugan 8bb33a29fe Bump dependencies 2026-11-11 13:02:32 +01:00
Halvor Haugan 8c88e4d1ba Remove some unused files + react-responsive-masonry 2026-10-26 12:30:07 +01:00
Halvor Haugan b9b51eaed9 Migrate to yarn 4.18.0 2026-10-26 12:19:37 +01:00
Ken 8ae419c0b4 ⬆️ Update v7-branch 2026-06-01 13:28:11 +02:00
Ken 7e7943f196 ⚡ optimize cpu and memory-usage 2026-01-05 10:13:16 +01:00
Ken f2702d7f3f 🔒 Resolve Nextjs CVE-2025-55184 and CVE-2025-55183 2025-12-12 09:44:26 +01:00
Halvor Haugan bf0618c381 Clean up MobileNav 2025-12-08 12:00:00 +01:00
Ken 5b76273cd8 🐛 Use localFetch of dataset for every sanity interaction 2025-12-04 16:34:53 +01:00
Ken cb6a4156aa 🐛 Use local import of data instead of fs-read 2025-12-04 16:16:49 +01:00
Ken fbf354421b 🐛 Resolve issues with logging after deploy 2025-12-04 16:02:51 +01:00
Ken a21646db55 🐛 Add theme type definitions 2025-12-04 15:46:23 +01:00
Ken 5f4ac75dd6 🐛 Add support for css-variables in styles 2025-12-04 15:42:46 +01:00
Ken 45944863ff 🐛 resolve build-errors caused by missing packages 2025-12-04 15:19:16 +01:00
Ken 7bd7f71fdd 🐛 'cd' into correct dir when building 2025-12-04 15:09:21 +01:00
Ken 683d650d65 🐛 Add npm access readers token to action 2025-12-04 14:42:37 +01:00
Ken af2a993ad7 Merge branch 'main' into v7-aksel.nav.no 2025-12-04 14:37:08 +01:00
Ken 92f1a283fd 📝 Update next-version 2025-12-04 11:23:49 +01:00
Halvor Haugan b8a8ecf6f5 remove unused deps 2025-12-03 11:47:28 +01:00
Ken 12ac05ba5b 🎉 Simplify asset-download, commit assets to public 2025-12-02 15:48:04 +01:00
Ken 9757c93088 🐛 Resolve build-errors 2025-12-02 15:44:12 +01:00
Ken 5a6c2c5a8f 🐛 Download assets on startup 2025-12-02 15:14:04 +01:00
Ken 02a06f2414 🐛 Fix dowload-script 2025-12-02 15:13:20 +01:00
Ken 9dba82dc0a 🎉 Set up build-env 2025-12-02 15:10:23 +01:00
Ken f1952ce49c 🎉 Local dataset read now works with groq-js 2025-12-02 15:05:09 +01:00
Ken ca950dde84 🎉 Local dataset fetching 2025-12-02 14:55:54 +01:00
Ken b9dc9c84a8 📝 Update image types 2025-12-02 14:39:43 +01:00
Ken 835c70b0ab 🎉 Added relative image linking 2025-12-02 14:31:16 +01:00
Ken d1e83406a0 📝 How to run 2025-12-02 13:11:20 +01:00
Ken 1a91fa08d2 🎉 Setup for cloning data 2025-12-02 13:11:05 +01:00
Ken d561ce4d23 🎉 Extracted all relevant types to fetch 2025-12-02 12:47:29 +01:00
Ken 483c8a30e4 🔥 Clean up document-references 2025-12-02 12:35:23 +01:00
Halvor Haugan 411f61465f Fix sticky 2025-12-01 16:50:31 +01:00
Halvor Haugan 9d0d75005a Remove more stuff 2025-12-01 16:35:45 +01:00
Ken f2f9ee8211 🔥 removed templates from sidebar 2025-12-01 16:06:30 +01:00
Ken 7db9f6af49 🔥 removed endringslogg 2025-12-01 16:03:57 +01:00
Ken b7819cba85 🔥 Remove removed routes from search 2025-12-01 15:59:40 +01:00
Ken c8b9afee99 📝 Announce old version in header 2025-12-01 15:55:33 +01:00
Ken 093e33d01f 🔥 Removed unused routes 2025-12-01 15:43:56 +01:00
Ken ac1b471a84 🐛 Website up and running from import error 2025-12-01 15:31:56 +01:00
Ken a05b9b1f2a 🔥 Removed theming from examples 2025-12-01 15:14:18 +01:00
Ken 1737ca88cb 🔥 Removed legacy sanity interface connection 2025-12-01 15:10:19 +01:00
Ken ee132cb09a 🔥 Fire removed sanity studio 2025-12-01 15:08:12 +01:00
Ken 3ab39815b9 🔥 Removed scripts 2025-12-01 15:01:46 +01:00
Ken 2dae2e6318 ✨ Initial setup for v7-release website 2025-12-01 14:54:53 +01:00
5817 changed files with 227723 additions and 155152 deletions

No files matched your search

+2 -3
View File
@@ -34,9 +34,8 @@ Alle base-pakkene våre har `fixed versjonering`. Dette betyr at en bump på en
```sh
"@navikt/ds-react",
"@navikt/ds-css",
"@navikt/aksel-icons",
"@navikt/ds-tokens",
"@navikt/ds-tailwind",
"@navikt/aksel-icons",
"@navikt/aksel",
"@navikt/aksel-stylelint"
"@navikt/ds-codemod"
```
+3 -9
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://unpkg.com/@changesets/config@4.0.0/schema.json",
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
"changelog": [
"@svitejs/changesets-changelog-github-compact",
{ "repo": "navikt/aksel" }
@@ -21,16 +21,10 @@
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": [
"eslint-plugin-aksel-local",
"aksel-changelog",
"aksel-analyzer",
"aksel-workspace-scripts",
"aksel.nav.no",
"website",
"aksel-sanity-studio",
"aksel-icons-figma-plugin",
"aksel-mcp",
"aksel-playroom"
"eslint-plugin-aksel-local",
"scripts"
],
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
-5
View File
@@ -1,5 +0,0 @@
---
"@navikt/ds-react": patch
---
DataGrid: Faster rendering when many details panels are expanded.
-5
View File
@@ -1,5 +0,0 @@
---
"@navikt/ds-react": patch
---
DataGrid.Table: Keyboard navigation no longer rebuilds the internal grid map on every arrow key press, improving responsiveness in large tables.
-5
View File
@@ -1,5 +0,0 @@
---
"@navikt/aksel-icons": minor
---
Icons: All icons are now available as CSS custom properties. Import `@navikt/aksel-icons/css/<IconName>.css` for a single icon (e.g. `--aksel-icon-magnifying-glass`), or `@navikt/aksel-icons/css/index.css` for every icon.
-5
View File
@@ -1,5 +0,0 @@
---
"@navikt/ds-react": patch
---
Combobox: Ignore option selection when an open Combobox becomes disabled.
-5
View File
@@ -1,5 +0,0 @@
---
"@navikt/ds-react": patch
---
Timeline: Optimize period parsing.
-5
View File
@@ -1,5 +0,0 @@
---
"@navikt/aksel-stylelint": patch
---
Stylelint: `@navikt/aksel-stylelint/recommended` now references its plugin by explicit file path, fixing `Could not find "."` errors with Stylelint 18.
-7
View File
@@ -1,7 +0,0 @@
# https://min-copilot.ansatt.nav.no/cplt
[propose]
allow_docker = true
allow_localhost_any = true
[propose.allow]
localhost = [3000, 6006, 6007]
+5 -5
View File
@@ -1,7 +1,7 @@
---
name: Bug Report
about: Meld inn bugs eller feil
title: "[BUG] "
title: "[BUG]"
labels: "bug \U0001F41B"
assignees: ""
---
@@ -21,7 +21,7 @@ assignees: ""
## Steg for å reprodusere problemet (valgfri)
<!--
1. Steg én
1. Steg en
2. Steg to
3. ....
-->
@@ -31,10 +31,10 @@ assignees: ""
## Eksempel/Tilleggsinformasjon (valgfri)
<!--
Screenshots, kodesnippets eller sandbox-lenke.
Legg gjerne ved info som nettleser eller desktop/mobil hvis det er relevant.
Screenshots eller kodesnippets
Legg gjerne ved ekstrainfo som nettleser eller desktop/mobil hvis det er relevant.
-->
```
Hvis mulig, legg gjerne til kode her, ev. lenke til kode i repo.
Hvis mulig, legg gjerne til kode her, evt lenke til kode i repo
```
+2 -2
View File
@@ -1,8 +1,8 @@
---
name: Feature Request
about: Foreslå noe nytt!
title: "[NEW] "
labels: nytt ✨
title: "[NEW]"
labels: new ✨
assignees: ""
---
+1 -1
View File
@@ -1,5 +1,5 @@
name: Eksempel på bruk
description: Send et eksempel på ting fra Aksel som er brukt i det fri (tokens, komponenter, maler, etc.)
description: Send et eksempel på ting fra Aksel som er brukt i det fri (tokens, komponenter, maler, etc)
title: "[Eksempel på bruk]: "
labels: ["dokumentasjon 📖"]
body:
+1 -1
View File
@@ -1,5 +1,5 @@
name: Ny komponent
description: Send et ønske om én eller flere nye komponenter
description: Send et ønske om en eller flere nye komponenter
title: "[Ny komponent]: "
labels: ["nytt ✨", "forespørsel 🥰", "komponenter 🧩"]
body:
+9
View File
@@ -0,0 +1,9 @@
---
name: Bug Report
about: Meld inn bugs eller feil
title: "[BUG]"
labels: "bug \U0001F41B"
assignees: ""
---
## Hva forventet du å finne for dette søket?
+2 -2
View File
@@ -1,5 +1,5 @@
name: Innspill til komponent
description: Send inn tilbakemelding på komponent
description: Send inn innspill til komponent
title: "[Innspill til komponent]: "
labels: ["forespørsel 🥰", "komponenter 🧩", "nytt ✨", "beta 🧪"]
body:
@@ -8,7 +8,7 @@ body:
value: |
Takk for at du bidrar inn i Aksel!
- type: textarea
id: feedback
id: feeback
attributes:
label: Tilbakemelding
description: Beskriv hvilke tilbakemeldinger du har til komponenten
+3 -3
View File
@@ -1,6 +1,6 @@
name: Innspill til ikon
description: Send inn tilbakemelding på ikon
title: "[Innspill til ikon]: "
description: Send inn innspill til ikon
title: "[Tilbakemelding på ikon]: "
labels: ["forespørsel 🥰", "ikoner 🖼️"]
body:
- type: markdown
@@ -8,7 +8,7 @@ body:
value: |
Takk for at du bidrar inn i Aksel!
- type: textarea
id: feedback
id: feeback
attributes:
label: Tilbakemelding
description: Beskriv hvilke tilbakemeldinger du har til ikonet
+3 -1
View File
@@ -8,8 +8,10 @@
- [ ] Examples
- [ ] Documentation / Decision Records
- [ ] Storybook
- [ ] Style mappings (`@navikt/core/css/config/_mappings.js`)
- [ ] Component tokens (`@navikt/core/css/tokens.json`)
- [ ] CSS class deprecations (`@navikt/aksel-stylelint/src/deprecations.ts`)
- [ ] Exports (`@navikt/core/react/src/index.ts` and `@navikt/core/react/package.json`)
- [ ] New component? CSS import (`@navikt/core/css/index.css`)
- [ ] Breaking change? Update migration guide. Consider codemod.
- [ ] Changeset (Format: `<Component>: <gitmoji?> <Text>` E.g. "Button: :sparkles: Add feature xyz")
- [ ] Changeset (Format: `<Component>: <gitmoji?> <Text>.` E.g. "Button: :sparkles: Add feature xyz.")
+1 -21
View File
@@ -19,12 +19,6 @@ inputs:
sanity_read_no_drafts_token:
description: "Sanity read-only token allowing only published documents"
required: true
sanity_deploy_studio:
description: "Sanity studio deployment token"
required: false
next_encryption_key:
description: "Overrides default NEXT_SERVER_ACTIONS_ENCRYPTION_KEY for production builds"
required: false
production:
description: "Toggle to differentiate production build (inject stuff based on this)"
default: false
@@ -54,25 +48,11 @@ runs:
echo "PRODUCTION=true" >> aksel.nav.no/website/.env
shell: bash
- name: Update sanity manifest
if: ${{ inputs.production == 'true' && inputs.sanity_deploy_studio != '' }}
run: yarn update:sanity-manifest
env:
SANITY_AUTH_TOKEN: ${{ inputs.sanity_deploy_studio }}
shell: bash
- name: Generate RSS feed
run: yarn workspace website update:rss
env:
SANITY_READ_NO_DRAFTS: ${{ inputs.sanity_read_no_drafts_token }}
shell: bash
- name: Build Next.js website
run: yarn workspace website build:with-playroom
run: yarn build:next
env:
SANITY_READ: ${{ inputs.sanity_read_token }}
SANITY_READ_NO_DRAFTS: ${{ inputs.sanity_read_no_drafts_token }}
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: ${{ inputs.next_encryption_key }}
shell: bash
- name: Upload static files to Nav CDN
+1 -1
View File
@@ -9,7 +9,7 @@ runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: yarn
+227 -70
View File
@@ -1,92 +1,249 @@
# Aksel - Copilot Instructions
# Aksel – GitHub Copilot Instructions
## Approach
This file tells Copilot how to work inside the Aksel monorepo. Prefer existing patterns and symbols, keep changes minimal, and reference real files in this repo.
- Narrow search space; don't replace reading code.
- Before editing: read target file + nearest related files (`index.ts`, story, test, exports).
- Prefer known paths/scripts over broad searches.
- User often edits files between turns. Before reviewing/refactoring, re-read the files (or `git diff`); never rely on earlier reads.
- Ignore release/deploy/changelog/ops scripts unless asked.
## How to act in this monorepo (quick)
## Behavior
- Edit only what’s asked. Don’t invent files/APIs or refactors.
- One file at a time. Provide a single coherent diff per file.
- Use existing exports, tokens, CSS layers, and patterns before adding new ones.
- When touching packages under `@navikt/*`, also update exports and stories nearby.
- Add or adjust tests and stories when behavior/visuals change.
- Link files with repo paths like `@navikt/react/src/...` or `aksel.nav.no/website/...`.
- Keep changes focused and minimal; avoid drive-by rewrites or formatting churn.
- Preserve public APIs unless the request explicitly calls for a breaking change (add a changeset if user-facing).
- Respond in the language the user prompted in, unless they request another language.
- Use concise, plain language without sacrificing technical precision or completeness.
- Lead with the outcome. Avoid filler and unnecessary apologies; summarize meaningful changes and validation when useful.
- Do not narrate routine tool use. Explain decisions, blockers, uncertainty, and tradeoffs when relevant.
- Show only what changes. Minimal diffs; don't paste whole files.
- Edit only the files the change needs (see Definition of done); no unrelated files. Ask only when truly blocked.
- Favor perf/security; avoid re-renders, top-level side effects, unsafe HTML, env var leaks.
## General code guidelines (Copilot)
## Repo summary
- Verify before you claim. Don’t speculate.
- Preserve unrelated code and existing structure.
- No apologies, no “I think”, no change summaries.
- Don’t ask to confirm info that’s already in context.
- Prefer explicit, descriptive names and follow current style.
- Favor performance and security; add robust error handling and safe logging.
- Keep functions small and focused; remove duplication.
- Replace hardcoded values with named constants when reasonable.
- Prefer composition over deep inheritance; keep modules cohesive.
- Nav's design system. Yarn 4 workspaces monorepo: published packages under `@navikt/` + docs site `aksel.nav.no/` + `apps/` + `tooling/`.
- Stack: TypeScript 6, React 19 (dev), Next.js 16, Storybook 10, Vitest 5, Oxlint 1 (with ESLint plugins as JS plugins), Stylelint 17, Prettier 3 (formatting). Node 22+, Yarn 4.18.0.
- Published `@navikt/*` packages must stay React 17 compatible (peer `react ^17 || ^18 || ^19`).
## Inline chat and editor completions
## Start and validate
- Default tone: short and answer-first. Aim for 1–3 concise sentences before any code.
- Show only what changes. Provide minimal diffs/snippets; don’t paste whole files or unrelated lines.
- Keep edits scoped to a single file unless explicitly asked to touch more.
- Use existing APIs, utilities, tokens, and patterns. Don’t add new deps unless requested.
- Preserve style: imports, naming, types, JSX patterns, token usage, CSS layers.
- Reference files and symbols with repo-relative paths and backticks, e.g. `@navikt/ds-react/src/...`.
- Extract requirements into a tiny checklist and proceed. Ask a question only when truly blocked.
- Avoid heavy formatting: bullets are OK; skip tables; use code fences only for code.
- For tests/visual changes: add/update the closest test/story next to the changed file.
- For Next.js: don’t mix `app/` and `pages/` in the same hierarchy; prefer server components where used; add "use client" only when required.
- For CSS/tokens: never hard-code colors/spacing; use tokens and existing layers.
- For performance/security: avoid unnecessary re-renders, side effects at module top-level, unsafe HTML, and leaking env vars.
1. `corepack enable`
2. `corepack yarn install` (needs `NPM_AUTH_TOKEN` with `read:packages`). Without a token: `corepack yarn workspaces focus aksel @navikt/aksel-icons @navikt/ds-tokens @navikt/ds-css @navikt/ds-react @navikt/ds-tailwind @navikt/aksel @navikt/aksel-stylelint`
3. `corepack yarn boot` (builds all public packages; rerun after `corepack yarn clean`)
## Repository overview
- Run agent commands through `corepack yarn`. Order: existing root/workspace script → Yarn-local binary → add a pinned dependency only when new tooling is required.
- Never use `npx`, `pnpx`, `pnpm dlx`, `yarn dlx`, global binaries or curl-piped installers. Public consumer docs (`npx @navikt/aksel`) are intentional; keep them.
- If Corepack/Yarn fails because of user-level configuration, report the exact error. Never edit user-home config.
- Under the `navikt/cplt` sandbox, `Parse error when loading ~/.yarnrc.yml` means the home rc file is unreadable (EPERM), not a repo issue. Stop retrying, report it and ask the user to fix it outside the sandbox. Don't touch the repo `.yarnrc.yml`.
- Choosing checks: use the `aksel-local-validation` skill. Changed-file lint first, then the affected workspace, then root suites only when needed.
Aksel is NAV’s design system: React components, CSS, design tokens, icons, tooling, and the documentation site. It’s a Yarn 4 workspaces monorepo.
## Definition of done
- Languages: TypeScript, CSS, React, Next.js
- Package manager: Yarn v4 (packageManager: `yarn@4.4.0`)
- Build: Per-package TypeScript builds (run via root scripts)
- Tests: Vitest + React Testing Library
- Behavior/visual change: update the implementation and closest existing story. Prefer Storybook play tests for UI behavior and unit tests for small utilities; combine overlapping coverage into existing stories instead of adding redundant stories.
- New public export: sync component `index.ts`, `src/index.ts` (or `src/preview.ts`) and `package.json` exports, then `corepack yarn workspace aksel-playroom sync-imports` (root `lint` fails on stale playroom imports).
- User-facing change to a published package: add a changeset (`add-changeset` skill) unless told otherwise.
Related files: `package.json`, `eslint.config.js`, `biome.json`, `tsconfig.json`.
## Commands
## Prerequisites
- `corepack yarn oxlint --deny-warnings <files...>` / `corepack yarn stylelint <files...>`
- `corepack yarn tsc --noEmit --incremental false --project <tsconfig>` - targeted type-check
- `corepack yarn workspace <name> <script>` - e.g. `@navikt/ds-react test` (Vitest + tsc), `website test`
- `corepack yarn lint` / `corepack yarn test` / `corepack yarn boot` - root suites (slow; cross-workspace changes only)
- `corepack yarn storybook` (:6006) / `corepack yarn storybook:aksel` (website examples, :6007, no secrets) / `corepack yarn storybook:test` (play tests, Playwright Firefox; Chromium when `COPILOT_CLI` is set) / `corepack yarn dev` (website, :3000, needs Sanity token)
- Targeted play tests: `corepack yarn storybook:test <path/to/X.tests.stories.tsx>` (repo root). Run after changing play tests. Under Copilot CLI this runs Chromium; if the browser can't launch, report tests as not run with the exact command.
- Yarn 4 via Corepack is expected by the repo (`packageManager` field).
- Some maintenance scripts use Deno (for example `scripts/deno/changelog/createMainChangelog.ts`). Install Deno if you run those.
- Most packages are public. If you encounter 401s from GitHub Packages in a custom setup, set `NPM_AUTH_TOKEN` with `read:packages`.
## Where to edit
For limited installs (no website), you can focus core packages:
Path-specific rules load automatically from `.github/instructions/` (see each `applyTo`).
```bash
yarn workspaces focus @navikt/aksel-icons @navikt/ds-tokens @navikt/ds-css @navikt/ds-react @navikt/ds-tailwind @navikt/aksel @navikt/aksel-stylelint
```
- `@navikt/core/react/src/<component>/` - components (`src/index.ts`, `src/preview.ts` + `package.json` exports = public API)
- `@navikt/core/css/src/` - component CSS (`index.css` imports + layers)
- `@navikt/core/tokens/src/` - design tokens · `@navikt/core/tailwind/` - Tailwind preset (generated from tokens)
- `@navikt/aksel-icons/icons/` - source SVG + YML (`src/` generated)
- `@navikt/aksel-stylelint/src/` - Stylelint rules + CSS class deprecations
- `@navikt/aksel/src/` - CLI and codemods
- `aksel.nav.no/website/` - Next.js docs site · `aksel.nav.no/sanity-studio/` - CMS schema
- `apps/playroom/`, `apps/mcp-server/`, `apps/figma-icon-plugin/` - apps · `tooling/` - ESLint plugin, scripts, changelog, analyzer
- `.changeset/` - release notes · Root config: `oxlint.config.mts`, `stylelint.config.mjs`, `vitest.config.mts`, `.storybook/`
## Repository structure (short)
## Skills and agents
```
@navikt/
├─ core/react # @navikt/ds-react – React components
├─ core/css # @navikt/ds-css – CSS
├─ core/tokens # @navikt/ds-tokens – tokens
├─ core/tailwind # @navikt/ds-tailwind – Tailwind preset
├─ aksel-icons # @navikt/aksel-icons – icons
├─ aksel-stylelint # @navikt/aksel-stylelint – stylelint rules
└─ aksel # @navikt/aksel – CLI/tooling
- `aksel-local-validation` - pick and run the right lint/type/test commands
- `code-review` - review a branch, diff or PR for consumer-facing risk
- `component-contract` - define and verify component behavior, state ownership and accessibility contracts
- `add-changeset` - write `.changeset/*.md` (never run interactive `yarn changeset`)
- `ds-component-restructure` - move/rename component files to canonical layout
- `website-example` - add aksel.nav.no examples/templates
aksel.nav.no/
├─ website # Next.js documentation site
└─ playroom # Component playground
## Gotchas
examples/ # Example apps
scripts/ # Build/maintenance scripts
```
- `.yarnrc.yml`: `enableScripts: false` (dependency install scripts don't run, e.g. Playwright browsers), `npmMinimalAgeGate: 7d` (new package versions under 7 days old are rejected), `defaultSemverRangePrefix: ""` (exact versions).
- Never hand-edit generated output: `esm/`, `cjs/`, `dist/`, `@navikt/aksel-icons/src/`, `@navikt/aksel/src/version.ts`, tokens `token_docs.js`, tailwind `tailwind.config.js`.
- Don't run scripts that write to Sanity/Figma/releases (`update:*`, `backup`, `fetch-new:icons`, `release`, `create-version`) unless asked.
## Build and development
## Coding defaults
Setup:
- Small diffs. No drive-by refactors or new deps unless asked.
- Propose the simplest maintainable solution first (explicit lists, member-name checks). No TS compiler API/AST tricks in tests (slow). Ask before escalating complexity.
- Copy nearest existing pattern before writing new code.
- Stable public APIs. Prefer additive props/exports.
- `node:` imports for Node builtins. Never import from `esm/`/`cjs/` output.
- Preserve JSDoc on public props/components when changing public APIs.
- Reuse helpers: `cl`, `composeEventHandlers`, `useId`, `omit`, `useClientLayoutEffect`, `useEventListener`.
- No tests for trivial logic. Only tests for complex logic that has a higher risk of breaking.
```bash
yarn && yarn boot
```
Useful scripts (root `package.json`):
```bash
yarn boot # Build all workspaces (topological)
yarn storybook # Storybook dev (port 6006)
yarn storybook:aksel # Storybook with website config (port 6007)
yarn dev # Run documentation website
yarn test # Run workspace tests
yarn lint # ESLint + Stylelint (full install recommended)
yarn clean # Clean generated files
yarn changeset # Create a changeset
yarn changelog # Generate main changelog (Deno)
yarn biome:lint # Optional extra linting with Biome
```
Note: `yarn lint` runs across the repo and may require full install (website ESLint plugins).
## CI and validation
- Pre-commit (Husky): `lint-staged`, plus internal version sync checks.
- CI typically runs: lint, unit tests, example/website checks, and Chromatic when applicable.
Common failure causes:
- Missing dependencies in a focused install when running repo-wide scripts
- TypeScript errors in dependent workspaces
- Stylelint token checks failing due to missing token imports
Quality gates to check locally before opening a PR:
- Build: `yarn boot` succeeds without TS errors.
- Lint/format: `yarn lint` (and optional `yarn biome:lint`) passes.
- Tests: `yarn test` for changed workspaces are green.
- Storybook/website (when UI changes): boots locally without errors.
## Tests
- Framework: Vitest (jsdom where needed)
- Website config: `aksel.nav.no/website/config/vitest.config.ts`
- Test locations: `**/__tests__/**` and `**/*.test.*`
- Add tests for new behavior and keep existing tests passing
- Prefer small, focused tests. Cover the happy path and 1–2 edge cases.
- Use Testing Library queries that reflect user intent (role/name), avoid brittle selectors.
## Linting and formatting
- ESLint config lives at root: `eslint.config.js` (React, Next.js, Storybook, Testing Library, custom rules)
- Stylelint is configured in root `package.json` with `@navikt/aksel-stylelint`
- Tokens are validated via Stylelint and `csstools/value-no-unknown-custom-properties`
- Biome config: `biome.json` (optional extra lint/format)
- Prettier uses import-sort; website adds Tailwind plugin via override
- Avoid disabling lint rules unless necessary; if you must, comment why.
## Packages and exports
- Synchronized versions: `@navikt/ds-react`, `@navikt/ds-css`, `@navikt/aksel-icons`, `@navikt/ds-tokens`, `@navikt/ds-tailwind`
- Typical package layout:
```
src/
├─ index.ts # Package exports
└─ <component>/
├─ index.ts # Component exports
├─ <component>.tsx # Implementation
└─ <component>.stories.tsx
```
When adding/editing components in `@navikt/react`:
- Update exports in `src/index.ts` and per-component `index.ts`
- Add/maintain stories and tests next to the component
- Follow existing props, theming, and token patterns
- Keep props stable; prefer additive changes. Deprecate before removal when possible.
- Ensure tree-shakeability; avoid side effects at module top-level.
## Common workflows
Add a new component
1. Implement under `@navikt/react/src/...`
2. Add stories and tests
3. Export from the package `index.ts`
4. Run `yarn boot` and fix types/lint
5. Create a changeset (`yarn changeset`)
Component change checklist (quick):
- [ ] Implementation updated under `@navikt/react/src/...`
- [ ] Exports updated (`src/index.ts` and component `index.ts`)
- [ ] Stories updated/added and render without errors
- [ ] Tests updated/added and passing (Vitest)
- [ ] Tokens/CSS use existing variables and layers (no hard-coded values)
- [ ] Changeset added if user-facing (version bump and notes)
Modify an existing component
1. Keep the public API stable unless explicitly requested
2. Update tests if behavior changes; update stories if visuals change
3. Add a changeset for user-facing changes
Troubleshooting
1. `yarn clean && yarn boot`
2. Check workspace dependency build order/issues
3. If focusing workspaces, avoid running repo-wide scripts that depend on the website
## Darkside theming and tokens
- Use design tokens and CSS layers; avoid hard-coded colors/spacing.
- Website imports Darkside CSS and tokens, see:
- `aksel.nav.no/website/pages/global.css` (imports `@navikt/ds-css/darkside`)
- `aksel.nav.no/website/pages/_app.tsx` (imports `@navikt/ds-tokens/darkside-css`)
- Stylelint enforces token usage; unknown/missing custom properties will fail lint.
## Website (Next.js)
- The website uses Next.js 15 and contains both `app/` and `pages/`. Place new files in the router that matches the surrounding code.
- Prefer server components where already used; mark client components with `"use client"` only when needed.
- Use Next Image, metadata APIs, and established utilities already in `aksel.nav.no/website`.
- Logging uses Next logger/pino; follow patterns in `aksel.nav.no/website` (see `next-logger.config.js`).
- Use existing route conventions and file organization; don’t mix `app/` and `pages/` in the same hierarchy.
- For Tailwind, prefer the shared preset `@navikt/ds-tailwind` and follow website `tailwind.config.js` patterns.
## Versioning and releases
- Use Changesets: `yarn changeset` and the root scripts:
- `yarn create-version` (applies versions, updates changelog, reinstalls)
- `yarn release` (boots, docgen, publish, git release helper)
- Main changelog script: `scripts/deno/changelog/createMainChangelog.ts`
## Answer snippets (for Copilot)
- Build: “Run yarn boot” (see root `package.json`)
- Storybook: “Run yarn storybook” (or `yarn storybook:aksel` for the website config)
- Lint/format: “Run yarn lint” (full install recommended) and optional `yarn biome:lint`
- Versioning: “Run yarn changeset, then yarn create-version”
- Theming: “Import @navikt/ds-css/darkside in global CSS and ensure tokens via @navikt/ds-tokens/darkside-css”
## Code reviews (for Copilot)
- Summarize the pull-request on maximum two sentences
- Avoid listing all changes, focus on the most important ones
- Check the description against the actual changes
- Ensure the PR title is descriptive and follows the conventional commit format
- Look for: correctness, style, performance, security, tests, documentation
- Check: existing patterns, exports, tokens, CSS layers
- Make sure import paths are correct, updated and use existing aliases where possible
- When components are changed, ensure stories and tests are updated accordingly
- When components are changed, make sure related examples for website and playroom are updated
- Make sure changes made to packages under `@navikt/` has a related changeset where relevant
### Extra review tips
- Watch for unnecessary bundle impact (large deps, dynamic imports, side effects).
- Confirm a11y basics: semantic elements, labels, focus order, keyboard support.
- Check security footguns: unsafe HTML, unescaped data, leaking env vars, weak CSP assumptions.
+17 -42
View File
@@ -3,11 +3,15 @@ registries:
npm-github:
type: npm-registry
url: https://npm.pkg.github.com
scope: "@navikt"
token: ${{ secrets.READER_TOKEN }}
updates:
- package-ecosystem: npm
cooldown:
default-days: 7
semver-major-days: 30
directory: /
exclude-paths:
- "examples/**"
schedule:
interval: weekly
day: monday
@@ -16,48 +20,28 @@ updates:
- npm-github
groups:
sanity:
applies-to: version-updates
patterns:
- "sanity"
- "sanity-*"
- "@sanity/*"
- "@portabletext/*"
- "@sanity/vision"
- "@sanity/schema"
eslint:
patterns:
- "eslint"
- "@eslint/*"
react:
applies-to: version-updates
patterns:
- "react"
- "react-dom"
storybook:
applies-to: version-updates
patterns:
- "*storybook*"
vitest:
applies-to: version-updates
# Group all other minor and patch updates to reduce noise
dependencies:
patterns:
- "vitest"
- "@vitest/*"
# Security updates ignore version-update groups, so mirror them to keep lockstep packages together.
sanity-security:
applies-to: security-updates
patterns:
- "sanity"
- "sanity-*"
- "@sanity/*"
- "@portabletext/*"
react-security:
applies-to: security-updates
patterns:
- "react"
- "react-dom"
storybook-security:
applies-to: security-updates
patterns:
- "*storybook*"
vitest-security:
applies-to: security-updates
patterns:
- "vitest"
- "@vitest/*"
- "*"
update-types:
- "minor"
- "patch"
- package-ecosystem: github-actions
directory: "/"
@@ -71,12 +55,3 @@ updates:
actions:
patterns:
- "*"
- package-ecosystem: docker
directories:
- "/aksel.nav.no"
- "/apps/*"
schedule:
interval: weekly
day: monday
time: "04:00"
@@ -1,21 +0,0 @@
---
description: "Writing changeset files for versioning public @navikt packages."
applyTo: ".changeset/**"
---
# Changesets
`yarn changeset` is interactive. Agents write `.changeset/<kebab-slug>.md` directly (see the `add-changeset` skill):
```md
---
"@navikt/ds-react": patch
---
Component: Short, user-facing description of the change
```
- Publishable packages (versioned together via `fixed` in `config.json`): `@navikt/ds-react`, `@navikt/ds-css`, `@navikt/ds-tokens`, `@navikt/ds-tailwind`, `@navikt/aksel-icons`, `@navikt/aksel`, `@navikt/aksel-stylelint`. List only the packages actually changed.
- Everything in `config.json` `ignore` (website, playroom, mcp, tooling, sanity) never gets a changeset.
- Summary format: `<Component>: <what changed for consumers>`. It ends up in the public changelog, so write for consumers, not reviewers.
- Don't edit `config.json`, `CHANGELOG.md` files or package versions. The release workflow does that.
@@ -1,13 +0,0 @@
---
description: "JS/TS conventions not enforced by Oxlint: loops, export style and placement."
applyTo: "**/*.ts,**/*.tsx,**/*.js,**/*.mts,**/*.mjs"
---
# JS/TS conventions
Formatting, import order and `import type` are handled by Prettier. Don't hand-tune them.
- Use `for...of` instead of `Array.forEach`. It supports `break`/`continue`/`await` and reads top-down.
- Use named exports only. Keep type exports separate from value exports (`export { X }` / `export type { XProps }`).
- Group exports at the bottom of the file, values first, then types. `react-docgen-typescript` (`docgen:meta`) depends on this pattern.
- Exceptions to default exports: `*.stories.tsx` (CSF meta default export, kept at the top), and Next.js pages/website examples that the framework requires.
@@ -1,15 +0,0 @@
---
description: "@navikt/aksel CLI and jscodeshift codemods: transforms, fixtures, migration registry."
applyTo: "@navikt/aksel/**"
---
# @navikt/aksel CLI and codemods
- Transforms live in `src/codemod/transforms/v<major>.0.0/<name>/<name>.ts`. Tests live in the sibling `tests/` folder.
- Every transform is registered in `src/migrations-config.ts` (`value`, `path` without extension, `description`, `ignoredExtensions`). `value` must be unique and is the public CLI name (`v8-accordion-variant`).
- Test with fixtures: `tests/<fixture>.input.js` → `tests/<fixture>.output.js`, run via `check(__dirname, { fixture, migration })` from `src/codemod/utils/check.ts`. Output is compared after Prettier, so whitespace doesn't matter.
- Reuse helpers in `src/codemod/utils/` (imports, props, JSX values, token translation) before writing new AST code.
- Fixtures (`*.input.*`, `*.output.*`) are excluded from Oxlint and TypeScript. Don't "fix" them to match lint rules.
- `src/version.ts` is generated by `build`. Don't edit it.
- `tests/migrations.test.ts` checks `dist/`. Run `corepack yarn workspace @navikt/aksel build` before `test`, or new transforms fail with "transform-path" errors.
- Consumer docs (`README.md`, website) show `npx @navikt/aksel ...` on purpose. Keep them.
-15
View File
@@ -1,15 +0,0 @@
---
description: "Editing @navikt/ds-css component styles: file setup, layers, selectors, tokens, deprecations."
applyTo: "@navikt/core/css/src/**"
---
# ds-css
- One file per component in `src/` (`badge.css`). A new file must be `@import`ed in `src/index.css` with the right `layer(...)`, otherwise it isn't bundled. Most components use `aksel.components.modules`; form controls go through `form/index.css`.
- Selectors use the `.aksel-<component>` prefix and BEM-ish `__part` / `--modifier`. State goes through `data-*` / ARIA attribute selectors, not extra modifier classes.
- Use tokens only: public `--ax-*`, component-internal `--__axc-*`. No hardcoded colors, spacing or radii. Stylelint flags unknown custom properties.
- Colors follow `data-color` (see nearby components). Don't add per-variant color selectors.
- Focus uses `:focus-visible` (outline + `outline-offset`). Check `forced-colors`, print and dark theme in Storybook.
- `prefers-reduced-motion` is handled globally in `baseline/baseline.css`. Don't duplicate it per component.
- `baseline/tokens.css` re-exports `@navikt/ds-tokens` build output. Edit tokens in `@navikt/core/tokens/src`, not here.
- Removing or renaming a class is breaking for consumers (for allready published components. Ignore for new components). Add it to `@navikt/aksel-stylelint/src/deprecations.ts`.
@@ -1,35 +0,0 @@
---
description: "Editing or adding @navikt/ds-react components, hooks and their public exports."
applyTo: "@navikt/core/react/src/**"
---
# ds-react components
Read the whole component folder (`index.ts`, source, `.meta.ts`, stories, tests) before editing. Moving or renaming files: use the `ds-component-restructure` skill.
For changes to component state, callbacks, async behavior, composition, semantics, keyboard or focus behavior, use the [component-contract skill](../skills/component-contract/SKILL.md).
## Compatibility (why: peer range is `react ^17 || ^18 || ^19`)
- No React 18/19-only APIs. Oxlint `no-restricted-imports` blocks e.g. `React.useId`. Use the internal `useId`, `useClientLayoutEffect`, etc. from `utils-external/`.
- Import React explicitly in TSX (`import React from "react"`). `tsconfig` uses classic `jsx: "react"`.
- Component `index.ts` starts with `"use client"`.
## Component patterns
- Preserve `forwardRef`, `className` merged via `cl("aksel-<name>", className)`, `...rest` spread, and `as`/`OverridableComponent` where they already exist.
- Compound components: `const X = Object.assign(XRoot, { Item: XItem })`. Never cast with `as XComponent`, because `docgen:meta` can't extract props from a cast.
- Colors: `data-color?: AkselColor`, not new `variant` color values.
- User-facing strings come from `useI18n("<Component>")` with keys in `utils/i18n/locales/{nb,nn,en}.ts`. Add the key to all three.
- Public props and components need JSDoc. Component JSDoc sits above the exported value, with `@see 🏷️ {@link XProps}` and an `@example`. Add `@default` when the default isn't obvious.
## Public API surface (breaking = major changeset)
- Stable component: export it from component `index.ts`, add it to `src/index.ts`, and add a `"./<Name>"` entry to `package.json` `exports` (both `import` and `require`).
- Preview/beta component: export from `src/preview.ts` plus `"./PREVIEW/<Name>"` in `package.json` instead of `src/index.ts`.
- Standalone components have `<Component>.meta.ts` (see `accordion/Accordion.meta.ts`). Sub-components don't.
- Changing exports: run `corepack yarn workspace aksel-playroom sync-imports`, or root `lint` fails.
## Validate
`corepack yarn workspace @navikt/ds-react test` (Vitest + `tsc` type-check). Add `build` when exports or types change.
@@ -1,14 +0,0 @@
---
description: "@navikt/aksel-icons: adding/updating SVG icons and metadata; generated React components."
applyTo: "@navikt/aksel-icons/**"
---
# aksel-icons
- Source: `icons/<Name>.svg` + `icons/<Name>.yml`. Every SVG has a matching YML and vice versa (checked by `__tests__/validate-files.test.ts`). Most icons also have a `<Name>Fill` variant.
- YML shape: copy a sibling, e.g. `icons/Airplane.yml` (`name`, `category`, `sub_category`, `keywords` (nb + en), `variant: Stroke|Fill`, `created_at: DD.MM.YYYY`).
- SVG root: `width="24" height="24" viewBox="0 0 24 24" fill="none"`. Shapes use `fill="#202733"` (becomes `currentColor` at build) and `stroke-width` 1.5 if any. `__tests__/validate-svg*.test.ts` enforces this. Copy a sibling SVG.
- `src/` React components, `dist/` and metadata are generated by `build` (SVGR). Never edit `src/` by hand.
- `core-icons/` is only used for the downloadable zip. `fetch-new:icons` pulls from Figma and needs a token. Don't run it unless asked.
- New icon = minor changeset for `@navikt/aksel-icons` (e.g. `Icons: New icons Timeline, TimelineFill`).
- Validate: `corepack yarn workspace @navikt/aksel-icons test`. Run `build` when checking generated output.
@@ -1,20 +0,0 @@
---
description: "Writing Vitest unit tests and Storybook stories/play tests."
applyTo: "**/*.test.ts,**/*.test.tsx,**/*.stories.tsx"
---
# Tests and stories
- User flows: use a story `play` function in `<Component>.tests.stories.tsx`. Pure logic and hooks: use Vitest `*.test.ts(x)` next to the source file.
- `corepack yarn storybook:test` runs every story that has a `play` function (Storybook auto-tags them `play-fn`, see root `vitest.config.mts`). To opt out, use `tags: ["skip-test"]` with a reason.
- Play-test files set `parameters: { chromatic: { disable: true } }`. Visual stories feed Chromatic; use `renderStoriesForChromatic` from `utils/` when adding visual variants.
- Import from `@storybook/react-vite` and `storybook/test`, not `@storybook/react` or `@storybook/test`.
- Query by role and accessible name (`getByRole("button", { name })`). Avoid snapshots unless nearby tests already use them.
- Cover the main path and meaningful edge cases in the nearest existing story/play-test where they belong. Prefer combining overlapping cases over adding redundant stories; add a separate story for a distinct user-visible scenario.
- A behavior change updates the nearest story and test in the same change. Use unit tests for small utilities and pure logic; use Storybook play tests for UI behavior.
- Describe behavioral coverage as starting state → action → observable result. Assert relevant intermediate states so a no-op cannot pass as success; pair a negative/blocked case with a successful control when it helps distinguish the behavior.
- Match assertions to timing: assert synchronous outcomes immediately and wait only for behavior that is contractually eventual. When changing ARIA relationships, verify the referenced element and resulting relationship, not just that an attribute exists.
- Before removing or consolidating tests, map each meaningful old scenario/assertion to its replacement story or test. Keep coverage that still represents a supported behavior; explain when a scenario is intentionally obsolete.
- For state, accessibility, keyboard or focus behavior, use the [component-contract skill](../skills/component-contract/SKILL.md) to identify the contract and evidence to preserve.
- ds-react unit tests run with `TZ=UTC`. Date logic must not depend on the local timezone.
- Browser tests need Playwright Firefox (Chromium under Copilot CLI, see `vitest.config.mts`). Yarn has `enableScripts: false`, so if the browser is missing, report it instead of installing globally.
@@ -1,13 +0,0 @@
---
description: "@navikt/ds-tokens and @navikt/ds-tailwind: token sources, generated output, build order."
applyTo: "@navikt/core/tokens/**,@navikt/core/tailwind/**"
---
# Tokens and Tailwind
- Token source of truth: `@navikt/core/tokens/src/tokens/**` (TypeScript), built with Style Dictionary (`src/index.ts`). Generated output: `dist/`, `token_docs.js`, and in tailwind `tailwind.config.js` and `tailwind4.css`. Never edit generated output; they're untracked.
- Public CSS variables are `--ax-*`. Renaming or removing a token is breaking: it needs a major changeset plus a token migration in `@navikt/aksel` (see `src/codemod/v8-tokens/`) when consumers must migrate.
- Light and dark themes are both generated. Check both when changing semantic color tokens.
- Tailwind is built from `@navikt/ds-tokens/js`, so build tokens first: `corepack yarn workspace @navikt/ds-tokens build`, then `corepack yarn workspace @navikt/ds-tailwind build`.
- Tests: `corepack yarn workspace @navikt/ds-tokens test` and `corepack yarn workspace @navikt/ds-tailwind test`.
- `build` also generates the Figma config/plugin (`src/figma`). Changes there affect design tooling. Mention it in the PR.
@@ -1,15 +0,0 @@
---
description: "aksel.nav.no Next.js website: routers, examples/templates, Sanity data."
applyTo: "aksel.nav.no/website/**"
---
# Website (aksel.nav.no)
- Both routers are in use: `app/` (App Router, `cacheComponents: true` in `next.config.ts`) and `pages/` (examples, templates, legacy). Stay in the router already used nearby. Don't move routes between them.
- In `app/`, dynamic data needs Suspense or `"use cache"`, following nearby code. Cache Components errors at build time are real failures.
- For website build, runtime, or performance issues, use the `website-debugging` skill to establish the cause before changing code.
- `pages/eksempler/**` and `pages/templates/**` are public code snippets. Use the `website-example` skill. Oxlint rules `aksel-local/{import-check,comment-check,args-check}` enforce their shape.
- Reuse `@/` utils and components nearby before adding abstractions.
- Examples/templates: `corepack yarn storybook:aksel` (no secrets). `corepack yarn dev` only for app behavior; it needs `SANITY_READ_NO_DRAFTS` and `NPM_AUTH_TOKEN`.
- Never run `update:*`, `backup` or other Sanity-writing scripts unless asked. They write to the CMS.
- Tests: `corepack yarn workspace website test`. Examples: `corepack yarn workspace website validate:eksempler`.
-22
View File
@@ -1,22 +0,0 @@
{
"lspServers": {
"typescript": {
"command": "node",
"args": [
"./node_modules/typescript-language-server/lib/cli.mjs",
"--stdio"
],
"fileExtensions": {
".ts": "typescript",
".tsx": "typescriptreact",
".js": "javascript",
".jsx": "javascriptreact",
".mjs": "javascript",
".cjs": "javascript",
".mts": "typescript",
".cts": "typescript"
},
"rootUri": "."
}
}
}
-56
View File
@@ -1,56 +0,0 @@
---
name: add-changeset
description: >
Create a Changesets entry for a user-facing change to a published Aksel
package without the interactive `yarn changeset` prompt. Use when a change
touches @navikt/ds-react, ds-css, ds-tokens, ds-tailwind, aksel-icons,
@navikt/aksel or aksel-stylelint and needs a version bump or changelog entry,
or when asked to "add a changeset".
argument-hint: "Package(s) and a one-line summary of the change"
---
# Add changeset
`yarn changeset` is interactive and will hang an agent. Write the file directly.
## 1. Decide if a changeset is needed
Needed: any consumer-visible change in a published package: behavior, props, CSS output, tokens, icons, CLI/codemods, types, or bug fixes.
Not needed:
- Changes only in ignored workspaces (`.changeset/config.json` → `ignore`: website, sanity-studio, playroom, mcp, tooling, figma plugin).
- Stories, tests, internal refactors with identical output, docs and repo tooling.
## 2. Pick the bump
| Bump | When |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `patch` | Bug fix, style fix, internal change with visible effect, new icon keyword |
| `minor` | New component, prop, export, token, icon or codemod; new preview component |
| `major` | Removed/renamed prop, export, CSS class or token, or changed default. Only when explicitly agreed: needs a codemod or migration notes |
The packages in `config.json` `fixed` release together, so one bump moves all of them to the same version. Still list only the packages whose code changed.
## 3. Write the file
Path: `.changeset/<three-random-lowercase-words>.md` (e.g. `brave-owls-sing`). The slug must not already exist.
```md
---
"@navikt/ds-react": minor
"@navikt/ds-css": minor
---
Lookup: New component `Lookup`
```
Rules:
- The summary starts with `<Component or area>:` (e.g. `Button:`, `Tokens:`, `Icons:`, `CLI:`), then what changed **for consumers**. One changeset per logical change.
- Optional gitmoji after the colon is fine (`:tada:` for new components), matching nearby entries.
- Don't edit `CHANGELOG.md`, package versions or `config.json`.
## Done when
The file exists, its frontmatter lists only changed published packages with valid bumps, and Prettier leaves it unchanged (`./node_modules/.bin/prettier --check .changeset/<file>.md`, or `corepack yarn prettier --check ...`).
@@ -1,108 +0,0 @@
---
name: aksel-local-validation
description: >
Select and run deterministic local validation for changes in the Aksel
monorepo. Use when linting, type-checking, testing, building, formatting, or
deciding which Yarn workspace commands cover changed files.
argument-hint: "Changed files or feature area to validate"
---
# Aksel local validation
Validate the smallest relevant surface with repository-pinned tools. Expand only when the result or dependency graph requires it.
## Hard rules
- Run repository commands through `corepack yarn`.
- Prefer an existing root or workspace script. Otherwise use a binary already pinned in the root/workspace dependency graph.
- Never use `npx`, `pnpx`, `pnpm dlx`, `yarn dlx`, a global binary, or a curl-piped installer.
- Do not add a dependency only to run a one-off check. If the task truly needs new tooling, add and lock it explicitly.
- Public consumer docs may intentionally show `npx @navikt/aksel`. Preserve those examples unless the task changes consumer setup.
- If Corepack/Yarn reports a user-level configuration error, stop and report it. Never edit configuration outside the repository.
- Do not claim type safety from Oxlint or tests. Run a TypeScript-aware command when TypeScript behavior changed.
## 1. Determine affected projects
Start from changed files, not the whole repository:
```sh
git diff --name-only --diff-filter=ACMR
git diff --cached --name-only --diff-filter=ACMR
git ls-files --others --exclude-standard
```
For each file, read its nearest `package.json`, `tsconfig.json`, test, story, and export surface before choosing commands.
| Path | Workspace | Preferred package checks |
| ------------------------------- | -------------------------- | ---------------------------------------------------------------------------------- |
| `@navikt/core/react/**` | `@navikt/ds-react` | `test` (Vitest + `tsc` type-check); `build` for export/type changes |
| `@navikt/core/css/**` | `@navikt/ds-css` | no `test` script: targeted `stylelint`, then `build` for imports/layers |
| `@navikt/core/tokens/**` | `@navikt/ds-tokens` | `test`, then `build` for generated output |
| `@navikt/core/tailwind/**` | `@navikt/ds-tailwind` | build `@navikt/ds-tokens` first, then `test` and `build` |
| `@navikt/aksel-icons/**` | `@navikt/aksel-icons` | `test`; `build` for generation/export changes |
| `@navikt/aksel-stylelint/**` | `@navikt/aksel-stylelint` | `test` |
| `@navikt/aksel/**` | `@navikt/aksel` | `build` **before** `test` (`migrations.test.ts` reads `dist/`) |
| `aksel.nav.no/website/**` | `website` | `test`; `validate:eksempler` for examples/templates; avoid `build` (needs secrets) |
| `aksel.nav.no/sanity-studio/**` | `aksel-sanity-studio` | `type-check`, then `test` when behavior changes |
| `apps/playroom/**` | `aksel-playroom` | no `test` script: `sync-imports` when ds-react exports change |
| `apps/mcp-server/**` | `aksel-mcp` | `test`, then `build` |
| `apps/figma-icon-plugin/**` | `aksel-icons-figma-plugin` | no `test` script: `build:plugin` |
| `tooling/changelog/**` | `aksel-changelog` | `typecheck` |
| `tooling/analyzer/**` | `aksel-analyzer` | `test` |
Read the current manifest before running a table command. A manifest is source of truth if scripts change.
## 2. Run changed-file lint
Group only matching files:
```sh
corepack yarn oxlint --deny-warnings <changed .ts/.tsx/.js/.jsx files>
corepack yarn stylelint <changed .css files>
```
Do not pass unsupported file types to make a command appear comprehensive. Documentation-only changes need no code validation unless a documentation check exists.
## 3. Run TypeScript validation
Use nearest applicable project:
```sh
corepack yarn tsc --noEmit --incremental false --project path/to/tsconfig.json
```
`--incremental false` prevents validation from creating `.tsbuildinfo` files. For a composite project, use its workspace build/type-check script instead.
Prefer the workspace's own `type-check`, `typecheck`, or `build` script instead when it generates inputs, uses a build-specific tsconfig, or performs required transforms:
```sh
corepack yarn workspace <workspace> type-check
corepack yarn workspace <workspace> typecheck
corepack yarn workspace <workspace> build
```
If no suitable TypeScript command exists, report the gap. Do not substitute lint as a type-check.
## 4. Run behavior tests
Run the closest test file or workspace test command supported by that package. For visual or interaction changes, include the nearest story and Storybook browser test required by repository instructions.
Examples:
```sh
corepack yarn workspace @navikt/ds-react test
corepack yarn workspace website test
corepack yarn workspace aksel-mcp test
```
## 5. Escalate deliberately
Escalation order:
1. changed-file lint;
2. nearest TypeScript project;
3. focused test or affected workspace script;
4. dependent workspace build/test;
5. root `lint`, `test`, or `boot` only for cross-workspace changes, shared configuration, generated public output, or a failure that requires broader evidence.
Stop when every behavior and type surface touched by the change has a passing check. Report pre-existing or environment failures exactly; do not hide them with a weaker fallback.
-143
View File
@@ -1,143 +0,0 @@
---
name: code-review
description: "Review Aksel changes or pull requests for consumer-breaking API changes, React 17/SSR compatibility, accessibility, export wiring, security, tests and changesets. Use when reviewing a branch, diff or pull request."
argument-hint: "PR number, branch or files to review"
---
# Aksel code review
## Repository context
- Aksel is Nav's design system. The published `@navikt/*` packages are used across Nav on React 17–19, with SSR and CSR, in light and dark themes.
- One bad release breaks many teams at once. Public API, accessibility and compatibility matter most.
- Report findings. Don't edit code unless asked.
## Process
1. **Scope**: identify what the user wants reviewed.
- PR: get `gh pr diff <n>` and the PR description. Note the stated goal.
- Branch: compare against its stated base (for example, `git diff origin/main...HEAD`).
- Current worktree or "my changes": inspect staged, unstaged, and relevant untracked files. Do not substitute the committed branch diff for worktree changes.
```sh
git status --short
git diff
git diff --cached
git ls-files --others --exclude-standard
```
Read in-scope untracked files too. If the requested review target is unclear, ask before choosing a scope.
2. **Read**: for every changed file, read the surrounding code, component `index.ts`, stories, tests and the matching `.github/instructions/*.instructions.md`. Never review from the diff alone.
3. **Check**: run the smallest validation that covers the change (`aksel-local-validation` skill). Tool output replaces style comments.
4. **Analyze** against the checklist below.
5. **Report** in the output format.
For changes to component state, callbacks, async behavior, composition, semantics, keyboard or focus behavior, use the [component-contract skill](../component-contract/SKILL.md) to trace the affected contract and identify observable evidence.
## Priorities
- 🔴 **Blocker**: must fix before merge. Breaks consumers, regresses accessibility, security issue or wrong behavior.
- 🟡 **Suggestion**: should fix. Missing test, docs or changeset, maintainability risk, over-editing.
- 💭 **Nit**: optional. Leave it out when in doubt.
Skip anything Oxlint, Stylelint, Prettier or TypeScript already catches. No praise-only or speculative comments. Every finding says **why** it matters and what to do.
## Checklist
### Public API (🔴)
- Removed or renamed prop, export, CSS class, token or CLI command, or a changed default, on something already published → needs a `major` changeset, a deprecation path, or a revert.
- New props and exports follow nearby naming and defaults. Prefer additive changes. Colors go through `data-color`, not new `variant` values.
- Public props and components have JSDoc. `@default` and `@example` match behavior.
### Compatibility (🔴)
- No React 18/19-only APIs in `@navikt/*` (peer `react ^17 || ^18 || ^19`). Use the helpers in `@navikt/core/react/src/utils-external/` (`useId`, `useClientLayoutEffect`).
- SSR-safe: no `window`, `document` or `localStorage` at module top level or during render.
- No new runtime dependency in a published package without a clear reason (bundle size, consumer installs).
### Accessibility (🔴)
- Semantic HTML first. Correct roles, accessible names and states.
- Full keyboard support, logical focus order, focus returned after close, visible `:focus-visible`.
- Works in `forced-colors`, dark theme and at 200% zoom. Motion relies on the global `prefers-reduced-motion` handling.
- User-facing strings use `useI18n`, with keys in `nb`, `nn` and `en` under `@navikt/core/react/src/utils/i18n/locales/`.
- Review only the interaction pattern in scope; do not apply modal focus or dismissal rules to unrelated popup patterns. Check changed accessible names and relationships against their rendered targets.
### Wiring (🔴 for new public components)
- Stable export: component `index.ts` → `@navikt/core/react/src/index.ts` → `package.json` `exports`. Preview: `@navikt/core/react/src/preview.ts` plus `"./PREVIEW/<Name>"`.
- New CSS file is imported in `@navikt/core/css/src/index.css` with a `layer(...)`.
- Removed published CSS classes are listed in `@navikt/aksel-stylelint/src/deprecations.ts`.
### CSS and tokens (🟡)
- `.aksel-<component>` prefix. State through `data-*` or ARIA selectors, not modifier classes.
- Tokens only (`--ax-*`, internal `--__axc-*`). No hardcoded colors, spacing or radii.
- Renaming or removing a token is breaking (🔴). Semantic color changes are checked in both light and dark.
### Security and performance
- 🔴 No `dangerouslySetInnerHTML` or unsanitized HTML from props. No secrets or `process.env` values leaking into published bundles or website client code.
- 🔴 GitHub Actions: minimal `permissions`, new third-party actions pinned to a commit SHA, no untrusted input (`github.event.*`) interpolated into `run:`.
- 🟡 No top-level side effects in library modules (breaks tree-shaking). No avoidable re-renders: stable context values, no new object or function props per render in hot paths.
### Tests and docs (🟡)
- Complex logic with a higher risk of breaking has a story `play` test or Vitest test proving the user-facing contract. Queries use role and accessible name.
- Tests are deterministic: no dependence on timezone, current time or randomness.
- Edge cases that are easy to miss: controlled vs. uncontrolled, `ref`/`className`/`...rest` forwarding, `as` prop, empty and very long content.
- For behavior changes, check that tests distinguish the intended outcome from a no-op and cover relevant intermediate states. When tests are consolidated or removed, verify meaningful old scenarios have replacement coverage.
- Website examples (`aksel.nav.no/website/pages/eksempler/`) are updated when usage changes.
### Component contracts and public consumer impact
- `COMP-01` **Contract evidence (🟡; 🔴 if behavior is broken):** For changed state, callbacks, async behavior, composition, keyboard or focus behavior, use the [component-contract skill](../component-contract/SKILL.md). Check that a nearby story or test demonstrates the changed user-visible outcome; if coverage is missing, request it.
- `COMP-02` **Public impact (🟡; 🔴 if a consumer contract regresses):** For changed hooks, contexts, CSS or tokens, trace affected exported components and inspect a representative consumer-visible contract. Do not stop at the implementation diff; request a public-boundary example or test when impact is not demonstrated.
- `COMP-03` **Generated API metadata (🟡; 🔴 if published API is wrong):** For public prop or JSDoc changes, check the relevant generated props metadata as well as exports and tests; request the documented metadata update or correction when stale.
### Release (🟡, 🔴 if breaking)
- User-facing change to a publishable package has a `.changeset/*.md` with the right bump and a `<Component>: <consumer-facing text>` summary. Website, playroom and tooling changes get none.
- Breaking change that consumers can migrate mechanically has a codemod in `@navikt/aksel/src/codemod/transforms/`, registered in `@navikt/aksel/src/migrations-config.ts`.
### Scope (🟡)
- Over-editing: diff out of proportion to the stated goal (unrelated renames, refactors, reformatting, extra validation). Tests don't catch it, and it makes the diff hard to review.
- No unrelated changes bundled. No skipped tests (`skip`, `!play-fn`) without a stated reason.
- AI-generated code: patterns copied without adapting to Aksel conventions, error handling that looks right but isn't, missing edge cases. Ask the author to explain non-obvious design choices.
## Output format
```md
### Summary
What the change does, overall verdict and key risks (1–3 sentences).
### Evidence and replay
- **Inspected:** relevant files, stories and behavior states reviewed.
- **Checks run:** exact command and result, or `Not run` with reason.
- **Not inspected:** relevant surfaces or behavior not verified.
- **Reviewer replay:** starting story/URL, action and expected result, or `Not needed`. Label unexecuted steps as unverified.
### Findings
| File | Line | Priority | Issue |
| ------------ | ---- | -------- | ------------------------------------------------------- |
| `Button.tsx` | 42 | 🔴 | `React.useId` breaks React 17: use internal `useId` |
| `button.css` | 10 | 🟡 | Hardcoded `#fff`: use `--ax-text-*` token for dark mode |
### Details
For each 🔴: why it matters and a suggested fix (short code snippet if helpful).
```
No findings: say so and list what you checked.
## Boundaries
- **Always**: read the actual code, run checks before reporting, order findings 🔴 → 🟡 → 💭.
- **Ask first**: architectural changes, adding or removing dependencies, reviewing outside this repository.
- **Never**: auto-fix or push, approve code you haven't read, skip the Public API, accessibility or security checks.
@@ -1,36 +0,0 @@
---
name: component-contract
description: "Define and verify component behavior, state ownership, accessibility and compatibility before changing a public component contract. Use when adding or changing controlled state, callbacks, async transitions, composition, keyboard or focus behavior, semantics, or consumer-facing props."
---
# Component contract
Use this workflow for the behavior affected by a component change. Do not turn every dimension below into a new requirement when the change does not touch it.
## Establish the existing contract
Read the component folder, including its exports, metadata, stories, tests and relevant CSS. Trace related hooks, contexts and callers when the behavior crosses component boundaries. Preserve established behavior unless the requested change intentionally changes it.
For the affected behavior, record a compact contract:
| Area | Define when relevant |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| State ownership | Controlled and uncontrolled inputs, defaults, source of truth, and what callbacks signal. A controlled component must reflect the owner's prop; do not assume a requested update was accepted. |
| Async transitions | Relevant pending, success, failure, cancellation or stale-result behavior, and what the user sees. |
| Composition | Which component or context owns registration, event handling and cleanup; how nesting or portals affect the behavior. |
| Accessibility | Native semantics, accessible name/content responsibility, roles, states and relationships. Identify what the library guarantees and what the consumer must provide. |
| Keyboard and focus | Supported keys, focus entry/movement/return, and whether each outcome is synchronous or eventual. Apply pattern-specific behavior only where that pattern is used. |
| Compatibility | Public API and React 17–19 / SSR constraints affected by the change. |
Keep the contract proportional to the change. Use existing documentation, JSDoc, stories and tests to record decisions; do not create a new metadata format or a separate document for every component.
## Verify the contract
- Identify the closest existing story or test that demonstrates each changed behavior. Prefer observable public-component outcomes over assertions on implementation details.
- Check that the starting state and intermediate outcome distinguish the intended behavior from a no-op.
- Cover meaningful accepted and rejected controlled updates, async outcomes, or nested interactions when they are part of the change.
- Verify accessible names and relationships against the actual rendered elements when those semantics change. Do not infer accessibility from attribute presence alone.
- Separate automated interaction evidence from visual inspection or manual assistive-technology checks; report what was not verified.
- Use `aksel-local-validation` to select commands. This skill defines behavior, not a separate test or validation process.
Do not copy another library's state semantics, API, React hooks or architectural layers as Aksel's contract. Follow Aksel's existing component and compatibility instructions.
@@ -1,304 +0,0 @@
---
name: ds-component-restructure
description: "Restructure a ds-react component's files into canonical folder/naming conventions. Use when reorganizing a component's file layout, splitting a monolithic component file, or renaming files to match domain conventions."
argument-hint: "Path to component folder, e.g. @navikt/core/react/src/accordion"
---
# ds-component-restructure
Restructure a `ds-react` component folder to follow canonical conventions — without breaking the public API.
## Canonical Structure
```
<component>/
├── index.ts # "use client" + re-exports from root
├── <Component>.stories.tsx # Storybook stories
├── <Component>.tests.stories.tsx # Storybook play-tests (only if play tests exist)
├── <Component>.meta.ts # Meta info for component. Used for documentation generation.
├── root/
│ ├── <Component>Root.tsx # Main compound component + sub-component namespace
│ ├── <Component>Root.context.ts # Context (if shared across sub-components)
│ ├── <Component>Root.test.ts # Unit tests for root (if needed)
│ └── use<Hook>.ts # Files for root-level hooks (if needed)
├── <subcomponent>/ # One dir per sub-component
│ ├── <Component><Subcomponent>.tsx
│ ├── <Component><Subcomponent>.test.ts # Unit tests for sub-component (if needed)
│ └── use<Hook>.ts # Files for sub-component-level hooks (if needed)
├── helpers/
│ ├── <helper-name>.ts
│ └── <helper-name>.test.ts # Unit tests co-located with helper
└── <Component>.types.ts # Shared types (if needed across multiple files)
```
### Naming Rules
| What | Pattern | Example |
| ---------------------- | --------------------------------------- | ----------------------------- |
| Root component file | `<Component>Root.tsx` | `AccordionRoot.tsx` |
| Sub-component file | `<Component><Subcomponent>.tsx` | `AccordionItem.tsx` |
| Internal sub-component | `<Component><Subcomponent>Internal.tsx` | `DialogBackdropInternal.tsx` |
| Sub-component dir | kebab-case sub-component name | `item/` |
| Context file | `<Component>Root.context.ts` | `AccordionRoot.context.ts` |
| Shared types file | `<Component>.types.ts` | `Accordion.types.ts` |
| Stories | `<Component>.stories.tsx` | `Accordion.stories.tsx` |
| Play-test stories | `<Component>.tests.stories.tsx` | `Accordion.tests.stories.tsx` |
| Unit test file | `<filename>.test.ts(x)` | `useAccordion.test.ts` |
| Meta file | `<Component>.meta.ts` | `Accordion.meta.ts` |
| `index.ts` | always `index.ts` | `index.ts` |
## Procedure
### 1. Read the Component
Read every file in the component folder — `index.ts`, all `.tsx`/`.ts` source files, meta file, stories, and test stories. Ignore inconsistencies in file naming and structure; the goal is to fix those, not preserve them. Use the `component`-field in the meta file to determine the proper naming if unsure.
**Done when:** every file's role (component, hook, type, context, test, metadata) is known, and every symbol currently exported from `index.ts` is recorded.
### 2. Map Files to Target Structure
Build this table before touching any files:
| Old path | New path | Renamed? | Publicly exported? | Importers to update |
| ------------------- | --------------------------- | -------- | ------------------ | --------------------- |
| `DialogTrigger.tsx` | `trigger/DialogTrigger.tsx` | No | Yes | `root/DialogRoot.tsx` |
| ... | | | | |
For each file:
- Determine new path and whether it moves/renames
- Check whether content needs splitting (e.g. types → `<Component>.types.ts`)
- List every file that imports it (grep for the filename or exported symbol)
- Flag circular dep risks: context importing sub-components, helpers importing component files, types importing non-types
Resolve circular deps before proceeding: move shared pieces to a lower-level file (types → `.types.ts`, utilities → `helpers/`).
**Done when:** every file in the component folder has a row in the table, and every circular-dependency risk is resolved or explicitly noted.
### 3. Create New Files
For each row in the mapping table where the path changes:
1. Create the file at the new path with all import paths updated to reflect new locations
2. Update every file in the "Importers to update" column — use the mapping table, don't rely on memory
3. Do NOT delete old files yet
> **Case-only renames** (path differs only in letter casing, e.g. `heading.stories.tsx` → `Heading.stories.tsx`) must NOT use the create-new + delete-old flow. On case-insensitive filesystems (macOS and Windows default) the two paths resolve to the **same file**, so creating the new file overwrites the old and the later "delete old" step destroys your content — and git records no rename. Handle these with the two-step `git mv` in the [Case-only renames](#case-only-renames) edge case instead.
### 4. Update Stories
Move stories to component root dir. If stories import from internal paths, update those paths.
**Exception:** A `stories/` subdirectory is acceptable if it already exists — do not flatten it.
**Casing:** Story filenames follow `<Component>.stories.tsx` (PascalCase). When the existing file is lowercase (e.g. `heading.stories.tsx`), this is a **case-only rename** — follow the [Case-only renames](#case-only-renames) procedure, do not create + delete.
### 5. Update `index.ts`
All exports are **named** — no `default` exports anywhere. For compound components, the root file is the single export source: it assembles the compound component, imports every publicly exported sub-component and its prop types, and exports all public values and types together. The component `index.ts` only re-exports these names from `./root/<Component>Root` — never directly from sub-component files or shared type files.
```ts
"use client";
export {
<Component>, // compound root (named)
<Component>Trigger, // sub-components (named)
// ...
} from "./root/<Component>Root";
export type {
<Component>Props,
<Component>TriggerProps,
// ...
} from "./root/<Component>Root";
```
Group values first, then types. If types live in `<Component>.types.ts`, import them into the root file and include them in its type export block. The `index.ts` still re-exports them from the root file.
#### Compound root file export pattern
In `<Component>Root.tsx`, assemble the compound component with `Object.assign` — **do not** use an `as <Component>Component` cast or a dedicated `<Component>Component` interface:
```tsx
import {
<Component>Trigger,
type <Component>TriggerProps,
} from "../trigger/<Component>Trigger";
const <Component>Root = forwardRef<HTMLDivElement, <Component>Props>((props, ref) => {
// ...
});
/**
* Component-level JSDoc (description, `@see`, `@example`) goes directly
* above the `Object.assign` — this is the exported `<Component>` value.
*/
const <Component> = Object.assign(<Component>Root, {
/**
* @see 🏷️ {@link <Component>TriggerProps}
*/
Trigger: <Component>Trigger,
// ...
});
export { <Component>, <Component>Trigger };
export type { <Component>Props, <Component>TriggerProps };
```
Follow `accordion/root/AccordionRoot.tsx`: export the imported sub-component values and prop types alongside the compound component at the bottom of the root file. Do not add internal-only sub-components to the public export chain. Sub-components must not import from the root file or component `index.ts`; use shared type/context/helper files to avoid circular dependencies.
Why `Object.assign` instead of `as`: the cast types the const as an interface that has no runtime `valueDeclaration`, so `react-docgen-typescript` (used by `yarn docgen:meta`) fails to extract props and can't document the component when it is exported via a grouped `export { }` statement. `Object.assign` lets TypeScript infer the intersection type from the value, so metadata generation works with named, bottom-of-file exports.
Sub-component files follow the same rule — named value export plus named type export at the bottom, no inline `export interface` and no default:
```tsx
interface <Component>TriggerProps extends React.HTMLAttributes<HTMLDivElement> {
// ...
}
const <Component>Trigger = forwardRef<HTMLDivElement, <Component>TriggerProps>(/* ... */);
export { <Component>Trigger };
export type { <Component>TriggerProps };
```
### 6. Update Meta File
Update imports in `<Component>.meta.ts` to reflect new paths. If the meta file imports from the component's `index.ts`, no changes are needed.
If component has no meta file, create `<Component>.meta.ts` in the component root. Copy the shape of `@navikt/core/react/src/accordion/Accordion.meta.ts`: import from `./index`, `name`, a `components` map using dotted keys for sub-components (`"Accordion.Item": AccordionItem`), and `keywords`, then `export { metadata }`.
Note that each standalone component should have a meta file. But a "sub-component" (like `AccordionItem`) does not need a meta file.
### 7. Validate No Breaking Changes
**Export diff.** Before deleting old files, compare the exported names recorded during Step 1 against the new `index.ts`. Every name must be present:
- Exports in new index.ts that are missing from original → regression
- Exports in original that are missing from new index.ts → missing export (breaking change)
Concretely: grep for `export` lines in both versions and diff them. All component names and type names must match exactly.
**Build.** Run `corepack yarn workspace @navikt/ds-react build` — must succeed with no TypeScript errors.
**Package root.** Verify `@navikt/core/react/src/index.ts` still re-exports the component. No changes needed there unless the component's `index.ts` path changed (it shouldn't).
**Root-only re-exports.** For compound components, verify every value and type export in the component `index.ts` comes from `./root/<Component>Root`, and every forwarded name is exported by that root file.
**No case-duplicate tracked paths.** This is the most common restructure bug. On case-insensitive filesystems (macOS, Windows) git can end up tracking **two** entries that differ only in casing (e.g. `accordion.stories.tsx` **and** `Accordion.stories.tsx`) while the working tree shows only one file. The stale entry then breaks tests and checkout on other machines. `ls` / file search will NOT reveal it — you must ask git. Run:
```sh
git ls-files | awk '{l=tolower($0); if(seen[l]++) print "CASE-DUP:", $0}'
```
Any output is a leftover. Remove the stale entry from the index by its **exact old path** (this does not touch the correctly-cased file on disk):
```sh
git rm --cached '<exact old lowercase path>'
```
Then confirm only the intended casing remains, e.g. `git ls-files '*<component>*stories*'`.
**Done when:** the export diff is empty, the build succeeds, the package-root re-export is verified, and `git ls-files` reports no case-duplicate paths.
### 8. Remove Old Files
Only after verifying new files are correct and `index.ts` exports match the original, delete the old files.
Skip any rows already moved with `git mv` (case-only renames and other `git mv` moves) — there is no separate old file to delete for those.
### 9. Unit Tests
Unit tests live next to the file they test — same directory, same base name with `.test.ts` / `.test.tsx` suffix.
| What to test | File location |
| ---------------------- | --------------------------------------------------- |
| Helper utility | `helpers/<helper-name>.test.ts` |
| Sub-component logic | `<subcomponent>/<Component><Subcomponent>.test.tsx` |
| Root component / hooks | `root/<Component>Root.test.tsx` |
| Context helpers | `root/<Component>Root.context.test.ts` |
Do NOT move existing test files unless their source file moves. When moving a source file, move its test file with it and update import paths.
## Edge Cases
### Case-only renames
A rename that changes **only letter casing** (e.g. `heading.stories.tsx` → `Heading.stories.tsx`, or `accordion.tsx` → `Accordion.tsx`) is a trap on case-insensitive filesystems (macOS and Windows defaults):
- Both paths point to the **same** file, so `create_file` at the new path overwrites the old content, and the Step 8 "delete old" then removes it entirely.
- git does not see a rename (`core.ignorecase=true`), so history is lost and other machines get checkout/case-collision conflicts.
Do the rename in **two `git mv` steps through a temporary distinct name** so git records it and it works on every filesystem:
```sh
git mv heading.stories.tsx heading.stories.tmp.tsx
git mv heading.stories.tmp.tsx Heading.stories.tsx
```
Then edit the file's contents/imports in place. Skip the create-new + delete-old flow for these rows entirely.
Guidelines:
- Prefer `git mv` for **all** moves/renames (not just case-only) so history is preserved; only fall back to create + delete when the file content is being split or substantially rewritten.
- Detect case-only renames while building the Step 2 mapping table: compare old vs new path lowercased — if they're equal but the raw paths differ, mark the row as case-only.
- The temporary name must differ by more than case (add `.tmp`), otherwise the same collision reoccurs.
### Flat components without sub-components
Leave flat components flat - do NOT introduce subdirs unless the component contains internal utility-components (files that render JSX but are not public sub-components). If such a utility-component exists, move it to its own subdir following the canonical naming.
Example: `button/Button.tsx` has no other sub-components, so should stay in root.
### `parts/` directory
If the component uses a `parts/` dir, break it up into separate sub-component dirs.
### Internal sub-components
Sub-components that are implementation details (not exported publicly) use the same dir/file naming as public ones (see Naming Rules) with an `Internal` suffix, but must NOT be exported from `index.ts`:
```
backdrop/
└── DialogBackdropInternal.tsx ← internal, NOT exported from index.ts
```
### Nested subcomponents
Do NOT nest subcomponent dirs inside other subcomponent dirs. All sub-component dirs must be direct children of the component root:
```
# Wrong
dialog/
└── header/
├── DialogHeader.tsx
└── title/
└── DialogTitle.tsx
# Correct
dialog/
├── header/
│ └── DialogHeader.tsx
└── title/
└── DialogTitle.tsx
```
## Constraints
- **No breaking changes.** Public export names and prop shapes must be identical after restructuring.
- **Named exports only.** No `default` exports; group named exports at the bottom of each file (values first, then types).
- **Compound exports flow through the root.** Component `index.ts` re-exports public values and types only from `root/<Component>Root.tsx`, not directly from sub-component or shared type files.
- **Compound roots use `Object.assign`.** No `as <Component>Component` cast or `<Component>Component` interface — it breaks `yarn docgen:meta` prop extraction.
- Component rules (React 17 compatibility, `"use client"`, `forwardRef`/`className`/`...rest`/`as`, tokens) live in `.github/instructions/ds-react-component.instructions.md`. They still apply.
- Keep JSDoc on public props and components. The component-level JSDoc block sits directly above the `Object.assign` call; sub-component `@see` tags go on the keys inside the `Object.assign` object literal.
## Reference Files
- Canonical example: `@navikt/core/react/src/dialog/`
- Package public API: `@navikt/core/react/src/index.ts`
- Component export pattern: `@navikt/core/react/src/accordion/root/AccordionRoot.tsx` and `@navikt/core/react/src/accordion/index.ts`
- Context pattern: `@navikt/core/react/src/dialog/root/DialogRoot.context.ts`
- Meta: `@navikt/core/react/src/accordion/Accordion.meta.ts`
-66
View File
@@ -1,66 +0,0 @@
---
name: website-example
description: >
Add or update a code example or template shown on aksel.nav.no
(aksel.nav.no/website/pages/eksempler/** or pages/templates/**). Use when
asked to create a component example, demo, usage snippet or page template
for the documentation site, or when an example fails the aksel-local Oxlint
rules or validate:eksempler.
argument-hint: "Component group and example name, e.g. button/loading"
---
# Website example
Examples are public copy-paste snippets. The code above the marker comment is shown verbatim on the site and opened in the sandbox.
## 1. Locate
- Component examples: `aksel.nav.no/website/pages/eksempler/<group>/<example-name>.tsx` (group = lowercase component, e.g. `lookup`).
- Page templates: `aksel.nav.no/website/pages/templates/<name>/...`.
- Read 1-2 sibling examples in the same group first and match their tone. Content is in Norwegian (bokmål).
## 2. Write the file
```tsx
import { Button } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
return <Button>Lagre</Button>;
};
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
export default withDsExample(Example);
/* Storybook story */
export const Demo = {
render: Example,
};
export const args: ExampleArgsT = {
index: 0,
desc: "Kort hint om når eksemplet brukes.",
};
```
Hard rules (enforced by `tooling/eslint` rules `aksel-local/*`):
- With the sandbox enabled (default), import only from `@navikt/*`, `react`, relative `../__parts*` helpers and exactly `@/web/examples/withDsExample`. Anything else breaks the "open in sandbox" button. Set `sandbox: false` in `args` only if it's unavoidable.
- The demo component must be named `Example` (the sandbox depends on it).
- The marker comment `// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE` sits directly above `export default`.
- `export const args: ExampleArgsT` with `index` (sort order within the group). Optional: `desc`, `title`, `sandbox: false`.
- `withDsExample(Example, { variant: "static" | ... })`: see its JSDoc for layout options.
## 3. Validate
```sh
corepack yarn oxlint --deny-warnings <file>
corepack yarn workspace website validate:eksempler
corepack yarn storybook:aksel # visual check at :6007, needs no secrets
```
Never run `update:eksempler`. It writes to the Sanity CMS. Tell the user that a maintainer syncs examples to Sanity and links a new group from the component page.
## Done when
Oxlint and `validate:eksempler` pass, and the story renders in `storybook:aksel`.
+50
View File
@@ -0,0 +1,50 @@
name: Deploy aksel.ansatt.dev.nav.no
on:
workflow_dispatch:
push:
branches:
- next
jobs:
build_and_deploy:
name: Build and deploy to dev-gcp
runs-on: ubuntu-latest
permissions:
packages: "write"
contents: "read"
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup and build Next.js application
uses: ./.github/actions/build-website
with:
use_cdn_assets: true
npm_auth_token: ${{ secrets.READER_TOKEN }}
nais_id_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
nais_project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
sanity_read_token: ${{ secrets.SANITY_READ }}
sanity_read_no_drafts_token: ${{ secrets.SANITY_READ_NO_DRAFTS }}
- name: Get complete tag
run: echo "TAG=DEV-$( date +%s )" >> $GITHUB_ENV
- uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
push_image: true
dockerfile: aksel.nav.no/Dockerfile
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy to dev-gcp
uses: nais/deploy/actions/deploy@master
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
RESOURCE: aksel.nav.no/nais-dev.yaml
CLUSTER: dev-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
+74
View File
@@ -0,0 +1,74 @@
name: Deploy aksel.nav.no
on:
push:
branches:
- main
paths:
- "aksel.nav.no/**"
env:
IMAGE: ghcr.io/navikt/aksel
jobs:
sync_data_with_sanity:
name: Update component data
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Run ts-docgen
run: yarn docgen
- name: Update sanity sync
env:
SANITY_WRITE: ${{ secrets.SANITY_WRITE }}
run: yarn workspace aksel.nav.no sanity:update
build_and_deploy:
name: Build and deploy to prod-gcp
runs-on: ubuntu-latest
permissions:
packages: "write"
contents: "read"
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup and build Next.js application
uses: ./.github/actions/build-website
with:
use_cdn_assets: true
production: true
npm_auth_token: ${{ secrets.READER_TOKEN }}
nais_id_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
nais_project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
sanity_read_token: ${{ secrets.SANITY_READ }}
sanity_read_no_drafts_token: ${{ secrets.SANITY_READ_NO_DRAFTS }}
- name: Get complete tag
run: echo "TAG=PROD-$( date +%s )" >> $GITHUB_ENV
- uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
push_image: true
dockerfile: aksel.nav.no/Dockerfile
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy to prod-gcp
uses: nais/deploy/actions/deploy@master
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
RESOURCE: aksel.nav.no/nais-prod.yaml
CLUSTER: prod-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
+61
View File
@@ -0,0 +1,61 @@
name: Deploy aksel-v7.nav.no
on:
workflow_dispatch:
push:
branches:
- v7-aksel.nav.no
jobs:
build_and_deploy:
name: Build and deploy to prod-gcp
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/v7-aksel.nav.no'
permissions:
packages: "write"
contents: "read"
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
- name: Install dependencies
run: cd v7-aksel.nav.no && yarn --immutable
shell: bash
env:
NPM_AUTH_TOKEN: ${{ secrets.READER_TOKEN }}
- name: Set production env var
run: |
echo "PRODUCTION=true" >> v7-aksel.nav.no/website/.env
shell: bash
- name: Build Next.js website
run: cd v7-aksel.nav.no/website && yarn build
shell: bash
- name: Get complete tag
run: echo "TAG=PROD-V7-$( date +%s )" >> $GITHUB_ENV
- uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
push_image: true
dockerfile: v7-aksel.nav.no/Dockerfile
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy to prod-gcp
uses: nais/deploy/actions/deploy@master
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
RESOURCE: v7-aksel.nav.no/nais-prod.yaml
CLUSTER: prod-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
-49
View File
@@ -1,49 +0,0 @@
name: Backup Sanity dataset
on:
workflow_dispatch:
schedule:
- cron: "0 0 */3 * *"
permissions:
contents: read
jobs:
execute:
name: Export and upload Sanity backup
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: yarn
cache-dependency-path: "yarn.lock"
- name: Install dependencies
run: cd aksel.nav.no/sanity-studio && yarn
env:
NPM_AUTH_TOKEN: ${{ secrets.READER_TOKEN }}
- name: Export dataset
run: cd aksel.nav.no/sanity-studio && yarn sanity:backup
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
- name: Authenticate with GCP
uses: "google-github-actions/auth@v3"
with:
credentials_json: "${{ secrets.gcp_credentials_prod }}"
- name: Upload backup to GCS bucket
uses: "google-github-actions/upload-cloud-storage@v3"
with:
path: ./aksel.nav.no/sanity-studio
destination: "aksel-website-prod/backups"
glob: "backup-*.tar.gz"
+39
View File
@@ -0,0 +1,39 @@
name: Backup sanity dateset to gcp
on:
workflow_dispatch:
schedule:
# 12.00 every 3 days
- cron: "0 0 */3 * *"
jobs:
execute:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # Required to retrieve git history
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: yarn
cache-dependency-path: "yarn.lock"
- name: Install dependencies
run: cd aksel.nav.no/website && yarn
env:
NPM_AUTH_TOKEN: ${{ secrets.READER_TOKEN }}
- name: Export dataset
run: cd aksel.nav.no/website && yarn backup
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
- name: Upload GCP bucket
uses: "google-github-actions/auth@v3"
with:
credentials_json: "${{ secrets.gcp_credentials_prod }}"
- uses: "google-github-actions/upload-cloud-storage@v3"
with:
path: ./aksel.nav.no/website
destination: "aksel-website-prod/backups"
glob: "backup-*.tar.gz"
-139
View File
@@ -1,139 +0,0 @@
name: E2E tests
on:
push:
branches: [main, next]
paths:
- "aksel.nav.no/**"
pull_request:
branches: [main, next]
paths:
- "aksel.nav.no/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
prebuild-app-for-playwright:
name: Build app for Playwright
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # Push E2E image to GHCR
env:
IMAGE_NAME: ${{ github.repository }}-e2e
REGISTRY: ghcr.io
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup and build Next.js application
uses: ./.github/actions/build-website
with:
use_cdn_assets: false
npm_auth_token: ${{ secrets.READER_TOKEN }}
nais_id_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
nais_project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
sanity_read_token: ${{ secrets.SANITY_READ }}
sanity_read_no_drafts_token: ${{ secrets.SANITY_READ_NO_DRAFTS }}
- name: Log in to the Container registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v7
with:
context: .
file: aksel.nav.no/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
outputs:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
run-playwright:
needs: [prebuild-app-for-playwright]
name: Playwright shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: read # Pull E2E image from GHCR
container:
image: mcr.microsoft.com/playwright:v1.61.1-noble
services:
testapp:
image: ${{ needs.prebuild-app-for-playwright.outputs.image }}
ports: ["3000:3000"]
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
SANITY_READ_NO_DRAFTS: ${{ secrets.SANITY_READ_NO_DRAFTS }}
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5]
shardTotal: [5]
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: yarn
cache-dependency-path: "yarn.lock"
- name: Install dependencies
run: yarn --immutable
shell: bash
env:
NPM_AUTH_TOKEN: ${{ secrets.READER_TOKEN }}
- name: Run Playwright
shell: bash
run: yarn e2e:docker --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
SANITY_READ_NO_DRAFTS: ${{ secrets.SANITY_READ_NO_DRAFTS }}
- name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: blob-report-${{ matrix.shardIndex }}
path: aksel.nav.no/website/blob-report
retention-days: 1
merge-reports:
name: Merge Playwright reports
if: ${{ !cancelled() }}
needs: [run-playwright]
runs-on: ubuntu-latest
steps:
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v8
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into HTML Report
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v7
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
retention-days: 14
-94
View File
@@ -1,94 +0,0 @@
name: Validation
on:
push:
branches:
- main
- next
pull_request:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
lint:
name: Lint (Oxlint, Stylelint etc.)
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Lint
run: yarn lint
validate-examples:
name: Validate component examples
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Validate example code
run: yarn validate:eksempler
test:
name: Unit and integration tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Test
run: yarn test
ui:
name: UI tests (Storybook)
runs-on: ubuntu-latest
timeout-minutes: 10
container:
image: mcr.microsoft.com/playwright:v1.61.1-noble
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Test UI
run: yarn storybook:test --max-workers=2
env:
HOME: /root
@@ -1,87 +0,0 @@
name: Visual regression
on:
push:
branches:
- "main"
paths:
- "@navikt/**"
pull_request:
branches: [main, next]
paths:
- "@navikt/**"
- "aksel.nav.no/website/pages/templates/**"
- ".storybook/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
chromatic:
name: Run Chromatic
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Chromatic
if: github.actor != 'dependabot[bot]'
uses: chromaui/action@latest
id: chromatic_tests
continue-on-error: false
with:
autoAcceptChanges: "main"
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true
buildScriptName: "build:storybook-chromatic"
onlyChanged: true
untraced: "**/package.json|yarn.lock|**/*.md"
externals: "**/core/css/src/**/*.css"
traceChanged: true
env:
STORYBOOK_GITHUB_SHA: ${{ github.sha }}
- name: Get commit sha
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]') && github.actor != 'dependabot[bot]'
id: vars
shell: bash
run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Find Comment
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]') && github.actor != 'dependabot[bot]'
uses: peter-evans/find-comment@v4
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: Storybook demo
- name: Create or update comment
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]') && github.actor != 'dependabot[bot]'
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
## [Storybook demo](${{steps.chromatic_tests.outputs.storybookUrl}}) / [Chromatic](${{steps.chromatic_tests.outputs.url}})
${{ steps.chromatic_tests.outputs.changeCount > 0 && format('### 📝 Changes to review: {0}', steps.chromatic_tests.outputs.changeCount) || '' }}
${{ steps.chromatic_tests.outputs.errorCount > 0 && format('### ❌ Stories with errors: {0}', steps.chromatic_tests.outputs.errorCount) || '' }}
${{ steps.chromatic_tests.outputs.interactionTestFailuresCount > 0 && format('### ❌ Failed interaction tests: {0}', steps.chromatic_tests.outputs.interactionTestFailuresCount) || '' }}
[${{ steps.vars.outputs.sha_short }}](https://github.com/navikt/aksel/commit/${{ steps.vars.outputs.sha_short }}) | ${{steps.chromatic_tests.outputs.componentCount}} components | ${{steps.chromatic_tests.outputs.specCount}} stories
edit-mode: replace
-150
View File
@@ -1,150 +0,0 @@
name: Bundle check (main branch)
on:
pull_request:
branches: [main]
paths:
- "@navikt/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
check-title:
name: Check PR title
runs-on: ubuntu-latest
outputs:
should-run: ${{ steps.check.outputs.should-run }}
steps:
- id: check
run: echo "should-run=${{ !contains(github.event.pull_request.title, 'Version Packages') }}" >> $GITHUB_OUTPUT
analyze-trunk:
name: Analyze main branch
runs-on: ubuntu-latest
needs: check-title
if: ${{ needs.check-title.outputs.should-run == 'true' }}
steps:
- name: Checkout main branch
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
ref: main
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Analyze main branch
run: yarn run:analyze-local-action
- name: Upload json report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: trunk-results.json
path: tooling/analyzer
retention-days: 1
analyze-local:
name: Analyze PR branch
runs-on: ubuntu-latest
needs: check-title
if: ${{ needs.check-title.outputs.should-run == 'true' }}
steps:
- name: Checkout PR branch
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Analyze PR branch
run: yarn run:analyze-branch-action
- name: Upload json report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: branch-results.json
path: tooling/analyzer
retention-days: 1
compare-against-trunk:
name: Compare bundles (PR vs main)
runs-on: ubuntu-latest
needs: [analyze-trunk, analyze-local]
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Download trunk json from GitHub Actions Artifacts
uses: actions/download-artifact@v8
with:
path: tooling/analyzer
name: trunk-results.json
- name: Download branch json from GitHub Actions Artifacts
uses: actions/download-artifact@v8
with:
path: tooling/analyzer
name: branch-results.json
- name: Analyze compare branches
run: yarn run:compare-bundles
- name: Upload comparison markdown to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: compare-markdown.md
path: tooling/analyzer
retention-days: 1
update-comment:
name: Post PR comment with results
runs-on: ubuntu-latest
needs: [compare-against-trunk]
permissions:
contents: read
pull-requests: write
steps:
- name: Download comparison markdown to GitHub Actions Artifacts
uses: actions/download-artifact@v8
with:
path: tooling/analyzer
name: compare-markdown.md
- name: Find Comment
uses: peter-evans/find-comment@v4
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: Bundle Analysis Result
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: tooling/analyzer/compare-markdown.md
edit-mode: replace
-85
View File
@@ -1,85 +0,0 @@
name: Bundle check (npm)
on:
pull_request:
branches: [main]
paths:
- "@navikt/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
check-title:
name: Check PR title
runs-on: ubuntu-latest
outputs:
should-run: ${{ steps.check.outputs.should-run }}
steps:
- id: check
run: echo "should-run=${{ contains(github.event.pull_request.title, 'Version Packages') }}" >> $GITHUB_OUTPUT
analyze:
name: Analyze npm packages
runs-on: ubuntu-latest
needs: check-title
if: ${{ needs.check-title.outputs.should-run == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Analyze Npm and local packages
run: yarn run:analyze-npm-action
- name: Run comparison analysis
run: yarn run:compare-bundles
- name: Upload comparison markdown to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: compare-markdown.md
path: tooling/analyzer
retention-days: 1
update-comment:
name: Post PR comment with results
runs-on: ubuntu-latest
needs: [analyze]
permissions:
contents: read
pull-requests: write
steps:
- name: Download comparison markdown to GitHub Actions Artifacts
uses: actions/download-artifact@v8
with:
path: tooling/analyzer
name: compare-markdown.md
- name: Find Comment
uses: peter-evans/find-comment@v4
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: Bundle Analysis Result
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body-path: tooling/analyzer/compare-markdown.md
edit-mode: replace
-57
View File
@@ -1,57 +0,0 @@
name: Upload Figma token config to CDN
on:
workflow_dispatch:
push:
branches:
- "main"
paths:
- "@navikt/core/tokens/**"
jobs:
upload:
name: Build and upload token config
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: "read"
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: lts/*
cache: yarn
cache-dependency-path: "yarn.lock"
- name: Install dependencies
run: yarn
env:
NPM_AUTH_TOKEN: ${{ secrets.READER_TOKEN }}
- name: Build tokens
run: yarn workspace @navikt/ds-tokens build
- name: Upload config to CDN
id: upload
uses: nais/deploy/actions/cdn-upload/v2@master
with:
team: designsystem
source: "./@navikt/core/tokens/figma-config.json"
destination: "./@navikt/tokens"
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
source_keep_parent_name: false
no_cache_paths: "@navikt/tokens/figma-config.json"
- name: Log uploaded file
run: echo uploaded file ${{ steps.upload.outputs.uploaded }}
shell: bash
-39
View File
@@ -1,39 +0,0 @@
name: Upload fonts to CDN
on:
workflow_dispatch:
push:
branches:
- "main"
paths:
- "fonts/**"
jobs:
upload:
name: Upload fonts to CDN
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
id-token: write
steps:
- name: Checkout code
uses: "actions/checkout@v7"
with:
persist-credentials: false
- name: Upload fonts to CDN
id: upload
uses: nais/deploy/actions/cdn-upload/v2@master
with:
team: aksel
source: ./fonts
destination: "" # uploads folder to team root
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Log uploaded file
run: echo uploaded file ${{ steps.upload.outputs.uploaded }}
shell: bash
+85
View File
@@ -0,0 +1,85 @@
name: Visual Regression Tests
on:
push:
branches:
- "main"
paths:
- "@navikt/**"
pull_request:
branches: [main, next]
paths:
- "@navikt/**"
- "aksel.nav.no/website/pages/templates/**"
- ".storybook/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
chromatic:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Chromatic
uses: chromaui/action@latest
id: chromatic_tests
continue-on-error: false
with:
autoAcceptChanges: "main"
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true
buildScriptName: "build:storybook-chromatic"
onlyChanged: true
untraced: "**/package.json|yarn.lock|**/*.md"
externals: "**/core/css/*.css|**/core/css/baseline/*.css|**/core/css/form/*.css|**/core/css/primitives/*.css"
traceChanged: true
env:
STORYBOOK_GITHUB_SHA: ${{ github.sha }}
- name: Get commit sha
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]')
id: vars
shell: bash
run: |
echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Find Comment
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]')
uses: peter-evans/find-comment@v4
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: Storybook demo
- name: Create or update comment
if: github.event_name != 'push' && !contains(github.event.head_commit.message, '[ci skip]')
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
## [Storybook demo](${{steps.chromatic_tests.outputs.storybookUrl}}) / [Chromatic](${{steps.chromatic_tests.outputs.url}})
${{ steps.chromatic_tests.outputs.changeCount > 0 && format('### 📝 Changes to review: {0}', steps.chromatic_tests.outputs.changeCount) || '' }}
${{ steps.chromatic_tests.outputs.errorCount > 0 && format('### ❌ Stories with errors: {0}', steps.chromatic_tests.outputs.errorCount) || '' }}
${{ steps.chromatic_tests.outputs.interactionTestFailuresCount > 0 && format('### ❌ Failed interaction tests: {0}', steps.chromatic_tests.outputs.interactionTestFailuresCount) || '' }}
[${{ steps.vars.outputs.sha_short }}](https://github.com/navikt/aksel/commit/${{ steps.vars.outputs.sha_short }}) | ${{steps.chromatic_tests.outputs.componentCount}} components | ${{steps.chromatic_tests.outputs.specCount}} stories
edit-mode: replace
+81
View File
@@ -0,0 +1,81 @@
name: CI
on:
push:
branches:
- main
- next
pull_request:
workflow_dispatch:
jobs:
lint:
name: "Linter: eslint, stylelint"
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Lint
run: yarn lint
validate-examples:
name: "Validate Aksel.nav.no component examples"
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Validate example code
run: yarn validate:eksempler
test:
name: "Tests: Unit and integration"
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Test
run: yarn test
ui:
name: "Tests: UI"
runs-on: ubuntu-latest
timeout-minutes: 10
container:
image: mcr.microsoft.com/playwright:v1.57.0-noble
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Test UI
run: yarn storybook:test --max-workers=2
-26
View File
@@ -1,26 +0,0 @@
name: Cleanup E2E Docker images
on:
schedule:
- cron: "0 4 * * 1"
workflow_dispatch:
permissions:
packages: write
jobs:
cleanup:
runs-on: ubuntu-latest
strategy:
matrix:
package-name:
- aksel-e2e
steps:
- name: Delete old container images
uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # actions/delete-package-versions@v5
with:
package-name: ${{ matrix.package-name }}
package-type: container
delete-only-untagged-versions: false
min-versions-to-keep: 1
token: ${{ secrets.GITHUB_TOKEN }}
-52
View File
@@ -1,52 +0,0 @@
name: Deploy dev (aksel.ansatt.dev.nav.no)
on:
workflow_dispatch:
push:
branches:
- next
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
build_and_deploy:
name: Build and deploy to dev-gcp
runs-on: ubuntu-latest
permissions:
contents: "read"
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup and build Next.js application
uses: ./.github/actions/build-website
with:
use_cdn_assets: true
npm_auth_token: ${{ secrets.READER_TOKEN }}
nais_id_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
nais_project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
sanity_read_token: ${{ secrets.SANITY_READ }}
sanity_read_no_drafts_token: ${{ secrets.SANITY_READ_NO_DRAFTS }}
- name: Build and push Docker image
uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
push_image: true
dockerfile: aksel.nav.no/Dockerfile
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy to dev-gcp
uses: nais/deploy/actions/deploy@v3
env:
RESOURCE: aksel.nav.no/nais-dev.yaml
CLUSTER: dev-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
-56
View File
@@ -1,56 +0,0 @@
name: Deploy Aksel MCP server
on:
push:
branches:
- main
paths:
- "apps/mcp-server/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
IMAGE: ghcr.io/navikt/aksel
jobs:
build_and_deploy:
name: Build and deploy to prod-gcp
runs-on: ubuntu-latest
permissions:
contents: "read"
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Test mcp-server
run: cd apps/mcp-server && yarn test
- name: Build mcp-server
run: cd apps/mcp-server && yarn build
- name: Build and push Docker image
uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
dockerfile: apps/mcp-server/Dockerfile
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy to prod-gcp
uses: nais/deploy/actions/deploy@v3
env:
RESOURCE: apps/mcp-server/.nais/app.yaml
CLUSTER: prod-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
-85
View File
@@ -1,85 +0,0 @@
name: Deploy prod (aksel.nav.no)
on:
push:
branches:
- main
paths:
- "aksel.nav.no/**"
- "apps/playroom/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
IMAGE: ghcr.io/navikt/aksel
jobs:
sync_data_with_sanity:
name: Sync data to Sanity
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- name: Run metadata generation
run: yarn docgen:meta
- name: Update Sanity
env:
SANITY_WRITE: ${{ secrets.SANITY_WRITE }}
GCP_CREDENTIALS: ${{ secrets.GCP_CREDENTIALS_BQ_READ }}
run: yarn workspace aksel.nav.no sanity:update
build_and_deploy:
name: Build and deploy to prod-gcp
runs-on: ubuntu-latest
permissions:
contents: "read"
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup and build Next.js application
uses: ./.github/actions/build-website
with:
use_cdn_assets: true
production: true
npm_auth_token: ${{ secrets.READER_TOKEN }}
nais_id_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
nais_project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
sanity_read_token: ${{ secrets.SANITY_READ }}
sanity_read_no_drafts_token: ${{ secrets.SANITY_READ_NO_DRAFTS }}
sanity_deploy_studio: ${{ secrets.SANITY_DEPLOY_STUDIO }}
next_encryption_key: ${{ secrets.NEXT_ENCRYPTION_KEY }}
- name: Build and push Docker image
uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
push_image: true
dockerfile: aksel.nav.no/Dockerfile
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy to prod-gcp
uses: nais/deploy/actions/deploy@v3
env:
RESOURCE: aksel.nav.no/nais-prod.yaml
CLUSTER: prod-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
-61
View File
@@ -1,61 +0,0 @@
name: Deploy prod (aksel-v7.nav.no)
on:
workflow_dispatch:
push:
branches:
- v7-aksel.nav.no
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
build_and_deploy:
name: Build and deploy to prod-gcp
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/v7-aksel.nav.no'
permissions:
contents: "read"
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
fetch-depth: 1
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: lts/*
- name: Install dependencies
run: cd v7-aksel.nav.no && yarn --immutable
shell: bash
- name: Set production env var
run: |
echo "PRODUCTION=true" >> v7-aksel.nav.no/website/.env
shell: bash
- name: Build Next.js website
run: cd v7-aksel.nav.no && yarn build
shell: bash
- name: Build and push Docker image
uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
push_image: true
dockerfile: v7-aksel.nav.no/Dockerfile
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: Deploy to prod-gcp
uses: nais/deploy/actions/deploy@v3
env:
RESOURCE: v7-aksel.nav.no/nais-prod.yaml
CLUSTER: prod-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
+129
View File
@@ -0,0 +1,129 @@
name: E2E / Playwright Tests
on:
push:
branches: [main, next]
paths:
- "aksel.nav.no/**"
pull_request:
branches: [main, next]
paths:
- "aksel.nav.no/**"
permissions:
actions: read
contents: read
packages: write
id-token: write
jobs:
prebuild-app-for-playwright:
name: "Prebuild app for Playwright tests"
runs-on: ubuntu-latest
env:
IMAGE_NAME: ${{ github.repository }}-e2e
REGISTRY: ghcr.io
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup and build Next.js application
uses: ./.github/actions/build-website
with:
use_cdn_assets: false
npm_auth_token: ${{ secrets.READER_TOKEN }}
nais_id_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
nais_project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
sanity_read_token: ${{ secrets.SANITY_READ }}
sanity_read_no_drafts_token: ${{ secrets.SANITY_READ_NO_DRAFTS }}
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: aksel.nav.no/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
outputs:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
run-playwright:
needs: [prebuild-app-for-playwright]
name: Playwright shard ${{ matrix.shardIndex }}
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.57.0-noble
services:
testapp:
image: ${{ needs.prebuild-app-for-playwright.outputs.image }}
ports: ["3000:3000"]
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
SANITY_READ_NO_DRAFTS: ${{ secrets.SANITY_READ_NO_DRAFTS }}
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5]
shardTotal: [5]
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: yarn
cache-dependency-path: "yarn.lock"
- name: Install dependencies
run: yarn --immutable
shell: bash
env:
NPM_AUTH_TOKEN: ${{ secrets.READER_TOKEN }}
- name: Run Playwright
shell: bash
run: yarn e2e:docker --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
SANITY_READ_NO_DRAFTS: ${{ secrets.SANITY_READ_NO_DRAFTS }}
- name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v5
with:
name: blob-report-${{ matrix.shardIndex }}
path: aksel.nav.no/website/blob-report
retention-days: 1
merge-reports:
if: ${{ !cancelled() }}
needs: [run-playwright]
runs-on: ubuntu-latest
steps:
- name: Download blob reports from GitHub Actions Artifacts
uses: actions/download-artifact@v6
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into HTML Report
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v5
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
retention-days: 14
+50
View File
@@ -0,0 +1,50 @@
name: "Update remote token config for figma variables plugin"
on:
workflow_dispatch:
push:
branches:
- "main"
paths:
- "@navikt/core/tokens/**"
jobs:
upload:
runs-on: ubuntu-latest
permissions:
contents: "read"
id-token: "write"
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: yarn
cache-dependency-path: "yarn.lock"
- name: Install dependencies
run: yarn
env:
NPM_AUTH_TOKEN: ${{ secrets.READER_TOKEN }}
- name: Build tokens
run: yarn workspace @navikt/ds-tokens build
- id: upload
uses: nais/deploy/actions/cdn-upload/v2@master
with:
team: designsystem
source: "./@navikt/core/tokens/figma-config.json"
destination: "./@navikt/tokens"
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
source_keep_parent_name: false
no_cache_paths: "@navikt/tokens/figma-config.json"
- run: echo uploaded file ${{ steps.upload.outputs.uploaded }}
shell: bash
+32
View File
@@ -0,0 +1,32 @@
name: "push-to-cdn"
on:
workflow_dispatch:
push:
branches:
- "main"
paths:
- "fonts/**"
jobs:
upload:
runs-on: ubuntu-latest
permissions:
contents: "read"
id-token: "write"
steps:
- uses: "actions/checkout@v6"
- id: upload
uses: nais/deploy/actions/cdn-upload/v2@master
with:
team: aksel
source: ./fonts
destination: "" # uploads folder to team root
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- run: echo uploaded file ${{ steps.upload.outputs.uploaded }}
shell: bash
@@ -0,0 +1,38 @@
name: Deploy referansesider SPA
on:
workflow_dispatch:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: "read"
id-token: "write"
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 1
- name: Setup project
uses: ./.github/actions/setup
with:
npm_auth_token: ${{ secrets.READER_TOKEN }}
- run: cd ./examples/referansesider && yarn && yarn build
- uses: nais/deploy/actions/spa-deploy/v2@master
with:
app: aksel-referansesider
team: designsystem
source: ./examples/referansesider/dist
ingress: https://aksel-referansesider.ansatt.dev.nav.no
environment: dev
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
cache_invalidation: true
+10 -9
View File
@@ -1,4 +1,4 @@
name: Release packages
name: Release
on:
push:
@@ -13,9 +13,8 @@ concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
build:
name: Release packages
# Only publish from main, also when dispatched manually
if: github.ref == 'refs/heads/main'
name: "Release packages"
if: ${{!contains(github.event.head_commit.message, '[ci skip]')}}
runs-on: ubuntu-latest
permissions:
id-token: "write"
@@ -23,18 +22,20 @@ jobs:
packages: "write"
pull-requests: "write"
steps:
- name: Checkout code
uses: actions/checkout@v7
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v7
- uses: actions/setup-node@v6
with:
node-version: lts/*
cache: yarn
cache-dependency-path: "yarn.lock"
- uses: denoland/setup-deno@v2
with:
deno-version: v1.x
- name: Install dependencies
run: yarn
env:
@@ -54,7 +55,7 @@ jobs:
# Needed for push to main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create .npmrc for GPR
- name: Creating .npmrc
run: |
cat << EOF > "$HOME/.npmrc"
@navikt:registry=https://npm.pkg.github.com
+29
View File
@@ -0,0 +1,29 @@
# https://github.com/actions/stale
name: Mark stale issues
on:
schedule:
- cron: "31 3 * * *"
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-issue-stale: 120
days-before-pr-stale: -1
days-before-close: 30
any-of-labels: "Besvart"
stale-issue-message: |-
Hei! Det ser ut som det ikke har vært aktivitet her på en stund. Etter 30 dager vil saken bli lukket automatisk.
Om saken fortsatt er relevant eller du har oppdateringer, vil en kommentar hindre at den lukkes. Du kan alltids åpne den igjen hvis det skulle være nødvendig!
stale-issue-label: "no-issue-activity"
ascending: true
operations-per-run: 200
@@ -0,0 +1,28 @@
name: Redeploy Naisjob (update-article-views)
on:
workflow_dispatch:
jobs:
execute:
runs-on: ubuntu-latest
permissions:
id-token: "write"
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1
- uses: nais/docker-build-push@v0
id: docker-push
with:
team: designsystem
push_image: true
dockerfile: scripts/deno/article-views/Dockerfile.update-article-views
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
- name: start naisjob prod-gcp
uses: nais/deploy/actions/deploy@master
env:
APIKEY: ${{ secrets.NAIS_DEPLOY_APIKEY }}
RESOURCE: scripts/deno/article-views/nais-update-views-job.yml
CLUSTER: prod-gcp
IMAGE: ${{ steps.docker-push.outputs.image }}
+9 -2
View File
@@ -90,10 +90,15 @@ dist/
assets.json
lib/
lerna-debug.log
examples/legacy/node_modules
examples/legacy/out
examples/webpack/node_modules
examples/webpack/out
.gitattributes
.iml
.DS_Store
*.d.ts
*-exports.less
*/figma-api/icons
*/ds-icons/png
@@ -107,12 +112,13 @@ utilities/**/css
*/**/NAV-ikonpakke-png.zip
*/**/NAV-ikonpakke-svg.zip
react-docgen.json
react-docgen-docs.json
cjs
esm
@navikt/community/navno/css
.vercel
_docs.json
_metadata.json
**/component-names.generated.ts
.yarn/*
.yarn/cache
@@ -130,6 +136,7 @@ _metadata.json
.next
!*/website/lib/
!*/website/public/
!aksel.nav.no/**/*.d.ts
!*/sanity/lib/
!*/sanity/static/
out
+34 -7
View File
@@ -1,9 +1,36 @@
echo "husky - DEPRECATED
Please remove the following two lines from $0:
#!/usr/bin/env sh
. \"\$(dirname -- \"\$0\")/_/husky.sh\"
if [ -z "$husky_skip_init" ]; then
debug () {
if [ "$HUSKY_DEBUG" = "1" ]; then
echo "husky (debug) - $1"
fi
}
They WILL FAIL in v10.0.0
"
readonly hook_name="$(basename -- "$0")"
debug "starting $hook_name..."
if [ "$HUSKY" = "0" ]; then
debug "HUSKY env variable is set to 0, skipping hook"
exit 0
fi
if [ -f ~/.huskyrc ]; then
debug "sourcing ~/.huskyrc"
. ~/.huskyrc
fi
readonly husky_skip_init=1
export husky_skip_init
sh -e "$0" "$@"
exitCode="$?"
if [ $exitCode != 0 ]; then
echo "husky - $hook_name hook exited with code $exitCode (error)"
fi
if [ $exitCode = 127 ]; then
echo "husky - command not found in PATH=$PATH"
fi
exit $exitCode
fi
+6 -10
View File
@@ -1,13 +1,9 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
CHANGED=`git diff $1 $2 --stat -- yarn.lock | wc -l`
if [ $CHANGED -gt 0 ];
then
printf '\n\033[1;33m'
printf ' ╔══════════════════════════════════════════╗\n'
printf ' ║ ║\n'
printf ' ║ !! yarn.lock has changed !! ║\n'
printf ' ║ ║\n'
printf ' ║ Run: yarn install ║\n'
printf ' ║ ║\n'
printf ' ╚══════════════════════════════════════════╝\n'
printf '\033[0m\n'
fi
echo
echo "*** 🟡 yarn.lock has changed 🟡 ***"
fi
+6 -10
View File
@@ -1,13 +1,9 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
CHANGED=`git diff HEAD@{1} --stat -- yarn.lock | wc -l`
if [ $CHANGED -gt 0 ];
then
printf '\n\033[1;33m'
printf ' ╔══════════════════════════════════════════╗\n'
printf ' ║ ║\n'
printf ' ║ !! yarn.lock has changed !! ║\n'
printf ' ║ ║\n'
printf ' ║ Run: yarn install ║\n'
printf ' ║ ║\n'
printf ' ╚══════════════════════════════════════════╝\n'
printf '\033[0m\n'
fi
echo
echo "*** 🟡 yarn.lock has changed 🟡 ***"
fi
+4 -2
View File
@@ -1,2 +1,4 @@
# Runs the local lint-staged checks before committing
./node_modules/.bin/lint-staged
#!/bin/sh
. "$(dirname $0)/_/husky.sh"
yarn lint-staged && yarn tsx ./scripts/in-sync-versions.ts
-10
View File
@@ -1,10 +0,0 @@
# Only run tests when pushing to main branch
while read local_ref local_sha remote_ref remote_sha; do
if [ "$remote_ref" = "refs/heads/main" ]; then
echo "Pushing to main branch - running tests...\n"
yarn test
exit $?
fi
done
exit 0
+2
View File
@@ -1,3 +1,5 @@
**/nais*.yaml
**/nais*.yml
**/codemod/**/tests/**/*.js
**/@navikt/aksel/**/*.input.*
**/@navikt/aksel/**/*.output.*
+5
View File
@@ -15,6 +15,11 @@ body {
padding: 1rem;
}
.colgap[data-theme="dark"],
.rowgap[data-theme="dark"] {
background-color: var(--a-gray-900);
}
.rowgap-wrap {
flex-wrap: wrap;
}
+36 -57
View File
@@ -1,11 +1,13 @@
import { defineMain } from "@storybook/react-vite/node";
import { StorybookConfig } from "@storybook/react-vite";
import FG from "fast-glob";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { loadCsf } from "storybook/internal/csf-tools";
import { InlineConfig } from "vite";
import turbosnap from "vite-plugin-turbosnap";
import TsconfigPathsPlugin from "vite-tsconfig-paths";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -15,64 +17,35 @@ const includeWebsiteStories = process.env.WITH_WEBSITE === "true";
const indexRegex = /export const args = {\s+index: (\d+),/;
const addons = [
getAbsolutePath("@storybook/addon-a11y"),
getAbsolutePath("@storybook/addon-themes"),
getAbsolutePath("@storybook/addon-docs"),
getAbsolutePath("@storybook/addon-vitest"),
];
if (process.env.NODE_ENV === "development") {
addons.push(getAbsolutePath("@github-ui/storybook-addon-performance-panel"));
}
export default defineMain({
export default {
experimental_indexers: (indexers) => {
// Changes here might need to be reflected in aksel.nav.no/website/.storybook/main.ts
const customIndexer = async (fileName: string, opts: any) => {
try {
let code = readFileSync(fileName, "utf-8").toString();
let code = readFileSync(fileName, "utf-8").toString();
const index = indexRegex.exec(code)?.[1];
const index = indexRegex.exec(code)?.[1];
code = code.split(
/\/\/ EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE/,
)[0];
code = code.split(
/\/\/ EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE/,
)[0];
const templatePath = fileName.split(`pages/templates/`)[1];
if (!templatePath) {
console.warn(`[Storybook] Invalid template path: ${fileName}`);
return [];
}
const [templateName, name] = fileName
.split(`pages/templates/`)[1]
.replace(".tsx", "")
.split("/");
const storyName = process.env.CHROMATIC
? `${templateName} [${index}] ${name}` // Chromatic does not support folders, and doesn't like |
: `${index} | ${name}`;
const [templateName, name] = templatePath
.replace(".tsx", "")
.split("/");
if (!templateName || !name) {
console.warn(
`[Storybook] Could not parse template name from: ${fileName}`,
);
return [];
}
const storyName = process.env.CHROMATIC
? `${templateName} [${index ?? "0"}] ${name}` // Chromatic does not support folders, and doesn't like |
: `${index ?? "0"} | ${name}`;
code += `
code += `
export default { title: "Templates/${templateName}/${storyName}" };
export const Demo = { render: Example };
Demo.storyName = "${storyName}";`;
return loadCsf(code, {
...opts,
fileName,
}).parse().indexInputs;
} catch (error) {
console.error(`[Storybook] Failed to index ${fileName}:`, error);
return [];
}
return loadCsf(code, {
...opts,
fileName,
}).parse().indexInputs;
};
return [
@@ -86,9 +59,14 @@ export default defineMain({
staticDirs: ["./public"],
stories: resolveStoriesPaths(),
stories: resolveStoriesPaths,
addons,
addons: [
getAbsolutePath("@storybook/addon-a11y"),
getAbsolutePath("@storybook/addon-themes"),
getAbsolutePath("@storybook/addon-docs"),
getAbsolutePath("@storybook/addon-vitest"),
],
framework: {
name: getAbsolutePath("@storybook/react-vite"),
@@ -103,20 +81,21 @@ export default defineMain({
async viteFinal(config, { configType }) {
const { mergeConfig } = await import("vite");
// The TsconfigPathsPlugin is only used to silence errors when importing nextjs components, but the imports does not acutally work.
const tsConfigPathsPluginOpts = { root: "aksel.nav.no/website/" };
return mergeConfig(config, {
build: { cssMinify: "lightningcss" },
resolve: {
tsconfigPaths: true,
},
plugins:
configType === "PRODUCTION"
? [
TsconfigPathsPlugin(tsConfigPathsPluginOpts),
turbosnap({
rootDir: config.root ?? process.cwd(),
}),
]
: [],
} satisfies typeof config);
: [TsconfigPathsPlugin(tsConfigPathsPluginOpts)],
} satisfies InlineConfig);
},
/* Lets us preview Roboto-flex font in storybook. */
@@ -129,9 +108,9 @@ export default defineMain({
features: {
actions: false,
},
});
} satisfies StorybookConfig;
function getAbsolutePath(value: string) {
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, "package.json")));
}
+63 -133
View File
@@ -1,104 +1,48 @@
import addonPerformancePanel from "@github-ui/storybook-addon-performance-panel";
import addonA11y from "@storybook/addon-a11y";
import addonDocs from "@storybook/addon-docs";
import addonThemes, { withThemeByClassName } from "@storybook/addon-themes";
import addonVitest from "@storybook/addon-vitest";
import { definePreview } from "@storybook/react-vite";
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import "../@navikt/core/css/src/data-token-filter.css";
import dataTokenFilterStyles from "../@navikt/core/css/src/data-token-filter.css?inline";
import "../@navikt/core/css/src/data-toolbar.css";
import dataToolbarStyles from "../@navikt/core/css/src/data-toolbar.css?inline";
import "../@navikt/core/css/src/form/combobox2.css";
import combobox2Styles from "../@navikt/core/css/src/form/combobox2.css?inline";
import "../@navikt/core/css/src/index.css";
import dsStyles from "../@navikt/core/css/src/index.css?inline";
import "../@navikt/core/css/src/listbox.css";
import listboxStyles from "../@navikt/core/css/src/listbox.css?inline";
import { withThemeByClassName } from "@storybook/addon-themes";
import { Preview } from "@storybook/nextjs";
import React, { useEffect, useLayoutEffect } from "react";
// @ts-expect-error - Temporary
import darksideCss from "../@navikt/core/css/darkside/index.css?inline";
// @ts-expect-error - Temporary
import defaultCss from "../@navikt/core/css/index.css?inline";
import { Provider } from "../@navikt/core/react/src/provider";
import type { Translations } from "../@navikt/core/react/src/utils/i18n/i18n.types";
import { en, nb, nn } from "../@navikt/core/react/src/utils/i18n/locales";
import { Theme } from "../@navikt/core/react/src/theme";
import { Translations } from "../@navikt/core/react/src/util/i18n/i18n.types";
import { en, nb, nn } from "../@navikt/core/react/src/util/i18n/locales";
import "./layout.css";
type Language = "nb" | "nn" | "en";
type WebComponentMode = "off" | "open" | "closed";
const ModeDecorator = ({ children, mode, theme }) => {
useLayoutEffect(() => {
const style = document.createElement("style");
style.innerHTML = mode === "darkside" ? darksideCss : defaultCss;
document.head.appendChild(style);
if (mode === "darkside") {
document.body.style.setProperty("background", "var(--ax-bg-default)");
}
return () => {
document.head.removeChild(style);
document.body.style.removeProperty("background");
};
}, [mode]);
return mode === "darkside" ? (
<Theme theme={theme || undefined} hasBackground={false}>
{children}
</Theme>
) : (
children
);
};
type Language = "nb" | "nn" | "en";
const locales: Record<Language, Translations> = {
nb,
nn,
en,
};
const StoryProviderDecorator = ({
children,
lang,
rootElement,
}: {
children: React.ReactNode;
lang: Language | undefined;
rootElement?: HTMLElement;
}) => {
useEffect(() => {
document.documentElement.lang = lang || "nb";
}, [lang]);
return (
<Provider
rootElement={rootElement}
locale={lang ? locales[lang] : undefined}
>
{children}
</Provider>
);
};
const webComponentStyles = [
dataTokenFilterStyles,
dataToolbarStyles,
listboxStyles,
combobox2Styles,
dsStyles,
].join("\n");
const WebComponentDecorator = ({
children,
mode,
}: {
children: (rootElement: HTMLDivElement) => React.ReactNode;
mode: Exclude<WebComponentMode, "off">;
}) => {
const hostRef = useRef<HTMLDivElement>(null);
const [rootElement, setRootElement] = useState<HTMLDivElement | null>(null);
useLayoutEffect(() => {
const host = hostRef.current;
if (!host) {
return;
}
const shadowRoot = host.attachShadow({ mode });
const style = document.createElement("style");
style.textContent = webComponentStyles;
shadowRoot.appendChild(style);
const mount = document.createElement("div");
shadowRoot.appendChild(mount);
setRootElement(mount);
return () => {
setRootElement(null);
};
}, [mode]);
return (
<>
<div ref={hostRef} key={mode} />
{rootElement ? createPortal(children(rootElement), rootElement) : null}
</>
);
};
const fonts = ["Source Sans 3", "Open Sans"];
const TypoDecorator = ({
@@ -111,26 +55,13 @@ const TypoDecorator = ({
useEffect(() => {
const fontVariable = fonts.includes(font) ? `"${font}", sans-serif` : null;
document.body.style.setProperty("--ax-font-family", fontVariable);
return () => {
document.body.style.removeProperty("--ax-font-family");
};
document.body.style.setProperty("--a-font-family", fontVariable);
}, [font]);
return <>{children}</>;
return children;
};
const addons = [addonA11y(), addonThemes(), addonDocs(), addonVitest()];
/**
* Fixes flaky interaction tests
*/
if (process.env.NODE_ENV === "development") {
addons.push(addonPerformancePanel());
}
export default definePreview({
addons,
export default {
parameters: {
options: {
storySort: {
@@ -162,6 +93,21 @@ export default definePreview({
},
globalTypes: {
mode: {
name: "Darkside",
toolbar: {
items: [
{
value: "default",
icon: "hourglass",
title: "Legacy theme",
},
{ value: "darkside", icon: "beaker", title: "Darkside theme" },
],
showName: true,
dynamicTitle: true,
},
},
language: {
toolbar: {
icon: "globe",
@@ -181,23 +127,11 @@ export default definePreview({
dynamicTitle: true,
},
},
webComponentWrapper: {
toolbar: {
icon: "component",
items: [
{ value: "off", title: "DOM: Default" },
{ value: "open", title: "Web component: Open shadow root" },
{ value: "closed", title: "Web component: Closed shadow root" },
],
dynamicTitle: true,
},
},
},
initialGlobals: {
mode: "default",
font: "Source Sans 3",
webComponentWrapper: "off",
},
decorators: [
@@ -206,22 +140,18 @@ export default definePreview({
<StoryFn />
</TypoDecorator>
),
(StoryFn, context) => (
<ModeDecorator mode={context.globals.mode} theme={context.globals.theme}>
<StoryFn />
</ModeDecorator>
),
(StoryFn, context) =>
context.globals.webComponentWrapper === "off" ? (
<StoryProviderDecorator lang={context.globals.language}>
context.globals.language ? (
<Provider locale={locales[context.globals.language as Language]}>
<StoryFn />
</StoryProviderDecorator>
</Provider>
) : (
<WebComponentDecorator mode={context.globals.webComponentWrapper}>
{(rootElement) => (
<StoryProviderDecorator
lang={context.globals.language}
rootElement={rootElement}
>
<StoryFn />
</StoryProviderDecorator>
)}
</WebComponentDecorator>
<StoryFn />
),
withThemeByClassName({
themes: {
@@ -231,4 +161,4 @@ export default definePreview({
defaultTheme: "light",
}),
],
});
} satisfies Preview;
+7
View File
@@ -0,0 +1,7 @@
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
import { setProjectAnnotations } from "@storybook/react-vite";
import * as projectAnnotations from "./preview";
// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
+5 -4
View File
@@ -1,12 +1,13 @@
{
"recommendations": [
"oxc.oxc-vscode",
"stylelint.vscode-stylelint",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"denoland.vscode-deno",
"esbenp.prettier-vscode",
"github.vscode-github-actions",
"ms-playwright.playwright",
"sanity-io.vscode-sanity",
"vunguyentuan.vscode-css-variables"
"stylelint.vscode-stylelint",
"vunguyentuan.vscode-css-variables",
"bradlc.vscode-tailwindcss"
]
}
+6 -2
View File
@@ -1,5 +1,9 @@
{
"cssVariables.lookupFiles": ["@navikt/core/tokens/dist/tokens.css"],
"cssVariables.lookupFiles": [
"@navikt/core/tokens/dist/tokens.css",
"@navikt/core/tokens/dist/darkside/tokens.css"
],
"eslint.problems.shortenToSingleLine": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.watcherExclude": {
"**/.yarn/**": true,
@@ -8,6 +12,6 @@
"**/cjs": true
},
"playwright.reuseBrowser": true,
"js/ts.tsdk.path": "node_modules/typescript/lib",
"typescript.tsdk": "node_modules/typescript/lib",
"deno.enablePaths": ["./scripts/deno"]
}
-1
View File
@@ -1,4 +1,3 @@
// oxlint-disable-next-line typescript/no-require-imports
const template = require("./config/template");
module.exports = {
-150
View File
@@ -1,155 +1,5 @@
# @navikt/aksel-icons
## 8.22.1
## 8.22.0
### Minor Changes
- Icons: New icons `Gift`, `ArrowsPointingIn` and `ArrowsPointingOut`. ([#5484](https://github.com/navikt/aksel/pull/5484))
### Patch Changes
- ESM: Mark ESM output as side-effect free, so bundlers like webpack, Turbopack and esbuild can tree-shake unused components and icons. ([#5472](https://github.com/navikt/aksel/pull/5472))
## 8.21.0
### Minor Changes
- Icons: Add 46 icons for text editing, tables, keyboard input, and filtering. ([#5458](https://github.com/navikt/aksel/pull/5458))
## 8.20.1
## 8.20.0
## 8.19.2
## 8.19.1
## 8.19.0
### Minor Changes
- Icons: Added new icon MenuHamburgerMagnifyingGlass ([#5368](https://github.com/navikt/aksel/pull/5368))
## 8.18.0
## 8.17.2
### Patch Changes
- Package: Fixed regression where package was published without dist-files ([`9673de4`](https://github.com/navikt/aksel/commit/9673de4c264479dd8ca04897003532595acad255))
## 8.17.1
## 8.17.0
## 8.16.2
## 8.16.1
## 8.16.0
## 8.15.0
### Minor Changes
- New icons: Timeline and TimelineFill ([#5050](https://github.com/navikt/aksel/pull/5050))
## 8.14.0
## 8.13.1
## 8.13.0
### Minor Changes
- Icons: New icons Armchair, FocusRoom, PhoneRoom and Sleep ([#4979](https://github.com/navikt/aksel/pull/4979))
## 8.12.1
## 8.12.0
## 8.11.1
## 8.11.0
### Minor Changes
- Icons: New icon `Newsletter` and `NewsletterFill` ([#4887](https://github.com/navikt/aksel/pull/4887))
## 8.10.6
## 8.10.5
## 8.10.4
## 8.10.3
## 8.10.2
## 8.10.1
## 8.10.0
## 8.9.1
## 8.9.0
## 8.8.0
## 8.7.0
## 8.6.0
## 8.5.2
## 8.5.1
## 8.5.0
## 8.4.1
## 8.4.0
## 8.3.0
## 8.2.2
## 8.2.1
## 8.2.0
## 8.1.0
## 8.0.1
## 8.0.0
## 7.40.0
### Minor Changes
- Icons: New icon MagnifyingGlassCheckmark ([#4489](https://github.com/navikt/aksel/pull/4489))
## 7.39.1
## 7.39.0
## 7.38.0
## 7.37.0
## 7.36.0
### Patch Changes
- Icons: Removed 'updated_at'-field from AkselIcon-type. ([#4399](https://github.com/navikt/aksel/pull/4399))
## 7.35.3
## 7.35.2
## 7.35.1
## 7.35.0
+39 -5
View File
@@ -2,11 +2,45 @@
## Foreslå nye ikoner
Om du ikke finner ikonet du leter etter kan du [lage et issue på GitHub](https://github.com/navikt/aksel/issues/new?template=new-icon.yaml).
Om du ikke finner et ikonet du leter etter kan du [åpne et issue på github](https://github.com/navikt/aksel/issues/new?labels=nytt+✨%2Cikoner+🖼%2Cforespørsel+🥰&template&template=new-icon.yaml&title=%5BNytt+ikon%5D%3A+).
## Oppdatere ikon
Hvis du tenker et ikon fortjener en oppdatering eller er utdatert kan [du foreslå det her](https://github.com/navikt/aksel/issues/new?template=update-icon.yml).
Hvis du tenker et ikon fortjener en oppdatering eller er utdatert kan [du foreslå det her.](https://github.com/navikt/aksel/issues/new?labels=forespørsel+🥰&template=update-icon.yml&title=%5BInnspill+til+ikon%5D%3A+)
## Legge til nye ikoner selv
Hvis du selv ønsker å legge til nye ikoner kan du kopiere filenes fra `/template` og legge de inn i `/icons` med egen data. Alle ikonene har naming-format `PascalCase`. Alle ikonene har 1 SVG-fil og 1 tilhørende YML-fil.
### Svg
- Sizing, width og height må være 24x24: `width="24" height="24" `
- Viewbox: `viewBox="0 0 24 24"`
- Fill på path: `fill="#202733"`
### Yml
```yml
name: Template # Samme som filnavn
category: Template category # Hovedkategori
sub_category: Sub category # Underkategori
keywords: # Samling med søkeord
- template1
- template2
- template3
variant: Stroke # Stroke eller Fill
updated_at: 02.01.2023 # Dato for siste oppdatering
created_at: 01.01.2023 # Dato for opprettelse
```
### Publisere oppdatering
Når du har lagt til nye ikoner kan du opprette en pull request. Denne vil bli automatisk testet og så gått gjennom av Aksel-teamet.
Legg gjerne til en `changeset` ved å kjøre `yarn changeset` i terminalen. Dette vil gi oss bedre oversikt over hva som er endret i endringslogger og automatisk bumpe pakke-versjonen ved merge.
#### Teste nytt ikon
Hvis du ønsker å teste om alt stemmer, både i `svg` og `yml` før du lager en PR kan du kjøre `yarn && cd navikt/aksel-icons && yarn test` i terminalen lokalt.
## Design av nye ikoner
@@ -17,9 +51,9 @@ Hvis du tenker et ikon fortjener en oppdatering eller er utdatert kan [du foresl
### Stroke (linje)
- Ikonet må bestå av kun linjer.
- Linjene skal være 1.5px, center-aligned.
- Ikonet må bestå av kun linjer
- Linjene skal være 1.5px, center-aligned
### Fill (fylt)
- Ikoner hvor shapes kan fylles, bør fylles.
- Ikoner hvor shapes kan fylles, bør fylles
-31
View File
@@ -59,37 +59,6 @@ declare module "*.svg" {
}
```
### CSS custom properties
All icons are available as CSS custom properties behind `/css`, one file per icon. Each file declares a `--aksel-icon-<kebab-case-name>` variable on `:root`, containing the icon as an SVG data URI.
```css
@import "@navikt/aksel-icons/css/MagnifyingGlass.css";
.search-button::before {
content: "";
display: inline-block;
width: 1.5rem;
height: 1.5rem;
background-color: currentColor;
mask: var(--aksel-icon-magnifying-glass) center / contain no-repeat;
}
@media (forced-colors: active) {
.search-button::before {
background-color: CanvasText;
}
}
```
To get every icon, import `@navikt/aksel-icons/css/index.css`. This file contains all ~1000 icons (≈700 kB, ≈135 kB gzipped), so prefer importing only the icons you use.
Import icon files once in a global stylesheet (e.g. your root layout or `globals.css`), not in CSS modules or per-component stylesheets. The variables are global, and the SVG is inlined in each file, so importing the same icon in several places duplicates it in every CSS chunk.
- Icons added with CSS are decorative only. Give the parent element an accessible name, e.g. with visible text or `aria-label`.
- In forced colors mode (e.g. Windows High Contrast), `background-color` is forced to the page background, which hides the icon. Use `background-color: CanvasText` inside `@media (forced-colors: active)` as shown above, so the icon follows the user's text color.
- If you use a Content Security Policy, `img-src` must allow `data:`.
## [Learn more](https://aksel.nav.no/ikoner)
## License
@@ -3,7 +3,7 @@ import { JSON_SCHEMA, load } from "js-yaml";
import fs from "node:fs";
import path from "node:path";
import { describe, expect, test } from "vitest";
import type { IconYml } from "../config/figma/make-configs";
import { IconYml } from "../config/figma/make-configs";
const basePath = path.resolve(__dirname, "../icons");
@@ -24,6 +24,8 @@ describe(`Each icons YML-config is valid`, () => {
expect(ymlData.keywords).toBeTruthy();
expect(ymlData.variant).toBeTruthy();
expect(ymlData.keywords.length).toBeGreaterThan(0);
expect(ymlData.updated_at).toBeTruthy();
expect(isDate(ymlData.updated_at)).toBeTruthy();
expect(ymlData.created_at).toBeTruthy();
expect(isDate(ymlData.created_at)).toBeTruthy();
});
@@ -1,44 +0,0 @@
import fastglob from "fast-glob";
import path from "node:path";
import { describe, expect, test } from "vitest";
import {
optimizeSvg,
svgToDataUri,
toCssVariableName,
// @ts-expect-error -- css-icons is JavaScript without a declaration file.
} from "../config/css-icons";
describe("toCssVariableName", () => {
test.each([
["MagnifyingGlass", "--aksel-icon-magnifying-glass"],
["Buildings2Fill", "--aksel-icon-buildings-2-fill"],
["ClipboardXMark", "--aksel-icon-clipboard-x-mark"],
["XMark", "--aksel-icon-x-mark"],
])("%s -> %s", (iconName, expected) => {
expect(toCssVariableName(iconName)).toBe(expected);
});
test("is unique for every icon", () => {
const names = fastglob
.sync("*.svg", { cwd: path.resolve(__dirname, "../icons") })
.map((file) => toCssVariableName(path.basename(file, ".svg")));
expect(new Set(names).size).toBe(names.length);
});
});
describe("svgToDataUri", () => {
const svg = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 3.25 4.75 10.5Z" fill="#202733"/>
</svg>`;
test("keeps viewBox, uses currentColor and escapes unsafe characters", () => {
const uri = svgToDataUri(optimizeSvg(svg));
const data = uri.slice('url("'.length, -'")'.length);
expect(uri.startsWith('url("data:image/svg+xml,')).toBe(true);
expect(data).toContain("viewBox='0 0 24 24'");
expect(data).toContain("fill='currentColor'");
expect(data).not.toMatch(/["#<>]|width=|height=/);
});
});
@@ -28,7 +28,7 @@ describe(`SVG should be correctly parsed`, () => {
</svg>
`;
test("should replace width and height with 1em, and fill with currentColor", () => {
test("a", () => {
expect(parseIcon(simpleSvg)).toEqual(simpleSvgResult);
expect(parseIcon(advancedSvg)).toEqual(advancedSvgResult);
});
+2 -2
View File
@@ -1,8 +1,8 @@
/**
* Fikser https://github.com/navikt/aksel/issues/1758
*/
// oxlint-disable-next-line typescript/no-require-imports
const fs = require("node:fs");
const fs = require("fs");
const basePath = "./dist/react/esm";
-96
View File
@@ -1,96 +0,0 @@
// oxlint-disable typescript/no-require-imports
const fastglob = require("fast-glob");
const kebabCase = require("lodash/kebabCase");
const path = require("node:path");
const {
existsSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} = require("node:fs");
const { optimize } = require("svgo");
/**
* @param {string} iconName e.g. "MagnifyingGlass"
* @returns {string} e.g. "--aksel-icon-magnifying-glass"
*/
function toCssVariableName(iconName) {
return `--aksel-icon-${kebabCase(iconName)}`;
}
/**
* @param {string} svgString
* @returns {string} Minified SVG with currentColor fill and preserved viewBox
*/
function optimizeSvg(svgString) {
return optimize(svgString.replaceAll("#202733", "currentColor"), {
multipass: true,
plugins: [
{
name: "preset-default",
params: { overrides: { removeViewBox: false } },
},
{ name: "removeAttrs", params: { attrs: "svg:(width|height)" } },
],
}).data;
}
/**
* Encodes SVG for use in a CSS `url()`. Keeps readable characters unescaped for smaller output.
* @param {string} svgString
* @returns {string}
*/
function svgToDataUri(svgString) {
const encoded = svgString
.replaceAll('"', "'")
.replace(/\s+/g, " ")
.trim()
.replace(/[^\x20-\x7e]|[%#<>{}"\\]/g, encodeURIComponent);
return `url("data:image/svg+xml,${encoded}")`;
}
/**
* @param {string[]} declarations
* @returns {string}
*/
function createCssFile(declarations) {
return `:root {\n${declarations.map((d) => ` ${d}`).join("\n")}\n}\n`;
}
function main() {
const basePath = path.resolve(__dirname, "../icons");
const cssFolder = path.resolve(__dirname, "../dist/css");
const svgList = fastglob.sync("*.svg", { cwd: basePath }).sort();
if (existsSync(cssFolder)) {
rmSync(cssFolder, { recursive: true, force: true });
}
mkdirSync(cssFolder, { recursive: true });
const declarations = [];
for (const svg of svgList) {
const iconName = path.basename(svg, ".svg");
const svgString = readFileSync(path.join(basePath, svg)).toString();
const declaration = `${toCssVariableName(iconName)}: ${svgToDataUri(optimizeSvg(svgString))};`;
declarations.push(declaration);
writeFileSync(
path.join(cssFolder, `${iconName}.css`),
createCssFile([declaration]),
);
}
writeFileSync(path.join(cssFolder, "index.css"), createCssFile(declarations));
}
module.exports = {
createCssFile,
main,
optimizeSvg,
svgToDataUri,
toCssVariableName,
};
@@ -1,4 +1,4 @@
import type {
import {
BadRequestErrorResponseWithErrMessage,
ErrorResponsePayloadWithErrMessage,
GetFileComponentsResponse,
@@ -12,17 +12,20 @@ const FIGMA_FILE_KEY = "wEdyFjCQSBR3U7FvrMbPXa";
export const fetchIcons = async (): Promise<PublishedComponent[]> => {
console.group("Fetching list of published icons from Figma...");
const data:
GetFileComponentsResponse | BadRequestErrorResponseWithErrMessage =
await fetch(`https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/components`, {
| GetFileComponentsResponse
| BadRequestErrorResponseWithErrMessage = await fetch(
`https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/components`,
{
headers: {
"Content-type": "application/json",
"X-FIGMA-TOKEN": process.env.FIGMA_TOKEN ?? "",
},
})
.then((x) => x.json())
.catch((e) => {
throw e.message;
});
},
)
.then((x) => x.json())
.catch((e) => {
throw e.message;
});
if (data.status !== 200) {
throw new Error(data.err);
@@ -1,4 +1,4 @@
import type { PublishedComponent } from "@figma/rest-api-spec";
import { PublishedComponent } from "@figma/rest-api-spec";
import { camelCase, startCase } from "lodash";
export const resolveName = (icon: PublishedComponent) => {
@@ -1,4 +1,4 @@
import type { PublishedComponent } from "@figma/rest-api-spec";
import { PublishedComponent } from "@figma/rest-api-spec";
import { dump } from "js-yaml";
import { writeFileSync } from "node:fs";
import { resolve } from "node:path";
@@ -10,6 +10,7 @@ export type IconYml = {
sub_category: string;
keywords: string[];
variant: string;
updated_at: string;
created_at: string;
};
@@ -41,6 +42,12 @@ export const makeConfig = (
variant: icon.name.includes("Variant=")
? icon.name.replace("Variant=", "")
: "Stroke",
updated_at: new Date(icon.updated_at)
.toISOString()
.split("T")[0]
.split("-")
.reverse()
.join("."),
created_at: new Date(icon.created_at)
.toISOString()
.split("T")[0]
@@ -52,7 +59,7 @@ export const makeConfig = (
const yml = dump(config, {
noRefs: true,
skipInvalid: false,
quoteStyle: "double",
quotingType: '"',
});
writeFileSync(resolve(dirLocation, `${config.name}.yml`), yml, {
+1 -2
View File
@@ -1,5 +1,4 @@
// oxlint-disable-next-line typescript/no-require-imports
const path = require("node:path");
const path = require("path");
const defaultIndexTemplate = (paths) => {
const exportEntries = paths.map((filePath) => {
+8 -7
View File
@@ -1,8 +1,7 @@
// oxlint-disable typescript/no-require-imports
const fastglob = require("fast-glob");
const path = require("node:path");
const { load, JSON_SCHEMA } = require("js-yaml");
const fs = require("node:fs");
const path = require("path");
const jsYaml = require("js-yaml");
const fs = require("fs");
const basePath = path.resolve(__dirname, "../icons");
@@ -13,9 +12,10 @@ const ymlList = fastglob
const metadata = {};
ymlList.forEach((file) => {
const ymlData = load(fs.readFileSync(`${basePath}/${file}`), {
schema: JSON_SCHEMA,
const ymlData = jsYaml.load(fs.readFileSync(`${basePath}/${file}`), {
schema: jsYaml.JSON_SCHEMA,
});
// biome-ignore lint/complexity/useOptionalChain: optional chain not supported in enviroment. Consider revriting to Typescript
if (ymlData.keywords && ymlData.keywords.includes("[ignore-docs]")) {
return;
}
@@ -42,9 +42,10 @@ fs.writeFileSync(
id: string;
name: string;
category: string;
sub_category: string;
sub_category: "Time",
keywords: string[];
variant: "stroke" | "fill";
updated_at: Date;
created_at: Date;
}
+2 -3
View File
@@ -1,13 +1,12 @@
// oxlint-disable typescript/no-require-imports
const fastglob = require("fast-glob");
const path = require("node:path");
const path = require("path");
const {
existsSync,
readFileSync,
mkdirSync,
rmSync,
writeFileSync,
} = require("node:fs");
} = require("fs");
function main() {
const basePath = path.resolve(__dirname, "../icons");
+8 -9
View File
@@ -1,4 +1,3 @@
// oxlint-disable typescript/no-require-imports
const AdmZip = require("adm-zip");
const {
existsSync,
@@ -6,10 +5,10 @@ const {
mkdirSync,
rmSync,
copyFileSync,
} = require("node:fs");
const path = require("node:path");
} = require("fs");
const path = require("path");
const fastGlob = require("fast-glob");
const { load, JSON_SCHEMA } = require("js-yaml");
const jsYaml = require("js-yaml");
const iconFolder = "./core-icons";
@@ -27,8 +26,8 @@ async function generateFile() {
const categories = new Map();
ymlList.forEach((file) => {
const ymlData = load(readFileSync(`${basePath}/${file}`), {
schema: JSON_SCHEMA,
const ymlData = jsYaml.load(readFileSync(`${basePath}/${file}`), {
schema: jsYaml.JSON_SCHEMA,
});
categories.set(
ymlData.category,
@@ -40,12 +39,12 @@ async function generateFile() {
categories.forEach((value, key) => {
mkdirSync(`${iconFolder}/${key}`);
value.forEach((icon) => {
value.forEach((icon) =>
copyFileSync(
`${basePath}/${icon}.svg`,
`${iconFolder}/${key}/${icon}.svg`,
);
});
),
);
});
}
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
!src
!*.d.ts
File renamed without changes.
@@ -0,0 +1,35 @@
{
"name": "aksel-icons-figma-plugin",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite -c ./vite.config.ui.ts",
"build:icon-plugin": "tsc && npm run build:ui && npm run build:plugin",
"build:ui": "vite build -c ./vite.config.ui.ts --emptyOutDir",
"build:plugin": "vite build -c ./vite.config.plugin.ts",
"preview": "vite preview"
},
"dependencies": {
"@navikt/ds-css": "*",
"@navikt/ds-react": "*",
"fuse.js": "^7.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@figma/plugin-typings": "^1.100.2",
"@svgr/core": "^8.1.0",
"@types/node": "^24.10.0",
"@types/postcss-url": "^10.0.4",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.0.0",
"@vitejs/plugin-react": "^5.1.0",
"postcss": "^8.4.31",
"postcss-url": "^10.1.3",
"typescript": "5.9.3",
"vite": "^7.2.4",
"vite-plugin-singlefile": "^2.2.0",
"vite-plugin-svgr": "^4.2.0"
}
}
@@ -1,5 +1,5 @@
import fs from "node:fs";
import type { Plugin } from "vite";
import { Plugin } from "vite";
function svgToDataURL(svgStr: string) {
const encoded = encodeURIComponent(svgStr)
@@ -1,6 +1,5 @@
import { type FilterPattern, createFilter } from "@rollup/pluginutils";
import { FilterPattern, createFilter } from "@rollup/pluginutils";
import type { Config } from "@svgr/core";
import { transform } from "@svgr/core";
import fs from "node:fs";
import type { Plugin } from "vite";
import { transformWithEsbuild } from "vite";
@@ -23,8 +22,9 @@ export default function viteSvgr({
return {
name: "vite-svgr-component",
async transform(_, id) {
async transform(code, id) {
if (filter(id)) {
const { transform } = await import("@svgr/core");
const svgCode = await fs.promises.readFile(
id.replace(/\?.*$/, ""),
"utf8",
Loaded 100 of 5817 files, more files were not shown because too many files have changed in this diff. Show more