Compare commits

..
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] a2e020d4f3 Version Packages (#3206)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-08 11:50:48 +02:00
Halvor Haugan 074c8da536 List: ✨ New size 'large' (#3207) 2024-10-08 11:30:47 +02:00
KenandHalvor Haugan bdbd6c24cc [Dependencies] Replaced ts-node with tsx, removed dotenv (#3198)
* refactor: ds-react uses tsx instead of ts-node

* refactor: ds-tailwind uses tsx

* refactor: Website backup and search uses tsx

* refactor: Test urls and rss-feed uses tsx

* refactor: Prop-updates uses tsx

* refactor: All website scripts uses tsx

* refactor: removed dotenv dependency from website

* feat: removed all dependencies to dotenv

* bug: Remove need for env in validate build script

* test: Create empty env file

* test: Scope env-file to build-dir

* bug: Fallback to empty env when no env exsists

* feat: Better error-messaging when fetching new icons

* Update @navikt/aksel-icons/package.json

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-10-08 10:18:51 +02:00
Ken f0e6c944d8 [Referansesider] Updated contrast tokens to new naming 2024-10-08 10:16:17 +02:00
Ken 22be148832 [Darkside] Moved contrast-tokens to text group
feat: Moved contrast tokens to 'text' collection
feat: Added support for adding custom scopes for each token
2024-10-08 10:06:43 +02:00
KenandHalvor Haugan aac12fe44f [Linting] Fixed useExhaustiveDependencies in biome (#3197)
* feat: Update biome

* feat: migrate biome config

* misc: Ignore semantic-tag rule biome

* refactor: resolved breaking changes after biome update

* feat: remove old excluded biome rule

* feat: Removed a11y/useFocusableInteractive ignore

* refactor: Fix useExhaustiveDependencies  biome

* memo: changeset

* memo: changeset gitmoji

* bug: Re-add selectedOptions to dependency array

* Update @navikt/core/react/src/overlays/dismissablelayer/DismissableLayer.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* bug: Fix initial focus for sidebar icon

* ⚡ remove reduntant memoization of isLimitReached

* bug: re-add sanity-plugin-iframe-pane

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-10-08 09:05:59 +02:00
Halvor Haugan 324358d086 FileUpload: 💥 Remove UNSAFE-prefix (#3205) 2024-10-07 15:16:25 +02:00
Ken 946cbeb016 [Liniting] Updated to biome v1.9 (#3196)
* feat: Update biome

* feat: migrate biome config

* misc: Ignore semantic-tag rule biome

* refactor: resolved breaking changes after biome update

* feat: remove old excluded biome rule

* feat: Removed a11y/useFocusableInteractive ignore

* refactor: Remove ariaRole rule

* bug: Remove extra fragments in dartepicker stories

* bug: Remove custom image rendering exception for safari on website
2024-10-05 15:41:23 +02:00
Sjur f4d22dae68 Endret Neutral-farger Darth Mode (#3204)
Neutral-fargene ble hentet fra fargene med lavere saturation, som gjorde at de mistet hintet av blått. Endret til den originale skalaen.
2024-10-04 22:09:27 +02:00
Ken ce2f5a9b90 [Darkside] Fixed missing test updates 2024-10-04 16:01:58 +02:00
github-actions[bot]andgithub-actions[bot] cf167064eb Version Packages (#3203)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-04 15:25:08 +02:00
Ken 73a6ae1bf1 [Darkside] Fixed no_cache_paths for figma config CDN 2024-10-04 15:24:06 +02:00
Halvor Haugan 95c84e1d01 List: Explicitly set list-style on ol to override reset-CSS (#3202) 2024-10-04 15:14:16 +02:00
Ken 4675857e8f [Darkside] Updated global colors config
feat: Avoid caching config for color-tokens
feat: Remove use of leonardo, hardcode global config
feat: Update 100-400 scale for darkmode
feat: Update 500-1000 scale for global colors
2024-10-04 15:06:32 +02:00
Julian Nymark 2a6e416b5c Referansesider: 🐛 use master branch for GA 2024-10-04 12:50:43 +02:00
Julian Nymark d3c3cafc97 Referansesider: 👷 point to maintained spa-deploy yml (#3199)
* Referansesider: 👷 point to maintained spa-deploy yml

new location: https://github.com/nais/deploy/tree/master/actions/spa-deploy

* Referansesider: 🐛 probably correct yml url (GH action)
2024-10-04 11:47:33 +02:00
KenandHalvor Haugan 678fa6692b [Darkside] Figma plugin for updating variables (#3191)
* feat: Implemented figma plugin

* feat: Can now detect production/development build

* feat: Now supports remote fetch for config

* misc: Stop reset from running by default

* feat:Semantic value handling

* feat: Pre-semantic implementation

* feat: Finished plugin API for semantic variables

* feat: Use this for Figma plugin extension

* bug: Fixed semantic collection naming

* bug: Handle reset correctly

* misc: avoid resetting when updating

* memo: Added comments

* bug: Ignore plugin buildfile from lint

* bug: Fix support for dynamic-page load in figma

* 📝 Better naming, simplified code-def syntax

* feat: Scoped ignorePattern for plugin.js

* memo: better logs for completing update

* Update @navikt/core/tokens/darkside/figma/plugin/AkselVariablesInterface.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/tokens/darkside/figma/plugin/AkselVariablesInterface.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* memo: Removed extra space for info logging

* refactor: Simplify for-loop for semantic color modes

* refactor: Inline creation of mode, collection and variable

* refactor: readable mapping variables

* feat: Better error-handling if variable is null

* refactor: Remove redundant getModes function

* refactor: Remove redundant getModes function

* refactor: Remove plugin watch function

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-10-04 11:19:43 +02:00
Ken 87aeb94c64 [Dependencies] Sanity 3.59 update (#3195)
* feat: Updated to sanity 3.59

* bug: Sanity updated icon and media types, so everything broke

* bug: Fallback to default media in article previews if everything is normal
2024-10-03 16:35:00 +02:00
github-actions[bot]andgithub-actions[bot] 4714a5ff32 Version Packages (#3181)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-10-03 16:01:01 +02:00
Halvor HauganandKen ca1d9be88f List: Add indentation and remove aria attributes (#3184)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2024-10-03 15:06:06 +02:00
Halvor Haugan 2654da8b64 update-article-views: Use latest docker image + remove reduntant stuff in workflow (#3189) 2024-10-03 08:52:48 +02:00
Julian Nymark 8d6418a594 Referansesider: 💄 use correct tokens 2024-10-02 14:24:10 +02:00
Julian Nymark 8269091fb8 Referansesider: 💄 outline offset for focus 2024-10-02 14:15:06 +02:00
Julian Nymark fd82184057 Referansesider: 🎨 aktivitetsplan buttons & select 2024-10-02 14:11:31 +02:00
Julian Nymark 39f2cb8614 Referansesider: 🎨 aktivitetsplan main card 2024-10-02 13:57:07 +02:00
Julian Nymark 4bf298f363 Referansesider: 🎨 Tag to own component, flesh out cards for aktivitetsplan 2024-10-02 12:55:54 +02:00
Halvor HauganandKen 3a239e4e17 [aksel.nav.no] Show breakpoints in GuidePanel examples (#3192)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2024-10-02 12:23:10 +02:00
Halvor Haugan 4fcfaf3deb [aksel.nav.no] Storybook: Change to Vite framework (#3185) 2024-10-02 10:14:17 +02:00
Julian Nymark 5dbc66daaa Referansesider: 🚧 Aktivitetsplan 2024-10-02 09:53:17 +02:00
Halvor Haugan 555a46b5da Adjust examples of Switch (#3188) 2024-10-01 13:55:03 +02:00
Julian Nymark 73b2bca156 Combobox: 💄 tweak the visibility of chevron (#3187)
* Combobox: 💄 tweak the visibility of chevron

* Combobox: 💄 dim visibility of icons for readonly datepicker, select, and combobox

* Combobox: 💄 dim visibility of icons for readonly datepicker, select, and combobox

* changeset
2024-10-01 13:47:53 +02:00
Julian Nymark 01411dc11f referansesider: 💄 sykepenger++ 2024-10-01 12:02:38 +02:00
Julian Nymark b1b0d23863 Referansesider: 💄 use more tokens 2024-09-30 14:56:17 +02:00
Julian Nymark cf59ce81bf Combobox disabled fixes (#3183)
* Combobox: 💄 remove hover border color when disabled

* Combobox: 💄 remove hover effect on chips when disabled
2024-09-30 13:53:48 +02:00
Julian Nymark 00c0ff31e1 Combobox: 💄 remove hover border color when disabled (#3182) 2024-09-30 13:38:22 +02:00
Julian Nymark d278060e59 Combobox: 🐛 implement Readonly + disabled (#3180)
* 🐛 [Combobox] implement disabled

* 💄 [Combobox] disabled

* ✨ [Combobox] implement readOnly

* 📝 [Combobox] add examples

* 📝 [Combobox] changeset

* 📝 [Combobox] changeset++

* 📝 [Combobox] stories (review fixes)

* 📝 [Combobox] stories (review fixes++)

* Combobox: 🎨 remove ToggleListButton when readonly or disabled

* Combobox: ♿ combobox input does not support native readonly, use nativeReadOnly={false}

* Revert "Combobox: 🎨 remove ToggleListButton when readonly or disabled"

This reverts commit e54a2eca32.

* Combobox: remove unused code
2024-09-30 11:24:47 +02:00
Ken e5d5b67262 [Referansesider] Migrated to new tokens 2024-09-27 14:13:54 +02:00
Ken 246f25cbaa misc: Update darkmode config for tokens 2024-09-27 14:01:51 +02:00
Ken b2a22438ad bug: Fixed CDN-location for figma config 2024-09-27 12:58:30 +02:00
KenandHalvor Haugan 8b8b724e40 [Darkside] Generate Token config to be consumed by Figma plugin (#3172)
* feat: Build figma config

* bug: Fix ignored gitignore config

* refactor: Rename collection -> name key

* feat: pre-update value to figma config

* feat: Re-resolve name with grouping

* feat: Better handling of relative units

* feat: Added better typings, workflow file

* feat: Updated name-parsing for pregen of config

* feat: Removed Unused eslint-disable directive

* feat: Extract figma scope when creating variables

* refactor: Simplified code by extracting logic

* feat: Updated workflow naming

* bug: re-add missing group for semantic color

* feat: Add metadata to figma config

* bug: Added missing 'contrast declaration

* bug: Calculate correct radius values

* refactor: Simplified config structure by unwrapping scopes

* refactor: Extracted logic for group check

* refactor: Only allow string or number as figma value in config

* feat: Added tests for value conversions

* refactor: Renamed figma config file

* update: Lock update

* refactor: Avoid using format as a workaround to extract token list

* Update @navikt/core/tokens/darkside/figma/index.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/tokens/darkside/figma/create-tokens.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/tokens/darkside/util.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* refactor: inline filter calls

* refactor: Better typing for as group declaration

* refactor: Avoid using objects as type definintions

* refactor: Inline semantic token check for roles

* refactor: date -> timestamp and NO locale

* refactor: token -> tokens

* refactor: removed redundant redaclaration of token name in test

* refactor: Avoid extra tests for same check

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-09-27 12:35:42 +02:00
Ken 9f9bdcc645 bug: Fixed old test for global tokens 2024-09-26 19:13:38 +02:00
Ken 82bdf8075a [Tokens]: Update darkmode tokens and separate config support
feat: Updated darkmode tokens
feat: Split global color config into separate files
2024-09-26 19:09:01 +02:00
Julian Nymark 410e543825 🎨 [referansesider] minside eslint 2024-09-26 15:47:29 +02:00
Julian Nymark 40308e0e77 🎨 [referansesider] minside dokumentarkiv 2024-09-26 15:39:02 +02:00
Julian Nymark fff309b573 💄 [referansesider] minside NotificationsPanel lightmode match figma 2024-09-26 14:44:44 +02:00
Julian Nymark fe7c2de8fa 💄 [referansesider] minside PassiveCards++ 2024-09-26 14:37:42 +02:00
Julian Nymark 6fedea0ba4 💄 [referansesider] minside PassiveCards 2024-09-25 15:02:05 +02:00
Halvor HauganandKen 9811327509 [aksel.nav.no] Adjust examples of Switch (#3175)
---------

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2024-09-25 14:21:19 +02:00
Ken cf2034f914 [Aksel.nav.no] Open all Accordions when searched (#3179)
* feat: Open all Accordions when searched

* refactor: Type node as possibly undefined
2024-09-25 14:19:15 +02:00
dependabot[bot] 9e5e7f8fdc build(deps): Bump rollup from 3.29.4 to 3.29.5 (#3178)
Bumps [rollup](https://github.com/rollup/rollup) from 3.29.4 to 3.29.5.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.29.4...v3.29.5)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 13:49:55 +02:00
Ken e9dc171d74 Referansesider: Dynamic route-list generation for header and homepage 2024-09-25 13:25:43 +02:00
Julian Nymark 544b80eeae 💄 [referansesider] Minside Cards 2024-09-25 13:17:10 +02:00
Julian Nymark 4358b53095 🎨 [referansesider] move Styled components to top layer++ 2024-09-25 12:07:57 +02:00
Julian Nymark de57099b09 🎨 [referansesider] move Styled components to top layer 2024-09-25 11:59:57 +02:00
Ken 5a97cb422d feat: Add cache-control to index.html metadata referansesider 2024-09-25 11:16:03 +02:00
Ken 67fb3bb3a1 feat: Added theme switcher for referansesider 2024-09-25 10:52:03 +02:00
Ken 7bdd5cb748 bug: Fixes darkmode cascading for darkside tokens 2024-09-25 10:10:24 +02:00
github-actions[bot]andgithub-actions[bot] 62bcab1ab0 Version Packages (#3170)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-25 09:36:07 +02:00
Halvor Haugan 61e60d9556 Modify changeset text for Accordion change 2024-09-24 17:17:18 +02:00
Halvor Haugan 9c1eba4450 Accordion: Remove position:relative to fix popover clipping 2024-09-24 16:53:46 +02:00
Julian Nymark 63986d9872 💄 [referansesider] remove overflow 2024-09-24 16:43:04 +02:00
Ken 761c8ef64f refactor: Update tokens use in Piktogram for sykepenger page 2024-09-24 16:31:30 +02:00
Ken 4b86b555d8 feat: Referansesider now properly handles nested routing 2024-09-24 16:10:58 +02:00
Julian Nymark 8e1e294fda 💄 [referansesider] minside add icons 2024-09-24 15:51:55 +02:00
Halvor Haugan f71594e477 ErrorSummary: Remove aria-attributes. (#3171) 2024-09-24 15:41:49 +02:00
Julian Nymark d2a89851cd 💄 [referansesider] responsive design fix 2024-09-24 15:31:25 +02:00
Ken d27da71304 feat: Refactored to use more tokens in referansesider 2024-09-24 15:26:19 +02:00
Julian Nymark fcf17fc750 💄 [referansesider] minside card CSS 2024-09-24 15:24:41 +02:00
Julian Nymark e1b091358b 🎨 [referansesider] add content to minside 2024-09-24 15:15:15 +02:00
dependabot[bot] 988ad39ad1 build(deps): Bump rollup in /examples/referansesider (#3173)
Bumps [rollup](https://github.com/rollup/rollup) from 4.21.3 to 4.22.4.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.21.3...v4.22.4)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-24 14:00:52 +02:00
Ken 524c87e3bd feat: Update referansesider header fontsize 2024-09-24 13:54:48 +02:00
Ken 8cf6123656 feat: Created /komponenter route for component-testing, refactored button-component
feat: Use func for sykepenger function
2024-09-24 13:32:24 +02:00
Julian Nymark 866e6a168c 💄 [referansesider] make sykepenger more responsive 2024-09-24 13:16:47 +02:00
Ken bc2be15c55 Update referansesider
- feat: Refactor to use aksel-icons package
- refactor: use google import for font instead of inline files
2024-09-24 13:06:55 +02:00
Julian Nymark 4e6fc073fe 🐛 [referansesider] only have tanstack router devtools in dev mode 2024-09-24 12:59:56 +02:00
Ken e895c691ce [Referansesider] SPA deploy to aksel-referansesider.ansatt.dev.nav.no (#3174)
* bug: Fix build-script

* feat: Workflow for SPA deploy of referansesider

* feat: Add workflow dispatch to SPA deploy
2024-09-24 12:40:08 +02:00
Halvor Haugan 186c0fec07 🆙 Upgrade some packages (#3165) 2024-09-23 17:24:04 +02:00
Ken 746a58b663 feat: Updated darkside tokens with figma 2024-09-23 16:30:13 +02:00
Julian Nymark d767aadde3 🎨 [referansesider] add crude dekoratør 2024-09-23 14:02:30 +02:00
KenandHalvor Haugan 4359428b40 [Pagination] Deprecated page-prop, synced design with Figma (#3164)
* feat: Mark page-prop as deprecated in pagination

* feat: Mark page-prop as deprecated in pagination with better comment

* feat: Synced pagination with Figma

* memo: Changeset

* memo: Add classes to deprecationlist in stylelint config

* memo: changeset for stylelint

* memo: changeset for stylelint

* Update @navikt/aksel-stylelint/src/deprecations.ts

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update .changeset/thirty-snakes-doubt.md

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-09-23 12:28:52 +02:00
Halvor Haugan d583276428 Combobox: 🏷️ Better typing for maxSelected prop (#3169) 2024-09-23 10:07:06 +02:00
230 changed files with 10045 additions and 10327 deletions

No files matched your search

+6 -2
View File
@@ -73,8 +73,11 @@ module.exports = {
},
{
files: ["**/*.test.*", "**/__tests__/*"],
plugins: ["vitest"],
extends: ["plugin:vitest/recommended", "plugin:testing-library/react"],
plugins: ["@vitest"],
extends: [
"plugin:@vitest/legacy-recommended",
"plugin:testing-library/react",
],
},
{
@@ -153,5 +156,6 @@ module.exports = {
"**/codemod/**/*.js",
"!.storybook",
"**/playwright-report/**",
"**/tokens/**/plugin.js",
],
};
+3
View File
@@ -34,6 +34,9 @@ jobs:
- name: Install dependencies
run: yarn
- name: Create empty .env file (Can be removed when env-file-if-exists is implemented in node v22)
run: echo "" > ./aksel.nav.no/website/.env
- name: Build application
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
+6
View File
@@ -32,6 +32,9 @@ jobs:
yarn boot
yarn docgen
- name: Create empty .env file (Can be removed when env-file-if-exists is implemented in node v22)
run: echo "" > ./aksel.nav.no/website/.env
- name: Update sanity sync
env:
SANITY_WRITE: ${{ secrets.SANITY_WRITE }}
@@ -68,6 +71,9 @@ jobs:
- name: Tests
run: yarn workspace website test
- name: Create empty .env file (Can be removed when env-file-if-exists is implemented in node v22)
run: echo "" > ./aksel.nav.no/website/.env
- name: Build application
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
+3
View File
@@ -23,6 +23,9 @@ jobs:
- name: Install dependencies
run: cd aksel.nav.no/website && yarn
- name: Create empty .env file (Can be removed when env-file-if-exists is implemented in node v22)
run: echo "" > ./aksel.nav.no/website/.env
- name: Export dataset
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
+3
View File
@@ -31,6 +31,9 @@ jobs:
run: |
yarn boot
- name: Create empty .env file (Can be removed when env-file-if-exists is implemented in node v22)
run: echo "" > ./aksel.nav.no/website/.env
- name: Build application
env:
SANITY_READ: ${{ secrets.SANITY_READ }}
+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@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: yarn
- name: set-env for yarn install to @navikt scope
shell: bash
run: echo "NPM_AUTH_TOKEN=${{ secrets.READER_TOKEN }}" >> $GITHUB_ENV
- name: Install dependencies
run: yarn
- 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
@@ -0,0 +1,37 @@
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:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: yarn
- run: yarn install
env:
NPM_AUTH_TOKEN: ${{ secrets.READER_TOKEN }}
- run: yarn boot
- 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 }}
@@ -5,24 +5,11 @@ jobs:
execute:
runs-on: ubuntu-latest
permissions:
packages: "write"
contents: "read"
id-token: "write"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Restore cache
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.os }}-build-${{ github.sha }}
restore-keys: |
${{ runner.os }}-build-
- uses: nais/docker-build-push@v0
id: docker-push
with:
+8
View File
@@ -1,5 +1,13 @@
# @navikt/aksel-icons
## 7.2.0
## 7.1.2
## 7.1.1
## 7.1.0
## 7.0.1
## 7.0.0
@@ -1,3 +1,4 @@
/* eslint-disable import/no-named-as-default-member */
import fastglob from "fast-glob";
import fs from "fs";
import jsYaml from "js-yaml";
@@ -1,6 +1,7 @@
const file_key = "wEdyFjCQSBR3U7FvrMbPXa";
export const fetchIcons = async () => {
console.info("Fetching new icons from Figma...");
const data = await fetch(
`https://api.figma.com/v1/files/${file_key}/components`,
{
@@ -15,6 +16,10 @@ export const fetchIcons = async () => {
throw e.message;
});
if (data?.err) {
throw new Error(data.err);
}
if (!data?.meta?.components || data?.meta?.components.length === 0) {
throw new Error("Fant ingen publiserte ikoner");
}
+1 -2
View File
@@ -1,4 +1,3 @@
import { config } from "dotenv";
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "fs";
import { resolve } from "path";
import { fetchDownloadUrls, fetchIcon, fetchIcons } from "./fetch-icons.mjs";
@@ -6,7 +5,6 @@ import { resolveName } from "./icon-name.mjs";
import { makeConfig } from "./make-configs.mjs";
import { paginate } from "./paginate.mjs";
config();
/* https://www.figma.com/file/wEdyFjCQSBR3U7FvrMbPXa/Core-Icons-Next?node-id=277%3A1221&t=mUJzFvnsceYYXNL5-0 */
const iconFolder = "./icons";
@@ -16,6 +14,7 @@ if (!process.env.FIGMA_TOKEN) {
}
async function main() {
console.info("Started icon-update from Figma");
let icons = await fetchIcons();
const totalIcons = icons.length;
@@ -18,7 +18,7 @@
"react-dom": "^18.0.0"
},
"devDependencies": {
"@figma/plugin-typings": "^1.84.0",
"@figma/plugin-typings": "^1.100.2",
"@svgr/core": "^8.1.0",
"@types/node": "^18.15.11",
"@types/postcss-url": "^10.0.4",
@@ -28,7 +28,7 @@
"postcss": "^8.4.31",
"postcss-url": "^10.1.3",
"typescript": "^5.1.6",
"vite": "^5.1.7",
"vite": "^5.4.6",
"vite-plugin-singlefile": "^1.0.0",
"vite-plugin-svgr": "^4.2.0"
}
@@ -109,7 +109,7 @@ const App = () => {
icon.removeEventListener("dragend", dragEvent);
}
};
}, [categories]);
}, []);
return (
<main tabIndex={-1} className="wrapper">
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "7.0.1",
"version": "7.2.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
@@ -62,7 +62,7 @@
"update-metadata": "node config/metadata.js",
"write-packagejson": "echo '{\"type\": \"module\"}' > dist/react/esm/package.json",
"build": "yarn copy && yarn create-icons && concurrently \"tsc\" \"tsc -p tsconfig.esm.json && tsc-alias -p tsconfig.esm.json && yarn write-packagejson\" && node config/cleanTypes.js && yarn update-metadata && yarn parse-svg",
"fetch-new:icons": "node config/figma/index.mjs",
"fetch-new:icons": "tsx --env-file=.env config/figma/index.mjs",
"zip:icons": "node config/zip.js",
"test": "vitest run",
"test:watch": "vitest watch"
@@ -73,7 +73,6 @@
"concurrently": "7.2.1",
"copyfiles": "^2.4.1",
"cross-zip-cli": "1.0.0",
"dotenv": "^10.0.0",
"fast-glob": "3.2.11",
"hast-util-select": "5.0.5",
"js-yaml": "^4.1.0",
@@ -83,6 +82,7 @@
"react": "^18.0.0",
"rehype-parse": "8.0.4",
"tsc-alias": "1.8.8",
"tsx": "^4.7.1",
"typescript": "^5.1.6",
"unified": "10.1.2",
"vitest": "^1.2.2"
+16
View File
@@ -1,5 +1,21 @@
# @navikt/aksel-stylelint
## 7.2.0
## 7.1.2
## 7.1.1
### Patch Changes
- List: :boom: Removed class names "navds-list--nested" and "navds-list\_\_item-content" ([#3184](https://github.com/navikt/aksel/pull/3184))
## 7.1.0
### Minor Changes
- Pagination: :boom: Added some classNames to list of deprecated (removed) classes. ([#3164](https://github.com/navikt/aksel/pull/3164))
## 7.0.1
## 7.0.0
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "7.0.1",
"version": "7.2.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
"repository": {
@@ -35,8 +35,8 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^7.0.1",
"@navikt/ds-tokens": "^7.0.1",
"@navikt/ds-css": "^7.2.0",
"@navikt/ds-tokens": "^7.2.0",
"concurrently": "7.2.1",
"postcss-selector-parser": "^6.0.13",
"postcss-value-parser": "^4.2.0",
@@ -60,4 +60,16 @@ export const deprecations: DeprecatedList = [
classes: ["navds-textarea__wrapper"],
message: "Removed in v6.0.0",
},
{
classes: [
"navds-pagination__prev-next-icon",
"navds-pagination__next-text",
"navds-pagination__prev-text",
],
message: "These classes had no effect and were removed in v7.1.0",
},
{
classes: ["navds-list--nested", "navds-list__item-content"],
message: "Removed in v7.1.1",
},
];
+28
View File
@@ -1,5 +1,33 @@
# @navikt/aksel
## 7.2.0
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@7.2.0
## 7.1.2
### Patch Changes
- Updated dependencies [[`95c84e1d0`](https://github.com/navikt/aksel/commit/95c84e1d014d880bc22eb0053a875771e8399540)]:
- @navikt/ds-css@7.1.2
## 7.1.1
### Patch Changes
- Updated dependencies [[`ca1d9be88`](https://github.com/navikt/aksel/commit/ca1d9be88f45f936ee8546af146f8783d8c43db2), [`73b2bca15`](https://github.com/navikt/aksel/commit/73b2bca156072c945c2fc4e4e849f50a00398eb6), [`d278060e5`](https://github.com/navikt/aksel/commit/d278060e59b17638cbda22dc194e89b01dc2113e)]:
- @navikt/ds-css@7.1.1
## 7.1.0
### Patch Changes
- Updated dependencies [[`9c1eba445`](https://github.com/navikt/aksel/commit/9c1eba44508f295cc6261ff96822b18557bd7ddc)]:
- @navikt/ds-css@7.1.0
## 7.0.1
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "7.0.1",
"version": "7.2.0",
"description": "Aksel command line interface. Handles css-imports, codemods and more",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
"dependencies": {
"@navikt/ds-css": "7.0.1",
"@navikt/ds-css": "7.2.0",
"axios": "1.7.4",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
+1
View File
@@ -1,6 +1,7 @@
import { applyTransform } from "jscodeshift/dist/testUtils";
import fs from "node:fs";
import path from "node:path";
// eslint-disable-next-line import/default
import prettier from "prettier";
import { describe, expect, test } from "vitest";
+24
View File
@@ -1,5 +1,29 @@
# @navikt/ds-css
## 7.2.0
## 7.1.2
### Patch Changes
- List: Explicitly set list-style on ol to override reset-CSS ([#3202](https://github.com/navikt/aksel/pull/3202))
## 7.1.1
### Patch Changes
- List: Add indentation and remove overly verbose aria attributes ([#3184](https://github.com/navikt/aksel/pull/3184))
- Datepicker/Select: :lipstick: dim non-interactable icons when readonly is set. ([#3187](https://github.com/navikt/aksel/pull/3187))
- Combobox: :bug: implement missing readonly and disabled ([#3180](https://github.com/navikt/aksel/pull/3180))
## 7.1.0
### Patch Changes
- Accordion: Remove 'relative' positioning when focused. This resolves some issues where floating elements like popover ended up clipping. ([`9c1eba445`](https://github.com/navikt/aksel/commit/9c1eba44508f295cc6261ff96822b18557bd7ddc))
## 7.0.1
### Patch Changes
-4
View File
@@ -2,10 +2,6 @@
--__ac-accordion-header-shadow-color: var(--ac-accordion-header-border, var(--a-border-divider));
}
.navds-accordion__item:focus-within {
position: relative;
}
/*************************
* Header *
*************************/
-1
View File
@@ -31,7 +31,6 @@
@media (forced-colors: active) {
.navds-copybutton {
background-color: ButtonFace;
border-color: ButtonText;
border: solid 1px ButtonText;
color: ButtonText;
}
+1
View File
@@ -281,6 +281,7 @@
/* Readonly */
.navds-date__field--readonly .navds-date__field-button {
cursor: default;
color: var(--a-gray-500);
}
.navds-date__caption-button {
+30
View File
@@ -20,6 +20,36 @@
--__ac-combobox-input-height: 1.5rem;
}
.navds-combobox--disabled {
opacity: 0.3;
}
.navds-combobox--disabled .navds-combobox__wrapper *:hover {
cursor: not-allowed;
}
.navds-combobox--disabled .navds-text-field__input {
border: 1px solid var(--a-border-default);
}
.navds-combobox--disabled .navds-combobox__selected-options {
pointer-events: none;
}
.navds-combobox--readonly {
pointer-events: none;
}
.navds-combobox--readonly .navds-combobox__button-toggle-list {
color: var(--a-gray-500);
}
.navds-combobox--readonly .navds-text-field__input,
.navds-combobox--readonly .navds-combobox__input {
background-color: var(--a-surface-subtle);
border-color: var(--a-border-subtle);
}
.navds-combobox__button-clear svg,
.navds-combobox__button-toggle-list svg,
.navds-combobox__list svg {
+4
View File
@@ -109,3 +109,7 @@
border-color: var(--a-border-subtle);
cursor: default;
}
.navds-select--readonly .navds-select__chevron {
color: var(--a-gray-500);
}
-1
View File
@@ -10,7 +10,6 @@
@media (forced-colors: active) {
.navds-internalheader {
background-color: ButtonFace;
border-color: ButtonText;
border: solid 1px ButtonText;
color: ButtonText;
}
+60 -71
View File
@@ -4,92 +4,81 @@
margin-block: var(--a-spacing-4);
}
.navds-list--nested ul,
.navds-list--nested ol {
margin-block: var(--a-spacing-2);
}
.navds-list--small ul,
.navds-list--small ol {
margin-block: var(--a-spacing-3);
}
.navds-list li {
margin-block-end: var(--a-spacing-2);
}
.navds-list:last-child > ul > li:last-child,
.navds-list:last-child > ol > li:last-child {
margin-bottom: 0;
}
.navds-list ul .navds-list__item {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--a-spacing-3);
padding-left: 0;
}
.navds-list ul .navds-list__item::marker {
content: none;
}
.navds-list ol .navds-list__item-marker--icon {
grid-column: 1 / 2;
display: flex;
}
.navds-list .navds-list__item-marker--bullet {
display: flex;
align-items: center;
height: var(--a-font-line-height-xlarge);
grid-column: 1 / 2;
color: var(--ac-list-marker-ul-color, var(--ac-list-marker-color, var(--a-icon-default)));
}
.navds-list--small .navds-list__item-marker--bullet {
height: var(--a-font-line-height-large);
}
.navds-list__item-marker--icon {
color: var(--ac-list-marker-icon-color, var(--ac-list-marker-color, var(--a-icon-default)));
display: flex;
align-items: center;
height: var(--a-font-line-height-xlarge);
padding-block-end: 1px;
font-size: 1.5rem;
}
.navds-list--small .navds-list__item-marker--icon {
height: var(--a-font-line-height-large);
}
.navds-list__item-marker--icon svg {
flex-shrink: 0;
}
.navds-list ul .navds-list__item-content {
grid-column: 2 / 3;
.navds-list .navds-list :where(ul, ol) {
margin-block: var(--a-spacing-2) 0;
}
.navds-list ol {
padding-left: var(--a-spacing-2);
list-style: decimal;
list-style: decimal; /* This is the default value, but some frameworks have `ol,ul { list-style:none }` */
padding-left: 1.7rem;
}
.navds-list ol li {
padding-left: var(--a-spacing-1);
margin-left: var(--a-spacing-2);
.navds-list ol > .navds-list__item {
padding-left: 0.3rem;
}
/* SAFARI HACK START */
/* stylelint-disable selector-type-no-unknown */
_::-webkit-full-page-media,
_:future,
:root .navds-list ol {
padding-left: 2.1rem;
}
_::-webkit-full-page-media,
_:future,
:root .navds-list ol .navds-list__item {
padding-left: 0;
}
/* SAFARI HACK END */
/* stylelint-enable selector-type-no-unknown */
.navds-list__item {
margin-block-end: var(--a-spacing-2);
}
.navds-list__item:last-child {
margin-block-end: 0;
}
.navds-list ul > .navds-list__item {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--a-spacing-2);
}
.navds-list ol li::marker {
font-weight: var(--a-font-weight-bold);
font-size: var(--a-font-size-large);
color: var(--ac-list-marker-ol-color, var(--ac-list-marker-color, var(--a-icon-default)));
line-height: var(--a-font-line-height-xlarge);
}
.navds-list--small ol li::marker {
font-size: var(--a-font-size-small);
line-height: var(--a-font-line-height-large);
.navds-list__item-marker {
width: 1.5rem;
}
.navds-list ul > li > .navds-list__item-marker {
display: flex;
align-items: center;
height: var(--a-font-line-height-xlarge);
}
.navds-list--small ul > li > .navds-list__item-marker {
height: var(--a-font-line-height-large);
}
.navds-list__item-marker--bullet {
padding-left: 0.8rem;
color: var(--ac-list-marker-ul-color, var(--ac-list-marker-color, var(--a-icon-default)));
}
.navds-list__item-marker--icon {
font-size: 1.5rem;
color: var(--ac-list-marker-icon-color, var(--ac-list-marker-color, var(--a-icon-default)));
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "7.0.1",
"version": "7.2.0",
"description": "CSS for NAV Designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -27,7 +27,7 @@
"css:get-version": "node config/get-version.js"
},
"devDependencies": {
"@navikt/ds-tokens": "^7.0.1",
"@navikt/ds-tokens": "^7.2.0",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
-1
View File
@@ -19,7 +19,6 @@
@media (forced-colors: active) {
.navds-read-more__button {
background-color: ButtonFace;
border-color: ButtonText;
border: solid 1px ButtonText;
color: ButtonText;
}
+52
View File
@@ -1,5 +1,57 @@
# @navikt/ds-react
## 7.2.0
### Minor Changes
- FileUpload: :boom: Remove UNSAFE-prefix ([#3205](https://github.com/navikt/aksel/pull/3205))
- List: :sparkles: New size 'large' ([#3207](https://github.com/navikt/aksel/pull/3207))
### Patch Changes
- Performance: :zap: Optimized memoization for rerendring in some components. ([#3197](https://github.com/navikt/aksel/pull/3197))
- Updated dependencies []:
- @navikt/ds-tokens@7.2.0
- @navikt/aksel-icons@7.2.0
## 7.1.2
### Patch Changes
- Updated dependencies []:
- @navikt/ds-tokens@7.1.2
- @navikt/aksel-icons@7.1.2
## 7.1.1
### Patch Changes
- List: Add indentation and remove overly verbose aria attributes ([#3184](https://github.com/navikt/aksel/pull/3184))
- Combobox: :bug: implement missing readonly and disabled ([#3180](https://github.com/navikt/aksel/pull/3180))
- Updated dependencies []:
- @navikt/ds-tokens@7.1.1
- @navikt/aksel-icons@7.1.1
## 7.1.0
### Minor Changes
- Pagination: :lipstick: Updated font-weight to 600 and marked 'page'-prop on 'Item' as deprecated. ([#3164](https://github.com/navikt/aksel/pull/3164))
### Patch Changes
- Combobox: :label: Better typing for maxSelected prop ([#3169](https://github.com/navikt/aksel/pull/3169))
- ErrorSummary: Remove aria-attributes. ([#3171](https://github.com/navikt/aksel/pull/3171))
- Updated dependencies []:
- @navikt/ds-tokens@7.1.0
- @navikt/aksel-icons@7.1.0
## 7.0.1
### Patch Changes
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "7.0.1",
"version": "7.2.0",
"description": "React components from the Norwegian Labour and Welfare Administration.",
"author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
"license": "MIT",
@@ -583,7 +583,7 @@
"./package.json": "./package.json"
},
"scripts": {
"docgen": "yarn ts-node ../../../scripts/docgen.ts",
"docgen": "yarn tsx ../../../scripts/docgen.ts",
"write-packagejson": "echo '{\"type\": \"module\"}' > esm/package.json",
"clean": "rimraf cjs esm",
"build": "concurrently \"tsc -p tsconfig.build.json\" \"tsc -p tsconfig.esm.json && tsc-alias -p tsconfig.esm.json && yarn write-packagejson\" ",
@@ -594,8 +594,8 @@
"dependencies": {
"@floating-ui/react": "0.25.4",
"@floating-ui/react-dom": "^2.0.9",
"@navikt/aksel-icons": "^7.0.1",
"@navikt/ds-tokens": "^7.0.1",
"@navikt/aksel-icons": "^7.2.0",
"@navikt/ds-tokens": "^7.2.0",
"clsx": "^2.1.0",
"date-fns": "^3.0.0",
"react-day-picker": "8.10.0"
@@ -614,8 +614,8 @@
"react-router-dom": "^6.3.0",
"rimraf": "6.0.1",
"swr": "^1.1.2",
"ts-node": "^10.9.1",
"tsc-alias": "1.8.8",
"tsx": "^4.19.1",
"typescript": "^5.1.6",
"vitest": "^1.2.2"
},
@@ -73,40 +73,35 @@ export const Default: StoryObj<DefaultStoryProps> = {
locale={props.locale}
disableWeekends={props.disableWeekends}
>
{!props.standalone && (
<>
{props.inputfield && props.mode !== "multiple" ? (
<>
{props.mode === "range" ? (
<div style={{ display: "flex", gap: "1rem" }}>
<DatePicker.Input
label="Fra"
size={props?.size}
{...rangeCtx.fromInputProps}
/>
<DatePicker.Input
label="Til"
size={props?.size}
{...rangeCtx.toInputProps}
/>
</div>
) : (
<>
<DatePicker.Input
label="Velg dato"
size={props?.size}
{...singleCtx.inputProps}
/>
</>
)}
</>
) : (
<Button onClick={() => setOpen((x) => !x)}>
Åpne datovelger
</Button>
)}
</>
)}
{!props.standalone &&
(props.inputfield && props.mode !== "multiple" ? (
<>
{props.mode === "range" ? (
<div style={{ display: "flex", gap: "1rem" }}>
<DatePicker.Input
label="Fra"
size={props?.size}
{...rangeCtx.fromInputProps}
/>
<DatePicker.Input
label="Til"
size={props?.size}
{...rangeCtx.toInputProps}
/>
</div>
) : (
<DatePicker.Input
label="Velg dato"
size={props?.size}
{...singleCtx.inputProps}
/>
)}
</>
) : (
<Button onClick={() => setOpen((x) => !x)}>
Åpne datovelger
</Button>
))}
</Comp>
</div>
);
@@ -19,7 +19,7 @@ const App = () => {
};
describe("Render datepicker", () => {
// eslint-disable-next-line vitest/expect-expect
// eslint-disable-next-line @vitest/expect-expect
test("Should not crash when e.target is window", async () => {
render(<App />);
@@ -4,29 +4,29 @@ import { describe, expect, test } from "vitest";
import { isValidDate, parseDate } from "..";
const check = (inp: string) =>
// eslint-disable-next-line vitest/valid-expect
// eslint-disable-next-line @vitest/valid-expect
expect(isValidDate(parseDate(inp, new Date(), nb, "date", false)));
const checkTwoDigit = (inp: string) =>
// eslint-disable-next-line vitest/valid-expect
// eslint-disable-next-line @vitest/valid-expect
expect(isValidDate(parseDate(inp, new Date(), nb, "date", true)));
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date", false);
describe("Parse date-inputs with 4-digit years", () => {
// eslint-disable-next-line vitest/expect-expect
// eslint-disable-next-line @vitest/expect-expect
test("No spaces", () => {
check("11052022").toBeTruthy();
check("15052022").toBeTruthy();
});
// eslint-disable-next-line vitest/expect-expect
// eslint-disable-next-line @vitest/expect-expect
test(". divider", () => {
check("1.5.2022").toBeTruthy();
check("11.05.2022").toBeTruthy();
});
// eslint-disable-next-line vitest/expect-expect
// eslint-disable-next-line @vitest/expect-expect
test("/ divider", () => {
check("1/5/2022").toBeTruthy();
check("10/5/2022").toBeTruthy();
@@ -34,7 +34,7 @@ describe("Parse date-inputs with 4-digit years", () => {
check("10/05/2022").toBeTruthy();
});
// eslint-disable-next-line vitest/expect-expect
// eslint-disable-next-line @vitest/expect-expect
test("- divider", () => {
check("1-5-2022").toBeTruthy();
check("10-5-2022").toBeTruthy();
@@ -44,19 +44,19 @@ describe("Parse date-inputs with 4-digit years", () => {
});
describe("Parse date-inputs with 2-digit years", () => {
// eslint-disable-next-line vitest/expect-expect
// eslint-disable-next-line @vitest/expect-expect
test("No spaces", () => {
checkTwoDigit("110522").toBeTruthy();
checkTwoDigit("150522").toBeTruthy();
});
// eslint-disable-next-line vitest/expect-expect
// eslint-disable-next-line @vitest/expect-expect
test(". divider", () => {
checkTwoDigit("1.5.22").toBeTruthy();
checkTwoDigit("11.05.22").toBeTruthy();
});
// eslint-disable-next-line vitest/expect-expect
// eslint-disable-next-line @vitest/expect-expect
test("/ divider", () => {
checkTwoDigit("1/5/22").toBeTruthy();
checkTwoDigit("10/5/22").toBeTruthy();
@@ -64,7 +64,7 @@ describe("Parse date-inputs with 2-digit years", () => {
checkTwoDigit("10/05/22").toBeTruthy();
});
// eslint-disable-next-line vitest/expect-expect
// eslint-disable-next-line @vitest/expect-expect
test("- divider", () => {
checkTwoDigit("1-5-22").toBeTruthy();
checkTwoDigit("10-5-22").toBeTruthy();
@@ -72,14 +72,14 @@ describe("Parse date-inputs with 2-digit years", () => {
checkTwoDigit("10-05-22").toBeTruthy();
});
// eslint-disable-next-line vitest/expect-expect
// eslint-disable-next-line @vitest/expect-expect
test("Dissallow 1 and 3 digit years", () => {
checkTwoDigit("11052").toBeFalsy();
checkTwoDigit("1105222").toBeFalsy();
checkTwoDigit("1105999").toBeFalsy();
});
// eslint-disable-next-line vitest/expect-expect
// eslint-disable-next-line @vitest/expect-expect
test("Dissallow date before year 1000", () => {
checkTwoDigit("11050999").toBeFalsy();
checkTwoDigit("11050010").toBeFalsy();
@@ -1,6 +1,7 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import { BodyShort, ErrorMessage, Label } from "../../typography";
import { ReadOnlyIcon } from "../ReadOnlyIcon";
import ComboboxWrapper from "./ComboboxWrapper";
import FilteredOptions from "./FilteredOptions/FilteredOptions";
import { useFilteredOptionsContext } from "./FilteredOptions/filteredOptionsContext";
@@ -27,6 +28,7 @@ export const Combobox = forwardRef<
inputProps,
showErrorMsg,
size = "medium",
readOnly,
} = useInputContext();
return (
@@ -44,6 +46,7 @@ export const Combobox = forwardRef<
"navds-sr-only": hideLabel,
})}
>
<ReadOnlyIcon nativeReadOnly={false} readOnly={readOnly} />
{label}
</Label>
{!!description && (
@@ -36,6 +36,7 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
children,
defaultValue,
disabled,
readOnly,
error,
errorId,
filteredOptions: externalFilteredOptions,
@@ -63,6 +64,7 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
defaultValue,
description: rest.description,
disabled,
readOnly,
error,
errorId,
id,
@@ -9,6 +9,7 @@ type ComboboxWrapperProps = {
inputProps: {
disabled?: boolean;
};
readOnly?: boolean;
inputSize: string;
toggleIsListOpen: (isListOpen: boolean) => void;
};
@@ -21,7 +22,7 @@ const ComboboxWrapper = ({
inputSize,
toggleIsListOpen,
}: ComboboxWrapperProps) => {
const { toggleOpenButtonRef, clearInput } = useInputContext();
const { toggleOpenButtonRef, clearInput, readOnly } = useInputContext();
const wrapperRef = useRef<HTMLDivElement | null>(null);
const [hasFocusWithin, setHasFocusWithin] = useState(false);
@@ -55,6 +56,8 @@ const ComboboxWrapper = ({
"navds-combobox--error": hasError,
"navds-combobox--disabled": !!inputProps.disabled,
"navds-combobox--focused": hasFocusWithin,
"navds-combobox--readonly": readOnly,
"navds-form-field--readonly": readOnly,
},
)}
onFocus={onFocusInsideWrapper}
@@ -54,6 +54,7 @@ const FilteredOptions = () => {
)}
{shouldRenderFilteredOptionsList && (
/* biome-ignore lint/a11y/useFocusableInteractive: Interaction is not handeled by listbox itself. */
<ul
ref={setFilteredOptionsRef}
role="listbox"
@@ -58,7 +58,8 @@ const FilteredOptionsProvider = ({
useState<HTMLUListElement | null>(null);
const virtualFocus = useVirtualFocus(filteredOptionsRef);
const {
inputProps: { "aria-describedby": partialAriaDescribedBy, id },
inputProps: { "aria-describedby": partialAriaDescribedBy, id, disabled },
readOnly,
value,
searchTerm,
setValue,
@@ -136,6 +137,9 @@ const FilteredOptionsProvider = ({
const toggleIsListOpen = useCallback(
(newState?: boolean) => {
if (disabled || readOnly) {
return;
}
virtualFocus.moveFocusToTop();
if (newState ?? !isInternalListOpen) {
setHideCaret(!!maxSelected?.isLimitReached);
@@ -147,6 +151,8 @@ const FilteredOptionsProvider = ({
maxSelected?.isLimitReached,
isInternalListOpen,
setHideCaret,
disabled,
readOnly,
],
);
@@ -32,6 +32,7 @@ interface Props {
defaultValue: ComboboxProps["defaultValue"];
description: ComboboxProps["description"];
disabled: ComboboxProps["disabled"];
readOnly: ComboboxProps["readOnly"];
error: ComboboxProps["error"];
errorId: ComboboxProps["errorId"];
id: ComboboxProps["id"];
@@ -48,6 +49,7 @@ const InputProvider = ({ children, value: props }: Props) => {
defaultValue = "",
description,
disabled,
readOnly,
error,
errorId,
id: externalId,
@@ -61,6 +63,7 @@ const InputProvider = ({ children, value: props }: Props) => {
{
description,
disabled,
readOnly,
error,
errorId,
id: externalId,
@@ -96,7 +99,7 @@ const InputProvider = ({ children, value: props }: Props) => {
setInternalValue("");
setSearchTerm("");
},
[externalOnChange, onClear, setInternalValue],
[externalOnChange, onClear],
);
const focusInput = useCallback(() => {
@@ -49,6 +49,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
setValue,
hideCaret,
setHideCaret,
readOnly,
} = useInputContext();
const {
selectedOptions,
@@ -149,6 +150,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
setIsMouseLastUsedInputDevice(false);
if (readOnly) {
return;
}
if (e.key === "Backspace") {
if (value === "" && shouldShowSelectedOptions) {
const lastSelectedOption =
@@ -214,6 +218,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
setValue,
searchTerm,
shouldShowSelectedOptions,
readOnly,
],
);
@@ -262,6 +267,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
aria-activedescendant={activeDecendantId}
aria-describedby={ariaDescribedBy}
aria-invalid={inputProps["aria-invalid"]}
readOnly={readOnly}
/>
);
},
@@ -45,6 +45,7 @@ export const InputController = forwardRef<
size = "medium",
inputRef,
toggleOpenButtonRef,
readOnly,
} = useInputContext();
const { activeDecendantId } = useFilteredOptionsContext();
@@ -65,6 +66,7 @@ export const InputController = forwardRef<
id={inputProps.id}
ref={mergedInputRef}
inputClassName={inputClassName}
readOnly={readOnly}
{...rest}
/>
) : (
@@ -74,6 +76,7 @@ export const InputController = forwardRef<
ref={mergedInputRef}
inputClassName={inputClassName}
shouldShowSelectedOptions={shouldShowSelectedOptions}
readOnly={readOnly}
{...rest}
/>
</SelectedOptions>
@@ -12,7 +12,7 @@ interface SelectedOptionsProps {
const Option = ({ option }: { option: ComboboxOption }) => {
const { isMultiSelect, removeSelectedOption } = useSelectedOptionsContext();
const { focusInput } = useInputContext();
const { focusInput, readOnly, inputProps } = useInputContext();
const onClick = (e) => {
e.stopPropagation();
@@ -28,7 +28,13 @@ const Option = ({ option }: { option: ComboboxOption }) => {
);
}
return <Chips.Removable onClick={onClick}>{option.label}</Chips.Removable>;
return readOnly || inputProps.disabled ? (
<Chips.Toggle variant="neutral" checkmark={false} as="div">
{option.label}
</Chips.Toggle>
) : (
<Chips.Removable onClick={onClick}>{option.label}</Chips.Removable>
);
};
const SelectedOptions: React.FC<SelectedOptionsProps> = ({
@@ -4,7 +4,7 @@ import { usePrevious } from "../../../util/hooks";
import { useInputContext } from "../Input/Input.context";
import { isInList } from "../combobox-utils";
import { useComboboxCustomOptions } from "../customOptionsContext";
import { ComboboxOption, ComboboxProps, MaxSelected } from "../types";
import { ComboboxOption, ComboboxProps } from "../types";
type SelectedOptionsContextValue = {
addSelectedOption: (option: ComboboxOption) => void;
@@ -12,7 +12,7 @@ type SelectedOptionsContextValue = {
removeSelectedOption: (option: ComboboxOption) => void;
prevSelectedOptions?: ComboboxOption[];
selectedOptions: ComboboxOption[];
maxSelected?: MaxSelected & { isLimitReached: boolean };
maxSelected?: ComboboxProps["maxSelected"] & { isLimitReached: boolean };
setSelectedOptions: (any) => void;
toggleOption: (
option: ComboboxOption,
@@ -105,9 +105,10 @@ const SelectedOptionsProvider = ({
(!!maxSelected?.limit && selectedOptions.length >= maxSelected.limit) ||
(!isMultiSelect && selectedOptions.length > 0);
// biome-ignore lint/correctness/useExhaustiveDependencies: We explicitly want to run this effect when selectedOptions changes to match the view with the selected options.
useEffect(() => {
setHideCaret(isLimitReached);
}, [selectedOptions, setHideCaret, isLimitReached]);
}, [isLimitReached, selectedOptions, setHideCaret]);
const toggleOption = useCallback(
(
@@ -7,7 +7,7 @@ import { VStack } from "../../layout/stack";
import { Modal } from "../../modal";
import { BodyLong } from "../../typography";
import { TextField } from "../textfield";
import { UNSAFE_Combobox } from "./index";
import { ComboboxProps, UNSAFE_Combobox } from "./index";
export default {
title: "ds-react/Combobox",
@@ -38,14 +38,16 @@ const options = [
"grapefruit",
];
export const Default: StoryFunction = (props) => (
export const Default: StoryFn<ComboboxProps & { maxSelected?: number }> = ({
maxSelected,
...rest
}) => (
<UNSAFE_Combobox
{...props}
maxSelected={{ limit: Number(props.maxSelected) }}
{...rest}
maxSelected={maxSelected && { limit: maxSelected }}
id="combobox"
/>
);
Default.args = {
options,
label: "Hva er din favorittfrukt?",
@@ -65,6 +67,9 @@ Default.argTypes = {
disabled: {
control: { type: "boolean" },
},
readOnly: {
control: { type: "boolean" },
},
isListOpen: {
control: { type: "boolean" },
},
@@ -482,12 +487,50 @@ export const InModal: StoryFn = () => {
};
export const Disabled: StoryFn = () => {
const selectedOptionsMultiple = ["napoleonskake", "donut"];
const selectedOptionsSingle = ["pushups"];
return (
<UNSAFE_Combobox
options={options}
label="Hva er dine favorittfrukter?"
disabled
/>
<VStack gap="2">
<UNSAFE_Combobox
options={options}
label="Hva er dine favorittfrukter?"
description="Bare de fruktene som du spiser minst 5 av om dagen teller vi som en favorittfrukt."
selectedOptions={selectedOptionsMultiple}
isMultiSelect
disabled
/>
<UNSAFE_Combobox
options={options}
label="Hva er din favoritthobby?"
description="Her snakker vi ting du bruker minst én time på hver dag."
selectedOptions={selectedOptionsSingle}
disabled
/>
</VStack>
);
};
export const Readonly: StoryFn = () => {
const selectedOptionsMultiple = ["napoleonskake", "donut"];
const selectedOptionsSingle = ["pushups"];
return (
<VStack gap="2">
<UNSAFE_Combobox
options={options}
label="Hva er dine favorittfrukter?"
description="Bare de fruktene som du spiser minst 5 av om dagen teller vi som en favorittfrukt."
selectedOptions={selectedOptionsMultiple}
isMultiSelect
readOnly
/>
<UNSAFE_Combobox
options={options}
label="Hva er din favoritthobby?"
description="Her snakker vi ting du bruker minst én time på hver dag."
selectedOptions={selectedOptionsSingle}
readOnly
/>
</VStack>
);
};
@@ -523,6 +566,8 @@ export const Chromatic: StoryFn = () => {
<WithError />
<H2>Disabled</H2>
<Disabled />
<H2>Readonly</H2>
<Readonly />
</VStack>
);
};
@@ -35,7 +35,7 @@ const CustomOptionsProvider = ({
);
focusInput();
},
[focusInput, setCustomOptions],
[focusInput],
);
const addCustomOption = useCallback(
@@ -47,7 +47,7 @@ const CustomOptionsProvider = ({
}
focusInput();
},
[focusInput, isMultiSelect, setCustomOptions],
[focusInput, isMultiSelect],
);
const customOptionsState = {
+10 -12
View File
@@ -16,17 +16,6 @@ export type ComboboxOption = {
value: string;
};
export type MaxSelected = {
/**
* The limit for maximum selected options
*/
limit: number;
/**
* Override the message to display when the limit for maximum selected options has been reached
*/
message?: string;
};
export interface ComboboxProps
extends FormFieldProps,
Omit<
@@ -135,7 +124,16 @@ export interface ComboboxProps
/**
* Options for the maximum number of selected options.
*/
maxSelected?: MaxSelected;
maxSelected?: {
/**
* The limit for maximum selected options
*/
limit: number;
/**
* Override the message to display when the limit for maximum selected options has been reached
*/
message?: string;
};
/**
* Set to `true` to enable inline autocomplete.
*
@@ -2,7 +2,7 @@ import cl from "clsx";
import React, { HTMLAttributes, forwardRef, useRef } from "react";
import { BodyShort, Heading } from "../../typography";
import { composeEventHandlers } from "../../util/composeEventHandlers";
import { useId, useMergeRefs } from "../../util/hooks";
import { useMergeRefs } from "../../util/hooks";
import ErrorSummaryItem from "./ErrorSummaryItem";
export interface ErrorSummaryProps
@@ -33,13 +33,13 @@ interface ErrorSummaryComponent
ErrorSummaryProps & React.RefAttributes<HTMLDivElement>
> {
/**
* Link to error.
* Error message with link to field.
*
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
*
* @example
* ```jsx
* <ErrorSummary.Item href="#1">
* <ErrorSummary.Item href="#id-til-alderfelt">
* Felt må fylles ut med alder
* </ErrorSummary.Item>
* ```
@@ -48,7 +48,7 @@ interface ErrorSummaryComponent
}
/**
* A component that displays a summary of errors.
* Summary of errors in a form.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/errorsummary)
* @see 🏷️ {@link ErrorSummaryProps}
@@ -77,15 +77,13 @@ export const ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(
},
ref,
) => {
const headingId = useId();
const sectionRef = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const headingRef = useRef<HTMLHeadingElement>(null);
const mergedRef = useMergeRefs(ref, sectionRef);
const mergedRef = useMergeRefs(ref, wrapperRef);
return (
<section
<div
ref={mergedRef}
{...rest}
className={cl(
@@ -94,11 +92,8 @@ export const ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(
`navds-error-summary--${size}`,
)}
tabIndex={-1}
aria-live="polite"
aria-relevant="all"
aria-labelledby={headingId}
onFocus={composeEventHandlers(rest.onFocus, (event) => {
if (event.target === sectionRef.current) {
if (event.target === wrapperRef.current) {
headingRef?.current?.focus();
}
})}
@@ -107,7 +102,6 @@ export const ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(
className="navds-error-summary__heading"
as={headingTag}
size="small"
id={headingId}
ref={headingRef}
tabIndex={-1}
>
@@ -116,7 +110,7 @@ export const ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(
<BodyShort as="ul" size={size} className="navds-error-summary__list">
{children}
</BodyShort>
</section>
</div>
);
},
) as ErrorSummaryComponent;
@@ -2,11 +2,7 @@ import { Meta, StoryFn, StoryObj } from "@storybook/react";
import { fireEvent, within } from "@storybook/test";
import React from "react";
import { ImageIcon } from "@navikt/aksel-icons";
import {
type FileObject,
UNSAFE_FileUpload as FileUpload,
type FilesPartitioned,
} from ".";
import { type FileObject, FileUpload, type FilesPartitioned } from ".";
const meta: Meta<typeof FileUpload.Dropzone> = {
title: "ds-react/FileUpload/Dropzone",
@@ -1,6 +1,6 @@
import { Meta, StoryFn } from "@storybook/react";
import React from "react";
import { FileItem, UNSAFE_FileUpload as FileUpload } from ".";
import { FileItem, FileUpload } from ".";
import { VStack } from "../../layout/stack";
const meta: Meta<typeof FileUpload.Item> = {
@@ -1,7 +1,7 @@
import { Meta, StoryFn } from "@storybook/react";
import React, { useEffect, useState } from "react";
import { UploadIcon } from "@navikt/aksel-icons";
import { UNSAFE_FileUpload as FileUpload, FileUploadItemProps } from ".";
import { FileUpload, FileUploadItemProps } from ".";
import { Alert } from "../../alert";
import { Button } from "../../button";
import { VStack } from "../../layout/stack";
@@ -1,9 +1,9 @@
"use client";
export { default as UNSAFE_FileUpload } from "./FileUpload";
export { default as UNSAFE_FileUploadDropzone } from "./parts/dropzone/Dropzone";
export { default as FileUpload } from "./FileUpload";
export { default as FileUploadDropzone } from "./parts/dropzone/Dropzone";
export { type FileUploadDropzoneProps } from "./parts/dropzone/dropzone.types";
export {
default as UNSAFE_FileUploadTrigger,
default as FileUploadTrigger,
type FileUploadTriggerProps,
} from "./parts/Trigger";
export {
@@ -15,7 +15,7 @@ export {
type FileRejectionReason,
} from "./FileUpload.types";
export {
default as UNSAFE_FileUploadItem,
default as FileUploadItem,
type FileUploadItemProps,
} from "./parts/item/Item";
export { type FileItem, type FileMetadata } from "./parts/item/Item.types";
@@ -22,14 +22,17 @@ export interface FormSummaryEditProps
export const FormSummaryEditLink: OverridableComponent<
FormSummaryEditProps,
HTMLAnchorElement
> = forwardRef(({ children = "Endre svar", className, ...rest }, ref) => (
<Link
ref={ref}
{...rest}
className={cl("navds-form-summary__edit", className)}
>
{children}
</Link>
));
> = forwardRef(
({ children = "Endre svar", className, as = "a", ...rest }, ref) => (
<Link
ref={ref}
as={as}
{...rest}
className={cl("navds-form-summary__edit", className)}
>
{children}
</Link>
),
);
export default FormSummaryEditLink;
+1 -1
View File
@@ -127,7 +127,7 @@ export {
export { ErrorSummary, type ErrorSummaryProps } from "./form/error-summary";
export { Fieldset, type FieldsetProps } from "./form/fieldset";
export {
UNSAFE_FileUpload,
FileUpload,
type FileAccepted,
type FileItem,
type FileMetadata,
@@ -63,7 +63,7 @@ export const PageBlock: OverridableComponent<PageBlockProps, HTMLDivElement> =
{...rest}
className={cl(
"navds-pageblock",
`navds-pageblock--${width}`,
width && `navds-pageblock--${width}`,
className,
{ "navds-pageblock--gutters": gutters },
)}
@@ -26,8 +26,8 @@ export type HStackProps = PrimitiveAsChildProps & Omit<StackProps, "direction">;
* </HStack>
*/
export const HStack: OverridableComponent<HStackProps, HTMLDivElement> =
forwardRef((props, ref) => {
return <Stack {...props} ref={ref} direction="row" />;
forwardRef(({ as = "div", ...rest }, ref) => {
return <Stack as={as} {...rest} ref={ref} direction="row" />;
});
export default HStack;
@@ -27,8 +27,10 @@ export type VStackProps = PrimitiveAsChildProps &
* </VStack>
*/
export const VStack: OverridableComponent<VStackProps, HTMLDivElement> =
forwardRef((props, ref) => {
return <Stack {...props} ref={ref} direction="column" wrap={false} />;
forwardRef(({ as = "div", ...rest }, ref) => {
return (
<Stack as={as} {...rest} ref={ref} direction="column" wrap={false} />
);
});
export default VStack;
+25 -29
View File
@@ -1,15 +1,27 @@
import cl from "clsx";
import React, { forwardRef, useContext } from "react";
import { BodyShort, Heading } from "../typography";
import { useId } from "../util/hooks";
import { BodyLong, BodyShort, Heading, HeadingProps } from "../typography";
import { ListItem } from "./ListItem";
import { ListContext } from "./context";
import { ListProps } from "./types";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { ListItemProps, ListProps } from "./types";
const headingSizeMap: Record<
Exclude<ListProps["size"], undefined>,
HeadingProps["size"]
> = {
small: "xsmall",
medium: "small",
large: "medium",
};
export interface ListComponent
extends React.ForwardRefExoticComponent<
ListProps & React.RefAttributes<HTMLDivElement>
> {
/**
* @see 🏷️ {@link ListItemProps}
*/
Item: typeof ListItem;
}
@@ -42,47 +54,31 @@ export const List = forwardRef<HTMLDivElement, ListProps>(
},
ref,
) => {
const ariaId = useId();
const { size: contextSize } = useContext(ListContext);
const { isNested, size: _size } = useContext(ListContext);
const listSize = size ?? _size ?? "medium";
const listSize = size ?? contextSize;
return (
<ListContext.Provider
value={{
listType: ListTag,
isNested: isNested === null ? false : true,
size: listSize,
}}
>
<div
<BodyLong
as="div"
{...rest}
size={listSize}
ref={ref}
className={cl("navds-list", `navds-list--${listSize}`, className, {
"navds-list--nested": isNested === null ? false : true,
})}
className={cl("navds-list", `navds-list--${listSize}`, className)}
>
{title && (
<Heading
id={`tittel-${ariaId}`}
size={listSize === "medium" ? "small" : "xsmall"}
as={headingTag}
>
<Heading size={headingSizeMap[listSize]} as={headingTag}>
{title}
</Heading>
)}
{description && (
<BodyShort size={listSize} id={`description-${ariaId}`}>
{description}
</BodyShort>
)}
<ListTag
aria-labelledby={title && `tittel-${ariaId}`}
aria-describedby={description && `description-${ariaId}`}
>
{children}
</ListTag>
</div>
{description && <BodyShort size={listSize}>{description}</BodyShort>}
<ListTag role="list">{children}</ListTag>
</BodyLong>
</ListContext.Provider>
);
},
+6 -6
View File
@@ -2,7 +2,7 @@ import cl from "clsx";
import React, { forwardRef, useContext } from "react";
import { BodyLong } from "../typography";
import { ListContext } from "./context";
import { ListItemProps } from "./types";
import type { ListItemProps } from "./types";
/**
* @see 🏷️ {@link ListItemProps}
@@ -21,7 +21,7 @@ export const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
<li {...rest} ref={ref} className={cl("navds-list__item", className)}>
{listType === "ul" && (
<div
className={cl({
className={cl("navds-list__item-marker", {
"navds-list__item-marker--icon": icon,
"navds-list__item-marker--bullet": !icon,
})}
@@ -30,8 +30,8 @@ export const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
icon
) : (
<svg
width="6"
height="6"
width="0.375rem"
height="0.375rem"
viewBox="0 0 6 6"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@@ -45,14 +45,14 @@ export const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
</div>
)}
<BodyLong as="div" size={size} className="navds-list__item-content">
<div>
{title && (
<BodyLong as="p" size={size} weight="semibold">
{title}
</BodyLong>
)}
{children}
</BodyLong>
</div>
</li>
);
},
+2 -4
View File
@@ -2,13 +2,11 @@ import { createContext } from "react";
import { ListProps } from "./types";
interface ListContextProps {
listType: ListProps["as"];
isNested: boolean | null;
size: ListProps["size"];
listType: Exclude<ListProps["as"], undefined>;
size: Exclude<ListProps["size"], undefined>;
}
export const ListContext = createContext<ListContextProps>({
listType: "ul",
isNested: null,
size: "medium",
});
+203 -95
View File
@@ -7,6 +7,7 @@ import {
HeadHeartIcon,
SplitHorizontalIcon,
} from "@navikt/aksel-icons";
import { Box } from "../layout/box";
import { VStack } from "../layout/stack";
import { BodyLong } from "../typography";
import List from "./List";
@@ -21,73 +22,97 @@ export default {
type Story = StoryObj<typeof List>;
export const Default = {
export const Default: Story = {
render: (props) => {
return (
<Box borderWidth="0 0 0 1" borderColor="border-subtle" maxWidth="500px">
<List {...props}>
<List.Item title="Lorem Ipsum Dolor Sit Amet">Beskrivelse.</List.Item>
<List.Item title="Consectetur Adipiscing Elit">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
<List.Item>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Iusto
fugiat atque accusantium iure sunt, ipsum voluptas, impedit harum,
minus rerum recusandae.
</List.Item>
<List.Item>
Impedit nemo eos sit adipisci non dolores.
<List>
<List.Item title="Tempor Incididunt">
Error assumenda officia
</List.Item>
<List.Item>Suscipit odit voluptatum</List.Item>
</List>
</List.Item>
<List.Item>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Iusto
fugiat atque accusantium iure sunt, ipsum voluptas.
<List as="ol">
<List.Item title="Tempor Incididunt">
Error assumenda officia
</List.Item>
<List.Item>Suscipit odit voluptatum</List.Item>
</List>
</List.Item>
</List>
</Box>
);
},
args: {
title: "",
description: "",
},
argTypes: {
size: { control: { type: "radio" }, options: ["small", "medium", "large"] },
as: { control: { type: "radio" }, options: ["ul", "ol"] },
},
};
export const Ordered: Story = {
render: () => {
return (
<List>
<List.Item title="Lorem Ipsum Dolor Sit Amet">
Lorem Ipsum Dolor Sit Amet
</List.Item>
<List.Item title="Consectetur Adipiscing Elit">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
<List.Item>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Iusto fugiat
atque accusantium iure sunt, ipsum voluptas, impedit harum, minus
rerum recusandae. Consequuntur sint distinctio nulla reprehenderit eum
suscipit quae libero.
</List.Item>
<List.Item>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Iusto fugiat
atque accusantium iure sunt, ipsum voluptas, impedit harum, minus
rerum recusandae. Consequuntur sint distinctio nulla reprehenderit eum
suscipit quae libero.
</List.Item>
</List>
<Box borderWidth="0 0 0 1" borderColor="border-subtle" maxWidth="500px">
<List as="ol">
<List.Item title="Lorem Ipsum Dolor Sit Amet">Beskrivelse.</List.Item>
<List.Item title="Consectetur Adipiscing Elit">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
<List.Item title="Quis Nostrud Exercitation Ullamco Eu Fugiat Nulla Pariatur Dolore">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis. Men
hvis den likevel blir lang, skal teksten brekke.
</List.Item>
<List.Item>Sed Do Eiusmod Tempor Incididunt</List.Item>
<List.Item>Ut Labore Et Dolore Magna Aliqua</List.Item>
<List.Item>Enim Ad Minim Veniam</List.Item>
<List.Item>Laboris Nisi Ut Aliquip Ex Ea Commodo</List.Item>
<List.Item>Duis Aute Irure Dolor In Reprehenderit</List.Item>
<List.Item>
Impedit nemo eos sit adipisci non dolores.
<List as="ol">
<List.Item title="Tempor Incididunt">
Error assumenda officia
</List.Item>
<List.Item>Suscipit odit voluptatum</List.Item>
</List>
</List.Item>
<List.Item title="Voluptate Velit Esse Cillum Dolore">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
<List>
<List.Item title="Tempor Incididunt">
Error assumenda officia
</List.Item>
<List.Item>Suscipit odit voluptatum</List.Item>
</List>
</List.Item>
<List.Item>Eu Fugiat Nulla Pariatur</List.Item>
</List>
</Box>
);
},
};
export const Ordered = {
render: () => {
return (
<List as="ol">
<List.Item title="Lorem Ipsum Dolor Sit Amet">
Lorem Ipsum Dolor Sit Amet
</List.Item>
<List.Item title="Consectetur Adipiscing Elit">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
<List.Item title="Sed Do Eiusmod Tempor Incididunt">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
<List.Item title="Ut Labore Et Dolore Magna Aliqua">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
<List.Item title="Enim Ad Minim Veniam">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
<List.Item title="Quis Nostrud Exercitation Ullamco">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
<List.Item title="Laboris Nisi Ut Aliquip Ex Ea Commodo">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
<List.Item title="Duis Aute Irure Dolor In Reprehenderit">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
<List.Item title="Voluptate Velit Esse Cillum Dolore">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
<List.Item title="Eu Fugiat Nulla Pariatur">
Beskrivelse på punktet i lista. Prøv å hold den kort og konsis.
</List.Item>
</List>
);
},
};
export const WithHeading = {
export const WithHeading: Story = {
render: () => {
return (
<List
@@ -108,10 +133,25 @@ export const WithHeading = {
},
};
export const SizesUl = {
export const SizesUl: Story = {
render: () => {
return (
<VStack gap="8">
<List
title="Large list"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel risus volutpat."
size="large"
>
<List.Item title="Lorem Ipsum Dolor Sit Amet">
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
<List.Item title="Consectetur Adipiscing Elit">
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
<List.Item>
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
</List>
<List
title="Medium list"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel risus volutpat."
@@ -147,10 +187,26 @@ export const SizesUl = {
},
};
export const SizesOl = {
export const SizesOl: Story = {
render: () => {
return (
<VStack gap="8">
<List
title="Large list"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel risus volutpat."
size="large"
as="ol"
>
<List.Item title="Lorem Ipsum Dolor Sit Amet">
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
<List.Item title="Consectetur Adipiscing Elit">
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
<List.Item>
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
</List>
<List
title="Medium list"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel risus volutpat."
@@ -188,10 +244,31 @@ export const SizesOl = {
},
};
export const SizesIcons = {
export const SizesIcons: Story = {
render: () => {
return (
<VStack gap="8">
<List
title="Large list"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel risus volutpat."
size="large"
>
<List.Item
icon={<HeadHeartIcon aria-hidden />}
title="Lorem Ipsum Dolor Sit Amet"
>
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
<List.Item
icon={<HeadHeartIcon aria-hidden />}
title="Consectetur Adipiscing Elit"
>
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
<List.Item icon={<HeadHeartIcon aria-hidden />}>
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
</List>
<List
title="Medium list"
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel risus volutpat."
@@ -239,7 +316,7 @@ export const SizesIcons = {
},
};
export const Icons = {
export const Icons: Story = {
render: () => {
return (
<List title="Best title">
@@ -266,39 +343,70 @@ export const Icons = {
},
};
export const Nested = {
export const Tokens: Story = {
render: () => {
return (
<List>
<List.Item title="Sed Do Eiusmod Tempor Incididunt">
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
<List>
<List.Item title="Lorem Ipsum Dolor Sit Amet">
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
<List.Item title="Consectetur Adipiscing Elit">
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
<List.Item>
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
</List.Item>
<List.Item>
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
<List style={{ "--ac-list-marker-color": "red" }}>
<List.Item>Consectetur Adipiscing Elit</List.Item>
<List.Item>
Impedit nemo eos sit adipisci non dolores.
<List
style={{
"--ac-list-marker-icon-color": "blue",
"--ac-list-marker-ul-color": "pink",
}}
>
<List.Item>Error assumenda officia</List.Item>
<List.Item icon={<HeadHeartIcon aria-hidden />}>
Suscipit odit voluptatum
</List.Item>
</List>
</List.Item>
<List.Item>
Beskrivelsen på punkter er nærmere forklart <a href="/">her</a>
<List.Item style={{ "--ac-list-marker-ol-color": "green" }}>
Iusto fugiat atque accusantium iure sunt.
<List as="ol">
<List.Item>Error assumenda officia</List.Item>
</List>
<List>
<List.Item>Suscipit odit voluptatum</List.Item>
</List>
</List.Item>
</List>
);
},
};
export const Spacing = {
export const Spacing: Story = {
render: () => {
return (
<>
<h2>Large</h2>
<BodyLong size="large" spacing>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sit minus
aliquid, veniam quibusdam saepe autem quia odio? Tenetur dicta
voluptates iste maiores perspiciatis? Sapiente in possimus iusto
numquam? Esse, voluptatibus.
</BodyLong>
<List size="large">
<List.Item>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Iusto
fugiat atque accusantium iure sunt, ipsum voluptas, impedit harum,
minus rerum recusandae. Consequuntur sint distinctio nulla
reprehenderit eum suscipit quae libero.
</List.Item>
<List.Item>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Iusto
fugiat atque accusantium iure sunt, ipsum voluptas, impedit harum,
minus rerum recusandae. Consequuntur sint distinctio nulla
reprehenderit eum suscipit quae libero.
</List.Item>
</List>
<BodyLong size="large" spacing>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sit minus
aliquid, veniam quibusdam saepe autem quia odio? Tenetur dicta
voluptates iste maiores perspiciatis? Sapiente in possimus iusto
numquam? Esse, voluptatibus.
</BodyLong>
<h2>Medium</h2>
<BodyLong spacing>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sit minus
@@ -359,43 +467,43 @@ export const Spacing = {
};
export const Chromatic: Story = {
render: () => (
render: (...args) => (
<VStack gap="2">
<div>
<h2>Default</h2>
<Default.render />
{Default.render?.(...args)}
</div>
<div>
<h2>Ordered</h2>
<Ordered.render />
{Ordered.render?.(...args)}
</div>
<div>
<h2>WithHeading</h2>
<WithHeading.render />
{WithHeading.render?.(...args)}
</div>
<div>
<h2>SizesUl</h2>
<SizesUl.render />
{SizesUl.render?.(...args)}
</div>
<div>
<h2>SizesOl</h2>
<SizesOl.render />
{SizesOl.render?.(...args)}
</div>
<div>
<h2>SizesIcons</h2>
<SizesIcons.render />
{SizesIcons.render?.(...args)}
</div>
<div>
<h2>Icons</h2>
<Icons.render />
{Icons.render?.(...args)}
</div>
<div>
<h2>Nested</h2>
<Nested.render />
<h2>Tokens</h2>
{Tokens.render?.(...args)}
</div>
<div>
<h2>Spacing</h2>
<Spacing.render />
{Spacing.render?.(...args)}
</div>
</VStack>
),
+2 -2
View File
@@ -19,10 +19,10 @@ export interface ListProps extends React.HTMLAttributes<HTMLDivElement> {
*/
headingTag?: React.ElementType<any>;
/**
* Changes padding, height and font-size.
* Changes margin-block on list and font size on items.
* @default "medium"
*/
size?: "medium" | "small";
size?: "small" | "medium" | "large";
}
export interface ListItemProps extends React.HTMLAttributes<HTMLLIElement> {
+2 -2
View File
@@ -127,7 +127,7 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
// We have to use JS because it doesn't work to set it with a prop (React bug?)
// Currently doesn't seem to work in Chrome. See also Tooltip.tsx
if (modalRef.current && portalNode) modalRef.current.autofocus = true;
}, [modalRef, portalNode]);
}, [portalNode]);
useEffect(() => {
// We need to have this in a useEffect so that the content renders before the modal is displayed,
@@ -140,7 +140,7 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
modalRef.current.close();
}
}
}, [modalRef, portalNode, open]);
}, [portalNode, open]);
useBodyScrollLock(modalRef, portalNode, isNested);
@@ -337,6 +337,8 @@ const DismissableLayerNode = forwardRef<HTMLDivElement, DismissableLayerProps>(
* If `disableOutsidePointerEvents` is true,
* we want to disable pointer events on the body when the first layer is opened.
*/
// biome-ignore lint/correctness/useExhaustiveDependencies: Every time the descendants change, we want to update the body pointer events since we might have added or removed a layer.
useEffect(() => {
if (!node || !enabled || !disableOutsidePointerEvents) return;
@@ -362,6 +364,7 @@ const DismissableLayerNode = forwardRef<HTMLDivElement, DismissableLayerProps>(
/**
* To make sure pointerEvents are enabled for all parents and siblings when the layer is removed from the DOM
*/
// biome-ignore lint/correctness/useExhaustiveDependencies: We explicitly want to run this on unmount, including every time the node updates to make sure we don't lock the application behind pointer-events: none.
useEffect(() => {
return () => descendants.values().forEach((x) => x.forceUpdate());
}, [descendants, node]);
@@ -189,21 +189,13 @@ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
size={size}
icon={
<ChevronLeftIcon
className="navds-pagination__prev-next-icon"
{...(prevNextTexts
? { "aria-hidden": true }
: { title: "Forrige" })}
/>
}
>
{prevNextTexts && (
<BodyShort
size={size === "xsmall" ? "small" : size}
className="navds-pagination__prev-text"
>
Forrige
</BodyShort>
)}
{prevNextTexts && `Forrige`}
</Item>
</li>
{getSteps({ page, count, siblingCount, boundaryCount }).map(
@@ -211,7 +203,10 @@ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
const n = Number(step);
return Number.isNaN(n) ? (
<li className="navds-pagination__ellipsis" key={`${step}${i}`}>
<BodyShort size={size === "xsmall" ? "small" : size}>
<BodyShort
size={size === "xsmall" ? "small" : size}
as="span"
>
...
</BodyShort>
</li>
@@ -223,9 +218,7 @@ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
page={n}
size={size}
>
<BodyShort size={size === "xsmall" ? "small" : size}>
{n}
</BodyShort>
{n}
</Item>
</li>
);
@@ -243,7 +236,6 @@ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
size={size}
icon={
<ChevronRightIcon
className="navds-pagination__prev-next-icon"
{...(prevNextTexts
? { "aria-hidden": true }
: { title: "Neste" })}
@@ -251,14 +243,7 @@ export const Pagination = forwardRef<HTMLElement, PaginationProps>(
}
iconPosition="right"
>
{prevNextTexts && (
<BodyShort
size={size === "xsmall" ? "small" : size}
className="navds-pagination__next-text"
>
Neste
</BodyShort>
)}
{prevNextTexts && `Neste`}
</Item>
</li>
</ul>
@@ -11,9 +11,10 @@ export interface PaginationItemProps extends ButtonProps {
*/
selected?: boolean;
/**
* The page the item represents
* Currently only sets `data-page` attribute.
* @deprecated Use `data-page` instead if you need to access the items page number in the future.
*/
page: number;
page?: number;
/**
* Changes padding, height and font-size
* @default "medium"
@@ -48,7 +49,7 @@ export const Item: PaginationItemType = forwardRef(
"navds-pagination__item--selected": selected,
})}
data-page={page}
/* TODO: Breaking change to remove. Add to future major version. */
/* TODO: Breaking change to remove since it's in use by some applications. Add to future major version. */
page={page}
{...(Component === "button" && { type: "button" })}
{...rest}
@@ -107,6 +107,7 @@ export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
}, [simulated?.seconds]);
return (
/* biome-ignore lint/a11y/useFocusableInteractive: Progressbar is not interactive. */
<div
ref={ref}
className={cl(
@@ -121,6 +122,7 @@ export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
? `Fremdrift kan ikke beregnes, antatt tid er: ${simulated?.seconds} sekunder`
: `${Math.round(value)} av ${Math.round(valueMax)}`
}
// biome-ignore lint/a11y/useAriaPropsForRole: We found that adding valueMin was not needed
role="progressbar"
aria-labelledby={ariaLabelledBy}
aria-label={ariaLabel}
+15 -16
View File
@@ -74,37 +74,36 @@ const AnimateHeight: React.FC<AnimateHeightProps> = ({
const animationClassesTimeoutID = useRef<Timeout>();
const timeoutID = useRef<Timeout>();
const initialHeight = useRef<Height>(height);
const initialOverflow = useRef<Overflow>("visible");
const duration = prefersReducedMotion ? 0 : userDuration;
let initHeight: Height = height;
let initOverflow: Overflow = "visible";
if (typeof initHeight === "number") {
if (typeof initialHeight.current === "number") {
// Reset negative height to 0
if (typeof height !== "string") {
initHeight = height < 0 ? 0 : height;
initialHeight.current = height < 0 ? 0 : height;
}
initOverflow = "hidden";
} else if (isPercentage(initHeight)) {
initialOverflow.current = "hidden";
} else if (isPercentage(initialHeight.current)) {
// If value is string "0%" make sure we convert it to number 0
initHeight = height === "0%" ? 0 : height;
initOverflow = "hidden";
initialHeight.current = height === "0%" ? 0 : height;
initialOverflow.current = "hidden";
}
const [currentHeight, setCurrentHeight] = useState<Height>(initHeight);
const [overflow, setOverflow] = useState<Overflow>(initOverflow);
const [currentHeight, setCurrentHeight] = useState<Height>(
initialHeight.current,
);
const [overflow, setOverflow] = useState<Overflow>(initialOverflow.current);
const [useTransitions, setUseTransitions] = useState<boolean>(false);
// ------------------ Did mount
useEffect(() => {
// Hide content if height is 0 (to prevent tabbing into it)
hideContent(contentElement.current, currentHeight);
// This should be explicitly run only on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
hideContent(contentElement.current, initialHeight.current);
}, []);
// ------------------ Height update
// biome-ignore lint/correctness/useExhaustiveDependencies: This should be explicitly run only on height change
useEffect(() => {
if (height !== prevHeight.current && contentElement.current) {
showContent(contentElement.current, prevHeight.current);
@@ -98,61 +98,6 @@ export const SizeSmall = () => <TableComponent size="small" />;
export const Buttons = () => <TableComponent size="small" button />;
export const WithDivs = () => {
return (
<div className="navds-table" role="table">
<div className="navds-table__header" role="rowgroup">
<div className="navds-table__row" role="row">
<div className="navds-table__header-cell" role="columnheader">
Fornavn
</div>
<div className="navds-table__header-cell" role="columnheader">
Etternavn
</div>
<div className="navds-table__header-cell" role="columnheader">
Rolle
</div>
</div>
</div>
<div className="navds-table__body" role="rowgroup">
<div className="navds-table__row" role="row">
<div className="navds-table__data-cell" role="cell">
Jean-Luc
</div>
<div className="navds-table__data-cell" role="cell">
Picard
</div>
<div className="navds-table__data-cell" role="cell">
Kaptein
</div>
</div>
<div className="navds-table__row" role="row">
<div className="navds-table__data-cell" role="cell">
William
</div>
<div className="navds-table__data-cell" role="cell">
Riker
</div>
<div className="navds-table__data-cell" role="cell">
Kommandør
</div>
</div>
<div className="navds-table__row" role="row">
<div className="navds-table__data-cell" role="cell">
Geordi
</div>
<div className="navds-table__data-cell" role="cell">
La Forge
</div>
<div className="navds-table__data-cell" role="cell">
Sjefsingeniør
</div>
</div>
</div>
</div>
);
};
const SelectionTable = ({ size = "medium" }: { size?: "small" | "medium" }) => {
const useToggleList = (initialState) => {
const [list, setList] = useState(initialState);
@@ -259,10 +204,6 @@ export const Chromatic = {
<h3>With Buttons</h3>
<Buttons />
</div>
<div>
<h3>Custom with divs</h3>
<WithDivs />
</div>
<div>
<h3>Selection</h3>
<Selection />
@@ -205,6 +205,7 @@ const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
syncHeight();
});
// biome-ignore lint/correctness/useExhaustiveDependencies: Since value is an external prop, we want to reset the renders on every time it changes, even when it is undefined or empty.
useEffect(() => {
renders.current = 0;
}, [value]);
@@ -40,11 +40,12 @@ export function createContext<T>(options: CreateContextOptions<T> = {}) {
* We use forwardRef to allow `ref` to be used as a regular context value
* @see https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-to-dom-components
*/
const Provider = forwardRef(
({ children, ...context }: ProviderProps<T>, ref) => {
const Provider = forwardRef<unknown, ProviderProps<T>>(
({ children, ...context }, ref) => {
// Only re-memoize when prop values change
// eslint-disable-next-line react-hooks/exhaustive-deps
const value = React.useMemo(() => context, Object.values(context)) as T;
// biome-ignore lint/correctness/useExhaustiveDependencies: Object.values(context) includes all dependencies.
const value = React.useMemo(() => context, Object.values(context)) as T; // eslint-disable-line react-hooks/exhaustive-deps
return (
<Context.Provider value={ref ? { ...value, ref } : value}>
@@ -1,5 +1,4 @@
import * as React from "react";
import { Box } from "../../../layout/box";
import { HStack } from "../../../layout/stack";
import { createDescendantContext } from "./useDescendant";
@@ -16,12 +15,16 @@ const [
_useDescendantsContext,
useDescendants,
useDescendant,
] = createDescendantContext<HTMLDivElement, { value?: string }>();
] = createDescendantContext<
HTMLButtonElement | HTMLInputElement,
{ value?: string }
>();
function Select({ children }: { children?: React.ReactNode }) {
const descendants = useDescendants();
const count = descendants.count();
// biome-ignore lint/correctness/useExhaustiveDependencies: We want count to be a trigger for focusing the last descendant here.
React.useEffect(() => {
descendants.last()?.node.focus();
}, [descendants, count]);
@@ -37,9 +40,8 @@ function Option({ value, disabled }: { value?: string; disabled?: boolean }) {
});
return (
<Box
<button
ref={register}
role="button"
tabIndex={0}
data-value={value}
onKeyDown={(event) => {
@@ -51,7 +53,7 @@ function Option({ value, disabled }: { value?: string; disabled?: boolean }) {
}}
>
Option {index + 1}
</Box>
</button>
);
}
+8
View File
@@ -1,5 +1,13 @@
# @navikt/ds-tailwind
## 7.2.0
## 7.1.2
## 7.1.1
## 7.1.0
## 7.0.1
## 7.0.0
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "7.0.1",
"version": "7.2.0",
"description": "Tailwind config based on @navikt/ds-tokens",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -15,7 +15,7 @@
"tailwind.config.d.ts"
],
"scripts": {
"build": "ts-node ./src",
"build": "yarn tsx ./src",
"test": "vitest run",
"test:watch": "vitest watch"
},
@@ -25,11 +25,11 @@
"directory": "@navikt/core/tailwind"
},
"devDependencies": {
"@navikt/ds-tokens": "^7.0.1",
"@navikt/ds-tokens": "^7.2.0",
"color": "4.2.3",
"lodash": "^4.17.21",
"tailwindcss": "^3.3.3",
"ts-node": "^10.9.1",
"tsx": "^4.19.1",
"typescript": "^5.1.6",
"vitest": "^1.2.2"
},
+2
View File
@@ -0,0 +1,2 @@
figma-config.json
plugin.js
+8
View File
@@ -1,5 +1,13 @@
# @navikt/ds-tokens
## 7.2.0
## 7.1.2
## 7.1.1
## 7.1.0
## 7.0.1
## 7.0.0
@@ -1,29 +1,11 @@
import { getGlobalScaleForColor } from "./leonardo";
import { contrastTokenConfig } from "./tokens/contrast";
import { neutralTokenConfig } from "./tokens/neutral";
import { globalColorDarkModeConfig } from "./tokens/global-dark";
import { globalColorLightModeConfig } from "./tokens/global-light";
import { radiusTokenConfig } from "./tokens/radius";
import { semanticTokenConfig } from "./tokens/semantic";
import { semanticTokensForAllRolesConfig } from "./tokens/semantic-roles";
import { spacingTokenConfig } from "./tokens/spacing";
import {
ColorTheme,
GlobalColorVariable,
globalColorRoles,
mergeConfigs,
tokensWithPrefix,
} from "./util";
export const globalScale = (
theme: ColorTheme,
): Record<string, GlobalColorVariable> => {
return globalColorRoles.reduce(
(acc, role) => {
acc[role] = getGlobalScaleForColor(role, theme);
return acc;
},
{} as Record<string, GlobalColorVariable>,
);
};
import { textContrastTokenConfig } from "./tokens/text-contrast";
import { mergeConfigs, tokensWithPrefix } from "./util";
/**
* Collection of configs for:
@@ -35,10 +17,9 @@ export const lightModeTokens = () => {
return tokensWithPrefix(
mergeConfigs([
semanticTokensForAllRolesConfig(),
contrastTokenConfig(),
semanticTokenConfig(),
neutralTokenConfig("light"),
globalScale("light"),
textContrastTokenConfig(),
semanticTokenConfig("light"),
globalColorLightModeConfig,
]),
);
};
@@ -49,7 +30,12 @@ export const lightModeTokens = () => {
*/
export const darkModeTokens = () => {
return tokensWithPrefix(
mergeConfigs([globalScale("dark"), neutralTokenConfig("dark")]),
mergeConfigs([
semanticTokensForAllRolesConfig(),
textContrastTokenConfig(),
semanticTokenConfig("dark"),
globalColorDarkModeConfig,
]),
);
};
@@ -0,0 +1,241 @@
import lodash from "lodash";
import StyleDictionary from "style-dictionary";
import { Dictionary, TransformedToken } from "style-dictionary/types";
import { createPropertyFormatter, getReferences } from "style-dictionary/utils";
import {
darkModeTokens,
lightModeTokens,
scaleTokens,
} from "../create-configuration";
import {
type SemanticTokenGroups,
type StyleDictionaryToken,
type TokenTypes,
} from "../util";
import type { FigmaToken } from "./figma-config.types";
const config = {
light: lightModeTokens(),
dark: darkModeTokens(),
scale: scaleTokens(),
};
const createStyleDictionaryForCollection = (
collection: keyof typeof config,
) => {
return new StyleDictionary({
tokens: config[collection],
platforms: {
[collection]: {
transformGroup: "css",
files: [],
},
},
});
};
export const getTokensForCollection = async (
collection: keyof typeof config,
) => {
const SD = createStyleDictionaryForCollection(collection);
await SD.hasInitialized;
const dictionary = await SD.getPlatformTokens(collection);
return dictionary.allTokens.map((token) => {
const reference = getReferences(token.original, dictionary.tokens);
/*
* Currently only supports 1 level of references.
*/
return reference.length > 0
? prepareToken(
{
...(token as TransformedTokenWithScopes),
alias: createTokenName(reference[0]),
},
dictionary,
)
: prepareToken(token as TransformedTokenWithScopes, dictionary);
});
};
type TransformedTokenWithScopes = TransformedToken & {
scopes: StyleDictionaryToken<TokenTypes>["scopes"];
};
function prepareToken(
token: TransformedTokenWithScopes,
dictionary: Dictionary,
): FigmaToken {
const formatter = createPropertyFormatter({
dictionary,
format: "css",
});
const cssVariable = formatter(token);
return {
name: createTokenName(token),
/* We can assume this since each config is typed with 'StyleDictionaryToken' */
type: token.type as TokenTypes,
value: figmaValue(token),
alias: token.alias,
comment: token.comment,
group: token.group,
code: {
WEB: `var(${cssVariable.trim().split(": ")[0]})`,
},
...figmaSettings(token),
};
}
/**
* @see https://www.figma.com/plugin-docs/api/VariableValue
*
* We do not need to handle the 'color' type since we can use Figma built-in for this
* @see https://www.figma.com/plugin-docs/api/properties/figma-util-rgba
*/
export function figmaValue(token: TransformedToken): string | number {
if (isRadiusToken(token) || isSpacingToken(token)) {
const float = parseFloat(token.value.replace("px", "").replace("rem", ""));
/*
* Figma does not support relative units, so we need to convert rem to px.
*/
if (token.value.includes("rem")) {
return float * 16;
}
return float;
}
return token.value;
}
/**
* Scopes allows us to define where in Figma the token can be used.
* @see https://www.figma.com/plugin-docs/api/VariableScope
*/
function figmaSettings(token: TransformedTokenWithScopes): {
figmaType: VariableResolvedDataType;
scopes: VariableScope[];
} {
let setting: ReturnType<typeof figmaSettings> | undefined;
if (isGlobalColor(token)) {
setting = createFigmaSettings("COLOR", ["ALL_FILLS", "STROKE_COLOR"]);
} else if (isTokenOfSemanticColorGroup(token, "background")) {
setting = createFigmaSettings("COLOR", ["FRAME_FILL", "SHAPE_FILL"]);
} else if (isTokenOfSemanticColorGroup(token, "border")) {
setting = createFigmaSettings("COLOR", ["STROKE_COLOR"]);
} else if (isTokenOfSemanticColorGroup(token, "text")) {
setting = createFigmaSettings("COLOR", ["SHAPE_FILL", "TEXT_FILL"]);
} else if (isRadiusToken(token)) {
setting = createFigmaSettings("FLOAT", ["CORNER_RADIUS"]);
} else if (isSpacingToken(token)) {
setting = createFigmaSettings("FLOAT", ["GAP"]);
}
if (!setting) {
console.warn(`No fitting type or scope found for token: ${token.name}`);
return createFigmaSettings("STRING", []);
}
token.scopes && setting.scopes.push(...token.scopes);
return setting;
}
function createFigmaSettings(
figmaType: VariableResolvedDataType,
scopes: VariableScope[],
): {
figmaType: VariableResolvedDataType;
scopes: VariableScope[];
} {
return { figmaType, scopes };
}
/**
* Allows us to extract the token name from the token object and create the correct grouping for Figma.
*/
export function createTokenName(token: TransformedToken) {
if (!token.attributes?.item || typeof token.attributes.item !== "string") {
throw new Error(`No item attribute found on token: ${token.name}`);
}
/*
* Because of the `StyleDictionaryTokenConfig`-type, we can assume that attributes.item will always exists.
*/
let name = lodash.startCase(token.attributes.item);
if (token.group) {
name = createGroupName(token.group) + name;
}
/*
* In the case we have a token "Text/Accent/Accent", we want to add a suffix to the name.
*/
const nameKeys = new Set(name.split("/"));
if (nameKeys.size !== name.split("/").length) {
name += " Default";
}
/*
* For pure value tokens, we want to add a prefix to the name to make it more readable in Figma.
*/
if (isRadiusToken(token)) {
return "Radius " + name;
}
if (isSpacingToken(token)) {
return "Spacing " + name;
}
return name;
}
/**
* By splitting the group by ".", we can create a more readable group name in Figma.
* @param group "background.primary"
* @returns "Background/Primary/"
* @see https://help.figma.com/hc/en-us/articles/14506821864087-Overview-of-variables-collections-and-modes#h_01H9V3QSVH2T1EYNXP7RNXZ8MV
*/
function createGroupName(group: string): string {
return (
group
.split(".")
.map((g) => lodash.startCase(g))
.join("/") + "/"
);
}
function isTokenOfSemanticColorGroup(
token: TransformedToken | FigmaToken,
groupType: SemanticTokenGroups,
): boolean {
const group = token.group as StyleDictionaryToken<TokenTypes>["group"];
if (!isSemanticColor(token)) {
return false;
}
return group?.includes(groupType) ?? false;
}
export function isGlobalColor(token: TransformedToken | FigmaToken): boolean {
const type = token.type as TokenTypes;
return type === "global-color";
}
export function isSemanticColor(token: TransformedToken | FigmaToken): boolean {
const type = token.type as TokenTypes;
return type === "color";
}
export function isRadiusToken(token: TransformedToken | FigmaToken): boolean {
const type = token.type as TokenTypes;
return type === "global-radius";
}
export function isSpacingToken(token: TransformedToken | FigmaToken): boolean {
const type = token.type as TokenTypes;
return type === "global-spacing";
}
@@ -0,0 +1,30 @@
import { StyleDictionaryToken, TokenTypes } from "../util";
export type FigmaToken = Omit<StyleDictionaryToken<TokenTypes>, "value"> & {
name: string;
alias?: string;
code: Variable["codeSyntax"];
value: string | number;
figmaType: VariableResolvedDataType;
scopes: VariableScope[];
};
export type FigmaConfigEntry = {
name: string;
hideFromPublishing: boolean;
tokens: FigmaToken[];
};
export type FigmaTokenConfig = {
version: string;
timestamp: string;
colors: { light: ColorThemeEntry; dark: ColorThemeEntry };
radius: FigmaConfigEntry;
spacing: FigmaConfigEntry;
};
type ColorThemeEntry = {
name: "light" | "dark";
global: FigmaConfigEntry;
semantic: FigmaConfigEntry;
};
@@ -0,0 +1,77 @@
import fs from "fs";
import { version } from "../../package.json";
import {
getTokensForCollection,
isGlobalColor,
isRadiusToken,
isSemanticColor,
isSpacingToken,
} from "./create-tokens";
import { FigmaTokenConfig } from "./figma-config.types";
buildFigmaConfig();
/**
* Builds a configuration JSON that can be used by Figma-plugins to
* generate 'variables' in a Figma-file based on our tokens.
*/
async function buildFigmaConfig() {
try {
const lightTokens = await getTokensForCollection("light");
const darkTokens = await getTokensForCollection("dark");
const scaleTokens = await getTokensForCollection("scale");
const preparedConfig: FigmaTokenConfig = {
version,
timestamp: new Date().toLocaleString("no-NO", {
timeZone: "Europe/Oslo",
}),
colors: {
light: {
name: "light",
global: {
name: "Global colors light",
hideFromPublishing: true,
tokens: lightTokens.filter(isGlobalColor),
},
semantic: {
name: "Semantic colors",
hideFromPublishing: false,
tokens: lightTokens.filter(isSemanticColor),
},
},
dark: {
name: "dark",
global: {
name: "Global colors dark",
hideFromPublishing: true,
tokens: darkTokens.filter(isGlobalColor),
},
semantic: {
name: "Semantic colors",
hideFromPublishing: false,
tokens: darkTokens.filter(isSemanticColor),
},
},
},
radius: {
name: "Radius",
hideFromPublishing: false,
tokens: scaleTokens.filter(isRadiusToken),
},
spacing: {
name: "Spacing",
hideFromPublishing: false,
tokens: scaleTokens.filter(isSpacingToken),
},
};
fs.writeFileSync(
"./figma-config.json",
JSON.stringify(preparedConfig, null, 2),
);
} catch (error) {
console.error("Error building Figma config:", error);
}
}
@@ -0,0 +1,234 @@
import _config from "../../../figma-config.json";
import { FigmaConfigEntry, FigmaTokenConfig } from "../figma-config.types";
import { FigmaPluginInterface } from "./FigmaPluginInterface";
type ScopedFigmaTokenConfig = Omit<FigmaTokenConfig, "version" | "timestamp">;
export class AkselVariablesInterface {
private Figma: FigmaPluginInterface;
private config: FigmaTokenConfig;
private meta: Pick<FigmaTokenConfig, "version" | "timestamp">;
private remoteConfigURL =
"https://cdn.nav.no/designsystem/@navikt/tokens/figma-config.json";
constructor() {
const config = _config as FigmaTokenConfig;
this.config = config;
this.meta = { timestamp: config.timestamp, version: config.version };
this.Figma = new FigmaPluginInterface();
}
/**
* Because of "dynamic-page" loading in Figma,
* we need to initialize the plugin trough async methods.
* This method should be called before any other methods.
*/
async init(): Promise<void> {
await this.Figma.init();
}
async useRemoteConfig(): Promise<void> {
const newConfig = await fetch(this.remoteConfigURL)
.then((res) => res.json())
.catch((err) => {
console.error(err);
throw new Error("Error fetching config from CDN 😱");
});
this.meta = { timestamp: newConfig.timestamp, version: newConfig.version };
this.config = newConfig;
}
updateVariables(): void {
this.updateGlobalColorCollection(this.config.colors.light.global);
this.updateGlobalColorCollection(this.config.colors.dark.global);
this.updateSemanticColorCollection(this.config.colors);
this.updateScaleCollection(this.config.radius);
this.updateScaleCollection(this.config.spacing);
console.info("Variables updated!");
}
private updateGlobalColorCollection(
globalColorTheme: FigmaConfigEntry,
): void {
const collection =
this.Figma.getCollection(globalColorTheme.name) ??
this.Figma.createCollection(globalColorTheme.name);
/**
* Correctly sorts the token-scale for global colors
* 000 - 100 - 200... etc.
*/
const getLastNumber = (name: string) => {
const matches = name.match(/\d+/g);
return matches ? parseInt(matches[matches.length - 1], 10) : 0;
};
const sortedTokens = globalColorTheme.tokens.sort(
(a, b) => getLastNumber(a.name) - getLastNumber(b.name),
);
for (const token of sortedTokens) {
/* Color values can only be defined by strings */
if (typeof token.value !== "string") {
throw new Error(`Token value is not a string: ${token}`);
}
const variable =
this.Figma.getVariable(token.name, collection.id) ??
this.Figma.createVariable(token.name, collection, token.figmaType);
this.Figma.setVariableValue(
variable,
figma.util.rgba(token.value),
collection.defaultModeId,
);
this.Figma.setVariableMetadata(variable, {
codeSyntax: token.code,
description: token.comment ?? "",
hiddenFromPublishing: collection.hiddenFromPublishing,
scopes: token.scopes,
});
}
console.info("Updated collection:", collection.name);
}
private updateScaleCollection(
globalScale:
| ScopedFigmaTokenConfig["radius"]
| ScopedFigmaTokenConfig["spacing"],
): void {
const collection =
this.Figma.getCollection(globalScale.name) ??
this.Figma.createCollection(globalScale.name);
const sortedTokens = globalScale.tokens.sort((a, b) => {
if (typeof a.value === "number" && typeof b.value === "number") {
return a.value - b.value;
}
return 0;
});
for (const token of sortedTokens) {
const variable =
this.Figma.getVariable(token.name, collection.id) ??
this.Figma.createVariable(token.name, collection, token.figmaType);
this.Figma.setVariableValue(
variable,
token.value,
collection.defaultModeId,
);
this.Figma.setVariableMetadata(variable, {
codeSyntax: token.code,
description: token.comment ?? "",
hiddenFromPublishing: collection.hiddenFromPublishing,
scopes: token.scopes,
});
}
console.info("Updated collection:", collection.name);
}
private updateSemanticColorCollection(
colorsConfig: ScopedFigmaTokenConfig["colors"],
): void {
const semanticCollectionName = colorsConfig.light.semantic.name;
const collection =
this.Figma.getCollection(semanticCollectionName) ??
this.Figma.createCollection(semanticCollectionName);
for (const colorEntry of Object.values(colorsConfig)) {
const modeName = colorEntry.name;
const globalCollection = this.Figma.getCollection(
colorsConfig[modeName].global.name,
);
if (!globalCollection) {
throw new Error(
`Global collection not found for: ${colorsConfig[modeName].global.name}`,
);
}
const mode =
this.Figma.getModeWithName(modeName, collection) ??
this.Figma.createMode(modeName, collection);
for (const token of colorsConfig[modeName].semantic.tokens) {
const variable =
this.Figma.getVariable(token.name, collection.id) ??
this.Figma.createVariable(token.name, collection, token.figmaType);
if (!token.alias) {
if (typeof token.value !== "string") {
throw new Error(
`Semantic tokens without alias requires value to be string: ${token}`,
);
}
this.Figma.setVariableValue(
variable,
figma.util.rgba(token.value),
mode.modeId,
);
continue;
}
const globalVariable = this.Figma.getVariable(
token.alias,
globalCollection.id,
);
if (!globalVariable) {
throw new Error(
`Global variable not found for alias: ${token.alias}`,
);
}
this.Figma.setVariableValue(
variable,
this.Figma.createVariableAlias(globalVariable),
mode.modeId,
);
this.Figma.setVariableMetadata(variable, {
codeSyntax: token.code,
description: token.comment ?? "",
hiddenFromPublishing: collection.hiddenFromPublishing,
scopes: token.scopes,
});
}
/* Make sure to remove "default" modes if they exist */
this.Figma.removeNonMatchingModes(
Object.values(colorsConfig).map((colorConfig) => colorConfig.name),
collection,
);
}
console.info("Updated collection:", collection.name);
}
/**
* Deletes all local collections in the Figma file.
* @important Use with caution! If the current variables are not published in figma,
* they will be lost forever!
*/
resetVariables(): void {
this.Figma.resetVariables();
}
exitWithMessage(message: string): void {
this.Figma.exit(message);
}
exit(): void {
this.Figma.exit(
`Finished updating variables for version ${this.meta.version}, last updated at ${this.meta.timestamp}`,
);
}
}
@@ -0,0 +1,210 @@
/**
* Acts as a wrapper for interfacing with the Figma PluginAPI
* @see https://www.figma.com/plugin-docs/api/api-reference/
*/
export class FigmaPluginInterface {
private collections: VariableCollection[];
private variables: Variable[];
constructor() {
this.collections = [];
this.variables = [];
}
async init() {
console.info("Initializing plugin...");
const collections =
await figma.variables.getLocalVariableCollectionsAsync();
const variables: Variable[] = [];
for (const collection of collections) {
for (const id of collection.variableIds) {
const variable = await figma.variables.getVariableByIdAsync(id);
if (!variable) {
throw new Error(
`Variable with id ${id} not found in collection ${collection.name}`,
);
}
variables.push(variable);
}
}
this.collections = collections;
this.variables = variables;
console.info("Plugin initialized!");
}
/* -------------------------------------------------------------------------- */
/* Collection handling */
/* -------------------------------------------------------------------------- */
getCollection(name: string): VariableCollection | undefined {
return this.collections.find((col) => col.name === name);
}
/**
* Creates a new collection if it doesn't already exist.
* If it does exist, it will return the existing collection.
*/
createCollection(name: string): VariableCollection {
const existingCollection = this.getCollection(name);
if (existingCollection) {
console.info("Collection already exists, skipping creation: ", name);
return existingCollection;
}
const collection = figma.variables.createVariableCollection(name);
this.collections.push(collection);
console.info("Creating new collection: ", name);
return collection;
}
/* -------------------------------------------------------------------------- */
/* Variable handling */
/* -------------------------------------------------------------------------- */
/**
* Returns a variable if it exists in the specified collection.
* Since variables can have the same name in different collections, the collectionId is required.
*/
getVariable(name: string, collectionId: string): Variable | undefined {
return this.variables.find(
(figmaVariable) =>
figmaVariable.name === name &&
figmaVariable.variableCollectionId === collectionId,
);
}
/**
* Creates a new variable if it doesn't already exist.
* If it does exist, it will return the existing variable.
*/
createVariable(
name: string,
collection: VariableCollection,
type: VariableResolvedDataType,
): Variable {
const existingVariable = this.getVariable(name, collection.id);
if (existingVariable) {
console.info("Variable already exists, skipping creation: ", name);
return existingVariable;
}
const variable = figma.variables.createVariable(name, collection, type);
this.variables.push(variable);
return variable;
}
createVariableAlias(variable: Variable) {
return figma.variables.createVariableAlias(variable);
}
setVariableValue(
variable: Variable,
value: VariableValue,
modeId: string,
): void {
variable.setValueForMode(modeId, value);
}
setVariableMetadata(
variable: Variable,
metadata: Pick<
Variable,
"scopes" | "codeSyntax" | "hiddenFromPublishing" | "description"
>,
): void {
const platforms: CodeSyntaxPlatform[] = ["WEB", "ANDROID", "iOS"];
platforms.forEach((platform: CodeSyntaxPlatform) => {
if (metadata.codeSyntax?.[platform]) {
variable.setVariableCodeSyntax(platform, metadata.codeSyntax[platform]);
}
});
variable.scopes = metadata.scopes;
variable.description = metadata.description ?? "";
variable.hiddenFromPublishing = metadata.hiddenFromPublishing;
}
/* -------------------------------------------------------------------------- */
/* Mode handling */
/* -------------------------------------------------------------------------- */
getModeWithName<T extends string>(
name: T,
collection: VariableCollection,
):
| {
modeId: string;
name: T;
}
| undefined {
const foundMode = collection.modes.find((mode) => mode.name === name);
return foundMode ? { modeId: foundMode.modeId, name } : undefined;
}
/**
* Creates a new mode if it doesn't already exist.
* If it does exist, it will return the existing mode.
*/
createMode<T extends string>(
name: T,
collection: VariableCollection,
): {
modeId: string;
name: T;
} {
const existingMode = collection.modes.find((mode) => mode.name === name);
if (existingMode) {
console.info("Mode already exists, skipping creation: ", name);
return { modeId: existingMode.modeId, name };
}
return {
modeId: collection.addMode(name),
name,
};
}
/**
* Removes all modes that are not in the provided list.
* Usefull for removing "default" modes, while preserving the custom modes.
*/
removeNonMatchingModes(
modes: string[],
collection: VariableCollection,
): {
modeId: string;
name: string;
}[] {
const existingModes = collection.modes;
for (const mode of existingModes) {
if (!modes.find((m) => m === mode.name)) {
collection.removeMode(mode.modeId);
}
}
return collection.modes;
}
/* -------------------------------------------------------------------------- */
/* Utilities */
/* -------------------------------------------------------------------------- */
/**
* Resets all variables and collections in the document. Use with caution!
*/
resetVariables() {
this.collections.forEach((collection) => {
console.info("Deleting collection: ", collection.name);
collection.remove();
});
this.collections = [];
this.variables = [];
}
exit(message?: string) {
console.info("Exiting plugin...");
figma.closePlugin(message);
}
}
@@ -0,0 +1,13 @@
{
"name": "Aksel tokens to variables",
"id": "1416051171368451170",
"api": "1.0.0",
"main": "plugin.js",
"capabilities": [],
"enableProposedApi": false,
"editorType": ["figma"],
"documentAccess": "dynamic-page",
"networkAccess": {
"allowedDomains": ["https://cdn.nav.no"]
}
}
@@ -0,0 +1,17 @@
import { AkselVariablesInterface } from "./AkselVariablesInterface";
const plugin = new AkselVariablesInterface();
main().catch((err) =>
plugin.exitWithMessage(`Error updating local variables: ${err.message}`),
);
async function main() {
await plugin.init();
if (process.env.NODE_ENV === "production") {
await plugin.useRemoteConfig();
}
plugin.updateVariables();
plugin.exit();
}
-183
View File
@@ -1,183 +0,0 @@
import {
BackgroundColor,
Color,
ContrastColor,
CssColor,
Theme,
} from "@adobe/leonardo-contrast-colors";
import {
ColorTheme,
GlobalColorRoles,
GlobalColorScale,
globalColorScales,
} from "./util";
const background = new BackgroundColor({
name: "white",
colorKeys: ["#fff"],
ratios: [1],
});
export const globalColorConfig: Record<
GlobalColorRoles,
{ colorKeys: CssColor[]; ratios: number[]; smooth: boolean }
> = {
/* Core */
neutral: {
colorKeys: ["#838c9a"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: false,
},
accent: {
colorKeys: ["#0090ff"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: false,
},
/* Status */
success: {
colorKeys: ["#2aa77d", "#e6ffeb"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: false,
},
info: {
colorKeys: ["#ebfcff", "#66cbec", "#144852"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: false,
},
warning: {
colorKeys: ["#ffeccc", "#ff9100", "#c77300", "#ffcc66"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: true,
},
danger: {
colorKeys: ["#ffb3cd", "#fa4456"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: false,
},
/* Brand */
brandOne: {
colorKeys: ["#99185e", "#7a134b"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: false,
},
brandTwo: {
colorKeys: ["#cc7566", "#fff3ec"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: true,
},
brandThree: {
colorKeys: ["#005b82", "#e6f1f8", "#00243a"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: true,
},
/* Data */
dataOne: {
colorKeys: ["#8269a2"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: false,
},
dataTwo: {
colorKeys: ["#d9e61e"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: false,
},
dataThree: {
colorKeys: ["#ffe45c"],
ratios: [1.08, 1.16, 1.28, 1.5, 3.5, 4.19, 4.89, 5.75, 7.2, 15],
smooth: false,
},
};
// Convert globalColorConfig to Color instances
const colorInstances = Object.entries(globalColorConfig).map(
([role, config]) =>
new Color({
name: role,
colorKeys: config.colorKeys,
ratios: config.ratios,
colorspace: "OKLCH",
smooth: config.smooth,
}),
);
const leonardoLightTheme = new Theme({
colors: colorInstances,
backgroundColor: background,
lightness: 100,
contrast: 1,
saturation: 100,
output: "HEX",
formula: "wcag2",
}).contrastColors;
const leonardoDarkTheme = new Theme({
colors: colorInstances,
backgroundColor: background,
lightness: 0,
contrast: 1,
saturation: 100,
output: "HEX",
formula: "wcag2",
}).contrastColors;
const getColorScaleFromLeonardo = (
role: GlobalColorRoles,
scale: GlobalColorScale,
theme: ColorTheme,
): CssColor => {
/* We have to remove 'ContrastColorBackground' from first position */
const leonardoTheme = (
theme === "light" ? leonardoLightTheme : leonardoDarkTheme
).slice(1) as ContrastColor[];
const colorContrastRole = leonardoTheme.find((c) => c.name === role);
if (!colorContrastRole) {
throw new Error(`Color ${role} not found in leonardo config`);
}
/**
* @example colorContrastRole format
* {
* name: 'dataThree',
* values: [
* { name: 'dataThree100', contrast: 1.08, value: '#200500' },
* { name: 'dataThree200', contrast: 1.16, value: '#2b0f00' },
* ...
* ]
*}
*/
const color = colorContrastRole.values.find(
(value) => value.name === `${role}${scale}`,
);
if (!color || !color.value) {
throw new Error(`Color ${role}.${scale} not found in leonardo config`);
}
return color.value;
};
type GlobalColor = {
value: string;
type: "global-color";
group: GlobalColorRoles;
};
export const getGlobalScaleForColor = (
role: GlobalColorRoles,
theme: ColorTheme,
): Record<GlobalColorScale, GlobalColor> => {
return globalColorScales.reduce(
(acc, scale) => {
acc[scale] = {
value: getColorScaleFromLeonardo(role, scale, theme),
type: "global-color",
group: role,
};
return acc;
},
{} as Record<GlobalColorScale, GlobalColor>,
);
};
@@ -0,0 +1,89 @@
import { TransformedToken } from "style-dictionary/types";
import { describe, expect, test } from "vitest";
import { createTokenName, figmaValue } from "../figma/create-tokens";
const baseToken: TransformedToken = {
name: "tokenName",
attributes: { item: "primary" },
path: ["color", "background"],
original: { value: "#000000" },
filePath: "path/to/file.css",
isSource: true,
};
describe("createTokenName", () => {
test("should extract name from token with item attribute", () => {
const token: TransformedToken = baseToken;
expect(createTokenName({ ...token, attributes: { item: "primary" } })).toBe(
"Primary",
);
expect(
createTokenName({ ...token, attributes: { item: "bg-strong-hover" } }),
).toBe("Bg Strong Hover");
});
test("should create correct grouping for token with group attribute", () => {
const token: TransformedToken = {
...baseToken,
attributes: { item: "primary" },
group: "color.background",
};
expect(createTokenName(token)).toBe("Color/Background/Primary");
});
test("should add 'Default' suffix for tokens with duplicate names", () => {
const token: TransformedToken = {
...baseToken,
attributes: { item: "accent" },
group: "text.accent",
};
expect(createTokenName(token)).toBe("Text/Accent/Accent Default");
});
test("should add 'Radius' prefix for global-radius type tokens", () => {
const token: TransformedToken = {
...baseToken,
attributes: { item: "small" },
type: "global-radius",
};
expect(createTokenName(token)).toBe("Radius Small");
});
test("should add 'Spacing' prefix for global-spacing type tokens", () => {
const token: TransformedToken = {
...baseToken,
attributes: { item: "4" },
type: "global-spacing",
};
expect(createTokenName(token)).toBe("Spacing 4");
});
});
describe("extracting figma value for token", () => {
test("should convert rem to px for global-radius type tokens", () => {
const token: TransformedToken = {
...baseToken,
value: "0.25rem",
type: "global-radius",
};
expect(figmaValue(token)).toBe(4);
});
test("should convert px to number for global-spacing type tokens", () => {
const token: TransformedToken = {
...baseToken,
value: "32px",
type: "global-spacing",
};
expect(figmaValue(token)).toBe(32);
});
test("should return the original value for non-radius and non-spacing type tokens", () => {
const token: TransformedToken = {
...baseToken,
type: "color",
value: "#111",
};
expect(figmaValue(token)).toBe("#111");
});
});
@@ -1,11 +0,0 @@
import { describe, expect, test } from "vitest";
import { globalColorConfig } from "../leonardo";
describe("leonardo token setup", () => {
Object.entries(globalColorConfig).forEach(([role, { colorKeys, ratios }]) => {
test(`${role} should have correct colorKeys and ratios for each role`, () => {
expect(colorKeys.length).toBeGreaterThan(0);
expect(ratios.length).toEqual(10);
});
});
});
@@ -1,19 +1,25 @@
import { describe, expect, test } from "vitest";
import { globalScale } from "../create-configuration";
import { contrastTokenConfig } from "../tokens/contrast";
import { neutralTokenConfig } from "../tokens/neutral";
import { globalColorDarkModeConfig } from "../tokens/global-dark";
import { globalColorLightModeConfig } from "../tokens/global-light";
import { radiusTokenConfig } from "../tokens/radius";
import { semanticTokenConfig } from "../tokens/semantic";
import { semanticTokensForAllRolesConfig } from "../tokens/semantic-roles";
import { spacingTokenConfig } from "../tokens/spacing";
import { textContrastTokenConfig } from "../tokens/text-contrast";
const configKeysWithGroup = ["value", "type", "group"];
const configKeys = ["value", "type"];
describe("Validate token configurations", () => {
test(`Semantic tokens`, () => {
test(`Semantic tokens lightmode`, () => {
expect(
validateConfig(semanticTokenConfig(), configKeysWithGroup),
validateConfig(semanticTokenConfig("light"), configKeysWithGroup),
).toBeTruthy();
});
test(`Semantic tokens darkmode`, () => {
expect(
validateConfig(semanticTokenConfig("dark"), configKeysWithGroup),
).toBeTruthy();
});
@@ -23,33 +29,21 @@ describe("Validate token configurations", () => {
).toBeTruthy();
});
test(`Contrast tokens`, () => {
test(`Text-contrast tokens`, () => {
expect(
validateConfig(contrastTokenConfig(), configKeysWithGroup),
validateConfig(textContrastTokenConfig(), configKeysWithGroup),
).toBeTruthy();
});
test(`Global lightmode scale`, () => {
expect(
validateConfig(globalScale("light"), configKeysWithGroup),
validateConfig(globalColorLightModeConfig, configKeysWithGroup),
).toBeTruthy();
});
test(`Global darkmode scale`, () => {
expect(
validateConfig(globalScale("dark"), configKeysWithGroup),
).toBeTruthy();
});
test(`Neutral light unique tokens`, () => {
expect(
validateConfig(neutralTokenConfig("light"), configKeysWithGroup),
).toBeTruthy();
});
test(`Neutral dark unique tokens`, () => {
expect(
validateConfig(neutralTokenConfig("dark"), configKeysWithGroup),
validateConfig(globalColorDarkModeConfig, configKeysWithGroup),
).toBeTruthy();
});
@@ -67,7 +61,11 @@ function validateConfig(
requiredKeys: string[],
): boolean {
for (const key in obj) {
if (typeof obj[key] === "object" && obj[key] !== null) {
if (
typeof obj[key] === "object" &&
obj[key] !== null &&
!Array.isArray(obj[key])
) {
if (!validateConfig(obj[key], requiredKeys)) {
return false;
}
@@ -1,74 +0,0 @@
import { GlobalColorRoles, StyleDictionaryToken } from "../util";
export const contrastTokenConfig = (): {
contrast: {
[key in GlobalColorRoles]: StyleDictionaryToken<"color">;
};
} => ({
contrast: {
/* core */
accent: {
value: "white",
type: "color",
group: "contrast",
},
neutral: {
value: "white",
type: "color",
group: "contrast",
},
/* Status */
danger: {
value: "white",
type: "color",
group: "contrast",
},
info: {
value: "white",
type: "color",
group: "contrast",
},
success: {
value: "white",
type: "color",
group: "contrast",
},
warning: {
value: "white",
type: "color",
group: "contrast",
},
/* Brand */
brandOne: {
value: "white",
type: "color",
group: "contrast",
},
brandTwo: {
value: "white",
type: "color",
group: "contrast",
},
brandThree: {
value: "white",
type: "color",
group: "contrast",
},
/* Data */
dataOne: {
value: "white",
type: "color",
group: "contrast",
},
dataThree: {
value: "white",
type: "color",
group: "contrast",
},
dataTwo: {
value: "white",
type: "color",
group: "contrast",
},
},
});
@@ -0,0 +1,142 @@
import { GlobalConfigT } from "../util";
export const globalColorDarkModeConfig: GlobalConfigT = {
neutral: {
"000": {
value: "black",
type: "global-color",
group: "neutral",
},
"100": { value: "#161d28", type: "global-color", group: "neutral" },
"200": { value: "#1c232f", type: "global-color", group: "neutral" },
"300": { value: "#242b37", type: "global-color", group: "neutral" },
"400": { value: "#2e3641", type: "global-color", group: "neutral" },
"500": { value: "#656d7b", type: "global-color", group: "neutral" },
"600": { value: "#767e8c", type: "global-color", group: "neutral" },
"700": { value: "#8a92a0", type: "global-color", group: "neutral" },
"800": { value: "#949ba8", type: "global-color", group: "neutral" },
"900": { value: "#a5acb6", type: "global-color", group: "neutral" },
"1000": { value: "#c4c9d0", type: "global-color", group: "neutral" },
},
accent: {
"100": { value: "#101d2d", type: "global-color", group: "accent" },
"200": { value: "#152436", type: "global-color", group: "accent" },
"300": { value: "#1a2c41", type: "global-color", group: "accent" },
"400": { value: "#23374f", type: "global-color", group: "accent" },
"500": { value: "#2e6db8", type: "global-color", group: "accent" },
"600": { value: "#447fc9", type: "global-color", group: "accent" },
"700": { value: "#5f94d8", type: "global-color", group: "accent" },
"800": { value: "#6d9ddd", type: "global-color", group: "accent" },
"900": { value: "#84aee6", type: "global-color", group: "accent" },
"1000": { value: "#b0caf1", type: "global-color", group: "accent" },
},
success: {
"100": { value: "#00230b", type: "global-color", group: "success" },
"200": { value: "#032911", type: "global-color", group: "success" },
"300": { value: "#0b3118", type: "global-color", group: "success" },
"400": { value: "#183d24", type: "global-color", group: "success" },
"500": { value: "#207c41", type: "global-color", group: "success" },
"600": { value: "#368e52", type: "global-color", group: "success" },
"700": { value: "#4ca265", type: "global-color", group: "success" },
"800": { value: "#5eab72", type: "global-color", group: "success" },
"900": { value: "#78ba87", type: "global-color", group: "success" },
"1000": { value: "#a4d4ad", type: "global-color", group: "success" },
},
warning: {
"100": { value: "#371100", type: "global-color", group: "warning" },
"200": { value: "#3e1704", type: "global-color", group: "warning" },
"300": { value: "#471f0c", type: "global-color", group: "warning" },
"400": { value: "#542b18", type: "global-color", group: "warning" },
"500": { value: "#af5122", type: "global-color", group: "warning" },
"600": { value: "#c46233", type: "global-color", group: "warning" },
"700": { value: "#de753c", type: "global-color", group: "warning" },
"800": { value: "#e78040", type: "global-color", group: "warning" },
"900": { value: "#ed974b", type: "global-color", group: "warning" },
"1000": { value: "#f5bf6d", type: "global-color", group: "warning" },
},
danger: {
"100": { value: "#410409", type: "global-color", group: "danger" },
"200": { value: "#490b10", type: "global-color", group: "danger" },
"300": { value: "#521517", type: "global-color", group: "danger" },
"400": { value: "#5f2021", type: "global-color", group: "danger" },
"500": { value: "#c8343f", type: "global-color", group: "danger" },
"600": { value: "#dd4a50", type: "global-color", group: "danger" },
"700": { value: "#eb6772", type: "global-color", group: "danger" },
"800": { value: "#ee7685", type: "global-color", group: "danger" },
"900": { value: "#f28ea2", type: "global-color", group: "danger" },
"1000": { value: "#f6b7cd", type: "global-color", group: "danger" },
},
/* Info is currently just a copy of brandThree */
info: {
"100": { value: "#131e26", type: "global-color", group: "brandThree" },
"200": { value: "#18242c", type: "global-color", group: "brandThree" },
"300": { value: "#1e2c35", type: "global-color", group: "brandThree" },
"400": { value: "#273843", type: "global-color", group: "brandThree" },
"500": { value: "#43718d", type: "global-color", group: "brandThree" },
"600": { value: "#5a839b", type: "global-color", group: "brandThree" },
"700": { value: "#7396ab", type: "global-color", group: "brandThree" },
"800": { value: "#7f9fb3", type: "global-color", group: "brandThree" },
"900": { value: "#93afc0", type: "global-color", group: "brandThree" },
"1000": { value: "#b7cbd7", type: "global-color", group: "brandThree" },
},
brandOne: {
"100": { value: "#321220", type: "global-color", group: "brandOne" },
"200": { value: "#3a1827", type: "global-color", group: "brandOne" },
"300": { value: "#42202e", type: "global-color", group: "brandOne" },
"400": { value: "#4e2a39", type: "global-color", group: "brandOne" },
"500": { value: "#a35276", type: "global-color", group: "brandOne" },
"600": { value: "#b06886", type: "global-color", group: "brandOne" },
"700": { value: "#be7f99", type: "global-color", group: "brandOne" },
"800": { value: "#c58aa2", type: "global-color", group: "brandOne" },
"900": { value: "#cf9eb1", type: "global-color", group: "brandOne" },
"1000": { value: "#e1c0cc", type: "global-color", group: "brandOne" },
},
brandTwo: {
"100": { value: "#2d160e", type: "global-color", group: "brandTwo" },
"200": { value: "#351d15", type: "global-color", group: "brandTwo" },
"300": { value: "#3d241c", type: "global-color", group: "brandTwo" },
"400": { value: "#493027", type: "global-color", group: "brandTwo" },
"500": { value: "#975e4a", type: "global-color", group: "brandTwo" },
"600": { value: "#ab6f5b", type: "global-color", group: "brandTwo" },
"700": { value: "#c0836e", type: "global-color", group: "brandTwo" },
"800": { value: "#c78e79", type: "global-color", group: "brandTwo" },
"900": { value: "#d1a08c", type: "global-color", group: "brandTwo" },
"1000": { value: "#e3c2b1", type: "global-color", group: "brandTwo" },
},
brandThree: {
"100": { value: "#131e26", type: "global-color", group: "brandThree" },
"200": { value: "#18242c", type: "global-color", group: "brandThree" },
"300": { value: "#1e2c35", type: "global-color", group: "brandThree" },
"400": { value: "#273843", type: "global-color", group: "brandThree" },
"500": { value: "#43718d", type: "global-color", group: "brandThree" },
"600": { value: "#5a839b", type: "global-color", group: "brandThree" },
"700": { value: "#7396ab", type: "global-color", group: "brandThree" },
"800": { value: "#7f9fb3", type: "global-color", group: "brandThree" },
"900": { value: "#93afc0", type: "global-color", group: "brandThree" },
"1000": { value: "#b7cbd7", type: "global-color", group: "brandThree" },
},
dataOne: {
"100": { value: "#221a2c", type: "global-color", group: "dataOne" },
"200": { value: "#272032", type: "global-color", group: "dataOne" },
"300": { value: "#2f273a", type: "global-color", group: "dataOne" },
"400": { value: "#3a3245", type: "global-color", group: "dataOne" },
"500": { value: "#776491", type: "global-color", group: "dataOne" },
"600": { value: "#8876a1", type: "global-color", group: "dataOne" },
"700": { value: "#9b8bb0", type: "global-color", group: "dataOne" },
"800": { value: "#a495b7", type: "global-color", group: "dataOne" },
"900": { value: "#b2a7c3", type: "global-color", group: "dataOne" },
"1000": { value: "#cdc5d9", type: "global-color", group: "dataOne" },
},
dataTwo: {
"100": { value: "#1c1f00", type: "global-color", group: "dataTwo" },
"200": { value: "#222500", type: "global-color", group: "dataTwo" },
"300": { value: "#292d00", type: "global-color", group: "dataTwo" },
"400": { value: "#343900", type: "global-color", group: "dataTwo" },
"500": { value: "#6a7200", type: "global-color", group: "dataTwo" },
"600": { value: "#7c8400", type: "global-color", group: "dataTwo" },
"700": { value: "#8f9800", type: "global-color", group: "dataTwo" },
"800": { value: "#99a200", type: "global-color", group: "dataTwo" },
"900": { value: "#a9b325", type: "global-color", group: "dataTwo" },
"1000": { value: "#c5d14c", type: "global-color", group: "dataTwo" },
},
};
@@ -0,0 +1,141 @@
import { GlobalConfigT } from "../util";
export const globalColorLightModeConfig: GlobalConfigT = {
neutral: {
"000": {
value: "white",
type: "global-color",
group: "neutral",
},
"100": { value: "#f5f6f7", type: "global-color", group: "neutral" },
"200": { value: "#ecedef", type: "global-color", group: "neutral" },
"300": { value: "#e1e3e7", type: "global-color", group: "neutral" },
"400": { value: "#cfd3d8", type: "global-color", group: "neutral" },
"500": { value: "#818997", type: "global-color", group: "neutral" },
"600": { value: "#6f7785", type: "global-color", group: "neutral" },
"700": { value: "#5d6573", type: "global-color", group: "neutral" },
"800": { value: "#555d6a", type: "global-color", group: "neutral" },
"900": { value: "#49515e", type: "global-color", group: "neutral" },
"1000": { value: "#202733", type: "global-color", group: "neutral" },
},
accent: {
"100": { value: "#f1f7ff", type: "global-color", group: "accent" },
"200": { value: "#e4eeff", type: "global-color", group: "accent" },
"300": { value: "#d4e5fd", type: "global-color", group: "accent" },
"400": { value: "#bad5fb", type: "global-color", group: "accent" },
"500": { value: "#428ae3", type: "global-color", group: "accent" },
"600": { value: "#2277d5", type: "global-color", group: "accent" },
"700": { value: "#0063c1", type: "global-color", group: "accent" },
"800": { value: "#005bb6", type: "global-color", group: "accent" },
"900": { value: "#004ea3", type: "global-color", group: "accent" },
"1000": { value: "#002459", type: "global-color", group: "accent" },
},
success: {
"100": { value: "#e2fde8", type: "global-color", group: "success" },
"200": { value: "#d5f6db", type: "global-color", group: "success" },
"300": { value: "#c4edcd", type: "global-color", group: "success" },
"400": { value: "#a8dfb4", type: "global-color", group: "success" },
"500": { value: "#199d4f", type: "global-color", group: "success" },
"600": { value: "#00893c", type: "global-color", group: "success" },
"700": { value: "#007629", type: "global-color", group: "success" },
"800": { value: "#006c1f", type: "global-color", group: "success" },
"900": { value: "#005e0f", type: "global-color", group: "success" },
"1000": { value: "#002e00", type: "global-color", group: "success" },
},
warning: {
"100": { value: "#fff5e4", type: "global-color", group: "warning" },
"200": { value: "#ffebc7", type: "global-color", group: "warning" },
"300": { value: "#ffdea5", type: "global-color", group: "warning" },
"400": { value: "#ffcb6f", type: "global-color", group: "warning" },
"500": { value: "#e75e01", type: "global-color", group: "warning" },
"600": { value: "#c95100", type: "global-color", group: "warning" },
"700": { value: "#ac4400", type: "global-color", group: "warning" },
"800": { value: "#a03e00", type: "global-color", group: "warning" },
"900": { value: "#8c3500", type: "global-color", group: "warning" },
"1000": { value: "#481700", type: "global-color", group: "warning" },
},
danger: {
"100": { value: "#fff3f7", type: "global-color", group: "danger" },
"200": { value: "#ffe9f0", type: "global-color", group: "danger" },
"300": { value: "#ffdae6", type: "global-color", group: "danger" },
"400": { value: "#ffc2d7", type: "global-color", group: "danger" },
"500": { value: "#fa4256", type: "global-color", group: "danger" },
"600": { value: "#e81c3b", type: "global-color", group: "danger" },
"700": { value: "#cc0023", type: "global-color", group: "danger" },
"800": { value: "#bd0015", type: "global-color", group: "danger" },
"900": { value: "#a60000", type: "global-color", group: "danger" },
"1000": { value: "#560000", type: "global-color", group: "danger" },
},
info: {
"100": { value: "#eef6fc", type: "global-color", group: "info" },
"200": { value: "#e3eff7", type: "global-color", group: "info" },
"300": { value: "#d7e6f0", type: "global-color", group: "info" },
"400": { value: "#c0d6e4", type: "global-color", group: "info" },
"500": { value: "#5a8fae", type: "global-color", group: "info" },
"600": { value: "#417da0", type: "global-color", group: "info" },
"700": { value: "#246b91", type: "global-color", group: "info" },
"800": { value: "#156389", type: "global-color", group: "info" },
"900": { value: "#00557d", type: "global-color", group: "info" },
"1000": { value: "#002942", type: "global-color", group: "info" },
},
brandOne: {
"100": { value: "#fbf3f6", type: "global-color", group: "brandOne" },
"200": { value: "#f8eaef", type: "global-color", group: "brandOne" },
"300": { value: "#f3dde5", type: "global-color", group: "brandOne" },
"400": { value: "#eccad6", type: "global-color", group: "brandOne" },
"500": { value: "#c26f92", type: "global-color", group: "brandOne" },
"600": { value: "#b65781", type: "global-color", group: "brandOne" },
"700": { value: "#a93d70", type: "global-color", group: "brandOne" },
"800": { value: "#a33069", type: "global-color", group: "brandOne" },
"900": { value: "#98185d", type: "global-color", group: "brandOne" },
"1000": { value: "#52002b", type: "global-color", group: "brandOne" },
},
brandTwo: {
"100": { value: "#fff4ee", type: "global-color", group: "brandTwo" },
"200": { value: "#fdebe0", type: "global-color", group: "brandTwo" },
"300": { value: "#fcddcd", type: "global-color", group: "brandTwo" },
"400": { value: "#f8c8b1", type: "global-color", group: "brandTwo" },
"500": { value: "#c0765d", type: "global-color", group: "brandTwo" },
"600": { value: "#a9654e", type: "global-color", group: "brandTwo" },
"700": { value: "#915541", type: "global-color", group: "brandTwo" },
"800": { value: "#874e3b", type: "global-color", group: "brandTwo" },
"900": { value: "#764332", type: "global-color", group: "brandTwo" },
"1000": { value: "#3c1f15", type: "global-color", group: "brandTwo" },
},
brandThree: {
"100": { value: "#eef6fc", type: "global-color", group: "brandThree" },
"200": { value: "#e3eff7", type: "global-color", group: "brandThree" },
"300": { value: "#d7e6f0", type: "global-color", group: "brandThree" },
"400": { value: "#c0d6e4", type: "global-color", group: "brandThree" },
"500": { value: "#5a8fae", type: "global-color", group: "brandThree" },
"600": { value: "#417da0", type: "global-color", group: "brandThree" },
"700": { value: "#246b91", type: "global-color", group: "brandThree" },
"800": { value: "#156389", type: "global-color", group: "brandThree" },
"900": { value: "#00557d", type: "global-color", group: "brandThree" },
"1000": { value: "#002942", type: "global-color", group: "brandThree" },
},
dataOne: {
"100": { value: "#f7f5f9", type: "global-color", group: "dataOne" },
"200": { value: "#f0edf4", type: "global-color", group: "dataOne" },
"300": { value: "#e6e1ed", type: "global-color", group: "dataOne" },
"400": { value: "#d7cfe3", type: "global-color", group: "dataOne" },
"500": { value: "#9580b1", type: "global-color", group: "dataOne" },
"600": { value: "#846ca4", type: "global-color", group: "dataOne" },
"700": { value: "#725a91", type: "global-color", group: "dataOne" },
"800": { value: "#6a5289", type: "global-color", group: "dataOne" },
"900": { value: "#5e467c", type: "global-color", group: "dataOne" },
"1000": { value: "#331a4d", type: "global-color", group: "dataOne" },
},
dataTwo: {
"100": { value: "#f4f9d1", type: "global-color", group: "dataTwo" },
"200": { value: "#ebf4a9", type: "global-color", group: "dataTwo" },
"300": { value: "#e0ec6b", type: "global-color", group: "dataTwo" },
"400": { value: "#d0dc00", type: "global-color", group: "dataTwo" },
"500": { value: "#878f00", type: "global-color", group: "dataTwo" },
"600": { value: "#757c00", type: "global-color", group: "dataTwo" },
"700": { value: "#646900", type: "global-color", group: "dataTwo" },
"800": { value: "#5c6100", type: "global-color", group: "dataTwo" },
"900": { value: "#515400", type: "global-color", group: "dataTwo" },
"1000": { value: "#2a2800", type: "global-color", group: "dataTwo" },
},
};
@@ -1,23 +0,0 @@
import { ColorTheme, StyleDictionaryTokenConfig } from "../util";
const NeutralScaleLight = (): StyleDictionaryTokenConfig<"global-color"> => ({
"000": {
value: "white",
type: "global-color",
group: "neutral",
},
});
const NeutralScaleDark = (): StyleDictionaryTokenConfig<"global-color"> => ({
"000": {
value: "black",
type: "global-color",
group: "neutral",
},
});
export const neutralTokenConfig = (theme: ColorTheme) => {
return {
neutral: theme === "light" ? NeutralScaleLight() : NeutralScaleDark(),
};
};
@@ -34,7 +34,7 @@ const configForRole = (
type: "color",
group: `background.${role}`,
},
[`${role}-moderate-active`]: {
[`${role}-moderate-pressed`]: {
value: `{a.${role}.400.value}`,
type: "color",
group: `background.${role}`,
@@ -46,17 +46,17 @@ const configForRole = (
group: `background.${role}`,
},
[`${role}-strong`]: {
value: `{a.${role}.700.value}`,
value: `{a.${role}.600.value}`,
type: "color",
group: `background.${role}`,
},
[`${role}-strong-hover`]: {
value: `{a.${role}.800.value}`,
value: `{a.${role}.700.value}`,
type: "color",
group: `background.${role}`,
},
[`${role}-strong-active`]: {
value: `{a.${role}.900.value}`,
[`${role}-strong-pressed`]: {
value: `{a.${role}.800.value}`,
type: "color",
group: `background.${role}`,
},
@@ -73,11 +73,11 @@ const configForRole = (
},
text: {
[role]: {
value: `{a.${role}.1000.value}`,
value: `{a.${role}.700.value}`,
type: "color",
group: `text.${role}`,
},
[`${role}-subtle`]: {
[`${role}-strong`]: {
value: `{a.${role}.900.value}`,
type: "color",
group: `text.${role}`,
+114 -71
View File
@@ -1,76 +1,119 @@
import { StyleDictionaryTokenConfig } from "../util";
import { ColorTheme, StyleDictionaryTokenConfig } from "../util";
export const semanticTokenConfig = (): StyleDictionaryTokenConfig<"color"> => ({
text: {
default: {
value: "{a.neutral.1000.value}",
type: "color",
group: "text",
export function semanticTokenConfig(theme: ColorTheme) {
return theme === "light" ? semanticTokensLight() : semanticTokensDark();
}
function semanticTokensDark(): StyleDictionaryTokenConfig<"color"> {
const baseline = semanticTokensLight();
return {
...baseline,
bg: {
...baseline.bg,
default: {
value: "#0E151F",
type: "color",
group: "background",
/**
* Allows token to be used on 'effect' properties in Figma,
* Bg-default is used between element and focus-marking.
*/
scopes: ["EFFECT_COLOR"],
},
raised: {
value: "{a.neutral.200.value}",
type: "color",
group: "background",
},
sunken: {
value: "#07090D",
type: "color",
group: "background",
},
},
subtle: {
value: "{a.neutral.900.value}",
type: "color",
group: "text",
};
}
function semanticTokensLight(): StyleDictionaryTokenConfig<"color"> {
return {
text: {
default: {
value: "{a.neutral.1000.value}",
type: "color",
group: "text",
},
subtle: {
value: "{a.neutral.900.value}",
type: "color",
group: "text",
},
icon: {
value: "{a.neutral.600.value}",
type: "color",
group: "text",
},
},
icon: {
value: "{a.neutral.600.value}",
type: "color",
group: "text",
bg: {
default: {
value: "{a.neutral.000.value}",
type: "color",
group: "background",
/**
* Allows token to be used on 'effect' properties in Figma,
* Bg-default is used between element and focus-marking.
*/
scopes: ["EFFECT_COLOR"],
},
input: {
value: "{a.neutral.000.value}",
type: "color",
group: "background",
},
raised: {
value: "{a.neutral.000.value}",
type: "color",
group: "background",
},
sunken: {
value: "{a.neutral.200.value}",
type: "color",
group: "background",
},
overlay: {
value: "rgba(2, 20, 49 , 0.49)",
type: "color",
group: "background",
},
},
},
bg: {
default: {
value: "{a.neutral.000.value}",
type: "color",
group: "background",
border: {
default: {
value: "{a.neutral.500.value}",
type: "color",
group: "border",
},
subtle: {
value: "{a.neutral.400.value}",
type: "color",
group: "border",
},
strong: {
value: "{a.neutral.600.value}",
type: "color",
group: "border",
},
/**
* TODO: Need to verify this value
* - Discuss with brand
*/
focus: {
value: "{a.accent.700.value}",
type: "color",
group: "border",
/**
* Allows token to be used on 'effect' properties in Figma, used for creating focus markings.
*/
scopes: ["EFFECT_COLOR"],
},
},
input: {
value: "{a.neutral.000.value}",
type: "color",
group: "background",
},
raised: {
value: "{a.neutral.000.value}",
type: "color",
group: "background",
},
sunken: {
value: "{a.neutral.200.value}",
type: "color",
group: "background",
},
overlay: {
/* TODO: Update alpha here, consier not having the token itself? */
value: "rgb(2 20 49 / 0.49)",
type: "color",
group: "background",
},
},
border: {
default: {
value: "{a.neutral.500.value}",
type: "color",
group: "border",
},
subtle: {
value: "{a.neutral.400.value}",
type: "color",
group: "border",
},
strong: {
/* TODO: Consider 700 scale here? */
value: "{a.neutral.600.value}",
type: "color",
group: "border",
},
/**
* TODO: Need to verify this value
* - Discuss with brand
*/
focus: {
value: "{a.accent.700.value}",
type: "color",
group: "border",
},
},
});
};
}
@@ -0,0 +1,69 @@
import { GlobalColorRoles, StyleDictionaryToken } from "../util";
export const textContrastTokenConfig = (): {
text: {
[key in `${GlobalColorRoles}-contrast`]: StyleDictionaryToken<"color">;
};
} => ({
text: {
/* core */
"accent-contrast": {
value: "{a.neutral.000.value}",
type: "color",
group: "text",
},
"neutral-contrast": {
value: "{a.neutral.000.value}",
type: "color",
group: "text",
},
/* Status */
"danger-contrast": {
value: "{a.neutral.000.value}",
type: "color",
group: "text",
},
"info-contrast": {
value: "{a.neutral.000.value}",
type: "color",
group: "text",
},
"success-contrast": {
value: "{a.neutral.000.value}",
type: "color",
group: "text",
},
"warning-contrast": {
value: "{a.neutral.000.value}",
type: "color",
group: "text",
},
/* Brand */
"brandOne-contrast": {
value: "{a.neutral.000.value}",
type: "color",
group: "text",
},
"brandTwo-contrast": {
value: "{a.neutral.000.value}",
type: "color",
group: "text",
},
"brandThree-contrast": {
value: "{a.neutral.000.value}",
type: "color",
group: "text",
},
/* Data */
"dataOne-contrast": {
value: "{a.neutral.000.value}",
type: "color",
group: "text",
},
"dataTwo-contrast": {
value: "{a.neutral.000.value}",
type: "color",
group: "text",
},
},
});
+45 -4
View File
@@ -1,5 +1,16 @@
import _ from "lodash";
type GlobalColorEntryT = {
value: string;
type: "global-color";
group: GlobalColorRoles;
};
export type GlobalConfigT = Record<
GlobalColorRoles,
Record<GlobalColorScale, GlobalColorEntryT> & { "000"?: GlobalColorEntryT }
>;
export const colorThemes = ["light", "dark"] as const;
export type ColorTheme = (typeof colorThemes)[number];
@@ -15,7 +26,6 @@ export const globalColorRoles = [
"brandThree",
"dataOne",
"dataTwo",
"dataThree",
] as const;
export type GlobalColorRoles = (typeof globalColorRoles)[number];
@@ -35,17 +45,48 @@ export const globalColorScales = [
export type GlobalColorScale = (typeof globalColorScales)[number];
type TokenTypes = "color" | "global-color" | "global-radius" | "global-spacing";
export type TokenTypes =
| "color"
| "global-color"
| "global-radius"
| "global-spacing";
export type SemanticTokenGroups = "background" | "border" | "text";
export type TokenGroup =
| GlobalColorRoles
| SemanticTokenGroups
| `${SemanticTokenGroups}.${GlobalColorRoles}`;
export type StyleDictionaryToken<T extends TokenTypes> = {
/**
* Token value
* @example "#000000"
* @example "1px"
* @example "{a.neutral.100.value}"
*/
value: string;
/**
* Token type
*/
type: T;
group?: string;
/**
* Group the token belongs to. Used for auto-documentation and categorization in Figma.
*/
group?: TokenGroup;
/**
* Optional comment for the token. Will be included in the generated documentation and in Figma.
*/
comment?: string;
/**
* Optional extra scopes for the token.
* Token will include all default scopes based on `type`, and any extra specified here.
*/
scopes?: VariableScope[];
};
export type StyleDictionaryTokenConfig<T extends TokenTypes> = {
[key: string]: StyleDictionaryToken<T> | StyleDictionaryTokenConfig<T>;
[key: string]: Record<string, StyleDictionaryToken<T>>;
};
export type GlobalColorVariable = Record<
+7 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "7.0.1",
"version": "7.2.0",
"description": "Design-tokens for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -19,7 +19,10 @@
],
"scripts": {
"build": "tsx ./config/build.ts > /dev/null && tsx ./config/version-tag.ts && yarn build:darkside",
"build:darkside": "tsx ./darkside",
"build:darkside": "tsx ./darkside && yarn build:figma-config && yarn build:plugin",
"build:figma-config": "tsx ./darkside/figma",
"build:plugin-dev": "esbuild darkside/figma/plugin/plugin.ts --target=es2016 --bundle --define:process.env.NODE_ENV=\\\"development\\\" --outfile='darkside/figma/plugin/plugin.js'",
"build:plugin": "esbuild darkside/figma/plugin/plugin.ts --target=es2016 --bundle --define:process.env.NODE_ENV=\\\"production\\\" --outfile='darkside/figma/plugin/plugin.js'",
"test": "vitest run",
"test:watch": "vitest watch"
},
@@ -29,8 +32,9 @@
"directory": "@navikt/core/tokens"
},
"devDependencies": {
"@adobe/leonardo-contrast-colors": "^1.0.0",
"@figma/plugin-typings": "^1.100.2",
"color": "4.2.3",
"esbuild": "^0.24.0",
"lightningcss": "^1.27.0",
"lodash": "^4.17.21",
"style-dictionary": "^4.1.1",
+5 -2
View File
@@ -3,7 +3,10 @@
"jsx": "react",
"esModuleInterop": true,
"resolveJsonModule": true,
"strict": true
"strict": true,
"types": ["@figma/plugin-typings"],
"moduleResolution": "Bundler",
"module": "Preserve"
},
"include": ["./stories"]
"include": ["./stories", "./darkside/figma"]
}
Loaded 100 of 230 files, more files were not shown because too many files have changed in this diff. Show more